use std::sync::Arc;
use sim_codec::{Input, Output, decode_with_codec, encode_with_codec};
use sim_kernel::{
Cx, DefaultFactory, EagerPolicy, EncodeOptions, Expr, NumberLiteral, ReadPolicy, Symbol,
};
use crate::{SceneCodecLib, apply, diff, map, node, scene_codec_symbol, text, validate_scene};
fn cx() -> Cx {
let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
sim_test_support::register_core_classes(&mut cx);
let lib = SceneCodecLib::new(cx.registry_mut().fresh_codec_id());
cx.load_lib(&lib).unwrap();
cx
}
fn num(domain: &str, canonical: &str) -> Expr {
Expr::Number(NumberLiteral {
domain: Symbol::new(domain),
canonical: canonical.to_owned(),
})
}
use sim_value::build::sym;
fn sample_scene() -> Expr {
node(
"graph",
vec![
("id", sym("graph-main")),
(
"bounds",
map(vec![("w", num("i64", "1200")), ("h", num("i64", "700"))]),
),
(
"nodes",
Expr::List(vec![
node(
"node",
vec![
("id", sym("n1")),
("title", Expr::String("Planner".to_owned())),
(
"at",
map(vec![("x", num("f64", "80")), ("y", num("f64", "120"))]),
),
("status", sym("ok")),
(
"target",
Expr::List(vec![sym("ref"), sym("agent"), sym("planner")]),
),
],
),
node(
"node",
vec![
("id", sym("n2")),
("title", Expr::String("Writer".to_owned())),
],
),
]),
),
("flags", Expr::Set(vec![sym("a"), sym("b")])),
("ports", Expr::Vector(vec![sym("in0"), sym("out0")])),
("blob", Expr::Bytes(vec![0, 1, 2, 255, 16])),
(
"note",
Expr::String("quote \" and \\ and \n newline".to_owned()),
),
("nothing", Expr::Nil),
("live", Expr::Bool(true)),
("dead", Expr::Bool(false)),
],
)
}
#[test]
fn text_form_roundtrips_losslessly() {
let scene = sample_scene();
let encoded = text::encode(sim_kernel::CodecId(7), &scene).unwrap();
let decoded = text::decode(sim_kernel::CodecId(7), &encoded).unwrap();
assert_eq!(scene, decoded);
}
#[test]
fn scene_roundtrips_through_codec_scene() {
let mut cx = cx();
let codec = scene_codec_symbol();
let scene = sample_scene();
let output = encode_with_codec(&mut cx, &codec, &scene, EncodeOptions::default()).unwrap();
let input = match output {
Output::Text(text) => Input::Text(text),
Output::Bytes(bytes) => Input::Bytes(bytes),
};
let decoded = decode_with_codec(&mut cx, &codec, input, ReadPolicy::default()).unwrap();
assert_eq!(scene, decoded);
}
#[test]
fn encoding_a_non_scene_fails_closed() {
let mut cx = cx();
let codec = scene_codec_symbol();
let not_a_scene = map(vec![("just", sym("data"))]);
let err = encode_with_codec(&mut cx, &codec, ¬_a_scene, EncodeOptions::default());
assert!(err.is_err(), "a kindless map must not encode as a scene");
}
#[test]
fn encoding_a_non_data_form_fails_closed() {
let mut cx = cx();
let codec = scene_codec_symbol();
let scene = node(
"box",
vec![(
"bad",
Expr::Call {
operator: Box::new(sym("f")),
args: vec![sym("x")],
},
)],
);
let err = encode_with_codec(&mut cx, &codec, &scene, EncodeOptions::default());
assert!(err.is_err(), "non-data forms must not encode as a scene");
}
#[test]
fn decoding_malformed_text_yields_a_diagnostic_not_a_panic() {
let mut cx = cx();
let codec = scene_codec_symbol();
for bad in [
"",
"(",
"{ S U\"k\" }",
"Znonsense",
"%(",
"R\"unterminated",
] {
let result = decode_with_codec(
&mut cx,
&codec,
Input::Text(bad.to_owned()),
ReadPolicy::default(),
);
assert!(
result.is_err(),
"malformed input {bad:?} must error, not panic"
);
}
}
#[test]
fn validate_reports_a_structured_path() {
let scene = node(
"graph",
vec![("nodes", Expr::List(vec![node("not-a-real-kind", vec![])]))],
);
let error = validate_scene(&scene).expect_err("must reject unknown nested kind");
assert!(
error.path_string().contains("nodes"),
"path: {}",
error.path_string()
);
assert!(error.message.contains("unrecognized scene kind"));
}
#[test]
fn validate_rejects_kindless_and_non_symbol_kinds() {
assert!(validate_scene(&map(vec![("x", sym("y"))])).is_err());
let bad_kind = Expr::Map(vec![(sym("kind"), Expr::String("graph".to_owned()))]);
assert!(validate_scene(&bad_kind).is_err());
}
#[test]
fn validates_music_editor_scene_kinds() {
for kind in ["piano-roll", "player-rack", "object-roll"] {
validate_scene(&node(kind, vec![("target", sym("target"))]))
.unwrap_or_else(|err| panic!("{kind}: {err}"));
}
}
#[test]
fn diff_then_apply_reconstructs_exactly() {
let old = sample_scene();
let cases = [
edit_field_value(&old),
add_a_key(&old),
remove_a_key(&old),
change_list_length(&old),
replace_with_different_type(),
old.clone(),
];
for new in cases {
let patch = diff(&old, &new);
let rebuilt = apply(&old, &patch).unwrap();
assert_eq!(
new, rebuilt,
"diff+apply must reconstruct the new scene exactly"
);
}
}
#[test]
fn diff_of_identical_scenes_is_a_noop() {
let scene = sample_scene();
let patch = diff(&scene, &scene);
let rebuilt = apply(&scene, &patch).unwrap();
assert_eq!(scene, rebuilt);
}
#[test]
fn reordering_map_keys_reconstructs_exact_key_order() {
let old = sample_scene();
let new = reorder_keys(&old);
assert_eq!(old, new, "canonical equality ignores key order");
assert_ne!(
structural_repr(&old),
structural_repr(&new),
"the key ORDER must actually differ"
);
let patch = diff(&old, &new);
let rebuilt = apply(&old, &patch).unwrap();
assert_eq!(
structural_repr(&rebuilt),
structural_repr(&new),
"apply must reconstruct the exact key order of new, not the old order"
);
}
fn structural_repr(value: &Expr) -> String {
format!("{value:?}")
}
#[test]
fn a_scene_patch_is_itself_a_valid_scene() {
let old = sample_scene();
let new = edit_field_value(&old);
let patch = diff(&old, &new);
let mut cx = cx();
let codec = scene_codec_symbol();
let output = encode_with_codec(&mut cx, &codec, &patch, EncodeOptions::default()).unwrap();
let input = match output {
Output::Text(text) => Input::Text(text),
Output::Bytes(bytes) => Input::Bytes(bytes),
};
let decoded = decode_with_codec(&mut cx, &codec, input, ReadPolicy::default()).unwrap();
assert_eq!(patch, decoded);
assert_eq!(new, apply(&old, &decoded).unwrap());
}
fn edit_field_value(scene: &Expr) -> Expr {
let mut new = scene.clone();
set_top_key(&mut new, "live", Expr::Bool(false));
new
}
fn add_a_key(scene: &Expr) -> Expr {
let mut new = scene.clone();
set_top_key(&mut new, "added", Expr::String("hello".to_owned()));
new
}
fn remove_a_key(scene: &Expr) -> Expr {
let Expr::Map(entries) = scene else {
unreachable!()
};
Expr::Map(
entries
.iter()
.filter(|(key, _)| !matches!(key, Expr::Symbol(s) if &*s.name == "flags"))
.cloned()
.collect(),
)
}
fn reorder_keys(scene: &Expr) -> Expr {
let Expr::Map(entries) = scene else {
unreachable!()
};
let mut reversed = entries.clone();
reversed.reverse();
Expr::Map(reversed)
}
fn change_list_length(scene: &Expr) -> Expr {
let mut new = scene.clone();
set_top_key(&mut new, "ports", Expr::Vector(vec![sym("only-one")]));
new
}
fn replace_with_different_type() -> Expr {
node(
"box",
vec![("label", Expr::String("totally different".to_owned()))],
)
}
fn set_top_key(scene: &mut Expr, key: &str, value: Expr) {
let Expr::Map(entries) = scene else {
unreachable!()
};
if let Some(slot) = entries
.iter_mut()
.find(|(entry_key, _)| matches!(entry_key, Expr::Symbol(s) if &*s.name == key))
{
slot.1 = value;
} else {
entries.push((Expr::Symbol(Symbol::new(key)), value));
}
}