use super::{COVERAGE_MANIFEST_VERSION, SnippetCoverageKey, SnippetCoverageLedger};
use anyhow::{Context, Result, bail};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
pub fn normalize(mut ledger: SnippetCoverageLedger) -> SnippetCoverageLedger {
ledger.expected.sort();
ledger.generated.sort();
ledger.generated_paths.sort();
ledger
.generated_metadata
.sort_by(|left, right| left.path.cmp(&right.path));
ledger.missing.sort_by(|left, right| left.key.cmp(&right.key));
ledger
.documented_exceptions
.sort_by(|left, right| left.key.cmp(&right.key));
ledger
}
pub fn resolve_curated_snippet_paths(project_root: &Path, patterns: &[String]) -> Result<Vec<PathBuf>> {
if patterns.is_empty() {
return Ok(Vec::new());
}
let project_root = if project_root.as_os_str().is_empty() {
Path::new(".")
} else {
project_root
};
let mut curated = BTreeSet::new();
for pattern in patterns {
reject_escaping_pattern(pattern)?;
let compiled =
glob::Pattern::new(pattern).with_context(|| format!("invalid curated snippet glob `{pattern}`"))?;
let matches = matching_relative_files(project_root, pattern, &compiled)?;
if matches.is_empty() {
bail!(
"curated snippet glob `{pattern}` matches no file under the project root `{}`. Patterns are \
relative to the project root (the directory holding alef.toml), not to \
`[crates.e2e.snippets].output`",
project_root.display()
);
}
curated.extend(matches);
}
Ok(curated.into_iter().collect())
}
pub fn reject_generated_curated_paths(curated: &[PathBuf], generated: &[PathBuf]) -> Result<()> {
let generated: BTreeSet<&Path> = generated.iter().map(PathBuf::as_path).collect();
for path in curated {
if generated.contains(path.as_path()) {
bail!(
"curated snippet declaration claims `{}`, which alef itself generates this run; \
a curated declaration must never claim a path alef writes",
path.display()
);
}
}
Ok(())
}
fn reject_escaping_pattern(pattern: &str) -> Result<()> {
let path = Path::new(pattern);
let escapes = path.components().any(|component| {
matches!(
component,
std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_)
)
});
if pattern.trim().is_empty() || escapes {
bail!("curated snippet glob `{pattern}` must be a relative path beneath the project root");
}
Ok(())
}
fn matching_relative_files(project_root: &Path, pattern: &str, compiled: &glob::Pattern) -> Result<Vec<PathBuf>> {
let search_root = project_root.join(pattern_search_root(pattern));
if !search_root.exists() {
return Ok(Vec::new());
}
let mut matches = Vec::new();
for entry in walkdir::WalkDir::new(&search_root).follow_links(true) {
let entry = entry.with_context(|| format!("failed to walk curated snippet root {}", search_root.display()))?;
if !entry.file_type().is_file() {
continue;
}
let relative = entry
.path()
.strip_prefix(project_root)
.with_context(|| format!("failed to relativize {}", entry.path().display()))?;
if compiled.matches_path(relative) {
matches.push(relative.to_path_buf());
}
}
Ok(matches)
}
fn pattern_search_root(pattern: &str) -> PathBuf {
let mut prefix = PathBuf::new();
for component in Path::new(pattern).components() {
let std::path::Component::Normal(part) = component else {
break;
};
if part.to_string_lossy().contains(['*', '?', '[', ']']) {
break;
}
prefix.push(part);
}
prefix
}
pub fn summary(curated: usize, generated: usize) -> String {
format!("{curated} curated, {generated} generated")
}
pub fn validate(ledger: &SnippetCoverageLedger) -> Result<()> {
if ledger.format_version != COVERAGE_MANIFEST_VERSION {
bail!(
"snippet coverage manifest version {} is unsupported; expected {}",
ledger.format_version,
COVERAGE_MANIFEST_VERSION
);
}
ensure_unique("expected", ledger.expected.iter())?;
ensure_unique("generated", ledger.generated.iter())?;
ensure_unique("missing", ledger.missing.iter().map(|entry| &entry.key))?;
ensure_unique(
"documented exceptions",
ledger.documented_exceptions.iter().map(|entry| &entry.key),
)?;
let expected = key_set(ledger.expected.iter());
let generated = key_set(ledger.generated.iter());
let missing = key_set(ledger.missing.iter().map(|entry| &entry.key));
let exceptions = key_set(ledger.documented_exceptions.iter().map(|entry| &entry.key));
ensure_subset("generated", &generated, &expected)?;
ensure_subset("missing", &missing, &expected)?;
ensure_subset("documented exceptions", &exceptions, &expected)?;
ensure_disjoint("generated", &generated, "missing", &missing)?;
ensure_disjoint("generated", &generated, "documented exceptions", &exceptions)?;
ensure_disjoint("missing", &missing, "documented exceptions", &exceptions)?;
let classified: BTreeSet<_> = generated
.union(&missing)
.cloned()
.chain(exceptions.iter().cloned())
.collect();
if classified != expected {
let first = expected
.difference(&classified)
.next()
.expect("unequal sets have an unclassified key");
bail!(
"snippet coverage cell `{}` / `{}` is not classified",
first.fixture_id,
first.language
);
}
validate_generated_metadata(ledger, &generated)?;
for exception in &ledger.documented_exceptions {
if exception.reason.trim().is_empty() {
bail!(
"snippet coverage exception for `{}` / `{}` has an empty reason",
exception.key.fixture_id,
exception.key.language
);
}
}
Ok(())
}
pub fn validate_tracked_files(ledger: &SnippetCoverageLedger, output: &Path) -> Result<()> {
let mut absent = Vec::new();
for relative in &ledger.generated_paths {
let path = super::ledger_paths::resolve_tracked_path(output, relative)?;
if !path.is_file() {
absent.push(path);
}
}
if absent.is_empty() {
return Ok(());
}
let mut detail = String::new();
for path in &absent {
detail.push_str("\n ");
detail.push_str(&path.display().to_string());
}
bail!(
"snippet coverage ledger records {} file(s) as generated in `generated_paths`, but they are \
absent from disk:{detail}",
absent.len()
);
}
pub fn validate_current(disk: SnippetCoverageLedger, computed: SnippetCoverageLedger) -> Result<()> {
validate(&disk)?;
validate(&computed)?;
if normalize(disk) != normalize(computed) {
bail!("snippet coverage ledger is stale");
}
Ok(())
}
pub fn orphaned_paths(previous: &SnippetCoverageLedger, current: &SnippetCoverageLedger) -> Vec<PathBuf> {
let evaluated_languages: BTreeSet<&str> = current.expected.iter().map(|key| key.language.as_str()).collect();
let still_generated: BTreeSet<&PathBuf> = current.generated_paths.iter().collect();
previous
.generated_metadata
.iter()
.filter(|entry| evaluated_languages.contains(entry.key.language.as_str()))
.filter(|entry| !still_generated.contains(&entry.path))
.map(|entry| entry.path.clone())
.collect()
}
fn validate_generated_metadata(ledger: &SnippetCoverageLedger, generated: &BTreeSet<SnippetCoverageKey>) -> Result<()> {
if ledger.generated_paths.len() != ledger.generated_metadata.len() {
bail!("snippet coverage generated paths and metadata have different lengths");
}
let paths: BTreeSet<_> = ledger.generated_paths.iter().collect();
if paths.len() != ledger.generated_paths.len() {
bail!("snippet coverage generated paths contain duplicates");
}
let metadata_paths: BTreeSet<_> = ledger.generated_metadata.iter().map(|entry| &entry.path).collect();
if paths != metadata_paths {
bail!("snippet coverage generated paths do not match metadata paths");
}
let metadata_keys = key_set(ledger.generated_metadata.iter().map(|entry| &entry.key));
if &metadata_keys != generated {
bail!("snippet coverage generated keys do not match metadata keys");
}
ensure_unique(
"generated metadata",
ledger.generated_metadata.iter().map(|entry| &entry.key),
)
}
fn key_set<'a>(keys: impl Iterator<Item = &'a SnippetCoverageKey>) -> BTreeSet<SnippetCoverageKey> {
keys.cloned().collect()
}
fn ensure_unique<'a>(label: &str, keys: impl Iterator<Item = &'a SnippetCoverageKey>) -> Result<()> {
let mut seen = BTreeSet::new();
for key in keys {
if !seen.insert(key) {
bail!(
"snippet coverage {label} contains duplicate cell `{}` / `{}`",
key.fixture_id,
key.language
);
}
}
Ok(())
}
fn ensure_subset(
label: &str,
values: &BTreeSet<SnippetCoverageKey>,
expected: &BTreeSet<SnippetCoverageKey>,
) -> Result<()> {
if let Some(key) = values.difference(expected).next() {
bail!(
"snippet coverage {label} contains unknown cell `{}` / `{}`",
key.fixture_id,
key.language
);
}
Ok(())
}
fn ensure_disjoint(
left_label: &str,
left: &BTreeSet<SnippetCoverageKey>,
right_label: &str,
right: &BTreeSet<SnippetCoverageKey>,
) -> Result<()> {
if let Some(key) = left.intersection(right).next() {
bail!(
"snippet coverage cell `{}` / `{}` appears in both {left_label} and {right_label}",
key.fixture_id,
key.language
);
}
Ok(())
}
#[cfg(test)]
mod curated_snippet_tests {
use super::{reject_generated_curated_paths, resolve_curated_snippet_paths};
use std::path::PathBuf;
fn write(directory: &std::path::Path, relative: &str, content: &str) {
let path = directory.join(relative);
std::fs::create_dir_all(path.parent().expect("relative path has a parent")).expect("create parent directory");
std::fs::write(path, content).expect("write curated fixture file");
}
#[test]
fn a_matching_glob_is_recorded_as_curated() {
let directory = tempfile::tempdir().expect("temp dir");
write(directory.path(), "docker/quick-start.md", "curated by hand");
let curated = resolve_curated_snippet_paths(directory.path(), &["docker/*.md".to_string()])
.expect("a matching pattern resolves");
assert_eq!(curated, vec![PathBuf::from("docker/quick-start.md")]);
}
#[test]
fn a_curated_file_outside_the_generated_output_tree_is_declarable() {
let directory = tempfile::tempdir().expect("temp dir");
write(
directory.path(),
"docs/snippets/generated/python/a.md",
"alef wrote this",
);
write(directory.path(), "docs/snippets/cli/quickstart.md", "by hand");
let curated = resolve_curated_snippet_paths(directory.path(), &["docs/snippets/cli/*.md".to_string()])
.expect("a pattern naming a file beside the generated tree must resolve, not trip the anti-vacuity guard");
assert_eq!(curated, vec![PathBuf::from("docs/snippets/cli/quickstart.md")]);
}
#[test]
fn a_glob_matching_zero_files_is_refused_not_silently_accepted() {
let directory = tempfile::tempdir().expect("temp dir");
write(directory.path(), "docker/quick-start.md", "curated by hand");
let error = resolve_curated_snippet_paths(directory.path(), &["dcoker/*.md".to_string()])
.expect_err("a glob matching nothing must be refused");
assert!(error.to_string().contains("dcoker/*.md"), "{error}");
assert!(error.to_string().contains("matches no file"), "{error}");
}
#[test]
fn a_wildcard_pattern_matching_zero_files_still_fails_under_project_root_semantics() {
let directory = tempfile::tempdir().expect("temp dir");
write(directory.path(), "docs/snippets/cli/quickstart.md", "by hand");
let error = resolve_curated_snippet_paths(directory.path(), &["docs/snippets/**/*.mdx".to_string()])
.expect_err("a pattern whose extension matches nothing must still be refused");
assert!(error.to_string().contains("matches no file"), "{error}");
}
#[test]
fn a_pattern_escaping_the_project_root_is_refused() {
let directory = tempfile::tempdir().expect("temp dir");
write(directory.path(), "docs/a.md", "by hand");
for pattern in ["../outside/*.md", "/etc/*.md"] {
let error = resolve_curated_snippet_paths(directory.path(), &[pattern.to_string()])
.expect_err("an escaping pattern must be refused");
assert!(error.to_string().contains("beneath the project root"), "{error}");
}
}
#[test]
fn a_curated_path_that_alef_generates_is_refused() {
let error = reject_generated_curated_paths(
&[PathBuf::from("docs/snippets/python/quick-start.md")],
&[PathBuf::from("docs/snippets/python/quick-start.md")],
)
.expect_err("a curated path claiming generated output must be refused");
assert!(error.to_string().contains("alef itself generates"), "{error}");
}
#[test]
fn a_single_star_crossing_a_directory_separator_still_trips_the_generated_output_refusal() {
let directory = tempfile::tempdir().expect("temp dir");
write(
directory.path(),
"docs-site/src/snippets/generated/rust/getting-started/basic_usage.md",
"alef wrote this",
);
let curated = resolve_curated_snippet_paths(
directory.path(),
&["docs-site/src/snippets/*/getting-started/basic_usage.md".to_string()],
)
.expect("a bare `*` must cross `/` under the glob crate's default match options");
assert_eq!(
curated,
vec![PathBuf::from(
"docs-site/src/snippets/generated/rust/getting-started/basic_usage.md"
)],
"the `*` component must consume `generated/rust` as a single match, not stop at the first `/`"
);
let error = reject_generated_curated_paths(
&curated,
&[PathBuf::from(
"docs-site/src/snippets/generated/rust/getting-started/basic_usage.md",
)],
)
.expect_err("a curated glob that crosses into alef's own generated subtree must be refused");
let message = error.to_string().replace('\\', "/");
assert!(
message.contains("docs-site/src/snippets/generated/rust/getting-started/basic_usage.md"),
"{error}"
);
assert!(
message.contains("a curated declaration must never claim a path alef writes"),
"{error}"
);
}
#[test]
fn no_configured_globs_yields_no_curated_paths_without_touching_disk() {
let curated = resolve_curated_snippet_paths(std::path::Path::new("/does/not/exist"), &[])
.expect("no patterns never touches the filesystem");
assert!(curated.is_empty());
}
#[test]
fn a_glob_over_a_missing_directory_is_refused() {
let directory = tempfile::tempdir().expect("temp dir");
let missing_root = directory.path().join("never-created");
let error = resolve_curated_snippet_paths(&missing_root, &["**/*.md".to_string()])
.expect_err("a glob over a directory that was never generated must be refused");
assert!(error.to_string().contains("matches no file"), "{error}");
}
#[test]
fn summary_reports_curated_and_generated_counts() {
assert_eq!(super::summary(3, 431), "3 curated, 431 generated");
assert_eq!(super::summary(0, 0), "0 curated, 0 generated");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::fixture::SideEffectClass;
use crate::e2e::snippets::{DocumentedSnippetException, GeneratedSnippetMetadata, MissingSnippet};
use std::path::PathBuf;
fn key(language: &str) -> SnippetCoverageKey {
SnippetCoverageKey {
fixture_id: "sample_request".into(),
language: language.into(),
}
}
fn generated_ledger() -> SnippetCoverageLedger {
SnippetCoverageLedger {
format_version: COVERAGE_MANIFEST_VERSION,
generated_paths: vec![PathBuf::from("python/sample-request.md")],
generated_metadata: vec![GeneratedSnippetMetadata {
key: key("python"),
path: PathBuf::from("python/sample-request.md"),
language: "python".into(),
target: "python".into(),
session: "python".into(),
requires: Vec::new(),
side_effect: SideEffectClass::Safe,
}],
expected: vec![key("python")],
generated: vec![key("python")],
missing: Vec::new(),
documented_exceptions: Vec::new(),
}
}
#[test]
fn exact_partition_accepts_documented_exception() {
let mut ledger = generated_ledger();
ledger.generated_paths.clear();
ledger.generated_metadata.clear();
ledger.generated.clear();
ledger.documented_exceptions.push(DocumentedSnippetException {
key: key("python"),
reason: "the sample backend cannot express this recipe".into(),
reference: "docs/limitations.md".into(),
});
validate(&ledger).expect("documented exception completes partition");
}
#[test]
fn exact_partition_rejects_overlap_and_unknown_cells() {
let mut overlap = generated_ledger();
overlap.missing.push(MissingSnippet {
key: key("python"),
reason: "renderer unavailable".into(),
});
assert!(
validate(&overlap)
.expect_err("overlap must fail")
.to_string()
.contains("both generated and missing")
);
let mut unknown = generated_ledger();
unknown.generated.push(key("java"));
assert!(
validate(&unknown)
.expect_err("unknown cell must fail")
.to_string()
.contains("unknown cell")
);
}
#[test]
fn metadata_and_tracked_files_must_agree() {
let mut ledger = generated_ledger();
ledger.generated_metadata[0].path = PathBuf::from("python/other.md");
assert!(
validate(&ledger)
.expect_err("metadata mismatch must fail")
.to_string()
.contains("metadata paths")
);
let ledger = generated_ledger();
let directory = tempfile::tempdir().expect("temporary directory");
assert!(
validate_tracked_files(&ledger, directory.path())
.expect_err("missing tracked file must fail")
.to_string()
.contains("absent from disk")
);
}
#[test]
fn validate_tracked_files_reports_every_absent_path_and_its_count() {
let mut ledger = generated_ledger();
ledger.generated_paths.push(PathBuf::from("python/second.md"));
let directory = tempfile::tempdir().expect("temporary directory");
let error = validate_tracked_files(&ledger, directory.path())
.expect_err("two claimed-generated files that do not exist must fail")
.to_string();
assert!(
error.contains("2 file(s)"),
"message must name the exact count of absent files, got: {error}"
);
assert!(
error.contains("python/sample-request.md") && error.contains("python/second.md"),
"message must name every absent path, not just the first: {error}"
);
}
#[test]
fn semantic_comparison_detects_added_fixture_language_cell() {
let disk = generated_ledger();
let mut computed = generated_ledger();
computed.expected.push(key("java"));
computed.missing.push(MissingSnippet {
key: key("java"),
reason: "renderer unavailable".into(),
});
assert!(
validate_current(disk, computed)
.expect_err("new semantic cell must make disk ledger stale")
.to_string()
.contains("stale")
);
}
#[test]
fn orphaned_paths_selects_a_key_that_moved_from_generated_to_missing() {
let mut previous = generated_ledger();
previous.generated_metadata.push(GeneratedSnippetMetadata {
key: key("swift"),
path: PathBuf::from("swift/sample-request.md"),
language: "swift".into(),
target: "swift".into(),
session: "swift".into(),
requires: Vec::new(),
side_effect: SideEffectClass::Safe,
});
let mut current = generated_ledger();
current.generated.clear();
current.generated_paths.clear();
current.generated_metadata.clear();
current.missing.push(MissingSnippet {
key: key("python"),
reason: "python fixture requires an extension-owned documentation recipe".into(),
});
let orphans = orphaned_paths(&previous, ¤t);
assert_eq!(orphans, vec![PathBuf::from("python/sample-request.md")]);
}
#[test]
fn orphaned_paths_selects_a_path_that_became_a_documented_exception() {
let previous = generated_ledger();
let mut current = generated_ledger();
current.generated.clear();
current.generated_paths.clear();
current.generated_metadata.clear();
current.documented_exceptions.push(DocumentedSnippetException {
key: key("python"),
reason: "fixture requires an extension-owned documentation recipe".into(),
reference: "docs/extensions.md".into(),
});
let orphans = orphaned_paths(&previous, ¤t);
assert_eq!(orphans, vec![PathBuf::from("python/sample-request.md")]);
}
#[test]
fn orphaned_paths_selects_a_path_whose_fixture_was_deleted_entirely() {
let previous = generated_ledger();
let surviving = SnippetCoverageKey {
fixture_id: "other_request".into(),
language: "python".into(),
};
let current = SnippetCoverageLedger {
format_version: COVERAGE_MANIFEST_VERSION,
generated_paths: vec![PathBuf::from("python/other-request.md")],
generated_metadata: vec![GeneratedSnippetMetadata {
key: surviving.clone(),
path: PathBuf::from("python/other-request.md"),
language: "python".into(),
target: "python".into(),
session: "python".into(),
requires: Vec::new(),
side_effect: SideEffectClass::Safe,
}],
expected: vec![surviving.clone()],
generated: vec![surviving],
missing: Vec::new(),
documented_exceptions: Vec::new(),
};
let orphans = orphaned_paths(&previous, ¤t);
assert_eq!(orphans, vec![PathBuf::from("python/sample-request.md")]);
}
#[test]
fn orphaned_paths_never_selects_a_path_alef_never_generated() {
let previous = generated_ledger();
let mut current = generated_ledger();
current.missing.push(MissingSnippet {
key: key("java"),
reason: "java fixture requires an extension-owned documentation recipe".into(),
});
let orphans = orphaned_paths(&previous, ¤t);
assert!(orphans.is_empty(), "expected no orphans, got: {orphans:?}");
}
#[test]
fn orphaned_paths_ignores_a_key_still_generated_this_run() {
let previous = generated_ledger();
let current = generated_ledger();
let orphans = orphaned_paths(&previous, ¤t);
assert!(orphans.is_empty(), "expected no orphans, got: {orphans:?}");
}
#[test]
fn orphaned_paths_ignores_a_language_not_evaluated_this_run() {
let previous = generated_ledger();
let current = SnippetCoverageLedger {
format_version: COVERAGE_MANIFEST_VERSION,
..SnippetCoverageLedger::default()
};
let orphans = orphaned_paths(&previous, ¤t);
assert!(orphans.is_empty(), "expected no orphans, got: {orphans:?}");
}
#[test]
fn corrupt_version_duplicate_and_empty_exception_are_rejected() {
let mut version = generated_ledger();
version.format_version = 0;
assert!(
validate(&version)
.expect_err("version must fail")
.to_string()
.contains("version 0")
);
let mut duplicate = generated_ledger();
duplicate.expected.push(key("python"));
assert!(
validate(&duplicate)
.expect_err("duplicate must fail")
.to_string()
.contains("duplicate")
);
let mut exception = generated_ledger();
exception.generated.clear();
exception.generated_paths.clear();
exception.generated_metadata.clear();
exception.documented_exceptions.push(DocumentedSnippetException {
key: key("python"),
reason: " ".into(),
reference: "docs/limitations.md".into(),
});
assert!(
validate(&exception)
.expect_err("empty reason must fail")
.to_string()
.contains("empty reason")
);
}
}