Skip to content

Migrate integration tests from pytest-operator (async) to jubilant (sync)#750

Draft
Copilot wants to merge 6 commits intomainfrom
copilot/migrate-to-jubilant-sync-tests
Draft

Migrate integration tests from pytest-operator (async) to jubilant (sync)#750
Copilot wants to merge 6 commits intomainfrom
copilot/migrate-to-jubilant-sync-tests

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Nov 12, 2025

Applicable spec: N/A

Overview

Migrates conftest.py and test_smoke.py from pytest-operator's async pattern to jubilant's sync pattern. Updates juju-channel from 3/stable to 4/candidate per Juju 4 testing requirements.

Rationale

pytest-jubilant provides synchronous equivalents for pytest-operator's async fixtures, simplifying test code and aligning with Juju 4 testing infrastructure.

Juju Events Changes

N/A

Module Changes

tests/integration/conftest.py

  • Enabled pytest_plugins = ["pytest_jubilant"]
  • Replaced OpsTest type with jubilant fixture parameter
  • Converted 13 async fixtures to sync: skip_by_cloud_type, deploy_model, kubernetes_cluster, api_client, metrics_agent, cos_model, cos_lite_installed, traefik_url, expected_dashboard_titles, related_grafana, grafana_password, related_prometheus
  • Changed type hints: AsyncGenerator[Model, None]Generator[Model, None, None]
  • Removed: pytest_asyncio, OpsTest imports, all async def/await keywords

tests/integration/test_smoke.py

  • Converted preserve_charm_config fixture and 2 test functions from async to sync
  • Replaced asyncio.gather(*[...]) with list comprehensions
  • Removed: asyncio, pytest_asyncio imports, all async def/await keywords

pyproject.toml

  • Added pytest-jubilant to integration dependencies

.github/workflows/integration_test.yaml

  • juju-channel: 3/stablejuju-channel: 4/candidate

Migration pattern:

# Before
@pytest_asyncio.fixture
async def kubernetes_cluster(ops_test: OpsTest) -> AsyncGenerator[Model, None]:
    bundle = await Bundle.create(ops_test)
    async with deploy_model(ops_test, "main", bundle) as model:
        yield model

# After
@pytest.fixture
def kubernetes_cluster(jubilant) -> Generator[Model, None, None]:
    bundle = Bundle.create(jubilant)
    with deploy_model(jubilant, "main", bundle) as model:
        yield model

Library Changes

N/A

Checklist

Test-only changes. Remaining integration test files (test_ceph.py, test_cos.py, test_dqlite.py, test_dualstack.py, test_etcd.py, test_ipv6_only.py, test_k8s.py, test_openstack.py, test_registry.py, test_upgrade.py) still use async patterns.

Original prompt

This section details on the original issue you should resolve

<issue_title>switching to jubilant for juju4 testing</issue_title>
<issue_description>### Enhancement Proposal

Task: Migrate pytest-operator async tests to jubilant (sync) and validate quickly with lint.

Context:

Repo: canonical/k8s-operator
Files to change (start here): conftest.py and test_smoke.py
We will NOT run integration tests in CI here. Instead use tox -e lint (mypy + lint) as a fast verification step.
Use the Jubilant migration guide as reference: https://documentation.ubuntu.com/jubilant/how-to/migrate-from-pytest-operator/
pytest-jubilant repo: https://github.com/canonical/pytest-jubilant
High-level goal:

Replace async pytest-operator usage (fixture ops_test, pytest_asyncio, async def, await, async with, asyncio.gather) with synchronous jubilant usage provided by pytest-jubilant.
Keep behavior identical where possible; make minimal changes. Add clear TODO comments where assumptions are made.
Run tox -e lint to verify mypy/lint issues, iterate until lint passes.
Detailed instructions for Copilot to perform changes

