1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
6 #![allow(unused_variables)]
8 use otter_api_tests::*;
10 pub use std::cell::{RefCell, RefMut};
13 pub use index_vec::Idx;
15 type Setup = Rc<RefCell<SetupCore>>;
26 fn su(&self) -> std::cell::Ref<SetupCore> { RefCell::borrow(&self.su_rc) }
27 fn su_mut(&self) -> RefMut<SetupCore> { self.su_rc.borrow_mut() }
29 fn wanted_tests(&self) -> TrackWantedTestsGuard {
30 TrackWantedTestsGuard(self.su_mut())
33 struct TrackWantedTestsGuard<'m>(RefMut<'m, SetupCore>);
34 deref_to_field_mut!{TrackWantedTestsGuard<'_>,
40 pub nick: &'static str,
45 pub nick: &'static str,
49 pub cseq: RawClientSequence,
50 pub dom: scraper::Html,
51 pub updates: mpsc::Receiver<Update>,
52 pub client: reqwest::blocking::Client,
58 use scraper::html::{*, Html};
62 fn select<'a,'b>(&'a self, selector: &'b Selector) -> Select<'a, 'b> {
67 fn element<S>(&self, sel: S) -> ElementRef
68 where S: TryInto<Selector>,
69 <S as TryInto<Selector>>::Error: Debug,
72 .select(&sel.try_into().unwrap())
77 fn e_attr<S>(&self, sel: S, attr: &str) -> &str
78 where S: TryInto<Selector>,
79 <S as TryInto<Selector>>::Error: Debug,
82 .element(sel).unwrap()
88 pub fn parse_html(resp: reqwest::blocking::Response) -> Html {
89 assert_eq!(resp.status(), 200);
90 let body = resp.text()?;
91 let dom = scraper::Html::parse_document(&body);
96 #[ext(pub, name=RequestBuilderExt)]
97 impl reqwest::blocking::RequestBuilder {
99 fn send(self) -> reqwest::blocking::Response { self.send()? }
102 fn send_parse_html(self) -> Html {
103 let resp = self.send()?;
109 use scraper_ext::{HtmlExt, RequestBuilderExt};
114 fn updates_parser<R:Read>(input: R, out: &mut mpsc::Sender<Update>) {
115 let mut accum: HashMap<String, String> = default();
116 for l in BufReader::new(input).lines() {
119 let mut l = l.splitn(2, ':');
120 let lhs = l.next().unwrap();
121 let rhs = l.next().unwrap();
122 let rhs = rhs.trim_start();
123 let ins = accum.insert(lhs.to_string(), rhs.to_string())
124 .is_none().expect("duplicate field");
127 let entry = mem::take(&mut accum);
128 let accum = (); // stops accidental use of accum
129 if entry.get("event").map(String::as_str) == Some("commsworking") {
130 eprintln!("commsworking: {}", entry["data"]);
131 } else if let Some(event) = entry.get("event") {
132 panic!("unexpected event: {}", event);
134 let update = &entry["data"];
135 let update = serde_json::from_str(update).unwrap();
136 if out.send(update).is_err() { break }
143 fn connect_player<'su>(&self, player: &Player) -> Session {
144 let client = reqwest::blocking::Client::new();
145 let loading = client.get(&player.url).send_parse_html()?;
146 let ptoken = loading.e_attr("#loading_token", "data-ptoken").unwrap();
149 let session = client.post(&self.su().ds.subst("@url@/_/session/Portrait")?)
150 .json(&json!({ "ptoken": ptoken }))
153 let ctoken = session.e_attr("#main-body", "data-ctoken").unwrap();
156 let gen: Generation = Generation(
157 session.e_attr("#main-body", "data-gen").unwrap()
162 let mut sse = client.get(
164 .also(&[("ctoken", ctoken),
165 ("gen", &gen.to_string())])
166 .subst("@url@/_/updates?ctoken=@ctoken@&gen=@gen@")?
169 let (mut wpipe, rpipe) = UnixStream::pair()?;
170 thread::spawn(move ||{
171 eprintln!("copy_to'ing");
172 match sse.copy_to(&mut wpipe) {
173 Err(re) => match (||{
174 // reqwest::Error won't give us the underlying io::Error :-/
175 wpipe.write_all(b"\n")?;
177 Ok::<_,io::Error>(())
179 Err(pe) if pe.kind() == ErrorKind::BrokenPipe => { Ok(()) }
180 Err(pe) => Err(AE::from(pe)),
181 Ok(_) => Err(AE::from(re)),
185 eprintln!("copy_to'd!");
188 let (mut csend, crecv) = mpsc::channel();
189 thread::spawn(move ||{
190 updates_parser(rpipe, &mut csend).expect("udpates parser failed")
197 ctoken: RawToken(ctoken.to_string()),
200 su_rc: self.su_rc.clone(),
206 use otter::prelude::define_index_type;
207 define_index_type!{ pub struct PIA = usize; }
208 define_index_type!{ pub struct PIB = usize; }
212 type Pieces<PI> = IndexVec<PI, PieceInfo<JsV>>;
213 type PiecesSlice<PI> = IndexSlice<PI,[PieceInfo<JsV>]>;
215 #[derive(Debug,Clone)]
216 pub struct PieceInfo<I> {
224 fn pieces<PI:Idx>(&self) -> Pieces<PI> {
225 let pieces = self.dom
226 .element("#pieces_marker")
227 .unwrap().next_siblings()
228 .map_loop(|puse: ego_tree::NodeRef<scraper::Node>| {
229 let puse = puse.value();
230 let puse = puse.as_element().ok_or(Loop::Continue)?;
231 let attr = puse.attr("data-info").ok_or(Loop::Break)?;
232 let pos = Pos::from_iter(["x","y"].iter().map(|attr|{
237 let id = puse.id.as_ref().unwrap();
238 let id = id.strip_prefix("use").unwrap().to_string();
239 let info = serde_json::from_str(attr).unwrap();
240 Loop::ok(PieceInfo { id, pos, info })
243 let nick = self.nick;
244 dbgc!(nick, &pieces);
249 fn api_piece_op(&mut self, piece: &str, opname: &str,
252 let cseq = self.cseq;
254 let su = self.su_rc.borrow_mut();
255 let resp = self.client.post(&su.ds.also(&[("opname",opname)])
256 .subst("@url@/_/api/@opname@")?)
258 "ctoken": self.ctoken,
265 assert_eq!(resp.status(), 200);
269 fn api_with_piece_op(&mut self, piece: &str,
270 pathfrag: &str, op: JsV) {
271 self.api_piece_op(piece, "grab", json!({}))?;
272 self.api_piece_op(piece, pathfrag, op)?;
273 self.api_piece_op(piece, "ungrab", json!({}))?;
277 fn api_with_piece_op_synch<PI:Idx>(
278 &mut self, pieces: &mut Pieces<PI>, i: PI,
279 pathfrag: &str, op: JsV
281 let piece = &pieces[i].id;
282 self.api_piece_op(piece, "grab", json!({}))?;
283 self.api_piece_op(piece, pathfrag, op)?;
284 self.synchu(pieces)?;
285 let piece = &pieces[i].id;
286 self.api_piece_op(piece, "ungrab", json!({}))?;
292 F: FnMut(&mut Session, Generation, &str, &JsV) -> Option<R>,
293 G: FnMut(&mut Session, Generation) -> Option<R>,
294 E: FnMut(&mut Session, Generation, &JsV)
295 -> Result<Option<R>, AE>
296 > (&mut self, mut g: G, mut f: F, mut ef: Option<E>) -> R {
297 let nick = self.nick;
299 let update = self.updates.recv()?;
300 let update = update.as_array().unwrap();
301 let new_gen = Generation(
307 dbgc!(nick, new_gen);
308 if let Some(y) = g(self, new_gen) { break 'overall y }
309 for ue in update[1].as_array().unwrap() {
310 let (k,v) = ue.as_object().unwrap().iter().next().unwrap();
314 f(self, new_gen, k, v)
315 } else if let Some(ef) = &mut ef {
316 ef(self, new_gen, v)?
318 panic!("synch error: {:?}", &v);
320 } { break 'overall y }
328 F: FnMut(&mut Session, Generation, &str, &JsV),
330 mut pieces: Option<&mut Pieces<PI>>,
331 ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV)
336 let mut su = self.su_rc.borrow_mut();
337 su.mgmt_conn.game_synch(TABLE.parse().unwrap())?
339 let efwrap = ef.map(|ef| {
340 move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) }
343 |session, gen | (gen == exp).as_option(),
344 |session, gen, k, v| {
345 if let Some(pieces) = pieces.as_mut() {
346 update_update_pieces(&session.nick, pieces, k, v);
356 fn synchu<PI:Idx>(&mut self, pieces: &mut Pieces<PI>) {
357 self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?;
361 fn synch(&mut self) {
362 self.synchx::<PIA,_>(None, None, |_session, _gen, _k, _v|())?;
366 pub fn update_update_pieces<PI:Idx>(
368 pieces: &mut Pieces<PI>,
371 if k != "Piece" { return }
372 let v = v.as_object().unwrap();
373 let piece = v["piece"].as_str().unwrap();
374 let p = pieces.iter_mut().find(|p| p.id == piece);
375 let p = if let Some(p) = p { p } else { return };
376 let (op, d) = v["op"].as_object().unwrap().iter().next().unwrap();
378 fn coord(j: &JsV) -> Pos {
380 j.as_array().unwrap().iter()
381 .map(|n| n.as_i64().unwrap().try_into().unwrap())
382 .collect::<ArrayVec<_>>().into_inner().unwrap()
390 "Modify" | "ModifyQuiet" => {
391 let d = d.as_object().unwrap();
392 p.pos = coord(&d["pos"]);
395 .as_object_mut().unwrap()
396 .insert(k.to_string(), v.clone());
400 panic!("unknown op {:?} {:?}", &op, &d);
408 pub fn otter<S:AsRef<str>>(&mut self, args: &[S]) {
409 let args: Vec<String> =
410 ["--account", "server:"].iter().cloned().map(Into::into)
411 .chain(args.iter().map(|s| s.as_ref().to_owned()))
413 self.su().ds.otter(&args)?;
417 fn library_load(&mut self) {
418 prepare_game(&self.su().ds, TABLE)?;
420 let command = self.su().ds.ss(
421 "library-add @table@ wikimedia chess-blue-?"
423 let add_err = self.otter(&command)
424 .expect_err("library-add succeeded after reset!");
425 assert_eq!(add_err.downcast::<ExitStatusError>()?.0.code(),
426 Some(EXIT_NOTFOUND));
428 let mut session = self.connect_player(&self.alice)?;
429 let pieces = session.pieces::<PIA>()?;
430 let llm = pieces.into_iter()
431 .filter(|pi| pi.info["desc"] == "a library load area marker")
432 .collect::<ArrayVec<_>>();
433 let llm: [_;2] = llm.into_inner().unwrap();
436 for (llm, pos) in izip!(&llm, [PosC([5,5]), PosC([50,25])].iter()) {
437 session.api_with_piece_op(&llm.id, "m", json![pos.0])?;
443 .expect("library-add failed after place!");
445 let mut added = vec![];
446 session.synchx::<PIA,_>(None, None,
447 |session, gen, k, v| if_chain! {
449 let piece = v["piece"].as_str().unwrap().to_string();
450 let op = v["op"].as_object().unwrap();
451 if let Some(_) = op.get("Insert");
452 then { added.push(piece); }
456 assert_eq!(added.len(), 6);
460 fn hidden_hand(&mut self) {
461 prepare_game(&self.su().ds, TABLE)?;
462 let mut alice = self.connect_player(&self.alice)?;
463 let mut bob = self.connect_player(&self.bob)?;
464 self.su_mut().mgmt_conn.fakerng_load(&[&"1"])?;
466 let mut a_pieces = alice.pieces::<PIA>()?;
467 let mut b_pieces = alice.pieces::<PIB>()?;
469 let [hand] = a_pieces.iter_enumerated()
470 .filter(|(i,p)| p.info["desc"] == otter::hand::UNCLAIMED_DESC)
472 .collect::<ArrayVec<[_;1]>>()
473 .into_inner().unwrap();
476 alice.api_with_piece_op_synch(&mut a_pieces, hand, "k", json!({
478 "wrc": "Unpredictable",
481 fn find_pawns<PI:Idx>(pieces: &PiecesSlice<PI>) -> [PI; 2] {
482 let mut pawns = pieces.iter_enumerated()
483 .filter(|(i,p)| p.info["desc"].as_str().unwrap().ends_with(" pawn"))
486 .collect::<ArrayVec<[_;2]>>()
487 .into_inner().unwrap();
489 pawns.sort_by_key(|&p| -pieces[p].pos.0[0]);
493 let a_pawns = find_pawns(a_pieces.as_slice());
494 let b_pawns = find_pawns(b_pieces.as_slice());
495 // at this point the indices correspond
499 for (&pawn, &xoffset) in izip!(&a_pawns, [10,20].iter()) {
500 alice.api_with_piece_op(&a_pieces[pawn].id, "m", json![
501 (a_pieces[hand].pos + PosC([xoffset, 0]))?.0
505 alice.synchu(&mut a_pieces)?;
506 bob.synchu(&mut b_pieces)?;
509 let b_pos = &b_pieces[p].pos;
510 let got = a_pawns.iter().find(|&&p| &a_pieces[p].pos == b_pos);
511 assert_eq!(got, None);
514 for (xi, &p) in a_pawns.iter().enumerate().take(1) {
515 let xix: Coord = xi.try_into().unwrap();
516 let pos = PosC([ (xix + 1) * 15, 20 ]);
517 a_pieces[p].pos = pos;
518 alice.api_with_piece_op_synch(&mut a_pieces, p, "m", json![pos.0])?;
521 alice.synchu(&mut a_pieces)?;
522 bob.synchu(&mut b_pieces)?;
523 assert_eq!(b_pieces[b_pawns[1]].pos,
524 a_pieces[a_pawns[0]].pos);
527 // to repro a bug, have Alice move the black pawn out again
528 // observe yellow highlight in bob's view and black pawn is in wrong
534 fn tests(mut c: Ctx) {
535 test!(c, "library-load", c.library_load()?);
536 test!(c, "hidden-hand", c.hidden_hand()?);
542 let (opts, _cln, instance, mut su) = setup_core(&[module_path!()])?;
543 let spec = su.ds.game_spec_data()?;
544 let [alice, bob]: [Player; 2] = su.ds.setup_static_users(
546 |sus| Ok(Player { nick: sus.nick, url: sus.url })
548 .try_into().unwrap();
550 let su_rc = Rc::new(RefCell::new(su));
551 tests(Ctx { opts, spec, su_rc, alice, bob })?;