chiark / gitweb /
69b727a7ef4279721f08a31e06d8f4aa926313c9
[otter.git] / apitest / atmain.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 pub use otter_api_tests::*;
6
7 pub use std::cell::{RefCell, RefMut};
8 pub use std::rc::Rc;
9
10 pub type Setup = Rc<RefCell<SetupCore>>;
11
12 pub use index_vec::Idx;
13
14 struct TrackWantedTestsGuard<'m>(RefMut<'m, SetupCore>);
15 deref_to_field_mut!{TrackWantedTestsGuard<'_>,
16                     TrackWantedTests,
17                     0.wanted_tests}
18
19 #[allow(dead_code)]
20 struct UsualCtx {
21   opts: Opts,
22   su_rc: Setup,
23   spec: GameSpec,
24   alice: Player,
25   bob: Player,
26   prctx: PathResolveContext,
27   has_lib_markers: bool,
28 }
29
30 impl UsualCtx {
31   pub fn su(&self) -> std::cell::Ref<SetupCore>{ RefCell::borrow(&self.su_rc) }
32   pub fn su_mut(&self) -> RefMut<SetupCore> { self.su_rc.borrow_mut() }
33   pub fn ds(&self) -> DirSubst { RefCell::borrow(&self.su_rc).ds.clone() }
34
35   pub fn wanted_tests(&self) -> TrackWantedTestsGuard {
36     TrackWantedTestsGuard(self.su_mut())
37   }
38 }
39
40 #[derive(Debug)]
41 struct Player {
42   pub nick: &'static str,
43   url: String,
44 }
45
46 struct Session {
47   pub nick: &'static str,
48   pub su_rc: Setup,
49   pub ctoken: RawToken,
50   pub gen: Generation,
51   pub cseq: RawClientSequence,
52   pub dom: scraper::Html,
53   pub updates: mpsc::Receiver<Update>,
54   pub client: reqwest::blocking::Client,
55 }
56
57 mod scraper_ext {
58   use super::*;
59   use scraper::*;
60   use scraper::html::Html;
61
62   #[ext(pub)]
63   impl Html {
64     #[throws(as Option)]
65     fn element<S>(&self, sel: S) -> ElementRef
66     where S: TryInto<Selector>,
67           <S as TryInto<Selector>>::Error: Debug,
68     {
69       self
70         .select(&sel.try_into().unwrap())
71         .next()?
72     }
73
74     #[throws(as Option)]
75     fn e_attr<S>(&self, sel: S, attr: &str) -> &str
76     where S: TryInto<Selector>,
77           <S as TryInto<Selector>>::Error: Debug,
78     {
79       self
80         .element(sel).unwrap()
81         .value().attr(attr)?
82     }
83   }
84
85   #[throws(Explode)]
86   pub fn parse_html(resp: reqwest::blocking::Response) -> Html {
87     assert_eq!(resp.status(), 200);
88     let body = resp.text()?;
89     let dom = scraper::Html::parse_document(&body);
90     //dbgc!(&&dom);
91     dom
92   }
93
94   #[ext(pub, name=RequestBuilderExt)]
95   impl reqwest::blocking::RequestBuilder {
96     #[throws(Explode)]
97     fn send(self) -> reqwest::blocking::Response { self.send()? }
98
99     #[throws(Explode)]
100     fn send_parse_html(self) -> Html {
101       let resp = self.send()?;
102       parse_html(resp)?
103     }
104   }
105 }
106
107 use scraper_ext::{HtmlExt, RequestBuilderExt};
108
109 type Update = JsV;
110
111 #[throws(Explode)]
112 fn updates_parser<R:Read>(input: R, out: &mut mpsc::Sender<Update>) {
113   let mut accum: HashMap<String, String> = default();
114   for l in BufReader::new(input).lines() {
115     let l = l?;
116     if ! l.is_empty() {
117       let mut l = l.splitn(2, ':');
118       let lhs = l.next().unwrap();
119       let rhs = l.next().unwrap();
120       let rhs = rhs.trim_start();
121       let () = accum.insert(lhs.to_string(), rhs.to_string())
122         .is_none().expect("duplicate field");
123       continue;
124     }
125     let entry = mem::take(&mut accum);
126     #[allow(unused_variables)] let accum = (); // stops accidental use of accum
127     if entry.get("event").map(String::as_str) == Some("commsworking") {
128       eprintln!("commsworking: {}", entry["data"]);
129     } else if let Some(event) = entry.get("event") {
130       panic!("unexpected event: {}", event);
131     } else {
132       let update = &entry["data"];
133       let update = serde_json::from_str(update).unwrap();
134       if out.send(update).is_err() { break }
135     }
136   }
137 }
138
139 impl UsualCtx {
140   #[throws(Explode)]
141   fn connect_player(&self, player: &Player) -> Session {
142     let client = reqwest::blocking::Client::new();
143     let loading = client.get(&player.url).send_parse_html()?;
144     let ptoken = loading.e_attr("#loading_token", "data-ptoken").unwrap();
145     dbgc!(&ptoken);
146
147     let session = client.post(&self.su().ds.subst("@url@/_/session/Portrait")?)
148       .json(&json!({ "ptoken": ptoken }))
149       .send_parse_html()?;
150
151     let ctoken = session.e_attr("#main-body", "data-ctoken").unwrap();
152     dbgc!(&ctoken);
153
154     let gen: Generation = Generation(
155       session.e_attr("#main-body", "data-gen").unwrap()
156         .parse().unwrap()
157     );
158     dbgc!(gen);
159
160     let mut sse = client.get(
161       &self.su().ds
162         .also(&[("ctoken", ctoken),
163                 ("gen",    &gen.to_string())])
164         .subst("@url@/_/updates?ctoken=@ctoken@&gen=@gen@")?
165     ).send()?;
166
167     let (mut wpipe, rpipe) = UnixStream::pair()?;
168     thread::spawn(move ||{
169       eprintln!("copy_to'ing");
170       match sse.copy_to(&mut wpipe) {
171         Err(re) => match (||{
172           // reqwest::Error won't give us the underlying io::Error :-/
173           wpipe.write_all(b"\n")?;
174           wpipe.flush()?;
175           Ok::<_,io::Error>(())
176         })() {
177           Err(pe) if pe.kind() == ErrorKind::BrokenPipe => { Ok(()) }
178           Err(pe) => Err(AE::from(pe)),
179           Ok(_) => Err(AE::from(re)),
180         }
181         Ok(_n) => Ok(()),
182       }.unwrap();
183       eprintln!("copy_to'd!"); 
184     });
185
186     let (mut csend, crecv) = mpsc::channel();
187     thread::spawn(move ||{
188       updates_parser(rpipe, &mut csend).expect("udpates parser failed")
189     });
190
191     Session {
192       nick: player.nick,
193       client, gen,
194       cseq: 42,
195       ctoken: RawToken(ctoken.to_string()),
196       dom: session,
197       updates: crecv,
198       su_rc: self.su_rc.clone(),
199     }
200   }
201
202   pub fn chdir_root<F>(&mut self, f: F)
203   where F: FnOnce(&mut Self) -> Result<(),Explode>
204   {
205     let tmp = self.su().ds.abstmp.clone();
206     env::set_current_dir("/").expect("cd /");
207     self.prctx = PathResolveContext::RelativeTo(tmp.clone());
208     f(self).expect("run test");
209     env::set_current_dir(&tmp).expect("cd back");
210     self.prctx = default();
211   }
212 }
213
214 mod pi {
215   use otter::prelude::define_index_type;
216   define_index_type!{ pub struct PIA = usize; }
217   define_index_type!{ pub struct PIB = usize; }
218 }
219 pub use pi::*;
220
221 type Pieces<PI> = IndexVec<PI, PieceInfo<JsV /*~PreparedPieceState*/>>;
222 type PiecesSlice<PI> = IndexSlice<PI,[PieceInfo<JsV>]>;
223
224 #[derive(Debug,Clone)]
225 pub struct PieceInfo<I> {
226   id: String,
227   pos: Pos,
228   info: I,
229 }
230
231 impl PieceInfo<JsV> {
232   fn assert_desc_contains(&self, needle: &str) {
233     let desc = self.info["desc"].as_str().unwrap();
234     assert!(desc.contains(needle), "desc={desc:?}");
235   }
236 }
237
238 impl Session {
239   #[throws(Explode)]
240   fn pieces<PI:Idx>(&self) -> Pieces<PI> {
241     let pieces = {
242       let mut pieces: Pieces<PI> = default();
243       for puse in self.dom
244         .element("#pieces_marker")
245         .unwrap().next_siblings()
246       {
247         let puse = puse.value();
248         if_let!{ Some(puse) = puse.as_element(); else continue; };
249         if_let!{ Some(attr) = puse.attr("data-info"); else break; };
250         let pos = Pos::from_iter(["x","y"].iter().map(|attr|{
251           puse
252             .attr(attr).unwrap()
253             .parse().unwrap()
254         })).unwrap();
255         let id = puse.id.as_ref().unwrap();
256         let id = id.strip_prefix("use").unwrap().to_string();
257         let info = serde_json::from_str(attr).unwrap();
258         pieces.push(PieceInfo { id, pos, info });
259       }
260       pieces
261     };
262     let nick = self.nick;
263     dbgc!(nick, &pieces);
264     pieces
265   }
266
267   #[throws(Explode)]
268   fn api_piece_op_single<O:PieceOp>(&mut self, piece: &str, o: O) {
269     let (opname, payload) = if let Some(o) = o.api() { o } else { return };
270
271     self.cseq += 1;
272     let cseq = self.cseq;
273
274     let su = self.su_rc.borrow_mut();
275     let resp = self.client.post(&su.ds.also(&[("opname",opname)])
276                                 .subst("@url@/_/api/@opname@")?)
277       .json(&json!({
278         "ctoken": self.ctoken,
279         "piece": piece,
280         "gen": self.gen,
281         "cseq": cseq,
282         "op": payload,
283       }))
284       .send()?;
285     assert_eq!(resp.status(), 200);
286   }
287
288   #[throws(Explode)]
289   fn api_piece<P:PieceSpecForOp, O:PieceOp>(
290     &mut self, g: GrabHow, mut p: P, o: O
291   ) {
292     if let GH::With | GH::Grab = g {
293       self.api_piece_op_single(p.id(), ("grab", json!({})))?;
294     }
295     if let Some(u) = p.for_update() {
296       o.update(u);
297     }
298     {
299       self.api_piece_op_single(p.id(), o)?;
300     }
301     if let Some(s) = p.for_synch() {
302       self.synchu(s)?;
303     }
304     if let GH::With | GH::Ungrab = g {
305       self.api_piece_op_single(p.id(), ("ungrab", json!({})))?;
306     }
307   }
308
309   #[throws(Explode)]
310   fn await_update<
311     R,
312     G: FnMut(&mut Session, Generation) -> Option<R>,
313     F: FnMut(&mut Session, Generation, &str, &JsV) -> Option<R>,
314     E: FnMut(&mut Session, Generation, &JsV)
315              -> Result<Option<R>, AE>
316    > (&mut self, mut g: G, mut f: F, mut ef: Option<E>) -> R {
317     let nick = self.nick;
318     'overall: loop {
319       let update = self.updates.recv()?;
320       let update = update.as_array().unwrap();
321       let new_gen = Generation(
322         update[0]
323           .as_i64().unwrap()
324           .try_into().unwrap()
325       );
326       self.gen = new_gen;
327       dbgc!(nick, new_gen);
328       if let Some(y) = g(self, new_gen) { break 'overall y }
329       for ue in update[1].as_array().unwrap() {
330         let (k,v) = ue.as_object().unwrap().iter().next().unwrap();
331         dbgc!(nick, k, &v);
332         if let Some(y) = {
333           if k != "Error" {
334             info!("update ok {} {:?}", k, v);
335             f(self, new_gen, k, v)
336           } else if let Some(ef) = &mut ef {
337             warn!("update error {:?}", v);
338             ef(self, new_gen, v)?
339           } else {
340             panic!("synch error: {:?}", &(k, v));
341           }
342         } { break 'overall y }
343       }
344     }
345   }
346
347   #[throws(Explode)]
348   fn synchx<
349     PI: Idx,
350     F: FnMut(&mut Session, Generation, &str, &JsV),
351   > (&mut self,
352      mut pieces: Option<&mut Pieces<PI>>,
353      ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV)
354                                -> Result<(), AE>>,
355      mut f: F
356   ) {
357     let exp = {
358       self.su_rc.borrow_mut().mgmt_conn()
359         .game_synch(TABLE.parse().unwrap())?
360     };
361     let efwrap = ef.map(|ef| {
362       move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) }
363     });
364     self.await_update(
365       |_session, gen      | (gen == exp).as_option(),
366       | session, gen, k, v| {
367         if let Some(pieces) = pieces.as_mut() {
368           update_update_pieces(session.nick, pieces, k, v);
369         }
370         f(session,gen,k,v);
371         None
372       },
373       efwrap,
374     )?;
375   }
376
377   #[throws(Explode)]
378   fn synchu<PI:Idx>(&mut self, pieces: &mut Pieces<PI>) {
379     self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?;
380   }
381
382   #[throws(Explode)]
383   fn synch(&mut self) {
384     self.synchx::<PIA,_>(None, None, |_session, _gen, _k, _v|())?;
385   }
386 }
387
388 #[ext(pub)]
389 impl<PI> IndexSlice<PI, [PieceInfo<JsV>]> where PI: index_vec::Idx {
390   fn filter_by_desc_glob(&self, desc_glob: &str)
391                        -> Box<dyn Iterator<Item=(PI, &PieceInfo<JsV>)> + '_> {
392     let glob = glob::Pattern::new(desc_glob).unwrap();
393     let iter = self.iter_enumerated()
394       .filter(move |(_i,p)| glob.matches(p.info["desc"].as_str().unwrap()));
395     Box::new(iter) as _
396   }
397   fn ids_by_desc_glob(&self, desc_glob: &str)
398                       -> Box<dyn Iterator<Item=PI> + '_> {
399     let iter = self.filter_by_desc_glob(desc_glob)
400       .map(|(id,_p)| id);
401     Box::new(iter) as _
402   }
403   fn find_by_desc_glob(&self, desc_glob: &str) -> PI {
404     let [pc] = self.filter_by_desc_glob(desc_glob)
405       .map(|(i,_p)| i)
406       .collect::<ArrayVec<_,1>>()
407       .into_inner().expect("multiple pieces matched, unexpectedly");
408     dbgc!(desc_glob, pc);
409     pc
410   }
411 }
412
413 pub fn update_update_pieces<PI:Idx>(
414   _nick: &str,
415   pieces: &mut Pieces<PI>,
416   k: &str, v: &JsV
417 ) {
418   fn coord(j: &JsV) -> Pos {
419     PosC::from_iter_2(
420       j.as_array().unwrap().iter()
421         .map(|n| n.as_i64().unwrap().try_into().unwrap())
422     )
423   }
424
425   fn findp<'p, PI:Idx>(pieces: &'p mut Pieces<PI>,
426                        v: &'_ serde_json::Map<String,JsV>)
427                        -> Option<&'p mut PieceInfo<JsV>> {
428     let piece = v.get("piece")?.as_str()?;
429     pieces.iter_mut().find(|p| p.id == piece)
430   }
431
432   let v = v.as_object().unwrap();
433   let p = findp(pieces, v);
434
435   if k == "Recorded" {
436     let p = p.unwrap();
437     for k in ["zg", "svg", "desc"] {
438       let v = &v[k];
439       if ! v.is_null() {
440         p.info.set(k, v);
441       }
442     }
443   } else if k == "Piece" {
444     let (op, d) = v["op"].as_object().unwrap().iter().next().unwrap();
445
446     match op.as_str() {
447       "Delete" => {
448         let p = p.unwrap();
449         p.info = default();
450       },
451       "Insert" | "InsertQuiet" => {
452         assert!(p.is_none());
453         let piece = v["piece"].as_str().unwrap();
454         pieces.push(PieceInfo {
455           id: piece.into(),
456           pos: coord(d.get("pos").unwrap()),
457           info: d.clone(),
458         });
459       },
460       "Move" | "MoveQuiet" => {
461         p.unwrap().pos = coord(d);
462       },
463       "Modify" | "ModifyQuiet" => {
464         let p = p.unwrap();
465         let d = d.as_object().unwrap();
466         p.pos = coord(&d["pos"]);
467         p.info.extend(d);
468       },
469       _ => {
470         panic!("unknown op {:?} {:?}", &op, &d);
471       },
472     };
473   } else if k == "Image" {
474     let p = p.unwrap();
475     let im = v.get("im").unwrap();
476     p.info.extend(im.as_object().unwrap());
477   } else if k.starts_with("MoveHist") {
478   } else if k == "RecordedUnpredictable" {
479     let p = p.unwrap();
480     let ns = v.get("ns").unwrap();
481     p.info.extend(ns.as_object().unwrap());
482   } else if k.starts_with("SetTable") {
483   } else if k == "AddPlayer" || k == "RemovePlayer" {
484   } else if k == "UpdateBundles" || k == "SetLinks" {
485   } else if k == "Log" {
486   } else {
487     panic!("Unknown update: {k} {v:?}");
488   }
489 }
490
491 pub type PieceOpData = (&'static str, JsV);
492 pub trait PieceOp: Debug {
493   fn api(&self) -> Option<PieceOpData>;
494   fn update(&self, _pi: &mut PieceInfo<JsV>) { info!("no update {:?}", self) }
495 }
496 impl PieceOp for PieceOpData {
497   fn api(&self) -> Option<PieceOpData> { Some((self.0, self.1.clone())) }
498 }
499 impl PieceOp for Pos {
500   fn api(&self) -> Option<PieceOpData> { Some(("m", json![self.coords])) }
501   fn update(&self, pi: &mut PieceInfo<JsV>) { pi.pos = *self }
502 }
503 impl PieceOp for () {
504   fn api(&self) -> Option<PieceOpData> { None }
505   fn update(&self, _pi: &mut PieceInfo<JsV>) {  }
506 }
507
508 pub trait PieceSpecForOp: Debug {
509   fn id(&self) -> &str;
510   type PI: Idx;
511   fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> { None }
512   fn for_synch(&mut self) -> Option<&mut Pieces<Self::PI>> { None }
513 }
514
515 impl PieceSpecForOp for str {
516   type PI = PIA;
517   fn id(&self) -> &str { self }
518 }
519 impl PieceSpecForOp for &String {
520   type PI = PIA;
521   fn id(&self) -> &str { self }
522 }
523
524 type PuUp<'pcs, PI> = (&'pcs mut Pieces<PI>, PI);
525 #[derive(Debug)]
526 /// Synchronise after op but before any ungrab.
527 pub struct PuSynch<T>(T);
528
529 macro_rules! impl_PieceSpecForOp {
530   ($($amp:tt $mut:tt)?) => {
531
532     impl<PI:Idx> PieceSpecForOp for $($amp $mut)? PuUp<'_, PI> {
533       type PI = PI;
534       fn id(&self) -> &str { &self.0[self.1].id }
535       fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> {
536         Some(&mut self.0[self.1])
537       }
538     }
539
540     impl<PI:Idx> PieceSpecForOp for PuSynch<$($amp $mut)? PuUp<'_,PI>> {
541       type PI = PI;
542       fn id(&self) -> &str { self.0.id() }
543       fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> {
544         self.0.for_update()
545       }
546       fn for_synch(&mut self) -> Option<&mut Pieces<PI>> {
547         Some(self.0.0)
548       }
549     }
550   }
551 }
552 impl_PieceSpecForOp!{}
553 impl_PieceSpecForOp!{&mut}
554
555 #[derive(Debug,Copy,Clone)]
556 pub enum GrabHow { Raw, Grab, Ungrab, With }
557 pub use GrabHow as GH;
558
559
560 impl UsualCtx {
561   #[throws(AE)]
562   pub fn otter(&mut self, args: &dyn OtterArgsSpec) -> OtterOutput {
563     let args: Vec<String> =
564       ["--account", "server:"].iter().cloned().map(Into::into)
565       .chain(args.to_args(&self.su().ds).into_iter())
566       .collect();
567     self.su().ds.otter_prctx(&self.prctx, &args)?
568   }
569
570   #[throws(Explode)]
571   pub fn prepare_game(&mut self) {
572     prepare_game(&self.su().ds, &self.prctx, TABLE)?;
573     self.has_lib_markers = false;
574   }
575
576   #[throws(AE)]
577   pub fn otter_resetting(&mut self, args: &dyn OtterArgsSpec)
578                          -> OtterOutput {
579     self.has_lib_markers = false;
580     self.otter(args)?
581   }
582
583   #[throws(Explode)]
584   fn some_library_add(&mut self, command: &dyn OtterArgsSpec) -> Vec<String> {
585     let mut session = if ! dbgc!(self.has_lib_markers) {
586       let add_err = self.otter(command)
587         .expect_err("library-add succeeded after reset!");
588       assert_eq!(add_err.downcast::<ExitStatusError>()?.0.code(),
589                  Some(EXIT_NOTFOUND));
590
591       let mut session = self.connect_player(&self.alice)?;
592       let pieces = session.pieces::<PIA>()?;
593       let llm: [_;2] = pieces
594         .filter_by_desc_glob("a library load area marker")
595         .map(|(_id,p)| p)
596         .collect::<ArrayVec<_,2>>()
597         .into_inner().unwrap();
598       dbgc!(&llm);
599
600       for (llm, &pos) in izip!(&llm, [PosC::new(5,5), PosC::new(50,25)].iter())
601       {
602         session.api_piece(GH::With, &llm.id, pos)?;
603       }
604       self.has_lib_markers = true;
605
606       session.synch()?;
607       session
608     } else {
609       self.connect_player(&self.alice)?
610     };
611
612     self.otter(command)
613       .expect("library-add failed after place!");
614
615     let mut added = vec![];
616     session.synchx::<PIA,_>(None, None,
617       |_session, _gen, k, v| if_chain! {
618         if k == "Piece";
619         let piece = v["piece"].as_str().unwrap().to_string();
620         let op = v["op"].as_object().unwrap();
621         if let Some(_) = op.get("InsertQuiet");
622         then { added.push(piece); }
623       }
624     )?;
625
626     dbgc!(&added);
627     added
628   }
629
630   #[throws(Explode)]
631   fn stop_and_restart_server(&mut self) {
632     let mut su = self.su_rc.borrow_mut();
633     let old_pid = su.server_child.id() as nix::libc::pid_t;
634     nix::sys::signal::kill(nix::unistd::Pid::from_raw(old_pid),
635                            nix::sys::signal::SIGTERM)?;
636     let st = dbgc!(su.server_child.wait()?);
637     assert_eq!(st.signal(), Some(nix::sys::signal::SIGTERM as i32));
638     su.restart_gameserver()?;
639   }
640
641   #[throws(Explode)]
642   pub fn check_library_item(&mut self, itemlib: &str, item: &str,
643                         desc: &str) {
644     let ds = self.su().ds.also(&[
645       ("itemlib", itemlib),
646       ("item",    item   ),
647     ]);
648     let command = ds.gss("library-add --lib @itemlib@ @item@")?;
649     let added = self.some_library_add(&command)?;
650     assert_eq!( added.len(), 1 );
651
652     let output: String = self.otter(&ds.gss("list-pieces")?)?.into();
653     assert_eq!( Regex::new(
654       &format!(
655         r#"(?m)(?:[^\w-]|^){}[^\w-].*\W{}(?:\W|$)"#,
656         item, desc,
657       )
658     )?
659                 .find_iter(&output).count(),
660                 1,
661                 "got: {}", &output);
662   }
663
664   #[throws(Explode)]
665   pub fn upload_and_check_bundle(
666     &mut self, bundle_stem: &str,
667     libname: &str, item: &str,
668     desc: &str,
669     with: &mut dyn FnMut(&mut UsualCtx) -> Result<(), Explode>
670   ) {
671     let ds = self.su().ds.also(&[("bundle_stem", &bundle_stem)]);
672     let bundle_file = ds.subst("@examples@/@bundle_stem@.zip")?;
673     let ds = ds.also(&[("bundle", &bundle_file)]);
674     self.otter(&ds.gss("upload-bundle @bundle@")?)?;
675     let mut bundles = self.otter(&ds.gss("list-bundles")?)?;
676     let bundles = String::from(&mut bundles);
677     assert!(bundles.starts_with("00000.zip Loaded"));
678     self.otter(&ds.gss("download-bundle 0")?)?;
679     let st = Command::new("cmp").args(&[&bundle_file, "00000.zip"]).status()?;
680     if ! st.success() { panic!("cmp failed {}", st) }
681
682     self.check_library_item(libname,item,desc)?;
683
684     self.stop_and_restart_server()?;
685
686     let id =
687       self.su().mgmt_conn().list_pieces()?
688       .0.iter()
689       .find(|pi| pi.itemname.as_str() == item)
690       .unwrap()
691       .piece;
692     self.su().mgmt_conn().alter_game(vec![MGI::DeletePiece(id)], None)?;
693
694     self.check_library_item(libname,item,desc)?;
695
696     with(self)?;
697
698     self.clear_reset_to_demo()?;
699   }
700   
701   #[throws(Explode)]
702   pub fn clear_reset_to_demo(&mut self) {
703     self.otter_resetting(&G("clear-game"))?;
704     self.otter_resetting(&G("reset demo"))?;
705   }
706 }
707
708 impl UsualCtx {
709   #[throws(AE)]
710   pub fn setup() -> Self {
711     let (opts, _instance, su) = setup_core(
712       &[module_path!()],
713     )?;
714     let spec = su.ds.game_spec_data()?;
715     let mut mc = su.mgmt_conn();
716     let [alice, bob]: [Player; 2] =
717       su.ds.setup_static_users(&mut mc, default())?
718       .into_iter().map(|sus| Player { nick: sus.nick, url: sus.url })
719       .collect::<ArrayVec<_,2>>().into_inner().unwrap();
720     drop(mc);
721
722     let su_rc = Rc::new(RefCell::new(su));
723     UsualCtx {
724       opts, spec, su_rc, alice, bob,
725       has_lib_markers: false,
726       prctx: default(),
727     }
728   }
729 }
730
731 portmanteau_has!("at-otter.rs",   at_otter);
732 portmanteau_has!("at-bundles.rs", at_bundles);
733 portmanteau_has!("at-hidden.rs",  at_hidden);
734 portmanteau_has!("at-currency.rs", at_currency);
735
736 #[throws(AE)]
737 fn main() { portmanteau_main("at")? }