Top-level plugin enablement
In conftest.py add (near top, after imports) the line:
pytest_plugins = ["pytest_jubilant"]
Remove the import of OpsTest typing:
Remove: from pytest_operator.plugin import OpsTest
Replace pytest-async patterns with sync
Replace imports:
Remove import pytest_asyncio if only used for fixture decorators in this file.
Ensure import pytest remains.
Replace fixture decorators:
@pytest_asyncio.fixture -> @pytest.fixture
Keep scope="module" and params=... when present.
Convert async def fixture/test -> def and remove awaits inside their bodies.
Convert @contextlib.asynccontextmanager to @contextlib.contextmanager and async def -> def and change async with -> with.
Replace AsyncGenerator typing with normal Generator or remove typing if uncertain.
Replace ops_test usage with jubilant
Replace fixture parameter ops_test -> jubilant in fixtures/tests.
Remove await before jubilant.* calls. Example:
await ops_test.model.get_controller() -> jubilant.model.get_controller()
async with ops_test.fast_forward(ONE_MIN): -> with jubilant.fast_forward(ONE_MIN):
await ops_test.track_model(...) -> jubilant.track_model(...)
await ops_test.add_k8s(...) -> jubilant.add_k8s(...)
await ops_test.forget_model(...) -> jubilant.forget_model(...)
await ops_test.juju(...) -> jubilant.juju(...)
If code calls helper methods on ops_test that are async-only in pytest-operator and do not map directly to jubilant, add # TODO: verify jubilant API comment and keep the call but remove await (so mypy will flag it if needed).
Replace asyncio.gather / parallel awaits
Convert:
pre = await asyncio.gather(*[app.get_config() for app in apps])
-> pre = [app.get_config() for app in apps]
Convert any await asyncio.gather(...) into an appropriate synchronous equivalent (list comprehension or loop).
If any juju API remains asynchronous and no sync mapping exists, add a TODO comment and keep a placeholder that will be resolved later.
Tests conversion example (apply mechanically)
For test functions in test_smoke.py:
async def test_nodes_ready(kubernetes_cluster: juju.model.Model): -> def test_nodes_ready(kubernetes_cluster: juju.model.Model):
Remove await from calls inside the test.
If ready_nodes was async, replace with sync call or mark TODO.
Typing & mypy adjustments
Remove or change Async-specific typing:
Replace -> AsyncGenerator[Model, None] to -> Generator[Model, None, None] or drop the return type.
Where OpsTest type was used, remove the type annotation or replace with Any (import from typing if used).
Add in-file TODO comments for any unresolved types to make future iterations easier.
Add small in-code audit comments
Where you remove await from a call that previously returned an awaitable, add a short comment like:
MIGRATION: removed await per jubilant; verify this method is sync in jubilant
Where you convert async with to with, add:
MIGRATION: switched to non-async context manager (jubilant)
Minimal behavioral preservation
Preserve pytestmark = [...] lines and other test metadata unchanged.
Preserve DEFAULT_SNAP_INSTALLATION, TEST_DATA, and other constants.
Commit message and PR summary
Suggested commit message:
tests(integration): migrate conftest + test_smoke from pytest-operator async -> jubilant sync; verify with lint
Suggested PR description items to include:
What changed (files + short summary).
How it was validated (ran tox -e lint).
TODOs / follow-ups (list of other integration tests that still reference ops_test, any outstanding mypy errors left intentionally with comments).
Verification steps (fast checks only — do not run integration tests):

Collection check via lint target (this ensures imports and types are validated by mypy/flake):

