use std::fmt::Write as _;
use std::path::{Path, PathBuf};
const EXIT_REFUSED: i32 = 2;
const FIXTURE_SUBDIR: &str = "capsules";
const TEST_SUBDIR: &str = "integration";
const SUPPORT_MODULE: &str = "capsule_support";
pub struct GenerateOptions<'a> {
pub capsule: &'a str,
pub name: Option<&'a str>,
pub tests_dir: &'a str,
pub force: bool,
}
pub fn generate(opts: &GenerateOptions<'_>) {
match generate_inner(opts) {
Ok(report) => println!("{report}"),
Err(error) => {
eprintln!("autumn capsule test: {error}");
std::process::exit(EXIT_REFUSED);
}
}
}
#[allow(
clippy::too_many_lines,
reason = "one linear conversion: validate, slug, write the fixture, write \
the test, scaffold the hook, register the module. Splitting it \
would scatter the refusal conditions that have to be checked \
before anything is written."
)]
fn generate_inner(opts: &GenerateOptions<'_>) -> Result<String, String> {
let source = Path::new(opts.capsule);
let json = std::fs::read_to_string(source)
.map_err(|error| format!("could not read {}: {error}", source.display()))?;
let capsule = autumn_web::capsule::Capsule::from_json(&json).map_err(|error| {
format!(
"{} is not a capsule this build can convert: {error}",
source.display()
)
})?;
if let Some(reason) = autumn_web::capsule::refusal_reason(&capsule) {
return Err(format!(
"{} cannot be replayed, so it cannot become a regression test: {reason}",
source.display()
));
}
if let Some(job) = capsule.job.as_ref() {
return Err(format!(
"{} records a failure inside job {:?}, not a request. Job capsules replay with \
`autumn replay`, which dispatches the job's handler; there is no router-driven \
test to generate for one.",
source.display(),
job.name
));
}
let slug = opts.name.map_or_else(|| slugify(&capsule.id), slugify);
if slug.is_empty() {
return Err(
"the capsule's id has no characters usable in a test name; pass --name".to_owned(),
);
}
let tests_dir = PathBuf::from(opts.tests_dir);
let capsules = tests_dir.join(FIXTURE_SUBDIR);
let suite = tests_dir.join(TEST_SUBDIR);
let fixture_path = capsules.join(format!("{slug}.json"));
let test_path = suite.join(format!("capsule_{slug}.rs"));
if !opts.force {
for existing in [&fixture_path, &test_path] {
if existing.exists() {
return Err(format!(
"{} already exists; pass --force to overwrite it, or --name to convert this \
capsule under a different name",
existing.display()
));
}
}
}
std::fs::create_dir_all(&capsules)
.map_err(|error| format!("could not create {}: {error}", capsules.display()))?;
std::fs::create_dir_all(&suite)
.map_err(|error| format!("could not create {}: {error}", suite.display()))?;
std::fs::write(&fixture_path, json.as_bytes())
.map_err(|error| format!("could not write {}: {error}", fixture_path.display()))?;
std::fs::write(&test_path, generated_test(&slug, &capsule).as_bytes())
.map_err(|error| format!("could not write {}: {error}", test_path.display()))?;
let mut report = String::new();
let _ = writeln!(report, "wrote {}", fixture_path.display());
let _ = writeln!(report, "wrote {}", test_path.display());
let support_path = suite.join(format!("{SUPPORT_MODULE}.rs"));
if support_path.exists() {
let _ = writeln!(
report,
"kept {} (edit it to add the routes this capsule needs)",
support_path.display()
);
} else {
std::fs::write(&support_path, SUPPORT_SCAFFOLD.as_bytes())
.map_err(|error| format!("could not write {}: {error}", support_path.display()))?;
let _ = writeln!(
report,
"wrote {} — add the app's routes to `router` before running the test",
support_path.display()
);
}
let mod_path = suite.join("mod.rs");
match register_module(&mod_path, &format!("capsule_{slug}"), SUPPORT_MODULE) {
Ok(true) => {
let _ = writeln!(report, "registered the modules in {}", mod_path.display());
}
Ok(false) => {
let _ = writeln!(
report,
"{} already declares the modules",
mod_path.display()
);
}
Err(error) => {
let _ = writeln!(
report,
"could not update {}: {error}\n add these lines yourself:\n mod \
{SUPPORT_MODULE};\n mod capsule_{slug};",
mod_path.display()
);
}
}
match ensure_suite_harness(&tests_dir) {
Ok(Some(created)) => {
let _ = writeln!(
report,
"wrote {} — the Cargo test target that compiles the suite",
created.display()
);
}
Ok(None) => {}
Err(error) => {
let _ = writeln!(
report,
"could not create a test target for the suite: {error}\n add a file at \
{}/{HARNESS_NAME} containing:\n mod {TEST_SUBDIR};",
tests_dir.display()
);
}
}
let _ = write!(
report,
"\nRun it with: cargo test capsule_{slug}\nThe whole corpus: autumn capsule verify"
);
Ok(report)
}
fn ensure_suite_harness(tests_dir: &Path) -> Result<Option<PathBuf>, String> {
let declaration = format!("mod {TEST_SUBDIR};");
if let Ok(entries) = std::fs::read_dir(tests_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "rs")
&& std::fs::read_to_string(&path).is_ok_and(|source| declares(&source, TEST_SUBDIR))
{
return Ok(None);
}
}
}
let harness = tests_dir.join(HARNESS_NAME);
let source = format!(
"//! Cargo test target for the capsule regression suite.\n //!\n //! Cargo does not descend into `tests/` subdirectories, so this file is\n //! what makes `tests/{TEST_SUBDIR}/` compile at all. Without it the\n //! generated capsule tests are never built and `cargo test capsule_`\n //! silently matches nothing.\n\n {declaration}\n"
);
std::fs::write(&harness, source)
.map_err(|error| format!("could not write {}: {error}", harness.display()))?;
Ok(Some(harness))
}
const HARNESS_NAME: &str = "capsule_tests.rs";
fn slugify(id: &str) -> String {
let mut slug = String::with_capacity(id.len());
for character in id.chars() {
if character.is_ascii_alphanumeric() {
slug.push(character.to_ascii_lowercase());
} else if !slug.ends_with('_') {
slug.push('_');
}
}
let trimmed = slug.trim_matches('_').to_owned();
if trimmed.starts_with(|c: char| c.is_ascii_digit()) || is_rust_keyword(&trimmed) {
format!("c{trimmed}")
} else {
trimmed
}
}
fn is_rust_keyword(word: &str) -> bool {
const KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false",
"fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
"ref", "return", "self", "static", "struct", "super", "trait", "true", "type", "unsafe",
"use", "where", "while", "async", "await", "box", "become", "do", "final", "gen", "macro",
"override", "priv", "try", "typeof", "unsized", "virtual", "yield",
];
KEYWORDS.contains(&word)
}
fn register_module(
mod_path: &Path,
test_module: &str,
support_module: &str,
) -> Result<bool, String> {
let existing = if mod_path.exists() {
std::fs::read_to_string(mod_path)
.map_err(|error| format!("could not read {}: {error}", mod_path.display()))?
} else {
String::new()
};
let mut added = String::new();
if !declares(&existing, support_module) {
let _ = writeln!(added, "mod {support_module};");
}
if !declares(&existing, test_module) {
let _ = writeln!(added, "mod {test_module};");
}
if added.is_empty() {
return Ok(false);
}
let mut updated = existing;
if !updated.is_empty() && !updated.ends_with('\n') {
updated.push('\n');
}
updated.push_str(&added);
std::fs::write(mod_path, updated.as_bytes())
.map_err(|error| format!("could not write {}: {error}", mod_path.display()))?;
Ok(true)
}
fn declares(source: &str, module: &str) -> bool {
let needle = format!("mod {module};");
source
.lines()
.any(|line| line.trim_start().trim_start_matches("pub ") == needle)
}
fn generated_test(slug: &str, capsule: &autumn_web::capsule::Capsule) -> String {
let route = capsule
.request
.route
.clone()
.unwrap_or_else(|| capsule.request.uri.clone());
let recorded = capsule.captured_at.to_rfc3339();
let id = capsule.id.replace(['\n', '\r'], " ");
format!(
r#"//! Regression test generated by `autumn capsule test` from capsule `{id}`.
//!
//! Recorded {recorded} — `{method} {route}`.
//!
//! The capsule beside this file is the *whole* input: the clock, randomness,
//! outbound HTTP, job enqueues, cache, mail, the tenant and the database are
//! all served from it, so this test needs no network, database or queue.
//!
//! Regenerate with:
//! autumn capsule test tests/capsules/{slug}.json --force
//!
//! A `mismatch` here usually means the bug is fixed and this test has done its
//! job — re-record it or delete it. A `diverged` means the handler's effects
//! changed underneath the capsule.
use autumn_web::capsule::regression::RegressionCase;
const CAPSULE: &str = include_str!("../capsules/{slug}.json");
#[tokio::test]
async fn capsule_{slug}_still_reproduces() {{
RegressionCase::from_json(CAPSULE)
.expect("the committed capsule must parse; regenerate it after an Autumn upgrade")
.assert_reproduces(super::{SUPPORT_MODULE}::router)
.await;
}}
"#,
method = capsule.request.method,
)
}
const SUPPORT_SCAFFOLD: &str = r"//! Router factory shared by every generated capsule regression test.
//!
//! `autumn capsule test` scaffolds this file once and never overwrites it —
//! only you know which routes your capsules need. Add them below; everything
//! else (the clock, randomness, outbound HTTP, jobs, cache, mail, the tenant,
//! the database) is served from the capsule, so nothing here should reach a
//! live service.
use autumn_web::capsule::regression::RegressionContext;
use autumn_web::test::TestApp;
/// Build the application router a capsule replays against.
pub fn router(ctx: &RegressionContext<'_>) -> autumn_web::reexports::axum::Router {
TestApp::new()
// TODO: add the routes your capsules exercise, e.g.
// .routes(autumn_web::routes![checkout, charge])
.with_clock(ctx.clock())
.with_entropy(ctx.entropy())
.build()
.into_router()
}
";
pub struct VerifyOptions<'a> {
pub dir: &'a str,
pub check_only: bool,
}
pub fn verify(opts: &VerifyOptions<'_>) {
let report = match verify_inner(Path::new(opts.dir)) {
Ok(report) => report,
Err(error) => {
eprintln!("autumn capsule verify: {error}");
std::process::exit(EXIT_REFUSED);
}
};
println!("{}", report.text);
if report.unusable > 0 {
std::process::exit(1);
}
if opts.check_only {
return;
}
let status = std::process::Command::new("cargo")
.args(["test", TEST_NAME_PREFIX])
.status();
match status {
Ok(status) if status.success() => println!("\nthe corpus replays clean"),
Ok(status) => std::process::exit(status.code().unwrap_or(1)),
Err(error) => {
eprintln!("autumn capsule verify: could not run `cargo test`: {error}");
std::process::exit(EXIT_REFUSED);
}
}
}
fn unregistered_reason(fixture: &Path, suite: Option<&Path>, declarations: &str) -> Option<String> {
let slug = fixture.file_stem()?.to_str()?;
let module = format!("capsule_{slug}");
let suite = suite?;
let test_path = suite.join(format!("{module}.rs"));
if !test_path.exists() {
return Some(format!(
"its generated test {} is missing, so `cargo test {TEST_NAME_PREFIX}` would skip \
this capsule silently; regenerate it with `autumn capsule test {}`",
test_path.display(),
fixture.display()
));
}
if !declares(declarations, &module) {
return Some(format!(
"{} exists but is not declared in {}, so it is never compiled or run; add \
`mod {module};`",
test_path.display(),
suite.join("mod.rs").display()
));
}
None
}
const TEST_NAME_PREFIX: &str = "capsule_";
#[derive(Debug)]
struct CorpusReport {
text: String,
unusable: usize,
}
fn verify_inner(dir: &Path) -> Result<CorpusReport, String> {
let paths = autumn_web::capsule::regression::RegressionCase::corpus(dir)
.map_err(|error| format!("could not read the corpus at {}: {error}", dir.display()))?;
if paths.is_empty() {
return Err(format!(
"no capsules in {} — convert one with `autumn capsule test`",
dir.display()
));
}
let tests_dir = dir.parent();
let suite = tests_dir.map(|tests| tests.join(TEST_SUBDIR));
let declarations = suite
.as_ref()
.and_then(|suite| std::fs::read_to_string(suite.join("mod.rs")).ok())
.unwrap_or_default();
let harness = tests_dir.is_none_or(|tests| {
std::fs::read_dir(tests).is_ok_and(|entries| {
entries.flatten().any(|entry| {
let path = entry.path();
path.extension().is_some_and(|ext| ext == "rs")
&& std::fs::read_to_string(&path)
.is_ok_and(|source| declares(&source, TEST_SUBDIR))
})
})
});
let mut text = String::new();
let mut unusable = 0_usize;
for path in &paths {
match autumn_web::capsule::regression::RegressionCase::from_path(path) {
Ok(case) => {
if let Some(reason) = autumn_web::capsule::refusal_reason(case.capsule()) {
unusable = unusable.saturating_add(1);
let _ = writeln!(text, "REFUSED {}\n {reason}", path.display());
} else if let Some(reason) =
unregistered_reason(path, suite.as_deref(), &declarations)
{
unusable = unusable.saturating_add(1);
let _ = writeln!(text, "UNREGISTERED {}\n {reason}", path.display());
} else {
let _ = writeln!(text, "ok {}", path.display());
}
}
Err(error) => {
unusable = unusable.saturating_add(1);
let _ = writeln!(text, "UNREADABLE {}\n {error}", path.display());
}
}
}
if !harness {
unusable = unusable.saturating_add(paths.len());
let _ = writeln!(
text,
"\nNO TEST TARGET: no file in {} declares `mod {TEST_SUBDIR};`, so Cargo never \
compiles the generated suite and `cargo test {TEST_NAME_PREFIX}` would match \
nothing at all. Convert a capsule again to have one written, or add a \
`tests/{HARNESS_NAME}` containing that line.",
tests_dir.map_or_else(
|| dir.display().to_string(),
|tests| tests.display().to_string()
)
);
}
let _ = writeln!(
text,
"\n{} capsule(s) in {}, {unusable} unusable by this build",
paths.len(),
dir.display()
);
let _ = write!(text, "running the generated tests: cargo test capsule_");
Ok(CorpusReport { text, unusable })
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture_capsule(id: &str) -> String {
capsule_json(id, false)
}
fn capsule_json(id: &str, truncated: bool) -> String {
serde_json::json!({
"format_version": autumn_web::capsule::CAPSULE_FORMAT_VERSION,
"id": id,
"captured_at": "2026-08-27T10:00:00Z",
"autumn_version": "0.7.0",
"app": {"name": "shop", "profile": "prod", "debug_assertions": true},
"request": {
"method": "GET",
"uri": "/orders",
"route": "/orders",
"http_version": "HTTP/1.1",
"headers": [["authorization", "[FILTERED]"]],
"body": "absent",
"redacted_keys": ["header:authorization"],
},
"outcome": {"status": {"code": 500, "message": "boom"}},
"truncated": truncated,
})
.to_string()
}
#[test]
fn a_capsule_id_becomes_a_legal_rust_identifier() {
assert_eq!(slugify("req-01JB2K7Q"), "req_01jb2k7q");
assert_eq!(slugify("01JB2K7Q"), "c01jb2k7q");
assert_eq!(slugify("a//b__c"), "a_b_c");
assert_eq!(slugify("---"), "");
assert_eq!(slugify("type"), "ctype");
assert_eq!(slugify("mod"), "cmod");
assert_eq!(slugify("Self"), "cself");
}
#[test]
fn conversion_writes_the_capsule_bytes_verbatim() {
let dir = tempfile::tempdir().expect("tempdir");
let source = dir.path().join("capsule.json");
let json = fixture_capsule("req-1");
std::fs::write(&source, &json).expect("write capsule");
let tests = dir.path().join("tests");
generate_inner(&GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: tests.to_str().expect("utf-8 path"),
force: false,
})
.expect("conversion succeeds");
let fixture = std::fs::read_to_string(tests.join("capsules/req_1.json")).expect("fixture");
assert_eq!(
fixture, json,
"the committed fixture must be byte-identical to the capsule"
);
}
#[test]
fn conversion_writes_a_test_the_support_module_and_the_mod_declarations() {
let dir = tempfile::tempdir().expect("tempdir");
let source = dir.path().join("capsule.json");
std::fs::write(&source, fixture_capsule("req-2")).expect("write capsule");
let tests = dir.path().join("tests");
generate_inner(&GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: tests.to_str().expect("utf-8 path"),
force: false,
})
.expect("conversion succeeds");
let test = std::fs::read_to_string(tests.join("integration/capsule_req_2.rs"))
.expect("generated test");
assert!(
test.contains("include_str!(\"../capsules/req_2.json\")"),
"the test must load the committed fixture: {test}"
);
assert!(
test.contains("assert_reproduces"),
"the test must fail when the outcome diverges: {test}"
);
assert!(
tests.join("integration/capsule_support.rs").exists(),
"the router hook must be scaffolded"
);
let module = std::fs::read_to_string(tests.join("integration/mod.rs")).expect("mod.rs");
assert!(module.contains("mod capsule_req_2;"), "{module}");
assert!(module.contains("mod capsule_support;"), "{module}");
}
#[test]
fn a_second_conversion_does_not_duplicate_the_module_or_clobber_the_hook() {
let dir = tempfile::tempdir().expect("tempdir");
let tests = dir.path().join("tests");
for id in ["req-3", "req-4"] {
let source = dir.path().join(format!("{id}.json"));
std::fs::write(&source, fixture_capsule(id)).expect("write capsule");
generate_inner(&GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: tests.to_str().expect("utf-8 path"),
force: false,
})
.expect("conversion succeeds");
}
let hook = tests.join("integration/capsule_support.rs");
std::fs::write(&hook, "// edited by hand\n").expect("edit hook");
let source = dir.path().join("req-5.json");
std::fs::write(&source, fixture_capsule("req-5")).expect("write capsule");
generate_inner(&GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: tests.to_str().expect("utf-8 path"),
force: false,
})
.expect("conversion succeeds");
assert_eq!(
std::fs::read_to_string(&hook).expect("hook"),
"// edited by hand\n",
"the scaffolded hook must never be overwritten"
);
let module = std::fs::read_to_string(tests.join("integration/mod.rs")).expect("mod.rs");
assert_eq!(
module.matches("mod capsule_support;").count(),
1,
"the support module must be declared exactly once: {module}"
);
}
#[test]
fn converting_the_same_capsule_twice_is_refused_without_force() {
let dir = tempfile::tempdir().expect("tempdir");
let source = dir.path().join("capsule.json");
std::fs::write(&source, fixture_capsule("req-6")).expect("write capsule");
let tests = dir.path().join("tests");
let opts = GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: tests.to_str().expect("utf-8 path"),
force: false,
};
generate_inner(&opts).expect("first conversion succeeds");
let error = generate_inner(&opts).expect_err("the second must be refused");
assert!(error.contains("--force"), "{error}");
}
#[test]
fn a_truncated_capsule_is_never_committed_as_a_test() {
let dir = tempfile::tempdir().expect("tempdir");
let source = dir.path().join("capsule.json");
std::fs::write(&source, capsule_json("req-7", true)).expect("write capsule");
let error = generate_inner(&GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: dir.path().join("tests").to_str().expect("utf-8 path"),
force: false,
})
.expect_err("a truncated capsule must not be converted");
assert!(error.contains("truncated"), "{error}");
}
#[test]
fn an_incompatible_capsule_version_is_refused_with_an_actionable_message() {
let dir = tempfile::tempdir().expect("tempdir");
let source = dir.path().join("capsule.json");
let mut value: serde_json::Value =
serde_json::from_str(&fixture_capsule("req-8")).expect("parses");
value["format_version"] = serde_json::json!(1);
std::fs::write(&source, value.to_string()).expect("write capsule");
let error = generate_inner(&GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: dir.path().join("tests").to_str().expect("utf-8 path"),
force: false,
})
.expect_err("an incompatible capsule must not be converted");
assert!(error.contains("format version 1"), "{error}");
}
#[test]
fn a_job_capsule_is_not_converted_into_a_router_driven_test() {
let dir = tempfile::tempdir().expect("tempdir");
let source = dir.path().join("capsule.json");
let mut value: serde_json::Value =
serde_json::from_str(&fixture_capsule("req-11")).expect("parses");
value["job"] = serde_json::json!({"name": "send_receipt", "payload": {"order": 7}});
std::fs::write(&source, value.to_string()).expect("write");
let error = generate_inner(&GenerateOptions {
capsule: source.to_str().expect("utf-8 path"),
name: None,
tests_dir: dir.path().join("tests").to_str().expect("utf-8 path"),
force: false,
})
.expect_err("a job capsule must not be converted");
assert!(error.contains("autumn replay"), "{error}");
assert!(error.contains("send_receipt"), "{error}");
}
#[test]
fn an_empty_corpus_is_a_failure_not_a_vacuous_pass() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join("capsules")).expect("mkdir");
let error = verify_inner(&dir.path().join("capsules"))
.expect_err("an empty corpus must not report success");
assert!(error.contains("no capsules"), "{error}");
let missing = verify_inner(&dir.path().join("nope"))
.expect_err("a missing corpus must not report success");
assert!(missing.contains("could not read"), "{missing}");
}
#[test]
fn a_corpus_reports_readable_and_unusable_capsules_separately() {
let dir = tempfile::tempdir().expect("tempdir");
let corpus = dir.path().join("capsules");
std::fs::create_dir_all(&corpus).expect("mkdir");
std::fs::write(corpus.join("good.json"), fixture_capsule("req-9")).expect("write");
let suite = dir.path().join("integration");
std::fs::create_dir_all(&suite).expect("mkdir");
std::fs::write(suite.join("capsule_good.rs"), "").expect("write");
std::fs::write(suite.join("capsule_stale.rs"), "").expect("write");
std::fs::write(
suite.join("mod.rs"),
"mod capsule_good;\nmod capsule_stale;\n",
)
.expect("write");
std::fs::write(dir.path().join("capsule_tests.rs"), "mod integration;\n").expect("write");
std::fs::write(corpus.join("stale.json"), {
let mut value: serde_json::Value =
serde_json::from_str(&fixture_capsule("req-10")).expect("parses");
value["format_version"] = serde_json::json!(1);
value.to_string()
})
.expect("write");
let report = verify_inner(&corpus).expect("a non-empty corpus reports");
assert_eq!(
report.unusable, 1,
"the stale capsule must be counted, not skipped: {}",
report.text
);
assert!(report.text.contains("UNREADABLE"), "{}", report.text);
assert!(report.text.contains("ok "), "{}", report.text);
}
#[test]
fn a_capsule_whose_generated_test_is_gone_is_not_a_silent_pass() {
let dir = tempfile::tempdir().expect("tempdir");
let corpus = dir.path().join("capsules");
let suite = dir.path().join("integration");
std::fs::create_dir_all(&corpus).expect("mkdir");
std::fs::create_dir_all(&suite).expect("mkdir");
std::fs::write(corpus.join("orphan.json"), fixture_capsule("req-11")).expect("write");
std::fs::write(dir.path().join("capsule_tests.rs"), "mod integration;\n").expect("write");
std::fs::write(suite.join("mod.rs"), "mod capsule_orphan;\n").expect("write");
let report = verify_inner(&corpus).expect("a non-empty corpus reports");
assert_eq!(report.unusable, 1, "{}", report.text);
assert!(
report.text.contains("UNREGISTERED") && report.text.contains("is missing"),
"{}",
report.text
);
std::fs::write(suite.join("capsule_orphan.rs"), "").expect("write");
std::fs::write(suite.join("mod.rs"), "mod capsule_something_else;\n").expect("write");
let report = verify_inner(&corpus).expect("a non-empty corpus reports");
assert_eq!(report.unusable, 1, "{}", report.text);
assert!(
report.text.contains("not declared"),
"the report must say which half is missing: {}",
report.text
);
std::fs::write(suite.join("mod.rs"), "mod capsule_orphan;\n").expect("write");
let report = verify_inner(&corpus).expect("a non-empty corpus reports");
assert_eq!(report.unusable, 0, "{}", report.text);
}
#[test]
fn conversion_writes_a_cargo_test_target_for_the_suite() {
let dir = tempfile::tempdir().expect("tempdir");
let tests = dir.path().join("tests");
std::fs::create_dir_all(&tests).expect("mkdir");
let created = ensure_suite_harness(&tests)
.expect("a harness is writable")
.expect("none existed, so one is created");
let source = std::fs::read_to_string(&created).expect("reads");
assert!(
declares(&source, TEST_SUBDIR),
"the harness must declare the suite: {source}"
);
assert_eq!(created.file_name().expect("named"), HARNESS_NAME);
assert!(
ensure_suite_harness(&tests).expect("succeeds").is_none(),
"an existing harness is left alone"
);
let other = tempfile::tempdir().expect("tempdir");
let tests = other.path().join("tests");
std::fs::create_dir_all(&tests).expect("mkdir");
std::fs::write(tests.join("integration_tests.rs"), "mod integration;\n").expect("write");
assert!(
ensure_suite_harness(&tests).expect("succeeds").is_none(),
"any target that declares the suite counts"
);
}
#[test]
fn a_corpus_with_no_test_target_is_not_a_vacuous_pass() {
let dir = tempfile::tempdir().expect("tempdir");
let corpus = dir.path().join("capsules");
let suite = dir.path().join("integration");
std::fs::create_dir_all(&corpus).expect("mkdir");
std::fs::create_dir_all(&suite).expect("mkdir");
std::fs::write(corpus.join("solo.json"), fixture_capsule("req-12")).expect("write");
std::fs::write(suite.join("capsule_solo.rs"), "").expect("write");
std::fs::write(suite.join("mod.rs"), "mod capsule_solo;\n").expect("write");
let report = verify_inner(&corpus).expect("a non-empty corpus reports");
assert!(report.unusable > 0, "{}", report.text);
assert!(report.text.contains("NO TEST TARGET"), "{}", report.text);
std::fs::write(dir.path().join("capsule_tests.rs"), "mod integration;\n").expect("write");
let report = verify_inner(&corpus).expect("a non-empty corpus reports");
assert_eq!(report.unusable, 0, "{}", report.text);
}
#[test]
fn module_declarations_are_matched_whole_not_by_prefix() {
assert!(declares("mod capsule_a;\n", "capsule_a"));
assert!(!declares("mod capsule_ab;\n", "capsule_a"));
assert!(declares("pub mod capsule_a;\n", "capsule_a"));
}
}