use std::fs;
use std::path::{Path, PathBuf};
use brink_syntax_native::{SyntaxKind, parse};
fn fixtures_root() -> Result<PathBuf, String> {
let candidate =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/tier1-brink-respell");
candidate
.canonicalize()
.map_err(|e| format!("tests/tier1-brink-respell must exist at the repo root ({e})"))
}
fn fixture_files() -> Result<Vec<PathBuf>, String> {
let root = fixtures_root()?;
let entries = fs::read_dir(&root).map_err(|e| format!("reading {}: {e}", root.display()))?;
let mut files: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|p| p.is_dir())
.map(|dir| dir.join("story.brink"))
.filter(|p| p.is_file())
.collect();
files.sort();
Ok(files)
}
#[test]
fn every_respelled_fixture_parses_with_zero_errors() {
let files = fixture_files().unwrap();
assert!(
!files.is_empty(),
"expected at least one story.brink fixture under {}",
fixtures_root().unwrap().display()
);
let mut failures = Vec::new();
for path in &files {
let source = fs::read_to_string(path).unwrap();
let parsed = parse(&source);
if parsed.syntax().text().to_string() != source {
failures.push(format!(
"{}: lossy round-trip (CST text != source)",
path.display()
));
continue;
}
if !parsed.errors().is_empty() {
failures.push(format!(
"{}: {} parse error(s): {:?}",
path.display(),
parsed.errors().len(),
parsed.errors()
));
}
}
assert!(
failures.is_empty(),
"the following respelled fixtures did not parse clean:\n{}",
failures.join("\n")
);
}
#[test]
fn every_fixture_has_a_manifest() {
for path in fixture_files().unwrap() {
let manifest = path.with_file_name("manifest.toml");
assert!(
manifest.is_file(),
"{} has no sibling manifest.toml",
path.display()
);
}
}
#[test]
fn n1_affected_fixtures_parse_inline_diverts_as_divert_nodes() {
let cases: &[(&str, usize)] = &[
("sticky-choice", 1),
("exhibit-fogg-passage", 2),
("manual-stitch-v1", 3),
];
let root = fixtures_root().unwrap();
let mut failures = Vec::new();
for &(case, expected_inline_diverts) in cases {
let path = root.join(case).join("story.brink");
let source = fs::read_to_string(&path).unwrap();
let parsed = parse(&source);
assert!(
parsed.errors().is_empty(),
"{}: expected zero parse errors, got {:?}",
path.display(),
parsed.errors()
);
let inline_divert_count = parsed
.syntax()
.descendants()
.filter(|n| matches!(n.kind(), SyntaxKind::DIVERT_STMT | SyntaxKind::TUNNEL_CALL))
.filter(|n| !is_first_on_its_line(n))
.count();
if inline_divert_count != expected_inline_diverts {
failures.push(format!(
"{}: expected {expected_inline_diverts} inline DIVERT_STMT/TUNNEL_CALL node(s), found {inline_divert_count}",
path.display()
));
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
fn is_first_on_its_line(node: &brink_syntax_native::SyntaxNode) -> bool {
let Some(start) = node
.descendants_with_tokens()
.find_map(rowan::NodeOrToken::into_token)
else {
return false;
};
let mut prev = start.prev_token();
while let Some(tok) = prev {
match tok.kind() {
SyntaxKind::WHITESPACE => prev = tok.prev_token(),
SyntaxKind::NEWLINE => return true,
_ => return false,
}
}
true
}