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
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 15:51 +0000
1import subprocess
3import pytest
4import os
5import sys
6import tempfile
7import shutil
8from unittest.mock import Mock, patch, MagicMock
9from pathlib import Path
11from main import utils
14class TestGetBinPath:
15 """Tests for get_bin_path function"""
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)
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")
30 if os.name != "nt":
31 os.chmod(exe_path, 0o755)
33 yield temp_dir, bin_dir, exe_path
34 shutil.rmtree(temp_dir, ignore_errors=True)
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
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
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
49 # Simulate __file__ in the correct directory
50 monkeypatch.setattr(utils, '__file__', os.path.join(temp_dir, 'utils.py'))
52 # Remove _MEIPASS if it exists
53 if hasattr(sys, '_MEIPASS'):
54 delattr(sys, '_MEIPASS')
56 result = utils.get_bin_path("test_tool")
57 assert result == exe_path
59 def test_get_bin_path_system_path(self, temp_bin_dir):
60 """Verify search in system PATH"""
61 _, _, exe_path = temp_bin_dir
63 with patch('shutil.which', return_value=exe_path):
64 # Remove _MEIPASS if it exists
65 if hasattr(sys, '_MEIPASS'):
66 delattr(sys, '_MEIPASS')
68 result = utils.get_bin_path("test_tool")
69 assert result == exe_path
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')
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")
82 assert "Impossibile trovare" in str(exc_info.value) or "Could not find" in str(exc_info.value)
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
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")
94 with patch.object(sys, '_MEIPASS', temp_dir, create=True):
95 result = utils.get_bin_path("test_tool")
96 assert result.endswith(".exe")
99class TestGetAppDir:
100 """Tests for get_app_dir function"""
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
108 try:
109 result = utils.get_app_dir()
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)
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
123 try:
124 result = utils.get_app_dir()
125 assert isinstance(result, Path)
126 finally:
127 shutil.rmtree(temp_home, ignore_errors=True)
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
135 try:
136 result1 = utils.get_app_dir()
137 result2 = utils.get_app_dir()
139 assert result1 == result2
140 finally:
141 shutil.rmtree(temp_home, ignore_errors=True)
144class TestResourcePath:
145 """Tests for resource_path function"""
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")
152 assert result.startswith('/path/to/meipass')
153 assert result.endswith('resources/icon.png')
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')
161 test_dir = "/test/directory"
162 monkeypatch.setattr(utils, '__file__', os.path.join(test_dir, 'utils.py'))
164 result = utils.resource_path("resources/icon.png")
166 assert test_dir in result
167 assert "resources/icon.png" in result
169 def test_resource_path_absolute(self):
170 """Verify that it returns an absolute path"""
171 result = utils.resource_path("test.txt")
173 assert os.path.isabs(result)
176class TestGetShellPath:
177 """Tests for get_shell_path function"""
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 )
188 result = utils.get_shell_path()
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)
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 )
204 with patch.dict(os.environ, {'SHELL': '/bin/bash'}):
205 result = utils.get_shell_path()
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)
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)
219 result = utils.get_shell_path()
221 # Should use /bin/bash as fallback
222 assert "/bin/bash" in str(mock_run.call_args)
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"
229 with patch.dict(os.environ, {'PATH': test_path}):
230 result = utils.get_shell_path()
232 assert result == test_path
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"
240 with patch.dict(os.environ, {'PATH': test_path}):
241 result = utils.get_shell_path()
243 # Should return PATH from the environment
244 assert result == test_path
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"
251 with patch.dict(os.environ, {'PATH': test_path}):
252 result = utils.get_shell_path()
254 assert result == test_path
257class TestSetupFSLEnv:
258 """Tests for setup_fsl_env function"""
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 )
268 fsldir, fsloutputtype = utils.setup_fsl_env()
270 assert fsldir == "/usr/local/fsl"
271 assert fsloutputtype == "NIFTI_GZ"
272 mock_run.assert_called_once()
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 )
282 utils.setup_fsl_env()
284 call_args = str(mock_run.call_args)
285 assert "/bin/zsh" in call_args
286 assert "fsl.sh" in call_args
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 )
296 with pytest.raises(RuntimeError) as exc_info:
297 utils.setup_fsl_env()
299 assert "Impossibile leggere" in str(exc_info.value) or "Could not read" in str(exc_info.value)
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 )
309 with pytest.raises(RuntimeError):
310 utils.setup_fsl_env()
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()
319class TestPlatformSpecificBehavior:
320 """Tests for platform-specific behavior"""
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
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
347class TestEdgeCases:
348 """Tests for edge cases and unusual situations"""
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("")
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)
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)
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 )
375 fsldir, fsloutputtype = utils.setup_fsl_env()
377 # strip should remove whitespace
378 assert fsldir.strip() == "/usr/local/fsl"
379 assert fsloutputtype.strip() == "NIFTI_GZ"
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 == ""
389class TestIntegrationScenarios:
390 """Integration tests for realistic scenarios"""
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 )
402 path = utils.get_shell_path()
403 assert "/usr/local/bin" in path
405 # Setup FSL
406 mock_run.return_value = Mock(
407 stdout="/usr/local/fsl\nNIFTI_GZ\n",
408 returncode=0
409 )
411 fsldir, fsltype = utils.setup_fsl_env()
412 assert fsldir == "/usr/local/fsl"
413 assert fsltype == "NIFTI_GZ"
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')
421 test_dir = "/home/user/project"
422 monkeypatch.setattr(utils, '__file__', os.path.join(test_dir, 'utils.py'))
424 # Resource path should use project directory
425 resource = utils.resource_path("icon.png")
426 assert test_dir in resource
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
433 try:
434 app_dir = utils.get_app_dir()
435 assert app_dir.exists()
436 finally:
437 shutil.rmtree(temp_home, ignore_errors=True)
440class TestErrorHandling:
441 """Tests for error handling"""
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'
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()
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 )
468 # Should still work (strip handles it)
469 fsldir, fsltype = utils.setup_fsl_env()
470 assert "/usr/local/fsl" in fsldir
473if __name__ == "__main__":
474 pytest.main([__file__, "-v", "--tb=short"])