Coverage for tests/core/test_utils.py: 96%

261 statements  

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

1import subprocess 

2 

3import pytest 

4import os 

5import sys 

6import tempfile 

7import shutil 

8from unittest.mock import Mock, patch, MagicMock 

9from pathlib import Path 

10 

11from main import utils 

12 

13 

14class TestGetBinPath: 

15 """Tests for get_bin_path function""" 

16 

17 @pytest.fixture 

18 def temp_bin_dir(self): 

19 """Create a temporary directory with binaries""" 

20 temp_dir = tempfile.mkdtemp() 

21 bin_dir = os.path.join(temp_dir, "test_tool") 

22 os.makedirs(bin_dir) 

23 

24 # Create a mock executable file 

25 exe_name = "test_tool.exe" if os.name == "nt" else "test_tool" 

26 exe_path = os.path.join(bin_dir, exe_name) 

27 with open(exe_path, "w") as f: 

28 f.write("#!/bin/bash\necho test") 

29 

30 if os.name != "nt": 

31 os.chmod(exe_path, 0o755) 

32 

33 yield temp_dir, bin_dir, exe_path 

34 shutil.rmtree(temp_dir, ignore_errors=True) 

35 

36 def test_get_bin_path_pyinstaller(self, temp_bin_dir): 

37 """Verify search in sys._MEIPASS (PyInstaller)""" 

38 temp_dir, bin_dir, exe_path = temp_bin_dir 

39 

40 # Simulate PyInstaller 

41 with patch.object(sys, '_MEIPASS', temp_dir, create=True): 

42 result = utils.get_bin_path("test_tool") 

43 assert result == exe_path 

44 

45 def test_get_bin_path_local(self, temp_bin_dir, monkeypatch): 

46 """Verify search in local directory""" 

47 temp_dir, bin_dir, exe_path = temp_bin_dir 

48 

49 # Simulate __file__ in the correct directory 

50 monkeypatch.setattr(utils, '__file__', os.path.join(temp_dir, 'utils.py')) 

51 

52 # Remove _MEIPASS if it exists 

53 if hasattr(sys, '_MEIPASS'): 

54 delattr(sys, '_MEIPASS') 

55 

56 result = utils.get_bin_path("test_tool") 

57 assert result == exe_path 

58 

59 def test_get_bin_path_system_path(self, temp_bin_dir): 

60 """Verify search in system PATH""" 

61 _, _, exe_path = temp_bin_dir 

62 

63 with patch('shutil.which', return_value=exe_path): 

64 # Remove _MEIPASS if it exists 

65 if hasattr(sys, '_MEIPASS'): 

66 delattr(sys, '_MEIPASS') 

67 

68 result = utils.get_bin_path("test_tool") 

69 assert result == exe_path 

70 

71 def test_get_bin_path_not_found(self): 

72 """Verify exception when binary is not found""" 

73 # Remove _MEIPASS 

74 if hasattr(sys, '_MEIPASS'): 

75 delattr(sys, '_MEIPASS') 

76 

77 with patch('shutil.which', return_value=None), \ 

78 patch('os.path.exists', return_value=False): 

79 with pytest.raises(FileNotFoundError) as exc_info: 

80 utils.get_bin_path("nonexistent_tool") 

81 

82 assert "Impossibile trovare" in str(exc_info.value) or "Could not find" in str(exc_info.value) 

83 

84 @patch('platform.system', return_value='Windows') 

85 def test_get_bin_path_windows(self, mock_system, temp_bin_dir): 

86 """Verify that it searches for .exe on Windows""" 

87 temp_dir, bin_dir, _ = temp_bin_dir 

88 

89 # Create .exe on Windows 

90 exe_path = os.path.join(bin_dir, "test_tool.exe") 

91 with open(exe_path, "w") as f: 

92 f.write("test") 

93 

94 with patch.object(sys, '_MEIPASS', temp_dir, create=True): 

95 result = utils.get_bin_path("test_tool") 

96 assert result.endswith(".exe") 

97 

98 

99class TestGetAppDir: 

100 """Tests for get_app_dir function""" 

101 

102 def test_get_app_dir_creates_directory(self): 

103 """Verify that it creates the directory if it doesn't exist""" 

104 with patch('utils.QStandardPaths.writableLocation') as mock_location: 

105 temp_home = tempfile.mkdtemp() 

106 mock_location.return_value = temp_home 

107 

108 try: 

109 result = utils.get_app_dir() 

110 

111 assert result.exists() 

112 assert result.is_dir() 

113 assert result.name == "GliAAns-UI" 

114 finally: 

115 shutil.rmtree(temp_home, ignore_errors=True) 

116 

117 def test_get_app_dir_returns_path_object(self): 

