From a4e0e6e093a8fc07201c894b40bef53b3bf1fded Mon Sep 17 00:00:00 2001 From: Taddes Date: Mon, 29 Jan 2024 13:47:20 -0500 Subject: [PATCH 01/12] create makefile command --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index fbceb076c..e3dbf3fe1 100644 --- a/Makefile +++ b/Makefile @@ -43,6 +43,7 @@ lint: $(POETRY) run isort --sp $(PYPROJECT_TOML) -c $(TESTS_DIR) $(POETRY) run black --quiet --diff --config $(PYPROJECT_TOML) --check $(TESTS_DIR) $(POETRY) run flake8 --config $(FLAKE8_CONFIG) $(TESTS_DIR) + $(POETRY) run pydocstyle --config=$(PYPROJECT_TOML) $(POETRY) run mypy $(TESTS_DIR) --config-file=$(PYPROJECT_TOML) load: From d4bd1724781803cdb6cbf696fa4344b53f7b488a Mon Sep 17 00:00:00 2001 From: Taddes Date: Mon, 29 Jan 2024 13:48:21 -0500 Subject: [PATCH 02/12] install pydocstyle --- tests/poetry.lock | 30 +++++++++++++++++++++++++++++- tests/pyproject.toml | 1 + 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/tests/poetry.lock b/tests/poetry.lock index b9c8c7594..bede19c25 100644 --- a/tests/poetry.lock +++ b/tests/poetry.lock @@ -1495,6 +1495,23 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydocstyle" +version = "6.3.0" +description = "Python docstring style checker" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] + +[package.dependencies] +snowballstemmer = ">=2.2.0" + +[package.extras] +toml = ["tomli (>=1.2.3)"] + [[package]] name = "pyflakes" version = "3.1.0" @@ -1780,6 +1797,17 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -2004,4 +2032,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "cd8fe171c1c1c7a930658a902148a91f392c4ee6e5b8c48551a5d2a863e1dc71" +content-hash = "1d3d73e1e2d6e6995149fad8969a05c985db1dd404551ea7c89a7c12ddc7c8d0" diff --git a/tests/pyproject.toml b/tests/pyproject.toml index ed1ef6d15..dcf59ecbd 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -30,6 +30,7 @@ license = "Mozilla Public License Version 2.0" [tool.poetry.dependencies] python = "^3.10" websocket-client = "^1.7.0" +pydocstyle = "^6.3.0" [tool.poetry.group.dev.dependencies] black = "^23.12.0" From 870d10f6fdeaf43bd6e74b6fb6c0e2bc5a2db969 Mon Sep 17 00:00:00 2001 From: Taddes Date: Mon, 29 Jan 2024 15:13:19 -0500 Subject: [PATCH 03/12] add detailed configs and ignores for specific errors --- tests/pyproject.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/pyproject.toml b/tests/pyproject.toml index dcf59ecbd..a913b9652 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -20,6 +20,19 @@ warn_return_any = true warn_unused_ignores = true warn_unreachable = true +[tool.pydocstyle] +match = ".*\\.py" +convention = "pep257" +# Error Code Ref: https://www.pydocstyle.org/en/stable/error_codes.html +# D212 Multi-line docstring summary should start at the first line +add-select = ["D212"] +# D105 Docstrings for magic methods +# D107 Docstrings for __init__ +# D203 as it conflicts with D211 https://github.com/PyCQA/pydocstyle/issues/141 +# D205 1 blank line required between summary line and description, awkward spacing +# D400 First line should end with a period, doesn't work when sentence spans 2 lines +add-ignore = ["D105","D107","D203", "D205", "D400"] + [tool.poetry] name = "tests" version = "0.1.0" From 385e2771304724f5af4bf41bcec7c01cbf474858 Mon Sep 17 00:00:00 2001 From: Taddes Date: Mon, 29 Jan 2024 22:11:24 -0500 Subject: [PATCH 04/12] git and docker ignore additions --- .dockerignore | 1 + .gitignore | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 23d2f812e..0b95bd847 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ .DS_Store .git +.install.stamp .svn *.pyc *.egg-info diff --git a/.gitignore b/.gitignore index 931c70166..7a9d50527 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,7 @@ docs/old .vscode/* # circleCI -workspace \ No newline at end of file +workspace + +# For poetry install +.install.stamp \ No newline at end of file From c14c5750e7e6e597fa6eab0e5f53545cce57c5aa Mon Sep 17 00:00:00 2001 From: Taddes Date: Mon, 29 Jan 2024 22:11:58 -0500 Subject: [PATCH 05/12] add install_stamp for dependency installs with poetry and individual command for pydocstyle --- Makefile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Makefile b/Makefile index e3dbf3fe1..c5caae743 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,10 @@ LOAD_TEST_DIR := $(TESTS_DIR)/load POETRY := poetry --directory $(TESTS_DIR) DOCKER_COMPOSE := docker compose PYPROJECT_TOML := $(TESTS_DIR)/pyproject.toml +POETRY_LOCK := $(TESTS_DIR)/poetry.lock FLAKE8_CONFIG := $(TESTS_DIR)/.flake8 LOCUST_HOST := "wss://autoconnect.stage.mozaws.net" +INSTALL_STAMP := .install.stamp .PHONY: ddb @@ -17,6 +19,13 @@ ddb: mkdir $@ curl -sSL http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz | tar xzvC $@ +.PHONY: install +install: $(INSTALL_STAMP) ## Install dependencies with poetry +$(INSTALL_STAMP): $(PYPROJECT_TOML) $(POETRY_LOCK) + @if [ -z $(POETRY) ]; then echo "Poetry could not be found. See https://python-poetry.org/docs/"; exit 2; fi + $(POETRY) install + touch $(INSTALL_STAMP) + upgrade: $(CARGO) install cargo-edit || echo "\n$(CARGO) install cargo-edit failed, continuing.." @@ -37,6 +46,10 @@ integration-test: --junit-xml=$(TEST_RESULTS_DIR)/integration_test_results.xml \ -v $(PYTEST_ARGS) +.PHONY: pydocstyle +pydocstyle: $(INSTALL_STAMP) ## Run pydocstyle + $(POETRY) run pydocstyle -es $(TESTS_DIR) --count --config=$(PYPROJECT_TOML) + lint: $(POETRY) -V $(POETRY) install --no-root From 0fc31ec8b708cbfdca5221447d7d342bab573349 Mon Sep 17 00:00:00 2001 From: Taddes Date: Tue, 30 Jan 2024 10:25:44 -0500 Subject: [PATCH 06/12] add pydocstyle docstrings to missing functions and classes --- scripts/fernet_key.py | 4 +- scripts/gendpoint.py | 5 +- tests/integration/__init__.py | 1 + tests/integration/db.py | 31 ++++-- .../integration/test_integration_all_rust.py | 97 ++++++++++++++++++- tests/load/locustfiles/args.py | 4 +- tests/load/locustfiles/exceptions.py | 1 + tests/load/locustfiles/load.py | 2 +- tests/load/locustfiles/locustfile.py | 26 ++--- tests/load/locustfiles/models.py | 1 - 10 files changed, 140 insertions(+), 32 deletions(-) diff --git a/scripts/fernet_key.py b/scripts/fernet_key.py index a9667d32b..0793c5ef4 100644 --- a/scripts/fernet_key.py +++ b/scripts/fernet_key.py @@ -1,6 +1,4 @@ -""" -A command-line utility that generates endpoint encryption keys. -""" +"""A command-line utility that generates endpoint encryption keys.""" from __future__ import print_function from cryptography.fernet import Fernet diff --git a/scripts/gendpoint.py b/scripts/gendpoint.py index 17bbb1d16..cc05e86ef 100644 --- a/scripts/gendpoint.py +++ b/scripts/gendpoint.py @@ -1,5 +1,7 @@ +"""Module to process configuration from cli arguments and environment +variables. +""" #! env python3 - import argparse import os @@ -35,6 +37,7 @@ def config(env_args: os._Environ) -> argparse.Namespace: def main(): + """Process environment arguments/variables and set key.""" args = config(os.environ) if isinstance(args.key, list): key = args.key[0] diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index e69de29bb..72ebee739 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""__init__.py for integration tests.""" diff --git a/tests/integration/db.py b/tests/integration/db.py index 3b1f3169c..034865962 100644 --- a/tests/integration/db.py +++ b/tests/integration/db.py @@ -59,6 +59,8 @@ class ItemNotFound(Exception): class DynamoDBResource(threading.local): + """DynamoDBResource class subclassing threading.local""" + def __init__(self, **kwargs): conf = kwargs if not conf.get("endpoint_url"): @@ -87,7 +89,7 @@ def __getattr__(self, name): def get_latest_message_tablenames( self, prefix: str = "message", previous: int = 1 ) -> list[str]: - """Fetches the name of the last message table""" + """Fetch the name of the last message table.""" client = self._resource.meta.client paginator = client.get_paginator("list_tables") tables = [] @@ -102,11 +104,13 @@ def get_latest_message_tablenames( return tables[0 - previous :] def get_latest_message_tablename(self, prefix: str = "message") -> str: - """Fetches the name of the last message table""" + """Fetch the name of the last message table.""" return self.get_latest_message_tablenames(prefix=prefix, previous=1)[0] class DynamoDBTable(threading.local): + """DynamoDBTable class.""" + def __init__(self, ddb_resource: DynamoDBResource, *args, **kwargs) -> None: self._table = ddb_resource.Table(*args, **kwargs) @@ -118,13 +122,24 @@ def __getattr__(self, name): def generate_hash(key: bytes, payload: bytes) -> str: """Generate a HMAC for the uaid using the secret - :returns: HMAC hash and the nonce used as a tuple (nonce, hash). + :param key: key + :type: bytes + :param payload: payload + :type: bytes + :returns: A hexadecimal string of the HMAC hash and the nonce, used as a tuple (nonce, hash) + :rtype: str """ h = hmac.new(key=key, msg=payload, digestmod=hashlib.sha256) return h.hexdigest() def normalize_id(ident: uuid.UUID | str) -> str: + """Normalize and return ID as string + + :param ident: uuid.UUID or str identifier + :returns: string representation of UUID + :raises ValueError: raises an exception if UUID is invalid + """ if isinstance(ident, uuid.UUID): return str(ident) try: @@ -134,9 +149,10 @@ def normalize_id(ident: uuid.UUID | str) -> str: def base64url_encode(value: bytes | str) -> str: + """Encode an unpadded Base64 URL-encoded string per RFC 7515.""" if isinstance(value, str): value = bytes(value, "utf-8") - """Encodes an unpadded Base64 URL-encoded string per RFC 7515.""" + return base64.urlsafe_b64encode(value).strip(b"=").decode("utf-8") @@ -155,9 +171,7 @@ def base64url_encode(value: bytes | str) -> str: def get_month(delta: int = 0) -> datetime.date: - """Basic helper function to get a datetime.date object iterations months - ahead/behind of now. - """ + """Get a datetime.date object iterations months ahead/behind of now.""" new = last = datetime.date.today() # Move until we hit a new month, this avoids having to manually # check year changes as we push forward or backward since the Python @@ -308,7 +322,8 @@ def get_router_table( def track_provisioned(func: Callable[..., T]) -> Callable[..., T]: """Tracks provisioned exceptions and increments a metric for them named - after the function decorated""" + after the function decorated. + """ @wraps(func) def wrapper(self, *args, **kwargs): diff --git a/tests/integration/test_integration_all_rust.py b/tests/integration/test_integration_all_rust.py index 3800c6f87..250dcad7b 100644 --- a/tests/integration/test_integration_all_rust.py +++ b/tests/integration/test_integration_all_rust.py @@ -1,6 +1,4 @@ -""" -Rust Connection and Endpoint Node Integration Tests -""" +"""Rust Connection and Endpoint Node Integration Tests.""" import base64 import copy @@ -95,6 +93,7 @@ def get_free_port() -> int: + """Get free port.""" port: int s = socket.socket(socket.AF_INET, type=socket.SOCK_STREAM) s.bind(("localhost", 0)) @@ -110,6 +109,7 @@ def get_free_port() -> int: def get_db_settings() -> str | dict[str, str | int | float] | None: + """Get database settings.""" env_var = os.environ.get("DB_SETTINGS") if env_var: if os.path.isfile(env_var): @@ -212,6 +212,7 @@ def __init__(self, url) -> None: } def __getattribute__(self, name: str): + """Turn functions into deferToThread functions.""" # Python fun to turn all functions into deferToThread functions f = object.__getattribute__(self, name) if name.startswith("__"): @@ -223,6 +224,7 @@ def __getattribute__(self, name: str): return f def connect(self, connection_port: int | None = None): + """Connect.""" url = self.url if connection_port: # pragma: nocover url = "ws://localhost:{}/".format(connection_port) @@ -230,6 +232,7 @@ def connect(self, connection_port: int | None = None): return self.ws.connected if self.ws else None def hello(self, uaid: str | None = None, services: list[str] | None = None): + """Hello verification.""" if not self.ws: raise Exception("WebSocket client not available as expected") @@ -261,6 +264,7 @@ def hello(self, uaid: str | None = None, services: list[str] | None = None): return result def broadcast_subscribe(self, services: list[str]): + """Broadcast subscribe.""" if not self.ws: raise Exception("WebSocket client not available as expected") @@ -269,6 +273,7 @@ def broadcast_subscribe(self, services: list[str]): self.ws.send(msg) def register(self, chid: str | None = None, key=None, status=200): + """Register.""" if not self.ws: raise Exception("WebSocket client not available as expected") @@ -286,6 +291,7 @@ def register(self, chid: str | None = None, key=None, status=200): return result def unregister(self, chid): + """Unregister.""" msg = json.dumps(dict(messageType="unregister", channelID=chid)) log.debug("Send: %s", msg) self.ws.send(msg) @@ -294,6 +300,7 @@ def unregister(self, chid): return result def delete_notification(self, channel, message=None, status=204): + """Delete notification.""" messages = self.messages[channel] if not message: message = random.choice(messages) @@ -317,6 +324,7 @@ def send_notification( topic=None, headers=None, ): + """Send notification.""" if not channel: channel = random.choice(list(self.channels.keys())) @@ -375,6 +383,7 @@ def send_notification( return resp def get_notification(self, timeout=1): + """Get notification.""" orig_timeout = self.ws.gettimeout() self.ws.settimeout(timeout) try: @@ -387,6 +396,7 @@ def get_notification(self, timeout=1): self.ws.settimeout(orig_timeout) def get_broadcast(self, timeout=1): # pragma: nocover + """Get broadcast.""" orig_timeout = self.ws.gettimeout() self.ws.settimeout(timeout) try: @@ -402,6 +412,7 @@ def get_broadcast(self, timeout=1): # pragma: nocover self.ws.settimeout(orig_timeout) def ping(self): + """Test ping.""" log.debug("Send: %s", "{}") self.ws.send("{}") result = self.ws.recv() @@ -410,6 +421,7 @@ def ping(self): return result def ack(self, channel, version): + """Acknowledge message send.""" msg = json.dumps( dict( messageType="ack", @@ -420,13 +432,15 @@ def ack(self, channel, version): self.ws.send(msg) def disconnect(self): + """Disconnect""" self.ws.close() def sleep(self, duration: int): # pragma: nocover + """Sleep wrapper function.""" time.sleep(duration) def wait_for(self, func): - """Waits several seconds for a function to return True""" + """Wait several seconds for a function to return True""" times = 0 while not func(): # pragma: nocover time.sleep(1) @@ -440,6 +454,7 @@ def _get_vapid( payload: dict[str, str | int] | None = None, endpoint: str | None = None, ) -> dict[str, str | bytes]: + """Get vapid key.""" global CONNECTION_CONFIG if endpoint is None: @@ -471,6 +486,7 @@ def enqueue_output(out, queue): def print_lines_in_queues(queues, prefix): + """Print lines in queues to stdout.""" for queue in queues: is_empty = False while not is_empty: @@ -516,6 +532,8 @@ def max_logs(endpoint=None, conn=None): """ def max_logs_decorator(func): + """Decorate max_logs.""" + def wrapper(self, *args, **kwargs): if endpoint is not None: self.max_endpoint_logs = endpoint @@ -530,24 +548,30 @@ def wrapper(self, *args, **kwargs): @app.get("/v1/broadcasts") def broadcast_handler(): + """Broadcast handler setup.""" assert bottle.request.headers["Authorization"] == MOCK_MP_TOKEN MOCK_MP_POLLED.set() return dict(broadcasts=MOCK_MP_SERVICES) @app.post("/api/1/envelope/") -def sentry_handler(): +def sentry_handler() -> dict[str, str]: + """Sentry handler configuration.""" headers, item_headers, payload = bottle.request.body.read().splitlines() MOCK_SENTRY_QUEUE.put(json.loads(payload)) return {"id": "fc6d8c0c43fc4630ad850ee518f1b9d0"} class CustomClient(Client): + """Custom Client for testing.""" + def send_bad_data(self): + """Set `bad-data`""" self.ws.send("bad-data") def kill_process(process): + """Kill child processes.""" # This kinda sucks, but its the only way to nuke the child procs if process is None: return @@ -559,6 +583,7 @@ def kill_process(process): def get_rust_binary_path(binary): + """Get Rust binary path.""" global STRICT_LOG_COUNTS rust_bin = root_dir + "/target/release/{}".format(binary) @@ -578,6 +603,7 @@ def get_rust_binary_path(binary): def write_config_to_env(config, prefix): + """Write configurations to env.""" for key, val in config.items(): new_key = prefix + key log.debug("✍ config {} => {}".format(new_key, val)) @@ -585,6 +611,7 @@ def write_config_to_env(config, prefix): def capture_output_to_queue(output_stream): + """Capture output to log queue.""" log_queue = Queue() t = Thread(target=enqueue_output, args=(output_stream, log_queue)) t.daemon = True # thread dies with the program @@ -593,6 +620,7 @@ def capture_output_to_queue(output_stream): def setup_bt(): + """Set up BigTable emulator.""" global BT_PROCESS, BT_DB_SETTINGS log.debug("🐍🟢 Starting bigtable emulator") BT_PROCESS = subprocess.Popen("gcloud beta emulators bigtable start".split(" ")) @@ -617,6 +645,7 @@ def setup_bt(): def setup_dynamodb(): + """Set up DynamoDB.""" global DDB_PROCESS log.debug("🐍🟢 Starting dynamodb") @@ -643,6 +672,7 @@ def setup_dynamodb(): def setup_mock_server(): + """Set up mock server.""" global MOCK_SERVER_THREAD MOCK_SERVER_THREAD = Thread(target=app.run, kwargs=dict(port=MOCK_SERVER_PORT, debug=True)) @@ -654,6 +684,7 @@ def setup_mock_server(): def setup_connection_server(connection_binary): + """Set up connection server from config.""" global CN_SERVER, BT_PROCESS, DDB_PROCESS # NOTE: @@ -692,6 +723,7 @@ def setup_connection_server(connection_binary): def setup_megaphone_server(connection_binary): + """Set up megaphone server from configuration.""" global CN_MP_SERVER url = os.getenv("AUTOPUSH_MP_SERVER") @@ -715,6 +747,7 @@ def setup_megaphone_server(connection_binary): def setup_endpoint_server(): + """Set up endpoint server from configuration.""" global CONNECTION_CONFIG, EP_SERVER, BT_PROCESS # Set up environment @@ -755,6 +788,9 @@ def setup_endpoint_server(): def setup_module(): + """Set up module including BigTable or Dynamo + and connection, endpoint and megaphone servers. + """ global CN_SERVER, CN_QUEUES, CN_MP_SERVER, MOCK_SERVER_THREAD, STRICT_LOG_COUNTS, RUST_LOG if "SKIP_INTEGRATION" in os.environ: # pragma: nocover @@ -787,6 +823,7 @@ def setup_module(): def teardown_module(): + """Teardown module for dynamo, bigtable, and servers.""" if DDB_PROCESS: os.unsetenv("AWS_LOCAL_DYNAMODB") log.debug("🐍🔴 Stopping dynamodb") @@ -804,6 +841,8 @@ def teardown_module(): class TestRustWebPush(unittest.TestCase): + """Test class for Rust Web Push.""" + # Max log lines allowed to be emitted by each node type max_endpoint_logs = 8 max_conn_logs = 3 @@ -813,16 +852,19 @@ class TestRustWebPush(unittest.TestCase): } def tearDown(self): + """Tear down and log processing.""" process_logs(self) while not MOCK_SENTRY_QUEUE.empty(): MOCK_SENTRY_QUEUE.get_nowait() def host_endpoint(self, client): + """Return host endpoint.""" parsed = urlparse(list(client.channels.values())[0]) return "{}://{}".format(parsed.scheme, parsed.netloc) @inlineCallbacks def quick_register(self): + """Quick register.""" log.debug("🐍#### Connecting to ws://localhost:{}/".format(CONNECTION_PORT)) client = Client("ws://localhost:{}/".format(CONNECTION_PORT)) yield client.connect() @@ -833,6 +875,7 @@ def quick_register(self): @inlineCallbacks def shut_down(self, client=None): + """Shut down client.""" if client: yield client.disconnect() @@ -843,6 +886,7 @@ def _ws_url(self): @inlineCallbacks @max_logs(conn=4) def test_sentry_output_autoconnect(self): + """Test sentry output for autoconnect.""" if os.getenv("SKIP_SENTRY"): SkipTest("Skipping sentry test") return @@ -866,6 +910,7 @@ def test_sentry_output_autoconnect(self): @inlineCallbacks @max_logs(endpoint=1) def test_sentry_output_autoendpoint(self): + """Test sentry output for autoendpoint.""" if os.getenv("SKIP_SENTRY"): SkipTest("Skipping sentry test") return @@ -886,6 +931,7 @@ def test_sentry_output_autoendpoint(self): @max_logs(conn=4) def test_no_sentry_output(self): + """Test for no Sentry output.""" if os.getenv("SKIP_SENTRY"): SkipTest("Skipping sentry test") return @@ -902,6 +948,7 @@ def test_no_sentry_output(self): @inlineCallbacks def test_hello_echo(self): + """Test hello echo.""" client = Client(self._ws_url) yield client.connect() result = yield client.hello() @@ -911,6 +958,7 @@ def test_hello_echo(self): @inlineCallbacks def test_hello_with_bad_prior_uaid(self): + """Test hello with bard prior uaid.""" non_uaid = uuid.uuid4().hex client = Client(self._ws_url) yield client.connect() @@ -922,6 +970,7 @@ def test_hello_with_bad_prior_uaid(self): @inlineCallbacks def test_basic_delivery(self): + """Test basic delivery.""" data = str(uuid.uuid4()) client: Client = yield self.quick_register() result = yield client.send_notification(data=data) @@ -934,6 +983,7 @@ def test_basic_delivery(self): @inlineCallbacks def test_topic_basic_delivery(self): + """Test topic basic delivery.""" data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data, topic="Inbox") @@ -946,6 +996,7 @@ def test_topic_basic_delivery(self): @inlineCallbacks def test_topic_replacement_delivery(self): + """Test topic replacement delivery.""" data = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() @@ -968,6 +1019,7 @@ def test_topic_replacement_delivery(self): @inlineCallbacks @max_logs(conn=4) def test_topic_no_delivery_on_reconnect(self): + """Test topic no delivery on reconnect.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -993,6 +1045,7 @@ def test_topic_no_delivery_on_reconnect(self): @inlineCallbacks def test_basic_delivery_with_vapid(self): + """Test basic delivery with vapid.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid(payload=self.vapid_payload) @@ -1006,6 +1059,7 @@ def test_basic_delivery_with_vapid(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid(self): + """Test basic delivery with invalid vapid.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid(payload=self.vapid_payload, endpoint=self.host_endpoint(client)) @@ -1015,6 +1069,7 @@ def test_basic_delivery_with_invalid_vapid(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid_exp(self): + """Test basic delivery with invalid vapid exp.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid( @@ -1030,6 +1085,7 @@ def test_basic_delivery_with_invalid_vapid_exp(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid_auth(self): + """Test basic delivery with invalid vapid auth.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid( @@ -1042,6 +1098,7 @@ def test_basic_delivery_with_invalid_vapid_auth(self): @inlineCallbacks def test_basic_delivery_with_invalid_signature(self): + """Test basic delivery with invalid signature.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid( @@ -1056,6 +1113,7 @@ def test_basic_delivery_with_invalid_signature(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid_ckey(self): + """Test basic delivery with invalid vapid ckey.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid(payload=self.vapid_payload, endpoint=self.host_endpoint(client)) @@ -1065,6 +1123,7 @@ def test_basic_delivery_with_invalid_vapid_ckey(self): @inlineCallbacks def test_delivery_repeat_without_ack(self): + """Test delivery repeat without ack.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1086,6 +1145,7 @@ def test_delivery_repeat_without_ack(self): @inlineCallbacks def test_repeat_delivery_with_disconnect_without_ack(self): + """Test repeat delivery with disconnect without ack.""" data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data) @@ -1101,6 +1161,7 @@ def test_repeat_delivery_with_disconnect_without_ack(self): @inlineCallbacks def test_multiple_delivery_repeat_without_ack(self): + """Test multiple delivery repeat without ack.""" data = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() @@ -1130,6 +1191,7 @@ def test_multiple_delivery_repeat_without_ack(self): @inlineCallbacks def test_topic_expired(self): + """Test topic expired.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1148,6 +1210,7 @@ def test_topic_expired(self): @inlineCallbacks @max_logs(conn=4) def test_multiple_delivery_with_single_ack(self): + """Test multiple delivery with single ack.""" data = b"\x16*\xec\xb4\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() data2 = b":\xd8^\xac\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() client = yield self.quick_register() @@ -1188,6 +1251,7 @@ def test_multiple_delivery_with_single_ack(self): @inlineCallbacks def test_multiple_delivery_with_multiple_ack(self): + """Test multiple delivery with multiple ack.""" data = b"\x16*\xec\xb4\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() # "FirstMessage" data2 = b":\xd8^\xac\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() # "OtherMessage" client = yield self.quick_register() @@ -1216,6 +1280,7 @@ def test_multiple_delivery_with_multiple_ack(self): @inlineCallbacks def test_no_delivery_to_unregistered(self): + """Test no delivery to unregistered.""" data = str(uuid.uuid4()) client: Client = yield self.quick_register() assert client.channels @@ -1237,6 +1302,7 @@ def test_no_delivery_to_unregistered(self): @inlineCallbacks def test_ttl_0_connected(self): + """Test TTL 0 connected.""" data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data, ttl=0) @@ -1250,6 +1316,7 @@ def test_ttl_0_connected(self): @inlineCallbacks def test_ttl_0_not_connected(self): + """Test TTL 0 not connected.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1262,6 +1329,7 @@ def test_ttl_0_not_connected(self): @inlineCallbacks def test_ttl_expired(self): + """Test TTL expired.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1276,6 +1344,7 @@ def test_ttl_expired(self): @inlineCallbacks @max_logs(endpoint=28) def test_ttl_batch_expired_and_good_one(self): + """Test TTL batch expired with one good result.""" data = str(uuid.uuid4()).encode() data2 = base64.urlsafe_b64decode("0012") + str(uuid.uuid4()).encode() print(data2) @@ -1303,6 +1372,7 @@ def test_ttl_batch_expired_and_good_one(self): @inlineCallbacks @max_logs(endpoint=28) def test_ttl_batch_partly_expired_and_good_one(self): + """Test TTL batch partly expired with one good result.""" data = str(uuid.uuid4()) data1 = str(uuid.uuid4()) data2 = str(uuid.uuid4()) @@ -1339,6 +1409,7 @@ def test_ttl_batch_partly_expired_and_good_one(self): @inlineCallbacks def test_message_without_crypto_headers(self): + """Test message without crypto headers.""" data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data, use_header=False, status=400) @@ -1347,6 +1418,7 @@ def test_message_without_crypto_headers(self): @inlineCallbacks def test_empty_message_without_crypto_headers(self): + """Test empty message without crypto headers.""" client = yield self.quick_register() result = yield client.send_notification(use_header=False) assert result is not None @@ -1369,6 +1441,7 @@ def test_empty_message_without_crypto_headers(self): @inlineCallbacks def test_empty_message_with_crypto_headers(self): + """Test empty message with crypto headers.""" client = yield self.quick_register() result = yield client.send_notification() assert result is not None @@ -1447,6 +1520,7 @@ def test_delete_saved_notification(self): @inlineCallbacks def test_with_key(self): + """Test with key.""" private_key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) claims = { "aud": "http://localhost:{}".format(ENDPOINT_PORT), @@ -1474,6 +1548,7 @@ def test_with_key(self): @inlineCallbacks def test_with_bad_key(self): + """Test with bad key.""" chid = str(uuid.uuid4()) client = Client("ws://localhost:{}/".format(CONNECTION_PORT)) yield client.connect() @@ -1486,6 +1561,7 @@ def test_with_bad_key(self): @inlineCallbacks @max_logs(endpoint=44) def test_msg_limit(self): + """Test message limit.""" client = yield self.quick_register() uaid = client.uaid yield client.disconnect() @@ -1506,6 +1582,7 @@ def test_msg_limit(self): @inlineCallbacks def test_can_ping(self): + """Test can ping.""" client = yield self.quick_register() yield client.ping() assert client.ws.connected @@ -1544,14 +1621,18 @@ def test_internal_endpoints(self): class TestRustWebPushBroadcast(unittest.TestCase): + """Test class for Rust Web Push Broadcast.""" + max_endpoint_logs = 4 max_conn_logs = 1 def tearDown(self): + """Tear down.""" process_logs(self) @inlineCallbacks def quick_register(self, connection_port=None): + """Connect and register client.""" conn_port = connection_port or MP_CONNECTION_PORT client = Client("ws://localhost:{}/".format(conn_port)) yield client.connect() @@ -1561,6 +1642,7 @@ def quick_register(self, connection_port=None): @inlineCallbacks def shut_down(self, client=None): + """Shut down client connection.""" if client: yield client.disconnect() @@ -1570,6 +1652,7 @@ def _ws_url(self): @inlineCallbacks def test_broadcast_update_on_connect(self): + """Test broadcast update on connect.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear() @@ -1594,6 +1677,7 @@ def test_broadcast_update_on_connect(self): @inlineCallbacks def test_broadcast_update_on_connect_with_errors(self): + """Test broadcast update on connect with errors.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear() @@ -1611,6 +1695,7 @@ def test_broadcast_update_on_connect_with_errors(self): @inlineCallbacks def test_broadcast_subscribe(self): + """Test broadcast subscribe.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear() @@ -1639,6 +1724,7 @@ def test_broadcast_subscribe(self): @inlineCallbacks def test_broadcast_subscribe_with_errors(self): + """Test that broadcast returns expected errors.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear() @@ -1661,6 +1747,7 @@ def test_broadcast_subscribe_with_errors(self): @inlineCallbacks def test_broadcast_no_changes(self): + """Test to ensure there are no changes from broadcast.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear() diff --git a/tests/load/locustfiles/args.py b/tests/load/locustfiles/args.py index 96c4d2d64..def0911ee 100644 --- a/tests/load/locustfiles/args.py +++ b/tests/load/locustfiles/args.py @@ -1,3 +1,4 @@ +"""Load test arguments.""" # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. @@ -19,6 +20,7 @@ def parse_wait_time(val: str): raise ValueError("Invalid wait_time") -def float_or_int(val: str): +def float_or_int(val: str) -> int | float: + """Parse string value into float or integer.""" float_val: float = float(val) return int(float_val) if float_val.is_integer() else float_val diff --git a/tests/load/locustfiles/exceptions.py b/tests/load/locustfiles/exceptions.py index 0b9123c9a..ed4f022e8 100644 --- a/tests/load/locustfiles/exceptions.py +++ b/tests/load/locustfiles/exceptions.py @@ -1,3 +1,4 @@ +"""Custom exceptions for load tests.""" # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. diff --git a/tests/load/locustfiles/load.py b/tests/load/locustfiles/load.py index a9b7f332a..6af5b3b0d 100644 --- a/tests/load/locustfiles/load.py +++ b/tests/load/locustfiles/load.py @@ -27,7 +27,7 @@ def __init__(self, max_run_time: int, max_users: int): ) def calculate_users(self, run_time: int) -> int: - """Determined the number of active users given a run time. + """Determine the number of active users given a run time. Returns: int: The number of users diff --git a/tests/load/locustfiles/locustfile.py b/tests/load/locustfiles/locustfile.py index a75641ef3..66c007ca6 100644 --- a/tests/load/locustfiles/locustfile.py +++ b/tests/load/locustfiles/locustfile.py @@ -1,9 +1,8 @@ +"""Performance test module.""" # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. -"""Performance test module.""" - import base64 import json import logging @@ -61,6 +60,8 @@ def _(environment, **kwargs): class AutopushUser(FastHttpUser): + """AutopushUser class.""" + REST_HEADERS: dict[str, str] = {"TTL": "60", "Content-Encoding": "aes128gcm"} WEBSOCKET_HEADERS: dict[str, str] = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:61.0) " @@ -79,14 +80,15 @@ def __init__(self, environment) -> None: self.ws_greenlet: Greenlet | None = None def wait_time(self): + """Return the autopush wait time.""" return self.environment.autopush_wait_time(self) def on_start(self) -> Any: - """Called when a User starts running.""" + """Call when a User starts running.""" self.ws_greenlet = gevent.spawn(self.connect) def on_stop(self) -> Any: - """Called when a User stops running.""" + """Call when a User stops running.""" if self.ws: for channel_id in self.channels.keys(): self.send_unregister(self.ws, channel_id) @@ -96,7 +98,7 @@ def on_stop(self) -> Any: gevent.kill(self.ws_greenlet) def on_ws_open(self, ws: WebSocket) -> None: - """Called when opening a WebSocket. + """Call when opening a WebSocket. Args: ws: WebSocket class object @@ -104,7 +106,7 @@ def on_ws_open(self, ws: WebSocket) -> None: self.send_hello(ws) def on_ws_message(self, ws: WebSocket, data: str) -> None: - """Called when received data from a WebSocket. + """Call when received data from a WebSocket. Args: ws: WebSocket class object @@ -121,7 +123,7 @@ def on_ws_message(self, ws: WebSocket, data: str) -> None: del self.channels[message.channelID] def on_ws_error(self, ws: WebSocket, error: Exception) -> None: - """Called when there is a WebSocket error or if an exception is raised in a WebSocket + """Call when there is a WebSocket error or if an exception is raised in a WebSocket callback function. Args: @@ -141,7 +143,7 @@ def on_ws_error(self, ws: WebSocket, error: Exception) -> None: def on_ws_close( self, ws: WebSocket, close_status_code: int | None, close_msg: str | None ) -> None: - """Called when closing a WebSocket. + """Call when closing a WebSocket. Args: ws: WebSocket class object @@ -153,7 +155,7 @@ def on_ws_close( @task(weight=98) def send_notification(self): - """Sends a notification to a registered endpoint while connected to Autopush.""" + """Send a notification to a registered endpoint while connected to Autopush.""" if not self.ws or not self.channels: logger.debug("Task 'send_notification' skipped.") return @@ -163,7 +165,7 @@ def send_notification(self): @task(weight=1) def subscribe(self): - """Subscribes a user to an Autopush channel.""" + """Subscribe a user to an Autopush channel.""" if not self.ws: logger.debug("Task 'subscribe' skipped.") return @@ -173,7 +175,7 @@ def subscribe(self): @task(weight=1) def unsubscribe(self): - """Unsubscribes a user from an Autopush channel.""" + """Unsubscribe a user from an Autopush channel.""" if not self.ws or not self.channels: logger.debug("Task 'unsubscribe' skipped.") return @@ -182,7 +184,7 @@ def unsubscribe(self): self.send_unregister(self.ws, channel_id) def connect(self) -> None: - """Creates the WebSocketApp that will run indefinitely.""" + """Create the WebSocketApp that will run indefinitely.""" if not self.host: raise LocustError("'host' value is unavailable.") diff --git a/tests/load/locustfiles/models.py b/tests/load/locustfiles/models.py index 5b36d98f2..49aa3ecaf 100644 --- a/tests/load/locustfiles/models.py +++ b/tests/load/locustfiles/models.py @@ -1,7 +1,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. - """Load test models module.""" from typing import Any, Literal From d1f2721fa5635bf370ff2b2513155deb9e36dfa6 Mon Sep 17 00:00:00 2001 From: Taddes Date: Tue, 30 Jan 2024 10:37:26 -0500 Subject: [PATCH 07/12] annotation for lint command in ci --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f4841d920..4ffe4a339 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -74,7 +74,7 @@ jobs: pip install --upgrade pip pip install poetry - run: - name: isort, black, flake8 and mypy + name: isort, black, flake8, pydocstyle and mypy command: make lint test: @@ -84,7 +84,7 @@ jobs: username: $DOCKER_USER password: $DOCKER_PASS environment: - RUST_BACKTRACE: 1 + RUST_BACKTRACE: 1 - image: amazon/dynamodb-local:latest auth: username: $DOCKER_USER From ab1867d81e48d6384fee1503f552c0790789e6c0 Mon Sep 17 00:00:00 2001 From: Taddes Date: Tue, 30 Jan 2024 17:58:57 -0500 Subject: [PATCH 08/12] revisions to numpy docstring standard started in load tests --- tests/load/locustfiles/args.py | 1 - tests/load/locustfiles/exceptions.py | 2 +- tests/load/locustfiles/load.py | 29 ++++-- tests/load/locustfiles/locustfile.py | 145 ++++++++++++++++++--------- 4 files changed, 117 insertions(+), 60 deletions(-) diff --git a/tests/load/locustfiles/args.py b/tests/load/locustfiles/args.py index def0911ee..23f711645 100644 --- a/tests/load/locustfiles/args.py +++ b/tests/load/locustfiles/args.py @@ -2,7 +2,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. - from locust import between, constant diff --git a/tests/load/locustfiles/exceptions.py b/tests/load/locustfiles/exceptions.py index ed4f022e8..1d9e132b7 100644 --- a/tests/load/locustfiles/exceptions.py +++ b/tests/load/locustfiles/exceptions.py @@ -7,7 +7,7 @@ class ZeroStatusRequestError(Exception): """Custom exception for when a Locust request fails with a '0' status code.""" - def __init__(self): + def __init__(self) -> None: error_message: str = ( "A connection, timeout or similar error happened while sending a request " "from Locust. Status Code: 0" diff --git a/tests/load/locustfiles/load.py b/tests/load/locustfiles/load.py index 6af5b3b0d..a196ef345 100644 --- a/tests/load/locustfiles/load.py +++ b/tests/load/locustfiles/load.py @@ -29,8 +29,15 @@ def __init__(self, max_run_time: int, max_users: int): def calculate_users(self, run_time: int) -> int: """Determine the number of active users given a run time. - Returns: - int: The number of users + Parameters + ---------- + run_time : int + Run time in seconds + + Returns + ------- + run_time : int + The number of users """ return int(round((self.a * math.pow(run_time, 2)) + (self.b * run_time) + self.c)) @@ -57,13 +64,17 @@ def __init__(self): def tick(self) -> TickTuple | None: """Override defining the desired distribution for Autopush load testing. - Returns: - TickTuple: Distribution parameters - user_count: Total user count - spawn_rate: Number of users to start/stop per second when changing - number of users - user_classes: None or a List of user classes to be spawned - None: Instruction to stop the load test + Returns + ------- + TickTuple | None + + Distribution Parameters + ----------------------- + user_count : Total user count + spawn_rate : Number of users to start/stop per second when changing + number of users + user_classes : None or a List of user classes to be spawned + None : Instruction to stop the load test """ run_time: int = self.get_run_time() if run_time > self.MAX_RUN_TIME: diff --git a/tests/load/locustfiles/locustfile.py b/tests/load/locustfiles/locustfile.py index 66c007ca6..09f7dc700 100644 --- a/tests/load/locustfiles/locustfile.py +++ b/tests/load/locustfiles/locustfile.py @@ -84,11 +84,11 @@ def wait_time(self): return self.environment.autopush_wait_time(self) def on_start(self) -> Any: - """Call when a User starts running.""" + """Call when a websocket starts running.""" self.ws_greenlet = gevent.spawn(self.connect) def on_stop(self) -> Any: - """Call when a User stops running.""" + """Call when a websocket stops running.""" if self.ws: for channel_id in self.channels.keys(): self.send_unregister(self.ws, channel_id) @@ -100,17 +100,20 @@ def on_stop(self) -> Any: def on_ws_open(self, ws: WebSocket) -> None: """Call when opening a WebSocket. - Args: - ws: WebSocket class object + Parameters + ---------- + ws : WebSocket """ self.send_hello(ws) def on_ws_message(self, ws: WebSocket, data: str) -> None: """Call when received data from a WebSocket. - Args: - ws: WebSocket class object - data: utf-8 data received from the server + Parameters + ---------- + ws : WebSocket + data : str + utf-8 data received from the server """ message: Message | None = self.recv(data) if isinstance(message, HelloMessage): @@ -126,9 +129,10 @@ def on_ws_error(self, ws: WebSocket, error: Exception) -> None: """Call when there is a WebSocket error or if an exception is raised in a WebSocket callback function. - Args: - ws: WebSocket class object - error: Exception object + Parameters + ---------- + ws : WebSocket + error : Exception """ logger.error(str(error)) @@ -145,16 +149,19 @@ def on_ws_close( ) -> None: """Call when closing a WebSocket. - Args: - ws: WebSocket class object - close_status_code: WebSocket close status - close_msg: WebSocket close message + Parameters + ---------- + ws: WebSocket + close_status_code : int | None + WebSocket close status + close_msg : str | None + ebSocket close message """ if close_status_code or close_msg: logger.info(f"WebSocket closed. status={close_status_code} msg={close_msg}") @task(weight=98) - def send_notification(self): + def send_notification(self) -> None: """Send a notification to a registered endpoint while connected to Autopush.""" if not self.ws or not self.channels: logger.debug("Task 'send_notification' skipped.") @@ -174,7 +181,7 @@ def subscribe(self): self.send_register(self.ws, channel_id) @task(weight=1) - def unsubscribe(self): + def unsubscribe(self) -> None: """Unsubscribe a user from an Autopush channel.""" if not self.ws or not self.channels: logger.debug("Task 'unsubscribe' skipped.") @@ -203,11 +210,16 @@ def connect(self) -> None: def post_notification(self, endpoint_url: str) -> None: """Send a notification to Autopush. - Args: - endpoint_url: A channel destination endpoint url - Raises: - ZeroStatusRequestError: In the event that Locust experiences a network issue while - sending a notification. + Parameters + ---------- + endpoint_url : str + A channel destination endpoint url + + Raises + ------ + ZeroStatusRequestError + In the event that Locust experiences a network issue while + sending a notification. """ message_type: str = "notification" # Prefix random message with 'TestData' to more easily differentiate the payload @@ -236,8 +248,20 @@ def post_notification(self, endpoint_url: str) -> None: def recv(self, data: str) -> Message | None: """Verify the contents of an Autopush message and report response statistics to Locust. - Args: - data: utf-8 data received from the server + Parameters + ---------- + data : str + utf-8 data received from the server + + Returns + ------- + Message | None + TypeAlias for multiple Message children + HelloMessage | NotificationMessage | RegisterMessage | UnregisterMessage + + Raises + ------ + ValidationError | JSONDecodeError """ recv_time: float = time.perf_counter() exception: str | None = None @@ -304,11 +328,16 @@ def send_ack(self, ws: WebSocket, channel_id: str, version: str) -> None: After sending a notification, the client must also send an 'ack' to the server to confirm receipt. - Args: - ws: WebSocket class object - channel_id: Notification message channel ID - version: Notification message version - Raises: + Parameters + ---------- + ws: WebSocket + channel_id : str + Notification message channel ID + version : str + Notification message version + + Raises + ------ WebSocketException: Error raised by the WebSocket client """ message_type: str = "ack" @@ -324,10 +353,15 @@ def send_hello(self, ws: WebSocket) -> None: Connections must say hello after connecting to the server, otherwise the connection is quickly dropped. - Args: - ws: WebSocket class object - Raises: - WebSocketException: Error raised by the WebSocket client + Parameters + ---------- + ws : WebSocket + Websocket class object + + Raises + ------ + WebSocketException + Error raised by the WebSocket client """ message_type: str = "hello" data: dict[str, Any] = dict( @@ -339,14 +373,18 @@ def send_hello(self, ws: WebSocket) -> None: self.hello_record = HelloRecord(send_time=time.perf_counter()) self.send(ws, message_type, data) - def send_register(self, ws: WebSocket, channel_id: str) -> None: + def send_register(self, ws: WebSocket, channel_id: str): """Send a 'register' message to Autopush. - Args: - ws: WebSocket class object - channel_id: Notification message channel ID - Raises: - WebSocketException: Error raised by the WebSocket client + Parameters + ---------- + ws : WebSocket + channel_id : str + Notification message channel ID + + Raises + ------ + WebSocketException """ message_type: str = "register" data: dict[str, Any] = dict(messageType=message_type, channelID=channel_id) @@ -357,11 +395,15 @@ def send_register(self, ws: WebSocket, channel_id: str) -> None: def send_unregister(self, ws: WebSocketApp, channel_id: str) -> None: """Send an 'unregister' message to Autopush. - Args: - ws: WebSocket class object - channel_id: Notification message channel ID - Raises: - WebSocketException: Error raised by the WebSocket client + Parameters + ---------- + ws : WebSocket + channel_id : str + Notification message channel ID + + Raises + ------ + WebSocketException """ message_type: str = "unregister" data: dict[str, Any] = dict(messageType=message_type, channelID=channel_id) @@ -372,12 +414,17 @@ def send_unregister(self, ws: WebSocketApp, channel_id: str) -> None: def send(self, ws: WebSocket | WebSocketApp, message_type: str, data: dict[str, Any]) -> None: """Send a message to Autopush. - Args: - ws: WebSocket class object - message_type: Message type. Examples: 'ack', 'hello', 'register' or 'unregister' - data: Message data - Raises: - WebSocketException: Error raised by the WebSocket client + Parameters + ---------- + ws : WebSocket + message_type : str + Examples: 'ack', 'hello', 'register' or 'unregister' + data : dict[str, Any] + Message data + + Raises + ------ + WebSocketException """ try: ws.send(json.dumps(data)) From dbef84c4048ef43deab300f7af4776434d0f7fb1 Mon Sep 17 00:00:00 2001 From: Taddes Date: Tue, 30 Jan 2024 17:59:23 -0500 Subject: [PATCH 09/12] review comments for clearer test functions --- .../integration/test_integration_all_rust.py | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/tests/integration/test_integration_all_rust.py b/tests/integration/test_integration_all_rust.py index 250dcad7b..9e721e41c 100644 --- a/tests/integration/test_integration_all_rust.py +++ b/tests/integration/test_integration_all_rust.py @@ -224,7 +224,7 @@ def __getattribute__(self, name: str): return f def connect(self, connection_port: int | None = None): - """Connect.""" + """Establish a websocket connection to localhost at the provided `connection_port`.""" url = self.url if connection_port: # pragma: nocover url = "ws://localhost:{}/".format(connection_port) @@ -264,7 +264,7 @@ def hello(self, uaid: str | None = None, services: list[str] | None = None): return result def broadcast_subscribe(self, services: list[str]): - """Broadcast subscribe.""" + """Broadcast WebSocket subscribe.""" if not self.ws: raise Exception("WebSocket client not available as expected") @@ -432,7 +432,7 @@ def ack(self, channel, version): self.ws.send(msg) def disconnect(self): - """Disconnect""" + """Disconnect from the application websocket.""" self.ws.close() def sleep(self, duration: int): # pragma: nocover @@ -480,7 +480,12 @@ def _get_vapid( def enqueue_output(out, queue): +<<<<<<< HEAD for line in iter(out.readline, ""): +======= + """Add lines from the out buffer to the provided queue.""" + for line in iter(out.readline, b""): +>>>>>>> f775454 (review comments for clearer test functions) queue.put(line) out.close() @@ -532,7 +537,7 @@ def max_logs(endpoint=None, conn=None): """ def max_logs_decorator(func): - """Decorate max_logs.""" + """Overwrite `max_endpoint_logs` with a given endpoint if it is specified.""" def wrapper(self, *args, **kwargs): if endpoint is not None: @@ -566,7 +571,7 @@ class CustomClient(Client): """Custom Client for testing.""" def send_bad_data(self): - """Set `bad-data`""" + """Send an invalid data message via websocket to the autoconnect client.""" self.ws.send("bad-data") @@ -583,7 +588,9 @@ def kill_process(process): def get_rust_binary_path(binary): - """Get Rust binary path.""" + """Get path to pre-built Rust binary. + This presumes that the application has already been built with proper features. + """ global STRICT_LOG_COUNTS rust_bin = root_dir + "/target/release/{}".format(binary) @@ -603,7 +610,7 @@ def get_rust_binary_path(binary): def write_config_to_env(config, prefix): - """Write configurations to env.""" + """Write configurations to application read environment variables.""" for key, val in config.items(): new_key = prefix + key log.debug("✍ config {} => {}".format(new_key, val)) @@ -958,7 +965,7 @@ def test_hello_echo(self): @inlineCallbacks def test_hello_with_bad_prior_uaid(self): - """Test hello with bard prior uaid.""" + """Test hello with bad prior uaid.""" non_uaid = uuid.uuid4().hex client = Client(self._ws_url) yield client.connect() @@ -970,7 +977,7 @@ def test_hello_with_bad_prior_uaid(self): @inlineCallbacks def test_basic_delivery(self): - """Test basic delivery.""" + """Test basic regular push message delivery.""" data = str(uuid.uuid4()) client: Client = yield self.quick_register() result = yield client.send_notification(data=data) @@ -983,7 +990,7 @@ def test_basic_delivery(self): @inlineCallbacks def test_topic_basic_delivery(self): - """Test topic basic delivery.""" + """Test basic topic push message delivery.""" data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data, topic="Inbox") @@ -996,7 +1003,7 @@ def test_topic_basic_delivery(self): @inlineCallbacks def test_topic_replacement_delivery(self): - """Test topic replacement delivery.""" + """Test that a topic push message replaces it's prior version.""" data = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() @@ -1045,7 +1052,7 @@ def test_topic_no_delivery_on_reconnect(self): @inlineCallbacks def test_basic_delivery_with_vapid(self): - """Test basic delivery with vapid.""" + """Test delivery of a basic push message with a VAPID header.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid(payload=self.vapid_payload) @@ -1059,7 +1066,7 @@ def test_basic_delivery_with_vapid(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid(self): - """Test basic delivery with invalid vapid.""" + """Test basic delivery with invalid VAPID header.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid(payload=self.vapid_payload, endpoint=self.host_endpoint(client)) @@ -1069,7 +1076,7 @@ def test_basic_delivery_with_invalid_vapid(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid_exp(self): - """Test basic delivery with invalid vapid exp.""" + """Test basic delivery of a push message with invalid VAPID `exp` assertion.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid( @@ -1520,7 +1527,7 @@ def test_delete_saved_notification(self): @inlineCallbacks def test_with_key(self): - """Test with key.""" + """Test getting a locked subscription with a valid VAPID public key.""" private_key = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p) claims = { "aud": "http://localhost:{}".format(ENDPOINT_PORT), From 8ec18db5daf3837b7b2beed2c2a9bc0fdfed6136 Mon Sep 17 00:00:00 2001 From: Taddes Date: Tue, 30 Jan 2024 18:08:57 -0500 Subject: [PATCH 10/12] post rebase comment addition --- tests/integration/test_integration_all_rust.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/integration/test_integration_all_rust.py b/tests/integration/test_integration_all_rust.py index 9e721e41c..807db090a 100644 --- a/tests/integration/test_integration_all_rust.py +++ b/tests/integration/test_integration_all_rust.py @@ -480,12 +480,8 @@ def _get_vapid( def enqueue_output(out, queue): -<<<<<<< HEAD - for line in iter(out.readline, ""): -======= """Add lines from the out buffer to the provided queue.""" - for line in iter(out.readline, b""): ->>>>>>> f775454 (review comments for clearer test functions) + for line in iter(out.readline, ""): queue.put(line) out.close() From 9be84a7614445b296b97c44bda869a5dafdf592e Mon Sep 17 00:00:00 2001 From: Taddes Date: Thu, 1 Feb 2024 10:11:07 -0500 Subject: [PATCH 11/12] review tweaks to docstrings --- tests/load/locustfiles/load.py | 14 +++++++------- tests/load/locustfiles/locustfile.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/load/locustfiles/load.py b/tests/load/locustfiles/load.py index a196ef345..572ed685b 100644 --- a/tests/load/locustfiles/load.py +++ b/tests/load/locustfiles/load.py @@ -68,13 +68,13 @@ def tick(self) -> TickTuple | None: ------- TickTuple | None - Distribution Parameters - ----------------------- - user_count : Total user count - spawn_rate : Number of users to start/stop per second when changing - number of users - user_classes : None or a List of user classes to be spawned - None : Instruction to stop the load test + TickTuple Contained Parameters + user_count: Total user count + spawn_rate: Number of users to start/stop per second when changing + number of users + user_classes: None or a List of user classes to be spawned + + None: Instruction to stop the load test """ run_time: int = self.get_run_time() if run_time > self.MAX_RUN_TIME: diff --git a/tests/load/locustfiles/locustfile.py b/tests/load/locustfiles/locustfile.py index 09f7dc700..00fcebea4 100644 --- a/tests/load/locustfiles/locustfile.py +++ b/tests/load/locustfiles/locustfile.py @@ -84,11 +84,11 @@ def wait_time(self): return self.environment.autopush_wait_time(self) def on_start(self) -> Any: - """Call when a websocket starts running.""" + """Call when a User starts running.""" self.ws_greenlet = gevent.spawn(self.connect) def on_stop(self) -> Any: - """Call when a websocket stops running.""" + """Call when a User stops running.""" if self.ws: for channel_id in self.channels.keys(): self.send_unregister(self.ws, channel_id) From dbee9930f21af594f6ee5cdac8c60525da86a7cb Mon Sep 17 00:00:00 2001 From: Taddes Date: Sat, 3 Feb 2024 21:57:58 -0500 Subject: [PATCH 12/12] revised integration tests docstrings after review --- .../integration/test_integration_all_rust.py | 92 +++++++++++++------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/tests/integration/test_integration_all_rust.py b/tests/integration/test_integration_all_rust.py index 807db090a..8200fa529 100644 --- a/tests/integration/test_integration_all_rust.py +++ b/tests/integration/test_integration_all_rust.py @@ -273,7 +273,9 @@ def broadcast_subscribe(self, services: list[str]): self.ws.send(msg) def register(self, chid: str | None = None, key=None, status=200): - """Register.""" + """Register a new endpoint for the provided ChannelID. + Optionally locked to the provided VAPID Public key. + """ if not self.ws: raise Exception("WebSocket client not available as expected") @@ -291,7 +293,7 @@ def register(self, chid: str | None = None, key=None, status=200): return result def unregister(self, chid): - """Unregister.""" + """Unregister the ChannelID, which should invalidate the associated Endpoint.""" msg = json.dumps(dict(messageType="unregister", channelID=chid)) log.debug("Send: %s", msg) self.ws.send(msg) @@ -454,7 +456,9 @@ def _get_vapid( payload: dict[str, str | int] | None = None, endpoint: str | None = None, ) -> dict[str, str | bytes]: - """Get vapid key.""" + """Get VAPID information, including the `Authorization` header string, + public and private keys. + """ global CONNECTION_CONFIG if endpoint is None: @@ -648,7 +652,7 @@ def setup_bt(): def setup_dynamodb(): - """Set up DynamoDB.""" + """Set up DynamoDB emulator.""" global DDB_PROCESS log.debug("🐍🟢 Starting dynamodb") @@ -867,7 +871,9 @@ def host_endpoint(self, client): @inlineCallbacks def quick_register(self): - """Quick register.""" + """Perform a connection initialization, which includes a new connection, + `hello`, and channel registration. + """ log.debug("🐍#### Connecting to ws://localhost:{}/".format(CONNECTION_PORT)) client = Client("ws://localhost:{}/".format(CONNECTION_PORT)) yield client.connect() @@ -1022,7 +1028,7 @@ def test_topic_replacement_delivery(self): @inlineCallbacks @max_logs(conn=4) def test_topic_no_delivery_on_reconnect(self): - """Test topic no delivery on reconnect.""" + """Test that a topic message does not attempt to redeliver on reconnect.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1088,7 +1094,7 @@ def test_basic_delivery_with_invalid_vapid_exp(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid_auth(self): - """Test basic delivery with invalid vapid auth.""" + """Test basic delivery with invalid VAPID auth.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid( @@ -1101,7 +1107,7 @@ def test_basic_delivery_with_invalid_vapid_auth(self): @inlineCallbacks def test_basic_delivery_with_invalid_signature(self): - """Test basic delivery with invalid signature.""" + """Test that a basic delivery with invalid VAPID signature fails.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid( @@ -1116,7 +1122,7 @@ def test_basic_delivery_with_invalid_signature(self): @inlineCallbacks def test_basic_delivery_with_invalid_vapid_ckey(self): - """Test basic delivery with invalid vapid ckey.""" + """Test that basic delivery with invalid VAPID crypto-key fails.""" data = str(uuid.uuid4()) client = yield self.quick_register() vapid_info = _get_vapid(payload=self.vapid_payload, endpoint=self.host_endpoint(client)) @@ -1126,7 +1132,7 @@ def test_basic_delivery_with_invalid_vapid_ckey(self): @inlineCallbacks def test_delivery_repeat_without_ack(self): - """Test delivery repeat without ack.""" + """Test that message delivery repeats if the client does not acknowledge messages.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1148,7 +1154,9 @@ def test_delivery_repeat_without_ack(self): @inlineCallbacks def test_repeat_delivery_with_disconnect_without_ack(self): - """Test repeat delivery with disconnect without ack.""" + """Test that message delivery repeats if the client disconnects + without acknowledging the message. + """ data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data) @@ -1164,7 +1172,9 @@ def test_repeat_delivery_with_disconnect_without_ack(self): @inlineCallbacks def test_multiple_delivery_repeat_without_ack(self): - """Test multiple delivery repeat without ack.""" + """Test that the server will always try to deliver messages + until the client acknowledges them. + """ data = str(uuid.uuid4()) data2 = str(uuid.uuid4()) client = yield self.quick_register() @@ -1194,7 +1204,7 @@ def test_multiple_delivery_repeat_without_ack(self): @inlineCallbacks def test_topic_expired(self): - """Test topic expired.""" + """Test that the server will not deliver a message topic that has expired.""" data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1213,7 +1223,11 @@ def test_topic_expired(self): @inlineCallbacks @max_logs(conn=4) def test_multiple_delivery_with_single_ack(self): - """Test multiple delivery with single ack.""" + """Test that the server provides the right unacknowledged messages + if the client only acknowledges one of the received messages. + Note: the `data` fields are constructed so that they return + `FirstMessage` and `OtherMessage`, which may be useful for debugging. + """ data = b"\x16*\xec\xb4\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() data2 = b":\xd8^\xac\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() client = yield self.quick_register() @@ -1254,7 +1268,11 @@ def test_multiple_delivery_with_single_ack(self): @inlineCallbacks def test_multiple_delivery_with_multiple_ack(self): - """Test multiple delivery with multiple ack.""" + """Test that the server provides the no additional unacknowledged messages + if the client acknowledges both of the received messages. + Note: the `data` fields are constructed so that they return + `FirstMessage` and `OtherMessage`, which may be useful for debugging. + """ data = b"\x16*\xec\xb4\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() # "FirstMessage" data2 = b":\xd8^\xac\xc7\xac\xb1\xa8\x1e" + str(uuid.uuid4()).encode() # "OtherMessage" client = yield self.quick_register() @@ -1283,7 +1301,7 @@ def test_multiple_delivery_with_multiple_ack(self): @inlineCallbacks def test_no_delivery_to_unregistered(self): - """Test no delivery to unregistered.""" + """Test that the server does not try to deliver to unregistered channel IDs.""" data = str(uuid.uuid4()) client: Client = yield self.quick_register() assert client.channels @@ -1305,7 +1323,7 @@ def test_no_delivery_to_unregistered(self): @inlineCallbacks def test_ttl_0_connected(self): - """Test TTL 0 connected.""" + """Test that a message with a TTL=0 is delivered to a client that is actively connected.""" data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data, ttl=0) @@ -1319,7 +1337,9 @@ def test_ttl_0_connected(self): @inlineCallbacks def test_ttl_0_not_connected(self): - """Test TTL 0 not connected.""" + """Test that a message with a TTL=0 and a recipient client that is not connected, + is not delivered when the client reconnects. + """ data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1332,7 +1352,9 @@ def test_ttl_0_not_connected(self): @inlineCallbacks def test_ttl_expired(self): - """Test TTL expired.""" + """Test that messages with a TTL that has expired are not delivered + to a recipient client. + """ data = str(uuid.uuid4()) client = yield self.quick_register() yield client.disconnect() @@ -1347,7 +1369,10 @@ def test_ttl_expired(self): @inlineCallbacks @max_logs(endpoint=28) def test_ttl_batch_expired_and_good_one(self): - """Test TTL batch expired with one good result.""" + """Test that if a batch of messages are received while the recipient is offline, + only messages that have not expired are sent to the recipient. + This test checks if the latest pending message is not expired. + """ data = str(uuid.uuid4()).encode() data2 = base64.urlsafe_b64decode("0012") + str(uuid.uuid4()).encode() print(data2) @@ -1375,7 +1400,10 @@ def test_ttl_batch_expired_and_good_one(self): @inlineCallbacks @max_logs(endpoint=28) def test_ttl_batch_partly_expired_and_good_one(self): - """Test TTL batch partly expired with one good result.""" + """Test that if a batch of messages are received while the recipient is offline, + only messages that have not expired are sent to the recipient. + This test checks if there is an equal mix of expired and unexpired messages. + """ data = str(uuid.uuid4()) data1 = str(uuid.uuid4()) data2 = str(uuid.uuid4()) @@ -1412,7 +1440,7 @@ def test_ttl_batch_partly_expired_and_good_one(self): @inlineCallbacks def test_message_without_crypto_headers(self): - """Test message without crypto headers.""" + """Test that a message without crypto headers, but has data is not accepted.""" data = str(uuid.uuid4()) client = yield self.quick_register() result = yield client.send_notification(data=data, use_header=False, status=400) @@ -1421,7 +1449,7 @@ def test_message_without_crypto_headers(self): @inlineCallbacks def test_empty_message_without_crypto_headers(self): - """Test empty message without crypto headers.""" + """Test that a message without crypto headers, and does not have data, is accepted.""" client = yield self.quick_register() result = yield client.send_notification(use_header=False) assert result is not None @@ -1444,7 +1472,9 @@ def test_empty_message_without_crypto_headers(self): @inlineCallbacks def test_empty_message_with_crypto_headers(self): - """Test empty message with crypto headers.""" + """Test that an empty message with crypto headers does not send either `headers` + or `data` as part of the incoming websocket `notification` message. + """ client = yield self.quick_register() result = yield client.send_notification() assert result is not None @@ -1551,7 +1581,7 @@ def test_with_key(self): @inlineCallbacks def test_with_bad_key(self): - """Test with bad key.""" + """Test that a message registration request with bad VAPID public key is rejected.""" chid = str(uuid.uuid4()) client = Client("ws://localhost:{}/".format(CONNECTION_PORT)) yield client.connect() @@ -1564,7 +1594,7 @@ def test_with_bad_key(self): @inlineCallbacks @max_logs(endpoint=44) def test_msg_limit(self): - """Test message limit.""" + """Test that sent messages that are larger than our size limit are rejected.""" client = yield self.quick_register() uaid = client.uaid yield client.disconnect() @@ -1585,7 +1615,7 @@ def test_msg_limit(self): @inlineCallbacks def test_can_ping(self): - """Test can ping.""" + """Test that the client can send an active ping message and get a valid response.""" client = yield self.quick_register() yield client.ping() assert client.ws.connected @@ -1655,7 +1685,7 @@ def _ws_url(self): @inlineCallbacks def test_broadcast_update_on_connect(self): - """Test broadcast update on connect.""" + """Test that the client receives any pending broadcast updates on connect.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear() @@ -1680,7 +1710,9 @@ def test_broadcast_update_on_connect(self): @inlineCallbacks def test_broadcast_update_on_connect_with_errors(self): - """Test broadcast update on connect with errors.""" + """Test that the client can receive broadcast updates on connect + that may have produced internal errors. + """ global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear() @@ -1698,7 +1730,7 @@ def test_broadcast_update_on_connect_with_errors(self): @inlineCallbacks def test_broadcast_subscribe(self): - """Test broadcast subscribe.""" + """Test that the client can subscribe to new broadcasts.""" global MOCK_MP_SERVICES MOCK_MP_SERVICES = {"kinto:123": "ver1"} MOCK_MP_POLLED.clear()