Files
isokit/crates/isokit-script/src/tests.rs
T

211 lines
8.8 KiB
Rust

//! Runtime tests: the scripting API, prefabs, the document driver, errors.
use crate::doc::{Doc, Object};
use crate::{Bundle, Runtime};
use isokit_core::{render, RenderOpts};
use serde_json::{json, Map, Value};
fn rt(main: &str) -> Runtime {
let mut b = Bundle { main: Some("scene.rhai".into()), ..Default::default() };
b.sources.insert("scene.rhai".into(), main.into());
Runtime::new(&b).unwrap_or_else(|e| panic!("{e}"))
}
fn cfg() -> Map<String, Value> {
Map::new()
}
fn obj(id: &str, kind: &str, x: f64, y: f64, props: Value) -> Object {
Object { id: id.into(), kind: kind.into(), x, y, props: props.as_object().cloned().unwrap_or_default(), ..Default::default() }
}
#[test]
fn script_draws_with_mixed_numbers() {
let r = rt(r#"
let s = sprite("hall");
s.box(0, 0, 10, 6.5, 0, 3, "wall", #{ top: "roof" });
info("hall", "Hall", "10 x 6.5 m");
tag(5, 3, 4, "Hall", 5);
"#);
let b = r.build("roof", 0, &cfg(), &Doc::default()).unwrap();
let out = render(&b.scene, 0, &RenderOpts::default());
assert!(out.html.contains("iso-roof iso-t"), "{}", out.html);
assert!(out.html.contains("iso-wall iso-l"));
assert!(out.html.contains(">Hall<"));
assert_eq!(out.info["hall"].detail, "10 x 6.5 m");
}
#[test]
fn methods_from_the_library_work_on_sprites() {
let r = rt(r#"
let s = sprite("t");
s.tree_at(0, 0, 1.5);
sprite("c").car(0, 0, 4.4, 1.8, "car2");
"#);
let b = r.build("site", 1, &cfg(), &Doc::default()).unwrap();
let out = render(&b.scene, 1, &RenderOpts::default());
assert!(out.html.contains("iso-leaf iso-l"));
assert!(out.html.contains("iso-glassd"));
}
#[test]
fn seq_group_and_closures() {
let r = rt(r#"
let s = sprite("x");
let h = 2.0;
s.seq([[0, 0, |q| q.box(0, 0, 1, 1, 0, h, "wall")], [5, 0, |q| q.box(5, 0, 6, 1, 0, h, "roof")]]);
s.group("a drive", |q| q.box(0, 3, 1, 4, 0, 1, "car1"));
"#);
let b = r.build("site", 0, &cfg(), &Doc::default()).unwrap();
let out = render(&b.scene, 0, &RenderOpts::default());
// from the south-east the box at x = 5 is nearer, so it is drawn after the one at x = 0
let (a, c) = (out.html.find("iso-wall").unwrap(), out.html.find("iso-roof").unwrap());
assert!(a < c);
assert!(out.html.contains("<g class=\"iso-a iso-drive\">"));
}
#[test]
fn errors_point_at_the_line() {
let mut b = Bundle { main: Some("scene.rhai".into()), ..Default::default() };
b.sources.insert("scene.rhai".into(), "let s = sprite(\"a\");\n\ns.box(0, 0, 1, 1, 0, \"high\", \"wall\");\n".into());
let r = Runtime::new(&b).unwrap();
let e = r.build("site", 0, &cfg(), &Doc::default()).err().expect("error");
assert_eq!(e.file, "scene.rhai");
assert_eq!(e.line, Some(3), "{e}");
assert!(e.message.contains("number"), "{e}");
b.sources.insert("scene.rhai".into(), "let x = ;".into());
let e = Runtime::new(&b).err().expect("parse error");
assert_eq!(e.line, Some(1), "{e}");
}
#[test]
fn imports_resolve_from_the_bundle() {
let mut b = Bundle { main: Some("scene.rhai".into()), ..Default::default() };
b.sources.insert("lib/kiosk.rhai".into(), "fn kiosk(x, y) { sprite(\"kiosk\").box(x, y, x + 2, y + 2, 0, 2.5, \"wall\"); }".into());
b.sources.insert("scene.rhai".into(), "import \"lib/kiosk\" as k;\nk::kiosk(1, 2);".into());
let r = Runtime::new(&b).unwrap();
let s = r.build("site", 0, &cfg(), &Doc::default()).unwrap().scene;
assert_eq!(s.sprite_count(), 1);
}
#[test]
fn catalog_lists_library_and_project_prefabs() {
let mut b = Bundle { main: None, prelude: vec!["lib/kiosk.rhai".into()], ..Default::default() };
b.sources.insert(
"lib/kiosk.rhai".into(),
r#"
fn meta_kiosk() { #{ label: "Kiosk", category: "Shop", props: [ #{ key: "h", type: "float", def: 2.5 } ] } }
fn prefab_kiosk(o) { sprite().box(-1, -1, 1, 1, 0, o.p.h, "wall"); }
"#
.into(),
);
let r = Runtime::new(&b).unwrap();
let kinds: Vec<&str> = r.catalog().iter().map(|m| m.kind.as_str()).collect();
for k in ["site", "area", "road", "building", "tree", "car", "person", "tank", "kiosk"] {
assert!(kinds.contains(&k), "{k} missing from {kinds:?}");
}
let k = r.prefab("kiosk").unwrap();
assert_eq!(k.label, "Kiosk");
assert_eq!(k.source, "lib/kiosk.rhai");
assert_eq!(k.props[0].default, json!(2.5));
}
#[test]
fn document_objects_are_placed_in_their_frame() {
let r = Runtime::new(&Bundle::default()).unwrap();
let mut doc = Doc::default();
let mut b = obj("hall", "building", 30.0, 20.0, json!({"w": 10, "d": 6, "floors": 1}));
b.rot = 90.0;
b.name = "Main hall".into();
b.tag = "Hall".into();
doc.objects.push(b);
doc.objects.push(obj("t1", "tree", 5.0, 5.0, json!({})));
let built = r.build("roof", 1, &cfg(), &doc).unwrap();
// turned a quarter: 6 m along x, 10 m along y, centred on (30, 20)
let bx = *built.boxes.get("hall").unwrap_or_else(|| panic!("{:?}", built.scene.warnings));
assert!((bx[0] - 27.0).abs() < 1e-9 && (bx[3] - 33.0).abs() < 1e-9, "{bx:?}");
assert!((bx[1] - 15.0).abs() < 1e-9 && (bx[4] - 25.0).abs() < 1e-9, "{bx:?}");
let out = render(&built.scene, 1, &RenderOpts::default());
assert!(out.html.contains("data-obj=\"hall\""));
assert!(out.html.contains("data-obj=\"t1\""));
assert_eq!(out.info["hall"].name, "Main hall");
assert_eq!(out.info["t1"].name, "Tree");
assert!(out.html.contains(">Hall<"));
assert!(built.scene.warnings.is_empty(), "{:?}", built.scene.warnings);
}
#[test]
fn levels_cut_buildings_open_and_filter_objects() {
let r = Runtime::new(&Bundle::default()).unwrap();
let mut doc = Doc::default();
doc.objects.push(obj("b", "building", 0.0, 0.0, json!({"floors": 2})));
let mut desk = obj("desk", "tree", 0.0, 0.0, json!({}));
desk.levels = vec!["gf".into()];
doc.objects.push(desk);
let closed = r.build("roof", 0, &cfg(), &doc).unwrap();
let gf = r.build("gf", 0, &cfg(), &doc).unwrap();
let up = r.build("3f", 0, &cfg(), &doc).unwrap();
assert!(!closed.boxes.contains_key("desk"));
assert!(gf.boxes.contains_key("desk"));
// cut open: a floor sprite plus four wall sprites
assert!(gf.scene.sprite_count() > closed.scene.sprite_count());
let html = render(&gf.scene, 0, &RenderOpts::default()).html;
assert!(html.contains("iso-z-floor iso-t"));
let html = render(&up.scene, 0, &RenderOpts::default()).html;
assert!(html.contains("class=\"iso-dim\""), "ghosted above the top storey");
}
#[test]
fn every_library_prefab_renders_cleanly_in_every_rotation() {
let r = Runtime::new(&Bundle::default()).unwrap();
let mut doc = Doc::default();
for (i, m) in r.catalog().iter().enumerate() {
let mut o = obj(&format!("o{i}"), &m.kind, i as f64 * 15.0, 0.0, json!({}));
// switch every bool prop on to exercise all branches
for p in &m.props {
if p.kind == "bool" {
o.props.insert(p.key.clone(), json!(true));
}
}
doc.objects.push(o);
}
for level in ["site", "gf", "1f"] {
for rot in 0..4 {
let b = r.build(level, rot, &cfg(), &doc).unwrap_or_else(|e| panic!("{level}/{rot}: {e}"));
assert!(b.scene.warnings.is_empty(), "{level}/{rot}: {:?}", b.scene.warnings);
let out = render(&b.scene, rot, &RenderOpts::default());
assert!(!out.html.contains("NaN"), "{level}/{rot}");
for m in r.catalog() {
let id = format!("o{}", r.catalog().iter().position(|q| q.kind == m.kind).unwrap());
assert!(b.boxes.contains_key(&id), "{} drew nothing in {level}/{rot}", m.kind);
}
}
}
}
#[test]
fn unknown_types_and_failing_prefabs_warn_but_render() {
let mut b = Bundle { main: None, prelude: vec!["lib/bad.rhai".into()], ..Default::default() };
b.sources.insert("lib/bad.rhai".into(), "fn prefab_bad(o) { sprite().box(0, 0, 1, 1, 0, o.p.nope, \"wall\"); }".into());
let r = Runtime::new(&b).unwrap();
let mut doc = Doc::default();
doc.objects.push(obj("u", "ufo", 0.0, 0.0, json!({})));
doc.objects.push(obj("x", "bad", 0.0, 0.0, json!({})));
doc.objects.push(obj("t", "tree", 5.0, 0.0, json!({})));
let built = r.build("site", 0, &cfg(), &doc).unwrap();
let w = built.scene.warnings.join("\n");
assert!(w.contains("object u: unknown type"), "{w}");
assert!(w.contains("object x:"), "{w}");
assert!(built.boxes.contains_key("t"));
}
#[test]
fn cfg_reaches_the_script() {
let r = rt(r#"if cfg("big") { sprite("a").box(0, 0, 9, 9, 0, 9, "wall"); } else { sprite("a").box(0, 0, 1, 1, 0, 1, "wall"); }"#);
let mut c = Map::new();
c.insert("big".into(), json!(true));
let b = r.build("site", 0, &c, &Doc::default()).unwrap();
assert_eq!(b.scene.layers[2].sprites[0].world_box().unwrap()[5], 9.0);
}