118 """Verify that it returns a Path object""" 

119 with patch('utils.QStandardPaths.writableLocation') as mock_location: 

120 temp_home = tempfile.mkdtemp() 

121 mock_location.return_value = temp_home 

122 

123 try: 

124 result = utils.get_app_dir() 

125 assert isinstance(result, Path) 

126 finally: 

127 shutil.rmtree(temp_home, ignore_errors=True) 

128 

129 def test_get_app_dir_idempotent(self): 

130 """Verify that multiple calls return the same path""" 

131 with patch('utils.QStandardPaths.writableLocation') as mock_location: 

132 temp_home = tempfile.mkdtemp() 

133 mock_location.return_value = temp_home 

134 

135 try: 

136 result1 = utils.get_app_dir() 

137 result2 = utils.get_app_dir() 

138 

139 assert result1 == result2 

140 finally: 

141 shutil.rmtree(temp_home, ignore_errors=True) 

142 

143 

144class TestResourcePath: 

145 """Tests for resource_path function""" 

146 

147 def test_resource_path_pyinstaller(self): 

148 """Verify path with PyInstaller""" 

149 with patch.object(sys, '_MEIPASS', '/path/to/meipass', create=True): 

150 result = utils.resource_path("resources/icon.png") 

151 

152 assert result.startswith('/path/to/meipass') 

153 assert result.endswith('resources/icon.png') 

154 

155 def test_resource_path_development(self, monkeypatch): 

156 """Verify path in development mode""" 

157 # Remove _MEIPASS 

158 if hasattr(sys, '_MEIPASS'): 

159 delattr(sys, '_MEIPASS') 

160 

161 test_dir = "/test/directory" 

162 monkeypatch.setattr(utils, '__file__', os.path.join(test_dir, 'utils.py')) 

163 

164 result = utils.resource_path("resources/icon.png") 

165 

166 assert test_dir in result 

167 assert "resources/icon.png" in result 

168 

169 def test_resource_path_absolute(self): 

170 """Verify that it returns an absolute path""" 

171 result = utils.resource_path("test.txt") 

172 

173 assert os.path.isabs(result) 

174 

175 

176class TestGetShellPath: 

177 """Tests for get_shell_path function""" 

178 

179 @patch('platform.system', return_value='Darwin') 

180 @patch('subprocess.run') 

181 def test_get_shell_path_macos(self, mock_run, mock_system): 

182 """Verify PATH retrieval on macOS""" 

183 mock_run.return_value = Mock( 

184 stdout="/usr/local/bin:/usr/bin:/bin\n", 

185 returncode=0 

186 ) 

187 

188 result = utils.get_shell_path() 

189 

190 assert result == "/usr/local/bin:/usr/bin:/bin" 

191 mock_run.assert_called_once() 

192 # Verify that it uses zsh 

193 assert "/bin/zsh" in str(mock_run.call_args) 

194 

195 @patch('platform.system', return_value='Linux') 

196 @patch('subprocess.run') 

197 def test_get_shell_path_linux(self, mock_run, mock_system): 

198 """Verify PATH retrieval on Linux""" 

199 mock_run.return_value = Mock( 

200 stdout="/usr/local/bin:/usr/bin:/bin\n", 

201 returncode=0 

202 ) 

203 

204 with patch.dict(os.environ, {'SHELL': '/bin/bash'}): 

205 result = utils.get_shell_path() 

206 

207 assert result == "/usr/local/bin:/usr/bin:/bin" 

208 mock_run.assert_called_once() 

209 # Verify that it uses the user's shell 

210 assert "/bin/bash" in str(mock_run.call_args) 

211 

212 @patch('platform.system', return_value='Linux') 

213 def test_get_shell_path_linux_default_shell(self, mock_system): 

214 """Verify fallback to bash if SHELL is not defined""" 

215 with patch('subprocess.run') as mock_run, \ 

216 patch.dict(os.environ, {}, clear=True): 

217 mock_run.return_value = Mock(stdout="/usr/bin:/bin\n", returncode=0) 

218 

219 result = utils.get_shell_path() 

220 

221 # Should use /bin/bash as fallback 

222 assert "/bin/bash" in str(mock_run.call_args) 

223 

224 @patch('platform.system', return_value='Windows') 

225 def test_get_shell_path_windows(self, mock_system): 

226 """Verify PATH retrieval on Windows""" 

227 test_path = "C:\\Windows\\System32;C:\\Program Files" 

228 

229 with patch.dict(os.environ, {'PATH': test_path}): 

230 result = utils.get_shell_path() 

231 

232 assert result == test_path 

233 

234 @patch('platform.system', return_value='Darwin') 

