chiark / gitweb /
changelog: document further make-release changes
[otter.git] / jstest / jst-lower.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 // OTTER_JST_LOWER_ONLY=exhaustive-05
6
7 #![allow(clippy::or_fun_call)]
8 #![allow(clippy::unnecessary_operation)] // trips on #[throws(Explode)]
9
10 use otter_nodejs_tests::*;
11
12 pub type Vpid = VisiblePieceId;
13
14 #[derive(StructOpt,Debug,Clone)]
15 pub struct Opts {
16   pub nodejs: String,
17   pub script: String,
18 }
19
20 #[derive(Debug,Clone)]
21 pub struct StartPieceSpec {
22   id: Vpid,
23   pinned: bool,
24   moveable: PieceMoveable,
25   zupd: ZUpdateSpec,
26 }
27
28 #[macro_export]
29 macro_rules! sp {
30   { $id:expr, $pinned:expr, $moveable:ident } => {
31     StartPieceSpec { id: $id.try_into().unwrap(), pinned: $pinned,
32                      zupd: ZUS::Auto,
33                      moveable: PieceMoveable::$moveable }
34   };
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(),
39                        zg: Generation($zg),
40                      }),
41                      moveable: PieceMoveable::$moveable }
42   };
43 }
44
45 #[derive(Debug,Clone)]
46 #[derive(Eq,PartialEq,Ord,PartialOrd)]
47 #[derive(Serialize)]
48 pub struct StartPiece {
49   pinned: bool,
50   moveable: PieceMoveable,
51   zlevel: ZLevel,
52   zupd: ZUSD,
53 }
54
55 #[derive(Debug,Clone,Default)]
56 pub struct Tests {
57   tests: IndexMap<String, Test>,
58   only: Option<String>,
59 }
60
61 #[derive(Debug,Clone,Default)]
62 #[derive(Serialize)]
63 pub struct Test {
64   name: String,
65   #[serde(with = "indexmap::serde_seq")]
66   pieces: IndexMap<Vpid, StartPiece>,
67   targets: IndexSet<Vpid>,
68 }
69
70 #[derive(Debug)]
71 pub struct TestsAccumulator {
72   tests: Tests,
73   script: BufWriter<fs::File>,
74   tera: tera::Tera,
75 }
76
77 #[derive(Debug,Clone,EnumDiscriminants)]
78 #[strum_discriminants(derive(Ord,PartialOrd,Serialize))]
79 pub enum ZUpdateSpec {
80   Auto,
81   Spec(ZLevel),
82   GOnly,
83 }
84 use ZUpdateSpec as ZUS;
85 use ZUpdateSpecDiscriminants as ZUSD;
86
87 #[ext(pub)]
88 impl ZUSD {
89   fn show(&self) -> char {
90     match self {
91       ZUSD::Auto  => ' ',
92       ZUSD::Spec  => '!',
93       ZUSD::GOnly => 'g',
94     }
95   }
96 }
97
98 impl ZUpdateSpec {
99   pub fn next(self, last: &mut zcoord::Mutable, lastg: &mut Generation)
100               -> ZLevel {
101     match self {
102       ZUS::Auto => ZLevel {
103         z: last.increment().unwrap(),
104         zg: { lastg.increment(); *lastg },
105       },
106       ZUS::GOnly => ZLevel {
107         z: last.repack().unwrap(),
108         zg: { lastg.increment(); *lastg },
109       },
110       ZUS::Spec(zl) => {
111         *last = zl.z.clone_mut();
112         *lastg = zl.zg;
113         zl
114       },
115     }
116   }
117 }
118
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)?;
124   }
125 }
126 #[ext(pub)]
127 impl ZLevel {
128   fn show(&self) -> ZLevelShow<'_> { ZLevelShow(self) }
129 }
130
131 impl Test {
132   #[throws(Explode)]
133   pub fn check(&self) {
134     println!("-------------------- {} --------------------", &self.name);
135
136     let mut updated: HashMap<Vpid, ZLevel> = default();
137     let mut zg = Generation(100_000);
138
139     for l in BufReader::new(
140       fs::File::open(format!("{}.did",self.name))?
141     ).lines() {
142       let l = l?;
143       let (op, id, z) = l.splitn(3,' ').collect_tuple().unwrap();
144       assert_eq!(op, "setz");
145       let id = id.try_into()?;
146       let z = z.parse()?;
147       let zlevel = ZLevel { z, zg };
148       zg.increment();
149       let was = updated.insert(id, zlevel);
150       assert!(was.is_none(), "{:?}", id);
151     }
152
153     #[derive(Debug)]
154     struct PieceCollated<'o,'n> {
155       id: Vpid,
156       old_z: &'o ZLevel,
157       new_z: &'n ZLevel,
158       target: bool,
159       heavy: bool,
160       updated: bool,
161       zupd: ZUSD,
162     }
163
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);
169       PieceCollated {
170         id, new_z, old_z, updated,
171         heavy: start.heavy(),
172         target: self.targets.contains(&id),
173         zupd: start.zupd,
174       }
175     }).collect_vec();
176
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));
180       v
181     };
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} {}{}{} {} {}",
187                 p.id.to_string(),
188                 if p.target  { "T" } else { "_" },
189                 if p.heavy   { "H" } else { "_" },
190                 if p.updated { "U" } else { "_" },
191                 p.zupd.show(),
192                 zl.show());
193       };
194       pr(o, o.old_z); print!("    ");
195       pr(n, n.new_z); println!("");
196     }
197
198     // light targets are in same stacking order as before
199     // heavy targets are in same stacking order as before
200     {
201       for &want_heavy in &[false, true] {
202         for (o, n) in izip!(
203           old.iter().filter(|p| p.target && p.heavy == want_heavy),
204           new.iter().filter(|p| p.target && p.heavy == want_heavy),
205         ) {
206           assert_eq!(o.id, n.id);
207         }
208       }
209     }
210
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() {
217             // j is above i
218             if on[j].heavy && ! on[i].heavy {
219               // heavy above light
220               misheavy.insert((on[j].id, on[i].id));
221             }
222           }
223         }
224         misheavy
225       };
226       let old = misheavy(&old);
227       let new = misheavy(&new);
228       let newly = new.difference(&old).collect_vec();
229       assert!( newly.is_empty(), "{:?}", &newly );
230       old
231     };
232
233     // no light non-targets moved
234     {
235       for n in &new {
236         if ! n.heavy && ! n.target {
237           assert!( ! n.updated, "{:?}", n );
238         }
239       }
240     }
241
242     // z levels (at least of heavy) in updates all decrease
243     {
244       for n in &new {
245         if n.heavy && n.updated {
246           assert!( n.new_z < n.old_z, "{:?}", &n );
247         }
248       }
249     }
250
251     // all targets now below all light non-targets
252     {
253       let mut had_light_nontarget = None;
254       for n in &new {
255         if ! n.heavy && ! n.target {
256           had_light_nontarget = Some(n);
257         }
258         if n.target {
259           assert!( had_light_nontarget.is_none(),
260                    "{:?} {:?}", &n, had_light_nontarget);
261         }
262       }
263     }
264
265     // all heavy targets now below all non-targets
266     {
267       let mut had_nontarget = None;
268       for n in &new {
269         if ! n.target {
270           had_nontarget = Some(n);
271         }
272         if n.heavy && n.target {
273           assert!( had_nontarget.is_none(),
274                    "{:?} {:?}", &n, had_nontarget);
275         }
276       }
277     }
278
279     // all the z levels are still distinct and ordered
280     {
281       for (n0,n1) in new.iter().tuple_windows() {
282         assert!( n1.new_z > n0.new_z,
283                  "{:?} {:?}", &n0, &n1 );
284       }
285     }
286
287     // non-targets are moved only if they things are funky
288     {
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
296          })
297       {
298         for n in &new {
299           if n.updated {
300             assert!( n.target, "{:?}", n );
301           }
302         }
303       }
304     }
305   }
306 }
307
308 impl StartPiece {
309   pub fn heavy(&self) -> bool {
310     use PieceMoveable::*;
311     match (self.pinned, self.moveable) {
312       (true , _  ) => true,
313       (false, Yes) => false,
314       (false, No ) => true,
315       (_, IfWresting) => panic!(),
316     }
317   }
318 }
319
320 impl TestsAccumulator {
321   #[throws(Explode)]
322   pub fn new(opts: &Opts) -> Self {
323     let mut tera = tera::Tera::default();
324     tera.add_raw_template("js", TEMPLATE)?;
325
326     let script = fs::OpenOptions::new()
327       .write(true)
328       .append(true)
329       .truncate(false)
330       .create(false)
331       .open(&opts.script)?;
332     let script = BufWriter::new(script);
333
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())
337     }
338
339     TestsAccumulator {
340       tests, script, tera,
341     }
342   }
343
344   #[throws(Explode)]
345   pub fn finalise(mut self) -> Tests {
346     self.script.flush()?;
347     self.tests
348   }
349     
350   #[throws(Explode)]
351   pub fn add_test<T>(&mut self, name: &str,
352                      pieces: Vec<StartPieceSpec>,
353                      targets: Vec<T>)
354   where T: TryInto<Vpid> + Copy + Debug,
355   {
356     if let Some(only) = &self.tests.only {
357       if name != only { return; }
358     }
359     let mut zlast = ZCoord::default().clone_mut();
360     let mut zlastg = Generation(1000);
361
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 })
367       }
368     ).collect();
369
370     let targets: IndexSet<_> = targets.into_iter().map(
371       |s| s.try_into().map_err(|_|s).unwrap()
372     ).collect();
373
374     println!("-------------------- {} --------------------", name);
375     for (id,p) in pieces.iter().rev() {
376       println!("    {:6} {}{}  {} {}",
377                 id.to_string(),
378                 if targets.contains(id) { "T" } else { "_" },
379                 if p.heavy()            { "H" } else { "_" },
380                 p.zupd.show(),
381                 p.zlevel.show());
382     }
383
384     let test = Test {
385       name: name.into(),
386       pieces, targets,
387     };
388     let context = tera::Context::from_serialize(&test)?;
389     self.tera.render_to("js", &context, &mut self.script)?;
390
391     let already = self.tests.tests.insert(name.to_owned(), test);
392     assert!(already.is_none(), "duplicate test {:?}", &name);
393   }
394
395   #[throws(Explode)]
396   pub fn add_exhaustive(&mut self, nameprefix: &str, zupds: &[ZUpdateSpec]) {
397     let n: usize = match zupds.len() { 1 => 5, 2 => 4, _ => panic!() };
398
399     let ids: Vec<Vpid> = (0..n).map(
400       |i| format!("{}.{}", i+1, 1).try_into().unwrap()
401     ).collect_vec();
402
403     let pieces_configs = ids.iter().cloned().map(|id| {
404       iproduct!(
405         [false,true].iter().cloned(),
406         zupds.iter().cloned()
407       ).map( move |(bottom,zupd)| {
408         StartPieceSpec {
409           id,
410           pinned: bottom,
411           moveable: PieceMoveable::Yes,
412           zupd,
413         }
414       })
415     })
416       .multi_cartesian_product();
417
418     let target_configs = ids.iter().cloned()
419       .powerset();
420
421     for (ti, (pieces, targets)) in itertools::iproduct!(
422       pieces_configs,
423       target_configs
424     ).enumerate() {
425       if targets.is_empty() { continue }
426       let name = format!("exhaustive-{}-{:02x}",
427                          nameprefix, ti);
428       self.add_test(&name, pieces, targets)?;
429     }
430   }
431 }
432
433 impl Tests {
434   #[throws(AE)]
435   fn finish(self) {
436     match self.only {
437       None => { },
438       Some(only) => {
439         println!("^^^^^^^^^^^^^^^^^^^^ success ^^^^^^^^^^^^^^^^^^^^");
440         throw!(anyhow!("tests limited to {}, treating as failure", &only))
441       }
442     }
443   }
444 }
445
446 #[throws(Explode)]
447 fn main() {
448   let opts = Opts::from_args();
449
450   println!("==================== building ====================");
451
452   let mut ta = TestsAccumulator::new(&opts)?;
453
454   ta.add_test("simple", vec![
455     sp!("1.1", false, Yes),
456     sp!("2.1", false, Yes),
457   ], vec![
458     "2.1",
459   ])?;
460
461   ta.add_test("pair", vec![
462     sp!("1.1", false, Yes),
463     sp!("2.1", false, Yes),
464     sp!("3.1", false, Yes),
465   ], vec![
466     "3.1",
467     "2.1",
468   ])?;
469
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),
480   ], vec![
481     "73.7",
482   ])?;
483
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
497   ], vec![
498     "77.11",
499   ])?;
500
501   ta.add_exhaustive("z", &[ZUS::Auto            ])?;
502   ta.add_exhaustive("m", &[ZUS::Auto, ZUS::GOnly])?;
503   ta.add_exhaustive("g", &[           ZUS::GOnly])?;
504   
505   let tests = ta.finalise()?;
506
507   println!("==================== running ====================");
508
509   let mut cmd = Command::new(opts.nodejs);
510   cmd.arg(opts.script);
511   let status = cmd.status()?;
512   assert!(status.success(), "{}", status);
513
514   println!("==================== checking ====================");
515
516   for test in tests.tests.values() {
517     test.check()?;
518   }
519
520   tests.finish()?;
521 }
522
523 static TEMPLATE: &str = r#"
524
525 console.log('-------------------- {{ name }} --------------------')
526 jstest_did = fs.openSync('{{ name }}.did', 'w');
527
528 pieces = {
529 {% for p in pieces -%}
530   '{{ p.0 }}': {
531     pinned: {{ p.1.pinned }},
532     moveable: '{{ p.1.moveable }}',
533     z: '{{ p.1.zlevel.z }}',
534     zg: '{{ p.1.zlevel.zg }}',
535   },
536 {% endfor -%}
537 }
538
539 fake_dom = [
540   { special: "pieces_marker", dataset: { } },
541 {%- for p in pieces %}
542   { dataset: { piece: "{{ p.0 }}" } },
543 {%- endfor -%}
544   { special: "defs_marker", dataset: { } },
545 ];
546
547 pieces_marker = fake_dom[0];
548 defs_marker   = fake_dom[{{ pieces | length + 1 }}];
549
550 {%- for p in pieces %}
551 fake_dom[{{ loop.index0 }}].nextElementSibling = fake_dom[{{ loop.index }}];
552 {%- endfor %}
553 fake_dom[{{ pieces | length }}].nextElementSibling = fake_dom[{{ pieces | length + 1 }}];
554 defs_marker.previousElementSibling = fake_dom[{{ pieces | length }}];
555
556 uorecord = {
557   targets: [
558 {%- for t in targets %}
559       '{{ t }}',
560 {%- endfor %}
561   ],
562 };
563
564 lower_targets(uorecord);
565
566 fs.closeSync(jstest_did);
567 jstest_did = -1;
568
569 "#;