From 45bd7678cc6153293f19041b620f32d0c52e1516 Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Tue, 7 Apr 2026 20:54:58 -0400 Subject: [PATCH 01/10] feat: add prune-instances command --- craft_application/commands/__init__.py | 2 + craft_application/commands/other.py | 2 + craft_application/commands/prune_instances.py | 60 +++++++++++++++++++ craft_application/services/provider.py | 37 ++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 craft_application/commands/prune_instances.py diff --git a/craft_application/commands/__init__.py b/craft_application/commands/__init__.py index d199335b1..476ae6fd6 100644 --- a/craft_application/commands/__init__.py +++ b/craft_application/commands/__init__.py @@ -20,12 +20,14 @@ from .init import InitCommand from .lifecycle import get_lifecycle_command_group, LifecycleCommand, TestCommand from .other import get_other_command_group +from .prune_instances import PruneInstancesCommand from .remote import RemoteBuild # Not part of the default commands. __all__ = [ "AppCommand", "ExtensibleCommand", "InitCommand", + "PruneInstancesCommand", "RemoteBuild", "lifecycle", "LifecycleCommand", diff --git a/craft_application/commands/other.py b/craft_application/commands/other.py index 38b6de590..4c15a3bee 100644 --- a/craft_application/commands/other.py +++ b/craft_application/commands/other.py @@ -20,6 +20,7 @@ from craft_cli import CommandGroup, emit from . import InitCommand, base +from .prune_instances import PruneInstancesCommand if TYPE_CHECKING: # pragma: no cover import argparse @@ -29,6 +30,7 @@ def get_other_command_group() -> CommandGroup: """Return the lifecycle related command group.""" commands: list[type[base.AppCommand]] = [ InitCommand, + PruneInstancesCommand, VersionCommand, ] diff --git a/craft_application/commands/prune_instances.py b/craft_application/commands/prune_instances.py new file mode 100644 index 000000000..0493e89c5 --- /dev/null +++ b/craft_application/commands/prune_instances.py @@ -0,0 +1,60 @@ +# Copyright 2026 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program. If not, see . +"""Provider-related commands.""" + +from __future__ import annotations + +import textwrap +from typing import TYPE_CHECKING + +from . import base + +if TYPE_CHECKING: # pragma: no cover + import argparse + + +class PruneInstancesCommand(base.AppCommand): + """Prune instances for the active provider.""" + + name = "prune-instances" + help_msg = "Prune instances for the active provider" + overview = textwrap.dedent( + """ + Prune instances for the active provider. + """ + ) + + def _fill_parser(self, parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--all-providers", + action="store_true", + help="Prune instances from all providers", + ) + parser.add_argument( + "--provider", + help="Prune for a specific provider", + ) + parser.add_argument( + "--include-templates", + action="store_true", + help="Prune base instances (templates)", + ) + + def run(self, parsed_args: argparse.Namespace) -> None: + """Run the prune-instances command.""" + self._services.provider.prune_instances( + all_providers=parsed_args.all_providers, + provider_name=parsed_args.provider, + include_templates=parsed_args.include_templates, + ) diff --git a/craft_application/services/provider.py b/craft_application/services/provider.py index 0bfae3f8d..7c7842bef 100644 --- a/craft_application/services/provider.py +++ b/craft_application/services/provider.py @@ -597,3 +597,40 @@ def prepare_instance(instance: craft_providers.Executor) -> None: finally: if active_fetch_service: self._services.get("fetch").teardown_instance() + + def prune_instances( + self, + *, + all_providers: bool = False, + provider_name: str | None = None, + include_templates: bool = False, + ) -> None: + """Prune instances and optionally templates for the provider(s). + + :param all_providers: Whether to prune instances for all providers or just the + current provider. + :param provider_name: Optional name of the provider to prune (if all_providers + is False). + :param include_templates: Whether to also prune templates (if supported by the + provider). + """ + providers: list[craft_providers.Provider] = [] + if all_providers: + try: + providers.append(self._get_provider_by_name("lxd")) + providers.append(self._get_provider_by_name("LXD")) + except RuntimeError: + emit.debug("LXD provider not available, skipping.") + + try: + providers.append(self._get_provider_by_name("multipass")) + providers.append(self._get_provider_by_name("Multipass")) + except RuntimeError: + emit.debug("Multipass provider not available, skipping.") + + else: + providers.append(self.get_provider(name=provider_name)) + + for provider in providers: + emit.progress(f"Pruning instances for provider {provider.name!r}...") + provider.prune(prune_templates=include_templates) From 43a82507dc98638dea99ca40a82687bf3091ac20 Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Fri, 10 Apr 2026 12:31:08 -0400 Subject: [PATCH 02/10] tests: add test cases for prune_instances --- tests/unit/commands/test_other.py | 4 +- tests/unit/commands/test_prune_instances.py | 36 +++++++++++++++ tests/unit/services/test_provider.py | 51 +++++++++++++++++++++ 3 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 tests/unit/commands/test_prune_instances.py diff --git a/tests/unit/commands/test_other.py b/tests/unit/commands/test_other.py index d8a936ff0..25cd8abc7 100644 --- a/tests/unit/commands/test_other.py +++ b/tests/unit/commands/test_other.py @@ -18,10 +18,10 @@ import argparse import pytest -from craft_application.commands import InitCommand +from craft_application.commands import InitCommand, PruneInstancesCommand from craft_application.commands.other import VersionCommand, get_other_command_group -OTHER_COMMANDS = {InitCommand, VersionCommand} +OTHER_COMMANDS = {InitCommand, VersionCommand, PruneInstancesCommand} @pytest.mark.parametrize("commands", [OTHER_COMMANDS]) diff --git a/tests/unit/commands/test_prune_instances.py b/tests/unit/commands/test_prune_instances.py new file mode 100644 index 000000000..3555e9fde --- /dev/null +++ b/tests/unit/commands/test_prune_instances.py @@ -0,0 +1,36 @@ +# This file is part of craft-application. +# +# Copyright 2026 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. +# See the GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with this program. If not, see . +"""Tests for prune-instances command.""" + +import argparse + +from craft_application.commands import PruneInstancesCommand + + +def test_prune_instances_run(app_metadata, mock_services): + parsed_args = argparse.Namespace( + all_providers=True, + provider="lxd", + include_templates=True, + ) + command = PruneInstancesCommand({"app": app_metadata, "services": mock_services}) + command.run(parsed_args) + + mock_services.provider.prune_instances.assert_called_once_with( + all_providers=True, + provider_name="lxd", + include_templates=True, + ) diff --git a/tests/unit/services/test_provider.py b/tests/unit/services/test_provider.py index a2fb5311b..6da264934 100644 --- a/tests/unit/services/test_provider.py +++ b/tests/unit/services/test_provider.py @@ -1174,3 +1174,54 @@ def test_configure_instance_with_pro_skipped(mocker, provider_service): mock_instance.install_pro_client.assert_not_called() mock_instance.attach_pro_subscription.assert_not_called() mock_instance.enable_pro_service.assert_not_called() + + +def test_prune_instances_single_provider(monkeypatch, provider_service): + """Prune for a single provider.""" + mock_lxd = mock.MagicMock() + mock_lxd.name = "lxd" + + monkeypatch.setattr(provider_service, "get_provider", lambda name: mock_lxd) + + provider_service.prune_instances(provider_name="lxd", include_templates=True) + + mock_lxd.prune.assert_called_once_with(include_templates=True) + + +def test_prune_instances_all_providers(monkeypatch, provider_service): + """Prune for all supported providers.""" + mock_lxd = mock.MagicMock() + mock_lxd.name = "lxd" + mock_multipass = mock.MagicMock() + mock_multipass.name = "multipass" + + def mock_get_by_name(name): + if name == "lxd": + return mock_lxd + if name == "multipass": + return mock_multipass + raise RuntimeError(f"Unknown provider: {name}") + + monkeypatch.setattr(provider_service, "_get_provider_by_name", mock_get_by_name) + + provider_service.prune_instances(all_providers=True, include_templates=False) + + mock_lxd.prune.assert_called_once_with(include_templates=False) + mock_multipass.prune.assert_called_once_with(include_templates=False) + + +def test_prune_instances_all_providers_skips_unsupported(monkeypatch, provider_service): + """Prune for all supported providers, skipping those not supported on the host.""" + mock_lxd = mock.MagicMock() + mock_lxd.name = "lxd" + + def mock_get_by_name(name): + if name == "lxd": + return mock_lxd + raise RuntimeError("Unsupported on this platform") + + monkeypatch.setattr(provider_service, "_get_provider_by_name", mock_get_by_name) + + provider_service.prune_instances(all_providers=True) + + mock_lxd.prune.assert_called_once_with(include_templates=False) From 91c8ffb53a7bf2176128d75784984359d4078cae Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Fri, 10 Apr 2026 16:30:44 -0400 Subject: [PATCH 03/10] refactor: use prune_templates instead of include_templates for consistency --- craft_application/commands/prune_instances.py | 2 +- craft_application/services/provider.py | 6 +++--- tests/unit/commands/test_prune_instances.py | 4 ++-- tests/unit/services/test_provider.py | 12 ++++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/craft_application/commands/prune_instances.py b/craft_application/commands/prune_instances.py index 0493e89c5..cc1052f81 100644 --- a/craft_application/commands/prune_instances.py +++ b/craft_application/commands/prune_instances.py @@ -56,5 +56,5 @@ def run(self, parsed_args: argparse.Namespace) -> None: self._services.provider.prune_instances( all_providers=parsed_args.all_providers, provider_name=parsed_args.provider, - include_templates=parsed_args.include_templates, + prune_templates=parsed_args.include_templates, ) diff --git a/craft_application/services/provider.py b/craft_application/services/provider.py index 7c7842bef..3df9922e7 100644 --- a/craft_application/services/provider.py +++ b/craft_application/services/provider.py @@ -603,7 +603,7 @@ def prune_instances( *, all_providers: bool = False, provider_name: str | None = None, - include_templates: bool = False, + prune_templates: bool = False, ) -> None: """Prune instances and optionally templates for the provider(s). @@ -611,7 +611,7 @@ def prune_instances( current provider. :param provider_name: Optional name of the provider to prune (if all_providers is False). - :param include_templates: Whether to also prune templates (if supported by the + :param prune_templates: Whether to also prune templates (if supported by the provider). """ providers: list[craft_providers.Provider] = [] @@ -633,4 +633,4 @@ def prune_instances( for provider in providers: emit.progress(f"Pruning instances for provider {provider.name!r}...") - provider.prune(prune_templates=include_templates) + provider.prune(prune_templates=prune_templates) diff --git a/tests/unit/commands/test_prune_instances.py b/tests/unit/commands/test_prune_instances.py index 3555e9fde..4be1f814e 100644 --- a/tests/unit/commands/test_prune_instances.py +++ b/tests/unit/commands/test_prune_instances.py @@ -24,7 +24,7 @@ def test_prune_instances_run(app_metadata, mock_services): parsed_args = argparse.Namespace( all_providers=True, provider="lxd", - include_templates=True, + prune_templates=True, ) command = PruneInstancesCommand({"app": app_metadata, "services": mock_services}) command.run(parsed_args) @@ -32,5 +32,5 @@ def test_prune_instances_run(app_metadata, mock_services): mock_services.provider.prune_instances.assert_called_once_with( all_providers=True, provider_name="lxd", - include_templates=True, + prune_templates=True, ) diff --git a/tests/unit/services/test_provider.py b/tests/unit/services/test_provider.py index 6da264934..02c16e63b 100644 --- a/tests/unit/services/test_provider.py +++ b/tests/unit/services/test_provider.py @@ -1183,9 +1183,9 @@ def test_prune_instances_single_provider(monkeypatch, provider_service): monkeypatch.setattr(provider_service, "get_provider", lambda name: mock_lxd) - provider_service.prune_instances(provider_name="lxd", include_templates=True) + provider_service.prune_instances(provider_name="lxd", prune_templates=True) - mock_lxd.prune.assert_called_once_with(include_templates=True) + mock_lxd.prune.assert_called_once_with(prune_templates=True) def test_prune_instances_all_providers(monkeypatch, provider_service): @@ -1204,10 +1204,10 @@ def mock_get_by_name(name): monkeypatch.setattr(provider_service, "_get_provider_by_name", mock_get_by_name) - provider_service.prune_instances(all_providers=True, include_templates=False) + provider_service.prune_instances(all_providers=True, prune_templates=False) - mock_lxd.prune.assert_called_once_with(include_templates=False) - mock_multipass.prune.assert_called_once_with(include_templates=False) + mock_lxd.prune.assert_called_once_with(prune_templates=False) + mock_multipass.prune.assert_called_once_with(prune_templates=False) def test_prune_instances_all_providers_skips_unsupported(monkeypatch, provider_service): @@ -1224,4 +1224,4 @@ def mock_get_by_name(name): provider_service.prune_instances(all_providers=True) - mock_lxd.prune.assert_called_once_with(include_templates=False) + mock_lxd.prune.assert_called_once_with(prune_templates=False) From fef77965a7f79cc08326d54a4cd9584b3c122fa9 Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Wed, 15 Apr 2026 13:35:06 -0400 Subject: [PATCH 04/10] refactor prune_instances command --- craft_application/commands/prune_instances.py | 20 +++++----- craft_application/services/provider.py | 40 +++++++++++++------ 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/craft_application/commands/prune_instances.py b/craft_application/commands/prune_instances.py index cc1052f81..66e825051 100644 --- a/craft_application/commands/prune_instances.py +++ b/craft_application/commands/prune_instances.py @@ -28,27 +28,25 @@ class PruneInstancesCommand(base.AppCommand): """Prune instances for the active provider.""" name = "prune-instances" - help_msg = "Prune instances for the active provider" + help_msg = "Prune instances for the selected provider or all providers" overview = textwrap.dedent( """ - Prune instances for the active provider. + Prune instances using the standard provider-selection logic. + Use --provider to prune instances for a specific provider, or + --all-providers to prune instances from all providers. """ ) def _fill_parser(self, parser: argparse.ArgumentParser) -> None: - parser.add_argument( + provider_group = parser.add_mutually_exclusive_group() + provider_group.add_argument( "--all-providers", action="store_true", help="Prune instances from all providers", ) - parser.add_argument( + provider_group.add_argument( "--provider", - help="Prune for a specific provider", - ) - parser.add_argument( - "--include-templates", - action="store_true", - help="Prune base instances (templates)", + help="Prune instances for a specific provider; omit to use the standard provider-selection logic", ) def run(self, parsed_args: argparse.Namespace) -> None: @@ -56,5 +54,5 @@ def run(self, parsed_args: argparse.Namespace) -> None: self._services.provider.prune_instances( all_providers=parsed_args.all_providers, provider_name=parsed_args.provider, - prune_templates=parsed_args.include_templates, + prune_templates=parsed_args.prune_templates, ) diff --git a/craft_application/services/provider.py b/craft_application/services/provider.py index 3df9922e7..dcb3b5b5e 100644 --- a/craft_application/services/provider.py +++ b/craft_application/services/provider.py @@ -615,22 +615,36 @@ def prune_instances( provider). """ providers: list[craft_providers.Provider] = [] - if all_providers: - try: - providers.append(self._get_provider_by_name("lxd")) - providers.append(self._get_provider_by_name("LXD")) - except RuntimeError: - emit.debug("LXD provider not available, skipping.") - - try: - providers.append(self._get_provider_by_name("multipass")) - providers.append(self._get_provider_by_name("Multipass")) - except RuntimeError: - emit.debug("Multipass provider not available, skipping.") - else: + if provider_name: providers.append(self.get_provider(name=provider_name)) + elif all_providers: + self.add_provider_if_available( + providers, "lxd", "LXD provider not available, skipping LXD pruning." + ) + self.add_provider_if_available( + providers, + "multipass", + "Multipass provider not available, skipping multipass pruning.", + ) + else: + providers.append(self.get_provider()) for provider in providers: emit.progress(f"Pruning instances for provider {provider.name!r}...") provider.prune(prune_templates=prune_templates) + + def add_provider_if_available( + self, + providers: list[craft_providers.Provider], + provider_key: str, + unavailable_message: str, + ) -> None: + """Add provider to the providers list if it is available, otherwise logs the unavailable_message.""" + try: + provider = self._get_provider_by_name(provider_key) + provider.ensure_provider_is_available() + except (craft_providers.ProviderError, RuntimeError): + emit.debug(unavailable_message) + return + providers.append(provider) From 5b38cc6f3a6883cfbcc2946625230d9206f86fde Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Wed, 15 Apr 2026 13:45:46 -0400 Subject: [PATCH 05/10] give choices for prune_instances command for selecting provider --- craft_application/commands/prune_instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/craft_application/commands/prune_instances.py b/craft_application/commands/prune_instances.py index 66e825051..0ae3500d4 100644 --- a/craft_application/commands/prune_instances.py +++ b/craft_application/commands/prune_instances.py @@ -47,6 +47,7 @@ def _fill_parser(self, parser: argparse.ArgumentParser) -> None: provider_group.add_argument( "--provider", help="Prune instances for a specific provider; omit to use the standard provider-selection logic", + choices=["lxd", "multipass"], ) def run(self, parsed_args: argparse.Namespace) -> None: From dbc006877930dd4ac3b734e2e4c68ba38f2c54ae Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Wed, 15 Apr 2026 14:29:27 -0400 Subject: [PATCH 06/10] add test spread for prune instances --- .../testcraft/prune-instances/task.yaml | 22 +++++++++++++++++++ .../testcraft/prune-instances/testcraft.yaml | 11 ++++++++++ 2 files changed, 33 insertions(+) create mode 100644 tests/spread/testcraft/prune-instances/task.yaml create mode 100644 tests/spread/testcraft/prune-instances/testcraft.yaml diff --git a/tests/spread/testcraft/prune-instances/task.yaml b/tests/spread/testcraft/prune-instances/task.yaml new file mode 100644 index 000000000..5dd5ba867 --- /dev/null +++ b/tests/spread/testcraft/prune-instances/task.yaml @@ -0,0 +1,22 @@ +summary: Testcraft should prune its instances + +prepare: | + . /etc/os-release + sed -i "s/base: ubuntu@24.04/base: ubuntu@${VERSION_ID}/" testcraft.yaml + +execute: | + # Create a LXD instance by running a build. + # We use --use-lxd to ensure it uses the LXD provider. + testcraft build --use-lxd + + # Verify the instance exists. The name should start with 'testcraft-prune-instances-test-'. + lxc list --format csv -c n | grep "^testcraft-prune-instances-test-" + + # Prune instances. + testcraft prune-instances --all + + # Verify the instance is gone. + if lxc list --format csv -c n | grep "^testcraft-prune-instances-test-"; then + echo "Instance was not pruned!" + exit 1 + fi diff --git a/tests/spread/testcraft/prune-instances/testcraft.yaml b/tests/spread/testcraft/prune-instances/testcraft.yaml new file mode 100644 index 000000000..d2a150b77 --- /dev/null +++ b/tests/spread/testcraft/prune-instances/testcraft.yaml @@ -0,0 +1,11 @@ +name: prune-instances-test +summary: test summary +version: "0.1" + +base: ubuntu@24.04 +platforms: + amd64: + +parts: + my-part: + plugin: nil From 1449184e7417ba2dac81c386e6451dbaf94a568f Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Wed, 15 Apr 2026 20:29:54 -0400 Subject: [PATCH 07/10] fix test spread for prune instances --- tests/spread/testcraft/prune-instances/task.yaml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/spread/testcraft/prune-instances/task.yaml b/tests/spread/testcraft/prune-instances/task.yaml index 5dd5ba867..3c8b11a37 100644 --- a/tests/spread/testcraft/prune-instances/task.yaml +++ b/tests/spread/testcraft/prune-instances/task.yaml @@ -1,13 +1,11 @@ summary: Testcraft should prune its instances -prepare: | - . /etc/os-release - sed -i "s/base: ubuntu@24.04/base: ubuntu@${VERSION_ID}/" testcraft.yaml +systems: + - ubuntu-24.04-64 execute: | - # Create a LXD instance by running a build. - # We use --use-lxd to ensure it uses the LXD provider. - testcraft build --use-lxd + # Create a LXD instance. + testcraft pull --use-lxd # Verify the instance exists. The name should start with 'testcraft-prune-instances-test-'. lxc list --format csv -c n | grep "^testcraft-prune-instances-test-" @@ -20,3 +18,6 @@ execute: | echo "Instance was not pruned!" exit 1 fi + +restore: | + testcraft prune-instances --all From 1c06d19119bc2e2d150db5aeaa3dcdbf04a79ab7 Mon Sep 17 00:00:00 2001 From: Dylan <143847987+Aeonoi@users.noreply.github.com> Date: Fri, 24 Apr 2026 12:14:07 -0400 Subject: [PATCH 08/10] Update tests/spread/testcraft/prune-instances/task.yaml Co-authored-by: Callahan Kovacs Signed-off-by: Dylan <143847987+Aeonoi@users.noreply.github.com> --- tests/spread/testcraft/prune-instances/task.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/spread/testcraft/prune-instances/task.yaml b/tests/spread/testcraft/prune-instances/task.yaml index 3c8b11a37..d8971d414 100644 --- a/tests/spread/testcraft/prune-instances/task.yaml +++ b/tests/spread/testcraft/prune-instances/task.yaml @@ -11,8 +11,7 @@ execute: | lxc list --format csv -c n | grep "^testcraft-prune-instances-test-" # Prune instances. - testcraft prune-instances --all - + testcraft prune-instances --all-providers # Verify the instance is gone. if lxc list --format csv -c n | grep "^testcraft-prune-instances-test-"; then echo "Instance was not pruned!" From 5b7dad556422f8c36d07d59c4bc2d71dfb8f7791 Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Fri, 24 Apr 2026 12:30:58 -0400 Subject: [PATCH 09/10] fix up testcraft task.yaml for prune-instances --- craft_application/services/provider.py | 2 +- tests/spread/testcraft/prune-instances/task.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/craft_application/services/provider.py b/craft_application/services/provider.py index dcb3b5b5e..7ff4cc37f 100644 --- a/craft_application/services/provider.py +++ b/craft_application/services/provider.py @@ -617,7 +617,7 @@ def prune_instances( providers: list[craft_providers.Provider] = [] if provider_name: - providers.append(self.get_provider(name=provider_name)) + providers.append(self._get_provider_by_name(name=provider_name)) elif all_providers: self.add_provider_if_available( providers, "lxd", "LXD provider not available, skipping LXD pruning." diff --git a/tests/spread/testcraft/prune-instances/task.yaml b/tests/spread/testcraft/prune-instances/task.yaml index 3c8b11a37..16202c8f0 100644 --- a/tests/spread/testcraft/prune-instances/task.yaml +++ b/tests/spread/testcraft/prune-instances/task.yaml @@ -11,7 +11,7 @@ execute: | lxc list --format csv -c n | grep "^testcraft-prune-instances-test-" # Prune instances. - testcraft prune-instances --all + testcraft prune-instances --all-providers # Verify the instance is gone. if lxc list --format csv -c n | grep "^testcraft-prune-instances-test-"; then @@ -20,4 +20,4 @@ execute: | fi restore: | - testcraft prune-instances --all + testcraft prune-instances --all-providers From 51fc6d5fd8ac24029751e53834a349f1755d7c48 Mon Sep 17 00:00:00 2001 From: Aeonoi Date: Fri, 24 Apr 2026 12:43:15 -0400 Subject: [PATCH 10/10] add unit test for prune-instance with provider_name is None --- craft_application/services/provider.py | 2 +- tests/unit/services/test_provider.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/craft_application/services/provider.py b/craft_application/services/provider.py index 7ff4cc37f..7551e6455 100644 --- a/craft_application/services/provider.py +++ b/craft_application/services/provider.py @@ -628,7 +628,7 @@ def prune_instances( "Multipass provider not available, skipping multipass pruning.", ) else: - providers.append(self.get_provider()) + providers.append(self.get_provider(name=provider_name)) for provider in providers: emit.progress(f"Pruning instances for provider {provider.name!r}...") diff --git a/tests/unit/services/test_provider.py b/tests/unit/services/test_provider.py index 02c16e63b..5d68bff27 100644 --- a/tests/unit/services/test_provider.py +++ b/tests/unit/services/test_provider.py @@ -1188,6 +1188,18 @@ def test_prune_instances_single_provider(monkeypatch, provider_service): mock_lxd.prune.assert_called_once_with(prune_templates=True) +def test_prune_instances_none_provider(monkeypatch, provider_service): + """Prune for a single provider.""" + mock_lxd = mock.MagicMock() + mock_lxd.name = "lxd" + + monkeypatch.setattr(provider_service, "get_provider", lambda name: mock_lxd) + + provider_service.prune_instances(provider_name=None, prune_templates=True) + + mock_lxd.prune.assert_called_once_with(prune_templates=True) + + def test_prune_instances_all_providers(monkeypatch, provider_service): """Prune for all supported providers.""" mock_lxd = mock.MagicMock()