235 @patch('subprocess.run', side_effect=Exception("Command failed")) 

236 def test_get_shell_path_error_fallback(self, mock_run, mock_system): 

237 """Verify fallback on error""" 

238 test_path = "/fallback/path" 

239 

240 with patch.dict(os.environ, {'PATH': test_path}): 

241 result = utils.get_shell_path() 

242 

243 # Should return PATH from the environment 

244 assert result == test_path 

245 

246 @patch('platform.system', return_value='Unknown') 

247 def test_get_shell_path_unknown_system(self, mock_system): 

248 """Verify behavior on unknown system""" 

249 test_path = "/generic/path" 

250 

251 with patch.dict(os.environ, {'PATH': test_path}): 

252 result = utils.get_shell_path() 

253 

254 assert result == test_path 

255 

256 

257class TestSetupFSLEnv: 

258 """Tests for setup_fsl_env function""" 

259 

260 @patch('subprocess.run') 

261 def test_setup_fsl_env_success(self, mock_run): 

262 """Verify correct FSL setup""" 

263 mock_run.return_value = Mock( 

264 stdout="/usr/local/fsl\nNIFTI_GZ\n", 

265 returncode=0 

266 ) 

267 

268 fsldir, fsloutputtype = utils.setup_fsl_env() 

269 

270 assert fsldir == "/usr/local/fsl" 

271 assert fsloutputtype == "NIFTI_GZ" 

272 mock_run.assert_called_once() 

273 

274 @patch('subprocess.run') 

275 def test_setup_fsl_env_uses_zsh(self, mock_run): 

276 """Verify it uses zsh and sources fsl.sh""" 

277 mock_run.return_value = Mock( 

278 stdout="/usr/local/fsl\nNIFTI_GZ\n", 

279 returncode=0 

280 ) 

281 

282 utils.setup_fsl_env() 

283 

284 call_args = str(mock_run.call_args) 

285 assert "/bin/zsh" in call_args 

286 assert "fsl.sh" in call_args 

287 

288 @patch('subprocess.run') 

289 def test_setup_fsl_env_insufficient_output(self, mock_run): 

290 """Verify error with insufficient output""" 

291 mock_run.return_value = Mock( 

292 stdout="/usr/local/fsl\n", # Missing FSLOUTPUTTYPE 

293 returncode=0 

294 ) 

295 

296 with pytest.raises(RuntimeError) as exc_info: 

297 utils.setup_fsl_env() 

298 

299 assert "Impossibile leggere" in str(exc_info.value) or "Could not read" in str(exc_info.value) 

300 

301 @patch('subprocess.run') 

302 def test_setup_fsl_env_empty_output(self, mock_run): 

303 """Verify error with empty output""" 

304 mock_run.return_value = Mock( 

305 stdout="", 

306 returncode=0 

307 ) 

308 

309 with pytest.raises(RuntimeError): 

310 utils.setup_fsl_env() 

311 

312 @patch('subprocess.run', side_effect=subprocess.CalledProcessError(1, 'cmd')) 

313 def test_setup_fsl_env_command_fails(self, mock_run): 

314 """Verify handling of command error""" 

315 with pytest.raises(subprocess.CalledProcessError): 

316 utils.setup_fsl_env() 

317 

318 

319class TestPlatformSpecificBehavior: 

320 """Tests for platform-specific behavior""" 

321 

322 @patch('platform.system', return_value='Windows') 

323 def test_windows_executable_extension(self, mock_system): 

324 """Verify that Windows searches for .exe""" 

325 with patch('os.path.exists', return_value=True): 

326 with patch.object(sys, '_MEIPASS', '/test', create=True): 

327 # Should not raise exception 

328 try: 

329 result = utils.get_bin_path("test_tool") 

330 assert ".exe" in result 

331 except FileNotFoundError: 

332 # OK if not found, important that it searches for .exe 

333 pass 

334 

335 @patch('platform.system', return_value='Linux') 

336 def test_linux_no_extension(self, mock_system): 

337 """Verify that Linux does not add extension""" 

338 with patch('os.path.exists', return_value=True): 

339 with patch.object(sys, '_MEIPASS', '/test', create=True): 

340 try: 

341 result = utils.get_bin_path("test_tool") 

342 assert not result.endswith(".exe") 

343 except FileNotFoundError: 

344 pass 

345 

346 

347class TestEdgeCases: 

348 """Tests for edge cases and unusual situations""" 

349 

350 def test_get_bin_path_empty_name(self): 

351 """Verify behavior with empty name""" 

352 with pytest.raises((FileNotFoundError, ValueError)): 

353 utils.get_bin_path("") 

354 

355 def test_resource_path_empty_relative(self): 

