Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/aignostics/qupath/_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
95 changes: 40 additions & 55 deletions tests/aignostics/qupath/cli_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,45 +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) -> 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."""
# 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 = [
{"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

# Reinstall QuPath if it was installed before
if was_installed:
result = runner.invoke(cli, ["qupath", "install"])
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
Expand All @@ -71,12 +70,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)
Expand Down Expand Up @@ -128,10 +125,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
Expand All @@ -142,12 +135,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(
Expand Down Expand Up @@ -203,7 +192,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"])
26 changes: 26 additions & 0 deletions tests/aignostics/qupath/conftest.py
Original file line number Diff line number Diff line change
@@ -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}"
)
36 changes: 12 additions & 24 deletions tests/aignostics/qupath/gui_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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, (
Expand Down Expand Up @@ -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"])
Loading