Skip to content

Commit bb53a95

Browse files
committed
test(ring0): score the ingest path against a merged brand-new-package recipe
Ring 0 had no reproduction assertion at all: its packages are first-ever additions, so bump has nothing to operate on and the existing eon/qmcpack fixtures only assert properties of an already-authored copy of the merged recipe. This drives the ingest path end to end for eOn (Spack definition to emitted recipe) and scores the result against the merged eOn-2.17.10-foss-2026.1.eb with the miner's normalized scorer. The edit set is listed field by field and deliberately narrow: EasyBuild package name, target version and checksum, the product profile, and the solved dependency versions. Widening it until the score improves would measure nothing. Two assertions: the emitted recipe must parse, which is ring 0's floor, and it must reach the stated SEMANTIC bar. The bar assertion carries the normalized diff in its message, so a run that does not meet it reports what to fix rather than just that something is wrong.
1 parent c4f597f commit bb53a95

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

‎tests/ring0_ingest_repro.rs‎

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//! Ring 0 reproduction: does the ingest path plus a declared maintainer edit
2+
//! reproduce the merged easyconfig for a brand-new package?
3+
//!
4+
//! The ring-1 benchmark asks whether `bump` reproduces a version bump. Ring 0
5+
//! cannot use it: its packages are first-ever additions, so there is no prior
6+
//! recipe to bump from and `emit_next_generation` has nothing to operate on. The
7+
//! applicable path is ingest, foreign recipe to EasyBuild scaffold, and until
8+
//! now nothing asserted that it reproduces anything.
9+
//!
10+
//! What counts as a fair reproduction here matters more than the score. The
11+
//! edits below are the ones a maintainer genuinely supplies and a foreign recipe
12+
//! cannot know: the target version and its checksum, the EasyBuild package name,
13+
//! the product profile, and the solved dependency versions (the solver's job,
14+
//! benchmarked separately as `resolves`). Everything else has to come out of the
15+
//! ingested definition. Widening that edit set until the score improves would
16+
//! measure nothing, so each field is listed explicitly and the score is whatever
17+
//! it is.
18+
19+
use eb_stack::miner::{compare_reproduction, score_reproduction, ReproScore};
20+
use eb_stack::package::{
21+
LockedDependency, OutputRequest, ProductProfile, ProfileLock, PROFILE_LOCK_SCHEMA_VERSION,
22+
};
23+
use eb_stack::{
24+
emit_profile_easyconfigs, package_plan_from_foreign, parse_foreign_path,
25+
resolve_easyconfig_str, ForeignFormat, Toolchain,
26+
};
27+
use std::collections::BTreeMap;
28+
use std::path::PathBuf;
29+
30+
fn fixtures() -> PathBuf {
31+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures")
32+
}
33+
34+
fn foss_2026_1() -> Toolchain {
35+
Toolchain {
36+
name: "foss".into(),
37+
version: "2026.1".into(),
38+
}
39+
}
40+
41+
/// A dependency as the solver would hand it over: name, version, and whether it
42+
/// is build-only. Versions are the merged recipe's, because reproducing the
43+
/// *file* is the question here and choosing versions is the solver benchmark.
44+
fn dep(name: &str, version: &str, build: bool) -> LockedDependency {
45+
LockedDependency {
46+
name: name.into(),
47+
version: version.into(),
48+
versionsuffix: None,
49+
toolchain: foss_2026_1(),
50+
easyconfig_path: format!("{name}-{version}-foss-2026.1.eb"),
51+
build,
52+
}
53+
}
54+
55+
fn eon_lock() -> ProfileLock {
56+
ProfileLock {
57+
schema_version: PROFILE_LOCK_SCHEMA_VERSION,
58+
package: "eOn".into(),
59+
version: "2.17.10".into(),
60+
profile: "default".into(),
61+
toolchain: foss_2026_1(),
62+
versionsuffix: String::new(),
63+
dependencies: vec![
64+
dep("CMake", "4.2.1", true),
65+
dep("Meson", "1.10.2", true),
66+
dep("Ninja", "1.13.2", true),
67+
dep("pkgconf", "2.5.1", true),
68+
dep("Python", "3.14.2", false),
69+
dep("SciPy-bundle", "2026.05", false),
70+
dep("PyYAML", "6.0.3", false),
71+
dep("Eigen", "5.0.0", false),
72+
dep("Highway", "1.4.0", false),
73+
dep("inih", "62", false),
74+
dep("nlohmann_json", "3.12.0", false),
75+
dep("quill", "11.1.0", false),
76+
dep("readcon-core", "0.13.1", false),
77+
dep("rgpot", "2.5.3", false),
78+
],
79+
pin_outcomes: Vec::new(),
80+
exclusions: Vec::new(),
81+
solver: "resolvo".into(),
82+
}
83+
}
84+
85+
/// The single product eOn ships, with the toolchain options and build switches
86+
/// the maintainer chose.
87+
fn eon_profile() -> ProductProfile {
88+
ProductProfile {
89+
name: "default".into(),
90+
default: true,
91+
versionsuffix: Vec::new(),
92+
platform: None,
93+
architecture: None,
94+
features: BTreeMap::from([
95+
("tests".into(), true),
96+
("rgpot".into(), true),
97+
("fortran".into(), false),
98+
("cuh2".into(), false),
99+
]),
100+
parameters: BTreeMap::new(),
101+
toolchain_options: BTreeMap::from([("openmp".into(), true), ("pic".into(), true)]),
102+
config_options: vec![
103+
"-Dbuildtype=release".into(),
104+
"-Dwith_tests=true".into(),
105+
"-Dwith_rgpot=true".into(),
106+
],
107+
easyconfig_parameters: BTreeMap::new(),
108+
verification_commands: Vec::new(),
109+
}
110+
}
111+
112+
/// Ingest the Spack definition, apply the declared edits, and render the recipe.
113+
fn emit_eon_from_spack() -> String {
114+
let path = fixtures().join("foreign_ingest/spack_eon/package.py");
115+
let recipe = parse_foreign_path(&path, Some(ForeignFormat::Spack)).expect("parse spack eOn");
116+
let mut plan = package_plan_from_foreign(&recipe, &foss_2026_1());
117+
118+
// Maintainer-supplied, and unknowable from a Spack recipe pinned at 2.16.0:
119+
// the EasyBuild package name, the target version, and its checksum.
120+
plan.package.name = "eOn".into();
121+
plan.package.version = "2.17.10".into();
122+
if let Some(source) = plan.sources.first_mut() {
123+
source.sha256 =
124+
Some("1ade06d7a30afcd08f9f14194d7051478c48e7b8d11baaab8da830073e3d6f4a".into());
125+
}
126+
plan.profiles = vec![eon_profile()];
127+
plan.outputs = vec![OutputRequest {
128+
profile: "default".into(),
129+
stack: "foss-2026.1".into(),
130+
}];
131+
132+
let emitted = emit_profile_easyconfigs(&plan, &[eon_lock()]).expect("emit eOn recipe");
133+
assert_eq!(emitted.len(), 1, "one product profile, one file");
134+
emitted[0].text.clone()
135+
}
136+
137+
fn merged_eon() -> String {
138+
let path = fixtures().join("eon_foss_2026_1/easyconfigs/e/eOn/eOn-2.17.10-foss-2026.1.eb");
139+
std::fs::read_to_string(&path).expect("read merged eOn recipe")
140+
}
141+
142+
#[test]
143+
fn ingest_emits_a_parseable_recipe_for_a_brand_new_package() {
144+
// The floor: whatever the score turns out to be, ring 0 must at least
145+
// produce a file EasyBuild can read. An unparseable scaffold is the ring-0
146+
// equivalent of the ERROR bucket.
147+
let emitted = emit_eon_from_spack();
148+
let parsed = resolve_easyconfig_str(&emitted).expect("emitted recipe must parse");
149+
assert_eq!(parsed.name, "eOn");
150+
assert_eq!(parsed.version, "2.17.10");
151+
}
152+
153+
#[test]
154+
fn ring0_eon_scores_against_the_merged_recipe() {
155+
let emitted = emit_eon_from_spack();
156+
let target = merged_eon();
157+
let score = score_reproduction(&emitted, &target);
158+
159+
// Ring 0's stated bar is SEMANTIC-or-better. Asserting it is how the bar
160+
// gets enforced rather than described; when it fails, the diff below is the
161+
// finding, and the number to record is in the message.
162+
assert!(
163+
matches!(score, ReproScore::Exact | ReproScore::Semantic),
164+
"ring 0 eOn scored {} against the merged recipe, below the SEMANTIC bar.\n\
165+
normalized diff:\n{}",
166+
score.as_str(),
167+
compare_reproduction(&emitted, &target).render_raw_diff()
168+
);
169+
}

0 commit comments

Comments
 (0)