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.
Files changed (93) hide show
  1. hatch/__init__.py +21 -0
  2. hatch/cli_hatch.py +2748 -0
  3. hatch/environment_manager.py +1375 -0
  4. hatch/installers/__init__.py +25 -0
  5. hatch/installers/dependency_installation_orchestrator.py +636 -0
  6. hatch/installers/docker_installer.py +545 -0
  7. hatch/installers/hatch_installer.py +198 -0
  8. hatch/installers/installation_context.py +109 -0
  9. hatch/installers/installer_base.py +195 -0
  10. hatch/installers/python_installer.py +342 -0
  11. hatch/installers/registry.py +179 -0
  12. hatch/installers/system_installer.py +588 -0
  13. hatch/mcp_host_config/__init__.py +38 -0
  14. hatch/mcp_host_config/backup.py +458 -0
  15. hatch/mcp_host_config/host_management.py +572 -0
  16. hatch/mcp_host_config/models.py +602 -0
  17. hatch/mcp_host_config/reporting.py +181 -0
  18. hatch/mcp_host_config/strategies.py +513 -0
  19. hatch/package_loader.py +263 -0
  20. hatch/python_environment_manager.py +734 -0
  21. hatch/registry_explorer.py +171 -0
  22. hatch/registry_retriever.py +335 -0
  23. hatch/template_generator.py +179 -0
  24. hatch_xclam-0.7.0.dist-info/METADATA +150 -0
  25. hatch_xclam-0.7.0.dist-info/RECORD +93 -0
  26. hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
  27. hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
  28. hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
  29. hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
  30. tests/__init__.py +1 -0
  31. tests/run_environment_tests.py +124 -0
  32. tests/test_cli_version.py +122 -0
  33. tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
  34. tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
  35. tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
  36. tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
  37. tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
  38. tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
  39. tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
  40. tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
  41. tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
  42. tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
  43. tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
  44. tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
  45. tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
  46. tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
  47. tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
  48. tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
  49. tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
  50. tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
  51. tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
  52. tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
  53. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
  54. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
  55. tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
  56. tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
  57. tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
  58. tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
  59. tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
  60. tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
  61. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
  62. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
  63. tests/test_data_utils.py +472 -0
  64. tests/test_dependency_orchestrator_consent.py +266 -0
  65. tests/test_docker_installer.py +524 -0
  66. tests/test_env_manip.py +991 -0
  67. tests/test_hatch_installer.py +179 -0
  68. tests/test_installer_base.py +221 -0
  69. tests/test_mcp_atomic_operations.py +276 -0
  70. tests/test_mcp_backup_integration.py +308 -0
  71. tests/test_mcp_cli_all_host_specific_args.py +303 -0
  72. tests/test_mcp_cli_backup_management.py +295 -0
  73. tests/test_mcp_cli_direct_management.py +453 -0
  74. tests/test_mcp_cli_discovery_listing.py +582 -0
  75. tests/test_mcp_cli_host_config_integration.py +823 -0
  76. tests/test_mcp_cli_package_management.py +360 -0
  77. tests/test_mcp_cli_partial_updates.py +859 -0
  78. tests/test_mcp_environment_integration.py +520 -0
  79. tests/test_mcp_host_config_backup.py +257 -0
  80. tests/test_mcp_host_configuration_manager.py +331 -0
  81. tests/test_mcp_host_registry_decorator.py +348 -0
  82. tests/test_mcp_pydantic_architecture_v4.py +603 -0
  83. tests/test_mcp_server_config_models.py +242 -0
  84. tests/test_mcp_server_config_type_field.py +221 -0
  85. tests/test_mcp_sync_functionality.py +316 -0
  86. tests/test_mcp_user_feedback_reporting.py +359 -0
  87. tests/test_non_tty_integration.py +281 -0
  88. tests/test_online_package_loader.py +202 -0
  89. tests/test_python_environment_manager.py +882 -0
  90. tests/test_python_installer.py +327 -0
  91. tests/test_registry.py +51 -0
  92. tests/test_registry_retriever.py +250 -0
  93. 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()