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,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
|
+
|