Coverage for tests/core/test_controller.py: 99%

279 statements  

« 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 

7from PyQt6.QtWidgets import QApplication, QPushButton 

8 

9from main.controller import Controller 

10 

11 

12class TestControllerInitialization: 

13 """Tests for Controller Initialization""" 

14 

15 @pytest.fixture 

16 def mock_components(self): 

17 """Mock UI components""" 

18 with patch('main.controller.ImportPage') as MockImportPage, \ 

19 patch('main.controller.MainWindow') as MockMainWindow, \ 

20 patch('main.controller.WorkspaceTreeView') as MockTreeView, \ 

21 patch('main.controller.NiftiViewer') as MockNiftiViewer, \ 

22 patch('main.controller.get_app_dir') as mock_get_app_dir: 

23 temp_dir = tempfile.mkdtemp() 

24 mock_get_app_dir.return_value = MockPath(temp_dir) 

25 

26 MockImportPage.return_value = Mock() 

27 MockMainWindow.return_value = Mock() 

28 MockTreeView.return_value = Mock() 

29 MockNiftiViewer.return_value = Mock() 

30 

31 yield { 

32 'import_page': MockImportPage, 

33 'main_window': MockMainWindow, 

34 'tree_view': MockTreeView, 

35 'nifti_viewer': MockNiftiViewer, 

36 'temp_dir': temp_dir 

37 } 

38 

39 shutil.rmtree(temp_dir, ignore_errors=True) 

40 

41 def test_controller_initialization(self, qtbot, mock_components): 

42 """Verify correct controller initialization""" 

43 controller = Controller() 

44 

45 assert controller.context is not None 

46 assert controller.workspace_path is not None 

47 assert controller.current_page is not None 

48 

49 def test_context_contains_required_keys(self, qtbot, mock_components): 

50 """Verify that the context contains all necessary keys""" 

51 controller = Controller() 

52 

53 required_keys = [ 

54 "workspace_path", 

55 "update_main_buttons", 

56 "return_to_import", 

57 "history", 

58 "language_changed", 

59 "create_buttons", 

60 "selected_files_signal", 

61 "open_nifti_viewer", 

62 "settings" 

63 ] 

64 

65 for key in required_keys: 

66 assert key in controller.context 

67 

68 def test_components_created(self, qtbot, mock_components): 

69 """Verify creation of all components""" 

70 controller = Controller() 

71 

72 mock_components['import_page'].assert_called_once() 

73 mock_components['main_window'].assert_called_once() 

74 mock_components['tree_view'].assert_called_once() 

75 mock_components['nifti_viewer'].assert_called_once() 

76 

77 def test_workspace_directory_created(self, qtbot, mock_components): 

78 """Verify workspace directory creation""" 

79 controller = Controller() 

80 

81 workspace_path = controller.workspace_path 

82 # Verify that the path exists 

83 assert workspace_path is not None 

84 

85 def test_start_page_set(self, qtbot, mock_components): 

86 """Verify that the start page is ImportPage""" 

87 controller = Controller() 

88 

89 assert controller.start_page == controller.context["import_page"] 

90 assert controller.current_page == controller.start_page 

91 

92 def test_history_initialized(self, qtbot, mock_components): 

93 """Verify history initialization""" 

94 controller = Controller() 

95 

96 assert len(controller.context["history"]) > 0 

97 assert controller.start_page in controller.context["history"] 

98 

99 

100class TestControllerNavigation: 

101 """Tests for page navigation""" 

102 

103 @pytest.fixture 

104 def controller(self, qtbot): 

105 """Create controller for testing""" 

106 with patch('main.controller.ImportPage'), \ 

107 patch('main.controller.MainWindow'), \ 

108 patch('main.controller.WorkspaceTreeView'), \ 

109 patch('main.controller.NiftiViewer'), \ 

110 patch('main.controller.get_app_dir') as mock_app_dir: 

111 temp_dir = tempfile.mkdtemp() 

112 mock_app_dir.return_value = MockPath(temp_dir) 

113 

114 controller = Controller() 

115 

116 yield controller 

117 

118 shutil.rmtree(temp_dir, ignore_errors=True) 

119 

120 def test_go_to_next_page(self, controller): 

121 """Verify navigation to the next page""" 

122 mock_next_page = Mock() 

123 controller.current_page.next.return_value = mock_next_page 

124 

125 result = controller.go_to_next_page() 

126 

127 assert controller.current_page == mock_next_page 

128 assert result == mock_next_page 

129 

130 def test_go_to_next_page_none(self, controller): 

131 """Verify behavior when next returns None""" 

132 controller.current_page.next.return_value = None 

133 initial_page = controller.current_page 

134 

135 result = controller.go_to_next_page() 

136 

137 assert controller.current_page == initial_page 

138 assert result == initial_page 

139 

