Skip to content

Commit f8d6384

Browse files
committed
test(foreign): require structured recipe conditions
1 parent 3727da4 commit f8d6384

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

‎tests/foreign_conditions.rs‎

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use eb_stack::package::{ConditionContext, ConditionExpr};
2+
use eb_stack::{parse_foreign_path, ForeignFormat, ForeignRuleKind};
3+
use std::collections::BTreeMap;
4+
use std::path::PathBuf;
5+
6+
fn fixture(path: &str) -> PathBuf {
7+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path)
8+
}
9+
10+
fn feature_context(features: &[(&str, bool)]) -> ConditionContext {
11+
ConditionContext {
12+
package_version: "4.3.0".into(),
13+
features: features
14+
.iter()
15+
.map(|(name, enabled)| ((*name).to_string(), *enabled))
16+
.collect::<BTreeMap<_, _>>(),
17+
..ConditionContext::default()
18+
}
19+
}
20+
21+
#[test]
22+
fn spack_ignores_commented_directives_and_keeps_source_spans() {
23+
let recipe = parse_foreign_path(
24+
&fixture("fixtures/foreign_ingest/spack_qmcpack/package.py"),
25+
Some(ForeignFormat::Spack),
26+
)
27+
.expect("parse QMCPACK package.py");
28+
29+
let names: Vec<&str> = recipe
30+
.variants
31+
.iter()
32+
.map(|variant| variant.name.as_str())
33+
.collect();
34+
assert!(
35+
names.contains(&"mixed"),
36+
"real mixed variant missing: {names:?}"
37+
);
38+
assert!(
39+
!names.contains(&"+mixed"),
40+
"commented pseudo-directive was parsed: {names:?}"
41+
);
42+
43+
let mpi = recipe
44+
.variants
45+
.iter()
46+
.find(|variant| variant.name == "mpi")
47+
.expect("mpi variant");
48+
let source = mpi.provenance.first().expect("variant provenance");
49+
assert!(source.span.path.ends_with("spack_qmcpack/package.py"));
50+
assert!(source.span.start_line > 0);
51+
assert!(source.original.contains("variant(\"mpi\""));
52+
}
53+
54+
#[test]
55+
fn spack_preserves_conditional_hdf5_edges() {
56+
let recipe = parse_foreign_path(
57+
&fixture("fixtures/foreign_ingest/spack_qmcpack/package.py"),
58+
Some(ForeignFormat::Spack),
59+
)
60+
.expect("parse QMCPACK package.py");
61+
62+
let hdf5: Vec<_> = recipe
63+
.dependencies
64+
.iter()
65+
.filter(|dependency| dependency.name == "hdf5")
66+
.collect();
67+
assert_eq!(hdf5.len(), 2, "serial and parallel HDF5 edges: {hdf5:?}");
68+
69+
let serial = hdf5
70+
.iter()
71+
.find(|dependency| dependency.original_spec.as_deref() == Some("hdf5~mpi"))
72+
.expect("serial HDF5 edge");
73+
let parallel = hdf5
74+
.iter()
75+
.find(|dependency| dependency.original_spec.as_deref() == Some("hdf5+mpi"))
76+
.expect("parallel HDF5 edge");
77+
78+
let serial_context = feature_context(&[("phdf5", false), ("mpi", true)]);
79+
assert!(serial.condition.evaluate(&serial_context));
80+
assert!(!parallel.condition.evaluate(&serial_context));
81+
82+
let parallel_context = feature_context(&[("phdf5", true), ("mpi", true)]);
83+
assert!(!serial.condition.evaluate(&parallel_context));
84+
assert!(parallel.condition.evaluate(&parallel_context));
85+
}
86+
87+
#[test]
88+
fn spack_extracts_conflicts_and_requirements_as_rules() {
89+
let recipe = parse_foreign_path(
90+
&fixture("fixtures/foreign_ingest/spack_qmcpack/package.py"),
91+
Some(ForeignFormat::Spack),
92+
)
93+
.expect("parse QMCPACK package.py");
94+
95+
let conflicts = recipe
96+
.rules
97+
.iter()
98+
.filter(|rule| rule.kind == ForeignRuleKind::Conflict)
99+
.count();
100+
let requirements = recipe
101+
.rules
102+
.iter()
103+
.filter(|rule| rule.kind == ForeignRuleKind::Requirement)
104+
.count();
105+
assert_eq!(conflicts, 19, "QMCPACK conflict directives");
106+
assert_eq!(requirements, 2, "QMCPACK requirement directives");
107+
108+
let phdf5 = recipe
109+
.rules
110+
.iter()
111+
.find(|rule| {
112+
rule.kind == ForeignRuleKind::Conflict
113+
&& rule.spec == "+phdf5"
114+
&& rule.when.as_deref() == Some("~mpi")
115+
})
116+
.expect("parallel-HDF5/MPI conflict");
117+
assert!(phdf5.provenance.span.start_line > 0);
118+
assert!(phdf5
119+
.message
120+
.as_deref()
121+
.is_some_and(|message| message.contains("MPI")));
122+
}
123+
124+
#[test]
125+
fn conda_keeps_mutually_exclusive_platform_selectors() {
126+
let recipe = parse_foreign_path(
127+
&fixture("fixtures/foreign_ingest/conda_eon/recipe.yaml"),
128+
Some(ForeignFormat::CondaForge),
129+
)
130+
.expect("parse eOn recipe.yaml");
131+
132+
let blas: Vec<_> = recipe
133+
.dependencies
134+
.iter()
135+
.filter(|dependency| dependency.name == "libblas")
136+
.collect();
137+
assert_eq!(blas.len(), 2, "win and non-win BLAS edges: {blas:?}");
138+
139+
let win = ConditionContext {
140+
package_version: recipe.version.clone(),
141+
platform: Some("win".into()),
142+
..ConditionContext::default()
143+
};
144+
let linux = ConditionContext {
145+
package_version: recipe.version.clone(),
146+
platform: Some("linux".into()),
147+
..ConditionContext::default()
148+
};
149+
assert_eq!(
150+
blas.iter()
151+
.filter(|dependency| dependency.condition.evaluate(&win))
152+
.count(),
153+
1,
154+
"one Windows BLAS edge"
155+
);
156+
assert_eq!(
157+
blas.iter()
158+
.filter(|dependency| dependency.condition.evaluate(&linux))
159+
.count(),
160+
1,
161+
"one non-Windows BLAS edge"
162+
);
163+
assert!(blas.iter().all(|dependency| {
164+
dependency
165+
.provenance
166+
.first()
167+
.is_some_and(|source| source.span.path.ends_with("conda_eon/recipe.yaml"))
168+
}));
169+
}
170+
171+
#[test]
172+
fn unconditional_dependencies_use_always_condition() {
173+
let recipe = parse_foreign_path(
174+
&fixture("fixtures/foreign_ingest/spack_qmcpack/package.py"),
175+
Some(ForeignFormat::Spack),
176+
)
177+
.expect("parse QMCPACK package.py");
178+
let libxml2 = recipe
179+
.dependencies
180+
.iter()
181+
.find(|dependency| dependency.name == "libxml2")
182+
.expect("libxml2 dependency");
183+
assert_eq!(libxml2.condition, ConditionExpr::Always);
184+
}

0 commit comments

Comments
 (0)