|
1 | 1 | import sys |
2 | 2 | import os |
3 | | - |
4 | | -if sys.version_info[:2] == (3, 9): |
5 | | - print("SKIPPED: The rich switching test is currently unstable on Python 3.9.") |
6 | | - sys.exit(0) |
7 | | - |
8 | | -import sys |
9 | | -import os |
10 | 3 | from pathlib import Path |
11 | 4 | import json |
12 | 5 | import subprocess |
@@ -153,126 +146,86 @@ def create_test_bubbles(config_manager): |
153 | 146 |
|
154 | 147 | def test_python_import(expected_version: str, config_manager, is_bubble: bool): |
155 | 148 | print(_(' 🔧 Testing import of version {}...').format(expected_version)) |
156 | | - # FIX: Extract config dict from config_manager |
157 | 149 | config = config_manager.config |
158 | | - config_json_str = json.dumps(config) |
159 | | - |
160 | | - test_script_content = f'''\ |
| 150 | + # We must pass the project root to the subprocess so it can find the `omnipkg` source |
| 151 | + project_root_str = str(Path(__file__).resolve().parent.parent) |
| 152 | + |
| 153 | + # --- THIS IS THE NEW, ISOLATED TEST RUNNER --- |
| 154 | + # We create a self-contained script to run in a pristine, isolated subprocess. |
| 155 | + test_script_content = f""" |
161 | 156 | import sys |
162 | | -import importlib |
163 | | -from importlib.metadata import version, PackageNotFoundError |
164 | | -from pathlib import Path |
165 | 157 | import json |
| 158 | +import traceback |
| 159 | +from pathlib import Path |
166 | 160 |
|
167 | | -# Ensure omnipkg's root is in sys.path for importing its modules (e.g., omnipkg.loader) |
168 | | -sys.path.insert(0, r"{Path(__file__).resolve().parents[1].parent}") |
| 161 | +# Add the project root to the path to find the omnipkg library |
| 162 | +sys.path.insert(0, r'{project_root_str}') |
169 | 163 |
|
170 | | -from omnipkg.loader import omnipkgLoader |
| 164 | +try: |
| 165 | + from omnipkg.loader import omnipkgLoader |
| 166 | + from importlib.metadata import version |
171 | 167 |
|
172 | | -# omnipkg.core.ConfigManager is also imported in subprocess for omnipkgLoader if config is passed |
173 | | -def test_import_and_version(): |
174 | | - target_package_spec = "rich=={expected_version}" |
175 | | - |
176 | | - # Load config in the subprocess |
177 | | - subprocess_config = json.loads('{config_json_str}') |
| 168 | + # Load the config passed from the main test script |
| 169 | + config = json.loads('{json.dumps(config)}') |
| 170 | + is_bubble = {is_bubble} |
| 171 | + expected_version = "{expected_version}" |
| 172 | + target_spec = f"rich=={{expected_version}}" |
178 | 173 |
|
179 | | - # If it's the main environment test, we don't use the loader context for explicit switching, |
180 | | - # just import directly from the system state. |
181 | | - if not {is_bubble}: # Use the boolean value directly |
182 | | - try: |
183 | | - actual_version = version('rich') |
184 | | - expected_version = "{expected_version}" |
185 | | - assert actual_version == expected_version, f"Version mismatch! Expected {{expected_version}}, got {{actual_version}}" |
186 | | - print(f"✅ Imported and verified version {{actual_version}}") |
187 | | - except PackageNotFoundError: |
188 | | - print(f"❌ Test failed: Package 'rich' not found in main environment.", file=sys.stderr) |
189 | | - sys.exit(1) |
190 | | - except AssertionError as e: |
191 | | - print(f"❌ Test failed: {{e}}", file=sys.stderr) |
192 | | - sys.exit(1) |
193 | | - except Exception as e: |
194 | | - print(f"❌ An unexpected error occurred in main env test: {{e}}", file=sys.stderr) |
195 | | - sys.exit(1) |
196 | | - return |
197 | | -
|
198 | | - # For bubble tests, use the omnipkgLoader context manager |
199 | | - try: |
200 | | - with omnipkgLoader(target_package_spec, config=subprocess_config): |
201 | | - # Inside this block, the specific 'rich' version should be active |
| 174 | + if is_bubble: |
| 175 | + # For bubble tests, activate the loader |
| 176 | + with omnipkgLoader(target_spec, config=config): |
202 | 177 | import rich |
203 | 178 | actual_version = version('rich') |
204 | | - expected_version = "{expected_version}" |
205 | 179 | assert actual_version == expected_version, f"Version mismatch! Expected {{expected_version}}, got {{actual_version}}" |
206 | 180 | print(f"✅ Imported and verified version {{actual_version}}") |
207 | | - except PackageNotFoundError: |
208 | | - print(f"❌ Test failed: Package 'rich' not found in bubble context '{{target_package_spec}}'.", file=sys.stderr) |
209 | | - sys.exit(1) |
210 | | - except AssertionError as e: |
211 | | - print(f"❌ Test failed: {{e}}", file=sys.stderr) |
212 | | - sys.exit(1) |
213 | | - except Exception as e: |
214 | | - print(f"❌ An unexpected error occurred activating/testing bubble '{{target_package_spec}}': {{e}}", file=sys.stderr) |
215 | | - import traceback |
216 | | - traceback.print_exc(file=sys.stderr) |
217 | | - sys.exit(1) |
218 | | -
|
219 | | -if __name__ == "__main__": |
220 | | - test_import_and_version() |
221 | | -''' |
| 181 | + else: |
| 182 | + # For the main environment, just import directly |
| 183 | + import rich |
| 184 | + actual_version = version('rich') |
| 185 | + assert actual_version == expected_version, f"Version mismatch! Expected {{expected_version}}, got {{actual_version}}" |
| 186 | + print(f"✅ Imported and verified version {{actual_version}}") |
| 187 | +
|
| 188 | +except Exception as e: |
| 189 | + print(f"❌ TEST FAILED: {{e}}", file=sys.stderr) |
| 190 | + traceback.print_exc(file=sys.stderr) |
| 191 | + sys.exit(1) |
| 192 | +""" |
222 | 193 |
|
223 | | - site_packages = Path(config['site_packages_path']) |
224 | | - main_rich_dir = site_packages / 'rich' |
225 | | - main_rich_dist = next(site_packages.glob('rich-*.dist-info'), None) |
226 | | - cloaked_paths_by_test_harness = [] |
227 | 194 | temp_script_path = None |
228 | | - |
229 | 195 | try: |
230 | | - if is_bubble: |
231 | | - if main_rich_dir.exists(): |
232 | | - cloak_path = main_rich_dir.with_name(_('rich.{}test_harness_cloaked').format(int(time.time() * 1000))) |
233 | | - shutil.move(main_rich_dir, cloak_path) |
234 | | - cloaked_paths_by_test_harness.append((main_rich_dir, cloak_path)) |
235 | | - |
236 | | - if main_rich_dist and main_rich_dist.exists(): |
237 | | - cloak_path = main_rich_dist.with_name(_('{}.{}test_harness_cloaked').format(main_rich_dist.name, int(time.time() * 1000))) |
238 | | - shutil.move(main_rich_dist, cloak_path) |
239 | | - cloaked_paths_by_test_harness.append((main_rich_dist, cloak_path)) |
240 | | - |
241 | | - with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: |
| 196 | + # Write the mini-script to a temporary file |
| 197 | + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f: |
242 | 198 | f.write(test_script_content) |
243 | 199 | temp_script_path = f.name |
244 | 200 |
|
| 201 | + # Get the correct python executable from the config |
245 | 202 | python_exe = config.get('python_executable', sys.executable) |
246 | 203 |
|
247 | | - result = subprocess.run([python_exe, temp_script_path], capture_output=True, text=True, timeout=60) |
| 204 | + # Execute the mini-script in ISOLATED MODE |
| 205 | + cmd = [python_exe, '-I', temp_script_path] |
| 206 | + |
| 207 | + result = subprocess.run(cmd, capture_output=True, text=True, timeout=90) |
248 | 208 |
|
249 | 209 | if result.returncode == 0: |
250 | | - print(_(' └── {}').format(result.stdout.strip())) |
| 210 | + print(f" └── {result.stdout.strip()}") |
251 | 211 | return True |
252 | 212 | else: |
253 | | - print(_(' ❌ Subprocess FAILED for version {}:').format(expected_version)) |
254 | | - print(_(' STDERR: {}').format(result.stderr.strip())) |
| 213 | + print(f" ❌ Subprocess FAILED for version {expected_version}:") |
| 214 | + # Print stderr first as it contains the real traceback from the subprocess |
| 215 | + print(f" STDERR: {result.stderr.strip()}") |
| 216 | + if result.stdout.strip(): |
| 217 | + print(f" STDOUT: {result.stdout.strip()}") |
255 | 218 | return False |
256 | 219 |
|
257 | | - except subprocess.CalledProcessError as e: |
258 | | - print(_(' ❌ Subprocess FAILED for version {}:').format(expected_version)) |
259 | | - print(_(' STDERR: {}').format(e.stderr.strip())) |
| 220 | + except Exception as e: |
| 221 | + print(f' ❌ An unexpected error occurred while running the test subprocess: {e}') |
260 | 222 | return False |
261 | 223 |
|
262 | 224 | finally: |
263 | | - # Restore cloaked paths in reverse order |
264 | | - for original, cloaked in reversed(cloaked_paths_by_test_harness): |
265 | | - if cloaked.exists(): |
266 | | - if original.exists(): |
267 | | - shutil.rmtree(original, ignore_errors=True) |
268 | | - try: |
269 | | - shutil.move(cloaked, original) |
270 | | - except Exception as e: |
271 | | - print(_(' ⚠️ Test harness failed to restore {} from {}: {}').format(original.name, cloaked.name, e)) |
272 | | - |
| 225 | + # Ensure the temporary script is always cleaned up |
273 | 226 | if temp_script_path and os.path.exists(temp_script_path): |
274 | 227 | os.unlink(temp_script_path) |
275 | | - |
| 228 | + |
276 | 229 | def restore_install_strategy(config_manager, original_strategy): |
277 | 230 | """Restore the original install strategy""" |
278 | 231 | if original_strategy != 'stable-main': |
@@ -337,13 +290,20 @@ def run_comprehensive_test(): |
337 | 290 | omnipkg_core = OmnipkgCore(config_manager) |
338 | 291 | site_packages = Path(config_manager.config['site_packages_path']) |
339 | 292 |
|
340 | | - # Clean up test bubbles |
341 | | - for bubble in omnipkg_core.multiversion_base.glob('rich-*'): |
342 | | - if bubble.is_dir(): |
343 | | - print(_(' 🧹 Removing test bubble: {}').format(bubble.name)) |
344 | | - shutil.rmtree(bubble, ignore_errors=True) |
345 | | - |
346 | | - # Clean up cloaked packages |
| 293 | + # --- START OF THE FIX --- |
| 294 | + # Instead of manually deleting directories, use the omnipkg API |
| 295 | + # to perform a clean uninstall that also updates the knowledge base. |
| 296 | + |
| 297 | + print(_(' 🧹 Cleaning up test bubbles via omnipkg API...')) |
| 298 | + specs_to_uninstall = [f'rich=={v}' for v in BUBBLE_VERSIONS_TO_TEST] |
| 299 | + if specs_to_uninstall: |
| 300 | + # Uninstall all bubbles in one go. The `install_type='bubble'` |
| 301 | + # ensures we only target bubbles and don't touch the active version. |
| 302 | + omnipkg_core.smart_uninstall(specs_to_uninstall, force=True, install_type='bubble') |
| 303 | + |
| 304 | + # --- END OF THE FIX --- |
| 305 | + |
| 306 | + # Clean up any residual cloaked packages (this is still good practice) |
347 | 307 | for cloaked in site_packages.glob('rich.*_omnipkg_cloaked*'): |
348 | 308 | print(_(' 🧹 Removing residual cloaked: {}').format(cloaked.name)) |
349 | 309 | shutil.rmtree(cloaked, ignore_errors=True) |
|
0 commit comments