140 def test_go_to_previous_page(self, controller): 

141 """Verify navigation to the previous page""" 

142 mock_previous_page = Mock() 

143 controller.current_page.back.return_value = mock_previous_page 

144 

145 result = controller.go_to_previous_page() 

146 

147 assert controller.current_page == mock_previous_page 

148 assert result == mock_previous_page 

149 

150 def test_go_to_previous_page_none(self, controller): 

151 """Verify behavior when back returns None""" 

152 controller.current_page.back.return_value = None 

153 initial_page = controller.current_page 

154 

155 result = controller.go_to_previous_page() 

156 

157 assert controller.current_page == initial_page 

158 assert result == initial_page 

159 

160 def test_return_to_import(self, controller): 

161 """Verify return to import page""" 

162 # Simulate navigation 

163 mock_page1 = Mock() 

164 mock_page2 = Mock() 

165 controller.context["history"] = [controller.start_page, mock_page1, mock_page2] 

166 controller.current_page = mock_page2 

167 

168 result = controller.return_to_import() 

169 

170 assert controller.current_page == controller.start_page 

171 assert result == controller.start_page 

172 # Verify that reset_page is called on all pages 

173 mock_page1.reset_page.assert_called_once() 

174 mock_page2.reset_page.assert_called_once() 

175 

176 

177class TestControllerButtons: 

178 """Tests for button management""" 

179 

180 @pytest.fixture 

181 def controller(self, qtbot): 

182 with patch('main.controller.ImportPage'), \ 

183 patch('main.controller.MainWindow'), \ 

184 patch('main.controller.WorkspaceTreeView'), \ 

185 patch('main.controller.NiftiViewer'), \ 

186 patch('main.controller.get_app_dir') as mock_app_dir: 

187 temp_dir = tempfile.mkdtemp() 

188 mock_app_dir.return_value = MockPath(temp_dir) 

189 

190 controller = Controller() 

191 

192 yield controller 

193 

194 shutil.rmtree(temp_dir, ignore_errors=True) 

195 

196 def test_create_buttons(self, controller): 

197 """Verify button creation""" 

198 next_btn, back_btn = controller.create_buttons() 

199 

200 assert isinstance(next_btn, QPushButton) 

201 assert isinstance(back_btn, QPushButton) 

202 assert controller.next_button == next_btn 

203 assert controller.back_button == back_btn 

204 

205 def test_buttons_connected(self, controller): 

206 """Verify that the buttons are connected""" 

207 controller.create_buttons() 

208 

209 # Buttons should have connected signals 

210 # (difficult to test connections directly) 

211 assert controller.next_button is not None 

212 assert controller.back_button is not None 

213 

214 def test_update_buttons_state_ready(self, controller): 

215 """Verify button state update when ready""" 

216 controller.create_buttons() 

217 controller.current_page.is_ready_to_advance.return_value = True 

218 controller.current_page.is_ready_to_go_back.return_value = True 

219 

220 controller.update_buttons_state() 

221 

222 assert controller.next_button.isEnabled() 

223 assert controller.back_button.isEnabled() 

224 

225 def test_update_buttons_state_not_ready(self, controller): 

226 """Verify button state update when not ready""" 

227 controller.create_buttons() 

228 controller.current_page.is_ready_to_advance.return_value = False 

229 controller.current_page.is_ready_to_go_back.return_value = False 

230 

231 controller.update_buttons_state() 

232 

233 assert not controller.next_button.isEnabled() 

234 assert not controller.back_button.isEnabled() 

235 

236 def test_button_click_next(self, controller, qtbot): 

237 """Verify click on next button""" 

238 controller.create_buttons() 

239 mock_next_page = Mock() 

240 controller.current_page.next.return_value = mock_next_page 

241 

242 with qtbot.waitSignal(controller.next_button.clicked, timeout=1000): 

243 controller.next_button.click() 

244 

245 assert controller.current_page == mock_next_page 

246 

247 def test_button_click_back(self, controller, qtbot): 

248 """Verify click on back button""" 

249 controller.create_buttons() 

250 mock_prev_page = Mock() 

251 controller.current_page.back.return_value = mock_prev_page 

252 

253 with qtbot.waitSignal(controller.back_button.clicked, timeout=1000): 

254 controller.back_button.click() 

255 

256 assert controller.current_page == mock_prev_page 

257 

258 

259class TestControllerLanguage: 

260 """Tests for language management""" 

261 

262 @pytest.fixture 

263 def controller(self, qtbot): 

264 with patch('main.controller.ImportPage'), \ 

265 patch('main.controller.MainWindow'), \ 

266 patch('main.controller.WorkspaceTreeView'), \ 

267 patch('main.controller.NiftiViewer'), \ 

268 patch('main.controller.get_app_dir') as mock_app_dir: 

