mod common;
use std::collections::BTreeSet;
use delvewright_dsl::{DSL_VERSION, RawCampaign, check_campaign};
use serde_json::{Value, json};
const OLD: &str = DSL_VERSION;
const NEW: &str = DSL_VERSION;
struct Fixture {
plan_version: &'static str,
strand_optional: bool,
strand_deps: Vec<&'static str>,
finale_deps: Vec<&'static str>,
finale_optional: bool,
finale_requires: Option<&'static str>,
strand_sets: Option<&'static str>,
strand_after_finale: bool,
finale_triggered_by: Option<&'static str>,
}
impl Default for Fixture {
fn default() -> Self {
Fixture {
plan_version: NEW,
strand_optional: true,
strand_deps: vec![],
finale_deps: vec![],
finale_optional: false,
finale_requires: None,
strand_sets: None,
strand_after_finale: false,
finale_triggered_by: None,
}
}
}
const FINALE: &str = "quest/open-the-door";
const STRAND: &str = "quest/side-strand";
impl Fixture {
fn raw(&self) -> RawCampaign {
let mut c = common::valid_raw();
c.quest_plan = common::patch_doc(&c.quest_plan, |v| {
v["dsl_version"] = json!(self.plan_version);
let quests = v["content"]["quests"]
.as_array_mut()
.expect("stage-4 quests is an array");
assert_eq!(quests.len(), 1, "hello-world has exactly one planned quest");
quests[0]["mandatory"] = json!(!self.finale_optional);
quests[0]["depends_on"] = json!(self.finale_deps);
quests.push(json!({
"act": 1,
"area": "area/keep",
"depends_on": self.strand_deps,
"goal": "A side strand the mainline neither needs nor fears.",
"id": STRAND,
"mandatory": !self.strand_optional,
"npcs": [],
}));
});
c.quests = common::patch_doc(&c.quests, |v| {
v["dsl_version"] = json!(OLD);
let quests = v["content"]["quests"]
.as_array_mut()
.expect("stage-5 quests is an array");
assert_eq!(
quests.len(),
1,
"hello-world has exactly one expanded quest"
);
if let Some(flag) = self.finale_requires {
let objs = quests[0]["objectives"]
.as_array_mut()
.expect("objectives is an array");
objs[0]["requires_flags"] = json!([flag]);
}
if let Some(src) = self.finale_triggered_by {
quests[0]["trigger"] = json!({ "type": "quest-complete", "quest": src });
}
let trigger = if self.strand_after_finale {
json!({ "type": "quest-complete", "quest": FINALE })
} else {
json!({ "type": "campaign-start" })
};
let on_complete: Vec<Value> = match self.strand_sets {
Some(flag) => vec![json!({ "type": "set-flag", "flag": flag })],
None => vec![],
};
quests.push(json!({
"id": STRAND,
"objectives": [{
"anchor": "anchor/exit",
"id": "obj/strand-look",
"radius": 2,
"type": "reach-anchor",
}],
"on_complete": on_complete,
"trigger": trigger,
}));
});
c
}
fn codes(&self) -> BTreeSet<String> {
check_campaign(&self.raw())
.iter()
.map(|d| d.code.clone())
.collect()
}
}
fn set(codes: &[&str]) -> BTreeSet<String> {
codes.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn at_the_fence_optional_is_accepted() {
let f = Fixture::default();
assert_eq!(
f.codes(),
BTreeSet::new(),
"the same document at {NEW} must be green"
);
}
#[test]
fn the_optional_quest_is_off_the_spine() {
let raw = Fixture::default().raw();
let c = delvewright_dsl::parse_campaign(&raw).expect("fixture parses");
let spine = c.quest_plan.content.spine();
let optional = c.quest_plan.content.optional();
assert!(spine.contains(FINALE), "the finale is always on the spine");
assert!(
!spine.contains(STRAND),
"the strand must be off the closure"
);
assert_eq!(optional, [STRAND].into_iter().collect::<BTreeSet<_>>());
assert!(spine.intersection(&optional).next().is_none());
}
#[test]
fn an_optional_quest_may_be_triggered_by_the_mainline() {
let f = Fixture {
strand_after_finale: true,
..Fixture::default()
};
assert_eq!(f.codes(), BTreeSet::new());
}
#[test]
fn an_optional_quest_may_depend_on_a_mandatory_one() {
let f = Fixture {
strand_deps: vec![FINALE],
..Fixture::default()
};
assert_eq!(f.codes(), BTreeSet::new());
}
#[test]
fn an_optional_quest_in_the_finale_closure_is_refused() {
let f = Fixture {
finale_optional: true,
..Fixture::default()
};
assert_eq!(f.codes(), set(&["DW0866"]));
}
#[test]
fn a_mandatory_quest_off_the_closure_keeps_dw0132() {
let f = Fixture {
strand_optional: false,
..Fixture::default()
};
assert_eq!(f.codes(), set(&["DW0132"]));
}
#[test]
fn a_mandatory_quest_triggered_by_an_optional_one_is_refused() {
let f = Fixture {
finale_triggered_by: Some(STRAND),
..Fixture::default()
};
assert_eq!(f.codes(), set(&["DW0867"]));
}
#[test]
fn the_reversed_trigger_direction_is_green() {
let f = Fixture {
strand_after_finale: true,
..Fixture::default()
};
assert_eq!(f.codes(), BTreeSet::new());
}
#[test]
fn a_mandatory_depends_on_edge_reports_both_the_edge_and_the_closure() {
let f = Fixture {
finale_deps: vec![STRAND],
..Fixture::default()
};
assert_eq!(f.codes(), set(&["DW0866", "DW0867"]));
}
#[test]
fn a_mainline_key_behind_participation_is_refused() {
let f = Fixture {
finale_requires: Some("flag/strand-token"),
strand_sets: Some("flag/strand-token"),
..Fixture::default()
};
assert_eq!(f.codes(), set(&["DW0868"]));
}
#[test]
fn the_same_gate_greens_when_the_producer_is_mandatory() {
let f = Fixture {
finale_requires: Some("flag/strand-token"),
strand_sets: Some("flag/strand-token"),
strand_optional: false,
strand_deps: vec![],
finale_deps: vec![STRAND],
..Fixture::default()
};
assert_eq!(f.codes(), BTreeSet::new());
}
#[test]
fn every_refusal_binds_and_is_the_only_thing_that_could_have_caught_it() {
let cases: Vec<(&str, Fixture, &str)> = vec![
(
"finale declares itself optional",
Fixture {
finale_optional: true,
..Fixture::default()
},
"DW0866",
),
(
"mandatory finale triggered by an optional quest",
Fixture {
finale_triggered_by: Some(STRAND),
..Fixture::default()
},
"DW0867",
),
(
"mandatory objective keyed on an optional-only flag",
Fixture {
finale_requires: Some("flag/strand-token"),
strand_sets: Some("flag/strand-token"),
..Fixture::default()
},
"DW0868",
),
];
let mut bound = 0usize;
for (label, f, code) in &cases {
let got = f.codes();
assert!(got.contains(*code), "{label}: expected {code}, got {got:?}");
let withdrawn = Fixture {
strand_optional: false,
finale_optional: false,
..Fixture {
plan_version: f.plan_version,
strand_deps: f.strand_deps.clone(),
finale_deps: f.finale_deps.clone(),
finale_requires: f.finale_requires,
strand_sets: f.strand_sets,
strand_after_finale: f.strand_after_finale,
finale_triggered_by: f.finale_triggered_by,
..Fixture::default()
}
};
let after = withdrawn.codes();
assert!(
!after.contains(*code),
"{label}: {code} still fires with the partition withdrawn — something \
other than the optional declaration is catching this, so the \
demonstration says nothing about {code}. Got {after:?}"
);
bound += 1;
}
assert_eq!(bound, cases.len(), "every case must have been exercised");
assert!(bound >= 3, "binding count: {bound} refusals demonstrated");
}
#[test]
fn the_new_refusals_are_inert_when_nothing_is_optional() {
let new_codes = ["DW0866", "DW0867", "DW0868"];
let mut checked = 0usize;
for version in [DSL_VERSION, DSL_VERSION, OLD, NEW] {
for finale_deps in [vec![], vec![STRAND]] {
for strand_after_finale in [false, true] {
let f = Fixture {
plan_version: version,
strand_optional: false,
strand_deps: if finale_deps.is_empty() {
vec![FINALE]
} else {
vec![]
},
finale_deps: finale_deps.clone(),
strand_after_finale,
..Fixture::default()
};
let got = f.codes();
for code in new_codes {
assert!(
!got.contains(code),
"{code} fired at {version} with nothing optional: {got:?}"
);
}
checked += 1;
}
}
}
assert_eq!(checked, 16, "population actually swept");
}
#[test]
fn optional_does_not_launder_a_broken_strand() {
let mut c = Fixture::default().raw();
c.quests = common::patch_doc(&c.quests, |v| {
let quests = v["content"]["quests"].as_array_mut().unwrap();
let strand = quests
.iter_mut()
.find(|q| q["id"] == STRAND)
.expect("the strand is in the fixture");
strand["objectives"][0]["requires_flags"] = json!(["flag/nobody-sets-this"]);
});
let codes: BTreeSet<String> = check_campaign(&c).iter().map(|d| d.code.clone()).collect();
assert!(
codes.contains("DW0172"),
"an optional quest gated on an unproduced flag must still be refused, got {codes:?}"
);
}