Skip to content

Commit d45f8eb

Browse files
nadilasclaude
andcommitted
test: add template-fixture consistency tests for async handler templates
Adds tests that scaffold each template to a temp directory and verify the generated files match the corresponding fixture directory content. This catches drift between embedded templates and test fixtures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dafb9b9 commit d45f8eb

File tree

3 files changed

+119
-0
lines changed

3 files changed

+119
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/warp-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ tracing-subscriber.workspace = true
2121

2222
[dev-dependencies]
2323
tempfile = "3"
24+
walkdir = "2"

crates/warp-cli/src/commands/init.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ pub fn init(template: &str, path: Option<&str>) -> Result<()> {
2929
#[cfg(test)]
3030
mod tests {
3131
use super::*;
32+
use std::collections::BTreeSet;
3233

3334
#[test]
3435
fn test_unknown_template() {
@@ -97,4 +98,120 @@ mod tests {
9798
assert!(target.join("wit/deps/http/types.wit").exists());
9899
assert!(target.join("wit/deps/shim/dns.wit").exists());
99100
}
101+
102+
// ── Template ↔ Fixture consistency tests ─────────────────────
103+
//
104+
// These tests ensure the embedded template content stays in sync with
105+
// the fixture directories that integration tests build. A drift between
106+
// template and fixture means integration tests validate stale code.
107+
108+
/// Collect all relative file paths under `dir`, excluding build artifacts.
109+
fn collect_files(dir: &std::path::Path) -> BTreeSet<String> {
110+
let mut files = BTreeSet::new();
111+
for entry in walkdir::WalkDir::new(dir)
112+
.into_iter()
113+
.filter_map(|e| e.ok())
114+
.filter(|e| e.file_type().is_file())
115+
{
116+
let rel = entry
117+
.path()
118+
.strip_prefix(dir)
119+
.unwrap()
120+
.to_string_lossy()
121+
.to_string();
122+
// Skip build artifacts that aren't part of the template
123+
if rel.starts_with("target/") || rel == "Cargo.lock" {
124+
continue;
125+
}
126+
files.insert(rel);
127+
}
128+
files
129+
}
130+
131+
/// Normalize fixture content so it can be compared to template output.
132+
///
133+
/// Fixtures use their directory name as the project name (e.g.
134+
/// "async-rust-template") whereas templates use the placeholder
135+
/// "my-async-handler". The Go fixture also has a local `replace`
136+
/// directive needed for workspace builds that the template omits.
137+
fn normalize_fixture_content(
138+
content: &str,
139+
fixture_name: &str,
140+
) -> String {
141+
content
142+
.replace(fixture_name, "my-async-handler")
143+
// Go fixture has a local replace directive for workspace builds
144+
.replace(
145+
"\nreplace github.com/anthropics/warpgrid/packages/warpgrid-go => ../../../packages/warpgrid-go\n",
146+
"",
147+
)
148+
}
149+
150+
/// Scaffold a template and compare every generated file against the
151+
/// corresponding fixture file. Fails if content differs or if the
152+
/// fixture has files the template doesn't produce (or vice versa).
153+
///
154+
/// Known differences (project name, local replace directives) are
155+
/// normalized before comparison.
156+
fn assert_template_matches_fixture(template_name: &str, fixture_subdir: &str) {
157+
let dir = tempfile::tempdir().unwrap();
158+
let scaffolded = dir.path().join("project");
159+
crate::templates::scaffold(template_name, &scaffolded).unwrap();
160+
161+
let fixture_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
162+
.parent()
163+
.unwrap()
164+
.parent()
165+
.unwrap()
166+
.join("tests/fixtures")
167+
.join(fixture_subdir);
168+
169+
let scaffolded_files = collect_files(&scaffolded);
170+
let fixture_files = collect_files(&fixture_dir);
171+
172+
// Check for files in fixture but missing from template
173+
let missing_from_template: BTreeSet<_> =
174+
fixture_files.difference(&scaffolded_files).collect();
175+
assert!(
176+
missing_from_template.is_empty(),
177+
"Fixture '{fixture_subdir}' has files not produced by template '{template_name}': {missing_from_template:?}"
178+
);
179+
180+
// Check for files in template but missing from fixture
181+
let missing_from_fixture: BTreeSet<_> =
182+
scaffolded_files.difference(&fixture_files).collect();
183+
assert!(
184+
missing_from_fixture.is_empty(),
185+
"Template '{template_name}' produces files not in fixture '{fixture_subdir}': {missing_from_fixture:?}"
186+
);
187+
188+
// Compare content of every file (normalizing known differences)
189+
for file in &scaffolded_files {
190+
let scaffolded_content =
191+
std::fs::read_to_string(scaffolded.join(file)).unwrap();
192+
let fixture_content =
193+
std::fs::read_to_string(fixture_dir.join(file)).unwrap();
194+
let normalized_fixture =
195+
normalize_fixture_content(&fixture_content, fixture_subdir);
196+
assert_eq!(
197+
scaffolded_content, normalized_fixture,
198+
"Content mismatch in '{file}' between template '{template_name}' and fixture '{fixture_subdir}'"
199+
);
200+
}
201+
}
202+
203+
#[test]
204+
fn test_async_rust_template_matches_fixture() {
205+
assert_template_matches_fixture("async-rust", "async-rust-template");
206+
}
207+
208+
#[test]
209+
fn test_async_go_template_matches_fixture() {
210+
assert_template_matches_fixture("async-go", "async-go-template");
211+
}
212+
213+
#[test]
214+
fn test_async_ts_template_matches_fixture() {
215+
assert_template_matches_fixture("async-ts", "async-ts-template");
216+
}
100217
}

0 commit comments

Comments
 (0)