From 57e8d09aad9badaa5c0d00745bfc3187d01e5956 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 3 Mar 2026 09:44:54 +0100 Subject: [PATCH 1/3] refactor(tests): extract save/restore fixture into qupath conftest Co-Authored-By: Claude Sonnet 4.6 --- tests/aignostics/qupath/cli_test.py | 32 ++++--------------------- tests/aignostics/qupath/conftest.py | 26 +++++++++++++++++++++ tests/aignostics/qupath/gui_test.py | 36 ++++++++++------------------- 3 files changed, 43 insertions(+), 51 deletions(-) create mode 100644 tests/aignostics/qupath/conftest.py diff --git a/tests/aignostics/qupath/cli_test.py b/tests/aignostics/qupath/cli_test.py index 1d001deb4..c1da01cd0 100644 --- a/tests/aignostics/qupath/cli_test.py +++ b/tests/aignostics/qupath/cli_test.py @@ -23,12 +23,8 @@ @pytest.mark.flaky(retries=3, delay=5, only_on=[AssertionError]) @pytest.mark.timeout(timeout=60 * 10) @pytest.mark.sequential -def test_cli_install_and_uninstall(runner: CliRunner) -> None: +def test_cli_install_and_uninstall(runner: CliRunner, qupath_save_restore: None) -> None: """Check (un)install works for Windows, Mac and Linux package.""" - # Uninstall QuPath if it exists to have a clean state for the test - result = runner.invoke(cli, ["qupath", "uninstall"]) - was_installed = result.exit_code == 0 - # Test installation and uninstallation on different platforms if platform.system() == "Windows": platforms_to_test = [ @@ -57,10 +53,6 @@ def test_cli_install_and_uninstall(runner: CliRunner) -> None: assert "QuPath uninstalled successfully." in normalize_output(result.output) assert result.exit_code == 0 - # Reinstall QuPath if it was installed before - if was_installed: - result = runner.invoke(cli, ["qupath", "install"]) - @pytest.mark.e2e @pytest.mark.long_running @@ -71,12 +63,10 @@ def test_cli_install_and_uninstall(runner: CliRunner) -> None: @pytest.mark.flaky(retries=3, delay=5, only_on=[AssertionError]) @pytest.mark.timeout(timeout=60 * 10) @pytest.mark.sequential -def test_cli_install_launch_project_annotations_headless(runner: CliRunner, tmpdir, qupath_teardown) -> None: +def test_cli_install_launch_project_annotations_headless( + runner: CliRunner, tmpdir, qupath_teardown, qupath_save_restore: None +) -> None: """Check (un)install, launching headless, creating project and adding annotations works.""" - # Uninstall QuPath if it exists to have a clean state for the test - result = runner.invoke(cli, ["qupath", "uninstall"]) - was_installed = result.exit_code == 0 - # Step 1: System info determines QuPath is not installed result = runner.invoke(cli, ["system", "info"]) output_data = json.loads(result.stdout) @@ -128,10 +118,6 @@ def test_cli_install_launch_project_annotations_headless(runner: CliRunner, tmpd assert output_data["qupath"]["app"]["version"] is None assert result.exit_code == 0 - # Step 9: Reinstall QuPath if it was installed before - if was_installed: - result = runner.invoke(cli, ["qupath", "install"]) - @pytest.mark.e2e @pytest.mark.long_running @@ -142,12 +128,8 @@ def test_cli_install_launch_project_annotations_headless(runner: CliRunner, tmpd @pytest.mark.flaky(retries=3, delay=5, only_on=[AssertionError]) @pytest.mark.timeout(timeout=60 * 10) @pytest.mark.sequential -def test_cli_install_and_launch_ui(runner: CliRunner, qupath_teardown) -> None: +def test_cli_install_and_launch_ui(runner: CliRunner, qupath_teardown, qupath_save_restore: None) -> None: """Check (un)install and launching UI versin of QuPath works.""" - # Uninstall QuPath if it exists to have a clean state for the test - result = runner.invoke(cli, ["qupath", "uninstall"]) - was_installed = result.exit_code == 0 - # Step 1: Check QuPath launch fails if not installed result = runner.invoke(cli, ["qupath", "launch"]) assert "QuPath is not installed. Use 'uvx aignostics qupath install' to install it." in normalize_output( @@ -203,7 +185,3 @@ def test_cli_install_and_launch_ui(runner: CliRunner, qupath_teardown) -> None: result.output ) assert result.exit_code == 2 - - # Step 9: Reinstall QuPath if it was installed before - if was_installed: - result = runner.invoke(cli, ["qupath", "install"]) diff --git a/tests/aignostics/qupath/conftest.py b/tests/aignostics/qupath/conftest.py new file mode 100644 index 000000000..2ef9a52c7 --- /dev/null +++ b/tests/aignostics/qupath/conftest.py @@ -0,0 +1,26 @@ +"""Shared fixtures for QuPath tests.""" + +from collections.abc import Generator + +import pytest +from typer.testing import CliRunner + +from aignostics.cli import cli + + +@pytest.fixture +def qupath_save_restore(runner: CliRunner) -> Generator[None, None, None]: + """Uninstall QuPath for clean state, restore after test if it was installed.""" + result = runner.invoke(cli, ["qupath", "uninstall"]) + assert result.exit_code in {0, 2}, ( + f"Unexpected exit code {result.exit_code} from 'qupath uninstall': {result.output}" + ) + was_installed = result.exit_code == 0 + yield + if was_installed: + reinstall_result = runner.invoke(cli, ["qupath", "install"]) + if reinstall_result.exit_code != 0: + pytest.fail( + f"Failed to reinstall QuPath in qupath_save_restore fixture " + f"(exit code {reinstall_result.exit_code}). Output:\n{reinstall_result.output}" + ) diff --git a/tests/aignostics/qupath/gui_test.py b/tests/aignostics/qupath/gui_test.py index e86831514..1814d2801 100644 --- a/tests/aignostics/qupath/gui_test.py +++ b/tests/aignostics/qupath/gui_test.py @@ -46,12 +46,11 @@ ) @pytest.mark.timeout(timeout=60 * 10) @pytest.mark.sequential -async def test_gui_qupath_install_only(user: User, runner: CliRunner, silent_logging: None, record_property) -> None: +async def test_gui_qupath_install_only( + user: User, runner: CliRunner, silent_logging: None, qupath_save_restore: None, record_property +) -> None: """Test that the user can install and launch QuPath via the GUI.""" record_property("tested-item-id", "TC-QUPATH-01, SPEC-GUI-SERVICE") - result = runner.invoke(cli, ["qupath", "uninstall"]) - assert result.exit_code in {0, 2}, f"Uninstall command failed with exit code {result.exit_code}" - was_installed = not result.exit_code # Step 1: Check we are on the QuPath page await user.open("/qupath") @@ -75,9 +74,6 @@ async def test_gui_qupath_install_only(user: User, runner: CliRunner, silent_log await user.should_see(f"QuPath {QUPATH_VERSION} is installed and ready to execute.") await user.should_see(marker="BUTTON_QUPATH_LAUNCH") - if not was_installed: - result = runner.invoke(cli, ["qupath", "uninstall"]) - @pytest.mark.e2e @pytest.mark.long_running @@ -88,16 +84,12 @@ async def test_gui_qupath_install_only(user: User, runner: CliRunner, silent_log ) @pytest.mark.timeout(timeout=60 * 10) @pytest.mark.sequential -async def test_gui_qupath_install_and_launch( - user: User, runner: CliRunner, silent_logging: None, qupath_teardown, record_property +async def test_gui_qupath_install_and_launch( # noqa: PLR0913, PLR0917 + user: User, runner: CliRunner, silent_logging: None, qupath_teardown, qupath_save_restore: None, record_property ) -> None: """Test that the user can install and launch QuPath via the GUI.""" record_property("tested-item-id", "TC-QUPATH-01, SPEC-GUI-SERVICE") - result = runner.invoke(cli, ["qupath", "uninstall"]) - assert result.exit_code in {0, 2}, f"Uninstall command failed with exit code {result.exit_code}" - was_installed = not result.exit_code - # Step 1: Check we are on the QuPath page await user.open("/qupath") await user.should_see("QuPath Extension") @@ -142,9 +134,6 @@ async def test_gui_qupath_install_and_launch( except Exception as e: pytest.fail(f"Failed to kill QuPath process: {e}") - if not was_installed: - result = runner.invoke(cli, ["qupath", "uninstall"]) - @pytest.mark.e2e @pytest.mark.long_running @@ -155,7 +144,13 @@ async def test_gui_qupath_install_and_launch( @pytest.mark.timeout(timeout=60 * 15) @pytest.mark.sequential async def test_gui_run_qupath_install_to_inspect( # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917 - user: User, runner: CliRunner, tmp_path: Path, silent_logging: None, qupath_teardown: None, record_property + user: User, + runner: CliRunner, + tmp_path: Path, + silent_logging: None, + qupath_teardown: None, + qupath_save_restore: None, + record_property, ) -> None: """Test installing QuPath, downloading run results, creating QuPath project from it, and inspecting results.""" record_property("tested-item-id", "TC-QUPATH-01, SPEC-GUI-SERVICE") @@ -201,10 +196,6 @@ async def test_gui_run_qupath_install_to_inspect( # noqa: C901, PLR0912, PLR091 "aignostics.application._gui._page_application_run_describe.get_user_data_directory", return_value=tmp_path ): # Step 1: (Re)Install QuPath - result = runner.invoke(cli, ["qupath", "uninstall"]) - assert result.exit_code in {0, 2}, f"Uninstall command failed with exit code {result.exit_code}" - was_installed = not result.exit_code - result = runner.invoke(cli, ["qupath", "install"]) output = normalize_output(result.output, strip_ansi=True) assert f"QuPath v{QUPATH_VERSION} installed successfully" in output, ( @@ -345,6 +336,3 @@ async def test_gui_run_qupath_install_to_inspect( # noqa: C901, PLR0912, PLR091 # Validate the inspect command exited successfully assert result.exit_code == 0, f"QuPath inspect command failed with exit code {result.exit_code}" - - if not was_installed: - result = runner.invoke(cli, ["qupath", "uninstall"]) From e115699278fbe7c419bba889145a864094832647 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 3 Mar 2026 09:51:13 +0100 Subject: [PATCH 2/3] test(qupath): parametrize install/uninstall test for independent retries Co-Authored-By: Claude Sonnet 4.6 --- tests/aignostics/qupath/cli_test.py | 65 ++++++++++++++++------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/tests/aignostics/qupath/cli_test.py b/tests/aignostics/qupath/cli_test.py index c1da01cd0..87b7b91e5 100644 --- a/tests/aignostics/qupath/cli_test.py +++ b/tests/aignostics/qupath/cli_test.py @@ -13,6 +13,26 @@ from aignostics.qupath import QUPATH_VERSION from tests.conftest import normalize_output +_SKIP_IF_WINDOWS = pytest.mark.skipif(platform.system() == "Windows", reason="not supported on Windows") +_INSTALL_UNINSTALL_PLATFORM_CONFIGS = [ + pytest.param({"system": "Windows"}, id="windows"), + pytest.param( + {"system": "Linux"}, + id="linux", + marks=_SKIP_IF_WINDOWS, + ), + pytest.param( + {"system": "Darwin", "machine": "amd64"}, + id="darwin-amd64", + marks=_SKIP_IF_WINDOWS, + ), + pytest.param( + {"system": "Darwin", "machine": "arm64"}, + id="darwin-arm64", + marks=_SKIP_IF_WINDOWS, + ), +] + @pytest.mark.e2e @pytest.mark.long_running @@ -21,37 +41,24 @@ reason="QuPath is not supported on ARM64 Linux", ) @pytest.mark.flaky(retries=3, delay=5, only_on=[AssertionError]) -@pytest.mark.timeout(timeout=60 * 10) +@pytest.mark.timeout(timeout=60 * 5) @pytest.mark.sequential -def test_cli_install_and_uninstall(runner: CliRunner, qupath_save_restore: None) -> None: +@pytest.mark.parametrize("platform_config", _INSTALL_UNINSTALL_PLATFORM_CONFIGS) +def test_cli_install_and_uninstall(runner: CliRunner, qupath_save_restore: None, platform_config: dict) -> None: """Check (un)install works for Windows, Mac and Linux package.""" - # Test installation and uninstallation on different platforms - if platform.system() == "Windows": - platforms_to_test = [ - {"system": "Windows"}, - ] - else: - platforms_to_test = [ - {"system": "Windows"}, - {"system": "Linux"}, - {"system": "Darwin", "machine": "amd64"}, - {"system": "Darwin", "machine": "arm64"}, - ] - - for platform_config in platforms_to_test: - install_args = ["qupath", "install", "--platform-system", platform_config["system"]] - uninstall_args = ["qupath", "uninstall", "--platform-system", platform_config["system"]] - if "machine" in platform_config: - install_args.extend(["--platform-machine", platform_config["machine"]]) - uninstall_args.extend(["--platform-machine", platform_config["machine"]]) - - result = runner.invoke(cli, install_args) - assert f"QuPath v{QUPATH_VERSION} installed successfully" in normalize_output(result.output) - assert result.exit_code == 0 - - result = runner.invoke(cli, uninstall_args) - assert "QuPath uninstalled successfully." in normalize_output(result.output) - assert result.exit_code == 0 + install_args = ["qupath", "install", "--platform-system", platform_config["system"]] + uninstall_args = ["qupath", "uninstall", "--platform-system", platform_config["system"]] + if "machine" in platform_config: + install_args.extend(["--platform-machine", platform_config["machine"]]) + uninstall_args.extend(["--platform-machine", platform_config["machine"]]) + + result = runner.invoke(cli, install_args) + assert f"QuPath v{QUPATH_VERSION} installed successfully" in normalize_output(result.output) + assert result.exit_code == 0 + + result = runner.invoke(cli, uninstall_args) + assert "QuPath uninstalled successfully." in normalize_output(result.output) + assert result.exit_code == 0 @pytest.mark.e2e From c2ea7f7b3e5411430fc0ddd68300f66b0ef2da16 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 3 Mar 2026 09:55:26 +0100 Subject: [PATCH 3/3] fix(qupath): remove trailing equal sign from logs --- src/aignostics/qupath/_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aignostics/qupath/_service.py b/src/aignostics/qupath/_service.py index 77875b070..d8d9b6739 100644 --- a/src/aignostics/qupath/_service.py +++ b/src/aignostics/qupath/_service.py @@ -518,7 +518,7 @@ def _download_qupath( # noqa: C901, PLR0912, PLR0913, PLR0915, PLR0917 install_progress_queue.put_nowait(progress) logger.trace("Downloaded QuPath archive to '{}'", filepath) except requests.RequestException as e: - message = f"Failed to download QuPath from {url}=" + message = f"Failed to download QuPath from {url}" logger.exception(message) raise RuntimeError(message) from e except Exception: