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

1import os 

2import tempfile 

3 

4import pytest 

5from unittest.mock import Mock, patch, MagicMock 

6from PyQt6.QtWidgets import QDialogButtonBox, QRadioButton, QComboBox, QListWidgetItem, QListWidget 

7from PyQt6.QtCore import Qt 

8 

9from main.components.file_role_dialog import FileRoleDialog 

10 

11 

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) 

19 

20 anat_dir = os.path.join(subj_dir, "anat") 

21 os.makedirs(anat_dir, exist_ok=True) 

22 

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) 

26 

27 derivatives_dir = os.path.join(temp_workspace, "derivatives") 

28 os.makedirs(derivatives_dir, exist_ok=True) 

29 

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) 

34 

35 return temp_workspace 

36 

37class TestFileRoleDialogInitialization: 

38 """Tests for the dialog initialization.""" 

39 

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) 

44 

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 

49 

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') 

55 

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) 

63 

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) 

68 

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) 

76 

77 assert dialog.subj == "sub-01" 

78 assert dialog.button_second_group is None 

79 assert not hasattr(dialog, 'subj_combo') 

80 

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) 

88 

89 assert dialog.role == "anat" 

90 assert dialog.button_third_group is None 

91 assert not hasattr(dialog, 'level3_widget') 

92 

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) 

101 

102 assert dialog.main == "derivatives" 

103 assert dialog.subj == "sub-02" 

104 assert dialog.role is None 

105 

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) 

110 

111 assert not dialog.ok_button.isEnabled() 

112 

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) 

117 

118 assert dialog.windowTitle() != "" 

119 

120 

121class TestFindPatientDirs: 

122 """Tests for the _find_patient_dirs method.""" 

123 

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) 

128 

129 patient_dirs = dialog._find_patient_dirs() 

130 

131 assert len(patient_dirs) == 3 

132 

133 for p in patient_dirs: 

134 assert 'sub-' in os.path.basename(p) 

135 

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) 

140 

141 patient_dirs = dialog._find_patient_dirs() 

142 

143 for p in patient_dirs: 

144 assert 'derivatives' not in p.split(os.sep) 

145 

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) 

152 

153 patient_dirs = dialog._find_patient_dirs() 

154 

155 assert len(patient_dirs) == 0 

156 

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) 

162 

163 dialog = FileRoleDialog(workspace_path=temp_workspace) 

164 qtbot.addWidget(dialog) 

165 

166 patient_dirs = dialog._find_patient_dirs() 

167 

168 assert len(patient_dirs) == 3 

169 

170 dir_names = [os.path.basename(p) for p in patient_dirs] 

171 

172 assert "sub-01" in dir_names 

173 assert "sub-nested" in dir_names 

174 

175 

176class TestLevel1MainDerivatives: 

177 """Tests for level 1 (Main/Derivatives).""" 

178 

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) 

183 

184 dialog.opt_main.setChecked(True) 

185 

186 selections = dialog.get_selections() 

187 assert selections['main'] == "main subject files" 

188 

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) 

193 

194 dialog.opt_derivatives.setChecked(True) 

195 

196 selections = dialog.get_selections() 

197 assert selections['main'] == "derivatives" 

198 

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) 

203 

204 assert not dialog.derivative_extra_frame.isVisibleTo(dialog) 

205 

206 dialog.opt_derivatives.setChecked(True) 

207 

208 assert dialog.derivative_extra_frame.isVisibleTo(dialog) 

209 

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) 

214 

215 dialog.opt_derivatives.setChecked(True) 

216 assert dialog.derivative_extra_frame.isVisibleTo(dialog) 

217 

218 dialog.opt_main.setChecked(True) 

219 assert not dialog.derivative_extra_frame.isVisibleTo(dialog) 

220 

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) 

225 

226 dialog.opt_derivatives.setChecked(True) 

227 dialog.skull_strip_btn.setChecked(True) 

228 

229 selections = dialog.get_selections() 

230 assert selections['derivative'] == "skullstrips" 

231 

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) 

236 

237 dialog.opt_derivatives.setChecked(True) 

238 

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 ] 

244 

245 for button, expected_text in derivative_buttons: 

246 button.setChecked(True) 

247 selections = dialog.get_selections() 

248 assert selections['derivative'] == expected_text 

249 

250 

251class TestLevel2Subject: 

252 """Tests for level 2 (Subject).""" 

253 

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) 

258 

259 assert dialog.subj_combo.count() == 3 

260 

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 

265 

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) 

270 

271 dialog.subj_combo.setCurrentText("sub-02") 

272 

273 selections = dialog.get_selections() 

274 assert selections['subj'] == "sub-02" 

275 

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) 

280 

281 selections = dialog.get_selections() 

282 assert selections['subj'] is not None 

283 

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) 

291 

292 assert not hasattr(dialog, 'subj_combo') 

