diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..b5b4406 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,9 @@ +name: Tests + +on: + pull_request: + +jobs: + unit-tests: + uses: canonical/operator-workflows/.github/workflows/test.yaml@cea2ac306b4f4c1475d73b1a4c766d62e5b1c8a9 + secrets: inherit diff --git a/.licenserc.yaml b/.licenserc.yaml new file mode 100644 index 0000000..5aa32de --- /dev/null +++ b/.licenserc.yaml @@ -0,0 +1,37 @@ +header: + license: + spdx-id: Apache-2.0 + copyright-owner: Canonical Ltd. + copyright-year: 2024 + content: | + Copyright [year] [owner] + See LICENSE file for licensing details. + paths: + - '**' + paths-ignore: + - '.github/**' + - '**/.gitkeep' + - '**/*.cfg' + - '**/*.conf' + - '**/*.j2' + - '**/*.json' + - '**/*.md' + - '**/*.rule' + - '**/*.tmpl' + - '**/*.txt' + - '**/*.jinja' + - '.codespellignore' + - '.dockerignore' + - '.flake8' + - '.jujuignore' + - '.gitignore' + - '.licenserc.yaml' + - '.trivyignore' + - '.woke.yaml' + - '.woke.yml' + - 'CODEOWNERS' + - 'icon.svg' + - 'LICENSE' + - 'trivy.yaml' + - 'lib/**' + comment: on-failure diff --git a/.woke.yaml b/.woke.yaml new file mode 100644 index 0000000..ddc3772 --- /dev/null +++ b/.woke.yaml @@ -0,0 +1,7 @@ +ignore_files: + # Ignore ingress charm library as it uses non compliant terminology: + # whitelist. + - lib/charms/nginx_ingress_integrator/v0/nginx_route.py +rules: + # Ignore "master" - the database relation event received from the library. + - name: master diff --git a/charmcraft.yaml b/charmcraft.yaml index f432273..ddfdc4d 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -1,3 +1,6 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + # This file configures Charmcraft. # See https://juju.is/docs/sdk/charmcraft-config for guidance. diff --git a/pyproject.toml b/pyproject.toml index e10531c..724f93d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,11 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + +[tool.bandit] +exclude_dirs = ["/venv/"] +[tool.bandit.assert_used] +skips = ["*/*test.py", "*/test_*.py", "*tests/*.py"] + # Testing tools configuration [tool.coverage.run] branch = true @@ -11,36 +19,26 @@ log_cli_level = "INFO" # Formatting tools configuration [tool.black] -line-length = 99 +line-length = 120 target-version = ["py38"] -# Linting tools configuration -[tool.ruff] -line-length = 99 -select = ["E", "W", "F", "C", "N", "D", "I001"] -extend-ignore = [ - "D203", - "D204", - "D213", - "D215", - "D400", - "D404", - "D406", - "D407", - "D408", - "D409", - "D413", -] -ignore = ["E501", "D107"] -extend-exclude = ["__pycache__", "*.egg_info"] -per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]} +[tool.isort] +profile = "black" -[tool.ruff.mccabe] +# Linting tools configuration +[tool.flake8] +max-line-length = 120 +max-doc-length = 99 max-complexity = 10 - -[tool.codespell] -skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage" - -[tool.pyright] -include = ["src/**.py"] - +exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] +select = ["E", "W", "F", "C", "N", "R", "D", "H"] +# Ignore W503, E501 because using black creates errors with this +# Ignore D107 Missing docstring in __init__ +ignore = ["W503", "E501", "D107"] +# D100, D101, D102, D103: Ignore missing docstrings in tests +per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] +docstring-convention = "google" +# Check for properly formatted copyright header in each file +copyright-check = "True" +copyright-author = "Canonical Ltd." +copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" diff --git a/src/charm.py b/src/charm.py index 8c10c48..391f6a9 100755 --- a/src/charm.py +++ b/src/charm.py @@ -7,11 +7,12 @@ import logging from charms.nginx_ingress_integrator.v0.nginx_route import require_nginx_route -from log import log_event_handler from ops import main, pebble from ops.charm import CharmBase from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus from ops.pebble import CheckStatus + +from log import log_event_handler from relations.airbyte_server import AirbyteServer from state import State @@ -23,7 +24,12 @@ class AirbyteUIK8sOperatorCharm(CharmBase): - """Charm the application.""" + """Airbyte UI charm. + + Attrs: + _state: used to store data that is persisted across invocations. + external_hostname: DNS listing used for external connections. + """ @property def external_hostname(self): @@ -31,6 +37,11 @@ def external_hostname(self): return self.config["external-hostname"] or self.app.name def __init__(self, *args): + """Construct. + + Args: + args: Ignore. + """ super().__init__(*args) self._state = State(self.app, lambda: self.model.get_relation("peer")) @@ -59,7 +70,11 @@ def _require_nginx_route(self): @log_event_handler(logger) def _on_pebble_ready(self, event): - """Handle pebble-ready event.""" + """Handle pebble ready event. + + Args: + event: The event triggered when the relation changed. + """ self._update(event) @log_event_handler(logger) @@ -78,6 +93,11 @@ def _on_update_status(self, event): Args: event: The `update-status` event triggered at intervals. """ + try: + self._validate() + except ValueError: + return + container = self.unit.get_container(self.name) valid_pebble_plan = self._validate_pebble_plan(container) if not valid_pebble_plan: @@ -154,8 +174,10 @@ def _update(self, event): context = { "API_URL": "/api/v1/", "AIRBYTE_EDITION": "community", + "AIRBYTE_SERVER_HOST": f"{server_svc}:{INTERNAL_API_PORT}", "INTERNAL_API_HOST": f"{server_svc}:{INTERNAL_API_PORT}", "CONNECTOR_BUILDER_API_HOST": f"{server_svc}:{CONNECTOR_BUILDER_API_PORT}", + "CONNECTOR_BUILDER_API_URL": "/connector-builder-api", "KEYCLOAK_INTERNAL_HOST": "localhost", } diff --git a/src/relations/airbyte_server.py b/src/relations/airbyte_server.py index 8a9d38a..f63330c 100644 --- a/src/relations/airbyte_server.py +++ b/src/relations/airbyte_server.py @@ -5,9 +5,10 @@ import logging -from log import log_event_handler from ops import framework +from log import log_event_handler + logger = logging.getLogger(__name__) @@ -22,15 +23,9 @@ def __init__(self, charm): """ super().__init__(charm, "airbyte-server") self.charm = charm - charm.framework.observe( - charm.on.airbyte_server_relation_joined, self._on_airbyte_server_relation_changed - ) - charm.framework.observe( - charm.on.airbyte_server_relation_changed, self._on_airbyte_server_relation_changed - ) - charm.framework.observe( - charm.on.airbyte_server_relation_broken, self._on_airbyte_server_relation_broken - ) + charm.framework.observe(charm.on.airbyte_server_relation_joined, self._on_airbyte_server_relation_changed) + charm.framework.observe(charm.on.airbyte_server_relation_changed, self._on_airbyte_server_relation_changed) + charm.framework.observe(charm.on.airbyte_server_relation_broken, self._on_airbyte_server_relation_broken) @log_event_handler(logger) def _on_airbyte_server_relation_changed(self, event): diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index e8b264c..77619c4 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -1,7 +1,8 @@ -#!/usr/bin/env python3 -# Copyright 2024 Ali +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. +"""Charm integration tests.""" + import asyncio import logging from pathlib import Path @@ -29,7 +30,5 @@ async def test_build_and_deploy(ops_test: OpsTest): # Deploy the charm and wait for active/idle status await asyncio.gather( ops_test.model.deploy(charm, resources=resources, application_name=APP_NAME), - ops_test.model.wait_for_idle( - apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 - ), + ops_test.model.wait_for_idle(apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000), ) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..9902dfe --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. + + +"""Unit tests config.""" + +import ops.testing + +ops.testing.SIMULATE_CAN_CONNECT = True diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 15443a7..aea0e2f 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -1,68 +1,272 @@ -# Copyright 2024 Ali +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. # # Learn more about testing at: https://juju.is/docs/sdk/testing -import unittest -import ops -import ops.testing -from charm import AirbyteUiK8SOperatorCharm +"""Charm unit tests.""" +# pylint:disable=protected-access + +from unittest import TestCase, mock + +from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus +from ops.pebble import CheckStatus +from ops.testing import Harness + +from charm import AirbyteUIK8sOperatorCharm +from src.charm import CONNECTOR_BUILDER_API_PORT, INTERNAL_API_PORT, WEB_UI_PORT + +APP_NAME = "airbyte-webapp" +mock_incomplete_pebble_plan = {"services": {"airbyte-webapp": {"override": "replace"}}} + + +class TestCharm(TestCase): + """Unit tests for charm. + + Attrs: + maxDiff: Specifies max difference shown by failed tests. + """ + + maxDiff = None -class TestCharm(unittest.TestCase): def setUp(self): - self.harness = ops.testing.Harness(AirbyteUiK8SOperatorCharm) + """Create setup for the unit tests.""" + self.harness = Harness(AirbyteUIK8sOperatorCharm) self.addCleanup(self.harness.cleanup) + self.harness.set_can_connect(APP_NAME, True) + self.harness.set_leader(True) + self.harness.set_model_name("airbyte-model") self.harness.begin() - def test_httpbin_pebble_ready(self): - # Expected plan after Pebble ready with default config - expected_plan = { + def test_initial_plan(self): + """The initial pebble plan is empty.""" + initial_plan = self.harness.get_container_pebble_plan(APP_NAME).to_dict() + self.assertEqual(initial_plan, {}) + + def test_blocked_by_peer_relation_not_ready(self): + """The charm is blocked without a peer relation.""" + harness = self.harness + + # Simulate pebble readiness. + container = harness.model.unit.get_container(APP_NAME) + harness.charm.on.airbyte_webapp_pebble_ready.emit(container) + + # No plans are set yet. + got_plan = harness.get_container_pebble_plan(APP_NAME).to_dict() + self.assertEqual(got_plan, {}) + + # The BlockStatus is set with a message. + self.assertEqual(harness.model.unit.status, BlockedStatus("peer relation not ready")) + + def test_ingress(self): + """The charm relates correctly to the nginx ingress charm and can be configured.""" + harness = self.harness + + simulate_lifecycle(harness) + + nginx_route_relation_id = harness.add_relation("nginx-route", "ingress") + harness.charm._require_nginx_route() + + assert harness.get_relation_data(nginx_route_relation_id, harness.charm.app) == { + "service-namespace": harness.charm.model.name, + "service-hostname": harness.charm.app.name, + "service-name": harness.charm.app.name, + "service-port": str(WEB_UI_PORT), + "tls-secret-name": "airbyte-tls", + "backend-protocol": "HTTP", + } + + def test_ingress_update_hostname(self): + """The charm relates correctly to the nginx ingress charm and can be configured.""" + harness = self.harness + + simulate_lifecycle(harness) + + nginx_route_relation_id = harness.add_relation("nginx-route", "ingress") + + new_hostname = "new-airbyte-ui-k8s" + harness.update_config({"external-hostname": new_hostname}) + harness.charm._require_nginx_route() + + assert harness.get_relation_data(nginx_route_relation_id, harness.charm.app) == { + "service-namespace": harness.charm.model.name, + "service-hostname": new_hostname, + "service-name": harness.charm.app.name, + "service-port": str(WEB_UI_PORT), + "tls-secret-name": "airbyte-tls", + "backend-protocol": "HTTP", + } + + def test_ingress_update_tls(self): + """The charm relates correctly to the nginx ingress charm and can be configured.""" + harness = self.harness + + simulate_lifecycle(harness) + + nginx_route_relation_id = harness.add_relation("nginx-route", "ingress") + + new_tls = "new-tls" + harness.update_config({"tls-secret-name": new_tls}) + harness.charm._require_nginx_route() + + assert harness.get_relation_data(nginx_route_relation_id, harness.charm.app) == { + "service-namespace": harness.charm.model.name, + "service-hostname": harness.charm.app.name, + "service-name": harness.charm.app.name, + "service-port": str(WEB_UI_PORT), + "tls-secret-name": new_tls, + "backend-protocol": "HTTP", + } + + def test_ready(self): + """The pebble plan is correctly generated when the charm is ready.""" + harness = self.harness + + simulate_lifecycle(harness) + + # The plan is generated after pebble is ready. + want_plan = { "services": { - "httpbin": { - "override": "replace", - "summary": "httpbin", - "command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent", + APP_NAME: { + "summary": APP_NAME, + "command": "./docker-entrypoint.sh nginx", "startup": "enabled", - "environment": {"GUNICORN_CMD_ARGS": "--log-level info"}, + "override": "replace", + "environment": { + "API_URL": "/api/v1/", + "AIRBYTE_EDITION": "community", + "AIRBYTE_SERVER_HOST": "airbyte-k8s:8001", + "CONNECTOR_BUILDER_API_URL": "/connector-builder-api", + "INTERNAL_API_HOST": f"airbyte-k8s:{INTERNAL_API_PORT}", + "CONNECTOR_BUILDER_API_HOST": f"airbyte-k8s:{CONNECTOR_BUILDER_API_PORT}", + "KEYCLOAK_INTERNAL_HOST": "localhost", + }, + "on-check-failure": {"up": "ignore"}, + } + }, + "checks": { + "up": { + "override": "replace", + "period": "10s", + "http": {"url": f"http://localhost:{WEB_UI_PORT}"}, } }, } - # Simulate the container coming up and emission of pebble-ready event - self.harness.container_pebble_ready("httpbin") - # Get the plan now we've run PebbleReady - updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict() - # Check we've got the plan we expected - self.assertEqual(expected_plan, updated_plan) - # Check the service was started - service = self.harness.model.unit.get_container("httpbin").get_service("httpbin") + + got_plan = harness.get_container_pebble_plan(APP_NAME).to_dict() + self.assertEqual(got_plan, want_plan) + + # The service was started. + service = harness.model.unit.get_container(APP_NAME).get_service(APP_NAME) self.assertTrue(service.is_running()) - # Ensure we set an ActiveStatus with no message - self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus()) - - def test_config_changed_valid_can_connect(self): - # Ensure the simulated Pebble API is reachable - self.harness.set_can_connect("httpbin", True) - # Trigger a config-changed event with an updated value - self.harness.update_config({"log-level": "debug"}) - # Get the plan now we've run PebbleReady - updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict() - updated_env = updated_plan["services"]["httpbin"]["environment"] - # Check the config change was effective - self.assertEqual(updated_env, {"GUNICORN_CMD_ARGS": "--log-level debug"}) - self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus()) - - def test_config_changed_valid_cannot_connect(self): - # Trigger a config-changed event with an updated value - self.harness.update_config({"log-level": "debug"}) - # Check the charm is in WaitingStatus - self.assertIsInstance(self.harness.model.unit.status, ops.WaitingStatus) - - def test_config_changed_invalid(self): - # Ensure the simulated Pebble API is reachable - self.harness.set_can_connect("httpbin", True) - # Trigger a config-changed event with an updated value - self.harness.update_config({"log-level": "foobar"}) - # Check the charm is in BlockedStatus - self.assertIsInstance(self.harness.model.unit.status, ops.BlockedStatus) + + def test_update_status_up(self): + """The charm updates the unit status to active based on UP status.""" + harness = self.harness + + simulate_lifecycle(harness) + + container = harness.model.unit.get_container(APP_NAME) + container.get_check = mock.Mock(status="up") + container.get_check.return_value.status = CheckStatus.UP + harness.charm.on.update_status.emit() + + self.assertEqual(harness.model.unit.status, ActiveStatus()) + + def test_update_status_down(self): + """The charm updates the unit status to maintenance based on DOWN status.""" + harness = self.harness + + simulate_lifecycle(harness) + + container = harness.model.unit.get_container(APP_NAME) + container.get_check = mock.Mock(status="up") + container.get_check.return_value.status = CheckStatus.DOWN + harness.charm.on.update_status.emit() + + self.assertEqual(harness.model.unit.status, MaintenanceStatus("Status check: DOWN")) + + def test_incomplete_pebble_plan(self): + """The charm re-applies the pebble plan if incomplete.""" + harness = self.harness + simulate_lifecycle(harness) + + container = harness.model.unit.get_container(APP_NAME) + container.add_layer(APP_NAME, mock_incomplete_pebble_plan, combine=True) + harness.charm.on.update_status.emit() + + self.assertEqual( + harness.model.unit.status, + MaintenanceStatus("replanning application"), + ) + plan = harness.get_container_pebble_plan(APP_NAME).to_dict() + assert plan != mock_incomplete_pebble_plan + + @mock.patch("charm.AirbyteUIK8sOperatorCharm._validate_pebble_plan", return_value=True) + def test_missing_pebble_plan(self, mock_validate_pebble_plan): + """The charm re-applies the pebble plan if missing.""" + harness = self.harness + simulate_lifecycle(harness) + + mock_validate_pebble_plan.return_value = False + harness.charm.on.update_status.emit() + self.assertEqual( + harness.model.unit.status, + MaintenanceStatus("replanning application"), + ) + plan = harness.get_container_pebble_plan(APP_NAME).to_dict() + assert plan is not None + + +def simulate_lifecycle(harness): + """Simulate a healthy charm life-cycle. + + Args: + harness: ops.testing.Harness object used to simulate charm lifecycle. + """ + # Simulate pebble readiness. + container = harness.model.unit.get_container(APP_NAME) + harness.charm.on.airbyte_webapp_pebble_ready.emit(container) + + # Simulate peer relation readiness. + harness.add_relation("peer", "airbyte") + + # Add the airbyte relation. + harness.add_relation("airbyte-server", "airbyte-k8s") + + # Simulate server readiness. + app = type("App", (), {"name": "airbyte-ui-k8s"})() + relation = type( + "Relation", + (), + { + "data": {app: {"server_status": "ready", "server_name": "airbyte-k8s"}}, + "name": "ui", + "id": 42, + }, + )() + unit = type("Unit", (), {"app": app, "name": "airbyte-ui-k8s/0"})() + event = type("Event", (), {"app": app, "relation": relation, "unit": unit})() + harness.charm.airbyte_server._on_airbyte_server_relation_changed(event) + + +def make_ui_changed_event(rel_name): + """Create and return a mock relation changed event. + + The event is generated by the relation with the given name. + + Args: + rel_name: Relationship name. + + Returns: + Event dict. + """ + return type( + "Event", + (), + { + "data": {"status": "ready", "name": "airbyte-k8s"}, + "relation": type("Relation", (), {"name": rel_name}), + }, + ) diff --git a/tests/unit/test_state.py b/tests/unit/test_state.py new file mode 100644 index 0000000..54ba55e --- /dev/null +++ b/tests/unit/test_state.py @@ -0,0 +1,70 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Learn more about testing at: https://juju.is/docs/sdk/testing + + +"""State unit tests.""" + +import json +from unittest import TestCase + +from state import State + + +class TestState(TestCase): + """Unit tests for state. + + Attrs: + maxDiff: Specifies max difference shown by failed tests. + """ + + maxDiff = None + + def test_get(self): + """It is possible to retrieve attributes from the state.""" + state = make_state({"foo": json.dumps("bar")}) + self.assertEqual(state.foo, "bar") + self.assertIsNone(state.bad) + + def test_set(self): + """It is possible to set attributes in the state.""" + data = {"foo": json.dumps("bar")} + state = make_state(data) + state.foo = 42 + state.list = [1, 2, 3] + self.assertEqual(state.foo, 42) + self.assertEqual(state.list, [1, 2, 3]) + self.assertEqual(data, {"foo": "42", "list": "[1, 2, 3]"}) + + def test_del(self): + """It is possible to unset attributes in the state.""" + data = {"foo": json.dumps("bar"), "answer": json.dumps(42)} + state = make_state(data) + del state.foo + self.assertIsNone(state.foo) + self.assertEqual(data, {"answer": "42"}) + # Deleting a name that is not set does not error. + del state.foo + + def test_is_ready(self): + """The state is not ready when it is not possible to get relations.""" + state = make_state({}) + self.assertTrue(state.is_ready()) + + state = State("myapp", lambda: None) + self.assertFalse(state.is_ready()) + + +def make_state(data): + """Create state object. + + Args: + data: Data to be included in state. + + Returns: + State object with data. + """ + app = "myapp" + rel = type("Rel", (), {"data": {app: data}})() + return State(app, lambda: rel) diff --git a/tox.ini b/tox.ini index 5313fa2..ca77096 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,12 @@ -# Copyright 2024 Ali +# Copyright 2024 Canonical Ltd. # See LICENSE file for licensing details. [tox] no_package = True skip_missing_interpreters = True -env_list = format, lint, static, unit +env_list = fmt, lint, static, unit, coverage-report min_version = 4.0.0 +max-line-length=120 [vars] src_path = {tox_root}/src @@ -23,28 +24,44 @@ pass_env = CHARM_BUILD_DIR MODEL_SETTINGS -[testenv:format] -description = Apply coding style standards to code +[testenv:fmt] +description = Format the code deps = - black - ruff + black==22.8.0 + isort==5.10.1 commands = - black {[vars]all_path} - ruff --fix {[vars]all_path} + isort {[vars]src_path} {[vars]tests_path} + black {[vars]src_path} {[vars]tests_path} [testenv:lint] -description = Check code against coding style standards +description = Lint the code deps = - black - ruff - codespell + mypy + pylint + pydocstyle + pytest + black==22.8.0 + codespell==2.2.1 + flake8==5.0.4 + flake8-builtins==1.5.3 + flake8-copyright==0.2.3 + flake8-docstrings==1.6.0 + isort==5.10.1 + pep8-naming==0.13.2 + pyproject-flake8==5.0.4.post1 + flake8-docstrings-complete>=1.0.3 + flake8-test-docs>=1.0 commands = - # if this charm owns a lib, uncomment "lib_path" variable - # and uncomment the following line - # codespell {[vars]lib_path} - codespell {tox_root} - ruff {[vars]all_path} - black --check --diff {[vars]all_path} + pydocstyle {[vars]src_path} + codespell {toxinidir} --skip {toxinidir}/.git --skip {toxinidir}/.tox \ + --skip {toxinidir}/build --skip {toxinidir}/lib --skip {toxinidir}/venv \ + --skip {toxinidir}/.mypy_cache --skip {toxinidir}/icon.svg + pflake8 {[vars]src_path} {[vars]tests_path} + isort --check-only --diff {[vars]src_path} {[vars]tests_path} + black --check --diff {[vars]src_path} {[vars]tests_path} + mypy {[vars]all_path} --ignore-missing-imports --follow-imports=skip --install-types --non-interactive + pylint {[vars]all_path} --disable=E0401,W1203,W0613,W0718,R0903,W1514,C0103,R0913,C0301,W0212,R0902,C0104,W0640,R0801,W0511,R0914,R0912 + [testenv:unit] description = Run unit tests @@ -62,21 +79,33 @@ commands = {[vars]tests_path}/unit coverage report +[testenv:coverage-report] +description = Create test coverage report +deps = + coverage[toml] + pytest + -r{toxinidir}/requirements.txt +commands = + coverage report + [testenv:static] -description = Run static type checks +description = Run static analysis tests deps = - pyright - -r {tox_root}/requirements.txt + bandit[toml] + -r{toxinidir}/requirements.txt commands = - pyright {posargs} + bandit -c {toxinidir}/pyproject.toml -r {[vars]src_path} {[vars]tests_path} [testenv:integration] description = Run integration tests deps = - pytest - juju - pytest-operator - -r {tox_root}/requirements.txt + ipdb==0.13.9 + juju==3.1.0.1 + pytest==7.1.3 + pytest-operator==0.35.0 + temporalio==1.1.0 + pytest-asyncio==0.21 + -r{toxinidir}/requirements.txt commands = pytest -v \ -s \