Run locally:
tox -e lint
If tox -e lint is slow or not configured to run tests, run:
tox -e py39 -- -k test_smoke -q --collect-only
(only if you want pytest collection; still prefer tox...


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…r async to jubilant sync

Co-authored-by: addyess <10090033+addyess@users.noreply.github.com>
Copilot AI changed the title [WIP] Migrate pytest-operator async tests to jubilant Migrate integration tests from pytest-operator (async) to jubilant (sync) Nov 12, 2025
Copilot AI requested a review from addyess November 12, 2025 23:16
Adam Dyess added 4 commits November 13, 2025 14:11
…erences in conftest

- Convert all async helper functions to sync (removed async/await)
- Replace AsyncRetrying with Retrying, alru_cache with lru_cache
- Update all OpsTest references to jubilant parameter
- Fix jubilant.request references by adding request fixture parameter
- Remove asyncio imports throughout
- Add MIGRATION comments for clarity
- All lint checks passing
- Removed pytest-operator (conflicts with pytest-jubilant)
- Removed pytest-asyncio (no longer needed for sync tests)
- Removed async-lru (replaced with functools.lru_cache)
- Updated uv.lock to reflect dependency changes
- pytest-jubilant provides its own juju client wrapper
- Removed juju and its transitive dependencies from integration group
@addyess addyess force-pushed the copilot/migrate-to-jubilant-sync-tests branch from 4ee0c36 to 9944c3c Compare November 13, 2025 21:55
@github-actions
Copy link
Copy Markdown
Contributor

Test results for commit 9944c3c

Test coverage for 9944c3c

Name                                            Stmts   Miss Branch BrPart  Cover   Missing
-------------------------------------------------------------------------------------------
charms/worker/k8s/src/certificates.py             179    179     44      0     0%   6-429
charms/worker/k8s/src/charm.py                    596    263    184     30    51%   117-118, 224-225, 235-236, 238, 252, 266-273, 296-301, 307-323, 346->365, 360-363, 375, 384, 401-404, 409-411, 419-428, 451->456, 458-461, 495-496, 508->510, 515-517, 519-521, 533-547, 568, 576-582, 595-612, 629, 632, 642->exit, 664-680, 689-698, 710-722, 740-756, 766-781, 790-796, 808-850, 868-870, 872, 890-902, 911-929, 941-953, 964, 1005, 1008, 1017, 1027, 1030, 1040-1052, 1057-1063, 1085-1087, 1091-1094, 1105-1124, 1128-1129, 1149-1150, 1167, 1170-1171, 1177->1186
charms/worker/k8s/src/cloud_integration.py         80      3     24      3    94%   140, 149->151, 158-159
charms/worker/k8s/src/config/arg_files.py          79      0     22      2    98%   134->133, 152->150
charms/worker/k8s/src/config/bootstrap.py         145     24     54      5    78%   65-84, 158-159, 202-203, 205, 223, 252, 266->254
charms/worker/k8s/src/config/cluster.py            63      3     22     11    84%   37, 51->53, 59->61, 63->65, 66, 68, 73->75, 79->81, 85->87, 92->94, 98->100
charms/worker/k8s/src/config/extra_args.py         43      4     20      5    86%   32->27, 44, 45->exit, 82, 100, 116
charms/worker/k8s/src/config/option.py             20      0      8      0   100%
charms/worker/k8s/src/config/resource.py           33      7      4      1    73%   32, 57-58, 63-72
charms/worker/k8s/src/containerd.py               154     15     48     18    83%   45-48, 78->82, 82->85, 85->88, 234->236, 240->243, 243->245, 245->247, 247->250, 262, 269, 276, 327-328, 340-341, 355, 357, 359, 363
charms/worker/k8s/src/cos_integration.py           41     16      4      0    56%   82, 117-234, 238
charms/worker/k8s/src/endpoints.py                 40      1     18      1    97%   62
charms/worker/k8s/src/events/update_status.py     104     19     32      9    78%   93, 99-100, 124, 126, 142, 156-167, 173, 191, 192->194, 198
charms/worker/k8s/src/inspector.py                 62     10     24      6    79%   44-45, 74, 109, 122-123, 132, 138-143
charms/worker/k8s/src/k8s/client.py                20      4      2      1    77%   39->41, 47-48, 50-51
charms/worker/k8s/src/k8s/node.py                  34     14      4      0    53%   47-49, 52, 65-75
charms/worker/k8s/src/k8sd_api_manager.py         411     27     14      2    93%   712, 787-788, 1019, 1062-1064, 1073-1077, 1139, 1208, 1216, 1234, 1247-1248, 1309-1311, 1322-1325, 1342-1346
charms/worker/k8s/src/kube_control.py              44     32     10      0    22%   31-35, 44-90
charms/worker/k8s/src/literals.py                 110      0      0      0   100%
charms/worker/k8s/src/pki.py                       29      9      2      1    68%   35-36, 49-54, 65
charms/worker/k8s/src/protocols.py                 33      5      0      0    85%   60, 68, 76, 84, 92
charms/worker/k8s/src/reschedule.py                79      4      6      3    92%   155->158, 174-175, 191-192, 215->exit, 221->exit
charms/worker/k8s/src/snap.py                     180     16     58     11    89%   110, 188-189, 200-201, 214->207, 232-237, 291-292, 294, 297->288, 304->288, 308, 329-330
charms/worker/k8s/src/token_distributor.py        254    132     68      7    42%   112-114, 128, 182, 186, 211, 236->exit, 267, 287-288, 301-311, 332, 348-361, 407->406, 411, 415->420, 438-472, 506, 519, 529, 550-663, 682-735, 748-750
charms/worker/k8s/src/upgrade.py                  121     38     36      5    64%   81-108, 191->193, 202-205, 208-212, 219->221, 222-225, 242
charms/worker/k8s/src/utils.py                     16     16      4      0     0%   6-47
-------------------------------------------------------------------------------------------
TOTAL                                            2970    841    712    121    67%

Static code analysis report

Run started:2025-11-13 21:56:25.171163

Test results:
  No issues identified.

Code scanned:
  Total lines of code: 6004
  Total lines skipped (#nosec): 4
  Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 0

Run metrics:
  Total issues (by severity):
  	Undefined: 0
  	Low: 0
  	Medium: 0
  	High: 0
  Total issues (by confidence):
  	Undefined: 0
  	Low: 0
  	Medium: 0
  	High: 0
Files skipped (0):

@github-actions
Copy link
Copy Markdown
Contributor

Test results for commit 9944c3c

Test coverage for 9944c3c

Name                                            Stmts   Miss Branch BrPart  Cover   Missing
-------------------------------------------------------------------------------------------
charms/worker/k8s/src/certificates.py             179    179     44      0     0%   6-429
charms/worker/k8s/src/charm.py                    596    263    184     30    51%   117-118, 224-225, 235-236, 238, 252, 266-273, 296-301, 307-323, 346->365, 360-363, 375, 384, 401-404, 409-411, 419-428, 451->456, 458-461, 495-496, 508->510, 515-517, 519-521, 533-547, 568, 576-582, 595-612, 629, 632, 642->exit, 664-680, 689-698, 710-722, 740-756, 766-781, 790-796, 808-850, 868-870, 872, 890-902, 911-929, 941-953, 964, 1005, 1008, 1017, 1027, 1030, 1040-1052, 1057-1063, 1085-1087, 1091-1094, 1105-1124, 1128-1129, 1149-1150, 1167, 1170-1171, 1177->1186
charms/worker/k8s/src/cloud_integration.py         80      3     24      3    94%   140, 149->151, 158-159
charms/worker/k8s/src/config/arg_files.py          79      0     22      2    98%   134->133, 152->150
charms/worker/k8s/src/config/bootstrap.py         145     24     54      5    78%   65-84, 158-159, 202-203, 205, 223, 252, 266->254
charms/worker/k8s/src/config/cluster.py            63      3     22     11    84%   37, 51->53, 59->61, 63->65, 66, 68, 73->75, 79->81, 85->87, 92->94, 98->100
charms/worker/k8s/src/config/extra_args.py         43      4     20      5    86%   32->27, 44, 45->exit, 82, 100, 116
charms/worker/k8s/src/config/option.py             20      0      8      0   100%
charms/worker/k8s/src/config/resource.py           33      7      4      1    73%   32, 57-58, 63-72
charms/worker/k8s/src/containerd.py               154     15     48     18    83%   45-48, 78->82, 82->85, 85->88, 234->236, 240->243, 243->245, 245->247, 247->250, 262, 269, 276, 327-328, 340-341, 355, 357, 359, 363
charms/worker/k8s/src/cos_integration.py           41     16      4      0    56%   82, 117-234, 238
charms/worker/k8s/src/endpoints.py                 40      1     18      1    97%   62
charms/worker/k8s/src/events/update_status.py     104     19     32      9    78%   93, 99-100, 124, 126, 142, 156-167, 173, 191, 192->194, 198
charms/worker/k8s/src/inspector.py                 62     10     24      6    79%   44-45, 74, 109, 122-123, 132, 138-143
charms/worker/k8s/src/k8s/client.py                20      4      2      1    77%   39->41, 47-48, 50-51
charms/worker/k8s/src/k8s/node.py                  34     14      4      0    53%   47-49, 52, 65-75
charms/worker/k8s/src/k8sd_api_manager.py         411     27     14      2    93%   712, 787-788, 1019, 1062-1064, 1073-1077, 1139, 1208, 1216, 1234, 1247-1248, 1309-1311, 1322-1325, 1342-1346
charms/worker/k8s/src/kube_control.py              44     32     10      0    22%   31-35, 44-90
charms/worker/k8s/src/literals.py                 110      0      0      0   100%
charms/worker/k8s/src/pki.py                       29      9      2      1    68%   35-36, 49-54, 65
charms/worker/k8s/src/protocols.py                 33      5      0      0    85%   60, 68, 76, 84, 92
charms/worker/k8s/src/reschedule.py                79      4      6      3    92%   155->158, 174-175, 191-192, 215->exit, 221->exit
charms/worker/k8s/src/snap.py                     180     16     58     11    89%   110, 188-189, 200-201, 214->207, 232-237, 291-292, 294, 297->288, 304->288, 308, 329-330
charms/worker/k8s/src/token_distributor.py        254    132     68      7    42%   112-114, 128, 182, 186, 211, 236->exit, 267, 287-288, 301-311, 332, 348-361, 407->406, 411, 415->420, 438-472, 506, 519, 529, 550-663, 682-735, 748-750
charms/worker/k8s/src/upgrade.py                  121     38     36      5    64%   81-108, 191->193, 202-205, 208-212, 219->221, 222-225, 242
charms/worker/k8s/src/utils.py                     16     16      4      0     0%   6-47
-------------------------------------------------------------------------------------------
TOTAL                                            2970    841    712    121    67%

Static code analysis report

Run started:2025-11-13 21:57:19.671908

Test results:
  No issues identified.

Code scanned:
  Total lines of code: 6004
  Total lines skipped (#nosec): 4
  Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 0

Run metrics:
  Total issues (by severity):
  	Undefined: 0
  	Low: 0
  	Medium: 0
  	High: 0
  Total issues (by confidence):
  	Undefined: 0
  	Low: 0
  	Medium: 0
  	High: 0
Files skipped (0):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

switching to jubilant for juju4 testing

2 participants