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 pub use otter_api_tests::*;
7 pub use std::cell::{RefCell, RefMut};
10 pub type Setup = Rc<RefCell<SetupCore>>;
12 pub use index_vec::Idx;
14 struct TrackWantedTestsGuard<'m>(RefMut<'m, SetupCore>);
15 deref_to_field_mut!{TrackWantedTestsGuard<'_>,
26 prctx: PathResolveContext,
27 has_lib_markers: bool,
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() }
35 pub fn wanted_tests(&self) -> TrackWantedTestsGuard {
36 TrackWantedTestsGuard(self.su_mut())
42 pub nick: &'static str,
47 pub nick: &'static str,
51 pub cseq: RawClientSequence,
52 pub dom: scraper::Html,
53 pub updates: mpsc::Receiver<Update>,
54 pub client: reqwest::blocking::Client,
60 use scraper::html::Html;
65 fn element<S>(&self, sel: S) -> ElementRef
66 where S: TryInto<Selector>,
67 <S as TryInto<Selector>>::Error: Debug,
70 .select(&sel.try_into().unwrap())
75 fn e_attr<S>(&self, sel: S, attr: &str) -> &str
76 where S: TryInto<Selector>,
77 <S as TryInto<Selector>>::Error: Debug,
80 .element(sel).unwrap()
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);
94 #[ext(pub, name=RequestBuilderExt)]
95 impl reqwest::blocking::RequestBuilder {
97 fn send(self) -> reqwest::blocking::Response { self.send()? }
100 fn send_parse_html(self) -> Html {
101 let resp = self.send()?;
107 use scraper_ext::{HtmlExt, RequestBuilderExt};
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() {
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");
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);
132 let update = &entry["data"];
133 let update = serde_json::from_str(update).unwrap();
134 if out.send(update).is_err() { break }
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();
147 let session = client.post(&self.su().ds.subst("@url@/_/session/Portrait")?)
148 .json(&json!({ "ptoken": ptoken }))
151 let ctoken = session.e_attr("#main-body", "data-ctoken").unwrap();
154 let gen: Generation = Generation(
155 session.e_attr("#main-body", "data-gen").unwrap()
160 let mut sse = client.get(
162 .also(&[("ctoken", ctoken),
163 ("gen", &gen.to_string())])
164 .subst("@url@/_/updates?ctoken=@ctoken@&gen=@gen@")?
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")?;
175 Ok::<_,io::Error>(())
177 Err(pe) if pe.kind() == ErrorKind::BrokenPipe => { Ok(()) }
178 Err(pe) => Err(AE::from(pe)),
179 Ok(_) => Err(AE::from(re)),
183 eprintln!("copy_to'd!");
186 let (mut csend, crecv) = mpsc::channel();
187 thread::spawn(move ||{
188 updates_parser(rpipe, &mut csend).expect("udpates parser failed")
195 ctoken: RawToken(ctoken.to_string()),
198 su_rc: self.su_rc.clone(),
202 pub fn chdir_root<F>(&mut self, f: F)
203 where F: FnOnce(&mut Self) -> Result<(),Explode>
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();
214 type Pieces<PI> = IndexVec<PI, PieceInfo<JsV /*~PreparedPieceState*/>>;
215 type PiecesSlice<PI> = IndexSlice<PI,[PieceInfo<JsV>]>;
217 #[derive(Debug,Clone)]
218 pub struct PieceInfo<I> {
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:?}");
233 fn pieces<PI:Idx>(&self) -> Pieces<PI> {
235 let mut pieces: Pieces<PI> = default();
237 .element("#pieces_marker")
238 .unwrap().next_siblings()
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|{
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 });
255 let nick = self.nick;
256 dbgc!(nick, &pieces);
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 };
265 let cseq = self.cseq;
267 let su = self.su_rc.borrow_mut();
268 let resp = self.client.post(&su.ds.also(&[("opname",opname)])
269 .subst("@url@/_/api/@opname@")?)
271 "ctoken": self.ctoken,
278 assert_eq!(resp.status(), 200);
282 fn api_piece<P:PieceSpecForOp, O:PieceOp>(
283 &mut self, g: GrabHow, mut p: P, o: O
285 if let GH::With | GH::Grab = g {
286 self.api_piece_op_single(p.id(), ("grab", json!({})))?;
288 if let Some(u) = p.for_update() {
292 self.api_piece_op_single(p.id(), o)?;
294 if let Some(s) = p.for_synch() {
297 if let GH::With | GH::Ungrab = g {
298 self.api_piece_op_single(p.id(), ("ungrab", json!({})))?;
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;
312 let update = self.updates.recv()?;
313 let update = update.as_array().unwrap();
314 let new_gen = Generation(
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();
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)?
333 panic!("synch error: {:?}", &(k, v));
335 } { break 'overall y }
343 F: FnMut(&mut Session, Generation, &str, &JsV),
345 mut pieces: Option<&mut Pieces<PI>>,
346 ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV)
351 self.su_rc.borrow_mut().mgmt_conn()
352 .game_synch(TABLE.parse().unwrap())?
354 let efwrap = ef.map(|ef| {
355 move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) }
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);
371 fn synchu<PI:Idx>(&mut self, pieces: &mut Pieces<PI>) {
372 self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?;
376 fn synch(&mut self) {
377 self.synchx::<PIA,_>(None, None, |_session, _gen, _k, _v|())?;
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()));
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)
396 fn find_by_desc_glob(&self, desc_glob: &str) -> PI {
397 let [pc] = self.filter_by_desc_glob(desc_glob)
399 .collect::<ArrayVec<_,1>>()
400 .into_inner().expect("multiple pieces matched, unexpectedly");
401 dbgc!(desc_glob, pc);
406 pub fn update_update_pieces<PI:Idx>(
408 pieces: &mut Pieces<PI>,
411 fn coord(j: &JsV) -> Pos {
413 j.as_array().unwrap().iter()
414 .map(|n| n.as_i64().unwrap().try_into().unwrap())
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)
425 let v = v.as_object().unwrap();
426 let p = findp(pieces, v);
430 for k in ["zg", "svg", "desc"] {
436 } else if k == "Piece" {
437 let (op, d) = v["op"].as_object().unwrap().iter().next().unwrap();
444 "Insert" | "InsertQuiet" => {
445 assert!(p.is_none());
446 let piece = v["piece"].as_str().unwrap();
447 pieces.push(PieceInfo {
449 pos: coord(d.get("pos").unwrap()),
453 "Move" | "MoveQuiet" => {
454 p.unwrap().pos = coord(d);
456 "Modify" | "ModifyQuiet" => {
458 let d = d.as_object().unwrap();
459 p.pos = coord(&d["pos"]);
463 panic!("unknown op {:?} {:?}", &op, &d);
466 } else if k == "Image" {
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" {
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" {
480 panic!("Unknown update: {k} {v:?}");
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) }
489 impl PieceOp for PieceOpData {
490 fn api(&self) -> Option<PieceOpData> { Some((self.0, self.1.clone())) }
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 }
496 impl PieceOp for () {
497 fn api(&self) -> Option<PieceOpData> { None }
498 fn update(&self, _pi: &mut PieceInfo<JsV>) { }
501 pub trait PieceSpecForOp: Debug {
502 fn id(&self) -> &str;
504 fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> { None }
505 fn for_synch(&mut self) -> Option<&mut Pieces<Self::PI>> { None }
508 impl PieceSpecForOp for str {
510 fn id(&self) -> &str { self }
512 impl PieceSpecForOp for &String {
514 fn id(&self) -> &str { self }
517 type PuUp<'pcs, PI> = (&'pcs mut Pieces<PI>, PI);
519 /// Synchronise after op but before any ungrab.
520 pub struct PuSynch<T>(T);
522 macro_rules! impl_PieceSpecForOp {
523 ($($amp:tt $mut:tt)?) => {
525 impl<PI:Idx> PieceSpecForOp for $($amp $mut)? PuUp<'_, 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])
533 impl<PI:Idx> PieceSpecForOp for PuSynch<$($amp $mut)? PuUp<'_,PI>> {
535 fn id(&self) -> &str { self.0.id() }
536 fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> {
539 fn for_synch(&mut self) -> Option<&mut Pieces<PI>> {
545 impl_PieceSpecForOp!{}
546 impl_PieceSpecForOp!{&mut}
548 #[derive(Debug,Copy,Clone)]
549 pub enum GrabHow { Raw, Grab, Ungrab, With }
550 pub use GrabHow as GH;
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())
560 self.su().ds.otter_prctx(&self.prctx, &args)?
564 pub fn prepare_game(&mut self) {
565 prepare_game(&self.su().ds, &self.prctx, TABLE)?;
566 self.has_lib_markers = false;
570 pub fn otter_resetting(&mut self, args: &dyn OtterArgsSpec)
572 self.has_lib_markers = false;
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));
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")
589 .collect::<ArrayVec<_,2>>()
590 .into_inner().unwrap();
593 for (llm, &pos) in izip!(&llm, [PosC::new(5,5), PosC::new(50,25)].iter())
595 session.api_piece(GH::With, &llm.id, pos)?;
597 self.has_lib_markers = true;
602 self.connect_player(&self.alice)?
606 .expect("library-add failed after place!");
608 let mut added = vec![];
609 session.synchx::<PIA,_>(None, None,
610 |_session, _gen, k, v| if_chain! {
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); }
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()?;
635 pub fn check_library_item(&mut self, itemlib: &str, item: &str,
637 let ds = self.su().ds.also(&[
638 ("itemlib", itemlib),
641 let command = ds.gss("library-add --lib @itemlib@ @item@")?;
642 let added = self.some_library_add(&command)?;
643 assert_eq!( added.len(), 1 );
645 let output: String = self.otter(&ds.gss("list-pieces")?)?.into();
646 assert_eq!( Regex::new(
648 r#"(?m)(?:[^\w-]|^){}[^\w-].*\W{}(?:\W|$)"#,
652 .find_iter(&output).count(),
658 pub fn upload_and_check_bundle(
659 &mut self, bundle_stem: &str,
660 libname: &str, item: &str,
662 with: &mut dyn FnMut(&mut UsualCtx) -> Result<(), Explode>
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) }
675 self.check_library_item(libname,item,desc)?;
677 self.stop_and_restart_server()?;
680 self.su().mgmt_conn().list_pieces()?
682 .find(|pi| pi.itemname.as_str() == item)
685 self.su().mgmt_conn().alter_game(vec![MGI::DeletePiece(id)], None)?;
687 self.check_library_item(libname,item,desc)?;
691 self.clear_reset_to_demo()?;
695 pub fn clear_reset_to_demo(&mut self) {
696 self.otter_resetting(&G("clear-game"))?;
697 self.otter_resetting(&G("reset demo"))?;
703 pub fn setup() -> Self {
704 let (opts, _instance, su) = setup_core(
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();
715 let su_rc = Rc::new(RefCell::new(su));
717 opts, spec, su_rc, alice, bob,
718 has_lib_markers: false,
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);
730 fn main() { portmanteau_main("at")? }