Skip to content

Commit e864156

Browse files
committed
test: add corrupt repodata fixtures for conda#896
Capture the failure-mode data from PR conda#862 so subsequent behavioral commits can run against a known-bad cache without needing live conda-forge or Docker. Three pinned-metadata archives represent the three relevant mamba versions: - ``2.1.0-pkgs.tar.gz``: clean baseline. - ``2.1.1-pkgs.tar.gz``: corrupt (the bug's first appearance). - ``2.3.3-pkgs.tar.gz``: corrupt (the bug's last appearance before mamba 2.6.0's fix). Each archive contains ``info/index.json`` plus ``info/repodata_record.json`` exactly as that mamba version wrote them when installing the same explicit lockfile, so overlaying the metadata on a freshly warmed cache produces a "hybrid" cache: real package files + bug-faithful metadata. That is what later end-to-end tests will drive ``conda-lock lock`` against. The ``05-render-explicit-lockfiles.py`` harness plus the per-version ``lockfile-...yml`` outputs lock the reproduction inputs. Add ``tests/support/corrupt_repodata.py`` with the helpers a component test needs to drive the harness: warmed-cache fixture factory, corrupt-metadata overlay, ``conda-lock lock`` / ``conda-lock render --kind=explicit`` driver wrappers, and explicit-lockfile-URL extraction. None of this calls into new production code yet -- it is pure test scaffolding so the next commits can focus on behavior. Exclude ``tests/test-corrupt-repodata/`` from mypy / ty / pre-commit since the stage scripts are Docker-based one-shot data generators, not runtime code. The committed fixtures (.tar.gz / .lock / .yml) are what tests consume. References conda#896, mamba-org/mamba#4052, mamba-org/mamba#4110.
1 parent a6f5467 commit e864156

32 files changed

Lines changed: 15233 additions & 0 deletions

.pre-commit-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ repos:
6060
^conda_lock/_vendor/.*\.pyi$
6161
| ^tests/test-local-pip/setup\.py$
6262
| ^tests/test-pip-repositories/fake-private-package.*-1\.0\.0/setup\.py$
63+
| ^tests/test-corrupt-repodata/
6364
)
6465
# First exclude is due to:
6566
# conda_lock/_vendor/conda/__init__.py: error: Duplicate module named "conda_lock._vendor.conda" (also at "conda_lock/_vendor/conda.pyi")
6667
# Second two excludes are due to:
6768
# tests/test-pip-repositories/fake-private-package-1.0.0/setup.py: error: Duplicate module named "setup" (also at "tests/test-local-pip/setup.py")
69+
# Last exclude: tests/test-corrupt-repodata/ contains stage scripts from
70+
# the PR #862 reproduction harness; the committed fixtures are what our
71+
# tests consume, the scripts are reproduction scaffolding.

mypy.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@ warn_unused_ignores = true
1313
# tests/test-pip-repositories/fake-private-package-1.0.0/setup.py: error: Duplicate module named "setup" (also at "tests/test-local-pip/setup.py")
1414
# Third exclude is due to:
1515
# error: Cannot find implementation or library stub for module named "migrate_code" [import-not-found]
16+
# Fourth exclude: tests/test-corrupt-repodata/ contains stage scripts from
17+
# the PR #862 reproduction harness (Docker-based one-shot data generators).
18+
# The committed fixtures (.tar.gz, .lock, .yml) are what our tests consume;
19+
# the scripts are reproduction scaffolding only.
1620
exclude = (?x)(
1721
^tests/test-pip-repositories/fake-private-package-1.0.0/setup\.py$
1822
| ^tests/test-local-pip/setup\.py$
1923
| ^conda_lock/scripts/vendor_poetry/
24+
| ^tests/test-corrupt-repodata/
2025
)
2126

2227
# https://github.com/python/mypy/issues/12664

tests/support/corrupt_repodata.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Treat tar.gz files as binary (no line ending conversion)
2+
*.tar.gz binary
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Extracted archive contents are scratch output from re-running the
2+
# reproduction harness; never commit them.
3+
*-pkgs/
4+
5+
# The pinned fixture data below is deliberately committed even though
6+
# the repo-root .gitignore excludes *.tar.gz, tests/**/*.lock, and
7+
# tests/**/*.yml. The negations keep plain `git add` working; without
8+
# them the fixtures must be force-added, and a rebase or re-add can
9+
# silently drop them from history.
10+
!*-pkgs.tar.gz
11+
!01-explicit.lock
12+
!lockfile-*.yml
13+
!lockfile-*-explicit.lock

0 commit comments

Comments
 (0)