From 7e575d31ed17a5b8426c40e53cc51a77a2fecbfd Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Mon, 17 May 2021 18:15:10 +0100 Subject: [PATCH] apitest: Move a lot of code from at-otter to apitest/main Signed-off-by: Ian Jackson --- apitest/at-bundles.rs | 3 + apitest/at-otter.rs | 528 +---------------------------------------- apitest/main.rs | 533 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 536 insertions(+), 528 deletions(-) diff --git a/apitest/at-bundles.rs b/apitest/at-bundles.rs index a286802c..516ab88b 100644 --- a/apitest/at-bundles.rs +++ b/apitest/at-bundles.rs @@ -12,6 +12,9 @@ struct Ctx { su_rc: Setup, } +impl Ctx { +} + #[throws(Explode)] fn tests(_c: Ctx) { } diff --git a/apitest/at-otter.rs b/apitest/at-otter.rs index b0e6aedb..fb1843d4 100644 --- a/apitest/at-otter.rs +++ b/apitest/at-otter.rs @@ -4,535 +4,9 @@ use crate::*; -pub use index_vec::Idx; - -#[allow(dead_code)] -struct Ctx { - opts: Opts, - su_rc: Setup, - spec: GameSpec, - alice: Player, - bob: Player, - prctx: PathResolveContext, -} - -impl Ctx { - fn su(&self) -> std::cell::Ref { RefCell::borrow(&self.su_rc) } - fn su_mut(&self) -> RefMut { self.su_rc.borrow_mut() } - - fn wanted_tests(&self) -> TrackWantedTestsGuard { - TrackWantedTestsGuard(self.su_mut()) - } -} -struct TrackWantedTestsGuard<'m>(RefMut<'m, SetupCore>); -deref_to_field_mut!{TrackWantedTestsGuard<'_>, - TrackWantedTests, - 0.wanted_tests} - -#[derive(Debug)] -struct Player { - pub nick: &'static str, - url: String, -} - -struct Session { - pub nick: &'static str, - pub su_rc: Setup, - pub ctoken: RawToken, - pub gen: Generation, - pub cseq: RawClientSequence, - pub dom: scraper::Html, - pub updates: mpsc::Receiver, - pub client: reqwest::blocking::Client, -} - -mod scraper_ext { - use super::*; - use scraper::*; - use scraper::html::{*, Html}; - - #[ext(pub)] - impl Html { - fn select<'a,'b>(&'a self, selector: &'b Selector) -> Select<'a, 'b> { - self.select(selector) - } - - #[throws(as Option)] - fn element(&self, sel: S) -> ElementRef - where S: TryInto, - >::Error: Debug, - { - self - .select(&sel.try_into().unwrap()) - .next()? - } - - #[throws(as Option)] - fn e_attr(&self, sel: S, attr: &str) -> &str - where S: TryInto, - >::Error: Debug, - { - self - .element(sel).unwrap() - .value().attr(attr)? - } - } - - #[throws(Explode)] - pub fn parse_html(resp: reqwest::blocking::Response) -> Html { - assert_eq!(resp.status(), 200); - let body = resp.text()?; - let dom = scraper::Html::parse_document(&body); - //dbgc!(&&dom); - dom - } - - #[ext(pub, name=RequestBuilderExt)] - impl reqwest::blocking::RequestBuilder { - #[throws(Explode)] - fn send(self) -> reqwest::blocking::Response { self.send()? } - - #[throws(Explode)] - fn send_parse_html(self) -> Html { - let resp = self.send()?; - parse_html(resp)? - } - } -} - -use scraper_ext::{HtmlExt, RequestBuilderExt}; - -type Update = JsV; - -#[throws(Explode)] -fn updates_parser(input: R, out: &mut mpsc::Sender) { - let mut accum: HashMap = default(); - for l in BufReader::new(input).lines() { - let l = l?; - if ! l.is_empty() { - let mut l = l.splitn(2, ':'); - let lhs = l.next().unwrap(); - let rhs = l.next().unwrap(); - let rhs = rhs.trim_start(); - let () = accum.insert(lhs.to_string(), rhs.to_string()) - .is_none().expect("duplicate field"); - continue; - } - let entry = mem::take(&mut accum); - #[allow(unused_variables)] let accum = (); // stops accidental use of accum - if entry.get("event").map(String::as_str) == Some("commsworking") { - eprintln!("commsworking: {}", entry["data"]); - } else if let Some(event) = entry.get("event") { - panic!("unexpected event: {}", event); - } else { - let update = &entry["data"]; - let update = serde_json::from_str(update).unwrap(); - if out.send(update).is_err() { break } - } - } -} - -impl Ctx { - #[throws(Explode)] - fn connect_player<'su>(&self, player: &Player) -> Session { - let client = reqwest::blocking::Client::new(); - let loading = client.get(&player.url).send_parse_html()?; - let ptoken = loading.e_attr("#loading_token", "data-ptoken").unwrap(); - dbgc!(&ptoken); - - let session = client.post(&self.su().ds.subst("@url@/_/session/Portrait")?) - .json(&json!({ "ptoken": ptoken })) - .send_parse_html()?; - - let ctoken = session.e_attr("#main-body", "data-ctoken").unwrap(); - dbgc!(&ctoken); - - let gen: Generation = Generation( - session.e_attr("#main-body", "data-gen").unwrap() - .parse().unwrap() - ); - dbgc!(gen); - - let mut sse = client.get( - &self.su().ds - .also(&[("ctoken", ctoken), - ("gen", &gen.to_string())]) - .subst("@url@/_/updates?ctoken=@ctoken@&gen=@gen@")? - ).send()?; - - let (mut wpipe, rpipe) = UnixStream::pair()?; - thread::spawn(move ||{ - eprintln!("copy_to'ing"); - match sse.copy_to(&mut wpipe) { - Err(re) => match (||{ - // reqwest::Error won't give us the underlying io::Error :-/ - wpipe.write_all(b"\n")?; - wpipe.flush()?; - Ok::<_,io::Error>(()) - })() { - Err(pe) if pe.kind() == ErrorKind::BrokenPipe => { Ok(()) } - Err(pe) => Err(AE::from(pe)), - Ok(_) => Err(AE::from(re)), - } - Ok(_n) => Ok(()), - }.unwrap(); - eprintln!("copy_to'd!"); - }); - - let (mut csend, crecv) = mpsc::channel(); - thread::spawn(move ||{ - updates_parser(rpipe, &mut csend).expect("udpates parser failed") - }); - - Session { - nick: player.nick, - client, gen, - cseq: 42, - ctoken: RawToken(ctoken.to_string()), - dom: session, - updates: crecv, - su_rc: self.su_rc.clone(), - } - } - - pub fn chdir_root(&mut self, f: F) - where F: FnOnce(&mut Self) -> Result<(),Explode> - { - let tmp = self.su().ds.abstmp.clone(); - env::set_current_dir("/").expect("cd /"); - self.prctx = PathResolveContext::RelativeTo(tmp.clone()); - f(self).expect("run test"); - env::set_current_dir(&tmp).expect("cd back"); - self.prctx = default(); - } -} - -mod pi { - use otter::prelude::define_index_type; - define_index_type!{ pub struct PIA = usize; } - define_index_type!{ pub struct PIB = usize; } -} -pub use pi::*; - -type Pieces = IndexVec>; -type PiecesSlice = IndexSlice]>; - -#[derive(Debug,Clone)] -pub struct PieceInfo { - id: String, - pos: Pos, - info: I, -} - -impl Session { - #[throws(Explode)] - fn pieces(&self) -> Pieces { - let pieces = self.dom - .element("#pieces_marker") - .unwrap().next_siblings() - .map_loop(|puse: ego_tree::NodeRef| { - let puse = puse.value(); - let puse = puse.as_element().ok_or(Loop::Continue)?; - let attr = puse.attr("data-info").ok_or(Loop::Break)?; - let pos = Pos::from_iter(["x","y"].iter().map(|attr|{ - puse - .attr(attr).unwrap() - .parse().unwrap() - })).unwrap(); - let id = puse.id.as_ref().unwrap(); - let id = id.strip_prefix("use").unwrap().to_string(); - let info = serde_json::from_str(attr).unwrap(); - Loop::ok(PieceInfo { id, pos, info }) - }) - .collect(); - let nick = self.nick; - dbgc!(nick, &pieces); - pieces - } - - #[throws(Explode)] - fn api_piece_op_single(&mut self, piece: &str, o: O) { - let (opname, payload) = if let Some(o) = o.api() { o } else { return }; - - self.cseq += 1; - let cseq = self.cseq; - - let su = self.su_rc.borrow_mut(); - let resp = self.client.post(&su.ds.also(&[("opname",opname)]) - .subst("@url@/_/api/@opname@")?) - .json(&json!({ - "ctoken": self.ctoken, - "piece": piece, - "gen": self.gen, - "cseq": cseq, - "op": payload, - })) - .send()?; - assert_eq!(resp.status(), 200); - } - - #[throws(Explode)] - fn api_piece( - &mut self, g: GrabHow, mut p: P, o: O - ) { - if let GH::With | GH::Grab = g { - self.api_piece_op_single(p.id(), ("grab", json!({})))?; - } - if let Some(u) = p.for_update() { - o.update(u); - } - { - self.api_piece_op_single(p.id(), o)?; - } - if let Some(s) = p.for_synch() { - self.synchu(s)?; - } - if let GH::With | GH::Ungrab = g { - self.api_piece_op_single(p.id(), ("ungrab", json!({})))?; - } - } - - #[throws(Explode)] - fn await_update< - R, - F: FnMut(&mut Session, Generation, &str, &JsV) -> Option, - G: FnMut(&mut Session, Generation) -> Option, - E: FnMut(&mut Session, Generation, &JsV) - -> Result, AE> - > (&mut self, mut g: G, mut f: F, mut ef: Option) -> R { - let nick = self.nick; - 'overall: loop { - let update = self.updates.recv()?; - let update = update.as_array().unwrap(); - let new_gen = Generation( - update[0] - .as_i64().unwrap() - .try_into().unwrap() - ); - self.gen = new_gen; - dbgc!(nick, new_gen); - if let Some(y) = g(self, new_gen) { break 'overall y } - for ue in update[1].as_array().unwrap() { - let (k,v) = ue.as_object().unwrap().iter().next().unwrap(); - dbgc!(nick, k, &v); - if let Some(y) = { - if k != "Error" { - f(self, new_gen, k, v) - } else if let Some(ef) = &mut ef { - ef(self, new_gen, v)? - } else { - panic!("synch error: {:?}", &(k, v)); - } - } { break 'overall y } - } - } - } - - #[throws(Explode)] - fn synchx< - PI: Idx, - F: FnMut(&mut Session, Generation, &str, &JsV), - > (&mut self, - mut pieces: Option<&mut Pieces>, - ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV) - -> Result<(), AE>>, - mut f: F - ) { - let exp = { - self.su_rc.borrow_mut().mgmt_conn() - .game_synch(TABLE.parse().unwrap())? - }; - let efwrap = ef.map(|ef| { - move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) } - }); - self.await_update( - |_session, gen | (gen == exp).as_option(), - | session, gen, k, v| { - if let Some(pieces) = pieces.as_mut() { - update_update_pieces(&session.nick, pieces, k, v); - } - f(session,gen,k,v); - None - }, - efwrap, - )?; - } - - #[throws(Explode)] - fn synchu(&mut self, pieces: &mut Pieces) { - self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?; - } - - #[throws(Explode)] - fn synch(&mut self) { - self.synchx::(None, None, |_session, _gen, _k, _v|())?; - } -} - -pub fn update_update_pieces( - nick: &str, - pieces: &mut Pieces, - k: &str, v: &JsV -) { - if k != "Piece" { return } - let v = v.as_object().unwrap(); - let piece = v["piece"].as_str().unwrap(); - let p = pieces.iter_mut().find(|p| p.id == piece); - if_let!{ Some(p) = p; else return } - let (op, d) = v["op"].as_object().unwrap().iter().next().unwrap(); - - fn coord(j: &JsV) -> Pos { - PosC::from_iter_2( - j.as_array().unwrap().iter() - .map(|n| n.as_i64().unwrap().try_into().unwrap()) - ) - } - - match op.as_str() { - "Move" => { - p.pos = coord(d); - }, - "Modify" | "ModifyQuiet" => { - let d = d.as_object().unwrap(); - p.pos = coord(&d["pos"]); - for (k,v) in d { - p.info - .as_object_mut().unwrap() - .insert(k.to_string(), v.clone()); - } - }, - _ => { - panic!("unknown op {:?} {:?}", &op, &d); - }, - }; - dbgc!(nick, k,v,p); -} - -pub type PieceOpData = (&'static str, JsV); -pub trait PieceOp: Debug { - fn api(&self) -> Option; - fn update(&self, _pi: &mut PieceInfo) { info!("no update {:?}", self) } -} -impl PieceOp for PieceOpData { - fn api(&self) -> Option { Some((self.0, self.1.clone())) } -} -impl PieceOp for Pos { - fn api(&self) -> Option { Some(("m", json![self.coords])) } - fn update(&self, pi: &mut PieceInfo) { pi.pos = *self } -} -impl PieceOp for () { - fn api(&self) -> Option { None } - fn update(&self, _pi: &mut PieceInfo) { } -} - -pub trait PieceSpecForOp: Debug { - fn id(&self) -> &str; - type PI: Idx; - fn for_update(&mut self) -> Option<&mut PieceInfo> { None } - fn for_synch(&mut self) -> Option<&mut Pieces> { None } -} - -impl PieceSpecForOp for str { - type PI = PIA; - fn id(&self) -> &str { self } -} -impl PieceSpecForOp for &String { - type PI = PIA; - fn id(&self) -> &str { self } -} - -type PuUp<'pcs, PI> = (&'pcs mut Pieces, PI); -#[derive(Debug)] -/// Synchronise after op but before any ungrab. -pub struct PuSynch(T); - -macro_rules! impl_PieceSpecForOp { - ($($amp:tt $mut:tt)?) => { - - impl PieceSpecForOp for $($amp $mut)? PuUp<'_, PI> { - type PI = PI; - fn id(&self) -> &str { &self.0[self.1].id } - fn for_update(&mut self) -> Option<&mut PieceInfo> { - Some(&mut self.0[self.1]) - } - } - - impl PieceSpecForOp for PuSynch<$($amp $mut)? PuUp<'_,PI>> { - type PI = PI; - fn id(&self) -> &str { self.0.id() } - fn for_update(&mut self) -> Option<&mut PieceInfo> { - self.0.for_update() - } - fn for_synch(&mut self) -> Option<&mut Pieces> { - Some(self.0.0) - } - } - } -} -impl_PieceSpecForOp!{} -impl_PieceSpecForOp!{&mut} - -#[derive(Debug,Copy,Clone)] -pub enum GrabHow { Raw, Grab, Ungrab, With } -pub use GrabHow as GH; - +type Ctx = UsualCtx; impl Ctx { - #[throws(AE)] - pub fn otter>(&mut self, args: &[S]) -> OtterOutput { - let args: Vec = - ["--account", "server:"].iter().cloned().map(Into::into) - .chain(args.iter().map(|s| s.as_ref().to_owned())) - .collect(); - self.su().ds.otter_prctx(&self.prctx, &args)? - } - - #[throws(Explode)] - fn place_library_load_markers(&mut self) -> Session { - let mut session = self.connect_player(&self.alice)?; - let pieces = session.pieces::()?; - let llm = pieces.into_iter() - .filter(|pi| pi.info["desc"] == "a library load area marker") - .collect::>(); - let llm: [_;2] = llm.into_inner().unwrap(); - dbgc!(&llm); - - for (llm, &pos) in izip!(&llm, [PosC::new(5,5), PosC::new(50,25)].iter()) - { - session.api_piece(GH::With, &llm.id, pos)?; - } - - session.synch()?; - session - } - - #[throws(Explode)] - fn some_library_add(&mut self, command: &[String]) -> Vec { - let add_err = self.otter(command) - .expect_err("library-add succeeded after reset!"); - assert_eq!(add_err.downcast::()?.0.code(), - Some(EXIT_NOTFOUND)); - - let mut session = self.place_library_load_markers()?; - - self.otter(&command) - .expect("library-add failed after place!"); - - let mut added = vec![]; - session.synchx::(None, None, - |_session, _gen, k, v| if_chain! { - if k == "Piece"; - let piece = v["piece"].as_str().unwrap().to_string(); - let op = v["op"].as_object().unwrap(); - if let Some(_) = op.get("Insert"); - then { added.push(piece); } - } - )?; - - dbgc!(&added); - added - } - #[throws(Explode)] fn library_load(&mut self) { prepare_game(&self.su().ds, &self.prctx, TABLE)?; diff --git a/apitest/main.rs b/apitest/main.rs index 71dda64c..98afbb66 100644 --- a/apitest/main.rs +++ b/apitest/main.rs @@ -7,7 +7,538 @@ pub use otter_api_tests::*; pub use std::cell::{RefCell, RefMut}; pub use std::rc::Rc; -type Setup = Rc>; +pub type Setup = Rc>; + +pub use index_vec::Idx; + +struct TrackWantedTestsGuard<'m>(RefMut<'m, SetupCore>); +deref_to_field_mut!{TrackWantedTestsGuard<'_>, + TrackWantedTests, + 0.wanted_tests} + +#[allow(dead_code)] +struct UsualCtx { + opts: Opts, + su_rc: Setup, + spec: GameSpec, + alice: Player, + bob: Player, + prctx: PathResolveContext, +} + +impl UsualCtx { + fn su(&self) -> std::cell::Ref { RefCell::borrow(&self.su_rc) } + fn su_mut(&self) -> RefMut { self.su_rc.borrow_mut() } + + fn wanted_tests(&self) -> TrackWantedTestsGuard { + TrackWantedTestsGuard(self.su_mut()) + } +} + +#[derive(Debug)] +struct Player { + pub nick: &'static str, + url: String, +} + +struct Session { + pub nick: &'static str, + pub su_rc: Setup, + pub ctoken: RawToken, + pub gen: Generation, + pub cseq: RawClientSequence, + pub dom: scraper::Html, + pub updates: mpsc::Receiver, + pub client: reqwest::blocking::Client, +} + +mod scraper_ext { + use super::*; + use scraper::*; + use scraper::html::{*, Html}; + + #[ext(pub)] + impl Html { + fn select<'a,'b>(&'a self, selector: &'b Selector) -> Select<'a, 'b> { + self.select(selector) + } + + #[throws(as Option)] + fn element(&self, sel: S) -> ElementRef + where S: TryInto, + >::Error: Debug, + { + self + .select(&sel.try_into().unwrap()) + .next()? + } + + #[throws(as Option)] + fn e_attr(&self, sel: S, attr: &str) -> &str + where S: TryInto, + >::Error: Debug, + { + self + .element(sel).unwrap() + .value().attr(attr)? + } + } + + #[throws(Explode)] + pub fn parse_html(resp: reqwest::blocking::Response) -> Html { + assert_eq!(resp.status(), 200); + let body = resp.text()?; + let dom = scraper::Html::parse_document(&body); + //dbgc!(&&dom); + dom + } + + #[ext(pub, name=RequestBuilderExt)] + impl reqwest::blocking::RequestBuilder { + #[throws(Explode)] + fn send(self) -> reqwest::blocking::Response { self.send()? } + + #[throws(Explode)] + fn send_parse_html(self) -> Html { + let resp = self.send()?; + parse_html(resp)? + } + } +} + +use scraper_ext::{HtmlExt, RequestBuilderExt}; + +type Update = JsV; + +#[throws(Explode)] +fn updates_parser(input: R, out: &mut mpsc::Sender) { + let mut accum: HashMap = default(); + for l in BufReader::new(input).lines() { + let l = l?; + if ! l.is_empty() { + let mut l = l.splitn(2, ':'); + let lhs = l.next().unwrap(); + let rhs = l.next().unwrap(); + let rhs = rhs.trim_start(); + let () = accum.insert(lhs.to_string(), rhs.to_string()) + .is_none().expect("duplicate field"); + continue; + } + let entry = mem::take(&mut accum); + #[allow(unused_variables)] let accum = (); // stops accidental use of accum + if entry.get("event").map(String::as_str) == Some("commsworking") { + eprintln!("commsworking: {}", entry["data"]); + } else if let Some(event) = entry.get("event") { + panic!("unexpected event: {}", event); + } else { + let update = &entry["data"]; + let update = serde_json::from_str(update).unwrap(); + if out.send(update).is_err() { break } + } + } +} + +impl UsualCtx { + #[throws(Explode)] + fn connect_player<'su>(&self, player: &Player) -> Session { + let client = reqwest::blocking::Client::new(); + let loading = client.get(&player.url).send_parse_html()?; + let ptoken = loading.e_attr("#loading_token", "data-ptoken").unwrap(); + dbgc!(&ptoken); + + let session = client.post(&self.su().ds.subst("@url@/_/session/Portrait")?) + .json(&json!({ "ptoken": ptoken })) + .send_parse_html()?; + + let ctoken = session.e_attr("#main-body", "data-ctoken").unwrap(); + dbgc!(&ctoken); + + let gen: Generation = Generation( + session.e_attr("#main-body", "data-gen").unwrap() + .parse().unwrap() + ); + dbgc!(gen); + + let mut sse = client.get( + &self.su().ds + .also(&[("ctoken", ctoken), + ("gen", &gen.to_string())]) + .subst("@url@/_/updates?ctoken=@ctoken@&gen=@gen@")? + ).send()?; + + let (mut wpipe, rpipe) = UnixStream::pair()?; + thread::spawn(move ||{ + eprintln!("copy_to'ing"); + match sse.copy_to(&mut wpipe) { + Err(re) => match (||{ + // reqwest::Error won't give us the underlying io::Error :-/ + wpipe.write_all(b"\n")?; + wpipe.flush()?; + Ok::<_,io::Error>(()) + })() { + Err(pe) if pe.kind() == ErrorKind::BrokenPipe => { Ok(()) } + Err(pe) => Err(AE::from(pe)), + Ok(_) => Err(AE::from(re)), + } + Ok(_n) => Ok(()), + }.unwrap(); + eprintln!("copy_to'd!"); + }); + + let (mut csend, crecv) = mpsc::channel(); + thread::spawn(move ||{ + updates_parser(rpipe, &mut csend).expect("udpates parser failed") + }); + + Session { + nick: player.nick, + client, gen, + cseq: 42, + ctoken: RawToken(ctoken.to_string()), + dom: session, + updates: crecv, + su_rc: self.su_rc.clone(), + } + } + + pub fn chdir_root(&mut self, f: F) + where F: FnOnce(&mut Self) -> Result<(),Explode> + { + let tmp = self.su().ds.abstmp.clone(); + env::set_current_dir("/").expect("cd /"); + self.prctx = PathResolveContext::RelativeTo(tmp.clone()); + f(self).expect("run test"); + env::set_current_dir(&tmp).expect("cd back"); + self.prctx = default(); + } +} + +mod pi { + use otter::prelude::define_index_type; + define_index_type!{ pub struct PIA = usize; } + define_index_type!{ pub struct PIB = usize; } +} +pub use pi::*; + +type Pieces = IndexVec>; +type PiecesSlice = IndexSlice]>; + +#[derive(Debug,Clone)] +pub struct PieceInfo { + id: String, + pos: Pos, + info: I, +} + +impl Session { + #[throws(Explode)] + fn pieces(&self) -> Pieces { + let pieces = self.dom + .element("#pieces_marker") + .unwrap().next_siblings() + .map_loop(|puse: ego_tree::NodeRef| { + let puse = puse.value(); + let puse = puse.as_element().ok_or(Loop::Continue)?; + let attr = puse.attr("data-info").ok_or(Loop::Break)?; + let pos = Pos::from_iter(["x","y"].iter().map(|attr|{ + puse + .attr(attr).unwrap() + .parse().unwrap() + })).unwrap(); + let id = puse.id.as_ref().unwrap(); + let id = id.strip_prefix("use").unwrap().to_string(); + let info = serde_json::from_str(attr).unwrap(); + Loop::ok(PieceInfo { id, pos, info }) + }) + .collect(); + let nick = self.nick; + dbgc!(nick, &pieces); + pieces + } + + #[throws(Explode)] + fn api_piece_op_single(&mut self, piece: &str, o: O) { + let (opname, payload) = if let Some(o) = o.api() { o } else { return }; + + self.cseq += 1; + let cseq = self.cseq; + + let su = self.su_rc.borrow_mut(); + let resp = self.client.post(&su.ds.also(&[("opname",opname)]) + .subst("@url@/_/api/@opname@")?) + .json(&json!({ + "ctoken": self.ctoken, + "piece": piece, + "gen": self.gen, + "cseq": cseq, + "op": payload, + })) + .send()?; + assert_eq!(resp.status(), 200); + } + + #[throws(Explode)] + fn api_piece( + &mut self, g: GrabHow, mut p: P, o: O + ) { + if let GH::With | GH::Grab = g { + self.api_piece_op_single(p.id(), ("grab", json!({})))?; + } + if let Some(u) = p.for_update() { + o.update(u); + } + { + self.api_piece_op_single(p.id(), o)?; + } + if let Some(s) = p.for_synch() { + self.synchu(s)?; + } + if let GH::With | GH::Ungrab = g { + self.api_piece_op_single(p.id(), ("ungrab", json!({})))?; + } + } + + #[throws(Explode)] + fn await_update< + R, + F: FnMut(&mut Session, Generation, &str, &JsV) -> Option, + G: FnMut(&mut Session, Generation) -> Option, + E: FnMut(&mut Session, Generation, &JsV) + -> Result, AE> + > (&mut self, mut g: G, mut f: F, mut ef: Option) -> R { + let nick = self.nick; + 'overall: loop { + let update = self.updates.recv()?; + let update = update.as_array().unwrap(); + let new_gen = Generation( + update[0] + .as_i64().unwrap() + .try_into().unwrap() + ); + self.gen = new_gen; + dbgc!(nick, new_gen); + if let Some(y) = g(self, new_gen) { break 'overall y } + for ue in update[1].as_array().unwrap() { + let (k,v) = ue.as_object().unwrap().iter().next().unwrap(); + dbgc!(nick, k, &v); + if let Some(y) = { + if k != "Error" { + f(self, new_gen, k, v) + } else if let Some(ef) = &mut ef { + ef(self, new_gen, v)? + } else { + panic!("synch error: {:?}", &(k, v)); + } + } { break 'overall y } + } + } + } + + #[throws(Explode)] + fn synchx< + PI: Idx, + F: FnMut(&mut Session, Generation, &str, &JsV), + > (&mut self, + mut pieces: Option<&mut Pieces>, + ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV) + -> Result<(), AE>>, + mut f: F + ) { + let exp = { + self.su_rc.borrow_mut().mgmt_conn() + .game_synch(TABLE.parse().unwrap())? + }; + let efwrap = ef.map(|ef| { + move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) } + }); + self.await_update( + |_session, gen | (gen == exp).as_option(), + | session, gen, k, v| { + if let Some(pieces) = pieces.as_mut() { + update_update_pieces(&session.nick, pieces, k, v); + } + f(session,gen,k,v); + None + }, + efwrap, + )?; + } + + #[throws(Explode)] + fn synchu(&mut self, pieces: &mut Pieces) { + self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?; + } + + #[throws(Explode)] + fn synch(&mut self) { + self.synchx::(None, None, |_session, _gen, _k, _v|())?; + } +} + +pub fn update_update_pieces( + nick: &str, + pieces: &mut Pieces, + k: &str, v: &JsV +) { + if k != "Piece" { return } + let v = v.as_object().unwrap(); + let piece = v["piece"].as_str().unwrap(); + let p = pieces.iter_mut().find(|p| p.id == piece); + if_let!{ Some(p) = p; else return } + let (op, d) = v["op"].as_object().unwrap().iter().next().unwrap(); + + fn coord(j: &JsV) -> Pos { + PosC::from_iter_2( + j.as_array().unwrap().iter() + .map(|n| n.as_i64().unwrap().try_into().unwrap()) + ) + } + + match op.as_str() { + "Move" => { + p.pos = coord(d); + }, + "Modify" | "ModifyQuiet" => { + let d = d.as_object().unwrap(); + p.pos = coord(&d["pos"]); + for (k,v) in d { + p.info + .as_object_mut().unwrap() + .insert(k.to_string(), v.clone()); + } + }, + _ => { + panic!("unknown op {:?} {:?}", &op, &d); + }, + }; + dbgc!(nick, k,v,p); +} + +pub type PieceOpData = (&'static str, JsV); +pub trait PieceOp: Debug { + fn api(&self) -> Option; + fn update(&self, _pi: &mut PieceInfo) { info!("no update {:?}", self) } +} +impl PieceOp for PieceOpData { + fn api(&self) -> Option { Some((self.0, self.1.clone())) } +} +impl PieceOp for Pos { + fn api(&self) -> Option { Some(("m", json![self.coords])) } + fn update(&self, pi: &mut PieceInfo) { pi.pos = *self } +} +impl PieceOp for () { + fn api(&self) -> Option { None } + fn update(&self, _pi: &mut PieceInfo) { } +} + +pub trait PieceSpecForOp: Debug { + fn id(&self) -> &str; + type PI: Idx; + fn for_update(&mut self) -> Option<&mut PieceInfo> { None } + fn for_synch(&mut self) -> Option<&mut Pieces> { None } +} + +impl PieceSpecForOp for str { + type PI = PIA; + fn id(&self) -> &str { self } +} +impl PieceSpecForOp for &String { + type PI = PIA; + fn id(&self) -> &str { self } +} + +type PuUp<'pcs, PI> = (&'pcs mut Pieces, PI); +#[derive(Debug)] +/// Synchronise after op but before any ungrab. +pub struct PuSynch(T); + +macro_rules! impl_PieceSpecForOp { + ($($amp:tt $mut:tt)?) => { + + impl PieceSpecForOp for $($amp $mut)? PuUp<'_, PI> { + type PI = PI; + fn id(&self) -> &str { &self.0[self.1].id } + fn for_update(&mut self) -> Option<&mut PieceInfo> { + Some(&mut self.0[self.1]) + } + } + + impl PieceSpecForOp for PuSynch<$($amp $mut)? PuUp<'_,PI>> { + type PI = PI; + fn id(&self) -> &str { self.0.id() } + fn for_update(&mut self) -> Option<&mut PieceInfo> { + self.0.for_update() + } + fn for_synch(&mut self) -> Option<&mut Pieces> { + Some(self.0.0) + } + } + } +} +impl_PieceSpecForOp!{} +impl_PieceSpecForOp!{&mut} + +#[derive(Debug,Copy,Clone)] +pub enum GrabHow { Raw, Grab, Ungrab, With } +pub use GrabHow as GH; + + +impl UsualCtx { + #[throws(AE)] + pub fn otter>(&mut self, args: &[S]) -> OtterOutput { + let args: Vec = + ["--account", "server:"].iter().cloned().map(Into::into) + .chain(args.iter().map(|s| s.as_ref().to_owned())) + .collect(); + self.su().ds.otter_prctx(&self.prctx, &args)? + } + + #[throws(Explode)] + fn place_library_load_markers(&mut self) -> Session { + let mut session = self.connect_player(&self.alice)?; + let pieces = session.pieces::()?; + let llm = pieces.into_iter() + .filter(|pi| pi.info["desc"] == "a library load area marker") + .collect::>(); + let llm: [_;2] = llm.into_inner().unwrap(); + dbgc!(&llm); + + for (llm, &pos) in izip!(&llm, [PosC::new(5,5), PosC::new(50,25)].iter()) + { + session.api_piece(GH::With, &llm.id, pos)?; + } + + session.synch()?; + session + } + + #[throws(Explode)] + fn some_library_add(&mut self, command: &[String]) -> Vec { + let add_err = self.otter(command) + .expect_err("library-add succeeded after reset!"); + assert_eq!(add_err.downcast::()?.0.code(), + Some(EXIT_NOTFOUND)); + + let mut session = self.place_library_load_markers()?; + + self.otter(&command) + .expect("library-add failed after place!"); + + let mut added = vec![]; + session.synchx::(None, None, + |_session, _gen, k, v| if_chain! { + if k == "Piece"; + let piece = v["piece"].as_str().unwrap().to_string(); + let op = v["op"].as_object().unwrap(); + if let Some(_) = op.get("Insert"); + then { added.push(piece); } + } + )?; + + dbgc!(&added); + added + } +} portmanteau_has!("at-otter.rs", at_otter); portmanteau_has!("at-bundles.rs", at_bundles); -- 2.30.2