From d797cb2a06773195ea7f94b6ff2058a65d2c2fc7 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Mon, 9 Mar 2026 17:19:19 +1300 Subject: [PATCH 1/7] Add unit tests for custom charm functionality Use ops.testing (Scenario) with the proposed charmcraft extension autoloading from canonical/operator#2367 to test the charm's custom code on top of the paas-charm base. --- charm/tests/unit/test_charm.py | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 charm/tests/unit/test_charm.py diff --git a/charm/tests/unit/test_charm.py b/charm/tests/unit/test_charm.py new file mode 100644 index 0000000..c341664 --- /dev/null +++ b/charm/tests/unit/test_charm.py @@ -0,0 +1,66 @@ +# Copyright 2026 Canonical Ltd. +# See LICENSE file for licensing details. + +"""Unit tests for the dashboard charm's custom functionality.""" + +import pathlib +import sys + +import pytest + +CHARM_DIR = pathlib.Path(__file__).parents[2] +sys.path.insert(0, str(CHARM_DIR / "lib")) +sys.path.insert(0, str(CHARM_DIR / "src")) + +from ops import testing # noqa: E402 + +import charm # noqa: E402 + +_LOADDATA_CMD = ["python3", "manage.py", "loaddata", "initial_data.yaml"] + + +class TestLoadSampleDataAction: + """Test the load-sample-data action.""" + + def test_load_sample_data_runs_loaddata(self): + """load-sample-data executes Django loaddata command.""" + ctx = testing.Context(charm.DashboardCharm) + container = testing.Container( + "django-app", + can_connect=True, + execs={testing.Exec(_LOADDATA_CMD)}, + ) + state = testing.State(containers={container}, leader=True) + + ctx.run(ctx.on.action("load-sample-data"), state) + + exec_record = ctx.exec_history["django-app"][0] + assert exec_record.command == _LOADDATA_CMD + assert exec_record.working_dir is not None + assert ctx.action_results == {"result": "loaded sample data"} + + def test_load_sample_data_fails_on_exec_error(self): + """load-sample-data action fails when exec raises ExecError.""" + ctx = testing.Context(charm.DashboardCharm) + container = testing.Container( + "django-app", + can_connect=True, + execs={testing.Exec(_LOADDATA_CMD, return_code=1)}, + ) + state = testing.State(containers={container}, leader=True) + + with pytest.raises(testing.ActionFailed) as exc_info: + ctx.run(ctx.on.action("load-sample-data"), state) + + assert "unable to load sample data" in exc_info.value.message + + def test_load_sample_data_fails_on_api_error(self): + """load-sample-data fails when exec itself raises an APIError.""" + ctx = testing.Context(charm.DashboardCharm) + container = testing.Container("django-app", can_connect=True) + state = testing.State(containers={container}, leader=True) + + with pytest.raises(testing.ActionFailed) as exc_info: + ctx.run(ctx.on.action("load-sample-data"), state) + + assert "unable to load sample data" in exc_info.value.message From 7c5f52d6281668631fcbe8149c0744c49ee0c9cb Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 1 Apr 2026 12:55:59 +1300 Subject: [PATCH 2/7] chore: bump ops to get the 12-factor improvements in opst[testing] --- charm/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charm/requirements.txt b/charm/requirements.txt index d58a30c..575081c 100644 --- a/charm/requirements.txt +++ b/charm/requirements.txt @@ -1,2 +1,2 @@ -ops ~= 2.17 +ops ~= 3.7 paas-charm>=1.0,<2 From df9cfc1dfb86aa29b9b9f19c6cb9299ed3ad2359 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 1 Apr 2026 12:57:43 +1300 Subject: [PATCH 3/7] ci: ops[testing] is required for state transition tests --- charm/tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/charm/tox.ini b/charm/tox.ini index b6b376d..d308122 100644 --- a/charm/tox.ini +++ b/charm/tox.ini @@ -9,7 +9,7 @@ min_version = 4.0.0 [vars] src_path = {tox_root}/src -;tests_path = {tox_root}/tests +tests_path = {tox_root}/tests ;lib_path = {tox_root}/lib/charms/operator_name_with_underscores all_path = {[vars]src_path} @@ -49,6 +49,7 @@ description = Run unit tests deps = pytest coverage[toml] + ops[testing] -r {tox_root}/requirements.txt commands = coverage run --source={[vars]src_path} \ From 6e11595a9e65c73f2edb9eae6ebfe8ea4164b979 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 1 Apr 2026 13:03:12 +1300 Subject: [PATCH 4/7] ci: combine lint and static, per current best practice --- charm/tox.ini | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/charm/tox.ini b/charm/tox.ini index d308122..fa220b6 100644 --- a/charm/tox.ini +++ b/charm/tox.ini @@ -4,7 +4,7 @@ [tox] no_package = True skip_missing_interpreters = True -env_list = format, lint, static +env_list = format, lint min_version = 4.0.0 [vars] @@ -32,10 +32,12 @@ commands = ruff check --fix {[vars]all_path} [testenv:lint] -description = Check code against coding style standards +description = Check code against coding style standards and run static type checks deps = ruff codespell + pyright + -r {tox_root}/requirements.txt commands = # if this charm owns a lib, uncomment "lib_path" variable # and uncomment the following line @@ -43,6 +45,7 @@ commands = codespell {tox_root} ruff check {[vars]all_path} ruff format --check --diff {[vars]all_path} + pyright {posargs} [testenv:unit] description = Run unit tests @@ -61,14 +64,6 @@ commands = {[vars]tests_path}/unit coverage report -[testenv:static] -description = Run static type checks -deps = - pyright - -r {tox_root}/requirements.txt -commands = - pyright {posargs} - [testenv:integration] description = Run integration tests deps = From 9775a6e351c7ff7ba401bf897f46b6d79fcb39c6 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 1 Apr 2026 13:11:59 +1300 Subject: [PATCH 5/7] ci: run the charm tests as well. --- .github/workflows/checks.yaml | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index 2984a71..66fd3cc 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -6,11 +6,15 @@ on: - main pull_request: +permissions: {} + jobs: app-checks: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 + with: + persist-credentials: false - name: Run framework tests run: | cd dashboard @@ -31,3 +35,19 @@ jobs: run: | cd dashboard make makemigrations-check + + charm-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + - name: Lint + run: | + cd charm + uvx tox run -e lint + - name: Unit tests + run: | + cd charm + uvx tox run -e unit From c1bcddff3d94d23d5f7797ebf3865bbf3935115e Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Wed, 1 Apr 2026 13:14:57 +1300 Subject: [PATCH 6/7] test: remove unnecessary sys.path manipulation --- charm/tests/unit/test_charm.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/charm/tests/unit/test_charm.py b/charm/tests/unit/test_charm.py index c341664..0db718b 100644 --- a/charm/tests/unit/test_charm.py +++ b/charm/tests/unit/test_charm.py @@ -3,18 +3,10 @@ """Unit tests for the dashboard charm's custom functionality.""" -import pathlib -import sys - import pytest +from ops import testing -CHARM_DIR = pathlib.Path(__file__).parents[2] -sys.path.insert(0, str(CHARM_DIR / "lib")) -sys.path.insert(0, str(CHARM_DIR / "src")) - -from ops import testing # noqa: E402 - -import charm # noqa: E402 +import charm _LOADDATA_CMD = ["python3", "manage.py", "loaddata", "initial_data.yaml"] From 49c54c9412005053e3413d98785a4443de11a285 Mon Sep 17 00:00:00 2001 From: Tony Meyer Date: Tue, 7 Apr 2026 20:12:58 +1200 Subject: [PATCH 7/7] Apply suggestions from code review Co-authored-by: Tony Meyer --- charm/tests/unit/test_charm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charm/tests/unit/test_charm.py b/charm/tests/unit/test_charm.py index 0db718b..3ca90cd 100644 --- a/charm/tests/unit/test_charm.py +++ b/charm/tests/unit/test_charm.py @@ -32,7 +32,7 @@ def test_load_sample_data_runs_loaddata(self): assert ctx.action_results == {"result": "loaded sample data"} def test_load_sample_data_fails_on_exec_error(self): - """load-sample-data action fails when exec raises ExecError.""" + """load-sample-data action fails when the manage.py command fails.""" ctx = testing.Context(charm.DashboardCharm) container = testing.Container( "django-app", @@ -47,7 +47,7 @@ def test_load_sample_data_fails_on_exec_error(self): assert "unable to load sample data" in exc_info.value.message def test_load_sample_data_fails_on_api_error(self): - """load-sample-data fails when exec itself raises an APIError.""" + """load-sample-data fails when manage.py isn't available.""" ctx = testing.Context(charm.DashboardCharm) container = testing.Container("django-app", can_connect=True) state = testing.State(containers={container}, leader=True)