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,733 @@
1
+ """Tests for SystemInstaller.
2
+
3
+ This module contains comprehensive tests for the SystemInstaller class,
4
+ including unit tests with mocked system calls and integration tests with
5
+ dummy packages.
6
+ """
7
+
8
+ import unittest
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from unittest.mock import patch, MagicMock
13
+ from typing import Dict, Any
14
+
15
+ from wobble.decorators import regression_test, integration_test, slow_test
16
+
17
+ from hatch.installers.system_installer import SystemInstaller
18
+ from hatch.installers.installer_base import InstallationError
19
+ from hatch.installers.installation_context import InstallationContext, InstallationResult, InstallationStatus
20
+
21
+
22
+ class DummyContext(InstallationContext):
23
+ def __init__(self, env_path=None, env_name=None, simulation_mode=False, extra_config=None):
24
+ self.simulation_mode = simulation_mode
25
+ self.extra_config = extra_config or {}
26
+ self.environment_path = env_path
27
+ self.environment_name = env_name
28
+
29
+ def get_config(self, key, default=None):
30
+ return self.extra_config.get(key, default)
31
+
32
+
33
+ class TestSystemInstaller(unittest.TestCase):
34
+ """Test suite for SystemInstaller using unittest."""
35
+
36
+ def setUp(self):
37
+ self.installer = SystemInstaller()
38
+ self.mock_context = DummyContext(
39
+ env_path=Path("/test/env"),
40
+ env_name="test_env",
41
+ simulation_mode=False,
42
+ extra_config={}
43
+ )
44
+
45
+ @regression_test
46
+ def test_installer_type(self):
47
+ self.assertEqual(self.installer.installer_type, "system")
48
+
49
+ @regression_test
50
+ def test_supported_schemes(self):
51
+ self.assertEqual(self.installer.supported_schemes, ["apt"])
52
+
53
+ @regression_test
54
+ def test_can_install_valid_dependency(self):
55
+ dependency = {
56
+ "type": "system",
57
+ "name": "curl",
58
+ "version_constraint": ">=7.0.0",
59
+ "package_manager": "apt"
60
+ }
61
+
62
+ with patch.object(self.installer, '_is_platform_supported', return_value=True), \
63
+ patch.object(self.installer, '_is_apt_available', return_value=True):
64
+ self.assertTrue(self.installer.can_install(dependency))
65
+
66
+ @regression_test
67
+ def test_can_install_wrong_type(self):
68
+ dependency = {
69
+ "type": "python",
70
+ "name": "requests",
71
+ "version_constraint": ">=2.0.0"
72
+ }
73
+
74
+ self.assertFalse(self.installer.can_install(dependency))
75
+
76
+ @regression_test
77
+ def test_can_install_unsupported_platform(self):
78
+ dependency = {
79
+ "type": "system",
80
+ "name": "curl",
81
+ "version_constraint": ">=7.0.0",
82
+ "package_manager": "apt"
83
+ }
84
+
85
+ with patch.object(self.installer, '_is_platform_supported', return_value=False):
86
+ self.assertFalse(self.installer.can_install(dependency))
87
+
88
+ @regression_test
89
+ def test_can_install_apt_not_available(self):
90
+ dependency = {
91
+ "type": "system",
92
+ "name": "curl",
93
+ "version_constraint": ">=7.0.0",
94
+ "package_manager": "apt"
95
+ }
96
+
97
+ with patch.object(self.installer, '_is_platform_supported', return_value=True), \
98
+ patch.object(self.installer, '_is_apt_available', return_value=False):
99
+ self.assertFalse(self.installer.can_install(dependency))
100
+
101
+ @regression_test
102
+ def test_validate_dependency_valid(self):
103
+ dependency = {
104
+ "name": "curl",
105
+ "version_constraint": ">=7.0.0",
106
+ "package_manager": "apt"
107
+ }
108
+
109
+ self.assertTrue(self.installer.validate_dependency(dependency))
110
+
111
+ @regression_test
112
+ def test_validate_dependency_missing_name(self):
113
+ dependency = {
114
+ "version_constraint": ">=7.0.0",
115
+ "package_manager": "apt"
116
+ }
117
+
118
+ self.assertFalse(self.installer.validate_dependency(dependency))
119
+
120
+ @regression_test
121
+ def test_validate_dependency_missing_version_constraint(self):
122
+ dependency = {
123
+ "name": "curl",
124
+ "package_manager": "apt"
125
+ }
126
+
127
+ self.assertFalse(self.installer.validate_dependency(dependency))
128
+
129
+ @regression_test
130
+ def test_validate_dependency_invalid_package_manager(self):
131
+ dependency = {
132
+ "name": "curl",
133
+ "version_constraint": ">=7.0.0",
134
+ "package_manager": "yum"
135
+ }
136
+
137
+ self.assertFalse(self.installer.validate_dependency(dependency))
138
+
139
+ @regression_test
140
+ def test_validate_dependency_invalid_version_constraint(self):
141
+ dependency = {
142
+ "name": "curl",
143
+ "version_constraint": "invalid_version",
144
+ "package_manager": "apt"
145
+ }
146
+
147
+ self.assertFalse(self.installer.validate_dependency(dependency))
148
+
149
+ @regression_test
150
+ @patch('platform.system')
151
+ @patch('pathlib.Path.exists')
152
+ def test_is_platform_supported_debian(self, mock_exists, mock_system):
153
+ """Test platform support detection for Debian."""
154
+ mock_system.return_value = "Linux"
155
+ mock_exists.return_value = True
156
+
157
+ self.assertTrue(self.installer._is_platform_supported())
158
+ mock_exists.assert_called_with()
159
+
160
+ @regression_test
161
+ @patch('platform.system')
162
+ @patch('pathlib.Path.exists')
163
+ @patch('builtins.open')
164
+ def test_is_platform_supported_ubuntu(self, mock_open, mock_exists, mock_system):
165
+ """Test platform support detection for Ubuntu."""
166
+ mock_system.return_value = "Linux"
167
+ mock_exists.return_value = False
168
+
169
+ # Mock os-release file content
170
+ mock_file = MagicMock()
171
+ mock_file.read.return_value = "NAME=\"Ubuntu\"\nVERSION=\"20.04\""
172
+ mock_open.return_value.__enter__.return_value = mock_file
173
+
174
+ self.assertTrue(self.installer._is_platform_supported())
175
+
176
+ @regression_test
177
+ @patch('platform.system')
178
+ @patch('pathlib.Path.exists')
179
+ def test_is_platform_supported_unsupported(self, mock_exists, mock_system):
180
+ """Test platform support detection for unsupported systems."""
181
+ mock_system.return_value = "Windows"
182
+ mock_exists.return_value = False
183
+
184
+ self.assertFalse(self.installer._is_platform_supported())
185
+
186
+ @regression_test
187
+ @patch('shutil.which')
188
+ def test_is_apt_available_true(self, mock_which):
189
+ """Test apt availability detection when apt is available."""
190
+ mock_which.return_value = "/usr/bin/apt"
191
+
192
+ self.assertTrue(self.installer._is_apt_available())
193
+ mock_which.assert_called_once_with("apt")
194
+
195
+ @regression_test
196
+ @patch('shutil.which')
197
+ def test_is_apt_available_false(self, mock_which):
198
+ """Test apt availability detection when apt is not available."""
199
+ mock_which.return_value = None
200
+
201
+ self.assertFalse(self.installer._is_apt_available())
202
+
203
+ @regression_test
204
+ def test_build_apt_command_basic(self):
205
+ """Test building basic apt install command."""
206
+ dependency = {
207
+ "name": "curl",
208
+ "version_constraint": ">=7.0.0",
209
+ "package_manager": "apt"
210
+ }
211
+
212
+ command = self.installer._build_apt_command(dependency, self.mock_context)
213
+ self.assertEqual(command, ["sudo", "apt", "install", "curl"])
214
+
215
+ @regression_test
216
+ def test_build_apt_command_exact_version(self):
217
+ """Test building apt command with exact version constraint."""
218
+ dependency = {
219
+ "name": "curl",
220
+ "version_constraint": "==7.68.0",
221
+ "package_manager": "apt"
222
+ }
223
+
224
+ command = self.installer._build_apt_command(dependency, self.mock_context)
225
+ self.assertEqual(command, ["sudo", "apt", "install", "curl=7.68.0"])
226
+
227
+ @regression_test
228
+ def test_build_apt_command_automated(self):
229
+ """Test building apt command in automated mode."""
230
+ dependency = {
231
+ "name": "curl",
232
+ "version_constraint": ">=7.0.0",
233
+ "package_manager": "apt"
234
+ }
235
+
236
+ self.mock_context.extra_config = {"automated": True}
237
+ command = self.installer._build_apt_command(dependency, self.mock_context)
238
+ self.assertEqual(command, ["sudo", "apt", "install", "-y", "curl"])
239
+
240
+ @regression_test
241
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
242
+ @patch('subprocess.run')
243
+ def test_verify_installation_success(self, mock_run):
244
+ """Test successful installation verification."""
245
+ mock_run.return_value = subprocess.CompletedProcess(
246
+ args=["apt-cache", "policy", "curl"],
247
+ returncode=0,
248
+ stdout="curl:\n Installed: 7.68.0-1ubuntu2.7\n Candidate: 7.68.0-1ubuntu2.7\n Version table:\n *** 7.68.0-1ubuntu2.7 500\n 500 http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages\n 100 /var/lib/dpkg/status",
249
+ stderr=""
250
+ )
251
+
252
+ version = self.installer._verify_installation("curl")
253
+ self.assertTrue(isinstance(version, str) and len(version) > 0, f"Expected a non-empty version string, got: {version}")
254
+
255
+ @regression_test
256
+ @patch('subprocess.run')
257
+ def test_verify_installation_failure(self, mock_run):
258
+ """Test installation verification when package not found."""
259
+ mock_run.side_effect = subprocess.CalledProcessError(1, ["dpkg-query"])
260
+
261
+ version = self.installer._verify_installation("nonexistent")
262
+ self.assertIsNone(version)
263
+
264
+ @regression_test
265
+ def test_parse_apt_error_permission_denied(self):
266
+ """Test parsing permission denied error."""
267
+ error = subprocess.CalledProcessError(
268
+ 1, ["apt", "install", "curl"],
269
+ stderr="E: Could not open lock file - permission denied"
270
+ )
271
+ wrapped_error = InstallationError(
272
+ str(error.stderr),
273
+ dependency_name="curl",
274
+ error_code="APT_INSTALL_FAILED",
275
+ cause=error
276
+ )
277
+ message = self.installer._parse_apt_error(wrapped_error)
278
+ self.assertIn("permission denied", message.lower())
279
+ self.assertIn("sudo", message.lower())
280
+
281
+ @regression_test
282
+ def test_parse_apt_error_package_not_found(self):
283
+ """Test parsing package not found error."""
284
+ error = subprocess.CalledProcessError(
285
+ 100, ["apt", "install", "nonexistent"],
286
+ stderr="E: Unable to locate package nonexistent"
287
+ )
288
+ wrapped_error = InstallationError(
289
+ str(error.stderr),
290
+ dependency_name="nonexistent",
291
+ error_code="APT_INSTALL_FAILED",
292
+ cause=error
293
+ )
294
+ message = self.installer._parse_apt_error(wrapped_error)
295
+ self.assertIn("package not found", message.lower())
296
+ self.assertIn("apt update", message.lower())
297
+
298
+ @regression_test
299
+ def test_parse_apt_error_generic(self):
300
+ """Test parsing generic apt error."""
301
+ error = subprocess.CalledProcessError(
302
+ 1, ["apt", "install", "curl"],
303
+ stderr="Some unknown error occurred"
304
+ )
305
+ wrapped_error = InstallationError(
306
+ str(error.stderr),
307
+ dependency_name="curl",
308
+ error_code="APT_INSTALL_FAILED",
309
+ cause=error
310
+ )
311
+ message = self.installer._parse_apt_error(wrapped_error)
312
+ self.assertIn("apt command failed", message.lower())
313
+ self.assertIn("unknown error", message.lower())
314
+
315
+ @regression_test
316
+ @patch.object(SystemInstaller, 'validate_dependency')
317
+ @patch.object(SystemInstaller, '_build_apt_command')
318
+ @patch.object(SystemInstaller, '_run_apt_subprocess')
319
+ @patch.object(SystemInstaller, '_verify_installation')
320
+ def test_install_success(self, mock_verify, mock_execute, mock_build, mock_validate):
321
+ """Test successful installation."""
322
+ # Setup mocks
323
+ mock_validate.return_value = True
324
+ mock_build.return_value = ["apt", "install", "curl"]
325
+ mock_execute.return_value = 0
326
+ mock_verify.return_value = "7.68.0"
327
+
328
+ dependency = {
329
+ "name": "curl",
330
+ "version_constraint": ">=7.0.0",
331
+ "package_manager": "apt"
332
+ }
333
+
334
+ # Test with progress callback
335
+ progress_calls = []
336
+ def progress_callback(operation, progress, message):
337
+ progress_calls.append((operation, progress, message))
338
+
339
+ result = self.installer.install(dependency, self.mock_context, progress_callback)
340
+
341
+ # Verify result
342
+ self.assertEqual(result.dependency_name, "curl")
343
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
344
+ self.assertEqual(result.installed_version, "7.68.0")
345
+ self.assertEqual(result.metadata["package_manager"], "apt")
346
+
347
+ # Verify progress was reported
348
+ self.assertEqual(len(progress_calls), 4)
349
+ self.assertEqual(progress_calls[0][1], 0.0) # Start
350
+ self.assertEqual(progress_calls[-1][1], 100.0) # Complete
351
+
352
+ @regression_test
353
+ @patch.object(SystemInstaller, 'validate_dependency')
354
+ def test_install_invalid_dependency(self, mock_validate):
355
+ """Test installation with invalid dependency."""
356
+ mock_validate.return_value = False
357
+
358
+ dependency = {
359
+ "name": "curl",
360
+ "version_constraint": "invalid"
361
+ }
362
+
363
+ with self.assertRaises(InstallationError) as exc_info:
364
+ self.installer.install(dependency, self.mock_context)
365
+
366
+ self.assertEqual(exc_info.exception.error_code, "INVALID_DEPENDENCY")
367
+ self.assertIn("Invalid dependency", str(exc_info.exception))
368
+
369
+ @regression_test
370
+ @patch.object(SystemInstaller, 'validate_dependency')
371
+ @patch.object(SystemInstaller, '_build_apt_command')
372
+ @patch.object(SystemInstaller, '_run_apt_subprocess')
373
+ def test_install_apt_failure(self, mock_execute, mock_build, mock_validate):
374
+ """Test installation failure due to apt command error."""
375
+ mock_validate.return_value = True
376
+ mock_build.return_value = ["apt", "install", "curl"]
377
+ # Simulate failure on the first call (apt-get update)
378
+ mock_execute.side_effect = [1, 0]
379
+
380
+ dependency = {
381
+ "name": "curl",
382
+ "version_constraint": ">=7.0.0",
383
+ "package_manager": "apt"
384
+ }
385
+
386
+ with self.assertRaises(InstallationError) as exc_info:
387
+ self.installer.install(dependency, self.mock_context)
388
+
389
+ # Accept either update or install failure
390
+ self.assertEqual(exc_info.exception.error_code, "APT_UPDATE_FAILED")
391
+ self.assertEqual(exc_info.exception.dependency_name, "curl")
392
+
393
+ # Now simulate update success but install failure
394
+ mock_execute.side_effect = [0, 1]
395
+ with self.assertRaises(InstallationError) as exc_info2:
396
+ self.installer.install(dependency, self.mock_context)
397
+ self.assertEqual(exc_info2.exception.error_code, "APT_INSTALL_FAILED")
398
+ self.assertEqual(exc_info2.exception.dependency_name, "curl")
399
+
400
+ @regression_test
401
+ @patch.object(SystemInstaller, 'validate_dependency')
402
+ @patch.object(SystemInstaller, '_simulate_installation')
403
+ def test_install_simulation_mode(self, mock_simulate, mock_validate):
404
+ """Test installation in simulation mode."""
405
+ mock_validate.return_value = True
406
+ mock_simulate.return_value = InstallationResult(
407
+ dependency_name="curl",
408
+ status=InstallationStatus.COMPLETED,
409
+ metadata={"simulation": True}
410
+ )
411
+
412
+ self.mock_context.simulation_mode = True
413
+ dependency = {
414
+ "name": "curl",
415
+ "version_constraint": ">=7.0.0",
416
+ "package_manager": "apt"
417
+ }
418
+
419
+ result = self.installer.install(dependency, self.mock_context)
420
+
421
+ self.assertEqual(result.dependency_name, "curl")
422
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
423
+ self.assertTrue(result.metadata["simulation"])
424
+ mock_simulate.assert_called_once()
425
+
426
+ @regression_test
427
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
428
+ @patch.object(SystemInstaller, '_run_apt_subprocess')
429
+ def test_simulate_installation_success(self, mock_run):
430
+ """Test successful installation simulation."""
431
+ mock_run.return_value = 0
432
+
433
+ dependency = {
434
+ "name": "curl",
435
+ "version_constraint": ">=7.0.0",
436
+ "package_manager": "apt"
437
+ }
438
+
439
+ result = self.installer._simulate_installation(dependency, self.mock_context)
440
+
441
+ self.assertEqual(result.dependency_name, "curl")
442
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
443
+ self.assertTrue(result.metadata["simulation"])
444
+
445
+ @regression_test
446
+ @patch.object(SystemInstaller, '_run_apt_subprocess')
447
+ def test_simulate_installation_failure(self, mock_run):
448
+ """Test installation simulation failure."""
449
+ mock_run.return_value = 1
450
+ mock_run.side_effect = InstallationError(
451
+ "Simulation failed",
452
+ dependency_name="nonexistent",
453
+ error_code="APT_SIMULATION_FAILED"
454
+ )
455
+
456
+ dependency = {
457
+ "name": "nonexistent",
458
+ "version_constraint": ">=1.0.0",
459
+ "package_manager": "apt"
460
+ }
461
+
462
+ with self.assertRaises(InstallationError) as exc_info:
463
+ self.installer._simulate_installation(dependency, self.mock_context)
464
+
465
+ self.assertEqual(exc_info.exception.dependency_name, "nonexistent")
466
+ self.assertEqual(exc_info.exception.error_code, "APT_SIMULATION_FAILED")
467
+
468
+ @regression_test
469
+ @patch.object(SystemInstaller, '_run_apt_subprocess', return_value=0)
470
+ def test_uninstall_success(self, mock_execute):
471
+ """Test successful uninstall."""
472
+
473
+ dependency = {
474
+ "name": "curl",
475
+ "version_constraint": ">=7.0.0",
476
+ "package_manager": "apt"
477
+ }
478
+
479
+ result = self.installer.uninstall(dependency, self.mock_context)
480
+
481
+ self.assertEqual(result.dependency_name, "curl")
482
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
483
+ self.assertEqual(result.metadata["operation"], "uninstall")
484
+
485
+ @regression_test
486
+ @patch.object(SystemInstaller, '_run_apt_subprocess', return_value=0)
487
+ def test_uninstall_automated(self, mock_execute):
488
+ """Test uninstall in automated mode."""
489
+
490
+ self.mock_context.extra_config = {"automated": True}
491
+ dependency = {
492
+ "name": "curl",
493
+ "version_constraint": ">=7.0.0",
494
+ "package_manager": "apt"
495
+ }
496
+
497
+ result = self.installer.uninstall(dependency, self.mock_context)
498
+
499
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
500
+ # Verify -y flag is in the command (final command is in the metadata)
501
+ self.assertIn("-y", result.metadata.get("command_executed", []))
502
+
503
+ @regression_test
504
+ @patch.object(SystemInstaller, '_simulate_uninstall')
505
+ def test_uninstall_simulation_mode(self, mock_simulate):
506
+ """Test uninstall in simulation mode."""
507
+ mock_simulate.return_value = InstallationResult(
508
+ dependency_name="curl",
509
+ status=InstallationStatus.COMPLETED,
510
+ metadata={"operation": "uninstall", "simulation": True}
511
+ )
512
+
513
+ self.mock_context.simulation_mode = True
514
+ dependency = {
515
+ "name": "curl",
516
+ "version_constraint": ">=7.0.0",
517
+ "package_manager": "apt"
518
+ }
519
+
520
+ result = self.installer.uninstall(dependency, self.mock_context)
521
+
522
+ self.assertEqual(result.dependency_name, "curl")
523
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
524
+ self.assertTrue(result.metadata["simulation"])
525
+ mock_simulate.assert_called_once()
526
+
527
+
528
+ class TestSystemInstallerIntegration(unittest.TestCase):
529
+ """Integration tests for SystemInstaller using actual system dependencies."""
530
+
531
+ def setUp(self):
532
+ """Set up integration test fixtures."""
533
+ self.installer = SystemInstaller()
534
+ self.test_context = InstallationContext(
535
+ environment_path=Path("/tmp/test_env"),
536
+ environment_name="integration_test",
537
+ simulation_mode=True, # Always use simulation for integration tests
538
+ extra_config={"automated": True}
539
+ )
540
+
541
+
542
+
543
+ @integration_test(scope="system")
544
+ @slow_test
545
+ def test_validate_real_system_dependency(self):
546
+ """Test validation with real system dependency from dummy package."""
547
+ # This mimics the dependency from system_dep_pkg
548
+ dependency = {
549
+ "name": "curl",
550
+ "version_constraint": ">=7.0.0",
551
+ "package_manager": "apt"
552
+ }
553
+
554
+ self.assertTrue(self.installer.validate_dependency(dependency))
555
+
556
+ @integration_test(scope="system")
557
+ @slow_test
558
+ @patch.object(SystemInstaller, '_is_platform_supported')
559
+ @patch.object(SystemInstaller, '_is_apt_available')
560
+ def test_can_install_real_dependency(self, mock_apt_available, mock_platform_supported):
561
+ """Test can_install with real system dependency."""
562
+ mock_platform_supported.return_value = True
563
+ mock_apt_available.return_value = True
564
+
565
+ dependency = {
566
+ "type": "system",
567
+ "name": "curl",
568
+ "version_constraint": ">=7.0.0",
569
+ "package_manager": "apt"
570
+ }
571
+
572
+ self.assertTrue(self.installer.can_install(dependency))
573
+
574
+ @integration_test(scope="system")
575
+ @slow_test
576
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
577
+ def test_simulate_curl_installation(self):
578
+ """Test simulating installation of curl package."""
579
+ dependency = {
580
+ "name": "curl",
581
+ "version_constraint": ">=7.0.0",
582
+ "package_manager": "apt"
583
+ }
584
+
585
+ # Mock subprocess for simulation
586
+ with patch.object(self.installer, '_run_apt_subprocess') as mock_run:
587
+ mock_run.return_value = 0
588
+
589
+ result = self.installer._simulate_installation(dependency, self.test_context)
590
+
591
+ self.assertEqual(result.dependency_name, "curl")
592
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
593
+ self.assertTrue(result.metadata["simulation"])
594
+
595
+ @integration_test(scope="system")
596
+ @slow_test
597
+ def test_get_installation_info(self):
598
+ """Test getting installation info for system dependency."""
599
+ dependency = {
600
+ "type": "system",
601
+ "name": "curl",
602
+ "version_constraint": ">=7.0.0",
603
+ "package_manager": "apt"
604
+ }
605
+
606
+ with patch.object(self.installer, 'can_install', return_value=True):
607
+ info = self.installer.get_installation_info(dependency, self.test_context)
608
+
609
+ self.assertEqual(info["installer_type"], "system")
610
+ self.assertEqual(info["dependency_name"], "curl")
611
+ self.assertTrue(info["supported"])
612
+
613
+ @integration_test(scope="system")
614
+ @slow_test
615
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
616
+ def test_install_real_dependency(self):
617
+ """Test installing a real system dependency."""
618
+ dependency = {
619
+ "name": "sl", # Use a rarer package than 'curl'
620
+ "version_constraint": ">=5.02",
621
+ "package_manager": "apt"
622
+ }
623
+
624
+ # real installation
625
+ result = self.installer.install(dependency, self.test_context)
626
+
627
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
628
+ self.assertTrue(result.metadata["automated"])
629
+
630
+ @integration_test(scope="system")
631
+ @slow_test
632
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
633
+ def test_install_integration_with_real_subprocess(self):
634
+ """Test install method with real _run_apt_subprocess execution.
635
+
636
+ This integration test ensures that _run_apt_subprocess can actually run
637
+ without mocking, using apt-get --dry-run for safe testing.
638
+ """
639
+ dependency = {
640
+ "name": "curl",
641
+ "version_constraint": ">=7.0.0",
642
+ "package_manager": "apt"
643
+ }
644
+
645
+ # Create a test context that uses simulation mode for safety
646
+ test_context = InstallationContext(
647
+ environment_path=Path("/tmp/test_env"),
648
+ environment_name="integration_test",
649
+ simulation_mode=True,
650
+ extra_config={"automated": True}
651
+ )
652
+
653
+ # This will call _run_apt_subprocess with real subprocess execution
654
+ # but in simulation mode, so it's safe
655
+ result = self.installer.install(dependency, test_context)
656
+
657
+ self.assertEqual(result.dependency_name, "curl")
658
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
659
+ self.assertTrue(result.metadata["simulation"])
660
+ self.assertEqual(result.metadata["package_manager"], "apt")
661
+ self.assertTrue(result.metadata["automated"])
662
+
663
+ @integration_test(scope="system")
664
+ @slow_test
665
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
666
+ def test_run_apt_subprocess_direct_integration(self):
667
+ """Test _run_apt_subprocess directly with real system commands.
668
+
669
+ This test verifies that _run_apt_subprocess can handle actual apt commands
670
+ without any mocking, using safe commands that don't modify the system.
671
+ """
672
+ # Test with apt-cache policy (read-only command)
673
+ cmd = ["apt-cache", "policy", "curl"]
674
+ returncode = self.installer._run_apt_subprocess(cmd)
675
+
676
+ # Should return 0 (success) for a valid package query
677
+ self.assertEqual(returncode, 0)
678
+
679
+ # Test with apt-get dry-run (safe simulation command)
680
+ cmd = ["apt-get", "install", "--dry-run", "-y", "curl"]
681
+ returncode = self.installer._run_apt_subprocess(cmd)
682
+
683
+ # Should return 0 (success) for a valid dry-run
684
+ self.assertEqual(returncode, 0)
685
+
686
+ # Test with invalid package (should fail gracefully)
687
+ cmd = ["apt-cache", "policy", "nonexistent-package-12345"]
688
+ returncode = self.installer._run_apt_subprocess(cmd)
689
+
690
+ # Should return 0 even for non-existent package (apt-cache policy doesn't fail)
691
+ self.assertEqual(returncode, 0)
692
+
693
+ @integration_test(scope="system")
694
+ @slow_test
695
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
696
+ def test_install_with_version_constraint_integration(self):
697
+ """Test install method with version constraints and real subprocess calls."""
698
+ # Test with exact version constraint
699
+ dependency = {
700
+ "name": "curl",
701
+ "version_constraint": "==7.68.0",
702
+ "package_manager": "apt"
703
+ }
704
+
705
+ test_context = InstallationContext(
706
+ environment_path=Path("/tmp/test_env"),
707
+ environment_name="integration_test",
708
+ simulation_mode=True,
709
+ extra_config={"automated": True}
710
+ )
711
+
712
+ result = self.installer.install(dependency, test_context)
713
+
714
+ self.assertEqual(result.dependency_name, "curl")
715
+ self.assertEqual(result.status, InstallationStatus.COMPLETED)
716
+ self.assertTrue(result.metadata["simulation"])
717
+ # Check that the command includes the version constraint
718
+ self.assertIn("curl", result.metadata["command_simulated"])
719
+
720
+ @integration_test(scope="system")
721
+ @slow_test
722
+ @unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
723
+ def test_error_handling_in_run_apt_subprocess(self):
724
+ """Test error handling in _run_apt_subprocess with real commands."""
725
+ # Test with completely invalid command
726
+ cmd = ["nonexistent-command-12345"]
727
+
728
+ with self.assertRaises(InstallationError) as exc_info:
729
+ self.installer._run_apt_subprocess(cmd)
730
+
731
+ self.assertEqual(exc_info.exception.error_code, "APT_SUBPROCESS_ERROR")
732
+ self.assertIn("Unexpected error running apt command", exc_info.exception.message)
733
+