269 temp_dir = tempfile.mkdtemp() 

270 mock_app_dir.return_value = MockPath(temp_dir) 

271 

272 controller = Controller() 

273 

274 yield controller 

275 

276 shutil.rmtree(temp_dir, ignore_errors=True) 

277 

278 def test_set_language(self, controller): 

279 """Verify language setting""" 

280 with patch.object(controller.translator, 'load', return_value=True): 

281 controller.set_language("it") 

282 

283 # Verify that it is saved 

284 assert controller.settings.value("language") == "it" 

285 

286 def test_save_language(self, controller): 

287 """Verify saving language""" 

288 controller.save_language("it") 

289 

290 saved_lang = controller.settings.value("language") 

291 assert saved_lang == "it" 

292 

293 def test_language_changed_signal(self, controller, qtbot): 

294 """Verify language change signal emission""" 

295 with patch.object(controller.translator, 'load', return_value=True): 

296 with qtbot.waitSignal(controller.language_changed, timeout=1000): 

297 controller.language_changed.emit("it") 

298 

299 def test_translator_load_called(self, controller): 

300 """Verify that translator.load is called""" 

301 with patch.object(controller.translator, 'load', return_value=True) as mock_load: 

302 controller.set_language("en") 

303 

304 mock_load.assert_called_once() 

305 # Verify that the path contains the language code 

306 call_args = str(mock_load.call_args) 

307 assert "en.qm" in call_args 

308 

309 

310class TestControllerNiftiViewer: 

311 """Tests for opening NIfTI viewer""" 

312 

313 @pytest.fixture 

314 def controller(self, qtbot): 

315 with patch('main.controller.ImportPage'), \ 

316 patch('main.controller.MainWindow'), \ 

317 patch('main.controller.WorkspaceTreeView'), \ 

318 patch('main.controller.NiftiViewer') as MockViewer, \ 

319 patch('main.controller.get_app_dir') as mock_app_dir: 

320 temp_dir = tempfile.mkdtemp() 

321 mock_app_dir.return_value = MockPath(temp_dir) 

322 

323 mock_viewer = Mock() 

324 MockViewer.return_value = mock_viewer 

325 

326 controller = Controller() 

327 

328 yield controller 

329 

330 shutil.rmtree(temp_dir, ignore_errors=True) 

331 

332 def test_open_nifti_viewer(self, controller): 

333 """Verify NIfTI viewer opening""" 

334 test_path = "/path/to/scan.nii" 

335 

336 controller.open_nifti_viewer(test_path) 

337 

338 controller.context["nifti_viewer"].open_file.assert_called_once_with(test_path) 

339 controller.context["nifti_viewer"].show.assert_called_once() 

340 

341 

342class TestControllerStart: 

343 """Tests for application start""" 

344 

345 @pytest.fixture 

346 def controller(self, qtbot): 

347 with patch('main.controller.ImportPage'), \ 

348 patch('main.controller.MainWindow') as MockMainWindow, \ 

349 patch('main.controller.WorkspaceTreeView'), \ 

350 patch('main.controller.NiftiViewer'), \ 

351 patch('main.controller.get_app_dir') as mock_app_dir: 

352 temp_dir = tempfile.mkdtemp() 

353 mock_app_dir.return_value = MockPath(temp_dir) 

354 

355 mock_window = Mock() 

356 MockMainWindow.return_value = mock_window 

357 

358 controller = Controller() 

359 

360 yield controller 

361 

362 shutil.rmtree(temp_dir, ignore_errors=True) 

363 

364 def test_start_shows_main_window(self, controller): 

365 """Verify that start() shows the main window""" 

366 controller.start() 

367 

368 controller.main_window.show.assert_called_once() 

369 

370 

371class TestControllerSettings: 

372 """Tests for settings management""" 

373 

374 @pytest.fixture 

375 def clean_settings(self): 

376 """Cleans settings before and after test""" 

377 settings = QSettings("GliAAns") 

378 settings.clear() 

379 yield settings 

380 settings.clear() 

381 

382 def test_loads_saved_language(self, qtbot, clean_settings): 

383 """Verify loading of saved language""" 

384 clean_settings.setValue("language", "it") 

385 

386 with patch('main.controller.ImportPage'), \ 

387 patch('main.controller.MainWindow'), \ 

388 patch('main.controller.WorkspaceTreeView'), \ 

389 patch('main.controller.NiftiViewer'), \ 

390 patch('main.controller.get_app_dir') as mock_app_dir: 

391 temp_dir = tempfile.mkdtemp() 

392 mock_app_dir.return_value = MockPath(temp_dir) 

393 

394 controller = Controller() 

395 

396 assert controller.saved_lang == "it" 

397 

398 shutil.rmtree(temp_dir, ignore_errors=True) 

399 

400 def test_default_language_en(self, qtbot, clean_settings): 

