Skip to content

Commit 0904fbf

Browse files
committed
build: dual-output generated tests (tests/generated local, OUT_DIR CI/publish)
build.rs generates per-case test .rs into tests/generated for local builds (compiled via mod generated) and into OUT_DIR/generated for CI (CI=true) and publish verify (target/package), switched via cargo:rustc-cfg=use_out_dir. Cargo.toml excludes tests/ from the published package; build.rs early-returns when tests/cases is absent (published crate or dependent build), so it never writes outside OUT_DIR and never pollutes the registry source dir. cargo package verify passes.
1 parent fab94a5 commit 0904fbf

3 files changed

Lines changed: 66 additions & 38 deletions

File tree

float-pigment-forest/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ keywords.workspace = true
1010
license.workspace = true
1111
repository.workspace = true
1212
build = "build.rs"
13+
exclude = ["tests"]
1314

1415
[lib]
1516
name = "float_pigment_forest"

float-pigment-forest/build.rs

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,18 @@
33
// (create_node / create_text / set_style / append + layout_imperative +
44
// getters).
55
//
6-
// Output (per-case .rs under <gen_dir>/<topic>/<case>.rs, not one big file):
7-
// - OUT_DIR/generated/<topic>/<case>.rs + OUT_DIR/generated/all.rs
8-
// (all.rs does `include!("topic/case.rs")` for each case — paths resolve
9-
// relative to all.rs, which lives next to the <topic>/ dirs). tests/mod.rs
10-
// pulls all.rs in via `include!(OUT_DIR/generated/all.rs)`. This is what
11-
// compiles.
12-
// - Local (non-CI, non-publish-verify) builds ALSO write the same tree
13-
// under tests/generated/ for human review. CI (CI=true) and cargo
14-
// package/publish verify (CARGO_MANIFEST_DIR under target/package) skip
15-
// the tests/generated mirror — CI doesn't need it, and publish verify
16-
// forbids build.rs from writing outside OUT_DIR.
6+
// Output location depends on context:
7+
// - Local builds (no CI env, not under target/package): writes
8+
// tests/generated/<topic>/<case>.rs + nested mod tree, compiled by
9+
// tests/mod.rs via `mod generated;`. Source tree, human-readable.
10+
// - CI (CI=true) and cargo package/publish verify (CARGO_MANIFEST_DIR
11+
// under target/package): writes OUT_DIR/generated/<topic>/<case>.rs +
12+
// all.rs (which include!s each case), compiled by tests/mod.rs via
13+
// `include!(OUT_DIR/generated/all.rs)`. Keeps the source tree untouched
14+
// (publish verify forbids build.rs from writing outside OUT_DIR).
1715
//
18-
// Each per-case .rs puts `use crate::TestCtx;` *inside* the fn so that
19-
// all.rs's flat include! sequence doesn't trip "duplicate use" — each fn
20-
// brings TestCtx into its own scope.
16+
// build.rs emits `cargo:rustc-cfg=use_out_dir` in the OUT_DIR branch so
17+
// tests/mod.rs can switch between the two with #[cfg(use_out_dir)].
2118
//
2219
// The translator walks the parsed DOM and emits imperative calls in
2320
// document order. Layout assertions (data-expect-*) are collected during
@@ -37,8 +34,16 @@ use float_pigment_mlp::{
3734
fn main() {
3835
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
3936
let cases_dir = PathBuf::from(&manifest).join("tests/cases");
40-
let out = std::env::var("OUT_DIR").unwrap();
41-
let out_gen = PathBuf::from(&out).join("generated");
37+
// Published crate excludes tests/ (Cargo.toml `exclude`), so there are no
38+
// cases to translate — and we must not write anything: build.rs also runs
39+
// when a dependent crate builds us (in the registry source dir), which
40+
// must stay clean.
41+
if !cases_dir.exists() {
42+
return;
43+
}
44+
let is_ci = std::env::var("CI").is_ok();
45+
let is_publish_verify = manifest.contains("target/package");
46+
let use_out_dir = is_ci || is_publish_verify;
4247

4348
// topic -> Vec<(case_name, name_ident, body, ignore)>
4449
let mut by_topic: BTreeMap<String, Vec<(String, String, String, bool)>> = BTreeMap::new();
@@ -65,58 +70,77 @@ fn main() {
6570
}
6671
}
6772

68-
// OUT_DIR/generated: per-case .rs + all.rs (compiled). all.rs is always
69-
// emitted here; mod.rs/ tree is also emitted (harmless, aids browsing).
70-
write_cases_tree(&out_gen, &by_topic, true);
71-
72-
// Local mirror under tests/generated for human review.
73-
let is_ci = std::env::var("CI").is_ok();
74-
let is_publish_verify = manifest.contains("target/package");
75-
if !is_ci && !is_publish_verify {
73+
if use_out_dir {
74+
let out = std::env::var("OUT_DIR").unwrap();
75+
let out_gen = PathBuf::from(&out).join("generated");
76+
write_out_dir_tree(&out_gen, &by_topic);
77+
// Switch tests/mod.rs to include!(OUT_DIR/generated/all.rs).
78+
println!("cargo:rustc-cfg=use_out_dir");
79+
} else {
7680
let gen_dir = PathBuf::from(&manifest).join("tests/generated");
77-
write_cases_tree(&gen_dir, &by_topic, false);
81+
write_source_tree(&gen_dir, &by_topic);
7882
}
7983

8084
println!("cargo:rerun-if-changed=tests/cases");
8185
println!("cargo:rerun-if-changed=build.rs");
8286
}
8387

84-
/// Write per-case .rs under `<gen_dir>/<topic>/<case>.rs` plus a nested
85-
/// `mod.rs` tree (browsing aid) and, when `emit_all_rs`, an `all.rs` that
86-
/// `include!`s every case — that all.rs is what tests/mod.rs compiles.
87-
fn write_cases_tree(
88+
/// OUT_DIR branch: per-case .rs + all.rs. all.rs does
89+
/// `include!("topic/case.rs")` for each case (paths resolve relative to
90+
/// all.rs). tests/mod.rs include!s all.rs. Each fn has `use crate::TestCtx`
91+
/// inside the body so all.rs's flat include! does not duplicate the use.
92+
fn write_out_dir_tree(
8893
gen_dir: &Path,
8994
by_topic: &BTreeMap<String, Vec<(String, String, String, bool)>>,
90-
emit_all_rs: bool,
9195
) {
9296
let _ = fs::remove_dir_all(gen_dir);
9397
fs::create_dir_all(gen_dir).unwrap();
9498
let mut all_rs = String::from("// AUTO-GENERATED by build.rs. Do not edit.\n\n");
95-
let mut top_mod = String::from("// AUTO-GENERATED by build.rs. Do not edit.\n\n");
9699
for (topic, cases) in by_topic {
97100
let topic_dir = gen_dir.join(topic);
98101
fs::create_dir_all(&topic_dir).unwrap();
99102
let topic_ident = topic.replace('-', "_");
100-
let mut topic_mod = String::from("// AUTO-GENERATED. Do not edit.\n\n");
101103
for (name, name_ident, body, ignore) in cases {
102104
let fn_name = format!("html_{}_{}", topic_ident, name_ident);
103105
let ignore_attr = if *ignore { "#[ignore]\n" } else { "" };
104106
let case_rs = format!(
105107
"// AUTO-GENERATED from tests/cases/{topic}/{name}.html. Do not edit.\n\n{ignore_attr}#[rustfmt::skip]\n#[test]\nfn {fn_name}() {{\n use crate::TestCtx;\n{body}}}\n"
106108
);
107109
fs::write(topic_dir.join(format!("{name}.rs")), &case_rs).unwrap();
108-
// all.rs includes each case; path is relative to all.rs (gen_dir),
109-
// which sits next to <topic>/, so "<topic>/<case>.rs" resolves.
110110
all_rs.push_str(&format!("include!(\"{topic}/{name}.rs\");\n"));
111+
}
112+
}
113+
fs::write(gen_dir.join("all.rs"), all_rs).unwrap();
114+
}
115+
116+
/// Source-tree branch: per-case .rs + nested mod.rs tree under tests/generated.
117+
/// tests/mod.rs compiles it via `mod generated;`. Each case is its own mod,
118+
/// so file-level `use crate::TestCtx;` is fine.
119+
fn write_source_tree(
120+
gen_dir: &Path,
121+
by_topic: &BTreeMap<String, Vec<(String, String, String, bool)>>,
122+
) {
123+
let _ = fs::remove_dir_all(gen_dir);
124+
fs::create_dir_all(gen_dir).unwrap();
125+
let mut top_mod = String::from("// AUTO-GENERATED by build.rs. Do not edit.\n\n");
126+
for (topic, cases) in by_topic {
127+
let topic_dir = gen_dir.join(topic);
128+
fs::create_dir_all(&topic_dir).unwrap();
129+
let topic_ident = topic.replace('-', "_");
130+
let mut topic_mod = String::from("// AUTO-GENERATED. Do not edit.\n\n");
131+
for (name, name_ident, body, ignore) in cases {
132+
let fn_name = format!("html_{}_{}", topic_ident, name_ident);
133+
let ignore_attr = if *ignore { "#[ignore]\n" } else { "" };
134+
let case_rs = format!(
135+
"// AUTO-GENERATED from tests/cases/{topic}/{name}.html. Do not edit.\nuse crate::TestCtx;\n\n{ignore_attr}#[rustfmt::skip]\n#[test]\nfn {fn_name}() {{\n{body}}}\n"
136+
);
137+
fs::write(topic_dir.join(format!("{name}.rs")), &case_rs).unwrap();
111138
topic_mod.push_str(&format!("mod {name_ident};\n"));
112139
}
113140
fs::write(topic_dir.join("mod.rs"), topic_mod).unwrap();
114141
top_mod.push_str(&format!("mod {topic_ident};\n"));
115142
}
116-
fs::write(gen_dir.join("mod.rs"), top_mod).unwrap();
117-
if emit_all_rs {
118-
fs::write(gen_dir.join("all.rs"), all_rs).unwrap();
119-
}
143+
fs::write(gen_dir.join("mod.rs"), top_mod).unwrap()
120144
}
121145

122146
/// Parse the HTML and emit imperative TestCtx calls.

float-pigment-forest/tests/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,4 +677,7 @@ fn convert_grid_auto(grid_auto: GridAuto) -> LayoutGridAuto<Len> {
677677
}
678678
}
679679

680+
#[cfg(use_out_dir)]
680681
include!(concat!(env!("OUT_DIR"), "/generated/all.rs"));
682+
#[cfg(not(use_out_dir))]
683+
mod generated;

0 commit comments

Comments
 (0)