diff --git a/docs/.custom_wordlist.txt b/docs/.custom_wordlist.txt index 3470fed37..7c8f1c564 100644 --- a/docs/.custom_wordlist.txt +++ b/docs/.custom_wordlist.txt @@ -23,6 +23,7 @@ Parca protobuf pydantic pytest +README reinstantiate replan repo diff --git a/docs/explanation/testing.md b/docs/explanation/testing.md index 9f9bff5cf..17d170e8b 100644 --- a/docs/explanation/testing.md +++ b/docs/explanation/testing.md @@ -112,7 +112,7 @@ When writing an integration test, it is not sufficient to simply check that Juju ### Tools - [`pytest`](https://pytest.org/) or [`unittest`](https://docs.python.org/3/library/unittest.html) and -- [Jubilant](https://documentation.ubuntu.com/jubilant/) +- [Jubilant](https://documentation.ubuntu.com/jubilant/), which wraps the Juju CLI, together with [`pytest-jubilant`](https://github.com/canonical/pytest-jubilant), a pytest plugin that manages Juju models during tests Integration tests and unit tests should run using the minor version of Python that is shipped with the OS specified in `charmcraft.yaml` (the `base.run-on` key). For example, if Ubuntu 22.04 is specified in `charmcraft.yaml`, you can use the following tox configuration: @@ -172,7 +172,7 @@ Integration tests are a bit more complex, because these tests require a Juju con channel: 1.32-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 + run: tox -e integration -- --juju-model testing - name: Dump logs uses: canonical/charm-logdump-action@main if: failure() diff --git a/docs/howto/index.md b/docs/howto/index.md index b2590bff3..f22d57f00 100644 --- a/docs/howto/index.md +++ b/docs/howto/index.md @@ -140,7 +140,7 @@ Harness is a deprecated framework for writing unit tests. You should migrate to - {doc}`Migrate unit tests from Harness ` -pytest-operator and python-libjuju are deprecated. You should migrate integration tests to Jubilant. +pytest-operator and python-libjuju are deprecated. You should migrate integration tests to Jubilant and `pytest-jubilant`. - {doc}`Migrate integration tests from pytest-operator ` diff --git a/docs/howto/migrate/index.md b/docs/howto/migrate/index.md index 106ecda33..418c874c5 100644 --- a/docs/howto/migrate/index.md +++ b/docs/howto/migrate/index.md @@ -14,7 +14,7 @@ Migrate unit tests from Harness ## pytest-operator -pytest-operator and python-libjuju are deprecated. You should migrate integration tests to Jubilant. +pytest-operator and python-libjuju are deprecated. You should migrate integration tests to Jubilant and `pytest-jubilant`. ```{toctree} :maxdepth: 1 diff --git a/docs/howto/migrate/migrate-integration-tests-from-pytest-operator.md b/docs/howto/migrate/migrate-integration-tests-from-pytest-operator.md index 01fcb4a6a..67268dab2 100644 --- a/docs/howto/migrate/migrate-integration-tests-from-pytest-operator.md +++ b/docs/howto/migrate/migrate-integration-tests-from-pytest-operator.md @@ -1,10 +1,10 @@ (pytest-operator-migration)= # How to migrate integration tests from pytest-operator -Many charm integration tests use [pytest-operator](https://github.com/charmed-kubernetes/pytest-operator) and [python-libjuju](https://github.com/juju/python-libjuju). This guide explains how to migrate your integration tests from those libraries to Jubilant. +Older charm integration tests use [pytest-operator](https://github.com/charmed-kubernetes/pytest-operator) and [python-libjuju](https://github.com/juju/python-libjuju). This guide explains how to migrate your integration tests from those libraries to Jubilant and [`pytest-jubilant`](https://github.com/canonical/pytest-jubilant). ```{tip} -Try bootstrapping your migration with an AI Agent (such as GitHub Copilot or Claude Code). Instruct the agent to clone the canonical/jubilant and canonical/pytest-jubilant repositories, study them, and then migrate the charm integration tests to Jubilant. You should end up with a great starting point to then continue as outlined in the rest of this guide. +Try bootstrapping your migration with an AI Agent (such as GitHub Copilot or Claude Code). Instruct the agent to clone the `canonical/jubilant` and `canonical/pytest-jubilant` repositories, study them, and then migrate the charm integration tests to Jubilant. You should end up with a great starting point to then continue as outlined in the rest of this guide. ``` To get help while you're migrating tests, please keep the {external+jubilant:doc}`Jubilant API Reference ` handy, and make use of your IDE's autocompletion -- Jubilant tries to provide good type annotations and docstrings. @@ -12,15 +12,14 @@ To get help while you're migrating tests, please keep the {external+jubilant:doc Migrating your tests can be broken into three steps: 1. Update your dependencies -2. Add fixtures to `conftest.py` +2. Provide the resources your tests need 3. Update the tests themselves Let's look at each of these in turn. - ## Update your dependencies -The first thing you'll need to do is add `jubilant` as a dependency to your `tox.ini` or `pyproject.toml` dependencies. +The first thing you'll need to do is add `jubilant` and `pytest-jubilant` as dependencies to your `tox.ini` or `pyproject.toml`. Pin to the current stable major versions, which are maintained with strong backwards compatibility guarantees. You can also remove the dependencies on `juju` (python-libjuju), `pytest-operator`, and `pytest-asyncio`. @@ -32,60 +31,90 @@ If you're using `tox.ini`, the diff might look like: boto3 cosl - juju>=3.0 -+ jubilant~=1.0 ++ jubilant>=1.8,<2 ++ pytest-jubilant>=2,<3 pytest - pytest-operator - pytest-asyncio -r{toxinidir}/requirements.txt ``` -If you're migrating a large number of tests, you may want to do it in stages. In that case, keep the old dependencies in place till the end, and migrate tests one at a time, so that both pytest-operator and Jubilant tests can run together. +If you're migrating a large number of tests, you may want to do it in stages. In that case, keep the old dependencies in place till the end, and migrate tests one at a time, so that both `pytest-operator` and Jubilant tests can run together. Note that `pytest-operator` and `pytest-jubilant` use completely different CLI options, so make sure you provide the correct ones for each if you're trying to do something fancy. +## Provide the resources your tests need -## Add fixtures to `conftest.py` +Your integration tests may use a combination of `pytest-operator` features for the resources they need, including packed charms, Juju models, and deployed applications. This section covers how to provide these resources when writing `Jubilant` based integration tests. -The pytest-operator library includes pytest fixtures, but Jubilant does not include any fixtures, so you'll need to add one or two fixtures to your `conftest.py`. +### Provide packed charms to your Python tests -(a_juju_model_fixture)= -### A `juju` model fixture +`pytest-operator` provided a `build_charm` helper function. `pytest-jubilant` does not provide an equivalent helper, because it's cleaner to keep packing out of your Python integration tests. -Jubilant expects that a Juju controller has already been set up, either using [Concierge](https://github.com/jnsgruk/concierge) or a manual approach. However, you'll want a fixture that creates a temporary model. We recommend naming the fixture `juju`: +In CI, you may already follow a strategy of first packing your charms (in parallel), and then providing the packed charms to your (perhaps also parallelised) integration tests. A good way to provide the charms is via environment variables. -```python +Locally, we recommend decoupling packing from integration testing by performing packing separately. In a simple case, where you have a single charm to test, this can be done with a single `charmcraft pack` command. Your local `integration` step might then look like this: + +```ini +[testenv:integration] +pass_env = + CHARM_PATH +commands = + pytest --tb=native -vv --log-cli-level=DEBUG {toxinidir}/tests/integration {posargs} +``` + +In your integration tests themselves, you should define a fixture for your charm, which reads the environment variable if set, or falls back to looking for the packed charm in its expected location. + +```py # tests/integration/conftest.py +import os +import pathlib -import jubilant -import pytest -@pytest.fixture(scope='module') -def juju(request: pytest.FixtureRequest): - keep_models = bool(request.config.getoption('--keep-models')) +@pytest.fixture(scope="session") +def charm_path(): + # Assuming the current working directory is the charm root: + yield get_charm_path(env_var="CHARM_PATH", default_dir=pathlib.Path()) - with jubilant.temp_model(keep=keep_models) as juju: - juju.wait_timeout = 10 * 60 - yield juju # run the test +def get_charm_path(env_var: str, default_dir: pathlib.Path) -> pathlib.Path: + charm = os.environ.get(env_var) + if not charm: + charms = list(default_dir.glob('*.charm')) + assert charms, f'No charms were found in {default_dir}' + assert len(charms) == 1, f'Found more than one charm {charms}' + charm = charms[0] + path = pathlib.Path(charm).resolve() + assert path.is_file(), f'{path} is not a file' + return path +``` - if request.session.testsfailed: - log = juju.debug_log(limit=1000) - print(log, end='') +#### Packing multiple charms -def pytest_addoption(parser): - parser.addoption( - '--keep-models', - action='store_true', - default=False, - help='keep temporarily-created models', - ) +In a more complicated case where you have multiple charms, you should use `*CHARM_PATH` in `pass_env` instead, and use named environment variables to pass each charm's location (e.g. `FOO_CHARM_PATH`, `BAR_CHARM_PATH`). + +In this case, you'd want one fixture per charm. If you *don't* make these auto-use fixtures, then if you're running tests for just one charm, the other charms won't need to be packed. + +When you have multiple charms, it may be useful to provide a local `pack` step like this: +```ini +[testenv:pack] +commands = + bash -c "cd charms/foo && charmcraft pack" + bash -c "cd charms/bar && charmcraft pack" ``` +(a_juju_model_fixture)= +### The `juju` and `juju_factory` fixtures + +The `pytest-jubilant` plugin provides a module-scoped `juju` fixture that creates a temporary model, destroys it after the tests, and dumps debug logs on failure. It also provides CLI options such as `--no-juju-teardown` (to keep models) and `--juju-model` (to set a custom model name prefix). + +`pytest-jubilant` expects that a Juju controller has already been set up, either using [Concierge](https://github.com/jnsgruk/concierge) or a manual approach. The plugin automatically creates a temporary model per test module and tears it down afterward. + In your tests, use the fixture like this: ```python # tests/integration/test_charm.py -def test_active(juju: jubilant.Juju): - juju.deploy('mycharm') +def test_active(juju: jubilant.Juju, charm_path: pathlib.Path): + juju.deploy(charm_path) juju.wait(jubilant.all_active) # Or wait for just 'mycharm' to be active (ignoring other apps): @@ -94,31 +123,76 @@ def test_active(juju: jubilant.Juju): A few things to note about the fixture: -* It includes a command-line parameter `--keep-models`, to match pytest-operator. If the parameter is set, the fixture keeps the temporary model around after running the tests. -* It sets [`juju.wait_timeout`](jubilant.Juju.wait_timeout) to 10 minutes, to match python-libjuju's default `wait_for_idle` timeout. -* If any of the tests fail, it uses `juju.debug_log` to display the last 1000 lines of `juju debug-log` output. +* To keep models around after running the tests (matching pytest-operator's `--keep-models`), pass `--no-juju-teardown`. +* To match python-libjuju's 10-minute `wait_for_idle` timeout, set `juju.wait_timeout = 10 * 60` in a wrapper fixture or at the start of your test. +* If any of the tests fail, the plugin automatically dumps the last 1000 lines of `juju debug-log` output. * It is module-scoped, like pytest-operator's `ops_test` fixture. This means that a new model is created for every `test_*.py` file, but not for every test. +If your `test_*.py` module needs multiple Juju models (previously managed with `ops_test.track_model`), use the `juju_factory` fixture. This fixture lets you add additional models with their own unique suffixes -- no suffix is equivalent to the `juju` fixture. + +```py +import jubilant +import pytest +import pytest_jubilant + + +@pytest.mark.fixture(scope="module") +def other_model(juju_factory: pytest_jubilant.JujuFactory): + yield juju_factory.get_juju("other") + + +def test_cross_model(juju: jubilant.Juju, other_model: jubilant.Juju): + ... +``` + (how_to_migrate_an_application_fixture)= -### An application fixture +### Application setup -If you don't want to deploy your application in each test, you can add a module-scoped `app` fixture that deploys your charm and waits for it to go active. +A lot of the time, you won't want to deploy your application in each test. In this case, you should use tests marked with `juju_setup`. These can be skipped in subsequent test runs using `--no-juju-setup` if you previously kept your models up and the applications deployed with `--no-juju-teardown`. This corresponds to `pytest-operator`'s `skip_if_deployed` functionality. -The following fixture assumes that the charm has already been packed with `charmcraft pack` in a previous CI step (Jubilant has no equivalent of `ops_test.build_charm`): +```python +# tests/integration/test_actions.py +import pathlib + +import jubilant +import pytest + +APP = 'mycharm' + + +@pytest.mark.juju_setup +def test_deploy(juju: jubilant.Juju, my_charm: pathlib.Path): + juju.deploy(charm_path, APP) + juju.wait(jubilant.all_active) + assert ... + + +@pytest.mark.juju_setup +def test_some_setup_action(juju: jubilant.Juju): + juju.run(f'{APP}/0', 'some-setup-action') + assert ... + + +def test_some_repeatable_action(juju.jubilant.Juju): + task = juju.run(f'{APP}/0', 'some-setup-action') + assert task.results['...'] == '...' +``` + +Alternatively, if you just want your tests to depend on the deployed version of your application, you can write an application fixture. ```python # tests/integration/conftest.py - import pathlib import jubilant import pytest @pytest.fixture(scope='module') -def app(juju: jubilant.Juju): +def app(juju: jubilant.Juju, charm_path: pathlib.Path): + my_app_name = "mycharm" juju.deploy( - charm_path('mycharm'), - 'mycharm', + charm_path, + my_app_name, resources={ 'mycharm-image': 'ghcr.io/canonical/...', }, @@ -130,21 +204,10 @@ def app(juju: jubilant.Juju): ) # ... do any other application setup here ... juju.wait(jubilant.all_active) - - yield 'mycharm' # run the test - - -def charm_path(name: str) -> pathlib.Path: - """Return full absolute path to given test charm.""" - # We're in tests/integration/conftest.py, so parent*3 is repo top level. - charm_dir = pathlib.Path(__file__).parent.parent.parent - charms = [p.absolute() for p in charm_dir.glob(f'{name}_*.charm')] - assert charms, f'{name}_*.charm not found' - assert len(charms) == 1, 'more than one .charm file, unsure which to use' - return charms[0] + yield my_app_name ``` -In your tests, you'll need to specify that the test depends on both fixtures: +In your tests, you'll need to specify that the test depends on `juju` as well as `app` so that you have a reference to a `jubilant.Juju` object managing the correct model. ```python # tests/integration/test_charm.py @@ -154,7 +217,6 @@ def test_active(juju: jubilant.Juju, app: str): assert status.apps[app].is_active ``` - ## Update the tests themselves Many features of pytest-operator and python-libjuju map quite directly to Jubilant, except without using `async`. Here is a summary of what you need to change: diff --git a/docs/howto/write-and-structure-charm-code.md b/docs/howto/write-and-structure-charm-code.md index 19f2881e9..f8d4186c7 100644 --- a/docs/howto/write-and-structure-charm-code.md +++ b/docs/howto/write-and-structure-charm-code.md @@ -100,8 +100,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", ] # Additional groups docs = [ @@ -447,7 +447,7 @@ a cloud in which to deploy it, is required. This example uses [Concierge](https: run: uv tool install tox --with tox-uv - name: Run integration tests # Set a predictable model name so it can be consumed by charm-logdump-action - run: tox -e integration -- --model testing + run: tox -e integration -- --juju-model testing - name: Dump logs uses: canonical/charm-logdump-action@main if: failure() diff --git a/docs/howto/write-integration-tests-for-a-charm.md b/docs/howto/write-integration-tests-for-a-charm.md index 1f8d8a215..255aaa423 100644 --- a/docs/howto/write-integration-tests-for-a-charm.md +++ b/docs/howto/write-integration-tests-for-a-charm.md @@ -87,14 +87,14 @@ commands = {posargs} ``` -Also check that `pyproject.toml` has an `integration` dependency group. Again, if you initialised the charm with `charmcraft init` it should already be there. For example: +Also check that `pyproject.toml` has an `integration` dependency group. Again, if you initialised the charm with `charmcraft init` it should already be there. Integration tests use two packages: {external+jubilant:doc}`Jubilant `, which wraps the Juju CLI, and [`pytest-jubilant`](https://github.com/canonical/pytest-jubilant), a pytest plugin that manages Juju models during tests. Pin to the current stable major versions, which are maintained with strong backwards compatibility guarantees. For example: ```toml [dependency-groups] ... integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", ] ``` @@ -103,33 +103,16 @@ integration = [ ### Write fixtures -In `conftest.py` in your integration test directory, add these fixtures: +The [`pytest-jubilant`](https://github.com/canonical/pytest-jubilant) plugin provides a module-scoped `juju` fixture that creates a temporary Juju model for each test file and destroys the model when the tests have finished. It also dumps debug logs on test failure. This fixture is registered automatically when you install `pytest-jubilant`. In your test code, you use the {external+jubilant:doc}`Jubilant ` API directly — for example, `jubilant.Juju` for type annotations and helpers such as `jubilant.all_active`. + +In `conftest.py` in your integration test directory, add a `charm` fixture: ```python -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): @@ -143,13 +126,9 @@ def charm(): return next(pathlib.Path(".").glob("*.charm")).resolve() ``` -The integration tests will depend on these fixtures. - -These fixtures use [pytest](https://docs.pytest.org/en/stable/) and {external+jubilant:doc}`Jubilant `: - -- The `juju` fixture creates a temporary Juju model for each test file. The [](jubilant.temp_model) context manager creates a randomly-named model on entry, and destroys the model on exit. +The integration tests will depend on this fixture and on the `juju` fixture from `pytest-jubilant`. -- The `charm` fixture finds the charm to deploy (later we'll write a test that deploys the charm). This fixture doesn't pack your charm. You'll need to pack your charm before running the tests. +The `charm` fixture finds the charm to deploy (later we'll write a test that deploys the charm). This fixture doesn't pack your charm. You'll need to pack your charm before running the tests. For general guidance about `conftest.py`, see [conftest.py: sharing fixtures across multiple files](https://docs.pytest.org/en/stable/reference/fixtures.html#conftest-py-sharing-fixtures-across-multiple-files). @@ -184,7 +163,9 @@ For more examples of tests that deploy charms, see: - [cassandra-operator](https://github.com/canonical/cassandra-operator/blob/main/tests/integration/test_charm.py) - [httpbin-demo](https://github.com/canonical/operator/blob/main/examples/httpbin-demo/tests/integration/test_charm.py) -Tests run sequentially in the order they are written in the file. It can be useful to put tests that deploy applications in the top of the file as the applications can be used by other tests. For that reason, adding extra checks or `asserts` in this test is not recommended. +Tests run sequentially in the order they are written in the file. It can be useful to put tests that deploy applications in the top of the file as the applications can be used by other tests. You can mark such tests with `@pytest.mark.juju_setup` -- if you later use `--no-juju-setup` to skip them, the model must already exist (see {ref}`run-your-tests` below). Adding extra checks or `asserts` in deployment tests is not recommended. + +Similarly, if you have tests that perform destructive actions (for example, removing relations or applications), mark them with `@pytest.mark.juju_teardown`. These tests will be skipped when `--no-juju-teardown` is passed. ### Exercise your charm @@ -343,16 +324,20 @@ Jubilant provides an escape hatch to invoke the Juju CLI. This can be useful for ### Use several models -You can use Jubilant with several models, in the same cloud or in -different clouds. This way you can, for example, integrate machine charms -with Kubernetes charms easily. +If you need multiple Juju models in a single test module, use the `juju_factory` fixture provided by `pytest-jubilant`: ```python - model_a = jubilant.Juju("some-model") - model_b = jubilant.Juju("another-controller:a-model") - new_model = jubilant.Juju().add_model("a-model", "some-cloud", controller=..., config=..., credential=...) +import pytest +import pytest_jubilant + + +@pytest.fixture(scope="module") +def other_model(juju_factory: pytest_jubilant.JujuFactory): + return juju_factory.get_juju(suffix="other") ``` +Each call to `get_juju` creates a separate model. You can then use both `juju` and `other_model` in the same test. This is useful for cross-model scenarios, for example integrating machine charms with Kubernetes charms. + > See more: > - {external+juju:ref}`Juju offers ` > - {external+juju:ref}`How to manage clouds ` @@ -411,13 +396,19 @@ CHARM_PATH=/path/to/foo.charm tox -e integration Your tests will use the current Juju controller. By default, a new model will be created for each test module. The model will be destroyed when all the tests in the module have finished. This is determined by the scope of the `juju` fixture. -Use the `--cloud`, `--controller`, and `--model` parameters to specify the cloud, controller, and model name. If you specify the model name and include the `--keep-models` parameter, you can reuse a model from a previous test run. For example: +The `pytest-jubilant` plugin provides several command-line options for controlling model lifecycle, including rerunning tests with the same models and applications, automatically switching to the next `juju` fixture model, and saving the `juju debug-log` before tearing models down. Read more about them in [the repository README](https://github.com/canonical/pytest-jubilant). + +For example, to deploy on a first run and then iterate without redeploying: ```text -# in the initial execution, the new model will be created -tox -e integration -- --keep-models --model test-example-model -# in the next execution it will reuse the model created previously: -tox -e integration -- --keep-models --model test-example-model --no-deploy +# First run: deploy and keep the models +tox -e integration -- --juju-model mytest --no-juju-teardown +# Subsequent runs: skip deployment, reuse the models +tox -e integration -- --juju-model mytest --no-juju-setup --no-juju-teardown +``` + +```{tip} +After each test run, `pytest-jubilant` prints a summary with the exact command-line flags to reuse or keep your models for the next run. ``` There are different ways of specifying a subset of tests to run using `pytest`. With the `-k` option you can specify different expressions. For example, the next command will run all tests in the `test_charm.py` file except `test_one` function. @@ -429,6 +420,26 @@ tox -e integration -- tests/integration/test_charm.py -k "not test_one" > - [`pytest | How to invoke pytest`](https://docs.pytest.org/en/7.1.x/how-to/usage.html) > - [](#validate-your-charm-with-every-change) + +## View Juju logs + +If any tests fail, `pytest-jubilant` automatically prints the last 1000 lines of `juju debug-log` to stderr. You can also save the complete logs to disk with the `--juju-dump-logs` option. + +````{tip} +Use `--juju-dump-logs` in CI with `actions/upload-artifact` to make debug logs available as build artifacts: + +```yaml + # In your integration test job + - run: tox -e integration -- --juju-dump-logs + - name: Upload logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: juju-dump-logs + path: .logs +``` +```` + ## Generate crash dumps To generate crash dumps, you need the `juju-crashdump` tool . diff --git a/docs/index.md b/docs/index.md index 0c6c17e28..706f13b53 100644 --- a/docs/index.md +++ b/docs/index.md @@ -72,7 +72,7 @@ This documentation uses the [Diátaxis documentation structure](https://diataxis * - **[Concierge](https://github.com/canonical/concierge)** - A CLI tool for setting up charm development environments. * - **{external+jubilant:doc}`Jubilant `** - - A Python library that wraps the Juju CLI. Use Jubilant for your integration tests. + - A Python library that wraps the Juju CLI. Use Jubilant together with [`pytest-jubilant`](https://github.com/canonical/pytest-jubilant) for your integration tests. * - **{external+juju:doc}`Juju `** - The orchestration engine and CLI tool. You'll find the {external+juju:ref}`hooks reference ` especially helpful. Juju's hooks correspond to events that your charm can observe. * - **{external+pebble:doc}`Pebble `** diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md index 1e8fb368f..612869bb1 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/create-a-minimal-kubernetes-charm.md @@ -402,7 +402,7 @@ A charm should function correctly not just in a mocked environment, but also in For example, it should be able to pack, deploy, and integrate without throwing exceptions or getting stuck in a `waiting` or a `blocked` status -- that is, it should correctly reach a status of `active` or `idle`. -You can ensure this by writing integration tests for your charm. In the charming world, these are usually written with the [`jubilant`](https://documentation.ubuntu.com/jubilant/) library. +You can ensure this by writing integration tests for your charm. In the charming world, these are usually written with {external+jubilant:doc}`Jubilant ` and [`pytest-jubilant`](https://github.com/canonical/pytest-jubilant). In this section we'll write a small integration test to check that the charm packs and deploys correctly. @@ -434,10 +434,10 @@ def test_deploy(charm: pathlib.Path, juju: jubilant.Juju): juju.wait(jubilant.all_active) ``` -This test depends on two fixtures, which are defined in `tests/integration/conftest.py`: +This test depends on two fixtures: -- `charm` - The `.charm` file to deploy. -- `juju` - A Jubilant object for interacting with a temporary Juju model. +- `charm` - The `.charm` file to deploy. This fixture is defined in `tests/integration/conftest.py`. +- `juju` - A Jubilant object for interacting with a temporary Juju model. This fixture is provided by the `pytest-jubilant` plugin. ### Run the test @@ -447,7 +447,7 @@ Run the following command from anywhere in the `~/fastapi-demo` directory: tox -e integration ``` -The test takes some time to run as Jubilant adds a new model to an existing cluster (whose presence it assumes). If successful, it'll verify that your charm can pack and deploy as expected. +The test takes some time to run as a new Juju model is created and your charm is deployed. If successful, it'll verify that your packed charm can be deployed as expected. The result should be similar to the following output: diff --git a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md index d3f24f1fe..75d18d93b 100644 --- a/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md +++ b/docs/tutorial/from-zero-to-hero-write-your-first-kubernetes-charm/integrate-your-charm-with-postgresql.md @@ -480,7 +480,7 @@ Now run `tox -e unit` to make sure all test cases pass. ## Write an integration test -Now that our charm integrates with the database, if there's not a database relation, the app will be in `blocked` status instead of `active`. Let's tweak our existing integration test `test_deploy` accordingly, setting the expected status as `blocked` in `juju.wait`: +Now that our charm integrates with the database, if there's not a database relation, the app will be in `blocked` status instead of `active`. Let's tweak our existing integration test `test_deploy` accordingly, setting the expected status as `blocked` in `juju.wait`. Replace the contents of `tests/integration/test_charm.py` with: ```python import logging diff --git a/docs/tutorial/write-your-first-machine-charm.md b/docs/tutorial/write-your-first-machine-charm.md index 5fc68572e..a35c631fd 100644 --- a/docs/tutorial/write-your-first-machine-charm.md +++ b/docs/tutorial/write-your-first-machine-charm.md @@ -898,7 +898,7 @@ TOTAL 118 31 26 7 69% Integration tests are an important way to check that your charm works correctly when deployed. In contrast to unit tests, integration tests require Juju to be available, and events aren't simulated. -When you created the initial version of your charm, Charmcraft included integration tests. The tests use {external+jubilant:doc}`Jubilant ` to interact with Juju. We'll expand the tests to cover more of your charm's functionality. +When you created the initial version of your charm, Charmcraft included integration tests. The tests use {external+jubilant:doc}`Jubilant ` to interact with Juju. The [`pytest-jubilant`](https://github.com/canonical/pytest-jubilant) plugin provides the `juju` fixture used in the tests. We'll expand the tests to cover more of your charm's functionality. In `tests/integration/test_charm.py`, change `juju.wait(jubilant.all_active)` to: @@ -931,10 +931,10 @@ def test_block_on_invalid_config(charm: pathlib.Path, juju: jubilant.Juju): juju.config("tinyproxy", reset="slug") ``` -Each test depends on two fixtures, which are defined in `tests/integration/conftest.py`: +Each test depends on two fixtures: -- `charm` - The `.charm` file to deploy. Only `test_deploy` uses `charm`, but it's helpful for each test to depend on `charm`. This ensures that each test fails immediately if a `.charm` file isn't available. -- `juju` - A Jubilant object for interacting with a temporary Juju model. +- `charm` - The `.charm` file to deploy. Only `test_deploy` uses `charm`, but it's helpful for each test to depend on `charm`. This ensures that each test fails immediately if a `.charm` file isn't available. This fixture is defined in `tests/integration/conftest.py`. +- `juju` - A Jubilant object for interacting with a temporary Juju model. This fixture is provided by the `pytest-jubilant` plugin. The `juju` fixture is module-scoped. In other words, each test in `test_charm.py` affects the state of the same Juju model. This means that the order of the tests is significant. This also explains why we reset `slug` at the end of `test_block_on_invalid_config` - to ensure that any subsequent test could assume an unblocked charm. diff --git a/examples/httpbin-demo/pyproject.toml b/examples/httpbin-demo/pyproject.toml index 91f3d037a..04564ecb7 100644 --- a/examples/httpbin-demo/pyproject.toml +++ b/examples/httpbin-demo/pyproject.toml @@ -39,8 +39,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", "pyyaml", ] diff --git a/examples/httpbin-demo/tests/integration/conftest.py b/examples/httpbin-demo/tests/integration/conftest.py index 3b5091db8..3e5974dde 100644 --- a/examples/httpbin-demo/tests/integration/conftest.py +++ b/examples/httpbin-demo/tests/integration/conftest.py @@ -12,30 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): diff --git a/examples/httpbin-demo/uv.lock b/examples/httpbin-demo/uv.lock index 456aab1eb..62980adc1 100644 --- a/examples/httpbin-demo/uv.lock +++ b/examples/httpbin-demo/uv.lock @@ -133,7 +133,7 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "pyyaml" }, ] lint = [ @@ -152,8 +152,8 @@ requires-dist = [{ name = "ops", specifier = ">=2.23,<4" }] [package.metadata.requires-dev] integration = [ - { name = "jubilant" }, - { name = "pytest" }, + { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "pytest-jubilant", specifier = ">=2,<3" }, { name = "pyyaml" }, ] lint = [ @@ -190,14 +190,14 @@ wheels = [ [[package]] name = "jubilant" -version = "1.4.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/1a/ba5838825ac99db4ceec68fa146b594c97b682cf3ccf670b1ecf1261209d/jubilant-1.4.0.tar.gz", hash = "sha256:aa377699a8811fea29bfe0febb6b552d4593c02e666f5ba8c3fba24258700199", size = 27502, upload-time = "2025-08-27T00:11:46.295Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/06/4965ed70d4b9d405c8bdefc4926d94cae202014b4488a25aa8c1fe9fb462/jubilant-1.4.0-py3-none-any.whl", hash = "sha256:1df7eaf125fad8d0d3d35e6d83eca43bfbb7884debcd6c7f4b0822600e2a485c", size = 27185, upload-time = "2025-08-27T00:11:44.851Z" }, + { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, ] [[package]] @@ -313,6 +313,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/d98999f1d0a0c9313154fe7bf8f4e3639729b7052c736f2f12cd7025201c/pytest_jubilant-2.0.0.tar.gz", hash = "sha256:c5f8382ac0b43bca8ef87e309f587e2fc1751ff7b2677dd28a66989b6605e063", size = 16250, upload-time = "2026-03-29T23:39:57.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c3/234c3ec22b230e7e33732f91d19d72ad84101a97353b03be3450e4188c5c/pytest_jubilant-2.0.0-py3-none-any.whl", hash = "sha256:28fcf3750100eda16658c2967af099a39199af9fd102ab3b5ba230a028efec3e", size = 13354, upload-time = "2026-03-29T23:39:56.66Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" diff --git a/examples/k8s-1-minimal/pyproject.toml b/examples/k8s-1-minimal/pyproject.toml index efd533ee1..1f5227c25 100644 --- a/examples/k8s-1-minimal/pyproject.toml +++ b/examples/k8s-1-minimal/pyproject.toml @@ -39,8 +39,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", "PyYAML", ] diff --git a/examples/k8s-1-minimal/tests/integration/conftest.py b/examples/k8s-1-minimal/tests/integration/conftest.py index db029daad..57b8efd4b 100644 --- a/examples/k8s-1-minimal/tests/integration/conftest.py +++ b/examples/k8s-1-minimal/tests/integration/conftest.py @@ -13,32 +13,15 @@ # limitations under the License. # # The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# The pytest-jubilant plugin (https://github.com/canonical/pytest-jubilant) provides a +# module-scoped ``juju`` fixture that creates a temporary Juju model. # To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): diff --git a/examples/k8s-1-minimal/uv.lock b/examples/k8s-1-minimal/uv.lock index cf228ef03..da293a795 100644 --- a/examples/k8s-1-minimal/uv.lock +++ b/examples/k8s-1-minimal/uv.lock @@ -105,39 +105,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "jubilant" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/49/9ea5efac9127c76247d42e286e56e26d9b5c01edbf9f24bcfae9aab3cf81/jubilant-1.3.0.tar.gz", hash = "sha256:ff43d6eb67a986958db6317d7ff3df1c8c160d0c56736628919ac1f7319d444e", size = 26842, upload-time = "2025-07-24T22:31:55.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/97/ad9cbc4718cdc4feed0e841ccb2a3d15de7cb1187d63d1e2ba419cc34f51/jubilant-1.3.0-py3-none-any.whl", hash = "sha256:a5ea4a3bf487ab0286eaad0de9df145761657c08beb834931340b9ebb1f41292", size = 26484, upload-time = "2025-07-24T22:31:54.467Z" }, -] - [[package]] name = "fastapi-demo" version = "0.0.1" @@ -149,7 +116,7 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "pyyaml" }, ] lint = [ @@ -168,8 +135,8 @@ requires-dist = [{ name = "ops", specifier = ">=3,<4" }] [package.metadata.requires-dev] integration = [ - { name = "jubilant" }, - { name = "pytest" }, + { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "pytest-jubilant", specifier = ">=2,<3" }, { name = "pyyaml" }, ] lint = [ @@ -183,6 +150,39 @@ unit = [ { name = "pytest" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jubilant" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -296,6 +296,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/d98999f1d0a0c9313154fe7bf8f4e3639729b7052c736f2f12cd7025201c/pytest_jubilant-2.0.0.tar.gz", hash = "sha256:c5f8382ac0b43bca8ef87e309f587e2fc1751ff7b2677dd28a66989b6605e063", size = 16250, upload-time = "2026-03-29T23:39:57.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c3/234c3ec22b230e7e33732f91d19d72ad84101a97353b03be3450e4188c5c/pytest_jubilant-2.0.0-py3-none-any.whl", hash = "sha256:28fcf3750100eda16658c2967af099a39199af9fd102ab3b5ba230a028efec3e", size = 13354, upload-time = "2026-03-29T23:39:56.66Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" diff --git a/examples/k8s-2-configurable/pyproject.toml b/examples/k8s-2-configurable/pyproject.toml index efd533ee1..1f5227c25 100644 --- a/examples/k8s-2-configurable/pyproject.toml +++ b/examples/k8s-2-configurable/pyproject.toml @@ -39,8 +39,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", "PyYAML", ] diff --git a/examples/k8s-2-configurable/tests/integration/conftest.py b/examples/k8s-2-configurable/tests/integration/conftest.py index db029daad..57b8efd4b 100644 --- a/examples/k8s-2-configurable/tests/integration/conftest.py +++ b/examples/k8s-2-configurable/tests/integration/conftest.py @@ -13,32 +13,15 @@ # limitations under the License. # # The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# The pytest-jubilant plugin (https://github.com/canonical/pytest-jubilant) provides a +# module-scoped ``juju`` fixture that creates a temporary Juju model. # To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): diff --git a/examples/k8s-2-configurable/uv.lock b/examples/k8s-2-configurable/uv.lock index cf228ef03..da293a795 100644 --- a/examples/k8s-2-configurable/uv.lock +++ b/examples/k8s-2-configurable/uv.lock @@ -105,39 +105,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "jubilant" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/49/9ea5efac9127c76247d42e286e56e26d9b5c01edbf9f24bcfae9aab3cf81/jubilant-1.3.0.tar.gz", hash = "sha256:ff43d6eb67a986958db6317d7ff3df1c8c160d0c56736628919ac1f7319d444e", size = 26842, upload-time = "2025-07-24T22:31:55.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/97/ad9cbc4718cdc4feed0e841ccb2a3d15de7cb1187d63d1e2ba419cc34f51/jubilant-1.3.0-py3-none-any.whl", hash = "sha256:a5ea4a3bf487ab0286eaad0de9df145761657c08beb834931340b9ebb1f41292", size = 26484, upload-time = "2025-07-24T22:31:54.467Z" }, -] - [[package]] name = "fastapi-demo" version = "0.0.1" @@ -149,7 +116,7 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "pyyaml" }, ] lint = [ @@ -168,8 +135,8 @@ requires-dist = [{ name = "ops", specifier = ">=3,<4" }] [package.metadata.requires-dev] integration = [ - { name = "jubilant" }, - { name = "pytest" }, + { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "pytest-jubilant", specifier = ">=2,<3" }, { name = "pyyaml" }, ] lint = [ @@ -183,6 +150,39 @@ unit = [ { name = "pytest" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jubilant" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -296,6 +296,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/d98999f1d0a0c9313154fe7bf8f4e3639729b7052c736f2f12cd7025201c/pytest_jubilant-2.0.0.tar.gz", hash = "sha256:c5f8382ac0b43bca8ef87e309f587e2fc1751ff7b2677dd28a66989b6605e063", size = 16250, upload-time = "2026-03-29T23:39:57.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c3/234c3ec22b230e7e33732f91d19d72ad84101a97353b03be3450e4188c5c/pytest_jubilant-2.0.0-py3-none-any.whl", hash = "sha256:28fcf3750100eda16658c2967af099a39199af9fd102ab3b5ba230a028efec3e", size = 13354, upload-time = "2026-03-29T23:39:56.66Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" diff --git a/examples/k8s-3-postgresql/pyproject.toml b/examples/k8s-3-postgresql/pyproject.toml index efd533ee1..1f5227c25 100644 --- a/examples/k8s-3-postgresql/pyproject.toml +++ b/examples/k8s-3-postgresql/pyproject.toml @@ -39,8 +39,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", "PyYAML", ] diff --git a/examples/k8s-3-postgresql/tests/integration/conftest.py b/examples/k8s-3-postgresql/tests/integration/conftest.py index db029daad..57b8efd4b 100644 --- a/examples/k8s-3-postgresql/tests/integration/conftest.py +++ b/examples/k8s-3-postgresql/tests/integration/conftest.py @@ -13,32 +13,15 @@ # limitations under the License. # # The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# The pytest-jubilant plugin (https://github.com/canonical/pytest-jubilant) provides a +# module-scoped ``juju`` fixture that creates a temporary Juju model. # To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): diff --git a/examples/k8s-3-postgresql/uv.lock b/examples/k8s-3-postgresql/uv.lock index cf228ef03..da293a795 100644 --- a/examples/k8s-3-postgresql/uv.lock +++ b/examples/k8s-3-postgresql/uv.lock @@ -105,39 +105,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "jubilant" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/49/9ea5efac9127c76247d42e286e56e26d9b5c01edbf9f24bcfae9aab3cf81/jubilant-1.3.0.tar.gz", hash = "sha256:ff43d6eb67a986958db6317d7ff3df1c8c160d0c56736628919ac1f7319d444e", size = 26842, upload-time = "2025-07-24T22:31:55.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/97/ad9cbc4718cdc4feed0e841ccb2a3d15de7cb1187d63d1e2ba419cc34f51/jubilant-1.3.0-py3-none-any.whl", hash = "sha256:a5ea4a3bf487ab0286eaad0de9df145761657c08beb834931340b9ebb1f41292", size = 26484, upload-time = "2025-07-24T22:31:54.467Z" }, -] - [[package]] name = "fastapi-demo" version = "0.0.1" @@ -149,7 +116,7 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "pyyaml" }, ] lint = [ @@ -168,8 +135,8 @@ requires-dist = [{ name = "ops", specifier = ">=3,<4" }] [package.metadata.requires-dev] integration = [ - { name = "jubilant" }, - { name = "pytest" }, + { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "pytest-jubilant", specifier = ">=2,<3" }, { name = "pyyaml" }, ] lint = [ @@ -183,6 +150,39 @@ unit = [ { name = "pytest" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jubilant" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -296,6 +296,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/d98999f1d0a0c9313154fe7bf8f4e3639729b7052c736f2f12cd7025201c/pytest_jubilant-2.0.0.tar.gz", hash = "sha256:c5f8382ac0b43bca8ef87e309f587e2fc1751ff7b2677dd28a66989b6605e063", size = 16250, upload-time = "2026-03-29T23:39:57.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c3/234c3ec22b230e7e33732f91d19d72ad84101a97353b03be3450e4188c5c/pytest_jubilant-2.0.0-py3-none-any.whl", hash = "sha256:28fcf3750100eda16658c2967af099a39199af9fd102ab3b5ba230a028efec3e", size = 13354, upload-time = "2026-03-29T23:39:56.66Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" diff --git a/examples/k8s-4-action/pyproject.toml b/examples/k8s-4-action/pyproject.toml index efd533ee1..1f5227c25 100644 --- a/examples/k8s-4-action/pyproject.toml +++ b/examples/k8s-4-action/pyproject.toml @@ -39,8 +39,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", "PyYAML", ] diff --git a/examples/k8s-4-action/tests/integration/conftest.py b/examples/k8s-4-action/tests/integration/conftest.py index db029daad..57b8efd4b 100644 --- a/examples/k8s-4-action/tests/integration/conftest.py +++ b/examples/k8s-4-action/tests/integration/conftest.py @@ -13,32 +13,15 @@ # limitations under the License. # # The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# The pytest-jubilant plugin (https://github.com/canonical/pytest-jubilant) provides a +# module-scoped ``juju`` fixture that creates a temporary Juju model. # To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): diff --git a/examples/k8s-4-action/uv.lock b/examples/k8s-4-action/uv.lock index cf228ef03..da293a795 100644 --- a/examples/k8s-4-action/uv.lock +++ b/examples/k8s-4-action/uv.lock @@ -105,39 +105,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, ] -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "jubilant" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/49/9ea5efac9127c76247d42e286e56e26d9b5c01edbf9f24bcfae9aab3cf81/jubilant-1.3.0.tar.gz", hash = "sha256:ff43d6eb67a986958db6317d7ff3df1c8c160d0c56736628919ac1f7319d444e", size = 26842, upload-time = "2025-07-24T22:31:55.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/97/ad9cbc4718cdc4feed0e841ccb2a3d15de7cb1187d63d1e2ba419cc34f51/jubilant-1.3.0-py3-none-any.whl", hash = "sha256:a5ea4a3bf487ab0286eaad0de9df145761657c08beb834931340b9ebb1f41292", size = 26484, upload-time = "2025-07-24T22:31:54.467Z" }, -] - [[package]] name = "fastapi-demo" version = "0.0.1" @@ -149,7 +116,7 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "pyyaml" }, ] lint = [ @@ -168,8 +135,8 @@ requires-dist = [{ name = "ops", specifier = ">=3,<4" }] [package.metadata.requires-dev] integration = [ - { name = "jubilant" }, - { name = "pytest" }, + { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "pytest-jubilant", specifier = ">=2,<3" }, { name = "pyyaml" }, ] lint = [ @@ -183,6 +150,39 @@ unit = [ { name = "pytest" }, ] +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jubilant" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -296,6 +296,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/d98999f1d0a0c9313154fe7bf8f4e3639729b7052c736f2f12cd7025201c/pytest_jubilant-2.0.0.tar.gz", hash = "sha256:c5f8382ac0b43bca8ef87e309f587e2fc1751ff7b2677dd28a66989b6605e063", size = 16250, upload-time = "2026-03-29T23:39:57.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c3/234c3ec22b230e7e33732f91d19d72ad84101a97353b03be3450e4188c5c/pytest_jubilant-2.0.0-py3-none-any.whl", hash = "sha256:28fcf3750100eda16658c2967af099a39199af9fd102ab3b5ba230a028efec3e", size = 13354, upload-time = "2026-03-29T23:39:56.66Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" diff --git a/examples/k8s-5-observe/pyproject.toml b/examples/k8s-5-observe/pyproject.toml index 3f4fed20b..ac1a217fe 100644 --- a/examples/k8s-5-observe/pyproject.toml +++ b/examples/k8s-5-observe/pyproject.toml @@ -40,8 +40,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", "PyYAML", ] diff --git a/examples/k8s-5-observe/tests/integration/conftest.py b/examples/k8s-5-observe/tests/integration/conftest.py index db029daad..57b8efd4b 100644 --- a/examples/k8s-5-observe/tests/integration/conftest.py +++ b/examples/k8s-5-observe/tests/integration/conftest.py @@ -13,32 +13,15 @@ # limitations under the License. # # The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# The pytest-jubilant plugin (https://github.com/canonical/pytest-jubilant) provides a +# module-scoped ``juju`` fixture that creates a temporary Juju model. # To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): diff --git a/examples/k8s-5-observe/uv.lock b/examples/k8s-5-observe/uv.lock index f5969c7d5..612a1b37f 100644 --- a/examples/k8s-5-observe/uv.lock +++ b/examples/k8s-5-observe/uv.lock @@ -142,7 +142,7 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "pytest" }, + { name = "pytest-jubilant" }, { name = "pyyaml" }, ] lint = [ @@ -164,8 +164,8 @@ requires-dist = [ [package.metadata.requires-dev] integration = [ - { name = "jubilant" }, - { name = "pytest" }, + { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "pytest-jubilant", specifier = ">=2,<3" }, { name = "pyyaml" }, ] lint = [ @@ -202,14 +202,14 @@ wheels = [ [[package]] name = "jubilant" -version = "1.3.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/49/9ea5efac9127c76247d42e286e56e26d9b5c01edbf9f24bcfae9aab3cf81/jubilant-1.3.0.tar.gz", hash = "sha256:ff43d6eb67a986958db6317d7ff3df1c8c160d0c56736628919ac1f7319d444e", size = 26842, upload-time = "2025-07-24T22:31:55.833Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/97/ad9cbc4718cdc4feed0e841ccb2a3d15de7cb1187d63d1e2ba419cc34f51/jubilant-1.3.0-py3-none-any.whl", hash = "sha256:a5ea4a3bf487ab0286eaad0de9df145761657c08beb834931340b9ebb1f41292", size = 26484, upload-time = "2025-07-24T22:31:54.467Z" }, + { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, ] [[package]] @@ -458,6 +458,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/d98999f1d0a0c9313154fe7bf8f4e3639729b7052c736f2f12cd7025201c/pytest_jubilant-2.0.0.tar.gz", hash = "sha256:c5f8382ac0b43bca8ef87e309f587e2fc1751ff7b2677dd28a66989b6605e063", size = 16250, upload-time = "2026-03-29T23:39:57.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c3/234c3ec22b230e7e33732f91d19d72ad84101a97353b03be3450e4188c5c/pytest_jubilant-2.0.0-py3-none-any.whl", hash = "sha256:28fcf3750100eda16658c2967af099a39199af9fd102ab3b5ba230a028efec3e", size = 13354, upload-time = "2026-03-29T23:39:56.66Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" diff --git a/examples/machine-tinyproxy/pyproject.toml b/examples/machine-tinyproxy/pyproject.toml index 8dc0bee84..4973a698f 100644 --- a/examples/machine-tinyproxy/pyproject.toml +++ b/examples/machine-tinyproxy/pyproject.toml @@ -42,8 +42,8 @@ unit = [ ] # Dependencies of integration tests integration = [ - "jubilant", - "pytest", + "jubilant>=1.8,<2", + "pytest-jubilant>=2,<3", ] # Testing tools configuration diff --git a/examples/machine-tinyproxy/tests/integration/conftest.py b/examples/machine-tinyproxy/tests/integration/conftest.py index c8f3c4a50..80fa97b51 100644 --- a/examples/machine-tinyproxy/tests/integration/conftest.py +++ b/examples/machine-tinyproxy/tests/integration/conftest.py @@ -13,32 +13,15 @@ # limitations under the License. # # The integration tests use the Jubilant library. See https://documentation.ubuntu.com/jubilant/ +# The pytest-jubilant plugin (https://github.com/canonical/pytest-jubilant) provides a +# module-scoped ``juju`` fixture that creates a temporary Juju model. # To learn more about testing, see https://documentation.ubuntu.com/ops/latest/explanation/testing/ -import logging import os import pathlib -import sys -import time -import jubilant import pytest -logger = logging.getLogger(__name__) - - -@pytest.fixture(scope="module") -def juju(request: pytest.FixtureRequest): - """Create a temporary Juju model for running tests.""" - with jubilant.temp_model() as juju: - yield juju - - if request.session.testsfailed: - logger.info("Collecting Juju logs...") - time.sleep(0.5) # Wait for Juju to process logs. - log = juju.debug_log(limit=1000) - print(log, end="", file=sys.stderr) - @pytest.fixture(scope="session") def charm(): diff --git a/examples/machine-tinyproxy/uv.lock b/examples/machine-tinyproxy/uv.lock index e6181a0cc..365b9d362 100644 --- a/examples/machine-tinyproxy/uv.lock +++ b/examples/machine-tinyproxy/uv.lock @@ -161,14 +161,14 @@ wheels = [ [[package]] name = "jubilant" -version = "1.3.0" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/49/9ea5efac9127c76247d42e286e56e26d9b5c01edbf9f24bcfae9aab3cf81/jubilant-1.3.0.tar.gz", hash = "sha256:ff43d6eb67a986958db6317d7ff3df1c8c160d0c56736628919ac1f7319d444e", size = 26842, upload-time = "2025-07-24T22:31:55.833Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/1b/32b5ab87c138066c80e34c8cd0f7d34760ce9d203d89ec88f979039e015d/jubilant-1.8.0.tar.gz", hash = "sha256:a7cea68299dca94fac3e121a8c8ed92021d20aa2f4461e16822abbcd134d0b8b", size = 33036, upload-time = "2026-03-30T07:42:23.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/97/ad9cbc4718cdc4feed0e841ccb2a3d15de7cb1187d63d1e2ba419cc34f51/jubilant-1.3.0-py3-none-any.whl", hash = "sha256:a5ea4a3bf487ab0286eaad0de9df145761657c08beb834931340b9ebb1f41292", size = 26484, upload-time = "2025-07-24T22:31:54.467Z" }, + { url = "https://files.pythonhosted.org/packages/87/6b/71ceb8de590d02eccf18cca60cb687838b12db0926a6a870c2d17a0d26b3/jubilant-1.8.0-py3-none-any.whl", hash = "sha256:e0495ee645de5f2df81d044a3b6e2827b5a6277de02c6d30935b9632bd868a98", size = 33929, upload-time = "2026-03-30T07:42:22.419Z" }, ] [[package]] @@ -413,6 +413,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, ] +[[package]] +name = "pytest-jubilant" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jubilant" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/b6/d98999f1d0a0c9313154fe7bf8f4e3639729b7052c736f2f12cd7025201c/pytest_jubilant-2.0.0.tar.gz", hash = "sha256:c5f8382ac0b43bca8ef87e309f587e2fc1751ff7b2677dd28a66989b6605e063", size = 16250, upload-time = "2026-03-29T23:39:57.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/c3/234c3ec22b230e7e33732f91d19d72ad84101a97353b03be3450e4188c5c/pytest_jubilant-2.0.0-py3-none-any.whl", hash = "sha256:28fcf3750100eda16658c2967af099a39199af9fd102ab3b5ba230a028efec3e", size = 13354, upload-time = "2026-03-29T23:39:56.66Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -497,7 +510,7 @@ dependencies = [ [package.dev-dependencies] integration = [ { name = "jubilant" }, - { name = "pytest" }, + { name = "pytest-jubilant" }, ] lint = [ { name = "codespell" }, @@ -520,8 +533,8 @@ requires-dist = [ [package.metadata.requires-dev] integration = [ - { name = "jubilant" }, - { name = "pytest" }, + { name = "jubilant", specifier = ">=1.8,<2" }, + { name = "pytest-jubilant", specifier = ">=2,<3" }, ] lint = [ { name = "codespell" },