Coverage for tests/ui/test_skull_stripping_page.py: 99%

248 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-03 15:51 +0000

1import pytest 

2import os 

3import tempfile 

4import shutil 

5from unittest.mock import Mock, patch, MagicMock, call 

6from PyQt6.QtCore import QSettings, pyqtSignal, QObject 

7from PyQt6.QtWidgets import QMessageBox 

8 

9from main.ui.skull_stripping_page import SkullStrippingPage 

10 

11@pytest.fixture 

12def skull_page(qtbot, mock_context, mock_file_selector): 

13 with patch('subprocess.run', return_value=Mock(returncode=0)): 

14 previous_page = Mock() 

15 page = SkullStrippingPage(mock_context, previous_page) 

16 qtbot.addWidget(page) 

17 page.show() 

18 return page 

19 

20class TestSkullStrippingPageSetup: 

21 """Tests for SkullStrippingPage initialization""" 

22 

23 def test_page_initialization(self, skull_page): 

24 """Test correct initialization""" 

25 assert skull_page.context is not None 

26 assert skull_page.previous_page is not None 

27 assert skull_page.worker is None 

28 assert skull_page.canceled == False 

29 

30 def test_title_created(self, skull_page): 

31 """Test title creation""" 

32 assert skull_page.title is not None 

33 assert skull_page.title.text() != "" 

34 

35 def test_file_selector_created(self, skull_page): 

36 """Test file selector creation""" 

37 assert skull_page.file_selector_widget is not None 

38 

39 def test_run_button_created(self, skull_page): 

40 """Test run button creation""" 

41 assert skull_page.run_button is not None 

42 assert not skull_page.run_button.isEnabled() # Disabled initially 

43 

44 def test_cancel_button_created(self, skull_page): 

45 """Test cancel button creation""" 

46 assert skull_page.cancel_button is not None 

47 assert not skull_page.cancel_button.isVisible() # Hidden initially 

48 

49 def test_progress_bar_created(self, skull_page): 

50 """Test progress bar creation""" 

51 assert skull_page.progress_bar is not None 

52 assert not skull_page.progress_bar.isVisible() # Hidden initially 

53 

54 @patch('subprocess.run', return_value=Mock(returncode=0)) 

55 def test_bet_detected_when_available(self, mock_run, qtbot, mock_context, mock_file_selector): 

56 """Test BET detection when available""" 

57 page = SkullStrippingPage(mock_context, Mock()) 

58 qtbot.addWidget(page) 

59 

60 assert page.has_bet == True 

61 

62 @patch('subprocess.run', side_effect=FileNotFoundError()) 

63 def test_bet_not_detected_when_unavailable(self, mock_run, qtbot, mock_context, mock_file_selector): 

64 """Test that BET is not detected when unavailable""" 

65 page = SkullStrippingPage(mock_context, Mock()) 

66 qtbot.addWidget(page) 

67 

68 assert page.has_bet == False 

69 

70 

71class TestSkullStrippingPageBETParameters: 

72 """Tests for BET parameters""" 

73 

74 def test_f_parameter_default(self, skull_page): 

75 """Test default value for f parameter""" 

76 assert skull_page.f_spinbox.value() == 0.50 

77 

78 def test_f_parameter_range(self, skull_page): 

79 """Test range for f parameter""" 

80 assert skull_page.f_spinbox.minimum() == 0.0 

81 assert skull_page.f_spinbox.maximum() == 1.0 

82 

83 def test_g_parameter_default(self, skull_page): 

84 """Test default value for g parameter""" 

85 assert skull_page.g_spinbox.value() == 0.0 

86 

87 def test_coordinate_parameters_default(self, skull_page): 

88 """Test default coordinate values""" 

89 assert skull_page.c_x_spinbox.value() == 0 

90 assert skull_page.c_y_spinbox.value() == 0 

91 assert skull_page.c_z_spinbox.value() == 0 

92 

93 def test_brain_extracted_checkbox_default(self, skull_page): 

94 """Test that brain extracted is checked by default""" 

95 assert skull_page.opt_brain_extracted.isChecked() 

96 

97 def test_other_checkboxes_default(self, skull_page): 

98 """Test that other checkboxes are unchecked by default""" 

99 assert not skull_page.opt_m.isChecked() 

100 assert not skull_page.opt_t.isChecked() 

101 assert not skull_page.opt_s.isChecked() 

