Skip to content

Commit d167302

Browse files
authored
[DPE-6374] Add integration tests (#19)
1 parent 27de9e3 commit d167302

15 files changed

+404
-54
lines changed

.github/workflows/ci.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,18 @@ jobs:
7070
charmcraft-snap-channel: latest/beta/data-platform # TODO: remove after charmcraft 3.3 stable release
7171
path-to-charm-directory: .
7272
cache: false # TODO: change this to true once we are in charmcraftcache-hub
73+
74+
integration-test:
75+
name: Integration test charm
76+
needs:
77+
- lint
78+
- unit-test
79+
- build
80+
uses: canonical/data-platform-workflows/.github/workflows/[email protected]
81+
with:
82+
juju-agent-version: 3.6.1 # renovate: juju-agent-pin-minor
83+
_beta_allure_report: true
84+
artifact-prefix: ${{ needs.build.outputs.artifact-prefix }}
85+
cloud: lxd
86+
permissions:
87+
contents: write

.github/workflows/release.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ jobs:
1616
pull-requests: write # Need to create PR
1717
actions: write
1818

19-
2019
release:
2120
name: Release to Charmhub
2221
needs:

poetry.lock

+62-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+3
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ parameterized = "^0.9.0"
6666
[tool.poetry.group.integration.dependencies]
6767
pytest = "^8.3.3"
6868
pytest-github-secrets = {git = "https://github.com/canonical/data-platform-workflows", tag = "v26.0.0", subdirectory = "python/pytest_plugins/github_secrets"}
69+
pytest-asyncio = "^0.21.2"
6970
pytest-operator = "^0.37.0"
7071
pytest-operator-cache = {git = "https://github.com/canonical/data-platform-workflows", tag = "v26.0.0", subdirectory = "python/pytest_plugins/pytest_operator_cache"}
7172
pytest-operator-groups = {git = "https://github.com/canonical/data-platform-workflows", tag = "v26.0.0", subdirectory = "python/pytest_plugins/pytest_operator_groups"}
@@ -75,6 +76,8 @@ ops = "^2.17.0"
7576
pyyaml = "^6.0.2"
7677
urllib3 = "^2.2.3"
7778
protobuf = "5.28.2"
79+
allure-pytest = "^2.13.5"
80+
allure-pytest-collection-report = {git = "https://github.com/canonical/data-platform-workflows", tag = "v29.0.0", subdirectory = "python/pytest_plugins/allure_pytest_collection_report"}
7881

7982
[tool.coverage.run]
8083
branch = true

src/charm.py

+4-8
Original file line numberDiff line numberDiff line change
@@ -348,22 +348,18 @@ def _render_params(
348348
def prepare(self) -> bool:
349349
"""Prepare the benchmark service."""
350350
super().prepare()
351-
352-
# First, clean if a topic already existed
353-
self.clean()
354351
try:
355-
for attempt in Retrying(stop=stop_after_attempt(4), wait=wait_fixed(wait=15)):
352+
for attempt in Retrying(
353+
stop=stop_after_attempt(4), wait=wait_fixed(wait=20), reraise=True
354+
):
356355
with attempt:
357-
if model := self.database_state.model():
356+
if (model := self.database_state.model()) and not self.is_prepared():
358357
topic = NewTopic(
359358
name=model.db_name,
360359
num_partitions=self.config.parallel_processes * (len(self.peers) + 1),
361360
replication_factor=self.client.replication_factor,
362361
)
363362
self.client.create_topic(topic)
364-
else:
365-
logger.warning("No database model found")
366-
return False
367363
except Exception as e:
368364
logger.debug(f"Error creating topic: {e}")
369365

tests/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2022 Canonical Ltd.
1+
# Copyright 2025 Canonical Ltd.
22
# See LICENSE file for licensing details.
33

44
import argparse

tests/integration/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright 2025 Canonical Ltd.
2+
# See LICENSE file for licensing details.

tests/integration/conftest.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
6+
import subprocess
7+
8+
import juju
9+
import pytest
10+
import yaml
11+
from pytest_operator.plugin import OpsTest
12+
13+
from .helpers import (
14+
K8S_DB_MODEL_NAME,
15+
MICROK8S_CLOUD_NAME,
16+
)
17+
18+
19+
@pytest.fixture(scope="module", autouse=True)
20+
async def destroy_model_in_k8s(ops_test):
21+
yield
22+
23+
if ops_test.keep_model:
24+
return
25+
controller = juju.controller.Controller()
26+
await controller.connect()
27+
await controller.destroy_model(K8S_DB_MODEL_NAME)
28+
await controller.disconnect()
29+
30+
ctlname = list(yaml.safe_load(subprocess.check_output(["juju", "show-controller"])).keys())[0]
31+
32+
# We have deployed microk8s, and we do not need it anymore
33+
subprocess.run(["sudo", "snap", "remove", "--purge", "microk8s"], check=True)
34+
subprocess.run(["sudo", "snap", "remove", "--purge", "kubectl"], check=True)
35+
subprocess.run(
36+
["juju", "remove-cloud", "--client", "--controller", ctlname, MICROK8S_CLOUD_NAME],
37+
check=True,
38+
)
39+
40+
41+
@pytest.fixture(scope="module")
42+
async def kafka_benchmark_charm(ops_test: OpsTest):
43+
"""Kafka charm used for integration testing."""
44+
charm = await ops_test.build_charm(".")
45+
return charm

tests/integration/helpers.py

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
6+
import logging
7+
import subprocess
8+
from types import SimpleNamespace
9+
10+
from pytest_operator.plugin import OpsTest
11+
from tenacity import Retrying, stop_after_delay, wait_fixed
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
K8S_DB_MODEL_NAME = "database"
17+
MICROK8S_CLOUD_NAME = "cloudk8s"
18+
19+
20+
CONFIG_OPTS = {"workload_name": "test_mode", "parallel_processes": 1}
21+
SERIES = "jammy"
22+
KAFKA = "kafka"
23+
APP_NAME = "kafka-benchmark"
24+
KAFKA_CHANNEL = "3/edge"
25+
DEFAULT_NUM_UNITS = 2
26+
27+
28+
KRAFT_CONFIG = {
29+
"profile": "testing",
30+
"roles": "broker,controller",
31+
}
32+
33+
34+
MODEL_CONFIG = {
35+
"logging-config": "<root>=INFO;unit=DEBUG",
36+
"update-status-hook-interval": "1m",
37+
}
38+
39+
40+
def check_service(svc_name: str, unit_id: int = 0, retry_if_fail: bool = True) -> bool | None:
41+
def __check():
42+
try:
43+
return (
44+
subprocess.check_output(
45+
[
46+
"juju",
47+
"ssh",
48+
f"{APP_NAME}/{unit_id}",
49+
"--",
50+
"sudo",
51+
"systemctl",
52+
"is-active",
53+
svc_name,
54+
],
55+
text=True,
56+
).rstrip()
57+
== "active"
58+
)
59+
except Exception:
60+
return False
61+
62+
if not retry_if_fail:
63+
return __check()
64+
for attempt in Retrying(stop=stop_after_delay(150), wait=wait_fixed(15)):
65+
with attempt:
66+
return __check()
67+
68+
69+
async def run_action(
70+
ops_test, action_name: str, unit_name: str, timeout: int = 30, **action_kwargs
71+
):
72+
"""Runs the given action on the given unit."""
73+
client_unit = ops_test.model.units.get(unit_name)
74+
action = await client_unit.run_action(action_name, **action_kwargs)
75+
result = await action.wait()
76+
logging.info(f"request results: {result.results}")
77+
return SimpleNamespace(status=result.status or "completed", response=result.results)
78+
79+
80+
async def get_leader_unit_id(ops_test: OpsTest, app: str = APP_NAME) -> int:
81+
"""Helper function that retrieves the leader unit ID."""
82+
leader_unit = None
83+
for unit in ops_test.model.applications[app].units:
84+
if await unit.is_leader_from_status():
85+
leader_unit = unit
86+
break
87+
88+
if not leader_unit:
89+
raise ValueError("Leader unit not found")
90+
91+
return int(leader_unit.name.split("/")[1])

0 commit comments

Comments
 (0)