401 """Verify default language is English""" 

402 with patch('main.controller.ImportPage'), \ 

403 patch('main.controller.MainWindow'), \ 

404 patch('main.controller.WorkspaceTreeView'), \ 

405 patch('main.controller.NiftiViewer'), \ 

406 patch('main.controller.get_app_dir') as mock_app_dir: 

407 temp_dir = tempfile.mkdtemp() 

408 mock_app_dir.return_value = MockPath(temp_dir) 

409 

410 controller = Controller() 

411 

412 assert controller.saved_lang == "en" 

413 

414 shutil.rmtree(temp_dir, ignore_errors=True) 

415 

416 @patch('main.controller.set_log_level') 

417 def test_debug_log_setting(self, mock_set_log_level, qtbot, clean_settings): 

418 """Verify debug log setting""" 

419 import logging 

420 clean_settings.setValue("debug_log", True) 

421 

422 with patch('main.controller.ImportPage'), \ 

423 patch('main.controller.MainWindow'), \ 

424 patch('main.controller.WorkspaceTreeView'), \ 

425 patch('main.controller.NiftiViewer'), \ 

426 patch('main.controller.get_app_dir') as mock_app_dir: 

427 temp_dir = tempfile.mkdtemp() 

428 mock_app_dir.return_value = MockPath(temp_dir) 

429 

430 controller = Controller() 

431 

432 mock_set_log_level.assert_called_with(logging.DEBUG) 

433 

434 shutil.rmtree(temp_dir, ignore_errors=True) 

435 

436 

437class TestControllerIntegration: 

438 """Integration tests for complete flows""" 

439 

440 @pytest.fixture 

441 def controller(self, qtbot): 

442 with patch('main.controller.ImportPage') as MockImport, \ 

443 patch('main.controller.MainWindow'), \ 

444 patch('main.controller.WorkspaceTreeView'), \ 

445 patch('main.controller.NiftiViewer'), \ 

446 patch('main.controller.get_app_dir') as mock_app_dir: 

447 temp_dir = tempfile.mkdtemp() 

448 mock_app_dir.return_value = MockPath(temp_dir) 

449 

450 # Setup mock pages with realistic behaviors 

451 mock_import = Mock() 

452 mock_import.is_ready_to_advance.return_value = True 

453 mock_import.is_ready_to_go_back.return_value = False 

454 MockImport.return_value = mock_import 

455 

456 controller = Controller() 

457 

458 yield controller 

459 

460 shutil.rmtree(temp_dir, ignore_errors=True) 

461 

462 def test_full_navigation_flow(self, controller): 

463 """Test full navigation flow""" 

464 # Setup mock pages 

465 page2 = Mock() 

466 page2.is_ready_to_advance.return_value = True 

467 page2.is_ready_to_go_back.return_value = True 

468 page2.back.return_value = controller.start_page 

469 

470 controller.current_page.next.return_value = page2 

471 

472 # Navigate forward 

473 controller.go_to_next_page() 

474 assert controller.current_page == page2 

475 

476 # Navigate back 

477 controller.go_to_previous_page() 

478 assert controller.current_page == controller.start_page 

479 

480 def test_navigation_with_button_updates(self, controller): 

481 """Test navigation with button updates""" 

482 controller.create_buttons() 

483 

484 page2 = Mock() 

485 page2.is_ready_to_advance.return_value = False 

486 page2.is_ready_to_go_back.return_value = True 

487 

488 controller.current_page.next.return_value = page2 

489 

490 # Navigate 

491 controller.go_to_next_page() 

492 

493 # Buttons should be updated 

494 assert not controller.next_button.isEnabled() 

495 assert controller.back_button.isEnabled() 

496 

497 def test_return_to_import_resets_all(self, controller): 

498 """Test that return_to_import resets everything""" 

499 # Simulate deep navigation 

500 page2 = Mock() 

501 page3 = Mock() 

502 controller.context["history"] = [controller.start_page, page2, page3] 

503 controller.current_page = page3 

504 

505 # Return to import 

506 controller.return_to_import() 

507 

508 # Verify reset 

509 page2.reset_page.assert_called_once() 

510 page3.reset_page.assert_called_once() 

511 assert controller.current_page == controller.start_page 

512 

513 

514# Helper class for Path mock 

515class MockPath: 

516 """Mock of pathlib.Path for testing""" 

517 

518 def __init__(self, path_str): 

519 self.path_str = path_str 

520 

521 def __str__(self): 

522 return self.path_str 

523 

524 def __truediv__(self, other): 

525 return MockPath(os.path.join(self.path_str, other)) 

526 

527 def __repr__(self): 

528 return f"MockPath('{self.path_str}')" 

529 

530 

531if __name__ == "__main__": 

532 pytest.main([__file__, "-v", "--tb=short"])