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,524 @@
|
|
|
1
|
+
"""Tests for DockerInstaller.
|
|
2
|
+
|
|
3
|
+
This module contains comprehensive tests for the DockerInstaller class,
|
|
4
|
+
including unit tests with mocked Docker client and integration tests with
|
|
5
|
+
real Docker images.
|
|
6
|
+
"""
|
|
7
|
+
import unittest
|
|
8
|
+
import tempfile
|
|
9
|
+
import shutil
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import patch, MagicMock, Mock
|
|
12
|
+
from typing import Dict, Any
|
|
13
|
+
|
|
14
|
+
from wobble.decorators import regression_test, integration_test, slow_test
|
|
15
|
+
|
|
16
|
+
from hatch.installers.docker_installer import DockerInstaller, DOCKER_AVAILABLE, DOCKER_DAEMON_AVAILABLE
|
|
17
|
+
from hatch.installers.installer_base import InstallationError
|
|
18
|
+
from hatch.installers.installation_context import InstallationContext, InstallationResult, InstallationStatus
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DummyContext(InstallationContext):
|
|
22
|
+
"""Test implementation of InstallationContext."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, env_path=None, env_name=None, simulation_mode=False, extra_config=None):
|
|
25
|
+
"""Initialize dummy context.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
env_path (Optional[Path]): Environment path.
|
|
29
|
+
env_name (Optional[str]): Environment name.
|
|
30
|
+
simulation_mode (bool): Whether to run in simulation mode.
|
|
31
|
+
extra_config (Optional[Dict]): Extra configuration.
|
|
32
|
+
"""
|
|
33
|
+
self.env_path = env_path or Path("dummy_env")
|
|
34
|
+
self.env_name = env_name or "dummy"
|
|
35
|
+
self.simulation_mode = simulation_mode
|
|
36
|
+
self.extra_config = extra_config or {}
|
|
37
|
+
|
|
38
|
+
def get_config(self, key, default=None):
|
|
39
|
+
"""Get configuration value.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
key (str): Configuration key.
|
|
43
|
+
default: Default value if key not found.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Configuration value or default.
|
|
47
|
+
"""
|
|
48
|
+
return self.extra_config.get(key, default)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestDockerInstaller(unittest.TestCase):
|
|
52
|
+
"""Test suite for DockerInstaller using unittest."""
|
|
53
|
+
|
|
54
|
+
def setUp(self):
|
|
55
|
+
"""Set up test fixtures."""
|
|
56
|
+
self.installer = DockerInstaller()
|
|
57
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
58
|
+
self.context = DummyContext(
|
|
59
|
+
env_path=Path(self.temp_dir),
|
|
60
|
+
simulation_mode=False
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def tearDown(self):
|
|
64
|
+
"""Clean up test fixtures."""
|
|
65
|
+
shutil.rmtree(self.temp_dir)
|
|
66
|
+
|
|
67
|
+
@regression_test
|
|
68
|
+
def test_installer_type(self):
|
|
69
|
+
"""Test installer type property."""
|
|
70
|
+
self.assertEqual(
|
|
71
|
+
self.installer.installer_type, "docker",
|
|
72
|
+
f"Installer type mismatch: expected 'docker', got '{self.installer.installer_type}'"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@regression_test
|
|
76
|
+
def test_supported_schemes(self):
|
|
77
|
+
"""Test supported schemes property."""
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
self.installer.supported_schemes, ["dockerhub"],
|
|
80
|
+
f"Supported schemes mismatch: expected ['dockerhub'], got {self.installer.supported_schemes}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@regression_test
|
|
84
|
+
def test_can_install_valid_dependency(self):
|
|
85
|
+
"""Test can_install with valid Docker dependency."""
|
|
86
|
+
dependency = {
|
|
87
|
+
"name": "nginx",
|
|
88
|
+
"version_constraint": ">=1.25.0",
|
|
89
|
+
"type": "docker",
|
|
90
|
+
"registry": "dockerhub"
|
|
91
|
+
}
|
|
92
|
+
with patch.object(self.installer, '_is_docker_available', return_value=True):
|
|
93
|
+
self.assertTrue(
|
|
94
|
+
self.installer.can_install(dependency),
|
|
95
|
+
f"can_install should return True for valid dependency: {dependency}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@regression_test
|
|
99
|
+
def test_can_install_wrong_type(self):
|
|
100
|
+
"""Test can_install with wrong dependency type."""
|
|
101
|
+
dependency = {
|
|
102
|
+
"name": "requests",
|
|
103
|
+
"version_constraint": ">=2.0.0",
|
|
104
|
+
"type": "python"
|
|
105
|
+
}
|
|
106
|
+
self.assertFalse(
|
|
107
|
+
self.installer.can_install(dependency),
|
|
108
|
+
f"can_install should return False for non-docker dependency: {dependency}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@integration_test(scope="service")
|
|
112
|
+
@unittest.skipUnless(DOCKER_AVAILABLE and DOCKER_DAEMON_AVAILABLE, f"Docker library not available or Docker daemon not available: library={DOCKER_AVAILABLE}, daemon={DOCKER_DAEMON_AVAILABLE}")
|
|
113
|
+
def test_can_install_docker_unavailable(self):
|
|
114
|
+
"""Test can_install when Docker daemon is unavailable."""
|
|
115
|
+
dependency = {
|
|
116
|
+
"name": "nginx",
|
|
117
|
+
"version_constraint": ">=1.25.0",
|
|
118
|
+
"type": "docker"
|
|
119
|
+
}
|
|
120
|
+
with patch.object(self.installer, '_is_docker_available', return_value=False):
|
|
121
|
+
self.assertFalse(
|
|
122
|
+
self.installer.can_install(dependency),
|
|
123
|
+
f"can_install should return False when Docker is unavailable for dependency: {dependency}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@regression_test
|
|
127
|
+
def test_validate_dependency_valid(self):
|
|
128
|
+
"""Test validate_dependency with valid dependency."""
|
|
129
|
+
dependency = {
|
|
130
|
+
"name": "nginx",
|
|
131
|
+
"version_constraint": ">=1.25.0",
|
|
132
|
+
"type": "docker",
|
|
133
|
+
"registry": "dockerhub"
|
|
134
|
+
}
|
|
135
|
+
self.assertTrue(
|
|
136
|
+
self.installer.validate_dependency(dependency),
|
|
137
|
+
f"validate_dependency should return True for valid dependency: {dependency}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@regression_test
|
|
141
|
+
def test_validate_dependency_missing_name(self):
|
|
142
|
+
"""Test validate_dependency with missing name field."""
|
|
143
|
+
dependency = {
|
|
144
|
+
"version_constraint": ">=1.25.0",
|
|
145
|
+
"type": "docker"
|
|
146
|
+
}
|
|
147
|
+
self.assertFalse(
|
|
148
|
+
self.installer.validate_dependency(dependency),
|
|
149
|
+
f"validate_dependency should return False when 'name' is missing: {dependency}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@regression_test
|
|
153
|
+
def test_validate_dependency_missing_version_constraint(self):
|
|
154
|
+
"""Test validate_dependency with missing version_constraint field."""
|
|
155
|
+
dependency = {
|
|
156
|
+
"name": "nginx",
|
|
157
|
+
"type": "docker"
|
|
158
|
+
}
|
|
159
|
+
self.assertFalse(
|
|
160
|
+
self.installer.validate_dependency(dependency),
|
|
161
|
+
f"validate_dependency should return False when 'version_constraint' is missing: {dependency}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@regression_test
|
|
165
|
+
def test_validate_dependency_invalid_type(self):
|
|
166
|
+
"""Test validate_dependency with invalid type."""
|
|
167
|
+
dependency = {
|
|
168
|
+
"name": "nginx",
|
|
169
|
+
"version_constraint": ">=1.25.0",
|
|
170
|
+
"type": "python"
|
|
171
|
+
}
|
|
172
|
+
self.assertFalse(
|
|
173
|
+
self.installer.validate_dependency(dependency),
|
|
174
|
+
f"validate_dependency should return False for invalid type: {dependency}"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@regression_test
|
|
178
|
+
def test_validate_dependency_invalid_registry(self):
|
|
179
|
+
"""Test validate_dependency with unsupported registry."""
|
|
180
|
+
dependency = {
|
|
181
|
+
"name": "nginx",
|
|
182
|
+
"version_constraint": ">=1.25.0",
|
|
183
|
+
"type": "docker",
|
|
184
|
+
"registry": "gcr.io"
|
|
185
|
+
}
|
|
186
|
+
self.assertFalse(
|
|
187
|
+
self.installer.validate_dependency(dependency),
|
|
188
|
+
f"validate_dependency should return False for unsupported registry: {dependency}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@regression_test
|
|
192
|
+
def test_validate_dependency_invalid_version_constraint(self):
|
|
193
|
+
"""Test validate_dependency with invalid version constraint."""
|
|
194
|
+
dependency = {
|
|
195
|
+
"name": "nginx",
|
|
196
|
+
"version_constraint": "invalid_version",
|
|
197
|
+
"type": "docker"
|
|
198
|
+
}
|
|
199
|
+
self.assertFalse(
|
|
200
|
+
self.installer.validate_dependency(dependency),
|
|
201
|
+
f"validate_dependency should return False for invalid version_constraint: {dependency}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@regression_test
|
|
205
|
+
def test_version_constraint_validation(self):
|
|
206
|
+
"""Test various version constraint formats."""
|
|
207
|
+
valid_constraints = [
|
|
208
|
+
"1.25.0",
|
|
209
|
+
">=1.25.0", # Theoretically valid, but Docker works with tags and not version constraints. This is just to ensure the method can handle it.
|
|
210
|
+
"==1.25.0", # Theoretically valid, but Docker works with tags and not version constraints. This is just to ensure the method can handle it.
|
|
211
|
+
"<=2.0.0", # Theoretically valid, but Docker works with tags and not version constraints. This is just to ensure the method can handle it.
|
|
212
|
+
#"!=1.24.0", # Docker works with tags and not version constraint, so this one is really irrelevant
|
|
213
|
+
"latest",
|
|
214
|
+
"1.25",
|
|
215
|
+
"1"
|
|
216
|
+
]
|
|
217
|
+
for constraint in valid_constraints:
|
|
218
|
+
with self.subTest(constraint=constraint):
|
|
219
|
+
self.assertTrue(
|
|
220
|
+
self.installer._validate_version_constraint(constraint),
|
|
221
|
+
f"_validate_version_constraint should return True for valid constraint: '{constraint}'"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
@regression_test
|
|
225
|
+
def test_resolve_docker_tag(self):
|
|
226
|
+
"""Test Docker tag resolution from version constraints."""
|
|
227
|
+
test_cases = [
|
|
228
|
+
("latest", "latest"),
|
|
229
|
+
("1.25.0", "1.25.0"),
|
|
230
|
+
("==1.25.0", "1.25.0"), # Theoretically valid, but Docker works with tags and not version constraints. This is just to ensure the method can handle it.
|
|
231
|
+
(">=1.25.0", "1.25.0"), # Theoretically valid, but Docker works with tags and not version constraints. This is just to ensure the method can handle it.
|
|
232
|
+
("<=1.25.0", "1.25.0"), # Theoretically valid, but Docker works with tags and not version constraints. This is just to ensure the method can handle it.
|
|
233
|
+
#("!=1.24.0", "latest"), # Docker works with tags and not version constraint, so this one is really irrelevant
|
|
234
|
+
]
|
|
235
|
+
for constraint, expected in test_cases:
|
|
236
|
+
with self.subTest(constraint=constraint):
|
|
237
|
+
result = self.installer._resolve_docker_tag(constraint)
|
|
238
|
+
self.assertEqual(
|
|
239
|
+
result, expected,
|
|
240
|
+
f"_resolve_docker_tag('{constraint}') returned '{result}', expected '{expected}'"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@regression_test
|
|
244
|
+
def test_install_simulation_mode(self):
|
|
245
|
+
"""Test installation in simulation mode."""
|
|
246
|
+
dependency = {
|
|
247
|
+
"name": "nginx",
|
|
248
|
+
"version_constraint": ">=1.25.0",
|
|
249
|
+
"type": "docker",
|
|
250
|
+
"registry": "dockerhub"
|
|
251
|
+
}
|
|
252
|
+
simulation_context = DummyContext(simulation_mode=True)
|
|
253
|
+
progress_calls = []
|
|
254
|
+
def progress_callback(message, percent, status):
|
|
255
|
+
progress_calls.append((message, percent, status))
|
|
256
|
+
result = self.installer.install(dependency, simulation_context, progress_callback)
|
|
257
|
+
self.assertEqual(
|
|
258
|
+
result.status, InstallationStatus.COMPLETED,
|
|
259
|
+
f"Simulation install should return COMPLETED, got {result.status} with message: {result.metadata["message"]}"
|
|
260
|
+
)
|
|
261
|
+
self.assertIn(
|
|
262
|
+
"Simulated installation", result.metadata["message"],
|
|
263
|
+
f"Simulation install message should mention 'Simulated installation', got: {result.metadata["message"]}"
|
|
264
|
+
)
|
|
265
|
+
self.assertEqual(
|
|
266
|
+
len(progress_calls), 2,
|
|
267
|
+
f"Simulation install should call progress_callback twice (start and completion), got {len(progress_calls)} calls: {progress_calls}"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@regression_test
|
|
271
|
+
@unittest.skipUnless(DOCKER_AVAILABLE and DOCKER_DAEMON_AVAILABLE, f"Docker library not available or Docker daemon not available: library={DOCKER_AVAILABLE}, daemon={DOCKER_DAEMON_AVAILABLE}")
|
|
272
|
+
@patch('hatch.installers.docker_installer.docker')
|
|
273
|
+
def test_install_success(self, mock_docker):
|
|
274
|
+
"""Test successful Docker image installation."""
|
|
275
|
+
mock_client = Mock()
|
|
276
|
+
mock_docker.from_env.return_value = mock_client
|
|
277
|
+
mock_client.ping.return_value = True
|
|
278
|
+
mock_client.images.pull.return_value = Mock()
|
|
279
|
+
dependency = {
|
|
280
|
+
"name": "nginx",
|
|
281
|
+
"version_constraint": "1.25.0",
|
|
282
|
+
"type": "docker",
|
|
283
|
+
"registry": "dockerhub"
|
|
284
|
+
}
|
|
285
|
+
progress_calls = []
|
|
286
|
+
def progress_callback(message, percent, status):
|
|
287
|
+
progress_calls.append((message, percent, status))
|
|
288
|
+
result = self.installer.install(dependency, self.context, progress_callback)
|
|
289
|
+
self.assertEqual(
|
|
290
|
+
result.status, InstallationStatus.COMPLETED,
|
|
291
|
+
f"Install should return COMPLETED, got {result.status} with message: {result.metadata["message"]}"
|
|
292
|
+
)
|
|
293
|
+
mock_client.images.pull.assert_called_once_with("nginx:1.25.0")
|
|
294
|
+
self.assertGreater(
|
|
295
|
+
len(progress_calls), 0,
|
|
296
|
+
f"Install should call progress_callback at least once, got {len(progress_calls)} calls: {progress_calls}"
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
@regression_test
|
|
300
|
+
@unittest.skipUnless(DOCKER_AVAILABLE and DOCKER_DAEMON_AVAILABLE, f"Docker library not available or Docker daemon not available: library={DOCKER_AVAILABLE}, daemon={DOCKER_DAEMON_AVAILABLE}")
|
|
301
|
+
@patch('hatch.installers.docker_installer.docker')
|
|
302
|
+
def test_install_failure(self, mock_docker):
|
|
303
|
+
"""Test Docker installation failure."""
|
|
304
|
+
mock_client = Mock()
|
|
305
|
+
mock_docker.from_env.return_value = mock_client
|
|
306
|
+
mock_client.ping.return_value = True
|
|
307
|
+
mock_client.images.pull.side_effect = Exception("Network error")
|
|
308
|
+
dependency = {
|
|
309
|
+
"name": "nginx",
|
|
310
|
+
"version_constraint": "1.25.0",
|
|
311
|
+
"type": "docker"
|
|
312
|
+
}
|
|
313
|
+
with self.assertRaises(InstallationError, msg=f"Install should raise InstallationError on failure for dependency: {dependency}"):
|
|
314
|
+
self.installer.install(dependency, self.context)
|
|
315
|
+
|
|
316
|
+
@regression_test
|
|
317
|
+
def test_install_invalid_dependency(self):
|
|
318
|
+
"""Test installation with invalid dependency."""
|
|
319
|
+
dependency = {
|
|
320
|
+
"name": "nginx",
|
|
321
|
+
# Missing version_constraint
|
|
322
|
+
"type": "docker"
|
|
323
|
+
}
|
|
324
|
+
with self.assertRaises(InstallationError, msg=f"Install should raise InstallationError for invalid dependency: {dependency}"):
|
|
325
|
+
self.installer.install(dependency, self.context)
|
|
326
|
+
|
|
327
|
+
@regression_test
|
|
328
|
+
@unittest.skipUnless(DOCKER_AVAILABLE and DOCKER_DAEMON_AVAILABLE, f"Docker library not available or Docker daemon not available: library={DOCKER_AVAILABLE}, daemon={DOCKER_DAEMON_AVAILABLE}")
|
|
329
|
+
@patch('hatch.installers.docker_installer.docker')
|
|
330
|
+
def test_uninstall_success(self, mock_docker):
|
|
331
|
+
"""Test successful Docker image uninstallation."""
|
|
332
|
+
mock_client = Mock()
|
|
333
|
+
mock_docker.from_env.return_value = mock_client
|
|
334
|
+
mock_client.ping.return_value = True
|
|
335
|
+
mock_client.containers.list.return_value = []
|
|
336
|
+
mock_client.images.remove.return_value = None
|
|
337
|
+
dependency = {
|
|
338
|
+
"name": "nginx",
|
|
339
|
+
"version_constraint": "1.25.0",
|
|
340
|
+
"type": "docker",
|
|
341
|
+
"registry": "dockerhub"
|
|
342
|
+
}
|
|
343
|
+
result = self.installer.uninstall(dependency, self.context)
|
|
344
|
+
self.assertEqual(
|
|
345
|
+
result.status, InstallationStatus.COMPLETED,
|
|
346
|
+
f"Uninstall should return COMPLETED, got {result.status} with message: {result.metadata["message"]}"
|
|
347
|
+
)
|
|
348
|
+
mock_client.images.remove.assert_called_once_with("nginx:1.25.0", force=False)
|
|
349
|
+
|
|
350
|
+
@regression_test
|
|
351
|
+
def test_uninstall_simulation_mode(self):
|
|
352
|
+
"""Test uninstallation in simulation mode."""
|
|
353
|
+
dependency = {
|
|
354
|
+
"name": "nginx",
|
|
355
|
+
"version_constraint": "1.25.0",
|
|
356
|
+
"type": "docker",
|
|
357
|
+
"registry": "dockerhub"
|
|
358
|
+
}
|
|
359
|
+
simulation_context = DummyContext(simulation_mode=True)
|
|
360
|
+
result = self.installer.uninstall(dependency, simulation_context)
|
|
361
|
+
self.assertEqual(
|
|
362
|
+
result.status, InstallationStatus.COMPLETED,
|
|
363
|
+
f"Simulation uninstall should return COMPLETED, got {result.status} with message: {result.metadata["message"]}"
|
|
364
|
+
)
|
|
365
|
+
self.assertIn(
|
|
366
|
+
"Simulated removal", result.metadata["message"],
|
|
367
|
+
f"Simulation uninstall message should mention 'Simulated removal', got: {result.metadata["message"]}"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
@regression_test
|
|
371
|
+
def test_get_installation_info_docker_unavailable(self):
|
|
372
|
+
"""Test get_installation_info when Docker is unavailable."""
|
|
373
|
+
dependency = {
|
|
374
|
+
"name": "nginx",
|
|
375
|
+
"version_constraint": "1.25.0",
|
|
376
|
+
"type": "docker"
|
|
377
|
+
}
|
|
378
|
+
with patch.object(self.installer, '_is_docker_available', return_value=False):
|
|
379
|
+
info = self.installer.get_installation_info(dependency, self.context)
|
|
380
|
+
self.assertEqual(
|
|
381
|
+
info["installer_type"], "docker",
|
|
382
|
+
f"get_installation_info: installer_type should be 'docker', got {info['installer_type']}"
|
|
383
|
+
)
|
|
384
|
+
self.assertEqual(
|
|
385
|
+
info["dependency_name"], "nginx",
|
|
386
|
+
f"get_installation_info: dependency_name should be 'nginx', got {info['dependency_name']}"
|
|
387
|
+
)
|
|
388
|
+
self.assertFalse(
|
|
389
|
+
info["docker_available"],
|
|
390
|
+
f"get_installation_info: docker_available should be False, got {info['docker_available']}"
|
|
391
|
+
)
|
|
392
|
+
self.assertFalse(
|
|
393
|
+
info["can_install"],
|
|
394
|
+
f"get_installation_info: can_install should be False, got {info['can_install']}"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@regression_test
|
|
398
|
+
@unittest.skipUnless(DOCKER_AVAILABLE and DOCKER_DAEMON_AVAILABLE, f"Docker library not available or Docker daemon not available: library={DOCKER_AVAILABLE}, daemon={DOCKER_DAEMON_AVAILABLE}")
|
|
399
|
+
@patch('hatch.installers.docker_installer.docker')
|
|
400
|
+
def test_get_installation_info_image_installed(self, mock_docker):
|
|
401
|
+
"""Test get_installation_info for installed image."""
|
|
402
|
+
mock_client = Mock()
|
|
403
|
+
mock_docker.from_env.return_value = mock_client
|
|
404
|
+
mock_client.ping.return_value = True
|
|
405
|
+
mock_image = Mock()
|
|
406
|
+
mock_image.id = "sha256:abc123"
|
|
407
|
+
mock_image.tags = ["nginx:1.25.0"]
|
|
408
|
+
mock_client.images.get.return_value = mock_image
|
|
409
|
+
dependency = {
|
|
410
|
+
"name": "nginx",
|
|
411
|
+
"version_constraint": "1.25.0",
|
|
412
|
+
"type": "docker"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
with patch.object(self.installer, '_is_docker_available', return_value=True):
|
|
416
|
+
info = self.installer.get_installation_info(dependency, self.context)
|
|
417
|
+
|
|
418
|
+
self.assertTrue(info["docker_available"])
|
|
419
|
+
self.assertTrue(info["installed"])
|
|
420
|
+
self.assertEqual(info["image_id"], "sha256:abc123")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
class TestDockerInstallerIntegration(unittest.TestCase):
|
|
424
|
+
"""Integration tests for DockerInstaller using real Docker operations."""
|
|
425
|
+
|
|
426
|
+
def setUp(self):
|
|
427
|
+
"""Set up integration test fixtures."""
|
|
428
|
+
if not DOCKER_AVAILABLE or not DOCKER_DAEMON_AVAILABLE:
|
|
429
|
+
self.skipTest(f"Docker library not available or Docker daemon not available: library={DOCKER_AVAILABLE}, daemon={DOCKER_DAEMON_AVAILABLE}")
|
|
430
|
+
|
|
431
|
+
self.installer = DockerInstaller()
|
|
432
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
433
|
+
self.context = DummyContext(env_path=Path(self.temp_dir))
|
|
434
|
+
|
|
435
|
+
# Check if Docker daemon is actually available
|
|
436
|
+
if not self.installer._is_docker_available():
|
|
437
|
+
self.skipTest("Docker daemon not available")
|
|
438
|
+
|
|
439
|
+
def tearDown(self):
|
|
440
|
+
"""Clean up integration test fixtures."""
|
|
441
|
+
if hasattr(self, 'temp_dir'):
|
|
442
|
+
shutil.rmtree(self.temp_dir)
|
|
443
|
+
|
|
444
|
+
@integration_test(scope="service")
|
|
445
|
+
@slow_test
|
|
446
|
+
def test_docker_daemon_availability(self):
|
|
447
|
+
"""Test Docker daemon availability detection."""
|
|
448
|
+
self.assertTrue(self.installer._is_docker_available())
|
|
449
|
+
|
|
450
|
+
@integration_test(scope="service")
|
|
451
|
+
@slow_test
|
|
452
|
+
def test_install_and_uninstall_small_image(self):
|
|
453
|
+
"""Test installing and uninstalling a small Docker image.
|
|
454
|
+
|
|
455
|
+
This test uses the alpine image which is very small (~5MB) to minimize
|
|
456
|
+
download time and resource usage in CI environments.
|
|
457
|
+
"""
|
|
458
|
+
dependency = {
|
|
459
|
+
"name": "alpine",
|
|
460
|
+
"version_constraint": "latest",
|
|
461
|
+
"type": "docker",
|
|
462
|
+
"registry": "dockerhub"
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
progress_events = []
|
|
466
|
+
|
|
467
|
+
def progress_callback(message, percent, status):
|
|
468
|
+
progress_events.append((message, percent, status))
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
# Test installation
|
|
472
|
+
install_result = self.installer.install(dependency, self.context, progress_callback)
|
|
473
|
+
self.assertEqual(install_result.status, InstallationStatus.COMPLETED)
|
|
474
|
+
self.assertGreater(len(progress_events), 0)
|
|
475
|
+
|
|
476
|
+
# Verify image is installed
|
|
477
|
+
info = self.installer.get_installation_info(dependency, self.context)
|
|
478
|
+
self.assertTrue(info.get("installed", False))
|
|
479
|
+
|
|
480
|
+
# Test uninstallation
|
|
481
|
+
progress_events.clear()
|
|
482
|
+
uninstall_result = self.installer.uninstall(dependency, self.context, progress_callback)
|
|
483
|
+
self.assertEqual(uninstall_result.status, InstallationStatus.COMPLETED)
|
|
484
|
+
|
|
485
|
+
except InstallationError as e:
|
|
486
|
+
if e.error_code == "DOCKER_DAEMON_NOT_AVAILABLE":
|
|
487
|
+
self.skipTest(f"Integration test failed due to Docker/network issues: {e}")
|
|
488
|
+
else:
|
|
489
|
+
raise e
|
|
490
|
+
|
|
491
|
+
@integration_test(scope="service")
|
|
492
|
+
@slow_test
|
|
493
|
+
def test_docker_dep_pkg_integration(self):
|
|
494
|
+
"""Test integration with docker_dep_pkg dummy package.
|
|
495
|
+
|
|
496
|
+
This test validates the installer works with the real dependency format
|
|
497
|
+
from the Hatching-Dev docker_dep_pkg.
|
|
498
|
+
"""
|
|
499
|
+
# Dependency based on docker_dep_pkg/hatch_metadata.json
|
|
500
|
+
dependency = {
|
|
501
|
+
"name": "nginx",
|
|
502
|
+
"version_constraint": ">=1.25.0",
|
|
503
|
+
"type": "docker",
|
|
504
|
+
"registry": "dockerhub"
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
# Test validation
|
|
509
|
+
self.assertTrue(self.installer.validate_dependency(dependency))
|
|
510
|
+
|
|
511
|
+
# Test can_install
|
|
512
|
+
self.assertTrue(self.installer.can_install(dependency))
|
|
513
|
+
|
|
514
|
+
# Test installation info
|
|
515
|
+
info = self.installer.get_installation_info(dependency, self.context)
|
|
516
|
+
self.assertEqual(info["installer_type"], "docker")
|
|
517
|
+
self.assertEqual(info["dependency_name"], "nginx")
|
|
518
|
+
|
|
519
|
+
except Exception as e:
|
|
520
|
+
self.skipTest(f"Docker dep pkg integration test failed: {e}")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
if __name__ == "__main__":
|
|
524
|
+
unittest.main()
|