Change back to codecov (#2363)

This commit is contained in:
Adam Hopkins 2022-01-09 12:22:09 +02:00 committed by GitHub
parent 101151b419
commit 8b0eaa097c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 311 additions and 69 deletions

View File

@ -1,28 +0,0 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "scripts/"
- "tests/"
checks:
argument-count:
enabled: false
file-lines:
config:
threshold: 1000
method-count:
config:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10

View File

@ -3,13 +3,12 @@ branch = True
source = sanic source = sanic
omit = omit =
site-packages site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py sanic/__main__.py
sanic/compat.py
sanic/reloader_helpers.py sanic/reloader_helpers.py
sanic/simple.py sanic/simple.py
sanic/utils.py sanic/utils.py
sanic/cli
[html] [html]
directory = coverage directory = coverage
@ -21,3 +20,12 @@ exclude_lines =
noqa noqa
NOQA NOQA
pragma: no cover pragma: no cover
omit =
site-packages
sanic/__main__.py
sanic/compat.py
sanic/reloader_helpers.py
sanic/simple.py
sanic/utils.py
sanic/cli
skip_empty = True

View File

@ -20,7 +20,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
@ -29,9 +28,9 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install tox pip install tox
- uses: paambaati/codeclimate-action@v2.5.3 - name: Run coverage
if: always() run: tox -e coverage
env: - uses: codecov/codecov-action@v2
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
with: with:
coverageCommand: tox -e coverage files: ./coverage.xml
fail_ci_if_error: false

27
codecov.yml Normal file
View File

@ -0,0 +1,27 @@
coverage:
status:
patch:
default:
target: auto
threshold: 0.75
project:
default:
target: auto
threshold: 0.5
precision: 3
codecov:
require_ci_to_pass: false
ignore:
- "sanic/__main__.py"
- "sanic/compat.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
- "sanic/cli"
- ".github/"
- "changelogs/"
- "docker/"
- "docs/"
- "examples/"
- "scripts/"
- "tests/"

View File

@ -114,7 +114,7 @@ if TYPE_CHECKING: # no cov
Extend = TypeVar("Extend") # type: ignore Extend = TypeVar("Extend") # type: ignore
if OS_IS_WINDOWS: if OS_IS_WINDOWS: # no cov
enable_windows_color_support() enable_windows_color_support()
filterwarnings("once", category=DeprecationWarning) filterwarnings("once", category=DeprecationWarning)
@ -1554,7 +1554,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
) -> Task: ) -> Task:
if not isinstance(task, Future): if not isinstance(task, Future):
prepped = cls._prep_task(task, app, loop) prepped = cls._prep_task(task, app, loop)
if sys.version_info < (3, 8): if sys.version_info < (3, 8): # no cov
if name: if name:
error_logger.warning( error_logger.warning(
"Cannot set a name for a task when using Python 3.7. " "Cannot set a name for a task when using Python 3.7. "
@ -1598,7 +1598,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
:param task: future, couroutine or awaitable :param task: future, couroutine or awaitable
""" """
if name and sys.version_info == (3, 7): if name and sys.version_info < (3, 8): # no cov
name = None name = None
error_logger.warning( error_logger.warning(
"Cannot set a name for a task when using Python 3.7. Your " "Cannot set a name for a task when using Python 3.7. Your "
@ -1626,7 +1626,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
def get_task( def get_task(
self, name: str, *, raise_exception: bool = True self, name: str, *, raise_exception: bool = True
) -> Optional[Task]: ) -> Optional[Task]:
if sys.version_info < (3, 8): if sys.version_info < (3, 8): # no cov
error_logger.warning( error_logger.warning(
"This feature (get_task) is only supported on using " "This feature (get_task) is only supported on using "
"Python 3.8+." "Python 3.8+."
@ -1648,7 +1648,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
*, *,
raise_exception: bool = True, raise_exception: bool = True,
) -> None: ) -> None:
if sys.version_info < (3, 8): if sys.version_info < (3, 8): # no cov
error_logger.warning( error_logger.warning(
"This feature (cancel_task) is only supported on using " "This feature (cancel_task) is only supported on using "
"Python 3.8+." "Python 3.8+."
@ -1660,7 +1660,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
if msg: if msg:
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):
args = (msg,) args = (msg,)
else: else: # no cov
raise RuntimeError( raise RuntimeError(
"Cancelling a task with a message is only supported " "Cancelling a task with a message is only supported "
"on Python 3.9+." "on Python 3.9+."
@ -1672,7 +1672,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
... ...
def purge_tasks(self): def purge_tasks(self):
if sys.version_info < (3, 8): if sys.version_info < (3, 8): # no cov
error_logger.warning( error_logger.warning(
"This feature (purge_tasks) is only supported on using " "This feature (purge_tasks) is only supported on using "
"Python 3.8+." "Python 3.8+."
@ -1709,7 +1709,7 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
@property @property
def tasks(self): def tasks(self):
if sys.version_info < (3, 8): if sys.version_info < (3, 8): # no cov
error_logger.warning( error_logger.warning(
"This feature (tasks) is only supported on using " "This feature (tasks) is only supported on using "
"Python 3.8+." "Python 3.8+."

View File

@ -53,14 +53,14 @@ class ErrorHandler:
self._warn_fallback_deprecation() self._warn_fallback_deprecation()
@property @property
def fallback(self): def fallback(self): # no cov
# This is for backwards compat and can be removed in v22.6 # This is for backwards compat and can be removed in v22.6
if self._fallback is _default: if self._fallback is _default:
return DEFAULT_FORMAT return DEFAULT_FORMAT
return self._fallback return self._fallback
@fallback.setter @fallback.setter
def fallback(self, value: str): def fallback(self, value: str): # no cov
self._warn_fallback_deprecation() self._warn_fallback_deprecation()
if not isinstance(value, str): if not isinstance(value, str):
raise SanicException( raise SanicException(
@ -236,7 +236,7 @@ class ErrorHandler:
except Exception: except Exception:
try: try:
url = repr(request.url) url = repr(request.url)
except AttributeError: except AttributeError: # no cov
url = "unknown" url = "unknown"
response_message = ( response_message = (
"Exception raised in exception handler " '"%s" for uri: %s' "Exception raised in exception handler " '"%s" for uri: %s'
@ -281,7 +281,7 @@ class ErrorHandler:
if quiet is False or noisy is True: if quiet is False or noisy is True:
try: try:
url = repr(request.url) url = repr(request.url)
except AttributeError: except AttributeError: # no cov
url = "unknown" url = "unknown"
error_logger.exception( error_logger.exception(

View File

@ -6,7 +6,7 @@ from typing import Any, Dict
from warnings import warn from warnings import warn
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict( # no cov
version=1, version=1,
disable_existing_loggers=False, disable_existing_loggers=False,
loggers={ loggers={
@ -57,7 +57,7 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
) )
class Colors(str, Enum): class Colors(str, Enum): # no cov
END = "\033[0m" END = "\033[0m"
BLUE = "\033[01;34m" BLUE = "\033[01;34m"
GREEN = "\033[01;32m" GREEN = "\033[01;32m"
@ -65,23 +65,23 @@ class Colors(str, Enum):
RED = "\033[01;31m" RED = "\033[01;31m"
logger = logging.getLogger("sanic.root") logger = logging.getLogger("sanic.root") # no cov
""" """
General Sanic logger General Sanic logger
""" """
error_logger = logging.getLogger("sanic.error") error_logger = logging.getLogger("sanic.error") # no cov
""" """
Logger used by Sanic for error logging Logger used by Sanic for error logging
""" """
access_logger = logging.getLogger("sanic.access") access_logger = logging.getLogger("sanic.access") # no cov
""" """
Logger used by Sanic for access logging Logger used by Sanic for access logging
""" """
def deprecation(message: str, version: float): def deprecation(message: str, version: float): # no cov
version_info = f"[DEPRECATION v{version}] " version_info = f"[DEPRECATION v{version}] "
if sys.stdout.isatty(): if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}" version_info = f"{Colors.RED}{version_info}"

View File

@ -13,7 +13,7 @@ ASGISend = Callable[[ASGIMessage], Awaitable[None]]
ASGIReceive = Callable[[], Awaitable[ASGIMessage]] ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol: class MockProtocol: # no cov
def __init__(self, transport: "MockTransport", loop): def __init__(self, transport: "MockTransport", loop):
# This should be refactored when < 3.8 support is dropped # This should be refactored when < 3.8 support is dropped
self.transport = transport self.transport = transport
@ -56,7 +56,7 @@ class MockProtocol:
await self._not_paused.wait() await self._not_paused.wait()
class MockTransport: class MockTransport: # no cov
_protocol: Optional[MockProtocol] _protocol: Optional[MockProtocol]
def __init__( def __init__(

View File

@ -9,7 +9,7 @@ from websockets.typing import Data
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
from .impl import WebsocketImplProtocol from .impl import WebsocketImplProtocol
UTF8Decoder = codecs.getincrementaldecoder("utf-8") UTF8Decoder = codecs.getincrementaldecoder("utf-8")
@ -37,7 +37,7 @@ class WebsocketFrameAssembler:
"get_id", "get_id",
"put_id", "put_id",
) )
if TYPE_CHECKING: if TYPE_CHECKING: # no cov
protocol: "WebsocketImplProtocol" protocol: "WebsocketImplProtocol"
read_mutex: asyncio.Lock read_mutex: asyncio.Lock
write_mutex: asyncio.Lock write_mutex: asyncio.Lock
@ -131,7 +131,7 @@ class WebsocketFrameAssembler:
if self.paused: if self.paused:
self.protocol.resume_frames() self.protocol.resume_frames()
self.paused = False self.paused = False
if not self.get_in_progress: if not self.get_in_progress: # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# exception is here as a failsafe # exception is here as a failsafe
raise ServerError( raise ServerError(
@ -204,7 +204,7 @@ class WebsocketFrameAssembler:
if self.paused: if self.paused:
self.protocol.resume_frames() self.protocol.resume_frames()
self.paused = False self.paused = False
if not self.get_in_progress: if not self.get_in_progress: # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# exception is here as a failsafe # exception is here as a failsafe
raise ServerError( raise ServerError(
@ -212,7 +212,7 @@ class WebsocketFrameAssembler:
"asynchronous get was in progress." "asynchronous get was in progress."
) )
self.get_in_progress = False self.get_in_progress = False
if not self.message_complete.is_set(): if not self.message_complete.is_set(): # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# exception is here as a failsafe # exception is here as a failsafe
raise ServerError( raise ServerError(
@ -220,7 +220,7 @@ class WebsocketFrameAssembler:
"message was complete." "message was complete."
) )
self.message_complete.clear() self.message_complete.clear()
if self.message_fetched.is_set(): if self.message_fetched.is_set(): # no cov
# This should be guarded against with the read_mutex, # This should be guarded against with the read_mutex,
# and get_in_progress check, this exception is # and get_in_progress check, this exception is
# here as a failsafe # here as a failsafe

View File

@ -1,5 +1,4 @@
import asyncio import asyncio
import base64
import logging import logging
import random import random
import re import re
@ -205,7 +204,3 @@ def sanic_ext(ext_instance): # noqa
yield sanic_ext yield sanic_ext
with suppress(KeyError): with suppress(KeyError):
del sys.modules["sanic_ext"] del sys.modules["sanic_ext"]
def encode_basic_auth_credentials(username, password):
return base64.b64encode(f"{username}:{password}".encode()).decode("ascii")

View File

@ -1,3 +1,4 @@
import base64
import logging import logging
from json import dumps as json_dumps from json import dumps as json_dumps
@ -18,7 +19,10 @@ from sanic import Blueprint, Sanic
from sanic.exceptions import ServerError from sanic.exceptions import ServerError
from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters from sanic.request import DEFAULT_HTTP_CONTENT_TYPE, RequestParameters
from sanic.response import html, json, text from sanic.response import html, json, text
from tests.conftest import encode_basic_auth_credentials
def encode_basic_auth_credentials(username, password):
return base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
# ------------------------------------------------------------ # # ------------------------------------------------------------ #

237
tests/test_websockets.py Normal file
View File

@ -0,0 +1,237 @@
import re
from asyncio import Event, Queue, TimeoutError
from unittest.mock import AsyncMock, Mock, call
import pytest
from websockets.frames import CTRL_OPCODES, DATA_OPCODES, Frame
from sanic.exceptions import ServerError
from sanic.server.websockets.frame import WebsocketFrameAssembler
@pytest.mark.asyncio
async def test_ws_frame_get_message_incomplete_timeout_0():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get(0)
assert data is None
assembler.message_complete.is_set.assert_called_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message_in_progress():
assembler = WebsocketFrameAssembler(Mock())
assembler.get_in_progress = True
message = re.escape(
"Called get() on Websocket frame assembler "
"while asynchronous get is already in progress."
)
with pytest.raises(ServerError, match=message):
await assembler.get()
@pytest.mark.asyncio
async def test_ws_frame_get_message_incomplete():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get()
assert data is None
assembler.message_complete.wait.assert_awaited_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
data = await assembler.get()
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
@pytest.mark.asyncio
async def test_ws_frame_get_message_with_timeout():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
data = await assembler.get(0.1)
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
assert assembler.message_complete.is_set.call_count == 2
@pytest.mark.asyncio
async def test_ws_frame_get_message_with_timeouterror():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.wait = AsyncMock(return_value=True)
assembler.message_complete.is_set = Mock(return_value=True)
assembler.message_complete.wait.side_effect = TimeoutError("...")
data = await assembler.get(0.1)
assert data == b""
assembler.message_complete.wait.assert_awaited_once()
assert assembler.message_complete.is_set.call_count == 2
@pytest.mark.asyncio
async def test_ws_frame_get_not_completed():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=False)
data = await assembler.get()
assert data is None
@pytest.mark.asyncio
async def test_ws_frame_get_not_completed_start():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(side_effect=[False, True])
data = await assembler.get(0.1)
assert data is None
@pytest.mark.asyncio
async def test_ws_frame_get_paused():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(side_effect=[False, True])
assembler.paused = True
data = await assembler.get()
assert data is None
assembler.protocol.resume_frames.assert_called_once()
@pytest.mark.asyncio
async def test_ws_frame_get_data():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete = AsyncMock(spec=Event)
assembler.message_complete.is_set = Mock(return_value=True)
assembler.chunks = [b"foo", b"bar"]
data = await assembler.get()
assert data == b"foobar"
@pytest.mark.asyncio
async def test_ws_frame_get_iter_in_progress():
assembler = WebsocketFrameAssembler(Mock())
assembler.get_in_progress = True
message = re.escape(
"Called get_iter on Websocket frame assembler "
"while asynchronous get is already in progress."
)
with pytest.raises(ServerError, match=message):
[x async for x in assembler.get_iter()]
@pytest.mark.asyncio
async def test_ws_frame_get_iter_none_in_queue():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
assembler.chunks = [b"foo", b"bar"]
chunks = [x async for x in assembler.get_iter()]
assert chunks == [b"foo", b"bar"]
@pytest.mark.asyncio
async def test_ws_frame_get_iter_paused():
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
assembler.paused = True
[x async for x in assembler.get_iter()]
assembler.protocol.resume_frames.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_not_fetched(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_fetched.set()
message = re.escape(
"Websocket put() got a new message when the previous message was "
"not yet fetched."
)
with pytest.raises(ServerError, match=message):
await assembler.put(Frame(opcode, b""))
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_fetched(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_fetched = AsyncMock()
assembler.message_fetched.is_set = Mock(return_value=False)
await assembler.put(Frame(opcode, b""))
assembler.message_fetched.wait.assert_awaited_once()
assembler.message_fetched.clear.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_message_complete(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.message_complete.set()
message = re.escape(
"Websocket put() got a new message when a message was "
"already in its chamber."
)
with pytest.raises(ServerError, match=message):
await assembler.put(Frame(opcode, b""))
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_message_into_queue(opcode):
assembler = WebsocketFrameAssembler(Mock())
assembler.chunks_queue = AsyncMock(spec=Queue)
assembler.message_fetched = AsyncMock()
assembler.message_fetched.is_set = Mock(return_value=False)
await assembler.put(Frame(opcode, b"foo"))
assembler.chunks_queue.put.has_calls(
call(b"foo"),
call(None),
)
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", DATA_OPCODES)
async def test_ws_frame_put_not_fin(opcode):
assembler = WebsocketFrameAssembler(Mock())
retval = await assembler.put(Frame(opcode, b"foo", fin=False))
assert retval is None
@pytest.mark.asyncio
@pytest.mark.parametrize("opcode", CTRL_OPCODES)
async def test_ws_frame_put_skip_ctrl(opcode):
assembler = WebsocketFrameAssembler(Mock())
retval = await assembler.put(Frame(opcode, b""))
assert retval is None