chiark / gitweb /
apitest: Promote PIA and PIB to where wdriver can use them
[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 type Pieces<PI> = IndexVec<PI, PieceInfo<JsV /*~PreparedPieceState*/>>;
215 type PiecesSlice<PI> = IndexSlice<PI,[PieceInfo<JsV>]>;
216
217 #[derive(Debug,Clone)]
218 pub struct PieceInfo<I> {
219   id: String,
220   pos: Pos,
221   info: I,
222 }
223
224 impl PieceInfo<JsV> {
225   fn assert_desc_contains(&self, needle: &str) {
226     let desc = self.info["desc"].as_str().unwrap();
227     assert!(desc.contains(needle), "desc={desc:?}");
228   }
229 }
230
231 impl Session {
232   #[throws(Explode)]
233   fn pieces<PI:Idx>(&self) -> Pieces<PI> {
234     let pieces = {
235       let mut pieces: Pieces<PI> = default();
236       for puse in self.dom
237         .element("#pieces_marker")
238         .unwrap().next_siblings()
239       {
240         let puse = puse.value();
241         if_let!{ Some(puse) = puse.as_element(); else continue; };
242         if_let!{ Some(attr) = puse.attr("data-info"); else break; };
243         let pos = Pos::from_iter(["x","y"].iter().map(|attr|{
244           puse
245             .attr(attr).unwrap()
246             .parse().unwrap()
247         })).unwrap();
248         let id = puse.id.as_ref().unwrap();
249         let id = id.strip_prefix("use").unwrap().to_string();
250         let info = serde_json::from_str(attr).unwrap();
251         pieces.push(PieceInfo { id, pos, info });
252       }
253       pieces
254     };
255     let nick = self.nick;
256     dbgc!(nick, &pieces);
257     pieces
258   }
259
260   #[throws(Explode)]
261   fn api_piece_op_single<O:PieceOp>(&mut self, piece: &str, o: O) {
262     let (opname, payload) = if let Some(o) = o.api() { o } else { return };
263
264     self.cseq += 1;
265     let cseq = self.cseq;
266
267     let su = self.su_rc.borrow_mut();
268     let resp = self.client.post(&su.ds.also(&[("opname",opname)])
269                                 .subst("@url@/_/api/@opname@")?)
270       .json(&json!({
271         "ctoken": self.ctoken,
272         "piece": piece,
273         "gen": self.gen,
274         "cseq": cseq,
275         "op": payload,
276       }))
277       .send()?;
278     assert_eq!(resp.status(), 200);
279   }
280
281   #[throws(Explode)]
282   fn api_piece<P:PieceSpecForOp, O:PieceOp>(
283     &mut self, g: GrabHow, mut p: P, o: O
284   ) {
285     if let GH::With | GH::Grab = g {
286       self.api_piece_op_single(p.id(), ("grab", json!({})))?;
287     }
288     if let Some(u) = p.for_update() {
289       o.update(u);
290     }
291     {
292       self.api_piece_op_single(p.id(), o)?;
293     }
294     if let Some(s) = p.for_synch() {
295       self.synchu(s)?;
296     }
297     if let GH::With | GH::Ungrab = g {
298       self.api_piece_op_single(p.id(), ("ungrab", json!({})))?;
299     }
300   }
301
302   #[throws(Explode)]
303   fn await_update<
304     R,
305     G: FnMut(&mut Session, Generation) -> Option<R>,
306     F: FnMut(&mut Session, Generation, &str, &JsV) -> Option<R>,
307     E: FnMut(&mut Session, Generation, &JsV)
308              -> Result<Option<R>, AE>
309    > (&mut self, mut g: G, mut f: F, mut ef: Option<E>) -> R {
310     let nick = self.nick;
311     'overall: loop {
312       let update = self.updates.recv()?;
313       let update = update.as_array().unwrap();
314       let new_gen = Generation(
315         update[0]
316           .as_i64().unwrap()
317           .try_into().unwrap()
318       );
319       self.gen = new_gen;
320       dbgc!(nick, new_gen);
321       if let Some(y) = g(self, new_gen) { break 'overall y }
322       for ue in update[1].as_array().unwrap() {
323         let (k,v) = ue.as_object().unwrap().iter().next().unwrap();
324         dbgc!(nick, k, &v);
325         if let Some(y) = {
326           if k != "Error" {
327             info!("update ok {} {:?}", k, v);
328             f(self, new_gen, k, v)
329           } else if let Some(ef) = &mut ef {
330             warn!("update error {:?}", v);
331             ef(self, new_gen, v)?
332           } else {
333             panic!("synch error: {:?}", &(k, v));
334           }
335         } { break 'overall y }
336       }
337     }
338   }
339
340   #[throws(Explode)]
341   fn synchx<
342     PI: Idx,
343     F: FnMut(&mut Session, Generation, &str, &JsV),
344   > (&mut self,
345      mut pieces: Option<&mut Pieces<PI>>,
346      ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV)
347                                -> Result<(), AE>>,
348      mut f: F
349   ) {
350     let exp = {
351       self.su_rc.borrow_mut().mgmt_conn()
352         .game_synch(TABLE.parse().unwrap())?
353     };
354     let efwrap = ef.map(|ef| {
355       move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) }
356     });
357     self.await_update(
358       |_session, gen      | (gen == exp).as_option(),
359       | session, gen, k, v| {
360         if let Some(pieces) = pieces.as_mut() {
361           update_update_pieces(session.nick, pieces, k, v);
362         }
363         f(session,gen,k,v);
364         None
365       },
366       efwrap,
367     )?;
368   }
369
370   #[throws(Explode)]
371   fn synchu<PI:Idx>(&mut self, pieces: &mut Pieces<PI>) {
372     self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?;
373   }
374
375   #[throws(Explode)]
376   fn synch(&mut self) {
377     self.synchx::<PIA,_>(None, None, |_session, _gen, _k, _v|())?;
378   }
379 }
380
381 #[ext(pub)]
382 impl<PI> IndexSlice<PI, [PieceInfo<JsV>]> where PI: index_vec::Idx {
383   fn filter_by_desc_glob(&self, desc_glob: &str)
384                        -> Box<dyn Iterator<Item=(PI, &PieceInfo<JsV>)> + '_> {
385     let glob = glob::Pattern::new(desc_glob).unwrap();
386     let iter = self.iter_enumerated()
387       .filter(move |(_i,p)| glob.matches(p.info["desc"].as_str().unwrap()));
388     Box::new(iter) as _
389   }
390   fn ids_by_desc_glob(&self, desc_glob: &str)
391                       -> Box<dyn Iterator<Item=PI> + '_> {
392     let iter = self.filter_by_desc_glob(desc_glob)
393       .map(|(id,_p)| id);
394     Box::new(iter) as _
395   }
396   fn find_by_desc_glob(&self, desc_glob: &str) -> PI {
397     let [pc] = self.filter_by_desc_glob(desc_glob)
398       .map(|(i,_p)| i)
399       .collect::<ArrayVec<_,1>>()
400       .into_inner().expect("multiple pieces matched, unexpectedly");
401     dbgc!(desc_glob, pc);
402     pc
403   }
404 }
405
406 pub fn update_update_pieces<PI:Idx>(
407   _nick: &str,
408   pieces: &mut Pieces<PI>,
409   k: &str, v: &JsV
410 ) {
411   fn coord(j: &JsV) -> Pos {
412     PosC::from_iter_2(
413       j.as_array().unwrap().iter()
414         .map(|n| n.as_i64().unwrap().try_into().unwrap())
415     )
416   }
417
418   fn findp<'p, PI:Idx>(pieces: &'p mut Pieces<PI>,
419                        v: &'_ serde_json::Map<String,JsV>)
420                        -> Option<&'p mut PieceInfo<JsV>> {
421     let piece = v.get("piece")?.as_str()?;
422     pieces.iter_mut().find(|p| p.id == piece)
423   }
424
425   let v = v.as_object().unwrap();
426   let p = findp(pieces, v);
427
428   if k == "Recorded" {
429     let p = p.unwrap();
430     for k in ["zg", "svg", "desc"] {
431       let v = &v[k];
432       if ! v.is_null() {
433         p.info.set(k, v);
434       }
435     }
436   } else if k == "Piece" {
437     let (op, d) = v["op"].as_object().unwrap().iter().next().unwrap();
438
439     match op.as_str() {
440       "Delete" => {
441         let p = p.unwrap();
442         p.info = default();
443       },
444       "Insert" | "InsertQuiet" => {
445         assert!(p.is_none());
446         let piece = v["piece"].as_str().unwrap();
447         pieces.push(PieceInfo {
448           id: piece.into(),
449           pos: coord(d.get("pos").unwrap()),
450           info: d.clone(),
451         });
452       },
453       "Move" | "MoveQuiet" => {
454         p.unwrap().pos = coord(d);
455       },
456       "Modify" | "ModifyQuiet" => {
457         let p = p.unwrap();
458         let d = d.as_object().unwrap();
459         p.pos = coord(&d["pos"]);
460         p.info.extend(d);
461       },
462       _ => {
463         panic!("unknown op {:?} {:?}", &op, &d);
464       },
465     };
466   } else if k == "Image" {
467     let p = p.unwrap();
468     let im = v.get("im").unwrap();
469     p.info.extend(im.as_object().unwrap());
470   } else if k.starts_with("MoveHist") {
471   } else if k == "RecordedUnpredictable" {
472     let p = p.unwrap();
473     let ns = v.get("ns").unwrap();
474     p.info.extend(ns.as_object().unwrap());
475   } else if k.starts_with("SetTable") {
476   } else if k == "AddPlayer" || k == "RemovePlayer" {
477   } else if k == "UpdateBundles" || k == "SetLinks" {
478   } else if k == "Log" {
479   } else {
480     panic!("Unknown update: {k} {v:?}");
481   }
482 }
483
484 pub type PieceOpData = (&'static str, JsV);
485 pub trait PieceOp: Debug {
486   fn api(&self) -> Option<PieceOpData>;
487   fn update(&self, _pi: &mut PieceInfo<JsV>) { info!("no update {:?}", self) }
488 }
489 impl PieceOp for PieceOpData {
490   fn api(&self) -> Option<PieceOpData> { Some((self.0, self.1.clone())) }
491 }
492 impl PieceOp for Pos {
493   fn api(&self) -> Option<PieceOpData> { Some(("m", json![self.coords])) }
494   fn update(&self, pi: &mut PieceInfo<JsV>) { pi.pos = *self }
495 }
496 impl PieceOp for () {
497   fn api(&self) -> Option<PieceOpData> { None }
498   fn update(&self, _pi: &mut PieceInfo<JsV>) {  }
499 }
500
501 pub trait PieceSpecForOp: Debug {
502   fn id(&self) -> &str;
503   type PI: Idx;
504   fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> { None }
505   fn for_synch(&mut self) -> Option<&mut Pieces<Self::PI>> { None }
506 }
507
508 impl PieceSpecForOp for str {
509   type PI = PIA;
510   fn id(&self) -> &str { self }
511 }
512 impl PieceSpecForOp for &String {
513   type PI = PIA;
514   fn id(&self) -> &str { self }
515 }
516
517 type PuUp<'pcs, PI> = (&'pcs mut Pieces<PI>, PI);
518 #[derive(Debug)]
519 /// Synchronise after op but before any ungrab.
520 pub struct PuSynch<T>(T);
521
522 macro_rules! impl_PieceSpecForOp {
523   ($($amp:tt $mut:tt)?) => {
524
525     impl<PI:Idx> PieceSpecForOp for $($amp $mut)? PuUp<'_, PI> {
526       type PI = PI;
527       fn id(&self) -> &str { &self.0[self.1].id }
528       fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> {
529         Some(&mut self.0[self.1])
530       }
531     }
532
533     impl<PI:Idx> PieceSpecForOp for PuSynch<$($amp $mut)? PuUp<'_,PI>> {
534       type PI = PI;
535       fn id(&self) -> &str { self.0.id() }
536       fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> {
537         self.0.for_update()
538       }
539       fn for_synch(&mut self) -> Option<&mut Pieces<PI>> {
540         Some(self.0.0)
541       }
542     }
543   }
544 }
545 impl_PieceSpecForOp!{}
546 impl_PieceSpecForOp!{&mut}
547
548 #[derive(Debug,Copy,Clone)]
549 pub enum GrabHow { Raw, Grab, Ungrab, With }
550 pub use GrabHow as GH;
551
552
553 impl UsualCtx {
554   #[throws(AE)]
555   pub fn otter(&mut self, args: &dyn OtterArgsSpec) -> OtterOutput {
556     let args: Vec<String> =
557       ["--account", "server:"].iter().cloned().map(Into::into)
558       .chain(args.to_args(&self.su().ds).into_iter())
559       .collect();
560     self.su().ds.otter_prctx(&self.prctx, &args)?
561   }
562
563   #[throws(Explode)]
564   pub fn prepare_game(&mut self) {
565     prepare_game(&self.su().ds, &self.prctx, TABLE)?;
566     self.has_lib_markers = false;
567   }
568
569   #[throws(AE)]
570   pub fn otter_resetting(&mut self, args: &dyn OtterArgsSpec)
571                          -> OtterOutput {
572     self.has_lib_markers = false;
573     self.otter(args)?
574   }
575
576   #[throws(Explode)]
577   fn some_library_add(&mut self, command: &dyn OtterArgsSpec) -> Vec<String> {
578     let mut session = if ! dbgc!(self.has_lib_markers) {
579       let add_err = self.otter(command)
580         .expect_err("library-add succeeded after reset!");
581       assert_eq!(add_err.downcast::<ExitStatusError>()?.0.code(),
582                  Some(EXIT_NOTFOUND));
583
584       let mut session = self.connect_player(&self.alice)?;
585       let pieces = session.pieces::<PIA>()?;
586       let llm: [_;2] = pieces
587         .filter_by_desc_glob("a library load area marker")
588         .map(|(_id,p)| p)
589         .collect::<ArrayVec<_,2>>()
590         .into_inner().unwrap();
591       dbgc!(&llm);
592
593       for (llm, &pos) in izip!(&llm, [PosC::new(5,5), PosC::new(50,25)].iter())
594       {
595         session.api_piece(GH::With, &llm.id, pos)?;
596       }
597       self.has_lib_markers = true;
598
599       session.synch()?;
600       session
601     } else {
602       self.connect_player(&self.alice)?
603     };
604
605     self.otter(command)
606       .expect("library-add failed after place!");
607
608     let mut added = vec![];
609     session.synchx::<PIA,_>(None, None,
610       |_session, _gen, k, v| if_chain! {
611         if k == "Piece";
612         let piece = v["piece"].as_str().unwrap().to_string();
613         let op = v["op"].as_object().unwrap();
614         if let Some(_) = op.get("InsertQuiet");
615         then { added.push(piece); }
616       }
617     )?;
618
619     dbgc!(&added);
620     added
621   }
622
623   #[throws(Explode)]
624   fn stop_and_restart_server(&mut self) {
625     let mut su = self.su_rc.borrow_mut();
626     let old_pid = su.server_child.id() as nix::libc::pid_t;
627     nix::sys::signal::kill(nix::unistd::Pid::from_raw(old_pid),
628                            nix::sys::signal::SIGTERM)?;
629     let st = dbgc!(su.server_child.wait()?);
630     assert_eq!(st.signal(), Some(nix::sys::signal::SIGTERM as i32));
631     su.restart_gameserver()?;
632   }
633
634   #[throws(Explode)]
635   pub fn check_library_item(&mut self, itemlib: &str, item: &str,
636                         desc: &str) {
637     let ds = self.su().ds.also(&[
638       ("itemlib", itemlib),
639       ("item",    item   ),
640     ]);
641     let command = ds.gss("library-add --lib @itemlib@ @item@")?;
642     let added = self.some_library_add(&command)?;
643     assert_eq!( added.len(), 1 );
644
645     let output: String = self.otter(&ds.gss("list-pieces")?)?.into();
646     assert_eq!( Regex::new(
647       &format!(
648         r#"(?m)(?:[^\w-]|^){}[^\w-].*\W{}(?:\W|$)"#,
649         item, desc,
650       )
651     )?
652                 .find_iter(&output).count(),
653                 1,
654                 "got: {}", &output);
655   }
656
657   #[throws(Explode)]
658   pub fn upload_and_check_bundle(
659     &mut self, bundle_stem: &str,
660     libname: &str, item: &str,
661     desc: &str,
662     with: &mut dyn FnMut(&mut UsualCtx) -> Result<(), Explode>
663   ) {
664     let ds = self.su().ds.also(&[("bundle_stem", &bundle_stem)]);
665     let bundle_file = ds.subst("@examples@/@bundle_stem@.zip")?;
666     let ds = ds.also(&[("bundle", &bundle_file)]);
667     self.otter(&ds.gss("upload-bundle @bundle@")?)?;
668     let mut bundles = self.otter(&ds.gss("list-bundles")?)?;
669     let bundles = String::from(&mut bundles);
670     assert!(bundles.starts_with("00000.zip Loaded"));
671     self.otter(&ds.gss("download-bundle 0")?)?;
672     let st = Command::new("cmp").args(&[&bundle_file, "00000.zip"]).status()?;
673     if ! st.success() { panic!("cmp failed {}", st) }
674
675     self.check_library_item(libname,item,desc)?;
676
677     self.stop_and_restart_server()?;
678
679     let id =
680       self.su().mgmt_conn().list_pieces()?
681       .0.iter()
682       .find(|pi| pi.itemname.as_str() == item)
683       .unwrap()
684       .piece;
685     self.su().mgmt_conn().alter_game(vec![MGI::DeletePiece(id)], None)?;
686
687     self.check_library_item(libname,item,desc)?;
688
689     with(self)?;
690
691     self.clear_reset_to_demo()?;
692   }
693   
694   #[throws(Explode)]
695   pub fn clear_reset_to_demo(&mut self) {
696     self.otter_resetting(&G("clear-game"))?;
697     self.otter_resetting(&G("reset demo"))?;
698   }
699 }
700
701 impl UsualCtx {
702   #[throws(AE)]
703   pub fn setup() -> Self {
704     let (opts, _instance, su) = setup_core(
705       &[module_path!()],
706     )?;
707     let spec = su.ds.game_spec_data()?;
708     let mut mc = su.mgmt_conn();
709     let [alice, bob]: [Player; 2] =
710       su.ds.setup_static_users(&mut mc, default())?
711       .into_iter().map(|sus| Player { nick: sus.nick, url: sus.url })
712       .collect::<ArrayVec<_,2>>().into_inner().unwrap();
713     drop(mc);
714
715     let su_rc = Rc::new(RefCell::new(su));
716     UsualCtx {
717       opts, spec, su_rc, alice, bob,
718       has_lib_markers: false,
719       prctx: default(),
720     }
721   }
722 }
723
724 portmanteau_has!("at-otter.rs",   at_otter);
725 portmanteau_has!("at-bundles.rs", at_bundles);
726 portmanteau_has!("at-hidden.rs",  at_hidden);
727 portmanteau_has!("at-currency.rs", at_currency);
728
729 #[throws(AE)]
730 fn main() { portmanteau_main("at")? }