hatch-xclam 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hatch/__init__.py +21 -0
- hatch/cli_hatch.py +2748 -0
- hatch/environment_manager.py +1375 -0
- hatch/installers/__init__.py +25 -0
- hatch/installers/dependency_installation_orchestrator.py +636 -0
- hatch/installers/docker_installer.py +545 -0
- hatch/installers/hatch_installer.py +198 -0
- hatch/installers/installation_context.py +109 -0
- hatch/installers/installer_base.py +195 -0
- hatch/installers/python_installer.py +342 -0
- hatch/installers/registry.py +179 -0
- hatch/installers/system_installer.py +588 -0
- hatch/mcp_host_config/__init__.py +38 -0
- hatch/mcp_host_config/backup.py +458 -0
- hatch/mcp_host_config/host_management.py +572 -0
- hatch/mcp_host_config/models.py +602 -0
- hatch/mcp_host_config/reporting.py +181 -0
- hatch/mcp_host_config/strategies.py +513 -0
- hatch/package_loader.py +263 -0
- hatch/python_environment_manager.py +734 -0
- hatch/registry_explorer.py +171 -0
- hatch/registry_retriever.py +335 -0
- hatch/template_generator.py +179 -0
- hatch_xclam-0.7.0.dist-info/METADATA +150 -0
- hatch_xclam-0.7.0.dist-info/RECORD +93 -0
- hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
- hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
- hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
- hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/run_environment_tests.py +124 -0
- tests/test_cli_version.py +122 -0
- tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
- tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
- tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
- tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
- tests/test_data_utils.py +472 -0
- tests/test_dependency_orchestrator_consent.py +266 -0
- tests/test_docker_installer.py +524 -0
- tests/test_env_manip.py +991 -0
- tests/test_hatch_installer.py +179 -0
- tests/test_installer_base.py +221 -0
- tests/test_mcp_atomic_operations.py +276 -0
- tests/test_mcp_backup_integration.py +308 -0
- tests/test_mcp_cli_all_host_specific_args.py +303 -0
- tests/test_mcp_cli_backup_management.py +295 -0
- tests/test_mcp_cli_direct_management.py +453 -0
- tests/test_mcp_cli_discovery_listing.py +582 -0
- tests/test_mcp_cli_host_config_integration.py +823 -0
- tests/test_mcp_cli_package_management.py +360 -0
- tests/test_mcp_cli_partial_updates.py +859 -0
- tests/test_mcp_environment_integration.py +520 -0
- tests/test_mcp_host_config_backup.py +257 -0
- tests/test_mcp_host_configuration_manager.py +331 -0
- tests/test_mcp_host_registry_decorator.py +348 -0
- tests/test_mcp_pydantic_architecture_v4.py +603 -0
- tests/test_mcp_server_config_models.py +242 -0
- tests/test_mcp_server_config_type_field.py +221 -0
- tests/test_mcp_sync_functionality.py +316 -0
- tests/test_mcp_user_feedback_reporting.py +359 -0
- tests/test_non_tty_integration.py +281 -0
- tests/test_online_package_loader.py +202 -0
- tests/test_python_environment_manager.py +882 -0
- tests/test_python_installer.py +327 -0
- tests/test_registry.py +51 -0
- tests/test_registry_retriever.py +250 -0
- tests/test_system_installer.py +733 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
"""Tests for PythonEnvironmentManager.
|
|
2
|
+
|
|
3
|
+
This module contains tests for the Python environment management functionality,
|
|
4
|
+
including conda/mamba environment creation, configuration, and integration.
|
|
5
|
+
"""
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import unittest
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
11
|
+
|
|
12
|
+
from wobble.decorators import regression_test, integration_test, slow_test
|
|
13
|
+
|
|
14
|
+
from hatch.python_environment_manager import PythonEnvironmentManager, PythonEnvironmentError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestPythonEnvironmentManager(unittest.TestCase):
|
|
18
|
+
"""Test cases for PythonEnvironmentManager functionality."""
|
|
19
|
+
|
|
20
|
+
def setUp(self):
|
|
21
|
+
"""Set up test environment."""
|
|
22
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
23
|
+
self.environments_dir = Path(self.temp_dir) / "envs"
|
|
24
|
+
self.environments_dir.mkdir(exist_ok=True)
|
|
25
|
+
|
|
26
|
+
# Create manager instance for testing
|
|
27
|
+
self.manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
|
|
28
|
+
|
|
29
|
+
# Track environments created during this test for cleanup
|
|
30
|
+
self.created_environments = []
|
|
31
|
+
|
|
32
|
+
def tearDown(self):
|
|
33
|
+
"""Clean up test environment."""
|
|
34
|
+
# Clean up any conda/mamba environments created during this test
|
|
35
|
+
if hasattr(self, 'manager') and self.manager.is_available():
|
|
36
|
+
for env_name in self.created_environments:
|
|
37
|
+
try:
|
|
38
|
+
if self.manager.environment_exists(env_name):
|
|
39
|
+
self.manager.remove_python_environment(env_name)
|
|
40
|
+
except Exception:
|
|
41
|
+
pass # Best effort cleanup
|
|
42
|
+
|
|
43
|
+
# Clean up temporary directory
|
|
44
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
45
|
+
|
|
46
|
+
def _track_environment(self, env_name):
|
|
47
|
+
"""Track an environment for cleanup in tearDown."""
|
|
48
|
+
if env_name not in self.created_environments:
|
|
49
|
+
self.created_environments.append(env_name)
|
|
50
|
+
|
|
51
|
+
@regression_test
|
|
52
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=True)
|
|
53
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._get_conda_env_name', return_value='hatch_test_env')
|
|
54
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._get_python_executable_path', return_value='C:/fake/env/Scripts/python.exe')
|
|
55
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager.get_environment_path', return_value=Path('C:/fake/env'))
|
|
56
|
+
@patch('platform.system', return_value='Windows')
|
|
57
|
+
def test_get_environment_activation_info_windows(self, mock_platform, mock_get_env_path, mock_get_python_exec_path, mock_get_conda_env_name, mock_conda_env_exists):
|
|
58
|
+
"""Test get_environment_activation_info returns correct env vars on Windows."""
|
|
59
|
+
env_name = 'test_env'
|
|
60
|
+
manager = PythonEnvironmentManager(environments_dir=Path('C:/fake/envs'))
|
|
61
|
+
env_vars = manager.get_environment_activation_info(env_name)
|
|
62
|
+
self.assertIsInstance(env_vars, dict)
|
|
63
|
+
self.assertEqual(env_vars['CONDA_DEFAULT_ENV'], 'hatch_test_env')
|
|
64
|
+
self.assertEqual(env_vars['CONDA_PREFIX'], str(Path('C:/fake/env')))
|
|
65
|
+
self.assertIn('PATH', env_vars)
|
|
66
|
+
# On Windows, the path separator is ';' and paths are backslash
|
|
67
|
+
# Split PATH and check each expected directory is present as a component
|
|
68
|
+
path_dirs = env_vars['PATH'].split(';')
|
|
69
|
+
self.assertIn('C:\\fake\\env', path_dirs)
|
|
70
|
+
self.assertIn('C:\\fake\\env\\Scripts', path_dirs)
|
|
71
|
+
self.assertIn('C:\\fake\\env\\Library\\bin', path_dirs)
|
|
72
|
+
self.assertEqual(env_vars['PYTHON'], 'C:/fake/env/Scripts/python.exe')
|
|
73
|
+
|
|
74
|
+
@regression_test
|
|
75
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=True)
|
|
76
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._get_conda_env_name', return_value='hatch_test_env')
|
|
77
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._get_python_executable_path', return_value='/fake/env/bin/python')
|
|
78
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager.get_environment_path', return_value=Path('/fake/env'))
|
|
79
|
+
@patch('platform.system', return_value='Linux')
|
|
80
|
+
def test_get_environment_activation_info_unix(self, mock_platform, mock_get_env_path, mock_get_python_exec_path, mock_get_conda_env_name, mock_conda_env_exists):
|
|
81
|
+
"""Test get_environment_activation_info returns correct env vars on Unix."""
|
|
82
|
+
env_name = 'test_env'
|
|
83
|
+
manager = PythonEnvironmentManager(environments_dir=Path('/fake/envs'))
|
|
84
|
+
env_vars = manager.get_environment_activation_info(env_name)
|
|
85
|
+
self.assertIsInstance(env_vars, dict)
|
|
86
|
+
self.assertEqual(env_vars['CONDA_DEFAULT_ENV'], 'hatch_test_env')
|
|
87
|
+
self.assertEqual(env_vars['CONDA_PREFIX'], str(Path('/fake/env')))
|
|
88
|
+
self.assertIn('PATH', env_vars)
|
|
89
|
+
# On Unix, the path separator is ':' and paths are forward slash, but Path() may normalize to backslash on Windows
|
|
90
|
+
# Accept both possible representations for cross-platform test running
|
|
91
|
+
path_dirs = env_vars['PATH']
|
|
92
|
+
self.assertTrue('/fake/env/bin' in path_dirs or '\\fake\\env\\bin' in path_dirs, f"Expected '/fake/env/bin' or '\\fake\\env\\bin' to be in PATH: {env_vars['PATH']}")
|
|
93
|
+
self.assertEqual(env_vars['PYTHON'], '/fake/env/bin/python')
|
|
94
|
+
|
|
95
|
+
@regression_test
|
|
96
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=False)
|
|
97
|
+
def test_get_environment_activation_info_env_not_exists(self, mock_conda_env_exists):
|
|
98
|
+
"""Test get_environment_activation_info returns None if env does not exist."""
|
|
99
|
+
env_name = 'nonexistent_env'
|
|
100
|
+
manager = PythonEnvironmentManager(environments_dir=Path('/fake/envs'))
|
|
101
|
+
env_vars = manager.get_environment_activation_info(env_name)
|
|
102
|
+
self.assertIsNone(env_vars)
|
|
103
|
+
|
|
104
|
+
@regression_test
|
|
105
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=True)
|
|
106
|
+
@patch('hatch.python_environment_manager.PythonEnvironmentManager._get_python_executable_path', return_value=None)
|
|
107
|
+
def test_get_environment_activation_info_no_python(self, mock_get_python_exec_path, mock_conda_env_exists):
|
|
108
|
+
"""Test get_environment_activation_info returns None if python executable not found."""
|
|
109
|
+
env_name = 'test_env'
|
|
110
|
+
manager = PythonEnvironmentManager(environments_dir=Path('/fake/envs'))
|
|
111
|
+
env_vars = manager.get_environment_activation_info(env_name)
|
|
112
|
+
self.assertIsNone(env_vars)
|
|
113
|
+
|
|
114
|
+
@regression_test
|
|
115
|
+
def test_init(self):
|
|
116
|
+
"""Test PythonEnvironmentManager initialization."""
|
|
117
|
+
self.assertEqual(self.manager.environments_dir, self.environments_dir)
|
|
118
|
+
self.assertIsNotNone(self.manager.logger)
|
|
119
|
+
|
|
120
|
+
@regression_test
|
|
121
|
+
def test_detect_conda_mamba_with_mamba(self):
|
|
122
|
+
"""Test conda/mamba detection when mamba is available."""
|
|
123
|
+
with patch.object(PythonEnvironmentManager, "_detect_manager") as mock_detect:
|
|
124
|
+
# mamba found, conda found
|
|
125
|
+
mock_detect.side_effect = lambda manager: "/usr/bin/mamba" if manager == "mamba" else "/usr/bin/conda"
|
|
126
|
+
manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
|
|
127
|
+
self.assertEqual(manager.mamba_executable, "/usr/bin/mamba")
|
|
128
|
+
self.assertEqual(manager.conda_executable, "/usr/bin/conda")
|
|
129
|
+
|
|
130
|
+
@regression_test
|
|
131
|
+
def test_detect_conda_mamba_conda_only(self):
|
|
132
|
+
"""Test conda/mamba detection when only conda is available."""
|
|
133
|
+
with patch.object(PythonEnvironmentManager, "_detect_manager") as mock_detect:
|
|
134
|
+
# mamba not found, conda found
|
|
135
|
+
mock_detect.side_effect = lambda manager: None if manager == "mamba" else "/usr/bin/conda"
|
|
136
|
+
manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
|
|
137
|
+
self.assertIsNone(manager.mamba_executable)
|
|
138
|
+
self.assertEqual(manager.conda_executable, "/usr/bin/conda")
|
|
139
|
+
|
|
140
|
+
@regression_test
|
|
141
|
+
def test_detect_conda_mamba_none_available(self):
|
|
142
|
+
"""Test conda/mamba detection when neither is available."""
|
|
143
|
+
with patch.object(PythonEnvironmentManager, "_detect_manager", return_value=None):
|
|
144
|
+
manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
|
|
145
|
+
self.assertIsNone(manager.mamba_executable)
|
|
146
|
+
self.assertIsNone(manager.conda_executable)
|
|
147
|
+
|
|
148
|
+
@regression_test
|
|
149
|
+
def test_get_conda_env_name(self):
|
|
150
|
+
"""Test conda environment name generation."""
|
|
151
|
+
env_name = "test_env"
|
|
152
|
+
conda_name = self.manager._get_conda_env_name(env_name)
|
|
153
|
+
self.assertEqual(conda_name, "hatch_test_env")
|
|
154
|
+
|
|
155
|
+
@regression_test
|
|
156
|
+
@patch('subprocess.run')
|
|
157
|
+
def test_get_python_executable_path_windows(self, mock_run):
|
|
158
|
+
"""Test Python executable path on Windows."""
|
|
159
|
+
with patch('platform.system', return_value='Windows'):
|
|
160
|
+
env_name = "test_env"
|
|
161
|
+
|
|
162
|
+
# Mock conda info command to return environment path
|
|
163
|
+
mock_run.return_value = Mock(
|
|
164
|
+
returncode=0,
|
|
165
|
+
stdout='{"envs": ["/conda/envs/hatch_test_env"]}'
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
python_path = self.manager._get_python_executable_path(env_name)
|
|
169
|
+
expected = Path("/conda/envs/hatch_test_env/python.exe")
|
|
170
|
+
self.assertEqual(python_path, expected)
|
|
171
|
+
|
|
172
|
+
@regression_test
|
|
173
|
+
@patch('subprocess.run')
|
|
174
|
+
def test_get_python_executable_path_unix(self, mock_run):
|
|
175
|
+
"""Test Python executable path on Unix/Linux."""
|
|
176
|
+
with patch('platform.system', return_value='Linux'):
|
|
177
|
+
env_name = "test_env"
|
|
178
|
+
|
|
179
|
+
# Mock conda info command to return environment path
|
|
180
|
+
mock_run.return_value = Mock(
|
|
181
|
+
returncode=0,
|
|
182
|
+
stdout='{"envs": ["/conda/envs/hatch_test_env"]}'
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
python_path = self.manager._get_python_executable_path(env_name)
|
|
186
|
+
expected = Path("/conda/envs/hatch_test_env/bin/python")
|
|
187
|
+
self.assertEqual(python_path, expected)
|
|
188
|
+
|
|
189
|
+
@regression_test
|
|
190
|
+
def test_is_available_no_conda(self):
|
|
191
|
+
"""Test availability check when conda/mamba is not available."""
|
|
192
|
+
manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
|
|
193
|
+
manager.conda_executable = None
|
|
194
|
+
manager.mamba_executable = None
|
|
195
|
+
|
|
196
|
+
self.assertFalse(manager.is_available())
|
|
197
|
+
|
|
198
|
+
@regression_test
|
|
199
|
+
@patch('subprocess.run')
|
|
200
|
+
def test_is_available_with_conda(self, mock_run):
|
|
201
|
+
"""Test availability check when conda is available."""
|
|
202
|
+
self.manager.conda_executable = "/usr/bin/conda"
|
|
203
|
+
|
|
204
|
+
# Mock successful conda info
|
|
205
|
+
mock_run.return_value = Mock(returncode=0, stdout='{"platform": "linux-64"}')
|
|
206
|
+
|
|
207
|
+
self.assertTrue(self.manager.is_available())
|
|
208
|
+
|
|
209
|
+
@regression_test
|
|
210
|
+
def test_get_preferred_executable(self):
|
|
211
|
+
"""Test preferred executable selection."""
|
|
212
|
+
# Test mamba preferred over conda
|
|
213
|
+
self.manager.mamba_executable = "/usr/bin/mamba"
|
|
214
|
+
self.manager.conda_executable = "/usr/bin/conda"
|
|
215
|
+
self.assertEqual(self.manager.get_preferred_executable(), "/usr/bin/mamba")
|
|
216
|
+
|
|
217
|
+
# Test conda when mamba not available
|
|
218
|
+
self.manager.mamba_executable = None
|
|
219
|
+
self.assertEqual(self.manager.get_preferred_executable(), "/usr/bin/conda")
|
|
220
|
+
|
|
221
|
+
# Test None when neither available
|
|
222
|
+
self.manager.conda_executable = None
|
|
223
|
+
self.assertIsNone(self.manager.get_preferred_executable())
|
|
224
|
+
|
|
225
|
+
@regression_test
|
|
226
|
+
@patch('shutil.which')
|
|
227
|
+
@patch('subprocess.run')
|
|
228
|
+
def test_create_python_environment_success(self, mock_run, mock_which):
|
|
229
|
+
"""Test successful Python environment creation."""
|
|
230
|
+
# Patch mamba detection
|
|
231
|
+
mock_which.side_effect = lambda cmd: "/usr/bin/mamba" if cmd == "mamba" else None
|
|
232
|
+
|
|
233
|
+
# Patch subprocess.run for both validation and creation
|
|
234
|
+
def run_side_effect(cmd, *args, **kwargs):
|
|
235
|
+
if "info" in cmd:
|
|
236
|
+
# Validation call
|
|
237
|
+
return Mock(returncode=0, stdout='{"platform": "win-64"}')
|
|
238
|
+
elif "create" in cmd:
|
|
239
|
+
# Environment creation call
|
|
240
|
+
return Mock(returncode=0, stdout="Environment created")
|
|
241
|
+
else:
|
|
242
|
+
return Mock(returncode=0, stdout="")
|
|
243
|
+
mock_run.side_effect = run_side_effect
|
|
244
|
+
|
|
245
|
+
manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
|
|
246
|
+
|
|
247
|
+
# Mock environment existence check
|
|
248
|
+
with patch.object(manager, '_conda_env_exists', return_value=False):
|
|
249
|
+
result = manager.create_python_environment("test_env", python_version="3.11")
|
|
250
|
+
self.assertTrue(result)
|
|
251
|
+
mock_run.assert_called()
|
|
252
|
+
|
|
253
|
+
@regression_test
|
|
254
|
+
def test_create_python_environment_no_conda(self):
|
|
255
|
+
"""Test Python environment creation when conda/mamba is not available."""
|
|
256
|
+
self.manager.conda_executable = None
|
|
257
|
+
self.manager.mamba_executable = None
|
|
258
|
+
|
|
259
|
+
with self.assertRaises(PythonEnvironmentError):
|
|
260
|
+
self.manager.create_python_environment("test_env")
|
|
261
|
+
|
|
262
|
+
@regression_test
|
|
263
|
+
@patch('shutil.which')
|
|
264
|
+
@patch('subprocess.run')
|
|
265
|
+
def test_create_python_environment_already_exists(self, mock_run, mock_which):
|
|
266
|
+
"""Test Python environment creation when environment already exists."""
|
|
267
|
+
# Patch mamba detection
|
|
268
|
+
mock_which.side_effect = lambda cmd: "/usr/bin/mamba" if cmd == "mamba" else None
|
|
269
|
+
|
|
270
|
+
# Patch subprocess.run for both validation and creation
|
|
271
|
+
def run_side_effect(cmd, *args, **kwargs):
|
|
272
|
+
if "info" in cmd:
|
|
273
|
+
# Validation call
|
|
274
|
+
return Mock(returncode=0, stdout='{"platform": "win-64"}')
|
|
275
|
+
elif "create" in cmd:
|
|
276
|
+
# Environment creation call
|
|
277
|
+
return Mock(returncode=0, stdout="Environment created")
|
|
278
|
+
else:
|
|
279
|
+
return Mock(returncode=0, stdout="")
|
|
280
|
+
mock_run.side_effect = run_side_effect
|
|
281
|
+
|
|
282
|
+
# Mock environment already exists
|
|
283
|
+
with patch.object(self.manager, '_conda_env_exists', return_value=True):
|
|
284
|
+
result = self.manager.create_python_environment("test_env")
|
|
285
|
+
self.assertTrue(result)
|
|
286
|
+
# Ensure 'create' was not called, but 'info' was
|
|
287
|
+
create_calls = [call for call in mock_run.call_args_list if "create" in call[0][0]]
|
|
288
|
+
self.assertEqual(len(create_calls), 0)
|
|
289
|
+
|
|
290
|
+
@regression_test
|
|
291
|
+
@patch('subprocess.run')
|
|
292
|
+
def test_conda_env_exists(self, mock_run):
|
|
293
|
+
"""Test conda environment existence check."""
|
|
294
|
+
env_name = "test_env"
|
|
295
|
+
|
|
296
|
+
# Mock conda env list to return the environment
|
|
297
|
+
mock_run.return_value = Mock(
|
|
298
|
+
returncode=0,
|
|
299
|
+
stdout='{"envs": ["/conda/envs/hatch_test_env", "/conda/envs/other_env"]}'
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
self.assertTrue(self.manager._conda_env_exists(env_name))
|
|
303
|
+
|
|
304
|
+
@regression_test
|
|
305
|
+
@patch('subprocess.run')
|
|
306
|
+
def test_conda_env_not_exists(self, mock_run):
|
|
307
|
+
"""Test conda environment existence check when environment doesn't exist."""
|
|
308
|
+
env_name = "nonexistent_env"
|
|
309
|
+
|
|
310
|
+
# Mock conda env list to not return the environment
|
|
311
|
+
mock_run.return_value = Mock(
|
|
312
|
+
returncode=0,
|
|
313
|
+
stdout='{"envs": ["/conda/envs/other_env"]}'
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
self.assertFalse(self.manager._conda_env_exists(env_name))
|
|
317
|
+
|
|
318
|
+
@regression_test
|
|
319
|
+
@patch('subprocess.run')
|
|
320
|
+
def test_get_python_executable_exists(self, mock_run):
|
|
321
|
+
"""Test getting Python executable when environment exists."""
|
|
322
|
+
env_name = "test_env"
|
|
323
|
+
|
|
324
|
+
# Mock conda env list to show environment exists
|
|
325
|
+
def run_side_effect(cmd, *args, **kwargs):
|
|
326
|
+
if "env" in cmd and "list" in cmd:
|
|
327
|
+
return Mock(returncode=0, stdout='{"envs": ["/conda/envs/hatch_test_env"]}')
|
|
328
|
+
elif "info" in cmd and "--envs" in cmd:
|
|
329
|
+
return Mock(returncode=0, stdout='{"envs": ["/conda/envs/hatch_test_env"]}')
|
|
330
|
+
else:
|
|
331
|
+
return Mock(returncode=0, stdout='{}')
|
|
332
|
+
|
|
333
|
+
mock_run.side_effect = run_side_effect
|
|
334
|
+
|
|
335
|
+
# Mock that the file exists
|
|
336
|
+
with patch('pathlib.Path.exists', return_value=True):
|
|
337
|
+
result = self.manager.get_python_executable(env_name)
|
|
338
|
+
import platform
|
|
339
|
+
from pathlib import Path as _Path
|
|
340
|
+
if platform.system() == "Windows":
|
|
341
|
+
expected = str(_Path("\\conda\\envs\\hatch_test_env\\python.exe"))
|
|
342
|
+
else:
|
|
343
|
+
expected = str(_Path("/conda/envs/hatch_test_env/bin/python"))
|
|
344
|
+
self.assertEqual(result, expected)
|
|
345
|
+
|
|
346
|
+
@regression_test
|
|
347
|
+
def test_get_python_executable_not_exists(self):
|
|
348
|
+
"""Test getting Python executable when environment doesn't exist."""
|
|
349
|
+
env_name = "nonexistent_env"
|
|
350
|
+
|
|
351
|
+
with patch.object(self.manager, '_conda_env_exists', return_value=False):
|
|
352
|
+
result = self.manager.get_python_executable(env_name)
|
|
353
|
+
self.assertIsNone(result)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class TestPythonEnvironmentManagerIntegration(unittest.TestCase):
|
|
357
|
+
"""Integration test cases for PythonEnvironmentManager with real conda/mamba operations.
|
|
358
|
+
|
|
359
|
+
These tests require conda or mamba to be installed on the system and will create
|
|
360
|
+
real conda environments for testing. They are more comprehensive but slower than
|
|
361
|
+
the mocked unit tests.
|
|
362
|
+
"""
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def setUpClass(cls):
|
|
366
|
+
"""Set up class-level test environment."""
|
|
367
|
+
cls.temp_dir = tempfile.mkdtemp()
|
|
368
|
+
cls.environments_dir = Path(cls.temp_dir) / "envs"
|
|
369
|
+
cls.environments_dir.mkdir(exist_ok=True)
|
|
370
|
+
|
|
371
|
+
# Create manager instance for integration testing
|
|
372
|
+
cls.manager = PythonEnvironmentManager(environments_dir=cls.environments_dir)
|
|
373
|
+
|
|
374
|
+
# Track all environments created during integration tests
|
|
375
|
+
cls.all_created_environments = set()
|
|
376
|
+
|
|
377
|
+
# Skip all tests if conda/mamba is not available
|
|
378
|
+
if not cls.manager.is_available():
|
|
379
|
+
raise unittest.SkipTest("Conda/mamba not available for integration tests")
|
|
380
|
+
|
|
381
|
+
def setUp(self):
|
|
382
|
+
"""Set up individual test."""
|
|
383
|
+
# Track environments created during this specific test
|
|
384
|
+
self.test_environments = []
|
|
385
|
+
|
|
386
|
+
def tearDown(self):
|
|
387
|
+
"""Clean up individual test."""
|
|
388
|
+
# Clean up environments created during this specific test
|
|
389
|
+
for env_name in self.test_environments:
|
|
390
|
+
try:
|
|
391
|
+
if self.manager.environment_exists(env_name):
|
|
392
|
+
self.manager.remove_python_environment(env_name)
|
|
393
|
+
self.all_created_environments.discard(env_name)
|
|
394
|
+
except Exception:
|
|
395
|
+
pass # Best effort cleanup
|
|
396
|
+
|
|
397
|
+
def _track_environment(self, env_name):
|
|
398
|
+
"""Track an environment for cleanup."""
|
|
399
|
+
if env_name not in self.test_environments:
|
|
400
|
+
self.test_environments.append(env_name)
|
|
401
|
+
self.all_created_environments.add(env_name)
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
def tearDownClass(cls):
|
|
405
|
+
"""Clean up class-level test environment."""
|
|
406
|
+
# Clean up any remaining test environments
|
|
407
|
+
try:
|
|
408
|
+
# Clean up tracked environments
|
|
409
|
+
for env_name in list(cls.all_created_environments):
|
|
410
|
+
if cls.manager.environment_exists(env_name):
|
|
411
|
+
cls.manager.remove_python_environment(env_name)
|
|
412
|
+
|
|
413
|
+
# Clean up known test environment patterns (fallback)
|
|
414
|
+
known_patterns = [
|
|
415
|
+
"test_integration_env", "test_python_311", "test_python_312", "test_diagnostics_env",
|
|
416
|
+
"test_env_1", "test_env_2", "test_env_3", "test_env_4", "test_env_5",
|
|
417
|
+
"test_python_39", "test_python_310", "test_python_312", "test_cache_env1", "test_cache_env2"
|
|
418
|
+
]
|
|
419
|
+
for env_name in known_patterns:
|
|
420
|
+
if cls.manager.environment_exists(env_name):
|
|
421
|
+
cls.manager.remove_python_environment(env_name)
|
|
422
|
+
except Exception:
|
|
423
|
+
pass # Best effort cleanup
|
|
424
|
+
|
|
425
|
+
shutil.rmtree(cls.temp_dir, ignore_errors=True)
|
|
426
|
+
|
|
427
|
+
@integration_test(scope="system")
|
|
428
|
+
@slow_test
|
|
429
|
+
def test_conda_mamba_detection_real(self):
|
|
430
|
+
"""Test real conda/mamba detection on the system."""
|
|
431
|
+
manager_info = self.manager.get_manager_info()
|
|
432
|
+
|
|
433
|
+
# At least one should be available since we skip tests if neither is available
|
|
434
|
+
self.assertTrue(manager_info["is_available"])
|
|
435
|
+
self.assertTrue(
|
|
436
|
+
manager_info["conda_executable"] is not None or
|
|
437
|
+
manager_info["mamba_executable"] is not None
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Preferred manager should be set
|
|
441
|
+
self.assertIsNotNone(manager_info["preferred_manager"])
|
|
442
|
+
|
|
443
|
+
# Platform and Python version should be populated
|
|
444
|
+
self.assertIsNotNone(manager_info["platform"])
|
|
445
|
+
self.assertIsNotNone(manager_info["python_version"])
|
|
446
|
+
|
|
447
|
+
@integration_test(scope="system")
|
|
448
|
+
@slow_test
|
|
449
|
+
def test_manager_diagnostics_real(self):
|
|
450
|
+
"""Test real manager diagnostics."""
|
|
451
|
+
diagnostics = self.manager.get_manager_diagnostics()
|
|
452
|
+
|
|
453
|
+
# Should have basic information
|
|
454
|
+
self.assertIn("any_manager_available", diagnostics)
|
|
455
|
+
self.assertTrue(diagnostics["any_manager_available"])
|
|
456
|
+
self.assertIn("platform", diagnostics)
|
|
457
|
+
self.assertIn("python_version", diagnostics)
|
|
458
|
+
self.assertIn("environments_dir", diagnostics)
|
|
459
|
+
|
|
460
|
+
# Should test actual executables
|
|
461
|
+
if diagnostics["conda_executable"]:
|
|
462
|
+
self.assertIn("conda_works", diagnostics)
|
|
463
|
+
self.assertIn("conda_version", diagnostics)
|
|
464
|
+
|
|
465
|
+
if diagnostics["mamba_executable"]:
|
|
466
|
+
self.assertIn("mamba_works", diagnostics)
|
|
467
|
+
self.assertIn("mamba_version", diagnostics)
|
|
468
|
+
|
|
469
|
+
@integration_test(scope="system")
|
|
470
|
+
@slow_test
|
|
471
|
+
def test_create_and_remove_python_environment_real(self):
|
|
472
|
+
"""Test real Python environment creation and removal."""
|
|
473
|
+
env_name = "test_integration_env"
|
|
474
|
+
self._track_environment(env_name)
|
|
475
|
+
|
|
476
|
+
# Ensure environment doesn't exist initially
|
|
477
|
+
if self.manager.environment_exists(env_name):
|
|
478
|
+
self.manager.remove_python_environment(env_name)
|
|
479
|
+
|
|
480
|
+
# Create environment
|
|
481
|
+
result = self.manager.create_python_environment(env_name)
|
|
482
|
+
self.assertTrue(result, "Failed to create Python environment")
|
|
483
|
+
|
|
484
|
+
# Verify environment exists
|
|
485
|
+
self.assertTrue(self.manager.environment_exists(env_name))
|
|
486
|
+
|
|
487
|
+
# Verify Python executable is available
|
|
488
|
+
python_exec = self.manager.get_python_executable(env_name)
|
|
489
|
+
self.assertIsNotNone(python_exec, "Python executable not found")
|
|
490
|
+
self.assertTrue(Path(python_exec).exists(), f"Python executable doesn't exist: {python_exec}")
|
|
491
|
+
|
|
492
|
+
# Get environment info
|
|
493
|
+
env_info = self.manager.get_environment_info(env_name)
|
|
494
|
+
self.assertIsNotNone(env_info)
|
|
495
|
+
self.assertEqual(env_info["environment_name"], env_name)
|
|
496
|
+
self.assertIsNotNone(env_info["conda_env_name"])
|
|
497
|
+
self.assertIsNotNone(env_info["python_executable"])
|
|
498
|
+
|
|
499
|
+
# Remove environment
|
|
500
|
+
result = self.manager.remove_python_environment(env_name)
|
|
501
|
+
self.assertTrue(result, "Failed to remove Python environment")
|
|
502
|
+
|
|
503
|
+
# Verify environment no longer exists
|
|
504
|
+
self.assertFalse(self.manager.environment_exists(env_name))
|
|
505
|
+
|
|
506
|
+
@integration_test(scope="system")
|
|
507
|
+
@slow_test
|
|
508
|
+
def test_create_python_environment_with_version_real(self):
|
|
509
|
+
"""Test real Python environment creation with specific version."""
|
|
510
|
+
env_name = "test_python_311"
|
|
511
|
+
self._track_environment(env_name)
|
|
512
|
+
python_version = "3.11"
|
|
513
|
+
|
|
514
|
+
# Ensure environment doesn't exist initially
|
|
515
|
+
if self.manager.environment_exists(env_name):
|
|
516
|
+
self.manager.remove_python_environment(env_name)
|
|
517
|
+
|
|
518
|
+
# Create environment with specific Python version
|
|
519
|
+
result = self.manager.create_python_environment(env_name, python_version=python_version)
|
|
520
|
+
self.assertTrue(result, f"Failed to create Python {python_version} environment")
|
|
521
|
+
|
|
522
|
+
# Verify environment exists
|
|
523
|
+
self.assertTrue(self.manager.environment_exists(env_name))
|
|
524
|
+
|
|
525
|
+
# Verify Python version
|
|
526
|
+
actual_version = self.manager.get_python_version(env_name)
|
|
527
|
+
self.assertIsNotNone(actual_version)
|
|
528
|
+
self.assertTrue(actual_version.startswith("3.11"), f"Expected Python 3.11.x, got {actual_version}")
|
|
529
|
+
|
|
530
|
+
# Get comprehensive environment info
|
|
531
|
+
env_info = self.manager.get_environment_info(env_name)
|
|
532
|
+
self.assertIsNotNone(env_info)
|
|
533
|
+
self.assertTrue(env_info["python_version"].startswith("3.11"), f"Expected Python 3.11.x, got {env_info['python_version']}")
|
|
534
|
+
|
|
535
|
+
# Cleanup
|
|
536
|
+
self.manager.remove_python_environment(env_name)
|
|
537
|
+
|
|
538
|
+
@integration_test(scope="system")
|
|
539
|
+
@slow_test
|
|
540
|
+
def test_environment_diagnostics_real(self):
|
|
541
|
+
"""Test real environment diagnostics."""
|
|
542
|
+
env_name = "test_diagnostics_env"
|
|
543
|
+
|
|
544
|
+
# Ensure environment doesn't exist initially
|
|
545
|
+
if self.manager.environment_exists(env_name):
|
|
546
|
+
self.manager.remove_python_environment(env_name)
|
|
547
|
+
|
|
548
|
+
# Test diagnostics for non-existent environment
|
|
549
|
+
diagnostics = self.manager.get_environment_diagnostics(env_name)
|
|
550
|
+
self.assertFalse(diagnostics["exists"])
|
|
551
|
+
self.assertTrue(diagnostics["conda_available"])
|
|
552
|
+
|
|
553
|
+
# Create environment
|
|
554
|
+
self.manager.create_python_environment(env_name)
|
|
555
|
+
|
|
556
|
+
# Test diagnostics for existing environment
|
|
557
|
+
diagnostics = self.manager.get_environment_diagnostics(env_name)
|
|
558
|
+
self.assertTrue(diagnostics["exists"])
|
|
559
|
+
self.assertIsNotNone(diagnostics["python_executable"])
|
|
560
|
+
self.assertTrue(diagnostics["python_accessible"])
|
|
561
|
+
self.assertIsNotNone(diagnostics["python_version"])
|
|
562
|
+
self.assertTrue(diagnostics["python_version_accessible"])
|
|
563
|
+
self.assertTrue(diagnostics["python_executable_works"])
|
|
564
|
+
self.assertIsNotNone(diagnostics["environment_path"])
|
|
565
|
+
self.assertTrue(diagnostics["environment_path_exists"])
|
|
566
|
+
|
|
567
|
+
# Cleanup
|
|
568
|
+
self.manager.remove_python_environment(env_name)
|
|
569
|
+
|
|
570
|
+
@integration_test(scope="system")
|
|
571
|
+
@slow_test
|
|
572
|
+
def test_force_recreation_real(self):
|
|
573
|
+
"""Test force recreation of existing environment."""
|
|
574
|
+
env_name = "test_integration_env"
|
|
575
|
+
|
|
576
|
+
# Ensure environment doesn't exist initially
|
|
577
|
+
if self.manager.environment_exists(env_name):
|
|
578
|
+
self.manager.remove_python_environment(env_name)
|
|
579
|
+
|
|
580
|
+
# Create environment
|
|
581
|
+
result1 = self.manager.create_python_environment(env_name)
|
|
582
|
+
self.assertTrue(result1)
|
|
583
|
+
|
|
584
|
+
# Get initial Python executable
|
|
585
|
+
python_exec1 = self.manager.get_python_executable(env_name)
|
|
586
|
+
self.assertIsNotNone(python_exec1)
|
|
587
|
+
|
|
588
|
+
# Try to create again without force (should succeed but not recreate)
|
|
589
|
+
result2 = self.manager.create_python_environment(env_name, force=False)
|
|
590
|
+
self.assertTrue(result2)
|
|
591
|
+
|
|
592
|
+
# Try to create again with force (should recreate)
|
|
593
|
+
result3 = self.manager.create_python_environment(env_name, force=True)
|
|
594
|
+
self.assertTrue(result3)
|
|
595
|
+
|
|
596
|
+
# Verify environment still exists and works
|
|
597
|
+
self.assertTrue(self.manager.environment_exists(env_name))
|
|
598
|
+
python_exec3 = self.manager.get_python_executable(env_name)
|
|
599
|
+
self.assertIsNotNone(python_exec3)
|
|
600
|
+
|
|
601
|
+
# Cleanup
|
|
602
|
+
self.manager.remove_python_environment(env_name)
|
|
603
|
+
|
|
604
|
+
@integration_test(scope="system")
|
|
605
|
+
@slow_test
|
|
606
|
+
def test_list_environments_real(self):
|
|
607
|
+
"""Test listing environments with real conda environments."""
|
|
608
|
+
test_envs = ["test_env_1", "test_env_2"]
|
|
609
|
+
final_names = ["hatch_test_env_1", "hatch_test_env_2"]
|
|
610
|
+
|
|
611
|
+
# Track environments for cleanup
|
|
612
|
+
for env_name in test_envs:
|
|
613
|
+
self._track_environment(env_name)
|
|
614
|
+
|
|
615
|
+
# Clean up any existing test environments
|
|
616
|
+
for env_name in test_envs:
|
|
617
|
+
if self.manager.environment_exists(env_name):
|
|
618
|
+
self.manager.remove_python_environment(env_name)
|
|
619
|
+
|
|
620
|
+
# Create test environments
|
|
621
|
+
for env_name in test_envs:
|
|
622
|
+
result = self.manager.create_python_environment(env_name)
|
|
623
|
+
self.assertTrue(result, f"Failed to create {env_name}")
|
|
624
|
+
|
|
625
|
+
# List environments
|
|
626
|
+
env_list = self.manager.list_environments()
|
|
627
|
+
|
|
628
|
+
# Should include our test environments
|
|
629
|
+
for env_name in final_names:
|
|
630
|
+
self.assertIn(env_name, env_list, f"{env_name} not found in environment list")
|
|
631
|
+
|
|
632
|
+
# Cleanup
|
|
633
|
+
for env_name in final_names:
|
|
634
|
+
self.manager.remove_python_environment(env_name)
|
|
635
|
+
|
|
636
|
+
@integration_test(scope="system")
|
|
637
|
+
@slow_test
|
|
638
|
+
@unittest.skipIf(
|
|
639
|
+
not (Path("/usr/bin/python3.12").exists() or Path("/usr/bin/python3.9").exists()),
|
|
640
|
+
"Multiple Python versions not available for testing"
|
|
641
|
+
)
|
|
642
|
+
def test_multiple_python_versions_real(self):
|
|
643
|
+
"""Test creating environments with multiple Python versions."""
|
|
644
|
+
test_cases = [
|
|
645
|
+
("test_python_39", "3.9"),
|
|
646
|
+
("test_python_312", "3.12")
|
|
647
|
+
]
|
|
648
|
+
|
|
649
|
+
created_envs = []
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
for env_name, python_version in test_cases:
|
|
653
|
+
# Skip if this Python version is not available
|
|
654
|
+
try:
|
|
655
|
+
result = self.manager.create_python_environment(env_name, python_version=python_version)
|
|
656
|
+
if result:
|
|
657
|
+
created_envs.append(env_name)
|
|
658
|
+
|
|
659
|
+
# Verify Python version
|
|
660
|
+
actual_version = self.manager.get_python_version(env_name)
|
|
661
|
+
self.assertIsNotNone(actual_version)
|
|
662
|
+
self.assertTrue(
|
|
663
|
+
actual_version.startswith(python_version),
|
|
664
|
+
f"Expected Python {python_version}.x, got {actual_version}"
|
|
665
|
+
)
|
|
666
|
+
except Exception as e:
|
|
667
|
+
# Log but don't fail test if specific Python version is not available
|
|
668
|
+
print(f"Skipping Python {python_version} test: {e}")
|
|
669
|
+
|
|
670
|
+
finally:
|
|
671
|
+
# Cleanup
|
|
672
|
+
for env_name in created_envs:
|
|
673
|
+
try:
|
|
674
|
+
self.manager.remove_python_environment(env_name)
|
|
675
|
+
except Exception:
|
|
676
|
+
pass # Best effort cleanup
|
|
677
|
+
|
|
678
|
+
@integration_test(scope="system")
|
|
679
|
+
@slow_test
|
|
680
|
+
def test_error_handling_real(self):
|
|
681
|
+
"""Test error handling with real operations."""
|
|
682
|
+
# Test removing non-existent environment
|
|
683
|
+
result = self.manager.remove_python_environment("nonexistent_env")
|
|
684
|
+
self.assertTrue(result) # Removing non existent environment returns True because it does nothing
|
|
685
|
+
|
|
686
|
+
# Test getting info for non-existent environment
|
|
687
|
+
info = self.manager.get_environment_info("nonexistent_env")
|
|
688
|
+
self.assertIsNone(info)
|
|
689
|
+
|
|
690
|
+
# Test getting Python executable for non-existent environment
|
|
691
|
+
python_exec = self.manager.get_python_executable("nonexistent_env")
|
|
692
|
+
self.assertIsNone(python_exec)
|
|
693
|
+
|
|
694
|
+
# Test diagnostics for non-existent environment
|
|
695
|
+
diagnostics = self.manager.get_environment_diagnostics("nonexistent_env")
|
|
696
|
+
self.assertFalse(diagnostics["exists"])
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
class TestPythonEnvironmentManagerEnhancedFeatures(unittest.TestCase):
|
|
700
|
+
"""Test cases for enhanced features like shell launching and advanced diagnostics."""
|
|
701
|
+
|
|
702
|
+
def setUp(self):
|
|
703
|
+
"""Set up test environment."""
|
|
704
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
705
|
+
self.environments_dir = Path(self.temp_dir) / "envs"
|
|
706
|
+
self.environments_dir.mkdir(exist_ok=True)
|
|
707
|
+
|
|
708
|
+
# Create manager instance for testing
|
|
709
|
+
self.manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
|
|
710
|
+
|
|
711
|
+
# Track environments created during this test for cleanup
|
|
712
|
+
self.created_environments = []
|
|
713
|
+
|
|
714
|
+
def tearDown(self):
|
|
715
|
+
"""Clean up test environment."""
|
|
716
|
+
# Clean up any conda/mamba environments created during this test
|
|
717
|
+
if hasattr(self, 'manager') and self.manager.is_available():
|
|
718
|
+
for env_name in self.created_environments:
|
|
719
|
+
try:
|
|
720
|
+
if self.manager.environment_exists(env_name):
|
|
721
|
+
self.manager.remove_python_environment(env_name)
|
|
722
|
+
except Exception:
|
|
723
|
+
pass # Best effort cleanup
|
|
724
|
+
|
|
725
|
+
# Clean up temporary directory
|
|
726
|
+
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
727
|
+
|
|
728
|
+
def _track_environment(self, env_name):
|
|
729
|
+
"""Track an environment for cleanup in tearDown."""
|
|
730
|
+
if env_name not in self.created_environments:
|
|
731
|
+
self.created_environments.append(env_name)
|
|
732
|
+
|
|
733
|
+
@regression_test
|
|
734
|
+
@patch('subprocess.run')
|
|
735
|
+
def test_launch_shell_with_command(self, mock_run):
|
|
736
|
+
"""Test launching shell with specific command."""
|
|
737
|
+
env_name = "test_shell_env"
|
|
738
|
+
cmd = "print('Hello from Python')"
|
|
739
|
+
|
|
740
|
+
# Mock environment existence and Python executable
|
|
741
|
+
with patch.object(self.manager, 'environment_exists', return_value=True), \
|
|
742
|
+
patch.object(self.manager, 'get_python_executable', return_value="/path/to/python"):
|
|
743
|
+
|
|
744
|
+
mock_run.return_value = Mock(returncode=0)
|
|
745
|
+
|
|
746
|
+
result = self.manager.launch_shell(env_name, cmd)
|
|
747
|
+
self.assertTrue(result)
|
|
748
|
+
|
|
749
|
+
# Verify subprocess was called with correct arguments
|
|
750
|
+
mock_run.assert_called_once()
|
|
751
|
+
call_args = mock_run.call_args[0][0]
|
|
752
|
+
self.assertIn("/path/to/python", call_args)
|
|
753
|
+
self.assertIn("-c", call_args)
|
|
754
|
+
self.assertIn(cmd, call_args)
|
|
755
|
+
|
|
756
|
+
@regression_test
|
|
757
|
+
@patch('subprocess.run')
|
|
758
|
+
@patch('platform.system')
|
|
759
|
+
def test_launch_shell_interactive_windows(self, mock_platform, mock_run):
|
|
760
|
+
"""Test launching interactive shell on Windows."""
|
|
761
|
+
mock_platform.return_value = "Windows"
|
|
762
|
+
env_name = "test_shell_env"
|
|
763
|
+
|
|
764
|
+
# Mock environment existence and Python executable
|
|
765
|
+
with patch.object(self.manager, 'environment_exists', return_value=True), \
|
|
766
|
+
patch.object(self.manager, 'get_python_executable', return_value="/path/to/python"):
|
|
767
|
+
|
|
768
|
+
mock_run.return_value = Mock(returncode=0)
|
|
769
|
+
|
|
770
|
+
result = self.manager.launch_shell(env_name)
|
|
771
|
+
self.assertTrue(result)
|
|
772
|
+
|
|
773
|
+
# Verify subprocess was called for Windows
|
|
774
|
+
mock_run.assert_called_once()
|
|
775
|
+
call_args = mock_run.call_args[0][0]
|
|
776
|
+
self.assertIn("cmd", call_args)
|
|
777
|
+
self.assertIn("/c", call_args)
|
|
778
|
+
|
|
779
|
+
@regression_test
|
|
780
|
+
@patch('subprocess.run')
|
|
781
|
+
@patch('platform.system')
|
|
782
|
+
def test_launch_shell_interactive_unix(self, mock_platform, mock_run):
|
|
783
|
+
"""Test launching interactive shell on Unix."""
|
|
784
|
+
mock_platform.return_value = "Linux"
|
|
785
|
+
env_name = "test_shell_env"
|
|
786
|
+
|
|
787
|
+
# Mock environment existence and Python executable
|
|
788
|
+
with patch.object(self.manager, 'environment_exists', return_value=True), \
|
|
789
|
+
patch.object(self.manager, 'get_python_executable', return_value="/path/to/python"):
|
|
790
|
+
|
|
791
|
+
mock_run.return_value = Mock(returncode=0)
|
|
792
|
+
|
|
793
|
+
result = self.manager.launch_shell(env_name)
|
|
794
|
+
self.assertTrue(result)
|
|
795
|
+
|
|
796
|
+
# Verify subprocess was called with Python executable directly
|
|
797
|
+
mock_run.assert_called_once()
|
|
798
|
+
call_args = mock_run.call_args[0][0]
|
|
799
|
+
self.assertEqual(call_args, ["/path/to/python"])
|
|
800
|
+
|
|
801
|
+
@regression_test
|
|
802
|
+
def test_launch_shell_nonexistent_environment(self):
|
|
803
|
+
"""Test launching shell for non-existent environment."""
|
|
804
|
+
env_name = "nonexistent_env"
|
|
805
|
+
|
|
806
|
+
with patch.object(self.manager, 'environment_exists', return_value=False):
|
|
807
|
+
result = self.manager.launch_shell(env_name)
|
|
808
|
+
self.assertFalse(result)
|
|
809
|
+
|
|
810
|
+
@regression_test
|
|
811
|
+
def test_launch_shell_no_python_executable(self):
|
|
812
|
+
"""Test launching shell when Python executable is not found."""
|
|
813
|
+
env_name = "test_shell_env"
|
|
814
|
+
|
|
815
|
+
with patch.object(self.manager, 'environment_exists', return_value=True), \
|
|
816
|
+
patch.object(self.manager, 'get_python_executable', return_value=None):
|
|
817
|
+
|
|
818
|
+
result = self.manager.launch_shell(env_name)
|
|
819
|
+
self.assertFalse(result)
|
|
820
|
+
|
|
821
|
+
@regression_test
|
|
822
|
+
def test_get_manager_info_structure(self):
|
|
823
|
+
"""Test manager info structure and content."""
|
|
824
|
+
info = self.manager.get_manager_info()
|
|
825
|
+
|
|
826
|
+
# Verify required fields are present
|
|
827
|
+
required_fields = [
|
|
828
|
+
"conda_executable", "mamba_executable", "preferred_manager",
|
|
829
|
+
"is_available", "platform", "python_version"
|
|
830
|
+
]
|
|
831
|
+
|
|
832
|
+
for field in required_fields:
|
|
833
|
+
self.assertIn(field, info, f"Missing required field: {field}")
|
|
834
|
+
|
|
835
|
+
# Verify data types
|
|
836
|
+
self.assertIsInstance(info["is_available"], bool)
|
|
837
|
+
self.assertIsInstance(info["platform"], str)
|
|
838
|
+
self.assertIsInstance(info["python_version"], str)
|
|
839
|
+
|
|
840
|
+
@regression_test
|
|
841
|
+
def test_environment_diagnostics_structure(self):
|
|
842
|
+
"""Test environment diagnostics structure."""
|
|
843
|
+
env_name = "test_diagnostics"
|
|
844
|
+
diagnostics = self.manager.get_environment_diagnostics(env_name)
|
|
845
|
+
|
|
846
|
+
# Verify required fields are present
|
|
847
|
+
required_fields = [
|
|
848
|
+
"environment_name", "conda_env_name", "exists", "conda_available",
|
|
849
|
+
"manager_executable", "platform"
|
|
850
|
+
]
|
|
851
|
+
|
|
852
|
+
for field in required_fields:
|
|
853
|
+
self.assertIn(field, diagnostics, f"Missing required field: {field}")
|
|
854
|
+
|
|
855
|
+
# Verify basic structure
|
|
856
|
+
self.assertEqual(diagnostics["environment_name"], env_name)
|
|
857
|
+
self.assertEqual(diagnostics["conda_env_name"], f"hatch_{env_name}")
|
|
858
|
+
self.assertIsInstance(diagnostics["exists"], bool)
|
|
859
|
+
self.assertIsInstance(diagnostics["conda_available"], bool)
|
|
860
|
+
|
|
861
|
+
@regression_test
|
|
862
|
+
def test_manager_diagnostics_structure(self):
|
|
863
|
+
"""Test manager diagnostics structure."""
|
|
864
|
+
diagnostics = self.manager.get_manager_diagnostics()
|
|
865
|
+
|
|
866
|
+
# Verify required fields are present
|
|
867
|
+
required_fields = [
|
|
868
|
+
"conda_executable", "mamba_executable", "conda_available", "mamba_available",
|
|
869
|
+
"any_manager_available", "preferred_manager", "platform", "python_version",
|
|
870
|
+
"environments_dir"
|
|
871
|
+
]
|
|
872
|
+
|
|
873
|
+
for field in required_fields:
|
|
874
|
+
self.assertIn(field, diagnostics, f"Missing required field: {field}")
|
|
875
|
+
|
|
876
|
+
# Verify data types
|
|
877
|
+
self.assertIsInstance(diagnostics["conda_available"], bool)
|
|
878
|
+
self.assertIsInstance(diagnostics["mamba_available"], bool)
|
|
879
|
+
self.assertIsInstance(diagnostics["any_manager_available"], bool)
|
|
880
|
+
self.assertIsInstance(diagnostics["platform"], str)
|
|
881
|
+
self.assertIsInstance(diagnostics["python_version"], str)
|
|
882
|
+
self.assertIsInstance(diagnostics["environments_dir"], str)
|