293 assert not hasattr(dialog, 'level2_widget') 

294 

295 

296class TestLevel3Role: 

297 """Tests for level 3 (Role).""" 

298 

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) 

303 

304 dialog.anat_button.setChecked(True) 

305 

306 selections = dialog.get_selections() 

307 assert selections['role'] == "anat" 

308 

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) 

313 

314 dialog.ses_1_button.setChecked(True) 

315 

316 selections = dialog.get_selections() 

317 assert selections['role'] == "ses-01" 

318 

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) 

323 

324 dialog.ses_2_button.setChecked(True) 

325 

326 selections = dialog.get_selections() 

327 assert selections['role'] == "ses-02" 

328 

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) 

336 

337 assert not hasattr(dialog, 'level3_widget') 

338 assert dialog.button_third_group is None 

339 

340 

341class TestGetSelections: 

342 """Tests for the get_selections method.""" 

343 

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) 

348 

349 dialog.opt_main.setChecked(True) 

350 dialog.subj_combo.setCurrentText("sub-01") 

351 dialog.anat_button.setChecked(True) 

352 

353 selections = dialog.get_selections() 

354 

355 assert selections['main'] == "main subject files" 

356 assert selections['subj'] == "sub-01" 

357 assert selections['role'] == "anat" 

358 

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) 

363 

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) 

368 

369 selections = dialog.get_selections() 

370 

371 assert selections['main'] == "derivatives" 

372 assert selections['derivative'] == "skullstrips" 

373 assert selections['subj'] == "sub-02" 

374 assert selections['role'] == "anat" 

375 

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) 

380 

381 dialog.opt_main.setChecked(True) 

382 

383 selections = dialog.get_selections() 

384 

385 assert selections['main'] == "main subject files" 

386 assert selections.get('derivative') is None 

387 

388 

389class TestGetRelativePath: 

390 """Tests for the get_relative_path method.""" 

391 

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) 

396 

397 dialog.opt_main.setChecked(True) 

398 dialog.subj_combo.setCurrentText("sub-01") 

399 dialog.anat_button.setChecked(True) 

400 

401 path = dialog.get_relative_path() 

402 

403 assert path == os.path.join("sub-01", "anat") 

404 

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) 

409 

410 dialog.opt_main.setChecked(True) 

411 dialog.subj_combo.setCurrentText("sub-02") 

412 dialog.ses_1_button.setChecked(True) 

413 

414 path = dialog.get_relative_path() 

415 

416 assert path == os.path.join("sub-02", "ses-01", "pet") 

417 

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) 

422 

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) 

427 

428 path = dialog.get_relative_path() 

429 

430 assert path == os.path.join("derivatives", "skullstrips", "sub-01", "anat") 

431 

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) 

436 

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) 

441 

442 path = dialog.get_relative_path() 

443 

444 assert path == os.path.join("derivatives", "manual_masks", "sub-03", "ses-02", "pet") 

445 

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) 

450 

451 path = dialog.get_relative_path() 

452 

453 # With no selection (except default subject), path might be partial 

454 assert path is not None or path is None # Depends on implementation 

455 

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) 

460 

461 dialog.opt_main.setChecked(True) 

462 dialog.subj_combo.setCurrentText("sub-01") 

463 

464 dialog.ses_1_button.setChecked(True) 

465 path1 = dialog.get_relative_path() 

466 assert path1.endswith("pet") 

467 

468 dialog.ses_2_button.setChecked(True) 

469 path2 = dialog.get_relative_path() 

470 assert path2.endswith("pet") 

471 

472 dialog.anat_button.setChecked(True) 

473 path3 = dialog.get_relative_path() 

474 assert not path3.endswith("pet") 

475 

476 

477class TestUpdateOkButton: 

478 """Tests for the update_ok_button method.""" 

479 

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) 

484 

485 assert not dialog.ok_button.isEnabled() 

486 

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) 

491 

492 dialog.opt_main.setChecked(True) 

493 dialog.subj_combo.setCurrentText("sub-01") 

494 dialog.anat_button.setChecked(True) 

495 

496 assert dialog.ok_button.isEnabled() 

497 

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) 

502 

503 dialog.subj_combo.setCurrentText("sub-01") 

504 dialog.anat_button.setChecked(True) 

505 

506 assert not dialog.ok_button.isEnabled() 

507 

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) 

512 

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 

516 

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) 

521 

522 dialog.opt_main.setChecked(True) 

523 dialog.subj_combo.setCurrentText("sub-01") 

524 

525 assert not dialog.ok_button.isEnabled() 

526 

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) 

531 

532 dialog.opt_derivatives.setChecked(True) 

533 dialog.subj_combo.setCurrentText("sub-01") 

534 dialog.anat_button.setChecked(True) 

535 

536 assert not dialog.ok_button.isEnabled() 

537 

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) 

