From 53fdf30a5d2bc5242ed7f5974e8f9203bc50edba Mon Sep 17 00:00:00 2001 From: Ali Kelkawi Date: Mon, 27 Mar 2023 13:17:35 +0300 Subject: [PATCH 1/6] CSS-3449: add ci/cd and tests --- .github/workflows/ci.yaml | 31 ----- .github/workflows/integration_test.yaml | 27 ++++ .github/workflows/on_pull_request.yaml | 10 -- .github/workflows/on_push.yaml | 14 -- .github/workflows/promote_charm.yaml | 26 ++++ .github/workflows/test.yaml | 9 ++ .github/workflows/test_and_publish_charm.yaml | 23 +++ .woke.yaml | 7 + README.md | 3 + pyproject.toml | 5 + src/charm.py | 2 +- src/log.py | 4 +- tests/integration/test_charm.py | 79 +++++++++++ tests/unit/__init__.py | 3 + tests/unit/test_charm.py | 131 ++++++++++++++++++ tox.ini | 24 +++- 16 files changed, 339 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/integration_test.yaml delete mode 100644 .github/workflows/on_pull_request.yaml delete mode 100644 .github/workflows/on_push.yaml create mode 100644 .github/workflows/promote_charm.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .github/workflows/test_and_publish_charm.yaml create mode 100644 .woke.yaml create mode 100644 tests/integration/test_charm.py create mode 100644 tests/unit/test_charm.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 079f468..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: Tests -on: - workflow_call: - secrets: - charmcraft-credentials: - required: true - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install dependencies - run: python3 -m pip install tox - - name: Run linters - run: tox -e lint - lib-check: - name: Check libraries - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Check libs - uses: canonical/charming-actions/check-libraries@2.1.1 - with: - credentials: "${{ secrets.charmcraft-credentials }}" - github-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml new file mode 100644 index 0000000..b0334ee --- /dev/null +++ b/.github/workflows/integration_test.yaml @@ -0,0 +1,27 @@ +name: Integration tests + +on: + pull_request: + +jobs: + integration-test-microk8s: + name: Integration tests (microk8s) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup operator environment + uses: charmed-kubernetes/actions-operator@main + with: + juju-channel: 3.1/stable + provider: microk8s + channel: 1.25-strict/stable + - name: Run integration tests + # set a predictable model name so it can be consumed by charm-logdump-action + run: tox -e integration -- --model testing + - name: Dump logs + uses: canonical/charm-logdump-action@main + if: failure() + with: + app: temporal-ui-k8s + model: testing diff --git a/.github/workflows/on_pull_request.yaml b/.github/workflows/on_pull_request.yaml deleted file mode 100644 index 8a447d7..0000000 --- a/.github/workflows/on_pull_request.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: Tests -on: - pull_request: - -jobs: - run-tests: - name: Run Tests - uses: ./.github/workflows/ci.yaml - secrets: - charmcraft-credentials: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/.github/workflows/on_push.yaml b/.github/workflows/on_push.yaml deleted file mode 100644 index 8068e7b..0000000 --- a/.github/workflows/on_push.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: Tests -on: - push: - branches: - - master - - main - - track/** - -jobs: - run-tests: - name: Run Tests - uses: ./.github/workflows/ci.yaml - secrets: - charmcraft-credentials: ${{ secrets.CHARMHUB_TOKEN }} diff --git a/.github/workflows/promote_charm.yaml b/.github/workflows/promote_charm.yaml new file mode 100644 index 0000000..66649de --- /dev/null +++ b/.github/workflows/promote_charm.yaml @@ -0,0 +1,26 @@ +name: Promote charm + +on: + workflow_dispatch: + inputs: + origin-channel: + type: choice + description: 'Origin Channel' + options: + - latest/edge + destination-channel: + type: choice + description: 'Destination Channel' + options: + - latest/stable + secrets: + CHARMHUB_TOKEN: + required: true + +jobs: + promote-charm: + uses: canonical/operator-workflows/.github/workflows/promote_charm.yaml@main + with: + origin-channel: ${{ github.event.inputs.origin-channel }} + destination-channel: ${{ github.event.inputs.destination-channel }} + secrets: inherit diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..ee5fa3e --- /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@main + secrets: inherit diff --git a/.github/workflows/test_and_publish_charm.yaml b/.github/workflows/test_and_publish_charm.yaml new file mode 100644 index 0000000..a3c77cf --- /dev/null +++ b/.github/workflows/test_and_publish_charm.yaml @@ -0,0 +1,23 @@ +name: Publish to edge + +# On push to a "special" branch, we: +# * always publish to charmhub at latest/edge/branchname +# * always run tests +# where a "special" branch is one of main/master or track/**, as +# by convention these branches are the source for a corresponding +# charmhub edge channel. + +on: + push: + branches: + - main + - track/** + +jobs: + publish-to-edge: + uses: canonical/operator-workflows/.github/workflows/test_and_publish_charm.yaml@main + secrets: inherit + with: + integration-test-provider: microk8s + integration-test-provider-channel: 1.25-strict/stable + integration-test-juju-channel: 3.1/stable diff --git a/.woke.yaml b/.woke.yaml new file mode 100644 index 0000000..e29f251 --- /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/ingress.py +rules: + # Ignore "master" - the database relation event received from the library. + - name: master diff --git a/README.md b/README.md index a1ac829..f747b40 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![Charmhub Badge](https://charmhub.io/temporal-ui-k8s/badge.svg)](https://charmhub.io/temporal-ui-k8s) +[![Release Edge](https://github.com/canonical/temporal-ui-k8s-operator/actions/workflows/test_and_publish_charm.yaml/badge.svg)](https://github.com/canonical/temporal-ui-k8s-operator/actions/workflows/test_and_publish_charm.yaml) + # Temporal UI K8s Operator This is the Kubernetes Python Operator for the diff --git a/pyproject.toml b/pyproject.toml index 07ce97a..22412db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,11 @@ # Copyright 2022 Canonical Ltd 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 diff --git a/src/charm.py b/src/charm.py index cf95313..d549bfd 100755 --- a/src/charm.py +++ b/src/charm.py @@ -33,7 +33,7 @@ def render(template_name, context): """ charm_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) loader = FileSystemLoader(os.path.join(charm_dir, "templates")) - return Environment(loader=loader).get_template(template_name).render(**context) + return Environment(loader=loader, autoescape=True).get_template(template_name).render(**context) class TemporalUiK8SOperatorCharm(CharmBase): diff --git a/src/log.py b/src/log.py index db1e3b4..e6a34cd 100644 --- a/src/log.py +++ b/src/log.py @@ -17,7 +17,7 @@ def log_event_handler(logger): """ def decorator(method): - """Logging decorator wrapper. + """Log decorator wrapper. Args: method: method wrapped by the decorator. @@ -28,7 +28,7 @@ def decorator(method): @functools.wraps(method) def decorated(self, event): - """Logging decorator method. + """Log decorator method. Args: event: The event triggered when the relation changes. diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py new file mode 100644 index 0000000..ab48df4 --- /dev/null +++ b/tests/integration/test_charm.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# Copyright 2023 Canonical Ltd Ltd. +# See LICENSE file for licensing details. + + +"""Temporal UI charm integration tests.""" + +import logging +from pathlib import Path + +import pytest +import pytest_asyncio +import requests +import yaml +from pytest_operator.plugin import OpsTest + +logger = logging.getLogger(__name__) + +METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +APP_NAME = METADATA["name"] + +APP_NAME_SERVER = "temporal-k8s" +APP_NAME_ADMIN = "temporal-admin-k8s" + + +@pytest_asyncio.fixture(name="deploy", scope="module") +async def deploy(ops_test: OpsTest): + """The app is up and running.""" + charm = await ops_test.build_charm(".") + resources = {"temporal-ui-image": METADATA["containers"]["temporal-ui"]["upstream-source"]} + + # Deploy temporal server, temporal admin and postgresql charms. + await ops_test.model.deploy(charm, resources=resources, application_name=APP_NAME) + await ops_test.model.deploy(APP_NAME_SERVER, channel="edge") + await ops_test.model.deploy(APP_NAME_ADMIN, channel="edge") + await ops_test.model.deploy("postgresql-k8s", channel="edge") + + async with ops_test.fast_forward(): + await ops_test.model.wait_for_idle( + apps=[APP_NAME, APP_NAME_SERVER, APP_NAME_ADMIN], + status="blocked", + raise_on_blocked=False, + timeout=1200, + ) + await ops_test.model.wait_for_idle( + apps=["postgresql-k8s"], + status="active", + raise_on_blocked=False, + timeout=1200, + ) + + assert ops_test.model.applications[APP_NAME].units[0].workload_status == "blocked" + await ops_test.model.integrate(f"{APP_NAME_SERVER}:db", "postgresql-k8s:db") + await ops_test.model.integrate(f"{APP_NAME_SERVER}:visibility", "postgresql-k8s:db") + await ops_test.model.integrate(f"{APP_NAME_SERVER}:admin", f"{APP_NAME_ADMIN}:admin") + await ops_test.model.integrate(f"{APP_NAME}:ui", f"{APP_NAME_SERVER}:ui") + await ops_test.model.wait_for_idle( + apps=[APP_NAME, APP_NAME_SERVER, APP_NAME_ADMIN], + status="active", + raise_on_blocked=False, + timeout=1200, + ) + assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" + + +@pytest.mark.abort_on_fail +@pytest.mark.usefixtures("deploy") +class TestDeployment: + """Integration tests for Temporal UI charm.""" + + async def test_basic_client(self, ops_test: OpsTest): + """Perform GET request on the Temporal UI host.""" + status = await ops_test.model.get_status() # noqa: F821 + address = status["applications"][APP_NAME]["units"][f"{APP_NAME}/0"]["address"] + url = f"http://{address}:8080" + logger.info("curling app address: %s", url) + + response = requests.get(url, timeout=300) + assert response.status_code == 200 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 1739b9f..f6c3ccf 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,6 +1,9 @@ # Copyright 2023 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 new file mode 100644 index 0000000..815b87d --- /dev/null +++ b/tests/unit/test_charm.py @@ -0,0 +1,131 @@ +# Copyright 2023 Canonical Ltd. +# See LICENSE file for licensing details. +# +# Learn more about testing at: https://juju.is/docs/sdk/testing + + +"""Temporal admin charm unit tests.""" + +# pylint:disable=protected-access + +from unittest import TestCase + +from ops.model import BlockedStatus +from ops.testing import Harness + +from charm import TemporalUiK8SOperatorCharm + + +class TestCharm(TestCase): + """Unit tests. + + Attrs: + maxDiff: Specifies max difference shown by failed tests. + """ + + maxDiff = None + + def setUp(self): + """Set up for the unit tests.""" + self.harness = Harness(TemporalUiK8SOperatorCharm) + self.addCleanup(self.harness.cleanup) + self.harness.set_can_connect("temporal-ui", True) + self.harness.set_leader(True) + self.harness.begin() + + def test_initial_plan(self): + """The initial pebble plan is empty.""" + harness = self.harness + initial_plan = harness.get_container_pebble_plan("temporal-ui").to_dict() + self.assertEqual(initial_plan, {}) + + def test_blocked_by_temporal_server(self): + """The charm is blocked without a temporal:ui relation.""" + harness = self.harness + + # Simulate pebble readiness. + container = harness.model.unit.get_container("temporal-ui") + harness.charm.on.temporal_ui_pebble_ready.emit(container) + + # The BlockStatus is set with a message. + self.assertEqual( + harness.model.unit.status, + # BlockedStatus("ui:temporal relation: server is not ready"), + BlockedStatus("ui:temporal relation: not available"), + ) + + def test_ready(self): + """The pebble plan is correctly generated when the charm is ready.""" + harness = self.harness + + # Add the temporal relation. + harness.add_relation("ui", "temporal") + + simulate_lifecycle(harness) + + # The plan is generated after pebble is ready. + want_plan = { + "services": { + "temporal-ui": { + "summary": "temporal ui", + "command": "./ui-server --env charm start", + "startup": "enabled", + "override": "replace", + "environment": { + "LOG_LEVEL": "info", + }, + } + }, + } + + got_plan = harness.get_container_pebble_plan("temporal-ui").to_dict() + self.assertEqual(got_plan, want_plan) + + # The service was started. + service = harness.model.unit.get_container("temporal-ui").get_service("temporal-ui") + self.assertTrue(service.is_running()) + + +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("temporal-ui") + harness.charm.on.temporal_ui_pebble_ready.emit(container) + + # Simulate server readiness. + app = type("App", (), {"name": "temporal-ui-k8s"})() + relation = type( + "Relation", + (), + {"data": {app: {"server_status": "ready"}}, "name": "ui", "id": 42}, + )() + unit = type("Unit", (), {"app": app, "name": "temporal-ui-k8s/0"})() + event = type("Event", (), {"app": app, "relation": relation, "unit": unit})() + harness.charm._on_ui_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": { + "server_status": "ready", + }, + "relation": type("Relation", (), {"name": rel_name}), + }, + ) diff --git a/tox.ini b/tox.ini index 293e013..84259c4 100644 --- a/tox.ini +++ b/tox.ini @@ -2,13 +2,15 @@ # See LICENSE file for licensing details. [tox] -envlist = lint, unit +envlist = fmt, lint, unit, static, coverage-report skipsdist = True skip_missing_interpreters = True +max-line-length=120 [vars] src_path = {toxinidir}/src/ tst_path = {toxinidir}/tests/ +all_path = {[vars]src_path} {[vars]tst_path} [testenv] basepython = python3 @@ -49,12 +51,15 @@ deps = flake8-docstrings-complete>=1.0.3 flake8-test-docs>=1.0 commands = + 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]tst_path} isort --check-only --diff {[vars]src_path} {[vars]tst_path} black --check --diff {[vars]src_path} {[vars]tst_path} + mypy {[vars]all_path} --ignore-missing-imports --install-types --non-interactive + pylint {[vars]all_path} --disable=E0401,W1203,W0613,W0718,R0903,W1514 [testenv:unit] description = Run tests @@ -68,6 +73,23 @@ commands = -m pytest --ignore={[vars]tst_path}integration -v --tb native -s {posargs} 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 analysis tests +deps = + bandit[toml] + -r{toxinidir}/requirements.txt +commands = + bandit -c {toxinidir}/pyproject.toml -r {[vars]src_path} {[vars]tst_path} + [testenv:integration] description = Run integration tests deps = From 56a801437ee3f63818864613ba0aeca9aae321aa Mon Sep 17 00:00:00 2001 From: Ali Kelkawi Date: Mon, 27 Mar 2023 13:24:52 +0300 Subject: [PATCH 2/6] CSS-3449: fix metadata.yaml lint --- metadata.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/metadata.yaml b/metadata.yaml index c5de493..3e2ac57 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -13,7 +13,9 @@ description: | This charm provides the web UI which can be related to the Temporal server charm to view workflow runs. -maintainer: Commercial Systems +maintainers: + - Commercial Systems +docs: https://discourse.charmhub.io/t/temporal-ui-documentation-overview/9232 tags: - temporal - workflow From dc6f0dda557ac87dc043baf286527adf6b1f3195 Mon Sep 17 00:00:00 2001 From: Ali Kelkawi <81743070+kelkawi-a@users.noreply.github.com> Date: Mon, 27 Mar 2023 15:11:01 +0300 Subject: [PATCH 3/6] Update test_charm.py --- tests/integration/test_charm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index ab48df4..bc779e4 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -33,7 +33,7 @@ async def deploy(ops_test: OpsTest): await ops_test.model.deploy(charm, resources=resources, application_name=APP_NAME) await ops_test.model.deploy(APP_NAME_SERVER, channel="edge") await ops_test.model.deploy(APP_NAME_ADMIN, channel="edge") - await ops_test.model.deploy("postgresql-k8s", channel="edge") + await ops_test.model.deploy("postgresql-k8s", channel="edge", trust=True) async with ops_test.fast_forward(): await ops_test.model.wait_for_idle( From 94005105597d23bbd230b99175d43913426aae0e Mon Sep 17 00:00:00 2001 From: Ali Kelkawi Date: Tue, 28 Mar 2023 16:15:34 +0300 Subject: [PATCH 4/6] CSS-3449: fix integration test --- tests/integration/test_charm.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index bc779e4..b700887 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -53,13 +53,22 @@ async def deploy(ops_test: OpsTest): await ops_test.model.integrate(f"{APP_NAME_SERVER}:db", "postgresql-k8s:db") await ops_test.model.integrate(f"{APP_NAME_SERVER}:visibility", "postgresql-k8s:db") await ops_test.model.integrate(f"{APP_NAME_SERVER}:admin", f"{APP_NAME_ADMIN}:admin") + await ops_test.model.wait_for_idle( + apps=[APP_NAME_SERVER, APP_NAME_ADMIN], + status="active", + raise_on_blocked=False, + timeout=300, + ) + await ops_test.model.integrate(f"{APP_NAME}:ui", f"{APP_NAME_SERVER}:ui") + await ops_test.model.wait_for_idle( - apps=[APP_NAME, APP_NAME_SERVER, APP_NAME_ADMIN], + apps=[APP_NAME], status="active", raise_on_blocked=False, - timeout=1200, + timeout=300, ) + assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" From a9f5377a888eca3d4ac24367c1f887bfa1e4c258 Mon Sep 17 00:00:00 2001 From: Ali Kelkawi Date: Wed, 29 Mar 2023 19:32:11 +0300 Subject: [PATCH 5/6] minor fixes in unit tests --- tests/unit/test_charm.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 815b87d..f8a5265 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -15,6 +15,7 @@ from charm import TemporalUiK8SOperatorCharm +APP_NAME = "temporal-ui" class TestCharm(TestCase): """Unit tests. @@ -26,17 +27,16 @@ class TestCharm(TestCase): maxDiff = None def setUp(self): - """Set up for the unit tests.""" + """Setup for the unit tests.""" self.harness = Harness(TemporalUiK8SOperatorCharm) self.addCleanup(self.harness.cleanup) - self.harness.set_can_connect("temporal-ui", True) + self.harness.set_can_connect(APP_NAME, True) self.harness.set_leader(True) self.harness.begin() def test_initial_plan(self): """The initial pebble plan is empty.""" - harness = self.harness - initial_plan = harness.get_container_pebble_plan("temporal-ui").to_dict() + initial_plan = self.harness.get_container_pebble_plan(APP_NAME).to_dict() self.assertEqual(initial_plan, {}) def test_blocked_by_temporal_server(self): @@ -44,13 +44,12 @@ def test_blocked_by_temporal_server(self): harness = self.harness # Simulate pebble readiness. - container = harness.model.unit.get_container("temporal-ui") + container = harness.model.unit.get_container(APP_NAME) harness.charm.on.temporal_ui_pebble_ready.emit(container) # The BlockStatus is set with a message. self.assertEqual( harness.model.unit.status, - # BlockedStatus("ui:temporal relation: server is not ready"), BlockedStatus("ui:temporal relation: not available"), ) @@ -78,11 +77,11 @@ def test_ready(self): }, } - got_plan = harness.get_container_pebble_plan("temporal-ui").to_dict() + 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("temporal-ui").get_service("temporal-ui") + service = harness.model.unit.get_container(APP_NAME).get_service(APP_NAME) self.assertTrue(service.is_running()) @@ -93,7 +92,7 @@ def simulate_lifecycle(harness): harness: ops.testing.Harness object used to simulate charm lifecycle. """ # Simulate pebble readiness. - container = harness.model.unit.get_container("temporal-ui") + container = harness.model.unit.get_container(APP_NAME) harness.charm.on.temporal_ui_pebble_ready.emit(container) # Simulate server readiness. From f91148189a2094fb877619e352466fa1be76e907 Mon Sep 17 00:00:00 2001 From: Ali Kelkawi Date: Wed, 29 Mar 2023 19:32:53 +0300 Subject: [PATCH 6/6] minor fixes in unit tests --- tests/unit/test_charm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index f8a5265..568ccbb 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -17,6 +17,7 @@ APP_NAME = "temporal-ui" + class TestCharm(TestCase): """Unit tests.