chiark / gitweb /
at-otter hidden: Check that things are where we expect
[otter.git] / apitest / at-otter.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 #![allow(dead_code)]
6 #![allow(unused_variables)]
7
8 use otter_api_tests::*;
9
10 pub use std::cell::{RefCell, RefMut};
11 pub use std::rc::Rc;
12
13 pub use index_vec::Idx;
14
15 type Setup = Rc<RefCell<SetupCore>>;
16
17 struct Ctx {
18   opts: Opts,
19   su_rc: Setup,
20   spec: GameSpec,
21   alice: Player,
22   bob: Player,
23 }
24
25 impl Ctx {
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() }
28
29   fn wanted_tests(&self) -> TrackWantedTestsGuard {
30     TrackWantedTestsGuard(self.su_mut())
31   }
32 }
33 struct TrackWantedTestsGuard<'m>(RefMut<'m, SetupCore>);
34 deref_to_field_mut!{TrackWantedTestsGuard<'_>,
35                     TrackWantedTests,
36                     0.wanted_tests}
37
38 #[derive(Debug)]
39 struct Player {
40   pub nick: &'static str,
41   url: String,
42 }
43
44 struct Session {
45   pub nick: &'static str,
46   pub su_rc: Setup,
47   pub ctoken: RawToken,
48   pub gen: Generation,
49   pub cseq: RawClientSequence,
50   pub dom: scraper::Html,
51   pub updates: mpsc::Receiver<Update>,
52   pub client: reqwest::blocking::Client,
53 }
54
55 mod scraper_ext {
56   use super::*;
57   use scraper::*;
58   use scraper::html::{*, Html};
59
60   #[ext(pub)]
61   impl Html {
62     fn select<'a,'b>(&'a self, selector: &'b Selector) -> Select<'a, 'b> {
63       self.select(selector)
64     }
65
66     #[throws(as Option)]
67     fn element<S>(&self, sel: S) -> ElementRef
68     where S: TryInto<Selector>,
69           <S as TryInto<Selector>>::Error: Debug,
70     {
71       self
72         .select(&sel.try_into().unwrap())
73         .next()?
74     }
75
76     #[throws(as Option)]
77     fn e_attr<S>(&self, sel: S, attr: &str) -> &str
78     where S: TryInto<Selector>,
79           <S as TryInto<Selector>>::Error: Debug,
80     {
81       self
82         .element(sel).unwrap()
83         .value().attr(attr)?
84     }
85   }
86
87   #[throws(AE)]
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);
92     //dbgc!(&&dom);
93     dom
94   }
95
96   #[ext(pub, name=RequestBuilderExt)]
97   impl reqwest::blocking::RequestBuilder {
98     #[throws(AE)]
99     fn send(self) -> reqwest::blocking::Response { self.send()? }
100
101     #[throws(AE)]
102     fn send_parse_html(self) -> Html {
103       let resp = self.send()?;
104       parse_html(resp)?
105     }
106   }
107 }
108
109 use scraper_ext::{HtmlExt, RequestBuilderExt};
110
111 type Update = JsV;
112
113 #[throws(AE)]
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() {
117     let l = l?;
118     if ! l.is_empty() {
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");
125       continue;
126     }
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);
133     } else {
134       let update = &entry["data"];
135       let update = serde_json::from_str(update).unwrap();
136       if out.send(update).is_err() { break }
137     }
138   }
139 }
140
141 impl Ctx {
142   #[throws(AE)]
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();
147     dbgc!(&ptoken);
148
149     let session = client.post(&self.su().ds.subst("@url@/_/session/Portrait")?)
150       .json(&json!({ "ptoken": ptoken }))
151       .send_parse_html()?;
152
153     let ctoken = session.e_attr("#main-body", "data-ctoken").unwrap();
154     dbgc!(&ctoken);
155
156     let gen: Generation = Generation(
157       session.e_attr("#main-body", "data-gen").unwrap()
158         .parse().unwrap()
159     );
160     dbgc!(gen);
161
162     let mut sse = client.get(
163       &self.su().ds
164         .also(&[("ctoken", ctoken),
165                 ("gen",    &gen.to_string())])
166         .subst("@url@/_/updates?ctoken=@ctoken@&gen=@gen@")?
167     ).send()?;
168
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")?;
176           wpipe.flush()?;
177           Ok::<_,io::Error>(())
178         })() {
179           Err(pe) if pe.kind() == ErrorKind::BrokenPipe => { Ok(()) }
180           Err(pe) => Err(AE::from(pe)),
181           Ok(_) => Err(AE::from(re)),
182         }
183         Ok(_n) => Ok(()),
184       }.unwrap();
185       eprintln!("copy_to'd!"); 
186     });
187
188     let (mut csend, crecv) = mpsc::channel();
189     thread::spawn(move ||{
190       updates_parser(rpipe, &mut csend).expect("udpates parser failed")
191     });
192
193     Session {
194       nick: player.nick,
195       client, gen,
196       cseq: 42,
197       ctoken: RawToken(ctoken.to_string()),
198       dom: session,
199       updates: crecv,
200       su_rc: self.su_rc.clone(),
201     }
202   }
203 }
204
205 mod pi {
206   use otter::prelude::define_index_type;
207   define_index_type!{ pub struct PIA = usize; }
208   define_index_type!{ pub struct PIB = usize; }
209 }
210 pub use pi::*;
211
212 type Pieces<PI> = IndexVec<PI, PieceInfo<JsV>>;
213 type PiecesSlice<PI> = IndexSlice<PI,[PieceInfo<JsV>]>;
214
215 #[derive(Debug,Clone)]
216 pub struct PieceInfo<I> {
217   id: String,
218   pos: Pos,
219   info: I,
220 }
221
222 impl Session {
223   #[throws(AE)]
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|{
233           puse
234             .attr(attr).unwrap()
235             .parse().unwrap()
236         })).unwrap();
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 })
241       })
242       .collect();
243     let nick = self.nick;
244     dbgc!(nick, &pieces);
245     pieces
246   }
247
248   #[throws(AE)]
249   fn api_piece_op(&mut self, piece: &str, opname: &str,
250                   op: JsV) {
251     self.cseq += 1;
252     let cseq = self.cseq;
253
254     let su = self.su_rc.borrow_mut();
255     let resp = self.client.post(&su.ds.also(&[("opname",opname)])
256                                 .subst("@url@/_/api/@opname@")?)
257       .json(&json!({
258         "ctoken": self.ctoken,
259         "piece": piece,
260         "gen": self.gen,
261         "cseq": cseq,
262         "op": op,
263       }))
264       .send()?;
265     assert_eq!(resp.status(), 200);
266   }
267
268   #[throws(AE)]
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!({}))?;
274   }
275
276   #[throws(AE)]
277   fn api_with_piece_op_synch<PI:Idx>(
278     &mut self, pieces: &mut Pieces<PI>, i: PI,
279     pathfrag: &str, op: JsV
280   ) {
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!({}))?;
287   }
288
289   #[throws(AE)]
290   fn await_update<
291     R,
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;
298     'overall: loop {
299       let update = self.updates.recv()?;
300       let update = update.as_array().unwrap();
301       let new_gen = Generation(
302         update[0]
303           .as_i64().unwrap()
304           .try_into().unwrap()
305       );
306       self.gen = new_gen;
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();
311         dbgc!(nick, k, &v);
312         if let Some(y) = {
313           if k != "Error" {
314             f(self, new_gen, k, v)
315           } else if let Some(ef) = &mut ef {
316             ef(self, new_gen, v)?
317           } else {
318             panic!("synch error: {:?}", &v);
319           }
320         } { break 'overall y }
321       }
322     }
323   }
324
325   #[throws(AE)]
326   fn synchx<
327     PI: Idx,
328     F: FnMut(&mut Session, Generation, &str, &JsV),
329   > (&mut self,
330      mut pieces: Option<&mut Pieces<PI>>,
331      ef: Option<&mut dyn FnMut(&mut Session, Generation, &JsV)
332                                -> Result<(), AE>>,
333      mut f: F)
334   {
335     let exp = {
336       let mut su = self.su_rc.borrow_mut();
337       su.mgmt_conn.game_synch(TABLE.parse().unwrap())?
338     };
339     let efwrap = ef.map(|ef| {
340       move |s: &mut _, g, v: &_| { ef(s,g,v)?; Ok::<_,AE>(None) }
341     });
342     self.await_update(
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);
347         }
348         f(session,gen,k,v);
349         None
350       },
351       efwrap,
352     )?;
353   }
354
355   #[throws(AE)]
356   fn synchu<PI:Idx>(&mut self, pieces: &mut Pieces<PI>) {
357     self.synchx(Some(pieces), None, |_session, _gen, _k, _v| ())?;
358   }
359
360   #[throws(AE)]
361   fn synch(&mut self) {
362     self.synchx::<PIA,_>(None, None, |_session, _gen, _k, _v|())?;
363   }
364 }
365
366 pub fn update_update_pieces<PI:Idx>(
367   nick: &str,
368   pieces: &mut Pieces<PI>,
369   k: &str, v: &JsV
370 ) {
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();
377
378   fn coord(j: &JsV) -> Pos {
379     PosC(
380       j.as_array().unwrap().iter()
381         .map(|n| n.as_i64().unwrap().try_into().unwrap())
382         .collect::<ArrayVec<_>>().into_inner().unwrap()
383     )
384   }
385
386   match op.as_str() {
387     "Move" => {
388       p.pos = coord(d);
389     },
390     "Modify" | "ModifyQuiet" => {
391       let d = d.as_object().unwrap();
392       p.pos = coord(&d["pos"]);
393       for (k,v) in d {
394         p.info
395           .as_object_mut().unwrap()
396           .insert(k.to_string(), v.clone());
397       }
398     },
399     _ => {
400       panic!("unknown op {:?} {:?}", &op, &d);
401     },
402   };
403   dbgc!(nick, k,v,p);
404 }
405
406 impl Ctx {
407   #[throws(AE)]
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()))
412       .collect();
413     self.su().ds.otter(&args)?;
414   }
415
416   #[throws(AE)]
417   fn library_load(&mut self) {
418     prepare_game(&self.su().ds, TABLE)?;
419
420     let command = self.su().ds.ss(
421       "library-add @table@ wikimedia chess-blue-?"
422     )?;
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));
427
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();
434     dbgc!(&llm);
435
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])?;
438     }
439
440     session.synch()?;
441
442     self.otter(&command)
443       .expect("library-add failed after place!");
444
445     let mut added = vec![];
446     session.synchx::<PIA,_>(None, None,
447       |session, gen, k, v| if_chain! {
448         if k == "Piece";
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); }
453       }
454     )?;
455     dbgc!(&added);
456     assert_eq!(added.len(), 6);
457   }
458
459   #[throws(AE)]
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"])?;
465
466     let mut a_pieces = alice.pieces::<PIA>()?;
467     let mut b_pieces = alice.pieces::<PIB>()?;
468
469     let [hand] = a_pieces.iter_enumerated()
470       .filter(|(i,p)| p.info["desc"] == otter::hand::UNCLAIMED_DESC)
471       .map(|(i,_)| i)
472       .collect::<ArrayVec<[_;1]>>()
473       .into_inner().unwrap();
474     dbgc!(&hand);
475
476     alice.api_with_piece_op_synch(&mut a_pieces, hand, "k", json!({
477       "opname": "claim",
478       "wrc": "Unpredictable",
479     }))?;
480
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"))
484         .map(|(i,_)| i)
485         .take(2)
486         .collect::<ArrayVec<[_;2]>>()
487         .into_inner().unwrap();
488
489       pawns.sort_by_key(|&p| -pieces[p].pos.0[0]);
490       dbgc!(pawns)
491     }
492
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
496
497     bob.synch()?;
498
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
502       ])?;
503     }
504
505     alice.synchu(&mut a_pieces)?;
506     bob.synchu(&mut b_pieces)?;
507
508     for &p in &b_pawns {
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);
512     }
513
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])?;
519     }
520
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);
525
526
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
529     // place
530   }
531 }
532
533 #[throws(AE)]
534 fn tests(mut c: Ctx) {
535   test!(c, "library-load", c.library_load()?);
536   test!(c, "hidden-hand", c.hidden_hand()?);
537 }
538
539 #[throws(AE)]
540 fn main() {
541   {
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(
545       default(),
546       |sus| Ok(Player { nick: sus.nick, url: sus.url })
547     )?
548       .try_into().unwrap();
549     
550     let su_rc = Rc::new(RefCell::new(su));
551     tests(Ctx { opts, spec, su_rc, alice, bob })?;
552   }
553   info!("ok");
554 }