use std::sync::Arc;
use sim_codec::{Input, Output, decode_with_codec, encode_with_codec};
use sim_kernel::{Cx, DefaultFactory, EagerPolicy, EncodeOptions, Expr, ReadPolicy, Symbol};
use crate::gesture::{
GestureRecognizer, Hit, HitRole, PointerEvent, PointerPhase, intent_from_gesture,
};
use crate::{
INTENT_KINDS, IntentCodecLib, Origin, intent, intent_codec_symbol, intent_kind_of,
referenced_targets, required_fields, resolve_targets, validate_intent,
};
fn cx() -> Cx {
let mut cx = Cx::new(Arc::new(EagerPolicy), Arc::new(DefaultFactory));
sim_test_support::register_core_classes(&mut cx);
let lib = IntentCodecLib::new(cx.registry_mut().fresh_codec_id());
cx.load_lib(&lib).unwrap();
cx
}
use sim_value::build::sym;
fn sample_intent(kind: &str) -> Expr {
let fields = required_fields(kind)
.iter()
.map(|name| {
let value = if *name == "path" {
Expr::List(vec![Expr::Vector(vec![sym("k"), sym("title")])])
} else {
sym(&format!("{name}-value"))
};
(*name, value)
})
.collect();
intent(kind, Origin::human(7), fields)
}
#[test]
fn every_intent_kind_roundtrips_through_codec_intent() {
let mut cx = cx();
let codec = intent_codec_symbol();
for kind in INTENT_KINDS {
let value = sample_intent(kind);
validate_intent(&value).unwrap_or_else(|err| panic!("sample {kind} invalid: {err}"));
let output = encode_with_codec(&mut cx, &codec, &value, 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!(value, decoded, "kind {kind} must round-trip");
}
}
#[test]
fn validation_rejects_structural_problems() {
assert!(validate_intent(&Expr::Map(vec![])).is_err());
let unknown = Expr::Map(vec![(
sym("kind"),
Expr::Symbol(Symbol::qualified("intent", "nope")),
)]);
assert!(validate_intent(&unknown).is_err());
let no_origin = Expr::Map(vec![(
sym("kind"),
Expr::Symbol(Symbol::qualified("intent", "commit")),
)]);
assert!(validate_intent(&no_origin).is_err());
let bad_op = Expr::Map(vec![
(
sym("kind"),
Expr::Symbol(Symbol::qualified("intent", "commit")),
),
(
sym("origin"),
Expr::Map(vec![
(sym("operator"), sym("robot")),
(sym("at-tick"), sym("x")),
]),
),
(sym("pane"), sym("p")),
]);
assert!(validate_intent(&bad_op).is_err());
let missing = intent("wire", Origin::human(1), vec![("from", sym("a"))]);
assert!(validate_intent(&missing).is_err());
let bad_path = intent(
"edit-field",
Origin::human(1),
vec![
("target", sym("t")),
("path", sym("not-a-list")),
("value", sym("v")),
],
);
assert!(validate_intent(&bad_path).is_err());
}
#[test]
fn unknown_target_yields_a_diagnostic_not_a_mutation() {
let wire = intent(
"wire",
Origin::human(1),
vec![
(
"from",
Expr::Map(vec![(sym("node"), sym("n1")), (sym("port"), sym("out0"))]),
),
(
"to",
Expr::Map(vec![
(sym("node"), sym("missing")),
(sym("port"), sym("in0")),
]),
),
],
);
let known = |target: &Expr| matches!(target, Expr::Symbol(s) if &*s.name == "n1");
let error = resolve_targets(&wire, known).expect_err("unknown target must error");
assert!(
error.path_string().contains("to.node"),
"path: {}",
error.path_string()
);
let everything = |_: &Expr| true;
assert!(resolve_targets(&wire, everything).is_ok());
}
#[test]
fn referenced_targets_follow_the_kind() {
let select = intent(
"select",
Origin::human(1),
vec![("targets", Expr::List(vec![sym("a"), sym("b")]))],
);
assert_eq!(referenced_targets(&select).len(), 2);
let commit = intent("commit", Origin::human(1), vec![("pane", sym("p"))]);
assert!(referenced_targets(&commit).is_empty());
}
#[test]
fn performance_event_intent_targets_a_bound_source() {
let event = Expr::Map(vec![
(
sym("kind"),
Expr::Symbol(Symbol::qualified("music/performance-intent", "note-on")),
),
(sym("pitch"), Expr::String("60".to_owned())),
(sym("velocity"), Expr::String("96".to_owned())),
(sym("channel"), Expr::String("0".to_owned())),
]);
let target = Expr::Symbol(Symbol::qualified("music/performance-source", "keyboard"));
let value = intent(
"performance-event",
Origin::human(9),
vec![
("target", target.clone()),
(
"source",
Expr::Symbol(Symbol::qualified("music/performance-source", "keyboard")),
),
(
"input",
Expr::Symbol(Symbol::qualified("midi/input", "keyboard")),
),
("event", event),
],
);
validate_intent(&value).expect("performance event intent validates");
assert_eq!(
referenced_targets(&value),
vec![("target".to_owned(), target)]
);
}
#[test]
fn music_editor_intents_target_roll_and_rack() {
for kind in ["piano-roll-edit", "player-rack-edit", "arranger-edit"] {
let target = Expr::Symbol(Symbol::qualified("music/editor", kind));
let value = intent(
kind,
Origin::human(10),
vec![("target", target.clone()), ("action", sym("freeze"))],
);
validate_intent(&value).unwrap_or_else(|err| panic!("{kind}: {err}"));
assert_eq!(
referenced_targets(&value),
vec![("target".to_owned(), target)]
);
}
}
#[test]
fn mission_control_intents_require_mission_or_location() {
for kind in ["approve", "reject", "pause-agent", "rerun-validation"] {
let value = intent(kind, Origin::agent(1), vec![("mission", sym("m"))]);
validate_intent(&value).unwrap_or_else(|err| panic!("{kind}: {err}"));
assert_eq!(
referenced_targets(&value),
vec![("mission".to_owned(), sym("m"))]
);
}
let ask = intent(
"ask",
Origin::agent(2),
vec![
("mission", sym("m")),
("question", Expr::String("Proceed?".to_owned())),
],
);
validate_intent(&ask).expect("ask intent validates");
let split = intent(
"split-mission",
Origin::agent(3),
vec![
("mission", sym("m")),
("goals", Expr::List(vec![sym("a"), sym("b")])),
],
);
validate_intent(&split).expect("split-mission intent validates");
let replay = intent(
"replay-cassette",
Origin::human(4),
vec![("mission", sym("m")), ("at", sim_value::build::uint(2))],
);
validate_intent(&replay).expect("replay intent validates");
let open = intent(
"open-source",
Origin::human(5),
vec![("location", sym("span"))],
);
validate_intent(&open).expect("open-source intent validates");
assert_eq!(
referenced_targets(&open),
vec![("location".to_owned(), sym("span"))]
);
}
#[test]
fn recognizer_folds_pointer_stream_into_a_tap() {
let mut recognizer = GestureRecognizer::new();
let hit = Hit::on(HitRole::Node, sym("n1"));
assert!(recognizer.pointer(down(10.0, 10.0, hit.clone())).is_none());
let gesture = recognizer
.pointer(up(11.0, 11.0, hit.clone()))
.expect("release completes a gesture");
let value = intent_from_gesture(Origin::human(3), "pane-graph", &gesture).unwrap();
assert_eq!(
intent_kind_of(&value).map(|symbol| symbol.name.to_string()),
Some("select".to_owned())
);
}
#[test]
fn dragging_between_ports_wires_them() {
let mut recognizer = GestureRecognizer::new();
let from = Hit::on(HitRole::Port, sym("n1.out0"))
.with("node", sym("n1"))
.with("port", sym("out0"));
let to = Hit::on(HitRole::Port, sym("n2.in0"))
.with("node", sym("n2"))
.with("port", sym("in0"));
recognizer.pointer(down(0.0, 0.0, from));
recognizer.pointer(event(PointerPhase::Move, 40.0, 0.0, Hit::blank()));
let gesture = recognizer.pointer(up(80.0, 0.0, to)).expect("a drag");
let value = intent_from_gesture(Origin::human(1), "pane-graph", &gesture).unwrap();
assert_eq!(
intent_kind_of(&value).map(|symbol| symbol.name.to_string()),
Some("wire".to_owned())
);
validate_intent(&value).expect("a composed wire intent must validate");
}
#[test]
fn dragging_a_node_moves_it() {
let mut recognizer = GestureRecognizer::new();
let node = Hit::on(HitRole::Node, sym("n1"));
recognizer.pointer(down(0.0, 0.0, node));
recognizer.pointer(event(PointerPhase::Move, 50.0, 50.0, Hit::blank()));
let gesture = recognizer
.pointer(up(60.0, 70.0, Hit::blank()))
.expect("a drag");
let value = intent_from_gesture(Origin::human(1), "pane-graph", &gesture).unwrap();
assert_eq!(
intent_kind_of(&value).map(|symbol| symbol.name.to_string()),
Some("move".to_owned())
);
}
#[test]
fn key_commands_map_to_intents() {
let node = Hit::on(HitRole::Node, sym("n1"));
let delete = GestureRecognizer::key("delete", node);
let value = intent_from_gesture(Origin::agent(9), "pane-graph", &delete).unwrap();
assert_eq!(
intent_kind_of(&value).map(|symbol| symbol.name.to_string()),
Some("delete".to_owned())
);
let commit = GestureRecognizer::key("commit", Hit::blank());
let value = intent_from_gesture(Origin::human(1), "pane-graph", &commit).unwrap();
assert_eq!(
intent_kind_of(&value).map(|symbol| symbol.name.to_string()),
Some("commit".to_owned())
);
}
#[test]
fn meaningless_gestures_fail_closed() {
let button = Hit::on(HitRole::Button, sym("b"));
let tap = crate::gesture::RawGesture::Tap { hit: button };
assert!(intent_from_gesture(Origin::human(1), "p", &tap).is_err());
let drag = crate::gesture::RawGesture::Drag {
from: Hit::blank(),
to: Hit::blank(),
at: (1.0, 1.0),
};
assert!(intent_from_gesture(Origin::human(1), "p", &drag).is_err());
}
fn event(phase: PointerPhase, x: f64, y: f64, hit: Hit) -> PointerEvent {
PointerEvent { phase, x, y, hit }
}
fn down(x: f64, y: f64, hit: Hit) -> PointerEvent {
event(PointerPhase::Down, x, y, hit)
}
fn up(x: f64, y: f64, hit: Hit) -> PointerEvent {
event(PointerPhase::Up, x, y, hit)
}