102 assert not skull_page.opt_o.isChecked() 

103 

104 

105class TestSkullStrippingPageAdvancedOptions: 

106 """Tests for advanced options""" 

107 

108 def test_advanced_options_hidden_initially(self, skull_page): 

109 """Test that advanced options are hidden initially""" 

110 assert not skull_page.advanced_box.isVisible() 

111 

112 def test_toggle_advanced_shows_options(self, skull_page): 

113 """Test that toggle shows advanced options""" 

114 skull_page.advanced_btn.setChecked(True) 

115 skull_page.toggle_advanced() # call the function directly 

116 assert skull_page.advanced_box.isVisible() 

117 

118 def test_toggle_advanced_hides_options(self, skull_page, qtbot): 

119 """Test that toggle hides advanced options""" 

120 # First show 

121 skull_page.advanced_btn.setChecked(True) 

122 skull_page.toggle_advanced() 

123 assert skull_page.advanced_box.isVisible() 

124 

125 # Then hide 

126 skull_page.advanced_btn.setChecked(False) 

127 skull_page.toggle_advanced() 

128 assert not skull_page.advanced_box.isVisible() 

129 

130 def test_toggle_updates_button_text(self, skull_page): 

131 """Test that toggle updates the button text""" 

132 initial_text = skull_page.advanced_btn.text() 

133 

134 skull_page.advanced_btn.setChecked(True) 

135 skull_page.toggle_advanced() 

136 

137 assert skull_page.advanced_btn.text() != initial_text 

138 

139 

140class TestSkullStrippingPageProcessing: 

141 """Tests for processing""" 

142 

143 def test_run_without_files_shows_warning(self, skull_page, monkeypatch): 

144 """Test warning when no files are present""" 

145 skull_page.file_selector_widget.get_selected_files = Mock(return_value=[]) 

146 

147 warning_shown = False 

148 

149 def mock_warning(*args, **kwargs): 

150 nonlocal warning_shown 

151 warning_shown = True 

152 

153 monkeypatch.setattr(QMessageBox, 'warning', mock_warning) 

154 

155 skull_page.run_bet() 

156 assert warning_shown 

157 

158 @patch('main.ui.skull_stripping_page.SkullStripThread') 

159 def test_run_creates_worker_thread(self, MockThread, skull_page): 

160 """Test worker thread creation""" 

161 skull_page.file_selector_widget.get_selected_files = Mock( 

162 return_value=['/path/to/file.nii'] 

163 ) 

164 

165 mock_worker = Mock() 

166 MockThread.return_value = mock_worker 

167 

168 skull_page.run_bet() 

169 

170 MockThread.assert_called_once() 

171 mock_worker.start.assert_called_once() 

172 

173 @patch('main.ui.skull_stripping_page.SkullStripThread') 

174 def test_run_shows_progress_bar(self, MockThread, skull_page, qtbot): 

175 """Test that run shows the progress bar""" 

176 skull_page.file_selector_widget.get_selected_files = Mock( 

177 return_value=['/path/to/file.nii'] 

178 ) 

179 

180 mock_worker = Mock() 

181 MockThread.return_value = mock_worker 

182 

183 with qtbot.waitSignal(skull_page.processing, timeout=1000): 

184 skull_page.run_bet() 

185 

186 assert skull_page.progress_bar.isVisible() 

187 

188 def test_set_processing_mode_true(self, skull_page): 

189 """Test set_processing_mode(True)""" 

190 skull_page.set_processing_mode(True) 

191 

192 assert not skull_page.run_button.isVisible() 

193 assert skull_page.cancel_button.isVisible() 

194 

195 def test_cancel_processing(self, skull_page): 

196 """Test processing cancellation""" 

197 mock_worker = Mock() 

198 mock_worker.isRunning.return_value = True 

199 skull_page.worker = mock_worker 

200 

201 skull_page.cancel_processing() 

202 

203 assert skull_page.canceled == True 

204 mock_worker.cancel.assert_called_once() 

205 

206 

207class TestSkullStrippingPageProgressCallbacks: 

208 """Tests for progress callbacks""" 

209 

210 def test_on_progress_updated(self, skull_page): 

211 """Test progress message update""" 

212 test_message = "Processing file 1 of 3" 

213 skull_page.on_progress_updated(test_message) 

214 

215 assert skull_page.status_label.text() == test_message 

216 

