use std::ffi::OsStr;
use std::path::PathBuf;
#[test]
fn all_samples_resolve() {
let samples_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("samples");
let mut failures = Vec::new();
for entry in std::fs::read_dir(&samples_dir).expect("read samples dir") {
let path = entry.expect("readdir entry").path();
if path.extension() != Some(OsStr::new("lini")) {
continue;
}
let src = std::fs::read_to_string(&path).expect("read sample");
if let Err(e) = lini::check(&src) {
let name = path.file_name().unwrap().to_string_lossy().into_owned();
failures.push(format!("{}: {}", name, e));
}
}
assert!(
failures.is_empty(),
"the following samples failed to resolve:\n {}",
failures.join("\n ")
);
}
#[track_caller]
fn assert_resolve_error(src: &str, expect_msg_substr: &str) {
let err = lini::check(src).expect_err("expected resolve error");
let msg = err.to_string();
assert!(
msg.contains(expect_msg_substr),
"expected error containing {:?}, got {:?}",
expect_msg_substr,
msg
);
}
#[test]
fn err_duplicate_scene_id() {
assert_resolve_error("cat |rect| \"1\"\ncat |rect| \"2\"\n", "duplicate id 'cat'");
}
#[test]
fn err_duplicate_id_reports_previous_location() {
let src = "cat |rect| \"1\"\ncat |rect| \"2\"\n";
let err = lini::check(src).expect_err("expected resolve error");
let shown = err.display_with_source(src, "<test>").to_string();
assert!(
shown.contains("(previously at 1:1)"),
"expected the prior location, got {:?}",
shown
);
}
#[test]
fn err_duplicate_id_nested_in_container() {
assert_resolve_error("g |group| { a |rect|\na |rect| }\n", "duplicate id 'a'");
}
#[test]
fn same_local_id_across_instances_is_ok() {
lini::check("{ |room:group| { inlet |rect| } }\na |room|\nb |room|\n")
.expect("distinct instance paths must not collide");
}
#[test]
fn err_duplicate_scene_config() {
assert_resolve_error(
"{ |scene| gap:10\n|scene| gap:20 }\nx |rect|\n",
"'|scene|' may appear at most once",
);
}
#[test]
fn err_duplicate_wire_config() {
assert_resolve_error(
"{ |wire| stroke:red\n|wire| stroke:blue }\nx |rect|\n",
"'|wire|' may appear at most once",
);
}
#[test]
fn err_slant_skew_out_of_range() {
assert_resolve_error("a |slant| \"x\" skew:90\n", "skew:90 must be in (-89, 89)");
}
#[test]
fn err_unknown_shape_type() {
assert_resolve_error("cat |nosuch| \"x\"\n", "unknown type '|nosuch|'");
}
#[test]
fn err_unknown_style() {
assert_resolve_error("cat |rect| \"x\" .nope\n", "unknown style '.nope'");
}
#[test]
fn err_style_cycle() {
assert_resolve_error(
"{ .alpha .beta\n .beta .alpha }\ncat |rect|\n",
"cycle in style",
);
}
#[test]
fn err_shape_cycle() {
assert_resolve_error("{ |looper:looper| }\ncat |rect|\n", "cycle in");
}
#[test]
fn err_shape_name_collides_with_primitive() {
assert_resolve_error("{ |rect:oval| }\ncat |rect|\n", "'rect' is reserved");
}
#[test]
fn err_shape_name_collides_with_template() {
assert_resolve_error("{ |card:rect| }\ncat |rect|\n", "'card' is reserved");
}
#[test]
fn err_reserved_scene_id() {
assert_resolve_error("rect |rect| \"x\"\n", "'rect' is reserved");
}
#[test]
fn err_reserved_style_name() {
assert_resolve_error("{ .card weight:bold }\ncat |rect|\n", "'card' is reserved");
}
#[test]
fn wire_endpoint_dotpath_navigates_into_groups() {
lini::check("garden |group| { frog |rect| }\noutside |rect|\ngarden.frog -> outside\n")
.expect("dot-path resolves");
}
#[test]
fn type_defaults_apply_to_every_instance() {
lini::check("{ |rect| radius:5 }\ncat |rect| \"Cat\"\n").expect("rect defaults");
}
#[test]
fn type_defaults_unknown_type_errors() {
let err = lini::check("{ |frog| fill:green }\ncat |rect|\n").expect_err("unknown");
assert!(err.to_string().contains("unknown type"), "got: {}", err);
}
#[test]
fn type_defaults_duplicate_errors() {
let err =
lini::check("{ |rect| radius:5\n |rect| radius:9 }\ncat |rect|\n").expect_err("duplicate");
assert!(err.to_string().contains("duplicate"), "got: {}", err);
}
#[test]
fn wire_endpoint_bare_nested_name_errors_with_suggestions() {
let err = lini::check(
"kitchen |group| { mouse |rect| }\ngarden |group| { mouse |rect| }\nmouse -> kitchen\n",
)
.expect_err("not found");
let msg = err.to_string();
assert!(
msg.contains("not found at scene root")
&& msg.contains("'garden.mouse'")
&& msg.contains("'kitchen.mouse'"),
"got: {}",
msg
);
}
#[test]
fn body_wire_suggestion_is_scope_relative() {
let err =
lini::check("garden |group| { shelf |group| { bowl |rect| }\npot |rect|\nbowl -> pot }\n")
.expect_err("not found");
let msg = err.to_string();
assert!(
msg.contains("'shelf.bowl'"),
"scope-relative suggestion: {}",
msg
);
assert!(
!msg.contains("garden.shelf.bowl"),
"must not suggest the un-typeable root path: {}",
msg
);
}
#[test]
fn body_wire_suggestion_stays_in_scope() {
let err = lini::check(
"kitchen |group| { mouse |rect| }\ngarden |group| { cat |rect|\nmouse -> cat }\n",
)
.expect_err("not found");
let msg = err.to_string();
assert!(msg.contains("not found"), "{}", msg);
assert!(
!msg.contains("kitchen.mouse"),
"sealed body must not suggest an unreachable sibling: {}",
msg
);
}
#[test]
fn stroke_style_lints_with_rename_hint() {
let diags = lini::lint_str("x |rect| stroke-style:dashed\n").expect("lint");
assert!(
diags.iter().any(|d| d
.message
.contains("unknown attr 'stroke-style'; use 'line'")),
"got: {:?}",
diags.iter().map(|d| &d.message).collect::<Vec<_>>()
);
}