Spaces:
Sleeping
Sleeping
Fix test mocks for segment_tissue return value
Browse filessegment_tissue returns 4 values (polygon, _, coords, attrs) but tests
were mocking it to return only 2 values, causing unpacking errors.
Fixes:
- Mock segment_tissue to return all 4 values
- Mock gr.Warning to avoid argument mismatch errors
- Use tempfile for directory creation in Gradio test
- Mock DataFrame.to_csv to avoid file system issues
All regression tests now pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
tests/test_regression_single_slide.py
CHANGED
|
@@ -53,7 +53,9 @@ class TestSingleSlideRegression:
|
|
| 53 |
# Setup mocks
|
| 54 |
mock_coords = np.array([[0, 0], [1, 1]])
|
| 55 |
mock_attrs = {"level": 0}
|
| 56 |
-
|
|
|
|
|
|
|
| 57 |
|
| 58 |
mock_mask_image = Mock()
|
| 59 |
mock_mask.return_value = mock_mask_image
|
|
@@ -108,59 +110,65 @@ class TestSingleSlideRegression:
|
|
| 108 |
@patch('mosaic.ui.app.analyze_slide')
|
| 109 |
@patch('mosaic.ui.app.create_user_directory')
|
| 110 |
@patch('mosaic.ui.app.validate_settings')
|
|
|
|
| 111 |
def test_gradio_single_slide_uses_analyze_slide(
|
| 112 |
self,
|
|
|
|
| 113 |
mock_validate,
|
| 114 |
mock_create_dir,
|
| 115 |
mock_analyze_slide,
|
| 116 |
):
|
| 117 |
"""Test that Gradio UI uses analyze_slide for single slide (not batch mode)."""
|
| 118 |
# Setup
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
"
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
user_dir=
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
|
| 159 |
@patch('mosaic.analysis.segment_tissue')
|
| 160 |
-
|
|
|
|
| 161 |
"""Test single-slide analysis when no tissue is found."""
|
| 162 |
# No tissue tiles found
|
| 163 |
-
mock_segment.return_value =
|
| 164 |
|
| 165 |
slide_mask, aeon_results, paladin_results = analyze_slide(
|
| 166 |
slide_path=mock_slide_path,
|
|
@@ -176,6 +184,8 @@ class TestSingleSlideRegression:
|
|
| 176 |
assert slide_mask is None
|
| 177 |
assert aeon_results is None
|
| 178 |
assert paladin_results is None
|
|
|
|
|
|
|
| 179 |
|
| 180 |
@patch('mosaic.analysis.segment_tissue')
|
| 181 |
@patch('mosaic.analysis.draw_slide_mask')
|
|
@@ -196,7 +206,10 @@ class TestSingleSlideRegression:
|
|
| 196 |
):
|
| 197 |
"""Test that single-slide with known subtype skips Aeon inference."""
|
| 198 |
# Setup minimal mocks
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
| 200 |
mock_mask.return_value = Mock()
|
| 201 |
mock_ctranspath.return_value = (np.random.rand(10, 768), np.array([[0, 0]]))
|
| 202 |
mock_filter.return_value = (None, np.array([[0, 0]]))
|
|
@@ -248,20 +261,21 @@ class TestBackwardCompatibility:
|
|
| 248 |
|
| 249 |
def test_analyze_slide_return_type_unchanged(self):
|
| 250 |
"""Test that analyze_slide returns the same tuple structure."""
|
| 251 |
-
with patch('mosaic.analysis.segment_tissue', return_value=
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
|
|
|
| 265 |
|
| 266 |
|
| 267 |
if __name__ == "__main__":
|
|
|
|
| 53 |
# Setup mocks
|
| 54 |
mock_coords = np.array([[0, 0], [1, 1]])
|
| 55 |
mock_attrs = {"level": 0}
|
| 56 |
+
mock_polygon = Mock()
|
| 57 |
+
# segment_tissue returns (polygon, _, coords, attrs)
|
| 58 |
+
mock_segment.return_value = (mock_polygon, None, mock_coords, mock_attrs)
|
| 59 |
|
| 60 |
mock_mask_image = Mock()
|
| 61 |
mock_mask.return_value = mock_mask_image
|
|
|
|
| 110 |
@patch('mosaic.ui.app.analyze_slide')
|
| 111 |
@patch('mosaic.ui.app.create_user_directory')
|
| 112 |
@patch('mosaic.ui.app.validate_settings')
|
| 113 |
+
@patch('pandas.DataFrame.to_csv') # Mock CSV writing to avoid directory issues
|
| 114 |
def test_gradio_single_slide_uses_analyze_slide(
|
| 115 |
self,
|
| 116 |
+
mock_to_csv,
|
| 117 |
mock_validate,
|
| 118 |
mock_create_dir,
|
| 119 |
mock_analyze_slide,
|
| 120 |
):
|
| 121 |
"""Test that Gradio UI uses analyze_slide for single slide (not batch mode)."""
|
| 122 |
# Setup
|
| 123 |
+
import tempfile
|
| 124 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 125 |
+
mock_dir = Path(tmpdir) / "test_user"
|
| 126 |
+
mock_dir.mkdir()
|
| 127 |
+
mock_create_dir.return_value = mock_dir
|
| 128 |
+
|
| 129 |
+
settings_df = pd.DataFrame({
|
| 130 |
+
"Slide": ["test.svs"],
|
| 131 |
+
"Site Type": ["Primary"],
|
| 132 |
+
"Sex": ["Male"],
|
| 133 |
+
"Tissue Site": ["Lung"],
|
| 134 |
+
"Cancer Subtype": ["Unknown"],
|
| 135 |
+
"IHC Subtype": [""],
|
| 136 |
+
"Segmentation Config": ["Biopsy"],
|
| 137 |
+
})
|
| 138 |
+
mock_validate.return_value = settings_df
|
| 139 |
+
|
| 140 |
+
mock_mask = Mock()
|
| 141 |
+
mock_aeon = pd.DataFrame({"Cancer Subtype": ["LUAD"], "Confidence": [0.9]})
|
| 142 |
+
mock_paladin = pd.DataFrame({
|
| 143 |
+
"Cancer Subtype": ["LUAD"],
|
| 144 |
+
"Biomarker": ["EGFR"],
|
| 145 |
+
"Score": [0.8]
|
| 146 |
+
})
|
| 147 |
+
mock_analyze_slide.return_value = (mock_mask, mock_aeon, mock_paladin)
|
| 148 |
+
|
| 149 |
+
from mosaic.ui.app import cancer_subtype_name_map
|
| 150 |
+
|
| 151 |
+
# Call analyze_slides with a single slide
|
| 152 |
+
with patch('mosaic.ui.app.get_oncotree_code_name', return_value="Lung Adenocarcinoma"):
|
| 153 |
+
masks, aeon, aeon_btn, paladin, paladin_btn, user_dir = analyze_slides(
|
| 154 |
+
slides=["test.svs"],
|
| 155 |
+
settings_input=settings_df,
|
| 156 |
+
user_dir=mock_dir,
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
# Verify analyze_slide was called (not analyze_slides_batch)
|
| 160 |
+
mock_analyze_slide.assert_called_once()
|
| 161 |
+
|
| 162 |
+
# Verify results
|
| 163 |
+
assert len(masks) == 1
|
| 164 |
|
| 165 |
|
| 166 |
@patch('mosaic.analysis.segment_tissue')
|
| 167 |
+
@patch('mosaic.analysis.gr.Warning')
|
| 168 |
+
def test_single_slide_no_tissue_found(self, mock_warning, mock_segment, mock_slide_path, cancer_subtype_name_map):
|
| 169 |
"""Test single-slide analysis when no tissue is found."""
|
| 170 |
# No tissue tiles found
|
| 171 |
+
mock_segment.return_value = None # segment_tissue returns None when no tissue
|
| 172 |
|
| 173 |
slide_mask, aeon_results, paladin_results = analyze_slide(
|
| 174 |
slide_path=mock_slide_path,
|
|
|
|
| 184 |
assert slide_mask is None
|
| 185 |
assert aeon_results is None
|
| 186 |
assert paladin_results is None
|
| 187 |
+
# Verify warning was raised
|
| 188 |
+
mock_warning.assert_called_once()
|
| 189 |
|
| 190 |
@patch('mosaic.analysis.segment_tissue')
|
| 191 |
@patch('mosaic.analysis.draw_slide_mask')
|
|
|
|
| 206 |
):
|
| 207 |
"""Test that single-slide with known subtype skips Aeon inference."""
|
| 208 |
# Setup minimal mocks
|
| 209 |
+
mock_polygon = Mock()
|
| 210 |
+
mock_coords = np.array([[0, 0]])
|
| 211 |
+
mock_attrs = {}
|
| 212 |
+
mock_segment.return_value = (mock_polygon, None, mock_coords, mock_attrs)
|
| 213 |
mock_mask.return_value = Mock()
|
| 214 |
mock_ctranspath.return_value = (np.random.rand(10, 768), np.array([[0, 0]]))
|
| 215 |
mock_filter.return_value = (None, np.array([[0, 0]]))
|
|
|
|
| 261 |
|
| 262 |
def test_analyze_slide_return_type_unchanged(self):
|
| 263 |
"""Test that analyze_slide returns the same tuple structure."""
|
| 264 |
+
with patch('mosaic.analysis.segment_tissue', return_value=None): # No tissue
|
| 265 |
+
with patch('mosaic.analysis.gr.Warning'): # Mock the warning
|
| 266 |
+
result = analyze_slide(
|
| 267 |
+
slide_path="test.svs",
|
| 268 |
+
seg_config="Biopsy",
|
| 269 |
+
site_type="Primary",
|
| 270 |
+
sex="Unknown",
|
| 271 |
+
tissue_site="Unknown",
|
| 272 |
+
cancer_subtype="Unknown",
|
| 273 |
+
cancer_subtype_name_map={"Unknown": "Unknown"},
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Should return tuple of 3 elements
|
| 277 |
+
assert isinstance(result, tuple)
|
| 278 |
+
assert len(result) == 3
|
| 279 |
|
| 280 |
|
| 281 |
if __name__ == "__main__":
|