use super::{ExpectedViolation, Scenario};
const DOCUMENT: &str = r#"---
name: rust-naming
title: Rust naming
lang: rust
blurb: Functions stay snake_case
summary: Functions should keep the naming shape readers expect.
published: true
---
# Naming
Functions should be snake_case.
```toml cm:rules
[[rust.fn.where]]
id = "snake-case"
expr = "name =~ ^[a-z][a-z0-9_]*$"
```
```rust cm:file=src/lib.rs
pub fn tidy() {}
pub fn DoThing() {}
```
```cm:expect
rust.fn.snake-case @ src/lib.rs:L3
```
"#;
#[test]
fn parses_front_matter_rules_files_and_expects() {
let scenario = Scenario::parse(DOCUMENT).expect("parse scenario");
assert_eq!(scenario.meta.name, "rust-naming");
assert_eq!(scenario.meta.title, "Rust naming");
assert_eq!(scenario.meta.lang, "rust");
assert_eq!(
scenario.meta.summary,
"Functions should keep the naming shape readers expect."
);
assert!(scenario.meta.published);
assert!(
scenario
.rules
.as_deref()
.expect("rules block")
.contains("snake-case")
);
assert_eq!(scenario.files.len(), 1);
assert_eq!(scenario.files[0].path, "src/lib.rs");
assert_eq!(scenario.files[0].fence, "rust");
assert_eq!(
scenario.files[0].body,
"pub fn tidy() {}\n\npub fn DoThing() {}\n"
);
assert_eq!(
scenario.expects,
vec![ExpectedViolation {
rule_id: "rust.fn.snake-case".to_string(),
path: "src/lib.rs".to_string(),
lines: (3, 3),
}]
);
assert!(!scenario.effective_default_rules());
}
#[test]
fn scenario_runs_against_an_in_memory_workspace() {
let scenario = Scenario::parse(DOCUMENT).expect("parse scenario");
let run = scenario
.run(std::path::Path::new("."), "code+moniker://")
.expect("run scenario");
assert!(run.is_match(), "mismatch:\n{}", run.mismatch_summary());
assert_eq!(run.actual.len(), 1);
assert!(run.silent_rules.is_empty());
}
#[test]
fn mismatched_expectations_report_missing_and_unexpected() {
let document = DOCUMENT.replace("src/lib.rs:L3", "src/lib.rs:L1");
let scenario = Scenario::parse(&document).expect("parse scenario");
let run = scenario
.run(std::path::Path::new("."), "code+moniker://")
.expect("run scenario");
assert!(!run.is_match());
assert_eq!(run.missing.len(), 1);
assert_eq!(run.unexpected.len(), 1);
assert!(run.mismatch_summary().contains("missing:"));
assert!(run.mismatch_summary().contains("unexpected:"));
}
#[test]
fn bless_rewrites_the_expect_block_in_place() {
let document = DOCUMENT.replace("src/lib.rs:L3", "src/lib.rs:L1");
let scenario = Scenario::parse(&document).expect("parse scenario");
let run = scenario
.run(std::path::Path::new("."), "code+moniker://")
.expect("run scenario");
let blessed = scenario.bless(&document, &run.actual);
assert_eq!(blessed, DOCUMENT);
}
#[test]
fn bless_appends_an_expect_block_when_missing() {
let document = DOCUMENT
.replace(
"```cm:expect\nrust.fn.snake-case @ src/lib.rs:L3\n```\n",
"",
)
.trim_end()
.to_string();
let scenario = Scenario::parse(&document).expect("parse scenario");
assert!(scenario.expects.is_empty());
let blessed = scenario.bless(
&document,
&[ExpectedViolation {
rule_id: "rust.fn.snake-case".to_string(),
path: "src/lib.rs".to_string(),
lines: (3, 3),
}],
);
assert!(blessed.ends_with("```cm:expect\nrust.fn.snake-case @ src/lib.rs:L3\n```\n"));
Scenario::parse(&blessed).expect("blessed document still parses");
}
#[test]
fn rejects_escaping_paths_and_duplicates() {
for path in ["../evil.rs", "/abs.rs", "a/../b.rs", "a//b.rs"] {
let document = format!("```rust cm:file={path}\nfn x() {{}}\n```\n");
let error = Scenario::parse(&document).expect_err("path must be rejected");
assert!(error.message.contains("relative path"), "{error}");
}
let duplicated = "```rust cm:file=a.rs\n```\n\n```rust cm:file=a.rs\n```\n";
let error = Scenario::parse(duplicated).expect_err("duplicate file");
assert!(error.message.contains("duplicate file"), "{error}");
}
#[test]
fn longer_fences_escape_embedded_backticks() {
let document = "````md cm:file=docs/note.md\n```\ninner fence\n```\n````\n";
let scenario = Scenario::parse(document).expect("parse scenario");
assert_eq!(scenario.files[0].body, "```\ninner fence\n```\n");
}
#[test]
fn undemonstrated_directives_excuse_silent_rules_and_flag_stale_markers() {
let document = DOCUMENT.replace(
"```cm:expect\n",
"```cm:expect\n! rust.fn.never-fires not expressible in a small layout\n! rust.fn.snake-case stale excuse\n",
);
let document = document.replace(
"expr = \"name =~ ^[a-z][a-z0-9_]*$\"\n```",
"expr = \"name =~ ^[a-z][a-z0-9_]*$\"\n\n[[rust.fn.where]]\nid = \"never-fires\"\nexpr = \"name =~ .\"\n```",
);
assert!(document.contains("never-fires"), "fixture edit must apply");
let scenario = Scenario::parse(&document).expect("parse scenario");
assert_eq!(scenario.undemonstrated.len(), 2);
let run = scenario
.run(std::path::Path::new("."), "code+moniker://")
.expect("run scenario");
assert!(run.is_match(), "{}", run.mismatch_summary());
assert!(run.silent_rules.is_empty(), "{:?}", run.silent_rules);
assert_eq!(run.stale_undemonstrated, vec!["rust.fn.snake-case"]);
let blessed = scenario.bless(&document, &run.actual);
assert!(blessed.contains("! rust.fn.never-fires not expressible"));
assert!(blessed.contains("! rust.fn.snake-case stale excuse"));
Scenario::parse(&blessed).expect("blessed document still parses");
}
#[test]
fn unknown_front_matter_keys_and_bad_expects_fail_with_line_numbers() {
let bad_meta = "---\nnom: x\n---\n";
let error = Scenario::parse(bad_meta).expect_err("unknown key");
assert_eq!(error.line, 2);
let bad_expect = "```cm:expect\nrule-without-location\n```\n";
let error = Scenario::parse(bad_expect).expect_err("bad expect");
assert_eq!(error.line, 2);
}
#[test]
fn rejects_a_rule_declared_as_both_demonstrated_and_undemonstrated() {
for directives in [
"verdict workspace.path.boundary = pass\n! workspace.path.boundary cannot be demonstrated",
"! workspace.path.boundary cannot be demonstrated\nverdict workspace.path.boundary = pass",
] {
let document = format!("```cm:expect\n{directives}\n```\n");
let error = Scenario::parse(&document).expect_err("contradictory directives");
assert_eq!(error.line, 3);
assert!(error.message.contains("both"));
assert!(error.message.contains("workspace.path.boundary"));
}
}
#[test]
fn workspace_path_scenario_checks_exact_rule_verdicts() {
let document = include_str!("../../../../samples/catalog/workspace-path.cm.md");
let scenario = Scenario::parse(document).expect("parse workspace path scenario");
assert_eq!(scenario.verdicts.len(), 6);
let run = scenario
.run(std::path::Path::new("."), "code+moniker://")
.expect("run workspace path scenario");
assert!(run.is_match(), "mismatch:\n{}", run.mismatch_summary());
let wrong = document.replace(
"verdict workspace.path.short-budget-is-inconclusive = inconclusive",
"verdict workspace.path.short-budget-is-inconclusive = pass",
);
let scenario = Scenario::parse(&wrong).expect("parse wrong verdict");
let run = scenario
.run(std::path::Path::new("."), "code+moniker://")
.expect("run wrong verdict");
assert!(!run.is_match());
assert_eq!(run.verdict_mismatches.len(), 1);
assert!(
run.mismatch_summary()
.contains("workspace.path.short-budget-is-inconclusive")
);
assert!(run.mismatch_summary().contains("expected pass"));
assert!(run.mismatch_summary().contains("actual inconclusive"));
}