217 def test_on_progress_value_updated(self, skull_page): 

218 """Test progress bar value update""" 

219 skull_page.on_progress_value_updated(50) 

220 

221 assert skull_page.progress_bar.value() == 50 

222 

223 def test_on_all_completed_success(self, skull_page): 

224 """Test completion callback on success""" 

225 skull_page.on_all_completed(3, []) 

226 

227 assert not skull_page.progress_bar.isVisible() 

228 assert "3" in skull_page.status_label.text() 

229 

230 def test_on_all_completed_with_failures(self, skull_page): 

231 """Test completion callback with failures""" 

232 failed_files = ['/path/file1.nii', '/path/file2.nii'] 

233 skull_page.on_all_completed(1, failed_files) 

234 

235 assert not skull_page.progress_bar.isVisible() 

236 status_text = skull_page.status_label.text() 

237 assert "1" in status_text # Successes 

238 

239 def test_on_all_completed_all_failed(self, skull_page): 

240 """Test callback when all fail""" 

241 failed_files = ['/path/file1.nii', '/path/file2.nii'] 

242 skull_page.on_all_completed(0, failed_files) 

243 

244 assert not skull_page.progress_bar.isVisible() 

245 assert "failed" in skull_page.status_label.text().lower() 

246 

247 def test_on_worker_finished(self, skull_page, qtbot): 

248 """Test worker finished callback""" 

249 mock_worker = Mock() 

250 skull_page.worker = mock_worker 

251 

252 with qtbot.waitSignal(skull_page.processing, timeout=1000): 

253 skull_page.on_worker_finished() 

254 

255 skull_page.context["update_main_buttons"].assert_called() 

256 

257 

258class TestSkullStrippingPageExistingCheck: 

259 """Tests for existing skull strip check""" 

260 @pytest.fixture 

261 def temp_workspace(self): 

262 temp_dir = tempfile.mkdtemp() 

263 # Create structure with existing skull strip 

264 subject_dir = os.path.join(temp_dir, "derivatives", "skullstrips", "sub-01", "anat") 

265 os.makedirs(subject_dir) 

266 with open(os.path.join(subject_dir, "brain.nii.gz"), "w") as f: 

267 f.write("test") 

268 yield temp_dir 

269 shutil.rmtree(temp_dir, ignore_errors=True) 

270 

271 def test_has_existing_skull_strip_true(self, skull_page, temp_workspace): 

272 """Test detection of existing skull strip""" 

273 nifti_path = os.path.join(temp_workspace, "sub-01", "anat", "T1w.nii") 

274 

275 result = skull_page.has_existing_skull_strip(nifti_path, temp_workspace) 

276 

277 assert result == True 

278 

279 def test_has_existing_skull_strip_false(self, skull_page, temp_workspace): 

280 """Test when skull strip does not exist""" 

281 nifti_path = os.path.join(temp_workspace, "sub-02", "anat", "T1w.nii") 

282 

283 result = skull_page.has_existing_skull_strip(nifti_path, temp_workspace) 

284 

285 assert result == False 

286 

287 def test_has_existing_skull_strip_no_subject_id(self, skull_page, temp_workspace): 

288 """Test behavior without subject ID""" 

289 nifti_path = os.path.join(temp_workspace, "invalid", "T1w.nii") 

290 

291 result = skull_page.has_existing_skull_strip(nifti_path, temp_workspace) 

292 

293 assert result == False 

294 

295 

296class TestSkullStrippingPageNavigation: 

297 """Tests for navigation""" 

298 

299 def test_back_returns_previous_page(self, skull_page): 

300 """Test return to previous page""" 

301 result = skull_page.back() 

302 assert result == skull_page.previous_page 

303 skull_page.previous_page.on_enter.assert_called_once() 

304 

305 def test_back_blocked_during_processing(self, skull_page, monkeypatch): 

306 """Test that back is blocked during processing""" 

307 mock_worker = Mock() 

308 mock_worker.isRunning.return_value = True 

309 skull_page.worker = mock_worker 

310 

311 warning_shown = False 

312 

313 def mock_warning(*args, **kwargs): 

314 nonlocal warning_shown 

315 warning_shown = True 

316 

317 monkeypatch.setattr(QMessageBox, 'warning', mock_warning) 

318 

319 result = skull_page.back() 

320 

321 assert result is None 

322 assert warning_shown 

323 

324 def test_is_ready_to_advance_false(self, skull_page): 

