mod common;
use delvewright_dsl::{RawCampaign, check_campaign};
use serde_json::{Value, json};
use std::sync::LazyLock;
static GREEN: LazyLock<String> = LazyLock::new(|| {
common::at_dsl_version(
r#"{
"campaign_id": "hello-world",
"dsl_version": "%dsl_version%",
"stage": "layout-graph",
"content": {
"nodes": [
{ "id": "node/porch", "intent": "threshold" },
{ "id": "node/hall", "intent": "hub", "note": "where the keeper stands" },
{ "id": "node/cellar", "intent": "cache" },
{ "id": "node/vault", "intent": "goal-chamber" }
],
"edges": [
{ "id": "edge/porch-hall", "class": "walk", "a": "node/porch", "b": "node/hall" },
{ "id": "edge/hall-vault", "class": "barred", "a": "node/hall", "b": "node/vault",
"opens_from": "a", "gating": { "quest": "quest/open-the-door" } },
{ "id": "edge/cellar-stair", "class": "stair", "a": "node/hall", "b": "node/cellar" },
{ "id": "edge/cellar-drop", "class": "drop", "a": "node/porch", "b": "node/cellar",
"falls": "a-to-b" },
{ "id": "edge/vault-shortcut", "class": "walk", "a": "node/vault", "b": "node/porch",
"one_way": "a-to-b", "shortcut": true },
{ "id": "edge/vault-sightline", "class": "vision", "a": "node/porch", "b": "node/vault" }
],
"entry": "node/porch",
"goal": "node/vault",
"critical_path": ["node/porch", "node/hall", "node/vault"],
"beats": [
{ "quest": "quest/open-the-door", "objective": "obj/talk", "node": "node/porch" },
{ "quest": "quest/open-the-door", "objective": "obj/exit", "node": "node/hall" }
]
}
}"#,
)
});
static BRIEF: LazyLock<String> = LazyLock::new(|| {
common::at_dsl_version(
r#"{
"campaign_id": "hello-world",
"dsl_version": "%dsl_version%",
"stage": "geometry-brief",
"content": {
"facts": [
{ "id": "fact/hall-span", "value": 16.0, "unit": "blocks", "note": "The hall is sixteen across." }
]
}
}"#,
)
});
fn graph_with(patch: impl Fn(&mut Value)) -> String {
let mut v: Value = serde_json::from_str(GREEN.as_str()).expect("the green graph parses");
patch(&mut v);
serde_json::to_string(&v).expect("re-serialize")
}
fn campaign(graph: Option<String>, brief: Option<String>) -> RawCampaign {
RawCampaign {
layout_graph: graph,
site_plan: None,
detail_plan: None,
design: None,
geometry_brief: brief,
..common::valid_raw()
}
}
fn codes_of(raw: &RawCampaign) -> Vec<String> {
check_campaign(raw).into_iter().map(|d| d.code).collect()
}
fn validate(graph: Option<String>) -> Vec<String> {
codes_of(&campaign(graph, Some(BRIEF.to_string())))
}
fn reachability(graph: Option<String>) -> Vec<String> {
let raw = campaign(graph, Some(BRIEF.to_string()));
let c = delvewright_dsl::parse_campaign(&raw).expect("the fixture parses");
delvewright_dsl::layout::reachability(&c)
.into_iter()
.map(|d| d.code)
.collect()
}
fn nodes(v: &mut Value) -> &mut Vec<Value> {
v["content"]["nodes"].as_array_mut().expect("nodes")
}
fn edges(v: &mut Value) -> &mut Vec<Value> {
v["content"]["edges"].as_array_mut().expect("edges")
}
#[test]
fn the_hand_drawn_map_is_accepted() {
let got = validate(Some(GREEN.to_string()));
assert_eq!(
got,
vec!["DW0822".to_string(), "DW0813".to_string()],
"the green map should raise nothing but the two advisories"
);
assert!(reachability(Some(GREEN.to_string())).is_empty());
}
#[test]
fn a_campaign_with_no_map_documents_binds_nothing() {
assert!(codes_of(&campaign(None, None)).is_empty());
let raw = campaign(None, None);
let c = delvewright_dsl::parse_campaign(&raw).expect("parses");
let b = delvewright_dsl::LayoutBinding::of(&c);
assert_eq!(b, delvewright_dsl::LayoutBinding::default());
assert!(delvewright_dsl::layout::reachability(&c).is_empty());
}
#[test]
fn the_binding_ledger_counts_what_the_document_holds() {
let raw = campaign(Some(GREEN.to_string()), Some(BRIEF.to_string()));
let c = delvewright_dsl::parse_campaign(&raw).expect("parses");
let b = delvewright_dsl::LayoutBinding::of(&c);
assert_eq!(b.nodes, 4);
assert_eq!(b.edges, 6);
assert_eq!(b.traversal_edges, 5, "five of the six carry a body");
assert_eq!(b.one_way_edges, 2, "the drop and the shortcut");
assert_eq!(b.shortcut_edges, 1);
assert_eq!(b.gated_edges, 1, "only the barred door demands anything");
assert_eq!(b.beats, 2);
assert_eq!(b.spine_beats, 2, "the one quest is the finale");
assert_eq!(b.path_steps, 2);
assert_eq!(b.brief_facts, 1);
assert!(b.line().contains("4 node(s)"));
assert!(b.line().contains("1 fact(s)"));
}
#[test]
fn dw0814_a_duplicate_node_id() {
let g = graph_with(|v| {
let first = nodes(v)[0].clone();
nodes(v).push(first);
});
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
}
#[test]
fn dw0814_a_duplicate_edge_id() {
let g = graph_with(|v| {
let first = edges(v)[0].clone();
edges(v).push(first);
});
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
}
#[test]
fn dw0814_an_edge_end_naming_no_place() {
let g = graph_with(|v| edges(v)[0]["b"] = json!("node/nowhere"));
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
}
#[test]
fn dw0814_a_self_loop_at_every_class() {
for i in 0..6 {
let g = graph_with(|v| {
let a = edges(v)[i]["a"].clone();
edges(v)[i]["b"] = a;
});
let got = validate(Some(g));
assert!(
got.contains(&"DW0814".to_string()),
"edge {i} looped on itself and was accepted: {got:?}"
);
}
}
#[test]
fn dw0814_an_entry_or_goal_that_is_not_a_place() {
let g = graph_with(|v| v["content"]["entry"] = json!("node/nowhere"));
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
let g = graph_with(|v| v["content"]["goal"] = json!("node/nowhere"));
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
}
#[test]
fn dw0814_a_critical_path_step_or_beat_naming_no_place() {
let g = graph_with(|v| v["content"]["critical_path"][1] = json!("node/nowhere"));
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
let g = graph_with(|v| v["content"]["beats"][0]["node"] = json!("node/nowhere"));
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
}
#[test]
fn dw0814_a_place_with_no_intent() {
let g = graph_with(|v| nodes(v)[0]["intent"] = json!(" "));
assert!(validate(Some(g)).contains(&"DW0814".to_string()));
}
#[test]
fn dw0110_malformed_ids_are_the_ordinary_refusal() {
let g = graph_with(|v| nodes(v)[0]["id"] = json!("porch"));
assert!(validate(Some(g)).contains(&"DW0110".to_string()));
let g = graph_with(|v| edges(v)[0]["id"] = json!("Edge/Porch"));
assert!(validate(Some(g)).contains(&"DW0110".to_string()));
let bad_brief = BRIEF.replace("fact/hall-span", "hall-span");
let got = codes_of(&campaign(Some(GREEN.to_string()), Some(bad_brief)));
assert!(got.contains(&"DW0110".to_string()));
}
#[test]
fn dw0111_two_brief_facts_of_one_name() {
let mut v: Value = serde_json::from_str(BRIEF.as_str()).expect("parses");
let first = v["content"]["facts"][0].clone();
v["content"]["facts"]
.as_array_mut()
.expect("facts")
.push(first);
let got = codes_of(&campaign(
Some(GREEN.to_string()),
Some(serde_json::to_string(&v).expect("re-serialize")),
));
assert!(got.contains(&"DW0111".to_string()));
}
#[test]
fn dw0100_a_field_the_class_does_not_read() {
let g = graph_with(|v| edges(v)[0]["opens_from"] = json!("a"));
assert!(validate(Some(g)).contains(&"DW0100".to_string()));
let g = graph_with(|v| edges(v)[0]["falls"] = json!("a-to-b"));
assert!(validate(Some(g)).contains(&"DW0100".to_string()));
let g = graph_with(|v| {
edges(v)[3].as_object_mut().expect("object").remove("falls");
});
assert!(validate(Some(g)).contains(&"DW0100".to_string()));
}
#[test]
fn dw0813_names_the_standards_this_graph_rested_on() {
let raw = campaign(Some(GREEN.to_string()), Some(BRIEF.to_string()));
let notice = check_campaign(&raw)
.into_iter()
.find(|d| d.code == "DW0813")
.expect("the graph reads building metrics, so the notice is owed");
assert!(
notice.message.contains("pacing.route-blocks-per-minute"),
"the coefficient is named: {}",
notice.message
);
assert!(!notice.message.contains("opening."), "{}", notice.message);
}
#[test]
fn dw0818_a_beat_naming_a_quest_or_objective_that_does_not_exist() {
let g = graph_with(|v| v["content"]["beats"][0]["quest"] = json!("quest/nowhere"));
assert!(validate(Some(g)).contains(&"DW0818".to_string()));
let g = graph_with(|v| v["content"]["beats"][0]["objective"] = json!("obj/nowhere"));
assert!(validate(Some(g)).contains(&"DW0818".to_string()));
}
#[test]
fn dw0818_an_unwritten_stage_five_points_at_dw0150_instead_of_blaming_the_graph() {
let raw = RawCampaign {
quests: common::patch_doc(&common::read_valid("quests.json"), |v| {
v["content"] = json!({ "quests": [] });
}),
..campaign(Some(GREEN.to_string()), Some(BRIEF.to_string()))
};
let diags = check_campaign(&raw);
let borrowed: Vec<_> = diags
.iter()
.filter(|d| d.code == "DW0818" && d.path.ends_with("/quest"))
.collect();
assert_eq!(
borrowed.len(),
3,
"two beats and one gated connection: {diags:#?}"
);
for d in &borrowed {
assert!(d.message.contains("DW0150"), "{}", d.message);
assert!(
d.message.contains("Stage 5 declares no quests at all"),
"{}",
d.message
);
}
let g = graph_with(|v| v["content"]["beats"][0]["quest"] = json!("quest/nowhere"));
let d = check_campaign(&campaign(Some(g), Some(BRIEF.to_string())))
.into_iter()
.find(|d| d.code == "DW0818")
.expect("a beat naming a quest stage 5 does not have is still DW0818");
assert!(!d.message.contains("DW0150"), "{}", d.message);
}
#[test]
fn dw0818_gating_on_state_the_campaign_never_produces() {
let g = graph_with(|v| edges(v)[1]["gating"] = json!({ "flags": ["flag/never-set"] }));
assert!(validate(Some(g)).contains(&"DW0818".to_string()));
let g = graph_with(|v| edges(v)[1]["gating"] = json!({ "quest": "quest/nowhere" }));
assert!(validate(Some(g)).contains(&"DW0818".to_string()));
}
#[test]
fn dw0818_a_barred_way_that_says_nothing_about_what_opens_it() {
let g = graph_with(|v| edges(v)[1]["gating"] = json!({}));
assert!(validate(Some(g)).contains(&"DW0818".to_string()));
}
#[test]
fn dw0818_the_reverse_direction_every_beat_has_a_place() {
let g = graph_with(|v| {
v["content"]["beats"]
.as_array_mut()
.expect("beats")
.remove(1);
});
let got = validate(Some(g));
assert!(got.contains(&"DW0818".to_string()), "{got:?}");
let g = graph_with(|v| v["content"]["beats"] = json!([]));
let got = validate(Some(g));
assert_eq!(
got.iter().filter(|c| *c == "DW0818").count(),
2,
"one refusal per objective the graph left placeless: {got:?}"
);
}
#[test]
fn dw0818_a_beat_bound_to_two_places() {
let g = graph_with(|v| {
let mut second = v["content"]["beats"][0].clone();
second["node"] = json!("node/cellar");
v["content"]["beats"]
.as_array_mut()
.expect("beats")
.push(second);
});
assert!(validate(Some(g)).contains(&"DW0818".to_string()));
}
#[test]
fn dw0820_a_shortcut_that_is_the_only_way_between_its_places() {
let earned = graph_with(|v| edges(v)[2]["shortcut"] = json!(true));
assert!(
!validate(Some(earned)).contains(&"DW0820".to_string()),
"the stair lies on a loop while the drop is there"
);
let g = graph_with(|v| {
edges(v).remove(3); edges(v)[2]["shortcut"] = json!(true);
});
let got = validate(Some(g));
assert!(got.contains(&"DW0820".to_string()), "{got:?}");
}
#[test]
fn dw0820_the_same_mark_reds_once_its_loop_is_taken_away() {
assert!(!validate(Some(GREEN.to_string())).contains(&"DW0820".to_string()));
let g = graph_with(|v| {
edges(v).remove(1);
});
let got = validate(Some(g));
assert!(got.contains(&"DW0820".to_string()), "{got:?}");
}
#[test]
fn dw0820_reads_the_loop_spatially() {
let g = graph_with(|v| edges(v)[1]["one_way"] = json!("a-to-b"));
assert!(!validate(Some(g)).contains(&"DW0820".to_string()));
}
#[test]
fn dw0822_projects_the_route_and_refuses_nothing() {
let raw = campaign(Some(GREEN.to_string()), Some(BRIEF.to_string()));
let d = check_campaign(&raw)
.into_iter()
.find(|d| d.code == "DW0822")
.expect("the projection is printed on every graph");
assert_eq!(d.severity, delvewright_dsl::Severity::Warning);
assert!(d.message.contains("0 place(s)"), "{}", d.message);
assert!(d.message.contains("2 step(s)"));
assert!(d.message.contains("3 leg(s) UNPROJECTED"), "{}", d.message);
assert!(
d.message
.contains("carries no threshold and refuses nothing"),
"{}",
d.message
);
assert!(d.message.contains("`target_minutes`"), "{}", d.message);
assert!(
!check_campaign(&raw)
.iter()
.any(|d| d.severity == delvewright_dsl::Severity::Error)
);
}
#[test]
fn dw0816_a_place_the_closure_never_reaches() {
let g = graph_with(|v| {
edges(v).remove(3); edges(v).remove(2); });
let got = reachability(Some(g));
assert_eq!(
got,
vec!["DW0816".to_string()],
"exactly one place is cut off"
);
}
#[test]
fn dw0816_a_place_behind_state_nothing_reachable_grants() {
let g = graph_with(|v| v["content"]["beats"][1]["node"] = json!("node/vault"));
let got = reachability(Some(g));
assert!(got.contains(&"DW0816".to_string()), "{got:?}");
}
#[test]
fn dw0817_a_path_that_does_not_start_where_a_body_starts() {
let g = graph_with(|v| v["content"]["critical_path"] = json!(["node/hall", "node/vault"]));
assert!(reachability(Some(g)).contains(&"DW0817".to_string()));
}
#[test]
fn dw0817_a_step_no_connection_makes() {
let g = graph_with(|v| {
v["content"]["critical_path"] =
json!(["node/porch", "node/cellar", "node/hall", "node/vault"]);
});
let g2 = graph_with(|v| {
v["content"]["critical_path"] = json!(["node/porch", "node/vault"]);
});
assert!(!reachability(Some(g)).contains(&"DW0817".to_string()));
assert!(reachability(Some(g2)).contains(&"DW0817".to_string()));
}
#[test]
fn dw0817_a_step_through_a_gate_that_is_not_open_yet() {
let g = graph_with(|v| {
edges(v)[4]["one_way"] = json!("b-to-a");
v["content"]["critical_path"] = json!(["node/porch", "node/vault"]);
});
let got = reachability(Some(g));
assert!(got.contains(&"DW0817".to_string()), "{got:?}");
}
#[test]
fn dw0817_a_mandatory_beat_the_path_never_visits() {
let g = graph_with(|v| v["content"]["beats"][1]["node"] = json!("node/cellar"));
let got = reachability(Some(g));
assert!(got.contains(&"DW0817".to_string()), "{got:?}");
}
#[test]
fn dw0819_a_one_way_drop_into_a_place_with_no_way_out() {
let g = graph_with(|v| {
edges(v).remove(2); });
let got = reachability(Some(g));
assert!(got.contains(&"DW0819".to_string()), "{got:?}");
assert!(!got.contains(&"DW0816".to_string()), "{got:?}");
}
#[test]
fn dw0819_reads_what_the_body_can_be_holding() {
let g = graph_with(|v| {
edges(v)[2]["gating"] = json!({ "quest": "quest/open-the-door" });
});
let got = reachability(Some(g));
assert!(got.contains(&"DW0819".to_string()), "{got:?}");
}
#[test]
fn dw0816_the_closure_crosses_a_gated_carry_once_granted_and_not_before() {
use delvewright_dsl::layout::{Closure, Grant, Grants, LayoutGraphContent};
let graph: LayoutGraphContent = serde_json::from_value(json!({
"nodes": [
{ "id": "node/jetty", "intent": "jetty" },
{ "id": "node/far-shore", "intent": "landing" }
],
"edges": [
{ "id": "edge/strait", "class": "carry", "a": "node/jetty", "b": "node/far-shore",
"one_way": "a-to-b", "gating": { "flags": ["flag/boarded"] } }
],
"entry": "node/jetty",
"goal": "node/far-shore",
"critical_path": ["node/jetty", "node/far-shore"]
}))
.expect("the two-node graph parses");
let mut granted = Grants::default();
granted.by_node.insert(
"node/jetty".to_string(),
[Grant::Flag("flag/boarded".to_string())]
.into_iter()
.collect(),
);
let c = Closure::run(&graph, &granted);
assert!(c.reached.contains("node/far-shore"), "{:?}", c.reached);
let c = Closure::run(&graph, &Grants::default());
assert!(!c.reached.contains("node/far-shore"), "{:?}", c.reached);
let mut back = graph.clone();
back.entry = delvewright_dsl::ids::NodeId("node/far-shore".to_string());
let mut granted_far = Grants::default();
granted_far.by_node.insert(
"node/far-shore".to_string(),
[Grant::Flag("flag/boarded".to_string())]
.into_iter()
.collect(),
);
let c = Closure::run(&back, &granted_far);
assert!(!c.reached.contains("node/jetty"), "{:?}", c.reached);
}
#[test]
fn dw0818_a_carry_with_an_empty_gating() {
let g = graph_with(|v| {
edges(v)[1] = json!({ "id": "edge/hall-vault", "class": "carry", "a": "node/hall",
"b": "node/vault", "gating": {} });
});
assert!(validate(Some(g)).contains(&"DW0818".to_string()));
}