542 

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) 

547 

548 assert dialog.ok_button.isEnabled() 

549 

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) 

554 

555 assert not dialog.ok_button.isEnabled() 

556 

557 dialog.opt_main.setChecked(True) 

558 assert not dialog.ok_button.isEnabled() 

559 

560 dialog.subj_combo.setCurrentText("sub-01") 

561 assert not dialog.ok_button.isEnabled() 

562 

563 dialog.anat_button.setChecked(True) 

564 assert dialog.ok_button.isEnabled() 

565 

566 

567class TestDialogButtons: 

568 """Tests for the dialog buttons.""" 

569 

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) 

574 

575 assert dialog.ok_button is not None 

576 

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) 

581 

582 buttons = dialog.findChildren(QDialogButtonBox) 

583 assert len(buttons) > 0 

584 

585 button_box = buttons[0] 

586 cancel_btn = button_box.button(QDialogButtonBox.StandardButton.Cancel) 

587 assert cancel_btn is not None 

588 

589 

590class TestEdgeCases: 

591 """Tests for edge cases.""" 

592 

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) 

598 

599 assert dialog.subj_combo.count() == 0 

600 

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")) 

605 

606 dialog = FileRoleDialog(workspace_path=temp_dir) 

607 qtbot.addWidget(dialog) 

608 

609 assert dialog.subj_combo.count() == 1 

610 assert dialog.subj_combo.itemText(0) == "sub-only" 

611 

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}")) 

617 

618 dialog = FileRoleDialog(workspace_path=temp_dir) 

619 qtbot.addWidget(dialog) 

620 

621 assert dialog.subj_combo.count() == 50 

622 

623 

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"] 

627 

628 for name in special_names: 

629 os.makedirs(os.path.join(temp_workspace, name)) 

630 

631 dialog = FileRoleDialog(workspace_path=temp_workspace) 

632 qtbot.addWidget(dialog) 

633 

634 assert dialog.subj_combo.count() == len(special_names)+2 # For the two already created in the fixture 

635 

636 

637class TestIntegration: 

638 """Integration tests.""" 

639 

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) 

644 

645 dialog.opt_main.setChecked(True) 

646 dialog.subj_combo.setCurrentText("sub-02") 

647 dialog.anat_button.setChecked(True) 

648 

649 assert dialog.ok_button.isEnabled() 

650 

651 selections = dialog.get_selections() 

652 assert selections['main'] == "main subject files" 

653 assert selections['subj'] == "sub-02" 

654 assert selections['role'] == "anat" 

655 

656 path = dialog.get_relative_path() 

657 assert path == os.path.join("sub-02", "anat") 

658 

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) 

663 

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) 

668 

669 assert dialog.ok_button.isEnabled() 

670 

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" 

676 

677 path = dialog.get_relative_path() 

678 expected = os.path.join("derivatives", "manual_masks", "sub-03", "ses-01", "pet") 

679 assert path == expected 

680 

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) 

685 

686 dialog.opt_main.setChecked(True) 

687 dialog.subj_combo.setCurrentText("sub-01") 

688 dialog.anat_button.setChecked(True) 

689 

690 path1 = dialog.get_relative_path() 

691 

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) 

696 

697 path2 = dialog.get_relative_path() 

698 

699 assert path1 != path2 

700 assert "derivatives" in path2 

701 

702 

703class TestPartialInitialization: 

704 """Tests for partial initializations.""" 

705 

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) 

714 

715 assert hasattr(dialog, 'level3_widget') 

716 assert not hasattr(dialog, 'level1_widget') 

717 assert not hasattr(dialog, 'subj_combo') 

718 

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) 

727 

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 

732 

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) 

741 

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 

746 

747 class TestFilterSubjects: 

748 """Tests for the filter_subjects method (live filter behavior).""" 

749 

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)) 

755 

756 dialog = FileRoleDialog(workspace_path=temp_workspace) 

757 qtbot.addWidget(dialog) 

758 

759 lw = QListWidget() 

760 for s in subjects: 

761 item = QListWidgetItem(s) 

762 lw.addItem(item) 

763 dialog.subj_list = lw 

764 

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"} 

769 

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() 

773 

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) 

778 

779 dialog.filter_subjects("anything") # should not raise exceptions 

780 

781 class TestRegressionAndEdgeCases: 

782 """Additional regression and edge case tests.""" 

783 

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) 

789 

790 assert dialog.subj_combo.count() == 0 

791 

792 path = dialog.get_relative_path() 

793 assert path is None 

794 

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) 

799 

800 dialog.subj_combo.setCurrentText("sub-01") 

801 

802 path = dialog.get_relative_path() 

803 assert path is None or isinstance(path, str) 

804 

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")) 

810 

811 dialog = FileRoleDialog(workspace_path=temp_workspace) 

812 qtbot.addWidget(dialog) 

813 

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