// Copyright 2020-2021 Ian Jackson and contributors to Otter // SPDX-License-Identifier: AGPL-3.0-or-later // There is NO WARRANTY. // OTTER_JST_LOWER_ONLY=exhaustive-05 #![allow(clippy::or_fun_call)] #![allow(clippy::unnecessary_operation)] // trips on #[throws(Explode)] use otter_nodejs_tests::*; pub type Vpid = VisiblePieceId; #[derive(StructOpt,Debug,Clone)] pub struct Opts { pub nodejs: String, pub script: String, } #[derive(Debug,Clone)] pub struct StartPieceSpec { id: Vpid, pinned: bool, moveable: PieceMoveable, zupd: ZUpdateSpec, } #[macro_export] macro_rules! sp { { $id:expr, $pinned:expr, $moveable:ident } => { StartPieceSpec { id: $id.try_into().unwrap(), pinned: $pinned, zupd: ZUS::Auto, moveable: PieceMoveable::$moveable } }; { $id:expr, $pinned:expr, $moveable:ident, $z:expr, $zg:expr } => { StartPieceSpec { id: $id.try_into().unwrap(), pinned: $pinned, zupd: ZUS::Spec(ZLevel { z: $z.try_into().unwrap(), zg: Generation($zg), }), moveable: PieceMoveable::$moveable } }; } #[derive(Debug,Clone)] #[derive(Eq,PartialEq,Ord,PartialOrd)] #[derive(Serialize)] pub struct StartPiece { pinned: bool, moveable: PieceMoveable, zlevel: ZLevel, zupd: ZUSD, } #[derive(Debug,Clone,Default)] pub struct Tests { tests: IndexMap, only: Option, } #[derive(Debug,Clone,Default)] #[derive(Serialize)] pub struct Test { name: String, #[serde(with = "indexmap::serde_seq")] pieces: IndexMap, targets: IndexSet, } #[derive(Debug)] pub struct TestsAccumulator { tests: Tests, script: BufWriter, tera: tera::Tera, } #[derive(Debug,Clone,EnumDiscriminants)] #[strum_discriminants(derive(Ord,PartialOrd,Serialize))] pub enum ZUpdateSpec { Auto, Spec(ZLevel), GOnly, } use ZUpdateSpec as ZUS; use ZUpdateSpecDiscriminants as ZUSD; #[ext(pub)] impl ZUSD { fn show(&self) -> char { match self { ZUSD::Auto => ' ', ZUSD::Spec => '!', ZUSD::GOnly => 'g', } } } impl ZUpdateSpec { pub fn next(self, last: &mut zcoord::Mutable, lastg: &mut Generation) -> ZLevel { match self { ZUS::Auto => ZLevel { z: last.increment().unwrap(), zg: { lastg.increment(); *lastg }, }, ZUS::GOnly => ZLevel { z: last.repack().unwrap(), zg: { lastg.increment(); *lastg }, }, ZUS::Spec(zl) => { *last = zl.z.clone_mut(); *lastg = zl.zg; zl }, } } } pub struct ZLevelShow<'z>(pub &'z ZLevel); impl Display for ZLevelShow<'_> { #[throws(fmt::Error)] fn fmt(&self, f: &mut Formatter) { write!(f, "{:<21} {:6}", self.0.z.as_str(), self.0.zg)?; } } #[ext(pub)] impl ZLevel { fn show(&self) -> ZLevelShow<'_> { ZLevelShow(self) } } impl Test { #[throws(Explode)] pub fn check(&self) { println!("-------------------- {} --------------------", &self.name); let mut updated: HashMap = default(); let mut zg = Generation(100_000); for l in BufReader::new( fs::File::open(format!("{}.did",self.name))? ).lines() { let l = l?; let (op, id, z) = l.splitn(3,' ').collect_tuple().unwrap(); assert_eq!(op, "setz"); let id = id.try_into()?; let z = z.parse()?; let zlevel = ZLevel { z, zg }; zg.increment(); let was = updated.insert(id, zlevel); assert!(was.is_none(), "{:?}", id); } #[derive(Debug)] struct PieceCollated<'o,'n> { id: Vpid, old_z: &'o ZLevel, new_z: &'n ZLevel, target: bool, heavy: bool, updated: bool, zupd: ZUSD, } let coll = self.pieces.iter().map(|(&id, start)| { let old_z = &start.zlevel; let new_z = updated.get(&id); let updated = new_z.is_some(); let new_z = new_z.unwrap_or(&start.zlevel); PieceCollated { id, new_z, old_z, updated, heavy: start.heavy(), target: self.targets.contains(&id), zupd: start.zupd, } }).collect_vec(); let sorted = | kf: &dyn for <'r> Fn(&'r PieceCollated<'r,'r>) -> &'r _ | { let mut v: Vec<&PieceCollated> = coll.iter().collect_vec(); v.sort_by_key(|p| kf(p)); v }; let old = sorted(&|p: &PieceCollated| p.old_z); let new = sorted(&|p: &PieceCollated| p.new_z); for (o, n) in izip!(&old, &new).rev() { let pr = |p: &PieceCollated, zl: &ZLevel| { print!(" {:6} {}{}{} {} {}", p.id.to_string(), if p.target { "T" } else { "_" }, if p.heavy { "H" } else { "_" }, if p.updated { "U" } else { "_" }, p.zupd.show(), zl.show()); }; pr(o, o.old_z); print!(" "); pr(n, n.new_z); println!(""); } // light targets are in same stacking order as before // heavy targets are in same stacking order as before { for &want_heavy in &[false, true] { for (o, n) in izip!( old.iter().filter(|p| p.target && p.heavy == want_heavy), new.iter().filter(|p| p.target && p.heavy == want_heavy), ) { assert_eq!(o.id, n.id); } } } // no heavy are newly above light let old_misstacked = { let misheavy = |on: &[&PieceCollated]| { let mut misheavy = HashSet::new(); for i in 0..on.len() { for j in i+1..on.len() { // j is above i if on[j].heavy && ! on[i].heavy { // heavy above light misheavy.insert((on[j].id, on[i].id)); } } } misheavy }; let old = misheavy(&old); let new = misheavy(&new); let newly = new.difference(&old).collect_vec(); assert!( newly.is_empty(), "{:?}", &newly ); old }; // no light non-targets moved { for n in &new { if ! n.heavy && ! n.target { assert!( ! n.updated, "{:?}", n ); } } } // z levels (at least of heavy) in updates all decrease { for n in &new { if n.heavy && n.updated { assert!( n.new_z < n.old_z, "{:?}", &n ); } } } // all targets now below all light non-targets { let mut had_light_nontarget = None; for n in &new { if ! n.heavy && ! n.target { had_light_nontarget = Some(n); } if n.target { assert!( had_light_nontarget.is_none(), "{:?} {:?}", &n, had_light_nontarget); } } } // all heavy targets now below all non-targets { let mut had_nontarget = None; for n in &new { if ! n.target { had_nontarget = Some(n); } if n.heavy && n.target { assert!( had_nontarget.is_none(), "{:?} {:?}", &n, had_nontarget); } } } // all the z levels are still distinct and ordered { for (n0,n1) in new.iter().tuple_windows() { assert!( n1.new_z > n0.new_z, "{:?} {:?}", &n0, &n1 ); } } // non-targets are moved only if they things are funky { // funky could be one of: // - misstacked heavy // - heavy with same Z Coord (but obvs not Gen) as some light if old_misstacked.is_empty() && ! old.iter().tuple_windows().any(|(o0,o1)| { o0.heavy && ! o1.heavy && o1.old_z.z == o0.old_z.z }) { for n in &new { if n.updated { assert!( n.target, "{:?}", n ); } } } } } } impl StartPiece { pub fn heavy(&self) -> bool { use PieceMoveable::*; match (self.pinned, self.moveable) { (true , _ ) => true, (false, Yes) => false, (false, No ) => true, (_, IfWresting) => panic!(), } } } impl TestsAccumulator { #[throws(Explode)] pub fn new(opts: &Opts) -> Self { let mut tera = tera::Tera::default(); tera.add_raw_template("js", TEMPLATE)?; let script = fs::OpenOptions::new() .write(true) .append(true) .truncate(false) .create(false) .open(&opts.script)?; let script = BufWriter::new(script); let mut tests: Tests = default(); if let Some(only) = env::var_os("OTTER_JST_LOWER_ONLY") { tests.only = Some(only.into_string().unwrap()) } TestsAccumulator { tests, script, tera, } } #[throws(Explode)] pub fn finalise(mut self) -> Tests { self.script.flush()?; self.tests } #[throws(Explode)] pub fn add_test(&mut self, name: &str, pieces: Vec, targets: Vec) where T: TryInto + Copy + Debug, { if let Some(only) = &self.tests.only { if name != only { return; } } let mut zlast = ZCoord::default().clone_mut(); let mut zlastg = Generation(1000); let pieces: IndexMap = pieces.into_iter().map( |StartPieceSpec { id, pinned, moveable, zupd }| { let zupd_d = (&zupd).into(); let zlevel = zupd.next(&mut zlast, &mut zlastg); (id, StartPiece { pinned, moveable, zlevel, zupd: zupd_d }) } ).collect(); let targets: IndexSet<_> = targets.into_iter().map( |s| s.try_into().map_err(|_|s).unwrap() ).collect(); println!("-------------------- {} --------------------", name); for (id,p) in pieces.iter().rev() { println!(" {:6} {}{} {} {}", id.to_string(), if targets.contains(id) { "T" } else { "_" }, if p.heavy() { "H" } else { "_" }, p.zupd.show(), p.zlevel.show()); } let test = Test { name: name.into(), pieces, targets, }; let context = tera::Context::from_serialize(&test)?; self.tera.render_to("js", &context, &mut self.script)?; let already = self.tests.tests.insert(name.to_owned(), test); assert!(already.is_none(), "duplicate test {:?}", &name); } #[throws(Explode)] pub fn add_exhaustive(&mut self, nameprefix: &str, zupds: &[ZUpdateSpec]) { let n: usize = match zupds.len() { 1 => 5, 2 => 4, _ => panic!() }; let ids: Vec = (0..n).map( |i| format!("{}.{}", i+1, 1).try_into().unwrap() ).collect_vec(); let pieces_configs = ids.iter().cloned().map(|id| { iproduct!( [false,true].iter().cloned(), zupds.iter().cloned() ).map( move |(bottom,zupd)| { StartPieceSpec { id, pinned: bottom, moveable: PieceMoveable::Yes, zupd, } }) }) .multi_cartesian_product(); let target_configs = ids.iter().cloned() .powerset(); for (ti, (pieces, targets)) in itertools::iproduct!( pieces_configs, target_configs ).enumerate() { if targets.is_empty() { continue } let name = format!("exhaustive-{}-{:02x}", nameprefix, ti); self.add_test(&name, pieces, targets)?; } } } impl Tests { #[throws(AE)] fn finish(self) { match self.only { None => { }, Some(only) => { println!("^^^^^^^^^^^^^^^^^^^^ success ^^^^^^^^^^^^^^^^^^^^"); throw!(anyhow!("tests limited to {}, treating as failure", &only)) } } } } #[throws(Explode)] fn main() { let opts = Opts::from_args(); println!("==================== building ===================="); let mut ta = TestsAccumulator::new(&opts)?; ta.add_test("simple", vec![ sp!("1.1", false, Yes), sp!("2.1", false, Yes), ], vec![ "2.1", ])?; ta.add_test("pair", vec![ sp!("1.1", false, Yes), sp!("2.1", false, Yes), sp!("3.1", false, Yes), ], vec![ "3.1", "2.1", ])?; ta.add_test("found-2021-07-07-raises", vec![ sp!( "87.7", false, No), sp!( "81.7", false, Yes), sp!("110.7", false, Yes), // HELD 1#1 sp!( "64.7", false, No), sp!( "59.7", false, No), // HELD 2#1 sp!( "62.7", false, Yes), sp!( "73.7", false, Yes), sp!( "46.7", false, No), sp!( "7.7", false, Yes), ], vec![ "73.7", ])?; ta.add_test("found-2021-07-15-lowers", vec![ sp!( "87.11", false, No , "g1ea000000" , 38558 ), sp!( "59.11", false, No , "g1ea000000_0000001000", 39858 ), sp!( "64.11", false, No , "g1ea000000_0000002000", 39859 ), sp!( "23.11", false, Yes, "g1ea000000_0000002001", 46890 ), sp!( "96.11", false, Yes, "g1ea000000_0000002002", 46846 ), sp!( "8.11", false, Yes, "g1ea000000_0000002040", 45196 ), sp!( "46.11", false, No , "g1eb000000" , 38559 ), sp!( "66.11", false, No , "g1ed000000" , 38561 ), sp!( "47.11", false, Yes, "g1ee000000_0000004000", 47077 ), sp!( "11.11", false, Yes, "g1ee000000_000000c000", 49354 ), sp!("112.11", false, Yes, "g1qi000000" , 49288 ), sp!( "77.11", false, Yes, "g1ql000000" , 49436 ), // HELD 1#1 ], vec![ "77.11", ])?; ta.add_exhaustive("z", &[ZUS::Auto ])?; ta.add_exhaustive("m", &[ZUS::Auto, ZUS::GOnly])?; ta.add_exhaustive("g", &[ ZUS::GOnly])?; let tests = ta.finalise()?; println!("==================== running ===================="); let mut cmd = Command::new(opts.nodejs); cmd.arg(opts.script); let status = cmd.status()?; assert!(status.success(), "{}", status); println!("==================== checking ===================="); for test in tests.tests.values() { test.check()?; } tests.finish()?; } static TEMPLATE: &str = r#" console.log('-------------------- {{ name }} --------------------') jstest_did = fs.openSync('{{ name }}.did', 'w'); pieces = { {% for p in pieces -%} '{{ p.0 }}': { pinned: {{ p.1.pinned }}, moveable: '{{ p.1.moveable }}', z: '{{ p.1.zlevel.z }}', zg: '{{ p.1.zlevel.zg }}', }, {% endfor -%} } fake_dom = [ { special: "pieces_marker", dataset: { } }, {%- for p in pieces %} { dataset: { piece: "{{ p.0 }}" } }, {%- endfor -%} { special: "defs_marker", dataset: { } }, ]; pieces_marker = fake_dom[0]; defs_marker = fake_dom[{{ pieces | length + 1 }}]; {%- for p in pieces %} fake_dom[{{ loop.index0 }}].nextElementSibling = fake_dom[{{ loop.index }}]; {%- endfor %} fake_dom[{{ pieces | length }}].nextElementSibling = fake_dom[{{ pieces | length + 1 }}]; defs_marker.previousElementSibling = fake_dom[{{ pieces | length }}]; uorecord = { targets: [ {%- for t in targets %} '{{ t }}', {%- endfor %} ], }; lower_targets(uorecord); fs.closeSync(jstest_did); jstest_did = -1; "#;