325 """Test that advancing is not ready""" 

326 assert not skull_page.is_ready_to_advance() 

327 

328 def test_is_ready_to_go_back_true_when_idle(self, skull_page): 

329 """Test that going back is ready when idle""" 

330 assert skull_page.is_ready_to_go_back() 

331 

332 def test_is_ready_to_go_back_false_during_processing(self, skull_page): 

333 """Test that going back is not ready during processing""" 

334 mock_worker = Mock() 

335 mock_worker.isRunning.return_value = True 

336 skull_page.worker = mock_worker 

337 

338 assert not skull_page.is_ready_to_go_back() 

339 

340 

341class TestSkullStrippingPageReset: 

342 """Tests for page reset""" 

343 

344 def test_reset_clears_files(self, skull_page): 

345 """Test that reset clears files""" 

346 skull_page.reset_page() 

347 skull_page.file_selector_widget.clear_selected_files.assert_called_once() 

348 

349 def test_reset_parameters(self, skull_page): 

350 """Test that reset restores parameters""" 

351 # Modify parameters 

352 skull_page.f_spinbox.setValue(0.7) 

353 skull_page.g_spinbox.setValue(0.5) 

354 

355 # Reset 

356 skull_page.reset_page() 

357 

358 # Verify default values 

359 assert skull_page.f_spinbox.value() == 0.50 

360 assert skull_page.g_spinbox.value() == 0.0 

361 

362 def test_reset_checkboxes(self, skull_page): 

363 """Test that reset restores checkboxes""" 

364 # Modify checkboxes 

365 skull_page.opt_m.setChecked(True) 

366 skull_page.opt_t.setChecked(True) 

367 

368 # Reset 

369 skull_page.reset_page() 

370 

371 # Verify default values 

372 assert skull_page.opt_brain_extracted.isChecked() 

373 assert not skull_page.opt_m.isChecked() 

374 assert not skull_page.opt_t.isChecked() 

375 

376 def test_reset_hides_progress_bar(self, skull_page): 

377 """Test that reset hides progress bar""" 

378 skull_page.progress_bar.setVisible(True) 

379 

380 skull_page.reset_page() 

381 

382 assert not skull_page.progress_bar.isVisible() 

383 

384 def test_reset_cancels_running_worker(self, skull_page): 

385 """Test that reset cancels a running worker""" 

386 mock_worker = Mock() 

387 mock_worker.isRunning.return_value = True 

388 skull_page.worker = mock_worker 

389 

390 skull_page.reset_page() 

391 

392 mock_worker.cancel.assert_called_once() 

393 mock_worker.wait.assert_called_once() 

394 

395 

396class TestSkullStrippingPageTranslation: 

397 """Tests for translations""" 

398 

399 def test_translate_ui_updates_title(self, skull_page): 

400 """Test title update""" 

401 skull_page._translate_ui() 

402 assert skull_page.title.text() != "" 

403 

404 def test_translate_ui_updates_buttons(self, skull_page): 

405 """Test buttons update""" 

406 skull_page._translate_ui() 

407 assert skull_page.run_button.text() != "" 

408 assert skull_page.cancel_button.text() != "" 

409 

410 

411# Integration tests 

412class TestSkullStrippingPageIntegration: 

413 """Integration tests""" 

414 

415 @patch('main.ui.skull_stripping_page.SkullStripThread') 

416 def test_full_processing_workflow(self, MockThread, skull_page, qtbot): 

417 """Test full processing workflow""" 

418 # Setup 

419 skull_page.file_selector_widget.get_selected_files = Mock( 

420 return_value=['/path/file.nii'] 

421 ) 

422 

423 mock_worker = Mock() 

424 MockThread.return_value = mock_worker 

425 

426 # Start processing 

427 with qtbot.waitSignal(skull_page.processing, timeout=1000): 

428 skull_page.run_bet() 

429 

430 # Verify processing state 

431 assert skull_page.progress_bar.isVisible() 

432 assert skull_page.cancel_button.isVisible() 

433 assert not skull_page.run_button.isVisible() 

434 

435 # Simulate completion 

436 with qtbot.waitSignal(skull_page.processing, timeout=1000): 

437 skull_page.on_worker_finished() 

438 

439 # Verify final state 

440 assert not skull_page.cancel_button.isVisible() 

441 

442 

443if __name__ == "__main__": 

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