Skip to content

Commit 86535e8

Browse files
committed
fix: prepare coordinated candidates in one consumer graph
1 parent 6e6fb09 commit 86535e8

2 files changed

Lines changed: 61 additions & 11 deletions

File tree

‎tests/autoupdate/ci.py‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,24 @@ def archive_of(record):
300300
print(f"Candidate matrix: {len(jobs)} real build/run jobs.")
301301

302302

303+
def replace_candidate(text: str, identity: str, version: str) -> str:
304+
namespace, name = identity.split(":", 1)
305+
text, replacements = re.subn(
306+
rf'(?m)^(\s*(?:{re.escape(name)}|"{re.escape(namespace)}\.{re.escape(name)}")\s*=\s*(?:\{{\s*version\s*=\s*)?")[^"]+(".*)$',
307+
lambda m: m[1] + version + m[2], text)
308+
if not replacements:
309+
# A platform the member selects (mcpp SPEC-013).
310+
text, replacements = re.subn(
311+
rf'(?m)^(\s*platform\s*=\s*"(?:{re.escape(namespace)}\.)?{re.escape(name)}@)[^"]+(".*)$',
312+
lambda m: m[1] + version + m[2], text)
313+
if not replacements:
314+
raise RuntimeError("candidate dependency was not replaced: " + identity)
315+
return text
316+
317+
303318
def prepare(plan: Path, identity: str, output: Path, home: Path | None = None):
304-
record = next(r for r in json.loads(plan.read_text(encoding="utf-8"))["packages"] if r["id"] == identity)
319+
candidates = json.loads(plan.read_text(encoding="utf-8"))["packages"]
320+
record = next(r for r in candidates if r["id"] == identity)
305321
namespace, name = identity.split(":", 1)
306322
version = record["candidate"]
307323
member = direct_member(identity)
@@ -311,16 +327,13 @@ def prepare(plan: Path, identity: str, output: Path, home: Path | None = None):
311327
shutil.copytree(source, output, dirs_exist_ok=True,
312328
ignore=shutil.ignore_patterns("target", ".mcpp", "mcpp.lock"))
313329
text = (output / "mcpp.toml").read_text(encoding="utf-8")
314-
text, replacements = re.subn(
315-
rf'(?m)^(\s*(?:{re.escape(name)}|"{re.escape(namespace)}\.{re.escape(name)}")\s*=\s*(?:\{{\s*version\s*=\s*)?")[^"]+(".*)$',
316-
lambda m: m[1] + version + m[2], text)
317-
if not replacements:
318-
# A platform the member selects (mcpp SPEC-013): `platform = "<name>@<version>"`.
319-
text, replacements = re.subn(
320-
rf'(?m)^(\s*platform\s*=\s*"(?:{re.escape(namespace)}\.)?{re.escape(name)}@)[^"]+(".*)$',
321-
lambda m: m[1] + version + m[2], text)
322-
if not replacements:
323-
raise RuntimeError("candidate dependency was not replaced")
330+
# One change plan may advance an ABI group together. Replacing only
331+
# this row creates a mixture the resolver correctly rejects. Apply
332+
# exactly the planned candidates mapped to this same real consumer;
333+
# unplanned dependencies retain the fixture's constraints.
334+
for candidate in candidates:
335+
if direct_member(candidate["id"]) == member:
336+
text = replace_candidate(text, candidate["id"], candidate["candidate"])
324337
# Standalone consumer: no inherited workspace or stale relative index redirects.
325338
text = re.sub(r'(?ms)^\[indices(?:\.[^\]]+)?\]\s*\n.*?(?=^\[|\Z)', '', text)
326339
else:

‎tests/autoupdate/test_ci.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,43 @@ def descriptor(name):
2525

2626

2727
class BaselineTests(unittest.TestCase):
28+
def test_one_consumer_uses_all_and_only_its_planned_candidates(self):
29+
with tempfile.TemporaryDirectory() as directory:
30+
root = Path(directory)
31+
source = root / "tests/examples/kernel"
32+
source.mkdir(parents=True)
33+
(source / "mcpp.toml").write_text(
34+
'[package]\nname="kernel"\nversion="0.1.0"\n'
35+
'[dependencies.mcpplibs]\nabi="0.23.0"\n'
36+
'[target.linux.dependencies.mcpplibs]\nbackend="0.24.2"\n'
37+
'[build-dependencies.compat]\nhelper="1.0.0"\n')
38+
other = root / "tests/examples/other"
39+
other.mkdir()
40+
(other / "mcpp.toml").write_text(
41+
'[package]\nname="other"\nversion="0.1.0"\n'
42+
'[dependencies.compat]\nunrelated="1.0.0"\n')
43+
plan = root / "plan.json"
44+
candidates = [{"id": "mcpplibs:abi", "candidate": "0.24.0"},
45+
{"id": "mcpplibs:backend", "candidate": "0.25.0"},
46+
{"id": "compat:unrelated", "candidate": "2.0.0"}]
47+
with patch.object(ci, "ROOT", root):
48+
for identity in ("mcpplibs:abi", "mcpplibs:backend"):
49+
plan.write_text(json.dumps({"packages": candidates}))
50+
dest = root / identity.split(":")[1]
51+
ci.prepare(plan, identity, dest)
52+
manifest = tomllib.loads((dest / "mcpp.toml").read_text())
53+
self.assertEqual(manifest["dependencies"]["mcpplibs"]["abi"], "0.24.0")
54+
self.assertEqual(manifest["target"]["linux"]["dependencies"]["mcpplibs"]["backend"], "0.25.0")
55+
self.assertEqual(manifest["build-dependencies"]["compat"]["helper"], "1.0.0")
56+
self.assertNotIn("unrelated", manifest.get("dependencies", {}).get("compat", {}))
57+
# No implicit companion upgrade when it is absent from the plan.
58+
plan.write_text(json.dumps({"packages": candidates[:1]}))
59+
dest = root / "single"
60+
ci.prepare(plan, "mcpplibs:abi", dest)
61+
manifest = tomllib.loads((dest / "mcpp.toml").read_text())
62+
self.assertEqual(manifest["dependencies"]["mcpplibs"]["abi"], "0.24.0")
63+
self.assertEqual(manifest["target"]["linux"]["dependencies"]["mcpplibs"]["backend"], "0.24.2")
64+
2865
def test_commit_snapshot_is_manual_but_historical_bytes_are_still_guarded(self):
2966
with tempfile.TemporaryDirectory() as directory:
3067
root = Path(directory)

0 commit comments

Comments
 (0)