356 """Verify behavior with empty relative path""" 

357 result = utils.resource_path("") 

358 assert os.path.isabs(result) 

359 

360 def test_resource_path_absolute_input(self): 

361 """Verify behavior with absolute path as input""" 

362 abs_path = "/absolute/path/to/resource" 

363 result = utils.resource_path(abs_path) 

364 # Should still process it 

365 assert isinstance(result, str) 

366 

367 @patch('subprocess.run') 

368 def test_setup_fsl_env_whitespace_output(self, mock_run): 

369 """Verify handling of whitespace in output""" 

370 mock_run.return_value = Mock( 

371 stdout=" /usr/local/fsl \n NIFTI_GZ \n", 

372 returncode=0 

373 ) 

374 

375 fsldir, fsloutputtype = utils.setup_fsl_env() 

376 

377 # strip should remove whitespace 

378 assert fsldir.strip() == "/usr/local/fsl" 

379 assert fsloutputtype.strip() == "NIFTI_GZ" 

380 

381 def test_get_shell_path_empty_environ(self): 

382 """Verify behavior with empty PATH""" 

383 with patch.dict(os.environ, {'PATH': ''}): 

384 with patch('platform.system', return_value='Windows'): 

385 result = utils.get_shell_path() 

386 assert result == "" 

387 

388 

389class TestIntegrationScenarios: 

390 """Integration tests for realistic scenarios""" 

391 

392 @patch('platform.system', return_value='Darwin') 

393 @patch('subprocess.run') 

394 def test_macos_full_setup(self, mock_run, mock_system): 

395 """Test full scenario on macOS""" 

396 # Setup PATH 

397 mock_run.return_value = Mock( 

398 stdout="/usr/local/bin:/usr/bin\n", 

399 returncode=0 

400 ) 

401 

402 path = utils.get_shell_path() 

403 assert "/usr/local/bin" in path 

404 

405 # Setup FSL 

406 mock_run.return_value = Mock( 

407 stdout="/usr/local/fsl\nNIFTI_GZ\n", 

408 returncode=0 

409 ) 

410 

411 fsldir, fsltype = utils.setup_fsl_env() 

412 assert fsldir == "/usr/local/fsl" 

413 assert fsltype == "NIFTI_GZ" 

414 

415 def test_development_mode_paths(self, monkeypatch): 

416 """Test paths in development mode""" 

417 # Simulate development (no PyInstaller) 

418 if hasattr(sys, '_MEIPASS'): 

419 delattr(sys, '_MEIPASS') 

420 

421 test_dir = "/home/user/project" 

422 monkeypatch.setattr(utils, '__file__', os.path.join(test_dir, 'utils.py')) 

423 

424 # Resource path should use project directory 

425 resource = utils.resource_path("icon.png") 

426 assert test_dir in resource 

427 

428 # App dir should be created 

429 with patch('utils.QStandardPaths.writableLocation') as mock_loc: 

430 temp_home = tempfile.mkdtemp() 

431 mock_loc.return_value = temp_home 

432 

433 try: 

434 app_dir = utils.get_app_dir() 

435 assert app_dir.exists() 

436 finally: 

437 shutil.rmtree(temp_home, ignore_errors=True) 

438 

439 

440class TestErrorHandling: 

441 """Tests for error handling""" 

442 

443 @patch('subprocess.run', side_effect=FileNotFoundError("Shell not found")) 

444 @patch('platform.system', return_value='Linux') 

445 def test_shell_not_found_fallback(self, mock_system, mock_run): 

446 """Verify fallback when shell is not found""" 

447 with patch.dict(os.environ, {'PATH': '/backup/path'}): 

448 result = utils.get_shell_path() 

449 # Should use PATH from environment 

450 assert result == '/backup/path' 

451 

452 @patch('os.makedirs', side_effect=PermissionError("No permission")) 

453 def test_app_dir_permission_error(self, mock_makedirs): 

454 """Verify handling of permission error""" 

455 with patch('utils.QStandardPaths.writableLocation', return_value='/root/restricted'): 

456 with pytest.raises(PermissionError): 

457 utils.get_app_dir() 

458 

459 @patch('subprocess.run') 

460 def test_setup_fsl_malformed_output(self, mock_run): 

461 """Verify handling of malformed output""" 

462 # Output with strange characters 

463 mock_run.return_value = Mock( 

464 stdout="\x00/usr/local/fsl\nNIFTI_GZ\x00\n", 

465 returncode=0 

466 ) 

467 

468 # Should still work (strip handles it) 

469 fsldir, fsltype = utils.setup_fsl_env() 

470 assert "/usr/local/fsl" in fsldir 

471 

472 

473if __name__ == "__main__": 

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