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();
215 use otter::prelude::define_index_type;
216 define_index_type!{ pub struct PIA = usize; }
217 define_index_type!{ pub struct PIB = usize; }
221 type Pieces<PI> = IndexVec<PI, PieceInfo<JsV /*~PreparedPieceState*/>>;
222 type PiecesSlice<PI> = IndexSlice<PI,[PieceInfo<JsV>]>;
224 #[derive(Debug,Clone)]
225 pub struct PieceInfo<I> {
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:?}");
240 fn pieces<PI:Idx>(&self) -> Pieces<PI> {
242 let mut pieces: Pieces<PI> = default();
244 .element("#pieces_marker")
245 .unwrap().next_siblings()
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|{
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 });
262 let nick = self.nick;
263 dbgc!(nick, &pieces);
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 };
272 let cseq = self.cseq;
274 let su = self.su_rc.borrow_mut();
275 let resp = self.client.post(&su.ds.also(&[("opname",opname)])
276 .subst("@url@/_/api/@opname@")?)
278 "ctoken": self.ctoken,
285 assert_eq!(resp.status(), 200);
289 fn api_piece<P:PieceSpecForOp, O:PieceOp>(
290 &mut self, g: GrabHow, mut p: P, o: O
292 if let GH::With | GH::Grab = g {
293 self.api_piece_op_single(p.id(), ("grab", json!({})))?;
295 if let Some(u) = p.for_update() {
299 self.api_piece_op_single(p.id(), o)?;
301 if let Some(s) = p.for_synch() {
304 if let GH::With | GH::Ungrab = g {
305 self.api_piece_op_single(p.id(), ("ungrab", json!({})))?;
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;
319 let update = self.updates.recv()?;
320 let update = update.as_array().unwrap();
321 let new_gen = Generation(
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();
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)?
340 panic!("synch error: {:?}", &(k, v));
342 } { break 'overall y }
350 F: FnMut(&mut Session, Generation, &str, &JsV),
352 mut pieces: Option<&mut Pieces<PI>>,
353 ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV)
358 self.su_rc.borrow_mut().mgmt_conn()
359 .game_synch(TABLE.parse().unwrap())?
361 let efwrap = ef.map(|ef| {
362 move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) }
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);
378 fn synchu<PI:Idx>(&mut self, pieces: &mut Pieces<PI>) {
379 self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?;
383 fn synch(&mut self) {
384 self.synchx::<PIA,_>(None, None, |_session, _gen, _k, _v|())?;
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()));
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)
403 fn find_by_desc_glob(&self, desc_glob: &str) -> PI {
404 let [pc] = self.filter_by_desc_glob(desc_glob)
406 .collect::<ArrayVec<_,1>>()
407 .into_inner().expect("multiple pieces matched, unexpectedly");
408 dbgc!(desc_glob, pc);
413 pub fn update_update_pieces<PI:Idx>(
415 pieces: &mut Pieces<PI>,
418 fn coord(j: &JsV) -> Pos {
420 j.as_array().unwrap().iter()
421 .map(|n| n.as_i64().unwrap().try_into().unwrap())
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)
432 let v = v.as_object().unwrap();
433 let p = findp(pieces, v);
437 for k in ["zg", "svg", "desc"] {
443 } else if k == "Piece" {
444 let (op, d) = v["op"].as_object().unwrap().iter().next().unwrap();
451 "Insert" | "InsertQuiet" => {
452 assert!(p.is_none());
453 let piece = v["piece"].as_str().unwrap();
454 pieces.push(PieceInfo {
456 pos: coord(d.get("pos").unwrap()),
460 "Move" | "MoveQuiet" => {
461 p.unwrap().pos = coord(d);
463 "Modify" | "ModifyQuiet" => {
465 let d = d.as_object().unwrap();
466 p.pos = coord(&d["pos"]);
470 panic!("unknown op {:?} {:?}", &op, &d);
473 } else if k == "Image" {
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" {
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" {
487 panic!("Unknown update: {k} {v:?}");
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) }
496 impl PieceOp for PieceOpData {
497 fn api(&self) -> Option<PieceOpData> { Some((self.0, self.1.clone())) }
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 }
503 impl PieceOp for () {
504 fn api(&self) -> Option<PieceOpData> { None }
505 fn update(&self, _pi: &mut PieceInfo<JsV>) { }
508 pub trait PieceSpecForOp: Debug {
509 fn id(&self) -> &str;
511 fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> { None }
512 fn for_synch(&mut self) -> Option<&mut Pieces<Self::PI>> { None }
515 impl PieceSpecForOp for str {
517 fn id(&self) -> &str { self }
519 impl PieceSpecForOp for &String {
521 fn id(&self) -> &str { self }
524 type PuUp<'pcs, PI> = (&'pcs mut Pieces<PI>, PI);
526 /// Synchronise after op but before any ungrab.
527 pub struct PuSynch<T>(T);
529 macro_rules! impl_PieceSpecForOp {
530 ($($amp:tt $mut:tt)?) => {
532 impl<PI:Idx> PieceSpecForOp for $($amp $mut)? PuUp<'_, 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])
540 impl<PI:Idx> PieceSpecForOp for PuSynch<$($amp $mut)? PuUp<'_,PI>> {
542 fn id(&self) -> &str { self.0.id() }
543 fn for_update(&mut self) -> Option<&mut PieceInfo<JsV>> {
546 fn for_synch(&mut self) -> Option<&mut Pieces<PI>> {
552 impl_PieceSpecForOp!{}
553 impl_PieceSpecForOp!{&mut}
555 #[derive(Debug,Copy,Clone)]
556 pub enum GrabHow { Raw, Grab, Ungrab, With }
557 pub use GrabHow as GH;
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())
567 self.su().ds.otter_prctx(&self.prctx, &args)?
571 pub fn prepare_game(&mut self) {
572 prepare_game(&self.su().ds, &self.prctx, TABLE)?;
573 self.has_lib_markers = false;
577 pub fn otter_resetting(&mut self, args: &dyn OtterArgsSpec)
579 self.has_lib_markers = false;
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));
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")
596 .collect::<ArrayVec<_,2>>()
597 .into_inner().unwrap();
600 for (llm, &pos) in izip!(&llm, [PosC::new(5,5), PosC::new(50,25)].iter())
602 session.api_piece(GH::With, &llm.id, pos)?;
604 self.has_lib_markers = true;
609 self.connect_player(&self.alice)?
613 .expect("library-add failed after place!");
615 let mut added = vec![];
616 session.synchx::<PIA,_>(None, None,
617 |_session, _gen, k, v| if_chain! {
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); }
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()?;
642 pub fn check_library_item(&mut self, itemlib: &str, item: &str,
644 let ds = self.su().ds.also(&[
645 ("itemlib", itemlib),
648 let command = ds.gss("library-add --lib @itemlib@ @item@")?;
649 let added = self.some_library_add(&command)?;
650 assert_eq!( added.len(), 1 );
652 let output: String = self.otter(&ds.gss("list-pieces")?)?.into();
653 assert_eq!( Regex::new(
655 r#"(?m)(?:[^\w-]|^){}[^\w-].*\W{}(?:\W|$)"#,
659 .find_iter(&output).count(),
665 pub fn upload_and_check_bundle(
666 &mut self, bundle_stem: &str,
667 libname: &str, item: &str,
669 with: &mut dyn FnMut(&mut UsualCtx) -> Result<(), Explode>
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) }
682 self.check_library_item(libname,item,desc)?;
684 self.stop_and_restart_server()?;
687 self.su().mgmt_conn().list_pieces()?
689 .find(|pi| pi.itemname.as_str() == item)
692 self.su().mgmt_conn().alter_game(vec![MGI::DeletePiece(id)], None)?;
694 self.check_library_item(libname,item,desc)?;
698 self.clear_reset_to_demo()?;
702 pub fn clear_reset_to_demo(&mut self) {
703 self.otter_resetting(&G("clear-game"))?;
704 self.otter_resetting(&G("reset demo"))?;
710 pub fn setup() -> Self {
711 let (opts, _instance, su) = setup_core(
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();
722 let su_rc = Rc::new(RefCell::new(su));
724 opts, spec, su_rc, alice, bob,
725 has_lib_markers: false,
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);
737 fn main() { portmanteau_main("at")? }