Coverage for tests/components/test_file_role_dialog.py: 98%
482 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 os
2import tempfile
4import pytest
5from unittest.mock import Mock, patch, MagicMock
6from PyQt6.QtWidgets import QDialogButtonBox, QRadioButton, QComboBox, QListWidgetItem, QListWidget
7from PyQt6.QtCore import Qt
9from main.components.file_role_dialog import FileRoleDialog
12@pytest.fixture
13def workspace_with_subjects(temp_workspace):
14 """Creates a workspace with subjects and BIDS structure."""
15 subjects = ["sub-01", "sub-02", "sub-03"]
16 for subj in subjects:
17 subj_dir = os.path.join(temp_workspace, subj)
18 os.makedirs(subj_dir, exist_ok=True)
20 anat_dir = os.path.join(subj_dir, "anat")
21 os.makedirs(anat_dir, exist_ok=True)
23 for ses in ["ses-01", "ses-02"]:
24 ses_dir = os.path.join(subj_dir, ses, "pet")
25 os.makedirs(ses_dir, exist_ok=True)
27 derivatives_dir = os.path.join(temp_workspace, "derivatives")
28 os.makedirs(derivatives_dir, exist_ok=True)
30 for derivative in ["skullstrips", "manual_masks", "deep_learning_masks"]:
31 for subj in subjects:
32 deriv_subj_dir = os.path.join(derivatives_dir, derivative, subj, "anat")
33 os.makedirs(deriv_subj_dir, exist_ok=True)
35 return temp_workspace
37class TestFileRoleDialogInitialization:
38 """Tests for the dialog initialization."""
40 def test_initialization_full_dialog(self, qtbot, workspace_with_subjects):
41 """Test full dialog initialization (all levels)."""
42 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
43 qtbot.addWidget(dialog)
45 assert dialog.workspace_path == workspace_with_subjects
46 assert dialog.subj is None
47 assert dialog.role is None
48 assert dialog.main is None
50 # Verify levels are present
51 assert hasattr(dialog, 'level1_widget')
52 assert hasattr(dialog, 'level2_widget')
53 assert hasattr(dialog, 'level3_widget')
54 assert hasattr(dialog, 'ok_button')
56 def test_initialization_with_main_derivatives(self, qtbot, workspace_with_subjects):
57 """Test initialization with main='derivatives'."""
58 dialog = FileRoleDialog(
59 workspace_path=workspace_with_subjects,
60 main="derivatives"
61 )
62 qtbot.addWidget(dialog)
64 assert dialog.main == "derivatives"
65 assert dialog.button_first_group is None
66 assert hasattr(dialog, 'derivative_extra_frame')
67 assert dialog.derivative_extra_frame.isVisibleTo(dialog)
69 def test_initialization_with_subject(self, qtbot, workspace_with_subjects):
70 """Test initialization with a specified subject."""
71 dialog = FileRoleDialog(
72 workspace_path=workspace_with_subjects,
73 subj="sub-01"
74 )
75 qtbot.addWidget(dialog)
77 assert dialog.subj == "sub-01"
78 assert dialog.button_second_group is None
79 assert not hasattr(dialog, 'subj_combo')
81 def test_initialization_with_role(self, qtbot, workspace_with_subjects):
82 """Test initialization with a specified role."""
83 dialog = FileRoleDialog(
84 workspace_path=workspace_with_subjects,
85 role="anat"
86 )
87 qtbot.addWidget(dialog)
89 assert dialog.role == "anat"
90 assert dialog.button_third_group is None
91 assert not hasattr(dialog, 'level3_widget')
93 def test_initialization_partial_params(self, qtbot, workspace_with_subjects):
94 """Test initialization with partial parameters."""
95 dialog = FileRoleDialog(
96 workspace_path=workspace_with_subjects,
97 main="derivatives",
98 subj="sub-02"
99 )
100 qtbot.addWidget(dialog)
102 assert dialog.main == "derivatives"
103 assert dialog.subj == "sub-02"
104 assert dialog.role is None
106 def test_ok_button_initially_disabled(self, qtbot, workspace_with_subjects):
107 """Test that the OK button is initially disabled."""
108 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
109 qtbot.addWidget(dialog)
111 assert not dialog.ok_button.isEnabled()
113 def test_window_title_set(self, qtbot, workspace_with_subjects):
114 """Test that the window title is set."""
115 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
116 qtbot.addWidget(dialog)
118 assert dialog.windowTitle() != ""
121class TestFindPatientDirs:
122 """Tests for the _find_patient_dirs method."""
124 def test_find_patient_dirs_basic(self, qtbot, workspace_with_subjects):
125 """Test basic patient directory search."""
126 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
127 qtbot.addWidget(dialog)
129 patient_dirs = dialog._find_patient_dirs()
131 assert len(patient_dirs) == 3
133 for p in patient_dirs:
134 assert 'sub-' in os.path.basename(p)
136 def test_find_patient_dirs_excludes_derivatives(self, qtbot, workspace_with_subjects):
137 """Test that the search excludes derivatives."""
138 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
139 qtbot.addWidget(dialog)
141 patient_dirs = dialog._find_patient_dirs()
143 for p in patient_dirs:
144 assert 'derivatives' not in p.split(os.sep)
146 def test_find_patient_dirs_empty_workspace(self, qtbot):
147 """Test with an empty workspace."""
148 temp_dir = tempfile.mkdtemp()
149 os.makedirs(os.path.join(temp_dir, "empty"), exist_ok=True)
150 dialog = FileRoleDialog(workspace_path=temp_dir)
151 qtbot.addWidget(dialog)
153 patient_dirs = dialog._find_patient_dirs()
155 assert len(patient_dirs) == 0
157 def test_find_patient_dirs_nested_subjects(self, qtbot, temp_workspace):
158 """Test with nested subjects."""
159 # Setup path
160 nested_path = os.path.join(temp_workspace, "study", "cohort1", "sub-nested")
161 os.makedirs(nested_path)
163 dialog = FileRoleDialog(workspace_path=temp_workspace)
164 qtbot.addWidget(dialog)
166 patient_dirs = dialog._find_patient_dirs()
168 assert len(patient_dirs) == 3
170 dir_names = [os.path.basename(p) for p in patient_dirs]
172 assert "sub-01" in dir_names
173 assert "sub-nested" in dir_names
176class TestLevel1MainDerivatives:
177 """Tests for level 1 (Main/Derivatives)."""
179 def test_main_selected(self, qtbot, workspace_with_subjects):
180 """Test 'main' selection."""
181 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
182 qtbot.addWidget(dialog)
184 dialog.opt_main.setChecked(True)
186 selections = dialog.get_selections()
187 assert selections['main'] == "main subject files"
189 def test_derivatives_selected(self, qtbot, workspace_with_subjects):
190 """Test 'derivatives' selection."""
191 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
192 qtbot.addWidget(dialog)
194 dialog.opt_derivatives.setChecked(True)
196 selections = dialog.get_selections()
197 assert selections['main'] == "derivatives"
199 def test_derivatives_shows_extra_frame(self, qtbot, workspace_with_subjects):
200 """Test that selecting derivatives shows the extra frame."""
201 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
202 qtbot.addWidget(dialog)
204 assert not dialog.derivative_extra_frame.isVisibleTo(dialog)
206 dialog.opt_derivatives.setChecked(True)
208 assert dialog.derivative_extra_frame.isVisibleTo(dialog)
210 def test_main_hides_extra_frame(self, qtbot, workspace_with_subjects):
211 """Test that selecting main hides the extra frame."""
212 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
213 qtbot.addWidget(dialog)
215 dialog.opt_derivatives.setChecked(True)
216 assert dialog.derivative_extra_frame.isVisibleTo(dialog)
218 dialog.opt_main.setChecked(True)
219 assert not dialog.derivative_extra_frame.isVisibleTo(dialog)
221 def test_derivative_type_selection(self, qtbot, workspace_with_subjects):
222 """Test derivative type selection."""
223 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
224 qtbot.addWidget(dialog)
226 dialog.opt_derivatives.setChecked(True)
227 dialog.skull_strip_btn.setChecked(True)
229 selections = dialog.get_selections()
230 assert selections['derivative'] == "skullstrips"
232 def test_all_derivative_types(self, qtbot, workspace_with_subjects):
233 """Test all derivative types."""
234 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
235 qtbot.addWidget(dialog)
237 dialog.opt_derivatives.setChecked(True)
239 derivative_buttons = [
240 (dialog.skull_strip_btn, "skullstrips"),
241 (dialog.manual_mask_btn, "manual_masks"),
242 (dialog.deep_learning_mask, "deep_learning_seg")
243 ]
245 for button, expected_text in derivative_buttons:
246 button.setChecked(True)
247 selections = dialog.get_selections()
248 assert selections['derivative'] == expected_text
251class TestLevel2Subject:
252 """Tests for level 2 (Subject)."""
254 def test_subject_combo_populated(self, qtbot, workspace_with_subjects):
255 """Test that the subject combo box is populated."""
256 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
257 qtbot.addWidget(dialog)
259 assert dialog.subj_combo.count() == 3
261 subjects = [dialog.subj_combo.itemText(i) for i in range(dialog.subj_combo.count())]
262 assert "sub-01" in subjects
263 assert "sub-02" in subjects
264 assert "sub-03" in subjects
266 def test_subject_selection(self, qtbot, workspace_with_subjects):
267 """Test subject selection."""
268 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
269 qtbot.addWidget(dialog)
271 dialog.subj_combo.setCurrentText("sub-02")
273 selections = dialog.get_selections()
274 assert selections['subj'] == "sub-02"
276 def test_subject_combo_default_selection(self, qtbot, workspace_with_subjects):
277 """Test default subject combo selection."""
278 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
279 qtbot.addWidget(dialog)
281 selections = dialog.get_selections()
282 assert selections['subj'] is not None
284 def test_subject_not_shown_when_provided(self, qtbot, workspace_with_subjects):
285 """Test that the subject level is not shown if provided."""
286 dialog = FileRoleDialog(
287 workspace_path=workspace_with_subjects,
288 subj="sub-01"
289 )
290 qtbot.addWidget(dialog)
292 assert not hasattr(dialog, 'subj_combo')
293 assert not hasattr(dialog, 'level2_widget')
296class TestLevel3Role:
297 """Tests for level 3 (Role)."""
299 def test_anat_role_selection(self, qtbot, workspace_with_subjects):
300 """Test 'anat' role selection."""
301 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
302 qtbot.addWidget(dialog)
304 dialog.anat_button.setChecked(True)
306 selections = dialog.get_selections()
307 assert selections['role'] == "anat"
309 def test_ses01_role_selection(self, qtbot, workspace_with_subjects):
310 """Test 'ses-01' role selection."""
311 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
312 qtbot.addWidget(dialog)
314 dialog.ses_1_button.setChecked(True)
316 selections = dialog.get_selections()
317 assert selections['role'] == "ses-01"
319 def test_ses02_role_selection(self, qtbot, workspace_with_subjects):
320 """Test 'ses-02' role selection."""
321 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
322 qtbot.addWidget(dialog)
324 dialog.ses_2_button.setChecked(True)
326 selections = dialog.get_selections()
327 assert selections['role'] == "ses-02"
329 def test_role_not_shown_when_provided(self, qtbot, workspace_with_subjects):
330 """Test that the role level is not shown if provided."""
331 dialog = FileRoleDialog(
332 workspace_path=workspace_with_subjects,
333 role="anat"
334 )
335 qtbot.addWidget(dialog)
337 assert not hasattr(dialog, 'level3_widget')
338 assert dialog.button_third_group is None
341class TestGetSelections:
342 """Tests for the get_selections method."""
344 def test_get_selections_all_levels(self, qtbot, workspace_with_subjects):
345 """Test get_selections with all levels."""
346 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
347 qtbot.addWidget(dialog)
349 dialog.opt_main.setChecked(True)
350 dialog.subj_combo.setCurrentText("sub-01")
351 dialog.anat_button.setChecked(True)
353 selections = dialog.get_selections()
355 assert selections['main'] == "main subject files"
356 assert selections['subj'] == "sub-01"
357 assert selections['role'] == "anat"
359 def test_get_selections_derivatives(self, qtbot, workspace_with_subjects):
360 """Test get_selections with derivatives."""
361 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
362 qtbot.addWidget(dialog)
364 dialog.opt_derivatives.setChecked(True)
365 dialog.skull_strip_btn.setChecked(True)
366 dialog.subj_combo.setCurrentText("sub-02")
367 dialog.anat_button.setChecked(True)
369 selections = dialog.get_selections()
371 assert selections['main'] == "derivatives"
372 assert selections['derivative'] == "skullstrips"
373 assert selections['subj'] == "sub-02"
374 assert selections['role'] == "anat"
376 def test_get_selections_partial(self, qtbot, workspace_with_subjects):
377 """Test get_selections with partial selections."""
378 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
379 qtbot.addWidget(dialog)
381 dialog.opt_main.setChecked(True)
383 selections = dialog.get_selections()
385 assert selections['main'] == "main subject files"
386 assert selections.get('derivative') is None
389class TestGetRelativePath:
390 """Tests for the get_relative_path method."""
392 def test_relative_path_main_anat(self, qtbot, workspace_with_subjects):
393 """Test relative path: main/subject/anat."""
394 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
395 qtbot.addWidget(dialog)
397 dialog.opt_main.setChecked(True)
398 dialog.subj_combo.setCurrentText("sub-01")
399 dialog.anat_button.setChecked(True)
401 path = dialog.get_relative_path()
403 assert path == os.path.join("sub-01", "anat")
405 def test_relative_path_main_session(self, qtbot, workspace_with_subjects):
406 """Test relative path: main/subject/session/pet."""
407 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
408 qtbot.addWidget(dialog)
410 dialog.opt_main.setChecked(True)
411 dialog.subj_combo.setCurrentText("sub-02")
412 dialog.ses_1_button.setChecked(True)
414 path = dialog.get_relative_path()
416 assert path == os.path.join("sub-02", "ses-01", "pet")
418 def test_relative_path_derivatives(self, qtbot, workspace_with_subjects):
419 """Test relative path: derivatives/type/subject/anat."""
420 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
421 qtbot.addWidget(dialog)
423 dialog.opt_derivatives.setChecked(True)
424 dialog.skull_strip_btn.setChecked(True)
425 dialog.subj_combo.setCurrentText("sub-01")
426 dialog.anat_button.setChecked(True)
428 path = dialog.get_relative_path()
430 assert path == os.path.join("derivatives", "skullstrips", "sub-01", "anat")
432 def test_relative_path_derivatives_with_session(self, qtbot, workspace_with_subjects):
433 """Test relative path: derivatives with session."""
434 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
435 qtbot.addWidget(dialog)
437 dialog.opt_derivatives.setChecked(True)
438 dialog.manual_mask_btn.setChecked(True)
439 dialog.subj_combo.setCurrentText("sub-03")
440 dialog.ses_2_button.setChecked(True)
442 path = dialog.get_relative_path()
444 assert path == os.path.join("derivatives", "manual_masks", "sub-03", "ses-02", "pet")
446 def test_relative_path_empty(self, qtbot, workspace_with_subjects):
447 """Test empty relative path (no selection)."""
448 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
449 qtbot.addWidget(dialog)
451 path = dialog.get_relative_path()
453 # With no selection (except default subject), path might be partial
454 assert path is not None or path is None # Depends on implementation
456 def test_relative_path_session_pattern_matching(self, qtbot, workspace_with_subjects):
457 """Test that the ses-XX pattern adds /pet."""
458 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
459 qtbot.addWidget(dialog)
461 dialog.opt_main.setChecked(True)
462 dialog.subj_combo.setCurrentText("sub-01")
464 dialog.ses_1_button.setChecked(True)
465 path1 = dialog.get_relative_path()
466 assert path1.endswith("pet")
468 dialog.ses_2_button.setChecked(True)
469 path2 = dialog.get_relative_path()
470 assert path2.endswith("pet")
472 dialog.anat_button.setChecked(True)
473 path3 = dialog.get_relative_path()
474 assert not path3.endswith("pet")
477class TestUpdateOkButton:
478 """Tests for the update_ok_button method."""
480 def test_ok_button_disabled_initially(self, qtbot, workspace_with_subjects):
481 """Test that OK is disabled initially."""
482 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
483 qtbot.addWidget(dialog)
485 assert not dialog.ok_button.isEnabled()
487 def test_ok_button_enabled_all_selected(self, qtbot, workspace_with_subjects):
488 """Test that OK is enabled with all selections."""
489 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
490 qtbot.addWidget(dialog)
492 dialog.opt_main.setChecked(True)
493 dialog.subj_combo.setCurrentText("sub-01")
494 dialog.anat_button.setChecked(True)
496 assert dialog.ok_button.isEnabled()
498 def test_ok_button_disabled_no_main(self, qtbot, workspace_with_subjects):
499 """Test OK disabled without main."""
500 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
501 qtbot.addWidget(dialog)
503 dialog.subj_combo.setCurrentText("sub-01")
504 dialog.anat_button.setChecked(True)
506 assert not dialog.ok_button.isEnabled()
508 def test_ok_button_disabled_no_subject(self, qtbot, workspace_with_subjects):
509 """Test OK disabled without subject."""
510 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
511 qtbot.addWidget(dialog)
513 dialog.opt_main.setChecked(True)
514 dialog.anat_button.setChecked(True)
515 # This test may not be valid if the combo always has a default selection
517 def test_ok_button_disabled_no_role(self, qtbot, workspace_with_subjects):
518 """Test OK disabled without role."""
519 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
520 qtbot.addWidget(dialog)
522 dialog.opt_main.setChecked(True)
523 dialog.subj_combo.setCurrentText("sub-01")
525 assert not dialog.ok_button.isEnabled()
527 def test_ok_button_disabled_derivatives_no_type(self, qtbot, workspace_with_subjects):
528 """Test OK disabled with derivatives but no type."""
529 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
530 qtbot.addWidget(dialog)
532 dialog.opt_derivatives.setChecked(True)
533 dialog.subj_combo.setCurrentText("sub-01")
534 dialog.anat_button.setChecked(True)
536 assert not dialog.ok_button.isEnabled()
538 def test_ok_button_enabled_derivatives_complete(self, qtbot, workspace_with_subjects):
539 """Test OK enabled with complete derivatives."""
540 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
541 qtbot.addWidget(dialog)
543 dialog.opt_derivatives.setChecked(True)
544 dialog.skull_strip_btn.setChecked(True)
545 dialog.subj_combo.setCurrentText("sub-01")
546 dialog.anat_button.setChecked(True)
548 assert dialog.ok_button.isEnabled()
550 def test_ok_button_updates_on_change(self, qtbot, workspace_with_subjects):
551 """Test that OK updates on every change."""
552 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
553 qtbot.addWidget(dialog)
555 assert not dialog.ok_button.isEnabled()
557 dialog.opt_main.setChecked(True)
558 assert not dialog.ok_button.isEnabled()
560 dialog.subj_combo.setCurrentText("sub-01")
561 assert not dialog.ok_button.isEnabled()
563 dialog.anat_button.setChecked(True)
564 assert dialog.ok_button.isEnabled()
567class TestDialogButtons:
568 """Tests for the dialog buttons."""
570 def test_accept_button_exists(self, qtbot, workspace_with_subjects):
571 """Test that the Accept button exists."""
572 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
573 qtbot.addWidget(dialog)
575 assert dialog.ok_button is not None
577 def test_cancel_button_exists(self, qtbot, workspace_with_subjects):
578 """Test that the Cancel button exists."""
579 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
580 qtbot.addWidget(dialog)
582 buttons = dialog.findChildren(QDialogButtonBox)
583 assert len(buttons) > 0
585 button_box = buttons[0]
586 cancel_btn = button_box.button(QDialogButtonBox.StandardButton.Cancel)
587 assert cancel_btn is not None
590class TestEdgeCases:
591 """Tests for edge cases."""
593 def test_empty_workspace(self, qtbot):
594 """Test with empty workspace."""
595 temp_dir = tempfile.mkdtemp()
596 dialog = FileRoleDialog(workspace_path=temp_dir)
597 qtbot.addWidget(dialog)
599 assert dialog.subj_combo.count() == 0
601 def test_workspace_with_single_subject(self, qtbot, temp_workspace):
602 """Test with a single subject."""
603 temp_dir = tempfile.mkdtemp()
604 os.makedirs(os.path.join(temp_dir, "sub-only"))
606 dialog = FileRoleDialog(workspace_path=temp_dir)
607 qtbot.addWidget(dialog)
609 assert dialog.subj_combo.count() == 1
610 assert dialog.subj_combo.itemText(0) == "sub-only"
612 def test_workspace_with_many_subjects(self, qtbot):
613 """Test with many subjects."""
614 temp_dir = tempfile.mkdtemp()
615 for i in range(50):
616 os.makedirs(os.path.join(temp_dir, f"sub-{i:03d}"))
618 dialog = FileRoleDialog(workspace_path=temp_dir)
619 qtbot.addWidget(dialog)
621 assert dialog.subj_combo.count() == 50
624 def test_subject_with_special_characters(self, qtbot, temp_workspace):
625 """Test subject with special characters."""
626 special_names = ["sub-test_01", "sub-patient-A", "sub-123abc"]
628 for name in special_names:
629 os.makedirs(os.path.join(temp_workspace, name))
631 dialog = FileRoleDialog(workspace_path=temp_workspace)
632 qtbot.addWidget(dialog)
634 assert dialog.subj_combo.count() == len(special_names)+2 # For the two already created in the fixture
637class TestIntegration:
638 """Integration tests."""
640 def test_full_workflow_main_anat(self, qtbot, workspace_with_subjects):
641 """Test full workflow: main -> subject -> anat."""
642 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
643 qtbot.addWidget(dialog)
645 dialog.opt_main.setChecked(True)
646 dialog.subj_combo.setCurrentText("sub-02")
647 dialog.anat_button.setChecked(True)
649 assert dialog.ok_button.isEnabled()
651 selections = dialog.get_selections()
652 assert selections['main'] == "main subject files"
653 assert selections['subj'] == "sub-02"
654 assert selections['role'] == "anat"
656 path = dialog.get_relative_path()
657 assert path == os.path.join("sub-02", "anat")
659 def test_full_workflow_derivatives(self, qtbot, workspace_with_subjects):
660 """Test full workflow: derivatives."""
661 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
662 qtbot.addWidget(dialog)
664 dialog.opt_derivatives.setChecked(True)
665 dialog.manual_mask_btn.setChecked(True)
666 dialog.subj_combo.setCurrentText("sub-03")
667 dialog.ses_1_button.setChecked(True)
669 assert dialog.ok_button.isEnabled()
671 selections = dialog.get_selections()
672 assert selections['main'] == "derivatives"
673 assert selections['derivative'] == "manual_masks"
674 assert selections['subj'] == "sub-03"
675 assert selections['role'] == "ses-01"
677 path = dialog.get_relative_path()
678 expected = os.path.join("derivatives", "manual_masks", "sub-03", "ses-01", "pet")
679 assert path == expected
681 def test_change_selections_multiple_times(self, qtbot, workspace_with_subjects):
682 """Test changing selections multiple times."""
683 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
684 qtbot.addWidget(dialog)
686 dialog.opt_main.setChecked(True)
687 dialog.subj_combo.setCurrentText("sub-01")
688 dialog.anat_button.setChecked(True)
690 path1 = dialog.get_relative_path()
692 dialog.opt_derivatives.setChecked(True)
693 dialog.skull_strip_btn.setChecked(True)
694 dialog.subj_combo.setCurrentText("sub-02")
695 dialog.ses_2_button.setChecked(True)
697 path2 = dialog.get_relative_path()
699 assert path1 != path2
700 assert "derivatives" in path2
703class TestPartialInitialization:
704 """Tests for partial initializations."""
706 def test_only_role_missing(self, qtbot, workspace_with_subjects):
707 """Test with only role missing."""
708 dialog = FileRoleDialog(
709 workspace_path=workspace_with_subjects,
710 main="derivatives",
711 subj="sub-01"
712 )
713 qtbot.addWidget(dialog)
715 assert hasattr(dialog, 'level3_widget')
716 assert not hasattr(dialog, 'level1_widget')
717 assert not hasattr(dialog, 'subj_combo')
719 def test_only_subject_missing(self, qtbot, workspace_with_subjects):
720 """Test with only subject missing (main and role provided)."""
721 dialog = FileRoleDialog(
722 workspace_path=workspace_with_subjects,
723 main="derivatives",
724 role="anat"
725 )
726 qtbot.addWidget(dialog)
728 assert hasattr(dialog, 'level2_widget')
729 assert not hasattr(dialog, 'level1_widget')
730 assert not hasattr(dialog, 'level3_widget')
731 assert dialog.button_third_group is None
733 def test_only_main_missing(self, qtbot, workspace_with_subjects):
734 """Test with only main missing (subj and role provided)."""
735 dialog = FileRoleDialog(
736 workspace_path=workspace_with_subjects,
737 subj="sub-01",
738 role="ses-01"
739 )
740 qtbot.addWidget(dialog)
742 assert hasattr(dialog, 'level1_widget')
743 assert not hasattr(dialog, 'subj_combo')
744 assert not hasattr(dialog, 'level3_widget')
745 assert dialog.button_third_group is None
747 class TestFilterSubjects:
748 """Tests for the filter_subjects method (live filter behavior)."""
750 def test_filter_subjects_with_list_widget(self, qtbot, temp_workspace):
751 """Test filter when subj_list (QListWidget) exists."""
752 subjects = ["sub-one", "sub-two", "patient-three"]
753 for s in subjects:
754 os.makedirs(os.path.join(temp_workspace, s))
756 dialog = FileRoleDialog(workspace_path=temp_workspace)
757 qtbot.addWidget(dialog)
759 lw = QListWidget()
760 for s in subjects:
761 item = QListWidgetItem(s)
762 lw.addItem(item)
763 dialog.subj_list = lw
765 dialog.filter_subjects("sub")
766 visible_texts = [dialog.subj_list.item(i).text() for i in range(dialog.subj_list.count())
767 if not dialog.subj_list.item(i).isHidden()]
768 assert set(visible_texts) == {"sub-one", "sub-two"}
770 dialog.filter_subjects("zzz")
771 hidden_count = sum(1 for i in range(dialog.subj_list.count()) if dialog.subj_list.item(i).isHidden())
772 assert hidden_count == dialog.subj_list.count()
774 def test_filter_subjects_no_subj_list(self, qtbot, workspace_with_subjects):
775 """Calling filter_subjects when subj_list does not exist must not raise exceptions."""
776 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
777 qtbot.addWidget(dialog)
779 dialog.filter_subjects("anything") # should not raise exceptions
781 class TestRegressionAndEdgeCases:
782 """Additional regression and edge case tests."""
784 def test_get_relative_path_returns_none_when_no_selection(self, qtbot):
785 """If no selections (empty workspace), get_relative_path should return None."""
786 temp_dir = tempfile.mkdtemp()
787 dialog = FileRoleDialog(workspace_path=temp_dir)
788 qtbot.addWidget(dialog)
790 assert dialog.subj_combo.count() == 0
792 path = dialog.get_relative_path()
793 assert path is None
795 def test_get_relative_path_handles_unset_buttons(self, qtbot, workspace_with_subjects):
796 """Ensure get_relative_path doesn't crash if some buttons aren't set."""
797 dialog = FileRoleDialog(workspace_path=workspace_with_subjects)
798 qtbot.addWidget(dialog)
800 dialog.subj_combo.setCurrentText("sub-01")
802 path = dialog.get_relative_path()
803 assert path is None or isinstance(path, str)
805 def test_find_patient_dirs_ignores_non_sub_dirs(self, qtbot, temp_workspace):
806 """Ensure _find_patient_dirs ignores folders not starting with 'sub-'."""
807 os.makedirs(os.path.join(temp_workspace, "not-a-sub"))
808 os.makedirs(os.path.join(temp_workspace, "sub-valid"))
809 os.makedirs(os.path.join(temp_workspace, "also-not"))
811 dialog = FileRoleDialog(workspace_path=temp_workspace)
812 qtbot.addWidget(dialog)
814 patient_dirs = dialog._find_patient_dirs()
815 basenames = [os.path.basename(p) for p in patient_dirs]
816 assert "sub-valid" in basenames
817 assert "not-a-sub" not in basenames
818 assert "also-not" not in basenames