use drawlang_syntax::ast::*;
use drawlang_syntax::diag::Severity;
use drawlang_syntax::parse_source;
use drawlang_syntax::span::SourceFile;
fn example(name: &str) -> String {
let path = format!("{}/../../examples/{name}", env!("CARGO_MANIFEST_DIR"));
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"))
}
#[test]
fn examples_parse_clean() {
for name in ["hello.drawl", "gpu-topology.drawl", "cpu-arch.drawl"] {
let parsed = parse_source(&example(name));
let src = SourceFile::new(name, example(name));
let rendered: Vec<String> = parsed
.diagnostics
.iter()
.map(|d| {
drawlang_syntax::diag::render(d, &src, &drawlang_syntax::diag::Styles::plain())
})
.collect();
assert!(
parsed.diagnostics.is_empty(),
"{name} should parse clean, got:\n{}",
rendered.join("\n")
);
}
}
#[test]
fn five_seeded_errors_all_reported_with_correct_spans() {
let src = r#"drawl 0.1
canvas { theme paper }
node_a { label: "unterminated
node_b { width: 100px }
edge_x -> : "missing target"
constrain { align(node_a, node_b "#;
let parsed = parse_source(src);
let errors: Vec<_> = parsed
.diagnostics
.iter()
.filter(|d| d.severity == Severity::Error)
.collect();
let file = SourceFile::new("seeded.drawl", src);
let snippets: Vec<(&str, String)> = errors
.iter()
.map(|d| {
let span = d.primary_span().expect("every error carries a span");
(d.code, file.snippet(span).to_string())
})
.collect();
assert!(
errors.len() >= 5,
"expected at least 5 errors, got {}: {snippets:?}",
errors.len()
);
let has = |code: &str, frag: &str| {
snippets
.iter()
.any(|(c, text)| *c == code && (text.contains(frag) || frag.is_empty()))
};
assert!(
errors
.iter()
.any(|e| e.code == "E0103" || e.code == "E0110"),
"missing-colon error not reported: {snippets:?}"
);
assert!(
has("E0102", "\"unterminated"),
"unterminated string not caught: {snippets:?}"
);
assert!(has("E0105", "px"), "unit suffix not caught: {snippets:?}");
assert!(
errors.iter().any(
|e| e.code == "E0103" && (e.message.contains("path") || e.message.contains("name"))
),
"missing edge target not caught: {snippets:?}"
);
assert!(
errors
.iter()
.any(|e| e.message.contains("unclosed") || e.message.contains("close")),
"unclosed constrain block not caught: {snippets:?}"
);
}
#[test]
fn recovery_continues_after_errors() {
let src = "drawl 0.1\na { label: \"A\" }\nx: : 5\nb { label: \"B\" }\na -> b\n";
let parsed = parse_source(src);
assert!(parsed.has_errors());
let nodes: Vec<&str> = parsed
.file
.stmts
.iter()
.filter_map(|s| match &s.kind {
StmtKind::Node(n) => n.name.as_ref().map(|i| i.name.as_str()),
_ => None,
})
.collect();
assert_eq!(
nodes,
vec!["a", "b"],
"good statements must survive recovery"
);
assert!(
parsed
.file
.stmts
.iter()
.any(|s| matches!(s.kind, StmtKind::Edge(_))),
"edge after the error must survive recovery"
);
}
#[test]
fn dimension_and_range_lexing() {
let src = "drawl 0.1\ng: grid 2x4 { }\nfor i in 0..4 { x { } }\n";
let parsed = parse_source(src);
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let grid = parsed.file.stmts.iter().find_map(|s| match &s.kind {
StmtKind::Node(Node {
kind: NodeKind::Container { ctype, .. },
..
}) => Some(*ctype),
_ => None,
});
assert_eq!(grid, Some(ContainerType::Grid { cols: 2, rows: 4 }));
}
#[test]
fn interpolation_spans_point_into_source() {
let src = "drawl 0.1\nn { label: \"GPU {idx + 1}\" }\n";
let parsed = parse_source(src);
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let label = parsed.file.stmts.iter().find_map(|s| match &s.kind {
StmtKind::Node(Node {
kind: NodeKind::Plain { body },
..
}) => body.stmts.iter().find_map(|p| match &p.kind {
StmtKind::Prop(Prop {
value: Value::Str(sl),
..
}) => Some(sl.clone()),
_ => None,
}),
_ => None,
});
let label = label.expect("label prop");
assert_eq!(label.parts.len(), 2);
match &label.parts[1] {
StrPart::Expr(e) => {
assert_eq!(&src[e.span.start..e.span.end], "idx + 1");
}
other => panic!("expected expr part, got {other:?}"),
}
}
#[test]
fn did_you_mean_keyword_suggestions() {
let src = "drawl 0.1\ngroop host \"Host\" { }\n";
let parsed = parse_source(src);
let d = parsed
.diagnostics
.iter()
.find(|d| d.code == "E0110")
.expect("E0110");
assert!(
d.help.as_deref().unwrap_or("").contains("`group`"),
"expected did-you-mean group, got {:?}",
d.help
);
}
#[test]
fn back_arrow_normalizes_to_forward() {
let src = "drawl 0.1\na\nb\na <- b\n";
let parsed = parse_source(src);
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let edge = parsed.file.stmts.iter().find_map(|s| match &s.kind {
StmtKind::Edge(e) => Some(e),
_ => None,
});
let edge = edge.expect("edge");
assert_eq!(edge.op, EdgeOp::Forward);
assert_eq!(edge.from.display(), "b");
assert_eq!(edge.to.display(), "a");
}