Coverage for tests/ui/test_pipeline_review_page.py: 100%
360 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 json
2import os
3import pytest
4from unittest.mock import Mock, patch, MagicMock
5from PyQt6.QtWidgets import QApplication, QMessageBox
6from unittest.mock import patch
8from main.ui.pipeline_review_page import PipelineReviewPage
11@pytest.fixture
12def pipeline_config_basic(temp_workspace):
13 """Creates a base config with one patient."""
14 config = {
15 "sub-01": {
16 "mri": os.path.join(temp_workspace, "sub-01", "anat", "T1w.nii"),
17 "pet": os.path.join(temp_workspace, "sub-01", "pet", "pet.nii"),
18 "need_revision": False
19 }
20 }
22 pipeline_dir = os.path.join(temp_workspace, "pipeline")
23 os.makedirs(pipeline_dir, exist_ok=True)
24 config_path = os.path.join(pipeline_dir, "01_config.json")
26 with open(config_path, "w", encoding="utf-8") as f:
27 json.dump(config, f, indent=4)
29 return config_path, config
32@pytest.fixture
33def pipeline_config_multiple(temp_workspace):
34 """Creates config with multiple patients and versions."""
35 configs = [
36 {
37 "sub-01": {
38 "mri": os.path.join(temp_workspace, "sub-01", "anat", "T1w.nii"),
39 "need_revision": False
40 }
41 },
42 {
43 "sub-01": {
44 "mri": os.path.join(temp_workspace, "sub-01", "anat", "T1w.nii"),
45 "need_revision": False
46 },
47 "sub-02": {
48 "mri": "path/to/mri2.nii",
49 "need_revision": True
50 }
51 }
52 ]
54 pipeline_dir = os.path.join(temp_workspace, "pipeline")
55 os.makedirs(pipeline_dir, exist_ok=True)
57 paths = []
58 for idx, config in enumerate(configs, start=1):
59 config_path = os.path.join(pipeline_dir, f"{idx:02d}_config.json")
60 with open(config_path, "w", encoding="utf-8") as f:
61 json.dump(config, f, indent=4)
62 paths.append(config_path)
64 return paths, configs
67@pytest.fixture
68def pipeline_config_need_revision(temp_workspace):
69 """Creates config with patients needing revision."""
70 config = {
71 "sub-01": {
72 "mri": os.path.join(temp_workspace, "sub-01", "anat", "T1w.nii"),
73 "need_revision": True
74 },
75 "sub-02": {
76 "mri": "path/to/mri2.nii",
77 "need_revision": False
78 }
79 }
81 pipeline_dir = os.path.join(temp_workspace, "pipeline")
82 os.makedirs(pipeline_dir, exist_ok=True)
83 config_path = os.path.join(pipeline_dir, "01_config.json")
85 with open(config_path, "w", encoding="utf-8") as f:
86 json.dump(config, f, indent=4)
88 return config_path, config
91class TestPipelineReviewPageInitialization:
92 """Tests for page initialization."""
94 def test_initialization_basic(self, qtbot, mock_context, pipeline_config_basic):
95 """Test basic initialization."""
96 config_path, config = pipeline_config_basic
97 page = PipelineReviewPage(mock_context)
98 qtbot.addWidget(page)
100 assert page.context == mock_context
101 assert page.workspace_path == mock_context["workspace_path"]
102 assert page.config_path == config_path
103 assert page.pipeline_config == config
104 assert page.previous_page is None
105 assert page.next_page is None
107 def test_initialization_with_previous_page(self, qtbot, mock_context, pipeline_config_basic):
108 """Test initialization with a previous page."""
109 previous = Mock()
110 page = PipelineReviewPage(mock_context, previous_page=previous)
111 qtbot.addWidget(page)
113 assert page.previous_page == previous
115 def test_ui_elements_created(self, qtbot, mock_context, pipeline_config_basic):
116 """Test that all UI elements are created."""
117 page = PipelineReviewPage(mock_context)
118 qtbot.addWidget(page)
120 assert page.header is not None
121 assert page.config_info is not None
122 assert page.info_label is not None
123 assert isinstance(page.patient_widgets, dict)
125 def test_patient_widgets_created(self, qtbot, mock_context, pipeline_config_basic):
126 """Test creation of widgets for patients."""
127 page = PipelineReviewPage(mock_context)
128 qtbot.addWidget(page)
130 assert "sub-01" in page.patient_widgets
131 assert page.patient_widgets["sub-01"] is not None
134class TestConfigFileDetection:
135 """Tests for config file detection."""
137 def test_find_latest_config_single(self, qtbot, mock_context, pipeline_config_basic):
138 """Test detection with a single config file."""
139 config_path, _ = pipeline_config_basic
140 page = PipelineReviewPage(mock_context)
141 qtbot.addWidget(page)
143 assert page.config_path == config_path
144 assert os.path.basename(page.config_path) == "01_config.json"
146 def test_find_latest_config_multiple(self, qtbot, mock_context, pipeline_config_multiple):
147 """Test detection with multiple configs (should pick the latest)."""
148 paths, configs = pipeline_config_multiple
149 page = PipelineReviewPage(mock_context)
150 qtbot.addWidget(page)
152 # Should pick the config with the highest ID (02_config.json)
153 assert page.config_path == paths[-1]
154 assert os.path.basename(page.config_path) == "02_config.json"
155 assert page.pipeline_config == configs[-1]
157 def test_find_latest_config_no_pipeline_dir(self, qtbot, mock_context, temp_workspace):
158 """Test when the pipeline directory does not exist."""
159 # Remove the pipeline directory if it exists
160 pipeline_dir = os.path.join(temp_workspace, "pipeline")
161 if os.path.exists(pipeline_dir):
162 import shutil
163 shutil.rmtree(pipeline_dir)
165 page = PipelineReviewPage(mock_context)
166 qtbot.addWidget(page)
168 expected_path = os.path.join(temp_workspace, "pipeline", "pipeline_config.json")
169 assert page.config_path == expected_path
170 assert page.pipeline_config == {}
172 def test_find_latest_config_no_files(self, qtbot, mock_context, temp_workspace):
173 """Test when the pipeline directory is empty."""
174 pipeline_dir = os.path.join(temp_workspace, "pipeline")
175 os.makedirs(pipeline_dir, exist_ok=True)
177 page = PipelineReviewPage(mock_context)
178 qtbot.addWidget(page)
180 expected_path = os.path.join(pipeline_dir, "pipeline_config.json")
181 assert page.config_path == expected_path
182 assert page.pipeline_config == {}
184 def test_find_latest_config_invalid_names(self, qtbot, mock_context, temp_workspace):
185 """Test with config files having invalid names."""
186 pipeline_dir = os.path.join(temp_workspace, "pipeline")
187 os.makedirs(pipeline_dir, exist_ok=True)
189 # Create files with non-conforming names
190 invalid_files = ["config.json", "abc_config.json", "test.json"]
191 for fname in invalid_files:
192 with open(os.path.join(pipeline_dir, fname), "w") as f:
193 json.dump({}, f)
195 page = PipelineReviewPage(mock_context)
196 qtbot.addWidget(page)
198 # Should use default because no file is valid
199 expected_path = os.path.join(pipeline_dir, "pipeline_config.json")
200 assert page.config_path == expected_path
203class TestConfigLoading:
204 """Tests for configuration loading."""
206 def test_load_config_valid(self, qtbot, mock_context, pipeline_config_basic):
207 """Test loading a valid config."""
208 config_path, expected_config = pipeline_config_basic
209 page = PipelineReviewPage(mock_context)
210 qtbot.addWidget(page)
212 assert page.pipeline_config == expected_config
214 def test_load_config_invalid_json(self, qtbot, mock_context, temp_workspace, mock_logger):
215 """Test loading a config with invalid JSON."""
216 pipeline_dir = os.path.join(temp_workspace, "pipeline")
217 os.makedirs(pipeline_dir, exist_ok=True)
218 config_path = os.path.join(pipeline_dir, "01_config.json")
220 # Write invalid JSON
221 with open(config_path, "w") as f:
222 f.write("{invalid json")
224 with patch("main.ui.pipeline_review_page.log", mock_logger):
225 page = PipelineReviewPage(mock_context)
226 qtbot.addWidget(page)
228 assert page.pipeline_config == {}
229 mock_logger.error.assert_called_once()
231 def test_load_config_file_not_found(self, qtbot, mock_context, temp_workspace, mock_logger):
232 """Test when the config file does not exist."""
233 with patch("main.ui.pipeline_review_page.log", mock_logger):
234 page = PipelineReviewPage(mock_context)
235 qtbot.addWidget(page)
237 assert page.pipeline_config == {}
238 mock_logger.warning.assert_called_once()
241class TestSaveConfiguration:
242 """Tests for saving the configuration."""
244 def test_save_single_patient(self, qtbot, mock_context, pipeline_config_basic):
245 """Test saving a single patient's configuration."""
246 config_path, _ = pipeline_config_basic
247 page = PipelineReviewPage(mock_context)
248 qtbot.addWidget(page)
250 # Modify and save
251 new_files = {
252 "mri": "new_path.nii",
253 "pet": "new_pet.nii",
254 "need_revision": False
255 }
256 page._save_single_patient("sub-01", new_files)
258 # Verify save to file
259 with open(config_path, "r", encoding="utf-8") as f:
260 saved_config = json.load(f)
262 assert saved_config["sub-01"] == new_files
263 assert page.pipeline_config["sub-01"] == new_files
265 def test_save_multiple_patients(self, qtbot, mock_context, pipeline_config_basic):
266 """Test saving multiple patients."""
267 config_path, _ = pipeline_config_basic
268 page = PipelineReviewPage(mock_context)
269 qtbot.addWidget(page)
271 # Add a new patient
272 new_patient = {
273 "mri": "path2.nii",
274 "need_revision": False
275 }
276 page._save_single_patient("sub-02", new_patient)
278 # Verify
279 with open(config_path, "r", encoding="utf-8") as f:
280 saved_config = json.load(f)
282 assert "sub-01" in saved_config
283 assert "sub-02" in saved_config
284 assert saved_config["sub-02"] == new_patient
287class TestOnEnter:
288 """Tests for the on_enter method."""
290 def test_on_enter_no_changes(self, qtbot, mock_context, pipeline_config_basic, mock_logger):
291 """Test on_enter when there are no changes."""
292 page = PipelineReviewPage(mock_context)
293 qtbot.addWidget(page)
295 initial_config_path = page.config_path
296 initial_config = page.pipeline_config.copy()
298 with patch("main.ui.pipeline_review_page.log", mock_logger):
299 page.on_enter()
301 assert page.config_path == initial_config_path
302 assert page.pipeline_config == initial_config
303 mock_logger.debug.assert_called_once()
305 def test_on_enter_new_config_file(self, qtbot, mock_context, pipeline_config_basic, mock_logger):
306 """Test on_enter when a new config file is created."""
307 page = PipelineReviewPage(mock_context)
308 qtbot.addWidget(page)
310 # Create new config
311 pipeline_dir = os.path.join(mock_context["workspace_path"], "pipeline")
312 new_config_path = os.path.join(pipeline_dir, "02_config.json")
313 new_config = {
314 "sub-03": {
315 "mri": "new_path.nii",
316 "need_revision": False
317 }
318 }
319 with open(new_config_path, "w", encoding="utf-8") as f:
320 json.dump(new_config, f)
322 with patch("main.ui.pipeline_review_page.log", mock_logger):
323 page.on_enter()
325 assert page.config_path == new_config_path
326 assert page.pipeline_config == new_config
327 mock_logger.info.assert_called_once()
329 def test_on_enter_config_content_changed(self, qtbot, mock_context, pipeline_config_basic, mock_logger):
330 """Test on_enter when the config content changes."""
331 config_path, _ = pipeline_config_basic
332 page = PipelineReviewPage(mock_context)
333 qtbot.addWidget(page)
335 # Modify the config file directly
336 modified_config = {
337 "sub-01": {
338 "mri": "modified_path.nii",
339 "need_revision": True
340 }
341 }
342 with open(config_path, "w", encoding="utf-8") as f:
343 json.dump(modified_config, f)
345 with patch("main.ui.pipeline_review_page.log", mock_logger):
346 page.on_enter()
348 assert page.pipeline_config == modified_config
349 mock_logger.info.assert_called_once()
352class TestNextNavigation:
353 """Tests for navigating to the next page."""
355 @patch('ui.pipeline_execution_page.get_bin_path')
356 def test_next_all_saved(self, mock_get_bin, qtbot, mock_context, pipeline_config_basic):
357 """Test next when all patients are saved."""
358 mock_get_bin.return_value = "/fake/path/to/pipeline_runner"
360 page = PipelineReviewPage(mock_context)
361 qtbot.addWidget(page)
363 result = page.next(mock_context)
365 assert result != page
366 assert page.next_page is not None
367 assert "PipelineExecutionPage" in str(type(page.next_page))
369 def test_next_need_revision(self, qtbot, mock_context, pipeline_config_need_revision):
370 """Test next when patients need revision."""
371 config_path, _ = pipeline_config_need_revision
372 page = PipelineReviewPage(mock_context)
373 qtbot.addWidget(page)
375 with patch.object(QMessageBox, 'exec', return_value=QMessageBox.StandardButton.Ok):
376 result = page.next(mock_context)
378 assert result == page # Must stay on the current page
379 assert page.next_page is None
381 @patch('ui.pipeline_execution_page.get_bin_path')
382 def test_next_existing_next_page(self, mock_get_bin, qtbot, mock_context, pipeline_config_basic):
383 """Test next when next_page already exists."""
384 mock_get_bin.return_value = "/fake/path/to/pipeline_runner"
386 page = PipelineReviewPage(mock_context)
387 qtbot.addWidget(page)
389 # First call
390 result1 = page.next(mock_context)
391 first_next_page = page.next_page
393 # Simulate returning to the page
394 page.on_enter()
396 # Second call
397 result2 = page.next(mock_context)
399 # Must reuse the same next_page
400 assert page.next_page == first_next_page
402 @patch('ui.pipeline_execution_page.get_bin_path')
403 def test_next_updates_history(self, mock_get_bin, qtbot, mock_context, pipeline_config_basic):
404 """Test that next adds to history."""
405 mock_get_bin.return_value = "/fake/path/to/pipeline_runner"
407 page = PipelineReviewPage(mock_context)
408 qtbot.addWidget(page)
410 initial_history_len = len(mock_context["history"])
411 page.next(mock_context)
413 assert len(mock_context["history"]) == initial_history_len + 1
414 assert page.next_page in mock_context["history"]
417class TestBackNavigation:
418 """Tests for navigating to the previous page."""
420 def test_back_with_output_folder(self, qtbot, mock_context, pipeline_config_basic, mock_logger):
421 """Test back when output folder exists (must keep config)."""
422 config_path, _ = pipeline_config_basic
423 page = PipelineReviewPage(mock_context)
424 qtbot.addWidget(page)
426 # Create the output folder
427 pipeline_dir = os.path.dirname(config_path)
428 output_folder = os.path.join(pipeline_dir, "01_output")
429 os.makedirs(output_folder)
431 previous = Mock()
432 page.previous_page = previous
434 with patch("main.ui.pipeline_review_page.log", mock_logger):
435 result = page.back()
437 assert result == previous
438 assert os.path.exists(config_path) # Config must not be deleted
439 mock_logger.info.assert_called()
441 def test_back_without_output_folder(self, qtbot, mock_context, pipeline_config_basic, mock_logger):
442 """Test back when output folder does not exist (must delete config)."""
443 config_path, _ = pipeline_config_basic
444 page = PipelineReviewPage(mock_context)
445 qtbot.addWidget(page)
447 previous = Mock()
448 page.previous_page = previous
450 with patch("main.ui.pipeline_review_page.log", mock_logger):
451 result = page.back()
453 assert result == previous
454 assert not os.path.exists(config_path) # Config must be deleted
455 mock_logger.info.assert_called()
457 def test_back_no_previous_page(self, qtbot, mock_context, pipeline_config_basic):
458 """Test back without previous_page."""
459 page = PipelineReviewPage(mock_context)
460 qtbot.addWidget(page)
462 result = page.back()
464 assert result is None
466 def test_back_calls_on_enter(self, qtbot, mock_context, pipeline_config_basic):
467 """Test that back calls on_enter on the previous page."""
468 page = PipelineReviewPage(mock_context)
469 qtbot.addWidget(page)
471 previous = Mock()
472 page.previous_page = previous
474 page.back()
476 previous.on_enter.assert_called_once()
478 def test_back_error_handling(self, qtbot, mock_context, pipeline_config_basic, mock_logger):
479 """Test error handling during back."""
480 page = PipelineReviewPage(mock_context)
481 qtbot.addWidget(page)
483 # Make the config_path invalid
484 page.config_path = "/invalid/path/config.json"
486 previous = Mock()
487 page.previous_page = previous
489 with patch("main.ui.pipeline_review_page.log", mock_logger):
490 result = page.back()
492 assert result == previous
493 mock_logger.error.assert_called()
496class TestTranslation:
497 """Tests for translations."""
499 def test_translate_ui_called_on_init(self, qtbot, mock_context, pipeline_config_basic):
500 """Test that _translate_ui is called during init."""
501 with patch.object(PipelineReviewPage, '_translate_ui') as mock_translate:
502 page = PipelineReviewPage(mock_context)
503 qtbot.addWidget(page)
505 mock_translate.assert_called()
507 def test_translate_ui_updates_labels(self, qtbot, mock_context, pipeline_config_basic):
508 """Test that _translate_ui updates labels."""
509 page = PipelineReviewPage(mock_context)
510 qtbot.addWidget(page)
512 # Save original texts
513 original_header = page.header.text()
515 # Call _translate_ui
516 page._translate_ui()
518 # Verify texts were updated (even if identical)
519 assert page.header.text() is not None
520 assert page.config_info.text() is not None
521 assert page.info_label.text() is not None
523 def test_language_changed_signal(self, mock_context):
524 from main.ui.pipeline_review_page import PipelineReviewPage
525 page = PipelineReviewPage(mock_context)
527 # Replace the function but reconnect the signal
528 page._translate_ui = Mock()
529 mock_context["language_changed"].connect(page._translate_ui)
531 # Now emitting the signal must trigger the mock
532 mock_context["language_changed"].emit("it")
534 page._translate_ui.assert_called_once()
537class TestUISetup:
538 """Tests for UI configuration."""
540 def test_setup_ui_clears_existing_layout(self, qtbot, mock_context, pipeline_config_basic):
541 """Test that _setup_ui clears the existing layout."""
542 page = PipelineReviewPage(mock_context)
543 qtbot.addWidget(page)
545 initial_count = page.main_layout.count()
547 # Call _setup_ui again
548 page._setup_ui()
550 # The count might be the same or different, but it shouldn't crash
551 assert page.main_layout.count() >= 0
553 def test_setup_ui_creates_scroll_area(self, qtbot, mock_context, pipeline_config_basic):
554 """Test that a scroll area is created."""
555 page = PipelineReviewPage(mock_context)
556 qtbot.addWidget(page)
558 # Search for scroll area in the layout
559 scroll_found = False
560 for i in range(page.main_layout.count()):
561 widget = page.main_layout.itemAt(i).widget()
562 if widget and "QScrollArea" in str(type(widget)):
563 scroll_found = True
564 break
566 assert scroll_found
568 def test_patient_widgets_created_for_all_patients(self, qtbot, mock_context, pipeline_config_multiple):
569 """Test that widgets are created for all patients."""
570 paths, configs = pipeline_config_multiple
571 page = PipelineReviewPage(mock_context)
572 qtbot.addWidget(page)
574 # Must have widgets for all patients in the last config
575 last_config = configs[-1]
576 for patient_id in last_config.keys():
577 assert patient_id in page.patient_widgets
580class TestEdgeCases:
581 """Tests for edge cases."""
583 def test_empty_config(self, qtbot, mock_context, temp_workspace):
584 """Test with an empty config."""
585 pipeline_dir = os.path.join(temp_workspace, "pipeline")
586 os.makedirs(pipeline_dir, exist_ok=True)
587 config_path = os.path.join(pipeline_dir, "01_config.json")
589 with open(config_path, "w", encoding="utf-8") as f:
590 json.dump({}, f)
592 page = PipelineReviewPage(mock_context)
593 qtbot.addWidget(page)
595 assert page.pipeline_config == {}
596 assert len(page.patient_widgets) == 0
598 def test_very_large_config(self, qtbot, mock_context, temp_workspace):
599 """Test with many patients."""
600 pipeline_dir = os.path.join(temp_workspace, "pipeline")
601 os.makedirs(pipeline_dir, exist_ok=True)
602 config_path = os.path.join(pipeline_dir, "01_config.json")
604 # Create config with 100 patients
605 large_config = {
606 f"sub-{i:03d}": {
607 "mri": f"path{i}.nii",
608 "need_revision": False
609 }
610 for i in range(100)
611 }
613 with open(config_path, "w", encoding="utf-8") as f:
614 json.dump(large_config, f)
616 page = PipelineReviewPage(mock_context)
617 qtbot.addWidget(page)
619 assert len(page.pipeline_config) == 100
620 assert len(page.patient_widgets) == 100
622 def test_special_characters_in_patient_id(self, qtbot, mock_context, temp_workspace):
623 """Test with special characters in patient IDs."""
624 pipeline_dir = os.path.join(temp_workspace, "pipeline")
625 os.makedirs(pipeline_dir, exist_ok=True)
626 config_path = os.path.join(pipeline_dir, "01_config.json")
628 config = {
629 "sub-01_special-chars": {
630 "mri": "path.nii",
631 "need_revision": False
632 }
633 }
635 with open(config_path, "w", encoding="utf-8") as f:
636 json.dump(config, f)
638 page = PipelineReviewPage(mock_context)
639 qtbot.addWidget(page)
641 assert "sub-01_special-chars" in page.patient_widgets
643 def test_unicode_in_paths(self, qtbot, mock_context, temp_workspace):
644 """Test with unicode characters in paths."""
645 pipeline_dir = os.path.join(temp_workspace, "pipeline")
646 os.makedirs(pipeline_dir, exist_ok=True)
647 config_path = os.path.join(pipeline_dir, "01_config.json")
649 config = {
650 "sub-01": {
651 "mri": "path/àèéìòù/file.nii",
652 "need_revision": False
653 }
654 }
656 with open(config_path, "w", encoding="utf-8") as f:
657 json.dump(config, f)
659 page = PipelineReviewPage(mock_context)
660 qtbot.addWidget(page)
662 assert page.pipeline_config["sub-01"]["mri"] == "path/àèéìòù/file.nii"