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,281 @@
1
+ """Integration tests for non-TTY handling across the full workflow.
2
+
3
+ This module tests the complete integration of non-TTY handling from CLI
4
+ through to the dependency installation orchestrator, ensuring the full
5
+ workflow operates correctly in both TTY and non-TTY environments.
6
+ """
7
+
8
+ import unittest
9
+ import tempfile
10
+ import os
11
+ from pathlib import Path
12
+ from unittest.mock import patch
13
+ from hatch.environment_manager import HatchEnvironmentManager
14
+ from wobble.decorators import integration_test, slow_test
15
+ from test_data_utils import NonTTYTestDataLoader, TestDataLoader
16
+
17
+
18
+ class TestNonTTYIntegration(unittest.TestCase):
19
+ """Integration tests for non-TTY handling across the full workflow."""
20
+
21
+ def setUp(self):
22
+ """Set up integration test environment with centralized test data."""
23
+ self.temp_dir = tempfile.mkdtemp()
24
+ self.env_manager = HatchEnvironmentManager(
25
+ environments_dir=Path(self.temp_dir) / "envs",
26
+ simulation_mode=True
27
+ )
28
+ self.test_data = NonTTYTestDataLoader()
29
+ self.addCleanup(self._cleanup_temp_dir)
30
+
31
+ def _cleanup_temp_dir(self):
32
+ """Clean up temporary directory."""
33
+ import shutil
34
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
35
+
36
+ @integration_test(scope="component")
37
+ @slow_test
38
+ @patch('sys.stdin.isatty', return_value=False)
39
+ def test_cli_package_add_non_tty(self, mock_isatty):
40
+ """Test package addition in non-TTY environment via CLI."""
41
+ # Create test environment
42
+ self.env_manager.create_environment("test_env", "Test environment")
43
+
44
+ # Test package addition without hanging
45
+ test_loader = TestDataLoader()
46
+ pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
47
+
48
+ # Ensure the test package exists
49
+ if not pkg_path.exists():
50
+ self.skipTest(f"Test package not found: {pkg_path}")
51
+
52
+ result = self.env_manager.add_package_to_environment(
53
+ str(pkg_path),
54
+ "test_env",
55
+ auto_approve=False # Test environment variable handling
56
+ )
57
+
58
+ self.assertTrue(result, "Package addition should succeed in non-TTY mode")
59
+ mock_isatty.assert_called()
60
+
61
+ @integration_test(scope="component")
62
+ @slow_test
63
+ @patch.dict(os.environ, {'HATCH_AUTO_APPROVE': '1'})
64
+ def test_environment_variable_integration(self):
65
+ """Test HATCH_AUTO_APPROVE environment variable integration."""
66
+ # Create test environment
67
+ self.env_manager.create_environment("test_env", "Test environment")
68
+
69
+ # Test with centralized test data
70
+ test_loader = TestDataLoader()
71
+ pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
72
+
73
+ # Ensure the test package exists
74
+ if not pkg_path.exists():
75
+ self.skipTest(f"Test package not found: {pkg_path}")
76
+
77
+ result = self.env_manager.add_package_to_environment(
78
+ str(pkg_path),
79
+ "test_env",
80
+ auto_approve=False # Environment variable should override
81
+ )
82
+
83
+ self.assertTrue(result, "Package addition should succeed with HATCH_AUTO_APPROVE")
84
+
85
+ @integration_test(scope="component")
86
+ @slow_test
87
+ @patch('sys.stdin.isatty', return_value=False)
88
+ def test_multiple_package_installation_non_tty(self, mock_isatty):
89
+ """Test multiple package installation in non-TTY environment."""
90
+ # Create test environment
91
+ self.env_manager.create_environment("test_env", "Test environment")
92
+
93
+ test_loader = TestDataLoader()
94
+
95
+ # Install first package
96
+ base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
97
+ if base_pkg_path.exists():
98
+ result1 = self.env_manager.add_package_to_environment(
99
+ str(base_pkg_path),
100
+ "test_env",
101
+ auto_approve=False
102
+ )
103
+ self.assertTrue(result1, "First package installation should succeed")
104
+
105
+ # Install second package
106
+ utility_pkg_path = test_loader.packages_dir / "basic" / "utility_pkg"
107
+ if utility_pkg_path.exists():
108
+ result2 = self.env_manager.add_package_to_environment(
109
+ str(utility_pkg_path),
110
+ "test_env",
111
+ auto_approve=False
112
+ )
113
+ self.assertTrue(result2, "Second package installation should succeed")
114
+
115
+ @integration_test(scope="component")
116
+ @slow_test
117
+ @patch.dict(os.environ, {'HATCH_AUTO_APPROVE': 'true'})
118
+ def test_environment_variable_case_insensitive_integration(self):
119
+ """Test case-insensitive environment variable in full integration."""
120
+ # Create test environment
121
+ self.env_manager.create_environment("test_env", "Test environment")
122
+
123
+ test_loader = TestDataLoader()
124
+ pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
125
+
126
+ if not pkg_path.exists():
127
+ self.skipTest(f"Test package not found: {pkg_path}")
128
+
129
+ result = self.env_manager.add_package_to_environment(
130
+ str(pkg_path),
131
+ "test_env",
132
+ auto_approve=False
133
+ )
134
+
135
+ self.assertTrue(result, "Package addition should succeed with case-insensitive env var")
136
+
137
+ @integration_test(scope="component")
138
+ @slow_test
139
+ @patch('sys.stdin.isatty', return_value=True)
140
+ @patch.dict(os.environ, {'HATCH_AUTO_APPROVE': 'invalid'})
141
+ @patch('builtins.input', return_value='y')
142
+ def test_invalid_environment_variable_fallback_integration(self, mock_input, mock_isatty):
143
+ """Test fallback to interactive mode with invalid environment variable."""
144
+ # Create test environment
145
+ self.env_manager.create_environment("test_env", "Test environment")
146
+
147
+ test_loader = TestDataLoader()
148
+ pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
149
+
150
+ if not pkg_path.exists():
151
+ self.skipTest(f"Test package not found: {pkg_path}")
152
+
153
+ result = self.env_manager.add_package_to_environment(
154
+ str(pkg_path),
155
+ "test_env",
156
+ auto_approve=False
157
+ )
158
+
159
+ self.assertTrue(result, "Package addition should succeed with user approval")
160
+ # Verify that input was called (fallback to interactive mode)
161
+ mock_input.assert_called()
162
+
163
+
164
+ class TestNonTTYErrorScenarios(unittest.TestCase):
165
+ """Test error scenarios in non-TTY environments."""
166
+
167
+ def setUp(self):
168
+ """Set up test environment."""
169
+ self.temp_dir = tempfile.mkdtemp()
170
+ self.env_manager = HatchEnvironmentManager(
171
+ environments_dir=Path(self.temp_dir) / "envs",
172
+ simulation_mode=True
173
+ )
174
+ self.test_data = NonTTYTestDataLoader()
175
+ self.addCleanup(self._cleanup_temp_dir)
176
+
177
+ def _cleanup_temp_dir(self):
178
+ """Clean up temporary directory."""
179
+ import shutil
180
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
181
+
182
+ @integration_test(scope="component")
183
+ @slow_test
184
+ @patch('sys.stdin.isatty', return_value=True)
185
+ @patch('builtins.input', side_effect=KeyboardInterrupt())
186
+ def test_keyboard_interrupt_integration(self, mock_input, mock_isatty):
187
+ """Test KeyboardInterrupt handling in full integration."""
188
+ # Create test environment
189
+ self.env_manager.create_environment("test_env", "Test environment")
190
+
191
+ test_loader = TestDataLoader()
192
+ pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
193
+
194
+ if not pkg_path.exists():
195
+ self.skipTest(f"Test package not found: {pkg_path}")
196
+
197
+ result = self.env_manager.add_package_to_environment(
198
+ str(pkg_path),
199
+ "test_env",
200
+ auto_approve=False
201
+ )
202
+
203
+ # Should return False due to user cancellation
204
+ self.assertFalse(result, "Package installation should be cancelled by user")
205
+
206
+ @integration_test(scope="component")
207
+ @slow_test
208
+ @patch('sys.stdin.isatty', return_value=True)
209
+ @patch('builtins.input', side_effect=EOFError())
210
+ def test_eof_error_integration(self, mock_input, mock_isatty):
211
+ """Test EOFError handling in full integration."""
212
+ # Create test environment
213
+ self.env_manager.create_environment("test_env", "Test environment")
214
+
215
+ test_loader = TestDataLoader()
216
+ pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
217
+
218
+ if not pkg_path.exists():
219
+ self.skipTest(f"Test package not found: {pkg_path}")
220
+
221
+ result = self.env_manager.add_package_to_environment(
222
+ str(pkg_path),
223
+ "test_env",
224
+ auto_approve=False
225
+ )
226
+
227
+ # Should return False due to EOF error
228
+ self.assertFalse(result, "Package installation should be cancelled due to EOF")
229
+
230
+
231
+ class TestEnvironmentVariableIntegrationScenarios(unittest.TestCase):
232
+ """Test comprehensive environment variable scenarios in full integration."""
233
+
234
+ def setUp(self):
235
+ """Set up test environment."""
236
+ self.temp_dir = tempfile.mkdtemp()
237
+ self.env_manager = HatchEnvironmentManager(
238
+ environments_dir=Path(self.temp_dir) / "envs",
239
+ simulation_mode=True
240
+ )
241
+ self.test_data = NonTTYTestDataLoader()
242
+ self.addCleanup(self._cleanup_temp_dir)
243
+
244
+ def _cleanup_temp_dir(self):
245
+ """Clean up temporary directory."""
246
+ import shutil
247
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
248
+
249
+ @integration_test(scope="component")
250
+ @slow_test
251
+ def test_all_valid_environment_variables_integration(self):
252
+ """Test all valid environment variable values in integration."""
253
+ # Create test environment
254
+ self.env_manager.create_environment("test_env", "Test environment")
255
+
256
+ test_loader = TestDataLoader()
257
+ pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
258
+
259
+ if not pkg_path.exists():
260
+ self.skipTest(f"Test package not found: {pkg_path}")
261
+
262
+ # Test all valid environment variable values
263
+ valid_values = ["1", "true", "yes", "TRUE", "YES", "True"]
264
+
265
+ for i, value in enumerate(valid_values):
266
+ with self.subTest(env_value=value):
267
+ env_name = f"test_env_{i}"
268
+ self.env_manager.create_environment(env_name, f"Test environment {i}")
269
+
270
+ with patch.dict(os.environ, {'HATCH_AUTO_APPROVE': value}):
271
+ result = self.env_manager.add_package_to_environment(
272
+ str(pkg_path),
273
+ env_name,
274
+ auto_approve=False
275
+ )
276
+
277
+ self.assertTrue(result, f"Package installation should succeed with env var: {value}")
278
+
279
+
280
+ if __name__ == '__main__':
281
+ unittest.main()
@@ -0,0 +1,202 @@
1
+ import sys
2
+ import unittest
3
+ import tempfile
4
+ import shutil
5
+ import logging
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from wobble.decorators import regression_test, integration_test, slow_test
11
+
12
+ # Import path management removed - using test_data_utils for test dependencies
13
+
14
+ from hatch.environment_manager import HatchEnvironmentManager
15
+ from hatch.package_loader import HatchPackageLoader, PackageLoaderError
16
+ from hatch.registry_retriever import RegistryRetriever
17
+ from hatch.registry_explorer import find_package, get_package_release_url
18
+
19
+ # Configure logging
20
+ logging.basicConfig(
21
+ level=logging.DEBUG,
22
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
23
+ )
24
+ logger = logging.getLogger("hatch.package_loader_tests")
25
+
26
+ class OnlinePackageLoaderTests(unittest.TestCase):
27
+ """Tests for package downloading and caching functionality using online mode."""
28
+
29
+ def setUp(self):
30
+ """Set up test environment before each test."""
31
+ # Create temporary directories
32
+ self.temp_dir = tempfile.mkdtemp()
33
+ self.cache_dir = Path(self.temp_dir) / "cache"
34
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
35
+ self.env_dir = Path(self.temp_dir) / "envs"
36
+ self.env_dir.mkdir(parents=True, exist_ok=True)
37
+
38
+ # Initialize registry retriever in online mode
39
+ self.retriever = RegistryRetriever(
40
+ local_cache_dir=self.cache_dir,
41
+ simulation_mode=False # Use online mode
42
+ )
43
+
44
+ # Get registry data for test packages
45
+ self.registry_data = self.retriever.get_registry()
46
+
47
+ # Initialize package loader (needed for some lower-level tests)
48
+ self.package_loader = HatchPackageLoader(cache_dir=self.cache_dir)
49
+
50
+ # Initialize environment manager
51
+ self.env_manager = HatchEnvironmentManager(
52
+ environments_dir=self.env_dir,
53
+ cache_dir=self.cache_dir,
54
+ simulation_mode=False
55
+ )
56
+
57
+ def tearDown(self):
58
+ """Clean up test environment after each test."""
59
+ # Remove temporary directory
60
+ shutil.rmtree(self.temp_dir)
61
+
62
+ @integration_test(scope="service")
63
+ @slow_test
64
+ def test_download_package_online(self):
65
+ """Test downloading a package from online registry."""
66
+ # Use base_pkg_1 for testing since it's mentioned as a reliable test package
67
+ package_name = "base_pkg_1"
68
+ version = "==1.0.1"
69
+
70
+ # Add package to environment using the environment manager
71
+ result = self.env_manager.add_package_to_environment(
72
+ package_name,
73
+ version_constraint=version,
74
+ auto_approve=True # Automatically approve installation in tests
75
+ )
76
+ self.assertTrue(result, f"Failed to add package {package_name}@{version} to environment")
77
+
78
+ # Verify package is in environment
79
+ current_env = self.env_manager.get_current_environment()
80
+ env_data = self.env_manager.get_current_environment_data()
81
+ installed_packages = {pkg["name"]: pkg["version"] for pkg in env_data.get("packages", [])}
82
+ self.assertIn(package_name, installed_packages, f"Package {package_name} not found in environment")
83
+
84
+ # def test_multiple_package_versions(self):
85
+ # """Test downloading multiple versions of the same package."""
86
+ # package_name = "base_pkg_1"
87
+ # versions = ["1.0.0", "1.1.0"] # Test multiple versions if available
88
+
89
+ # # Find package data in the registry
90
+ # package_data = find_package(self.registry_data, package_name)
91
+ # self.assertIsNotNone(package_data, f"Package '{package_name}' not found in registry")
92
+
93
+ # # Try to download each version
94
+ # for version in versions:
95
+ # try:
96
+ # # Get package URL
97
+ # package_url = get_package_release_url(package_data, version)
98
+ # if package_url:
99
+ # # Download the package
100
+ # cached_path = self.package_loader.download_package(package_url, package_name, version)
101
+ # self.assertTrue(cached_path.exists(), f"Package download failed for {version}")
102
+ # logger.info(f"Successfully downloaded {package_name}@{version}")
103
+ # except Exception as e:
104
+ # logger.warning(f"Couldn't download {package_name}@{version}: {e}")
105
+
106
+ @integration_test(scope="service")
107
+ @slow_test
108
+ def test_install_and_caching(self):
109
+ """Test installing and caching a package."""
110
+ package_name = "base_pkg_1"
111
+ version = "1.0.1"
112
+ version_constraint = f"=={version}"
113
+
114
+ # Find package in registry
115
+ package_data = find_package(self.registry_data, package_name)
116
+ self.assertIsNotNone(package_data, f"Package {package_name} not found in registry")
117
+
118
+ # Create a specific test environment for this test
119
+ test_env_name = "test_install_env"
120
+ self.env_manager.create_environment(test_env_name, "Test environment for installation test")
121
+
122
+ # Add the package to the environment
123
+ try:
124
+ result = self.env_manager.add_package_to_environment(
125
+ package_name,
126
+ env_name=test_env_name,
127
+ version_constraint=version_constraint,
128
+ auto_approve=True # Automatically approve installation in tests
129
+ )
130
+
131
+ self.assertTrue(result, f"Failed to add package {package_name}@{version_constraint} to environment")
132
+
133
+ # Get environment path
134
+ env_path = self.env_manager.get_environment_path(test_env_name)
135
+ installed_path = env_path / package_name
136
+
137
+ # Verify installation
138
+ self.assertTrue(installed_path.exists(), f"Package not installed to environment directory: {installed_path}")
139
+ self.assertTrue((installed_path / "hatch_metadata.json").exists(), f"Installation missing metadata file: {installed_path / 'hatch_metadata.json'}")
140
+
141
+ # Verify the cache contains the package
142
+ cache_path = self.cache_dir / "packages" / f"{package_name}-{version}"
143
+ self.assertTrue(cache_path.exists(), f"Package not cached during installation: {cache_path}")
144
+ self.assertTrue((cache_path / "hatch_metadata.json").exists(), f"Cache missing metadata file: {cache_path / 'hatch_metadata.json'}")
145
+
146
+ logger.info(f"Successfully installed and cached package: {package_name}@{version}")
147
+ except Exception as e:
148
+ self.fail(f"Package installation raised exception: {e}")
149
+
150
+ @integration_test(scope="service")
151
+ @slow_test
152
+ def test_cache_reuse(self):
153
+ """Test that the cache is reused for multiple installs."""
154
+ package_name = "base_pkg_1"
155
+ version = "1.0.1"
156
+ version_constraint = f"=={version}"
157
+
158
+ # Find package in registry
159
+ package_data = find_package(self.registry_data, package_name)
160
+ self.assertIsNotNone(package_data, f"Package {package_name} not found in registry")
161
+
162
+ # Get package URL
163
+ package_url = get_package_release_url(package_data, version_constraint)
164
+ self.assertIsNotNone(package_url, f"No download URL found for {package_name}@{version_constraint}")
165
+
166
+ # Create two test environments
167
+ first_env = "test_cache_env1"
168
+ second_env = "test_cache_env2"
169
+ self.env_manager.create_environment(first_env, "First test environment for cache test")
170
+ self.env_manager.create_environment(second_env, "Second test environment for cache test")
171
+
172
+ # First install to create cache
173
+ start_time_first = time.time()
174
+ result_first = self.env_manager.add_package_to_environment(
175
+ package_name,
176
+ env_name=first_env,
177
+ version_constraint=version_constraint,
178
+ auto_approve=True # Automatically approve installation in tests
179
+ )
180
+ first_install_time = time.time() - start_time_first
181
+ logger.info(f"First installation took {first_install_time:.2f} seconds")
182
+ self.assertTrue(result_first, f"Failed to add package {package_name}@{version_constraint} to first environment")
183
+ first_env_path = self.env_manager.get_environment_path(first_env)
184
+ self.assertTrue((first_env_path / package_name).exists(), f"Package not found at the expected path: {first_env_path / package_name}")
185
+
186
+ # Second install - should use cache
187
+ start_time = time.time()
188
+ result_second = self.env_manager.add_package_to_environment(
189
+ package_name,
190
+ env_name=second_env,
191
+ version_constraint=version_constraint,
192
+ auto_approve=True # Automatically approve installation in tests
193
+ )
194
+ install_time = time.time() - start_time
195
+
196
+ logger.info(f"Second installation took {install_time:.2f} seconds (should be faster if cache used)")
197
+
198
+ second_env_path = self.env_manager.get_environment_path(second_env)
199
+ self.assertTrue((second_env_path / package_name).exists(), f"Package not found at the expected path: {second_env_path / package_name}")
200
+
201
+ if __name__ == "__main__":
202
+ unittest.main()