|
| 1 | +"""Shared fixtures and helpers for the PR #862 corrupt-cache repro. |
| 2 | +
|
| 3 | +The tests in ``tests/e2e/test_corrupt_repodata_repro.py`` reconstruct |
| 4 | +stage 04 of the PR #862 pipeline without Docker: warm a fresh |
| 5 | +package cache via ``micromamba create``, then overlay the corrupt |
| 6 | +metadata bundled in ``tests/test-corrupt-repodata/2.1.1-pkgs.tar.gz``. |
| 7 | +The helpers here own that dance plus the stage-05 explicit-render |
| 8 | +diff so individual tests can stay focused on assertions. |
| 9 | +
|
| 10 | +Kept under ``tests/support/`` rather than inline in the e2e file so |
| 11 | +new component-level tests in the future can reuse the warmed cache |
| 12 | +without copy-paste. |
| 13 | +""" |
| 14 | + |
| 15 | +from __future__ import annotations |
| 16 | + |
| 17 | +import os |
| 18 | +import shutil |
| 19 | +import subprocess |
| 20 | + |
| 21 | +from pathlib import Path |
| 22 | + |
| 23 | + |
| 24 | +TESTS_DIR = Path(__file__).resolve().parent.parent |
| 25 | +REPRO_DIR = TESTS_DIR / "test-corrupt-repodata" |
| 26 | + |
| 27 | + |
| 28 | +def overlay_corrupt_metadata(corrupt_pkgs_root: Path, cache: Path) -> int: |
| 29 | + """Mirror stage 04 of the PR #862 pipeline: copy the corrupt |
| 30 | + ``info/{index,repodata_record}.json`` from the extracted reference |
| 31 | + archive over the warmed cache. After this the cache has real package |
| 32 | + files (downloaded by ``micromamba create``) but the metadata exactly |
| 33 | + matches what micromamba 2.1.1 would have written. |
| 34 | + """ |
| 35 | + overlaid = 0 |
| 36 | + for pkg_dir in corrupt_pkgs_root.iterdir(): |
| 37 | + if not pkg_dir.is_dir(): |
| 38 | + continue |
| 39 | + src_info = pkg_dir / "info" |
| 40 | + if not src_info.is_dir(): |
| 41 | + continue |
| 42 | + for tgt_info in cache.glob(f"**/{pkg_dir.name}/info"): |
| 43 | + for f in src_info.iterdir(): |
| 44 | + shutil.copy2(f, tgt_info / f.name) |
| 45 | + overlaid += 1 |
| 46 | + return overlaid |
| 47 | + |
| 48 | + |
| 49 | +def conda_lock_lock_against( |
| 50 | + cache: Path, |
| 51 | + *, |
| 52 | + conda_exe: str, |
| 53 | + source: Path, |
| 54 | + out_lockfile: Path, |
| 55 | +) -> subprocess.CompletedProcess: |
| 56 | + """Drive ``conda-lock lock`` with ``CONDA_PKGS_DIRS=<cache>`` -- |
| 57 | + exactly the recipe from ``04-run-conda-lock.sh`` minus the Docker |
| 58 | + wrapper.""" |
| 59 | + return subprocess.run( |
| 60 | + [ |
| 61 | + "conda-lock", |
| 62 | + "lock", |
| 63 | + "--micromamba", |
| 64 | + f"--file={source}", |
| 65 | + "--platform=linux-64", |
| 66 | + f"--conda={conda_exe}", |
| 67 | + f"--lockfile={out_lockfile}", |
| 68 | + ], |
| 69 | + env={**os.environ, "CONDA_PKGS_DIRS": str(cache)}, |
| 70 | + capture_output=True, |
| 71 | + text=True, |
| 72 | + timeout=600, |
| 73 | + ) |
| 74 | + |
| 75 | + |
| 76 | +def conda_lock_render_explicit(lockfile: Path, out: Path) -> Path: |
| 77 | + """``conda-lock render --kind=explicit`` of an existing unified |
| 78 | + lockfile -- exactly what stage 05 (``05-render-explicit-lockfiles.py``) |
| 79 | + does. The explicit lockfile is what ``conda-lock install`` consumes |
| 80 | + just before installation, so its URL set is the *truest* statement |
| 81 | + of which packages will land in the user's environment. Comparing |
| 82 | + explicit URLs catches category-mutation bugs that a raw YAML name |
| 83 | + set would miss (a v1 lockfile entry only renders to the explicit |
| 84 | + output when its category survives ``--filter-categories``). |
| 85 | +
|
| 86 | + Returns the path to the rendered explicit lockfile. |
| 87 | + """ |
| 88 | + out.mkdir(parents=True, exist_ok=True) |
| 89 | + out_file = out / "explicit.lock" |
| 90 | + proc = subprocess.run( |
| 91 | + [ |
| 92 | + "conda-lock", |
| 93 | + "render", |
| 94 | + "--kind=explicit", |
| 95 | + "--platform=linux-64", |
| 96 | + f"--filename-template={out_file}", |
| 97 | + str(lockfile), |
| 98 | + ], |
| 99 | + capture_output=True, |
| 100 | + text=True, |
| 101 | + timeout=120, |
| 102 | + ) |
| 103 | + if proc.returncode != 0: |
| 104 | + raise RuntimeError( |
| 105 | + f"conda-lock render failed:\n" |
| 106 | + f"STDOUT:\n{proc.stdout[-2000:]}\n" |
| 107 | + f"STDERR:\n{proc.stderr[-2000:]}" |
| 108 | + ) |
| 109 | + if not out_file.is_file(): |
| 110 | + raise RuntimeError( |
| 111 | + f"conda-lock render did not produce expected file at {out_file}; " |
| 112 | + f"see {out.parent} for the actual output." |
| 113 | + ) |
| 114 | + return out_file |
| 115 | + |
| 116 | + |
| 117 | +def explicit_lockfile_urls(explicit_lock: Path) -> set[str]: |
| 118 | + """Pull the package-URL set out of an explicit lockfile. |
| 119 | +
|
| 120 | + The format is one URL (with optional ``#md5`` suffix) per non-comment |
| 121 | + line, after the ``@EXPLICIT`` marker. |
| 122 | + """ |
| 123 | + urls: set[str] = set() |
| 124 | + seen_marker = False |
| 125 | + for line in explicit_lock.read_text().splitlines(): |
| 126 | + line = line.strip() |
| 127 | + if not line or line.startswith("#"): |
| 128 | + continue |
| 129 | + if line == "@EXPLICIT": |
| 130 | + seen_marker = True |
| 131 | + continue |
| 132 | + if not seen_marker: |
| 133 | + continue |
| 134 | + urls.add(line.split("#", 1)[0]) |
| 135 | + return urls |
| 136 | + |
| 137 | + |
| 138 | +def parse_lockfile_packages(lockfile: Path): |
| 139 | + """Parse a unified conda-lock lockfile into ``LockedDependency`` |
| 140 | + objects via the production parser, *not* line-grepping. Surfaces |
| 141 | + category mutation bugs that a YAML-name comparison would miss.""" |
| 142 | + from conda_lock.lockfile import parse_conda_lock_file |
| 143 | + |
| 144 | + parsed = parse_conda_lock_file(lockfile) |
| 145 | + return parsed.package |
0 commit comments