VibecoderMcSwaggins's picture
feat: Phase 1A + Phase 2 - Local data loader and DeepISLES Docker wrapper (#3)
aef1f5a unverified
raw
history blame
11.1 kB
"""Tests for DeepISLES wrapper."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from stroke_deepisles_demo.core.exceptions import DeepISLESError, MissingInputError
from stroke_deepisles_demo.inference.deepisles import (
DeepISLESResult,
find_prediction_mask,
run_deepisles_on_folder,
validate_input_folder,
)
class TestValidateInputFolder:
"""Tests for validate_input_folder."""
def test_succeeds_with_required_files(self, temp_dir: Path) -> None:
"""Returns paths when required files exist."""
(temp_dir / "dwi.nii.gz").touch()
(temp_dir / "adc.nii.gz").touch()
dwi, adc, flair = validate_input_folder(temp_dir)
assert dwi == temp_dir / "dwi.nii.gz"
assert adc == temp_dir / "adc.nii.gz"
assert flair is None
def test_includes_flair_when_present(self, temp_dir: Path) -> None:
"""Returns FLAIR path when present."""
(temp_dir / "dwi.nii.gz").touch()
(temp_dir / "adc.nii.gz").touch()
(temp_dir / "flair.nii.gz").touch()
_dwi, _adc, flair = validate_input_folder(temp_dir)
assert flair == temp_dir / "flair.nii.gz"
def test_raises_when_dwi_missing(self, temp_dir: Path) -> None:
"""Raises MissingInputError when DWI is missing."""
(temp_dir / "adc.nii.gz").touch()
with pytest.raises(MissingInputError, match="dwi"):
validate_input_folder(temp_dir)
def test_raises_when_adc_missing(self, temp_dir: Path) -> None:
"""Raises MissingInputError when ADC is missing."""
(temp_dir / "dwi.nii.gz").touch()
with pytest.raises(MissingInputError, match="adc"):
validate_input_folder(temp_dir)
class TestFindPredictionMask:
"""Tests for find_prediction_mask."""
def test_finds_prediction_file(self, temp_dir: Path) -> None:
"""Finds prediction.nii.gz in output directory."""
results_dir = temp_dir / "results"
results_dir.mkdir()
pred_file = results_dir / "prediction.nii.gz"
pred_file.touch()
result = find_prediction_mask(temp_dir)
assert result == pred_file
def test_finds_alternate_name(self, temp_dir: Path) -> None:
"""Finds alternate named prediction files."""
results_dir = temp_dir / "results"
results_dir.mkdir()
pred_file = results_dir / "pred.nii.gz"
pred_file.touch()
result = find_prediction_mask(temp_dir)
assert result == pred_file
def test_falls_back_to_any_nifti(self, temp_dir: Path) -> None:
"""Falls back to any .nii.gz file if standard names not found."""
results_dir = temp_dir / "results"
results_dir.mkdir()
pred_file = results_dir / "some_output.nii.gz"
pred_file.touch()
result = find_prediction_mask(temp_dir)
assert result == pred_file
def test_raises_when_no_prediction(self, temp_dir: Path) -> None:
"""Raises DeepISLESError when no prediction found."""
results_dir = temp_dir / "results"
results_dir.mkdir()
with pytest.raises(DeepISLESError, match="prediction"):
find_prediction_mask(temp_dir)
def test_raises_when_results_dir_missing(self, temp_dir: Path) -> None:
"""Raises DeepISLESError when results directory missing."""
with pytest.raises(DeepISLESError, match="prediction"):
find_prediction_mask(temp_dir)
class TestRunDeepIslesOnFolder:
"""Tests for run_deepisles_on_folder."""
@pytest.fixture
def valid_input_dir(self, temp_dir: Path) -> Path:
"""Create a valid input directory with required files."""
(temp_dir / "dwi.nii.gz").touch()
(temp_dir / "adc.nii.gz").touch()
return temp_dir
def test_validates_input_files(self, temp_dir: Path) -> None:
"""Validates input files before running Docker."""
# Missing required files
with pytest.raises(MissingInputError):
run_deepisles_on_folder(temp_dir)
def test_calls_docker_with_correct_image(self, valid_input_dir: Path) -> None:
"""Calls Docker with DeepISLES image."""
with patch("stroke_deepisles_demo.inference.deepisles.run_container") as mock_run:
mock_run.return_value = MagicMock(exit_code=0, stdout="", stderr="")
with (
patch(
"stroke_deepisles_demo.inference.deepisles.ensure_gpu_available_if_requested"
),
patch(
"stroke_deepisles_demo.inference.deepisles.find_prediction_mask"
) as mock_find,
):
mock_find.return_value = valid_input_dir / "results" / "pred.nii.gz"
run_deepisles_on_folder(valid_input_dir)
# Check image name
call_args = mock_run.call_args
assert call_args.args[0] == "isleschallenge/deepisles"
def test_passes_fast_flag(self, valid_input_dir: Path) -> None:
"""Passes --fast True when fast=True."""
with patch("stroke_deepisles_demo.inference.deepisles.run_container") as mock_run:
mock_run.return_value = MagicMock(exit_code=0, stdout="", stderr="")
with (
patch(
"stroke_deepisles_demo.inference.deepisles.ensure_gpu_available_if_requested"
),
patch(
"stroke_deepisles_demo.inference.deepisles.find_prediction_mask"
) as mock_find,
):
mock_find.return_value = valid_input_dir / "results" / "pred.nii.gz"
run_deepisles_on_folder(valid_input_dir, fast=True)
# Check --fast in command
call_kwargs = mock_run.call_args.kwargs
command = call_kwargs.get("command", [])
assert "--fast" in command
assert "True" in command
def test_includes_flair_when_present(self, valid_input_dir: Path) -> None:
"""Includes FLAIR in command when present."""
(valid_input_dir / "flair.nii.gz").touch()
with patch("stroke_deepisles_demo.inference.deepisles.run_container") as mock_run:
mock_run.return_value = MagicMock(exit_code=0, stdout="", stderr="")
with (
patch(
"stroke_deepisles_demo.inference.deepisles.ensure_gpu_available_if_requested"
),
patch(
"stroke_deepisles_demo.inference.deepisles.find_prediction_mask"
) as mock_find,
):
mock_find.return_value = valid_input_dir / "results" / "pred.nii.gz"
run_deepisles_on_folder(valid_input_dir)
call_kwargs = mock_run.call_args.kwargs
command = call_kwargs.get("command", [])
assert "--flair_file_name" in command
assert "flair.nii.gz" in command
def test_raises_on_docker_failure(self, valid_input_dir: Path) -> None:
"""Raises DeepISLESError when Docker returns non-zero."""
with patch("stroke_deepisles_demo.inference.deepisles.run_container") as mock_run:
mock_run.return_value = MagicMock(exit_code=1, stdout="", stderr="Segmentation fault")
with (
patch(
"stroke_deepisles_demo.inference.deepisles.ensure_gpu_available_if_requested"
),
pytest.raises(DeepISLESError, match="failed"),
):
run_deepisles_on_folder(valid_input_dir)
def test_returns_result_with_prediction_path(self, valid_input_dir: Path) -> None:
"""Returns DeepISLESResult with prediction path."""
with patch("stroke_deepisles_demo.inference.deepisles.run_container") as mock_run:
mock_run.return_value = MagicMock(
exit_code=0, stdout="", stderr="", elapsed_seconds=10.0
)
with (
patch(
"stroke_deepisles_demo.inference.deepisles.ensure_gpu_available_if_requested"
),
patch(
"stroke_deepisles_demo.inference.deepisles.find_prediction_mask"
) as mock_find,
):
expected_path = valid_input_dir / "results" / "prediction.nii.gz"
mock_find.return_value = expected_path
result = run_deepisles_on_folder(valid_input_dir)
assert isinstance(result, DeepISLESResult)
assert result.prediction_path == expected_path
def test_passes_volume_mounts(self, valid_input_dir: Path, temp_dir: Path) -> None:
"""Passes correct volume mounts to Docker."""
# Create a separate output directory
output_dir = temp_dir / "output"
output_dir.mkdir()
with patch("stroke_deepisles_demo.inference.deepisles.run_container") as mock_run:
mock_run.return_value = MagicMock(exit_code=0, stdout="", stderr="")
with (
patch(
"stroke_deepisles_demo.inference.deepisles.ensure_gpu_available_if_requested"
),
patch(
"stroke_deepisles_demo.inference.deepisles.find_prediction_mask"
) as mock_find,
):
mock_find.return_value = output_dir / "results" / "pred.nii.gz"
run_deepisles_on_folder(valid_input_dir, output_dir=output_dir)
call_kwargs = mock_run.call_args.kwargs
volumes = call_kwargs.get("volumes", {})
# Should have input and output mounts (2 separate directories)
assert len(volumes) == 2
# Values should be container paths
assert "/input" in volumes.values()
assert "/output" in volumes.values()
@pytest.mark.integration
@pytest.mark.slow
class TestDeepIslesIntegration:
"""Integration tests requiring real Docker and DeepISLES image."""
def test_real_inference(self, synthetic_case_files: dict[str, object]) -> None:
"""Run actual DeepISLES inference on synthetic data."""
# This test requires:
# 1. Docker available
# 2. isleschallenge/deepisles image pulled
# 3. GPU (optional but recommended)
#
# Run with: pytest -m integration
import tempfile
from stroke_deepisles_demo.data.staging import stage_case_for_deepisles
# Create a separate staging directory
with tempfile.TemporaryDirectory() as staging_dir:
# Stage the synthetic files to the new directory
staged = stage_case_for_deepisles(
synthetic_case_files, # type: ignore[arg-type]
Path(staging_dir),
)
# Run inference
result = run_deepisles_on_folder(
staged.input_dir,
fast=True,
gpu=False, # Might not have GPU in CI
timeout=600,
)
# Verify output exists
assert result.prediction_path.exists()