1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
5 // OTTER_JST_LOWER_ONLY=exhaustive-05
7 #![allow(clippy::or_fun_call)]
8 #![allow(clippy::unnecessary_operation)] // trips on #[throws(Explode)]
10 use otter_nodejs_tests::*;
12 pub type Vpid = VisiblePieceId;
14 #[derive(StructOpt,Debug,Clone)]
20 #[derive(Debug,Clone)]
21 pub struct StartPieceSpec {
24 moveable: PieceMoveable,
30 { $id:expr, $pinned:expr, $moveable:ident } => {
31 StartPieceSpec { id: $id.try_into().unwrap(), pinned: $pinned,
33 moveable: PieceMoveable::$moveable }
35 { $id:expr, $pinned:expr, $moveable:ident, $z:expr, $zg:expr } => {
36 StartPieceSpec { id: $id.try_into().unwrap(), pinned: $pinned,
37 zupd: ZUS::Spec(ZLevel {
38 z: $z.try_into().unwrap(),
41 moveable: PieceMoveable::$moveable }
45 #[derive(Debug,Clone)]
46 #[derive(Eq,PartialEq,Ord,PartialOrd)]
48 pub struct StartPiece {
50 moveable: PieceMoveable,
55 #[derive(Debug,Clone,Default)]
57 tests: IndexMap<String, Test>,
61 #[derive(Debug,Clone,Default)]
65 #[serde(with = "indexmap::serde_seq")]
66 pieces: IndexMap<Vpid, StartPiece>,
67 targets: IndexSet<Vpid>,
71 pub struct TestsAccumulator {
73 script: BufWriter<fs::File>,
77 #[derive(Debug,Clone,EnumDiscriminants)]
78 #[strum_discriminants(derive(Ord,PartialOrd,Serialize))]
79 pub enum ZUpdateSpec {
84 use ZUpdateSpec as ZUS;
85 use ZUpdateSpecDiscriminants as ZUSD;
89 fn show(&self) -> char {
99 pub fn next(self, last: &mut zcoord::Mutable, lastg: &mut Generation)
102 ZUS::Auto => ZLevel {
103 z: last.increment().unwrap(),
104 zg: { lastg.increment(); *lastg },
106 ZUS::GOnly => ZLevel {
107 z: last.repack().unwrap(),
108 zg: { lastg.increment(); *lastg },
111 *last = zl.z.clone_mut();
119 pub struct ZLevelShow<'z>(pub &'z ZLevel);
120 impl Display for ZLevelShow<'_> {
121 #[throws(fmt::Error)]
122 fn fmt(&self, f: &mut Formatter) {
123 write!(f, "{:<21} {:6}", self.0.z.as_str(), self.0.zg)?;
128 fn show(&self) -> ZLevelShow<'_> { ZLevelShow(self) }
133 pub fn check(&self) {
134 println!("-------------------- {} --------------------", &self.name);
136 let mut updated: HashMap<Vpid, ZLevel> = default();
137 let mut zg = Generation(100_000);
139 for l in BufReader::new(
140 fs::File::open(format!("{}.did",self.name))?
143 let (op, id, z) = l.splitn(3,' ').collect_tuple().unwrap();
144 assert_eq!(op, "setz");
145 let id = id.try_into()?;
147 let zlevel = ZLevel { z, zg };
149 let was = updated.insert(id, zlevel);
150 assert!(was.is_none(), "{:?}", id);
154 struct PieceCollated<'o,'n> {
164 let coll = self.pieces.iter().map(|(&id, start)| {
165 let old_z = &start.zlevel;
166 let new_z = updated.get(&id);
167 let updated = new_z.is_some();
168 let new_z = new_z.unwrap_or(&start.zlevel);
170 id, new_z, old_z, updated,
171 heavy: start.heavy(),
172 target: self.targets.contains(&id),
177 let sorted = | kf: &dyn for <'r> Fn(&'r PieceCollated<'r,'r>) -> &'r _ | {
178 let mut v: Vec<&PieceCollated> = coll.iter().collect_vec();
179 v.sort_by_key(|p| kf(p));
182 let old = sorted(&|p: &PieceCollated| p.old_z);
183 let new = sorted(&|p: &PieceCollated| p.new_z);
184 for (o, n) in izip!(&old, &new).rev() {
185 let pr = |p: &PieceCollated, zl: &ZLevel| {
186 print!(" {:6} {}{}{} {} {}",
188 if p.target { "T" } else { "_" },
189 if p.heavy { "H" } else { "_" },
190 if p.updated { "U" } else { "_" },
194 pr(o, o.old_z); print!(" ");
195 pr(n, n.new_z); println!("");
198 // light targets are in same stacking order as before
199 // heavy targets are in same stacking order as before
201 for &want_heavy in &[false, true] {
203 old.iter().filter(|p| p.target && p.heavy == want_heavy),
204 new.iter().filter(|p| p.target && p.heavy == want_heavy),
206 assert_eq!(o.id, n.id);
211 // no heavy are newly above light
212 let old_misstacked = {
213 let misheavy = |on: &[&PieceCollated]| {
214 let mut misheavy = HashSet::new();
215 for i in 0..on.len() {
216 for j in i+1..on.len() {
218 if on[j].heavy && ! on[i].heavy {
220 misheavy.insert((on[j].id, on[i].id));
226 let old = misheavy(&old);
227 let new = misheavy(&new);
228 let newly = new.difference(&old).collect_vec();
229 assert!( newly.is_empty(), "{:?}", &newly );
233 // no light non-targets moved
236 if ! n.heavy && ! n.target {
237 assert!( ! n.updated, "{:?}", n );
242 // z levels (at least of heavy) in updates all decrease
245 if n.heavy && n.updated {
246 assert!( n.new_z < n.old_z, "{:?}", &n );
251 // all targets now below all light non-targets
253 let mut had_light_nontarget = None;
255 if ! n.heavy && ! n.target {
256 had_light_nontarget = Some(n);
259 assert!( had_light_nontarget.is_none(),
260 "{:?} {:?}", &n, had_light_nontarget);
265 // all heavy targets now below all non-targets
267 let mut had_nontarget = None;
270 had_nontarget = Some(n);
272 if n.heavy && n.target {
273 assert!( had_nontarget.is_none(),
274 "{:?} {:?}", &n, had_nontarget);
279 // all the z levels are still distinct and ordered
281 for (n0,n1) in new.iter().tuple_windows() {
282 assert!( n1.new_z > n0.new_z,
283 "{:?} {:?}", &n0, &n1 );
287 // non-targets are moved only if they things are funky
289 // funky could be one of:
290 // - misstacked heavy
291 // - heavy with same Z Coord (but obvs not Gen) as some light
292 if old_misstacked.is_empty() &&
293 ! old.iter().tuple_windows().any(|(o0,o1)| {
294 o0.heavy && ! o1.heavy &&
295 o1.old_z.z == o0.old_z.z
300 assert!( n.target, "{:?}", n );
309 pub fn heavy(&self) -> bool {
310 use PieceMoveable::*;
311 match (self.pinned, self.moveable) {
313 (false, Yes) => false,
314 (false, No ) => true,
315 (_, IfWresting) => panic!(),
320 impl TestsAccumulator {
322 pub fn new(opts: &Opts) -> Self {
323 let mut tera = tera::Tera::default();
324 tera.add_raw_template("js", TEMPLATE)?;
326 let script = fs::OpenOptions::new()
331 .open(&opts.script)?;
332 let script = BufWriter::new(script);
334 let mut tests: Tests = default();
335 if let Some(only) = env::var_os("OTTER_JST_LOWER_ONLY") {
336 tests.only = Some(only.into_string().unwrap())
345 pub fn finalise(mut self) -> Tests {
346 self.script.flush()?;
351 pub fn add_test<T>(&mut self, name: &str,
352 pieces: Vec<StartPieceSpec>,
354 where T: TryInto<Vpid> + Copy + Debug,
356 if let Some(only) = &self.tests.only {
357 if name != only { return; }
359 let mut zlast = ZCoord::default().clone_mut();
360 let mut zlastg = Generation(1000);
362 let pieces: IndexMap<Vpid,StartPiece> = pieces.into_iter().map(
363 |StartPieceSpec { id, pinned, moveable, zupd }| {
364 let zupd_d = (&zupd).into();
365 let zlevel = zupd.next(&mut zlast, &mut zlastg);
366 (id, StartPiece { pinned, moveable, zlevel, zupd: zupd_d })
370 let targets: IndexSet<_> = targets.into_iter().map(
371 |s| s.try_into().map_err(|_|s).unwrap()
374 println!("-------------------- {} --------------------", name);
375 for (id,p) in pieces.iter().rev() {
376 println!(" {:6} {}{} {} {}",
378 if targets.contains(id) { "T" } else { "_" },
379 if p.heavy() { "H" } else { "_" },
388 let context = tera::Context::from_serialize(&test)?;
389 self.tera.render_to("js", &context, &mut self.script)?;
391 let already = self.tests.tests.insert(name.to_owned(), test);
392 assert!(already.is_none(), "duplicate test {:?}", &name);
396 pub fn add_exhaustive(&mut self, nameprefix: &str, zupds: &[ZUpdateSpec]) {
397 let n: usize = match zupds.len() { 1 => 5, 2 => 4, _ => panic!() };
399 let ids: Vec<Vpid> = (0..n).map(
400 |i| format!("{}.{}", i+1, 1).try_into().unwrap()
403 let pieces_configs = ids.iter().cloned().map(|id| {
405 [false,true].iter().cloned(),
406 zupds.iter().cloned()
407 ).map( move |(bottom,zupd)| {
411 moveable: PieceMoveable::Yes,
416 .multi_cartesian_product();
418 let target_configs = ids.iter().cloned()
421 for (ti, (pieces, targets)) in itertools::iproduct!(
425 if targets.is_empty() { continue }
426 let name = format!("exhaustive-{}-{:02x}",
428 self.add_test(&name, pieces, targets)?;
439 println!("^^^^^^^^^^^^^^^^^^^^ success ^^^^^^^^^^^^^^^^^^^^");
440 throw!(anyhow!("tests limited to {}, treating as failure", &only))
448 let opts = Opts::from_args();
450 println!("==================== building ====================");
452 let mut ta = TestsAccumulator::new(&opts)?;
454 ta.add_test("simple", vec![
455 sp!("1.1", false, Yes),
456 sp!("2.1", false, Yes),
461 ta.add_test("pair", vec![
462 sp!("1.1", false, Yes),
463 sp!("2.1", false, Yes),
464 sp!("3.1", false, Yes),
470 ta.add_test("found-2021-07-07-raises", vec![
471 sp!( "87.7", false, No),
472 sp!( "81.7", false, Yes),
473 sp!("110.7", false, Yes), // HELD 1#1
474 sp!( "64.7", false, No),
475 sp!( "59.7", false, No), // HELD 2#1
476 sp!( "62.7", false, Yes),
477 sp!( "73.7", false, Yes),
478 sp!( "46.7", false, No),
479 sp!( "7.7", false, Yes),
484 ta.add_test("found-2021-07-15-lowers", vec![
485 sp!( "87.11", false, No , "g1ea000000" , 38558 ),
486 sp!( "59.11", false, No , "g1ea000000_0000001000", 39858 ),
487 sp!( "64.11", false, No , "g1ea000000_0000002000", 39859 ),
488 sp!( "23.11", false, Yes, "g1ea000000_0000002001", 46890 ),
489 sp!( "96.11", false, Yes, "g1ea000000_0000002002", 46846 ),
490 sp!( "8.11", false, Yes, "g1ea000000_0000002040", 45196 ),
491 sp!( "46.11", false, No , "g1eb000000" , 38559 ),
492 sp!( "66.11", false, No , "g1ed000000" , 38561 ),
493 sp!( "47.11", false, Yes, "g1ee000000_0000004000", 47077 ),
494 sp!( "11.11", false, Yes, "g1ee000000_000000c000", 49354 ),
495 sp!("112.11", false, Yes, "g1qi000000" , 49288 ),
496 sp!( "77.11", false, Yes, "g1ql000000" , 49436 ), // HELD 1#1
501 ta.add_exhaustive("z", &[ZUS::Auto ])?;
502 ta.add_exhaustive("m", &[ZUS::Auto, ZUS::GOnly])?;
503 ta.add_exhaustive("g", &[ ZUS::GOnly])?;
505 let tests = ta.finalise()?;
507 println!("==================== running ====================");
509 let mut cmd = Command::new(opts.nodejs);
510 cmd.arg(opts.script);
511 let status = cmd.status()?;
512 assert!(status.success(), "{}", status);
514 println!("==================== checking ====================");
516 for test in tests.tests.values() {
523 static TEMPLATE: &str = r#"
525 console.log('-------------------- {{ name }} --------------------')
526 jstest_did = fs.openSync('{{ name }}.did', 'w');
529 {% for p in pieces -%}
531 pinned: {{ p.1.pinned }},
532 moveable: '{{ p.1.moveable }}',
533 z: '{{ p.1.zlevel.z }}',
534 zg: '{{ p.1.zlevel.zg }}',
540 { special: "pieces_marker", dataset: { } },
541 {%- for p in pieces %}
542 { dataset: { piece: "{{ p.0 }}" } },
544 { special: "defs_marker", dataset: { } },
547 pieces_marker = fake_dom[0];
548 defs_marker = fake_dom[{{ pieces | length + 1 }}];
550 {%- for p in pieces %}
551 fake_dom[{{ loop.index0 }}].nextElementSibling = fake_dom[{{ loop.index }}];
553 fake_dom[{{ pieces | length }}].nextElementSibling = fake_dom[{{ pieces | length + 1 }}];
554 defs_marker.previousElementSibling = fake_dom[{{ pieces | length }}];
558 {%- for t in targets %}
564 lower_targets(uorecord);
566 fs.closeSync(jstest_did);