Coverage for tests/ui/test_skull_stripping_page.py: 99%
248 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
1import pytest
2import os
3import tempfile
4import shutil
5from unittest.mock import Mock, patch, MagicMock, call
6from PyQt6.QtCore import QSettings, pyqtSignal, QObject
7from PyQt6.QtWidgets import QMessageBox
9from main.ui.skull_stripping_page import SkullStrippingPage
11@pytest.fixture
12def skull_page(qtbot, mock_context, mock_file_selector):
13 with patch('subprocess.run', return_value=Mock(returncode=0)):
14 previous_page = Mock()
15 page = SkullStrippingPage(mock_context, previous_page)
16 qtbot.addWidget(page)
17 page.show()
18 return page
20class TestSkullStrippingPageSetup:
21 """Tests for SkullStrippingPage initialization"""
23 def test_page_initialization(self, skull_page):
24 """Test correct initialization"""
25 assert skull_page.context is not None
26 assert skull_page.previous_page is not None
27 assert skull_page.worker is None
28 assert skull_page.canceled == False
30 def test_title_created(self, skull_page):
31 """Test title creation"""
32 assert skull_page.title is not None
33 assert skull_page.title.text() != ""
35 def test_file_selector_created(self, skull_page):
36 """Test file selector creation"""
37 assert skull_page.file_selector_widget is not None
39 def test_run_button_created(self, skull_page):
40 """Test run button creation"""
41 assert skull_page.run_button is not None
42 assert not skull_page.run_button.isEnabled() # Disabled initially
44 def test_cancel_button_created(self, skull_page):
45 """Test cancel button creation"""
46 assert skull_page.cancel_button is not None
47 assert not skull_page.cancel_button.isVisible() # Hidden initially
49 def test_progress_bar_created(self, skull_page):
50 """Test progress bar creation"""
51 assert skull_page.progress_bar is not None
52 assert not skull_page.progress_bar.isVisible() # Hidden initially
54 @patch('subprocess.run', return_value=Mock(returncode=0))
55 def test_bet_detected_when_available(self, mock_run, qtbot, mock_context, mock_file_selector):
56 """Test BET detection when available"""
57 page = SkullStrippingPage(mock_context, Mock())
58 qtbot.addWidget(page)
60 assert page.has_bet == True
62 @patch('subprocess.run', side_effect=FileNotFoundError())
63 def test_bet_not_detected_when_unavailable(self, mock_run, qtbot, mock_context, mock_file_selector):
64 """Test that BET is not detected when unavailable"""
65 page = SkullStrippingPage(mock_context, Mock())
66 qtbot.addWidget(page)
68 assert page.has_bet == False
71class TestSkullStrippingPageBETParameters:
72 """Tests for BET parameters"""
74 def test_f_parameter_default(self, skull_page):
75 """Test default value for f parameter"""
76 assert skull_page.f_spinbox.value() == 0.50
78 def test_f_parameter_range(self, skull_page):
79 """Test range for f parameter"""
80 assert skull_page.f_spinbox.minimum() == 0.0
81 assert skull_page.f_spinbox.maximum() == 1.0
83 def test_g_parameter_default(self, skull_page):
84 """Test default value for g parameter"""
85 assert skull_page.g_spinbox.value() == 0.0
87 def test_coordinate_parameters_default(self, skull_page):
88 """Test default coordinate values"""
89 assert skull_page.c_x_spinbox.value() == 0
90 assert skull_page.c_y_spinbox.value() == 0
91 assert skull_page.c_z_spinbox.value() == 0
93 def test_brain_extracted_checkbox_default(self, skull_page):
94 """Test that brain extracted is checked by default"""
95 assert skull_page.opt_brain_extracted.isChecked()
97 def test_other_checkboxes_default(self, skull_page):
98 """Test that other checkboxes are unchecked by default"""
99 assert not skull_page.opt_m.isChecked()
100 assert not skull_page.opt_t.isChecked()
101 assert not skull_page.opt_s.isChecked()
102 assert not skull_page.opt_o.isChecked()
105class TestSkullStrippingPageAdvancedOptions:
106 """Tests for advanced options"""
108 def test_advanced_options_hidden_initially(self, skull_page):
109 """Test that advanced options are hidden initially"""
110 assert not skull_page.advanced_box.isVisible()
112 def test_toggle_advanced_shows_options(self, skull_page):
113 """Test that toggle shows advanced options"""
114 skull_page.advanced_btn.setChecked(True)
115 skull_page.toggle_advanced() # call the function directly
116 assert skull_page.advanced_box.isVisible()
118 def test_toggle_advanced_hides_options(self, skull_page, qtbot):
119 """Test that toggle hides advanced options"""
120 # First show
121 skull_page.advanced_btn.setChecked(True)
122 skull_page.toggle_advanced()
123 assert skull_page.advanced_box.isVisible()
125 # Then hide
126 skull_page.advanced_btn.setChecked(False)
127 skull_page.toggle_advanced()
128 assert not skull_page.advanced_box.isVisible()
130 def test_toggle_updates_button_text(self, skull_page):
131 """Test that toggle updates the button text"""
132 initial_text = skull_page.advanced_btn.text()
134 skull_page.advanced_btn.setChecked(True)
135 skull_page.toggle_advanced()
137 assert skull_page.advanced_btn.text() != initial_text
140class TestSkullStrippingPageProcessing:
141 """Tests for processing"""
143 def test_run_without_files_shows_warning(self, skull_page, monkeypatch):
144 """Test warning when no files are present"""
145 skull_page.file_selector_widget.get_selected_files = Mock(return_value=[])
147 warning_shown = False
149 def mock_warning(*args, **kwargs):
150 nonlocal warning_shown
151 warning_shown = True
153 monkeypatch.setattr(QMessageBox, 'warning', mock_warning)
155 skull_page.run_bet()
156 assert warning_shown
158 @patch('main.ui.skull_stripping_page.SkullStripThread')
159 def test_run_creates_worker_thread(self, MockThread, skull_page):
160 """Test worker thread creation"""
161 skull_page.file_selector_widget.get_selected_files = Mock(
162 return_value=['/path/to/file.nii']
163 )
165 mock_worker = Mock()
166 MockThread.return_value = mock_worker
168 skull_page.run_bet()
170 MockThread.assert_called_once()
171 mock_worker.start.assert_called_once()
173 @patch('main.ui.skull_stripping_page.SkullStripThread')
174 def test_run_shows_progress_bar(self, MockThread, skull_page, qtbot):
175 """Test that run shows the progress bar"""
176 skull_page.file_selector_widget.get_selected_files = Mock(
177 return_value=['/path/to/file.nii']
178 )
180 mock_worker = Mock()
181 MockThread.return_value = mock_worker
183 with qtbot.waitSignal(skull_page.processing, timeout=1000):
184 skull_page.run_bet()
186 assert skull_page.progress_bar.isVisible()
188 def test_set_processing_mode_true(self, skull_page):
189 """Test set_processing_mode(True)"""
190 skull_page.set_processing_mode(True)
192 assert not skull_page.run_button.isVisible()
193 assert skull_page.cancel_button.isVisible()
195 def test_cancel_processing(self, skull_page):
196 """Test processing cancellation"""
197 mock_worker = Mock()
198 mock_worker.isRunning.return_value = True
199 skull_page.worker = mock_worker
201 skull_page.cancel_processing()
203 assert skull_page.canceled == True
204 mock_worker.cancel.assert_called_once()
207class TestSkullStrippingPageProgressCallbacks:
208 """Tests for progress callbacks"""
210 def test_on_progress_updated(self, skull_page):
211 """Test progress message update"""
212 test_message = "Processing file 1 of 3"
213 skull_page.on_progress_updated(test_message)
215 assert skull_page.status_label.text() == test_message
217 def test_on_progress_value_updated(self, skull_page):
218 """Test progress bar value update"""
219 skull_page.on_progress_value_updated(50)
221 assert skull_page.progress_bar.value() == 50
223 def test_on_all_completed_success(self, skull_page):
224 """Test completion callback on success"""
225 skull_page.on_all_completed(3, [])
227 assert not skull_page.progress_bar.isVisible()
228 assert "3" in skull_page.status_label.text()
230 def test_on_all_completed_with_failures(self, skull_page):
231 """Test completion callback with failures"""
232 failed_files = ['/path/file1.nii', '/path/file2.nii']
233 skull_page.on_all_completed(1, failed_files)
235 assert not skull_page.progress_bar.isVisible()
236 status_text = skull_page.status_label.text()
237 assert "1" in status_text # Successes
239 def test_on_all_completed_all_failed(self, skull_page):
240 """Test callback when all fail"""
241 failed_files = ['/path/file1.nii', '/path/file2.nii']
242 skull_page.on_all_completed(0, failed_files)
244 assert not skull_page.progress_bar.isVisible()
245 assert "failed" in skull_page.status_label.text().lower()
247 def test_on_worker_finished(self, skull_page, qtbot):
248 """Test worker finished callback"""
249 mock_worker = Mock()
250 skull_page.worker = mock_worker
252 with qtbot.waitSignal(skull_page.processing, timeout=1000):
253 skull_page.on_worker_finished()
255 skull_page.context["update_main_buttons"].assert_called()
258class TestSkullStrippingPageExistingCheck:
259 """Tests for existing skull strip check"""
260 @pytest.fixture
261 def temp_workspace(self):
262 temp_dir = tempfile.mkdtemp()
263 # Create structure with existing skull strip
264 subject_dir = os.path.join(temp_dir, "derivatives", "skullstrips", "sub-01", "anat")
265 os.makedirs(subject_dir)
266 with open(os.path.join(subject_dir, "brain.nii.gz"), "w") as f:
267 f.write("test")
268 yield temp_dir
269 shutil.rmtree(temp_dir, ignore_errors=True)
271 def test_has_existing_skull_strip_true(self, skull_page, temp_workspace):
272 """Test detection of existing skull strip"""
273 nifti_path = os.path.join(temp_workspace, "sub-01", "anat", "T1w.nii")
275 result = skull_page.has_existing_skull_strip(nifti_path, temp_workspace)
277 assert result == True
279 def test_has_existing_skull_strip_false(self, skull_page, temp_workspace):
280 """Test when skull strip does not exist"""
281 nifti_path = os.path.join(temp_workspace, "sub-02", "anat", "T1w.nii")
283 result = skull_page.has_existing_skull_strip(nifti_path, temp_workspace)
285 assert result == False
287 def test_has_existing_skull_strip_no_subject_id(self, skull_page, temp_workspace):
288 """Test behavior without subject ID"""
289 nifti_path = os.path.join(temp_workspace, "invalid", "T1w.nii")
291 result = skull_page.has_existing_skull_strip(nifti_path, temp_workspace)
293 assert result == False
296class TestSkullStrippingPageNavigation:
297 """Tests for navigation"""
299 def test_back_returns_previous_page(self, skull_page):
300 """Test return to previous page"""
301 result = skull_page.back()
302 assert result == skull_page.previous_page
303 skull_page.previous_page.on_enter.assert_called_once()
305 def test_back_blocked_during_processing(self, skull_page, monkeypatch):
306 """Test that back is blocked during processing"""
307 mock_worker = Mock()
308 mock_worker.isRunning.return_value = True
309 skull_page.worker = mock_worker
311 warning_shown = False
313 def mock_warning(*args, **kwargs):
314 nonlocal warning_shown
315 warning_shown = True
317 monkeypatch.setattr(QMessageBox, 'warning', mock_warning)
319 result = skull_page.back()
321 assert result is None
322 assert warning_shown
324 def test_is_ready_to_advance_false(self, skull_page):
325 """Test that advancing is not ready"""
326 assert not skull_page.is_ready_to_advance()
328 def test_is_ready_to_go_back_true_when_idle(self, skull_page):
329 """Test that going back is ready when idle"""
330 assert skull_page.is_ready_to_go_back()
332 def test_is_ready_to_go_back_false_during_processing(self, skull_page):
333 """Test that going back is not ready during processing"""
334 mock_worker = Mock()
335 mock_worker.isRunning.return_value = True
336 skull_page.worker = mock_worker
338 assert not skull_page.is_ready_to_go_back()
341class TestSkullStrippingPageReset:
342 """Tests for page reset"""
344 def test_reset_clears_files(self, skull_page):
345 """Test that reset clears files"""
346 skull_page.reset_page()
347 skull_page.file_selector_widget.clear_selected_files.assert_called_once()
349 def test_reset_parameters(self, skull_page):
350 """Test that reset restores parameters"""
351 # Modify parameters
352 skull_page.f_spinbox.setValue(0.7)
353 skull_page.g_spinbox.setValue(0.5)
355 # Reset
356 skull_page.reset_page()
358 # Verify default values
359 assert skull_page.f_spinbox.value() == 0.50
360 assert skull_page.g_spinbox.value() == 0.0
362 def test_reset_checkboxes(self, skull_page):
363 """Test that reset restores checkboxes"""
364 # Modify checkboxes
365 skull_page.opt_m.setChecked(True)
366 skull_page.opt_t.setChecked(True)
368 # Reset
369 skull_page.reset_page()
371 # Verify default values
372 assert skull_page.opt_brain_extracted.isChecked()
373 assert not skull_page.opt_m.isChecked()
374 assert not skull_page.opt_t.isChecked()
376 def test_reset_hides_progress_bar(self, skull_page):
377 """Test that reset hides progress bar"""
378 skull_page.progress_bar.setVisible(True)
380 skull_page.reset_page()
382 assert not skull_page.progress_bar.isVisible()
384 def test_reset_cancels_running_worker(self, skull_page):
385 """Test that reset cancels a running worker"""
386 mock_worker = Mock()
387 mock_worker.isRunning.return_value = True
388 skull_page.worker = mock_worker
390 skull_page.reset_page()
392 mock_worker.cancel.assert_called_once()
393 mock_worker.wait.assert_called_once()
396class TestSkullStrippingPageTranslation:
397 """Tests for translations"""
399 def test_translate_ui_updates_title(self, skull_page):
400 """Test title update"""
401 skull_page._translate_ui()
402 assert skull_page.title.text() != ""
404 def test_translate_ui_updates_buttons(self, skull_page):
405 """Test buttons update"""
406 skull_page._translate_ui()
407 assert skull_page.run_button.text() != ""
408 assert skull_page.cancel_button.text() != ""
411# Integration tests
412class TestSkullStrippingPageIntegration:
413 """Integration tests"""
415 @patch('main.ui.skull_stripping_page.SkullStripThread')
416 def test_full_processing_workflow(self, MockThread, skull_page, qtbot):
417 """Test full processing workflow"""
418 # Setup
419 skull_page.file_selector_widget.get_selected_files = Mock(
420 return_value=['/path/file.nii']
421 )
423 mock_worker = Mock()
424 MockThread.return_value = mock_worker
426 # Start processing
427 with qtbot.waitSignal(skull_page.processing, timeout=1000):
428 skull_page.run_bet()
430 # Verify processing state
431 assert skull_page.progress_bar.isVisible()
432 assert skull_page.cancel_button.isVisible()
433 assert not skull_page.run_button.isVisible()
435 # Simulate completion
436 with qtbot.waitSignal(skull_page.processing, timeout=1000):
437 skull_page.on_worker_finished()
439 # Verify final state
440 assert not skull_page.cancel_button.isVisible()
443if __name__ == "__main__":
444 pytest.main([__file__, "-v", "--tb=short"])