chiark / gitweb /
ensure_eq, fix
[otter.git] / wdriver.rs
1 // Copyright 2020 Ian Jackson
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 #![feature(unboxed_closures)]
6 #![feature(fn_traits)]
7
8 pub use anyhow::{anyhow, ensure, Context};
9 pub use boolinator::Boolinator;
10 pub use fehler::{throw, throws};
11 pub use if_chain::if_chain;
12 pub use log::{debug, error, info, trace, warn};
13 pub use log::{log, log_enabled};
14 pub use nix::unistd::LinkatFlags;
15 pub use num_traits::NumCast;
16 pub use num_derive::FromPrimitive;
17 pub use parking_lot::{Mutex, MutexGuard};
18 pub use regex::{Captures, Regex};
19 pub use serde::{Serialize, Deserialize};
20 pub use structopt::StructOpt;
21 pub use strum::{EnumIter, EnumProperty, IntoEnumIterator, IntoStaticStr};
22 pub use thirtyfour_sync as t4;
23 pub use void::Void;
24
25 pub use t4::WebDriverCommands;
26 pub use t4::By;
27
28 pub use std::env;
29 pub use std::fmt::{self, Debug};
30 pub use std::collections::hash_map::HashMap;
31 pub use std::convert::TryInto;
32 pub use std::fs;
33 pub use std::io::{self, BufRead, BufReader, ErrorKind, Write};
34 pub use std::iter;
35 pub use std::mem;
36 pub use std::net::TcpStream;
37 pub use std::ops::Deref;
38 pub use std::os::unix::process::CommandExt;
39 pub use std::os::unix::fs::DirBuilderExt;
40 pub use std::os::linux::fs::MetadataExt; // todo why linux for st_mode??
41 pub use std::path;
42 pub use std::process::{Command, Stdio};
43 pub use std::thread::{self, sleep};
44 pub use std::time;
45
46 pub use otter::ensure_eq;
47 pub use otter::commands::{MgmtCommand, MgmtResponse};
48 pub use otter::commands::{MgmtGameInstruction, MgmtGameResponse};
49 pub use otter::commands::{MgmtGameUpdateMode};
50 pub use otter::gamestate::{self, Generation};
51 pub use otter::global::InstanceName;
52 pub use otter::mgmtchannel::MgmtChannel;
53 pub use otter::spec::{Coord, Pos, PosC};
54
55 pub type T4d = t4::WebDriver;
56 pub type WDE = t4::error::WebDriverError;
57
58 pub const MS : time::Duration = time::Duration::from_millis(1);
59 pub type AE = anyhow::Error;
60
61 pub const URL : &str = "http://localhost:8000";
62
63 pub fn default<T:Default>() -> T { Default::default() }
64
65 use t4::Capabilities;
66 use otter::config::DAEMON_STARTUP_REPORT;
67
68 const TABLE : &str = "server::dummy";
69 const CONFIG : &str = "server-config.toml";
70
71 #[derive(Copy,Clone,Debug,Eq,PartialEq,Ord,PartialOrd)]
72 #[derive(FromPrimitive,EnumIter,IntoStaticStr,EnumProperty)]
73 #[strum(serialize_all = "snake_case")]
74 pub enum StaticUser {
75   #[strum(props(Token="kmqAKPwK4TfReFjMor8MJhdRPBcwIBpe"))] Alice,
76   #[strum(props(Token="ccg9kzoTh758QrVE1xMY7BQWB36dNJTx"))] Bob,
77 }
78
79 pub trait AlwaysContext<T,E> {
80   fn always_context(self, msg: &'static str) -> anyhow::Result<T>;
81 }
82
83 impl<T,E> AlwaysContext<T,E> for Result<T,E>
84 where Self: anyhow::Context<T,E>,
85 {
86   fn always_context(self, msg: &'static str) -> anyhow::Result<T> {
87     let x = self.context(msg);
88     if x.is_ok() { info!("completed {}.", msg) };
89     x
90   }
91 }
92
93 pub trait JustWarn<T> {
94   fn just_warn(self) -> Option<T>;
95 }
96
97 impl<T> JustWarn<T> for Result<T,AE> {
98   fn just_warn(self) -> Option<T> {
99     match self {
100       Ok(x) => Some(x),
101       Err(e) => {
102         warn!("{:#}", e);
103         None
104       },
105     }
106   }
107 }
108
109 #[derive(Debug,Clone)]
110 #[derive(StructOpt)]
111 struct Opts {
112   #[structopt(long="--no-bwrap")]
113   no_bwrap: bool,
114
115   #[structopt(long="--tmp-dir", default_value="tmp")]
116   tmp_dir: String,
117
118   #[structopt(long="--pause", default_value="0ms")]
119   pause: humantime::Duration,
120
121   #[structopt(long="--geckodriver-args", default_value="")]
122   geckodriver_args: String,
123 }
124
125 #[derive(Debug)]
126 pub struct FinalInfoCollection;
127
128 type ScreenShotCount = u32;
129 type WindowState = Option<String>;
130
131 #[derive(Debug)]
132 pub struct Setup {
133   pub ds: DirSubst,
134   pub mgmt_conn: MgmtChannel,
135   driver: T4d,
136   current_window: WindowState,
137   screenshot_count: ScreenShotCount,
138   final_hook: FinalInfoCollection,
139   windows_squirreled: Vec<String>, // see Drop impl
140 }
141
142 #[derive(Clone,Debug)]
143 pub struct DirSubst {
144   pub tmp: String,
145   pub abstmp: String,
146   pub start_dir: String,
147   pub src: String,
148 }
149
150 pub struct Instance(InstanceName);
151
152 #[derive(Clone,Debug)]
153 pub struct Subst(HashMap<String,String>);
154
155 #[derive(Clone,Debug)]
156 pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
157
158 impl<'i,
159      T: AsRef<str> + 'i,
160      U: AsRef<str> + 'i,
161      L: IntoIterator<Item=&'i (T, U)>>
162   From<L> for Subst
163 {
164   fn from(l: L) -> Subst {
165     let map = l.into_iter()
166       .map(|(k,v)| (k.as_ref().to_owned(), v.as_ref().to_owned())).collect();
167     Subst(map)
168   }
169 }
170
171 pub trait Substitutor {
172   fn get(&self, kw: &str) -> Option<String>;
173
174   fn also<L: Into<Subst>>(&self, xl: L) -> ExtendedSubst<Self, Subst>
175   where Self: Clone + Sized {
176     ExtendedSubst(self.clone(), xl.into())
177   }
178
179   #[throws(AE)]
180   fn subst<S: AsRef<str>>(&self, s: S) -> String 
181   where Self: Sized {
182     #[throws(AE)]
183     fn inner(self_: &dyn Substitutor, s: &dyn AsRef<str>) -> String {
184       let s = s.as_ref();
185       let re = Regex::new(r"@(\w+)@").expect("bad re!");
186       let mut errs = vec![];
187       let out = re.replace_all(s, |caps: &regex::Captures| {
188         let kw = caps.get(1).expect("$1 missing!").as_str();
189         if kw == "" { return "".to_owned() }
190         let v = self_.get(kw);
191         v.unwrap_or_else(||{
192           errs.push(kw.to_owned());
193           "".to_owned()
194         })
195       });
196       if ! errs.is_empty() {
197         throw!(anyhow!("bad substitution(s) {:?} in {:?}",
198                        &errs, s));
199       }
200       out.into()
201     }
202     inner(self, &s)?
203   }
204
205   #[throws(AE)]
206   fn ss(&self, s: &str) -> Vec<String> 
207   where Self: Sized {
208     self.subst(s)?
209       .trim()
210       .split(' ')
211       .filter(|s| !s.is_empty())
212       .map(str::to_string)
213       .collect()
214   }
215 }
216
217 impl Substitutor for Subst {
218   fn get(&self, kw: &str) -> Option<String> {
219     self.0.get(kw).map(String::clone)
220   }
221 }
222
223 impl<B:Substitutor, X:Substitutor> Substitutor for ExtendedSubst<B, X> {
224   fn get(&self, kw: &str) -> Option<String> {
225     self.1.get(kw).or_else(|| self.0.get(kw))
226   }
227 }
228
229 impl Substitutor for DirSubst {
230   fn get(&self, kw: &str) -> Option<String> {
231     Some(match kw {
232       "url"    => URL.to_owned(),
233       "src"    => self.src.clone(),
234       "build"  => self.start_dir.clone(),
235       "abstmp" => self.abstmp.clone(),
236       "target" => format!("{}/target", &self.start_dir),
237       "specs"  => format!("{}/specs" , &self.src      ),
238       _ => return None,
239     })
240   }
241 }
242
243 mod cleanup_notify {
244   use anyhow::Context;
245   use fehler::{throw, throws};
246   use libc::_exit;
247   use nix::errno::Errno::*;
248   use nix::{unistd::*, fcntl::OFlag};
249   use nix::sys::signal::*;
250   use nix::Error::Sys;
251   use void::Void;
252   use std::io;
253   use std::os::unix::io::RawFd;
254   use std::panic::catch_unwind;
255   use std::process::Command;
256   type AE = anyhow::Error;
257
258   pub struct Handle(RawFd);
259
260   #[throws(io::Error)]
261   fn mkpipe() -> (RawFd,RawFd) {
262     pipe2(OFlag::O_CLOEXEC).map_err(nix2io)?
263   }
264
265   #[throws(io::Error)]
266   fn read_await(fd: RawFd) {
267     loop {
268       let mut buf = [0u8; 1];
269       match nix::unistd::read(fd, &mut buf) {
270         Ok(0) => break,
271         Ok(_) => throw!(io::Error::from_raw_os_error(libc::EINVAL)),
272         Err(Sys(EINTR)) => continue,
273         _ => throw!(io::Error::last_os_error()),
274       }
275     }
276   }
277
278   fn nix2io(_n: nix::Error) -> io::Error {
279     io::Error::last_os_error()
280   }
281
282   impl Handle {
283     #[throws(AE)]
284     pub fn new() -> Self {
285       let (reading_end, _writing_end) = mkpipe()
286         .context("create cleanup notify pipe")?;
287       // we leak the writing end, keeping it open only in this process
288       Handle(reading_end)
289     }
290
291     #[throws(AE)]
292     pub fn arm_hook(&self, cmd: &mut Command) { unsafe {
293       use std::os::unix::process::CommandExt;
294
295       let notify_writing_end = self.0;
296       let all_signals = nix::sys::signal::SigSet::all();
297
298       cmd.pre_exec(move || -> Result<(), io::Error> {
299         let semidaemon = nix::unistd::getpid();
300         let (reading_end, writing_end) = mkpipe()?;
301
302         match fork().map_err(nix2io)? {
303           ForkResult::Child => {
304             let _ = catch_unwind(move || -> Void {
305               let _ = sigprocmask(
306                 SigmaskHow::SIG_BLOCK,
307                 Some(&all_signals),
308                 None
309               );
310
311               let _ = close(writing_end);
312               let _ = nix::unistd::dup2(2, 1);
313
314               for fd in 2.. {
315                 if fd == notify_writing_end { continue }
316                 let r = close(fd);
317                 if fd >= writing_end && matches!(r, Err(Sys(EBADF))) {
318                   break;
319                 }                  
320               }
321               let _ = read_await(notify_writing_end);
322               let _ = kill(semidaemon, SIGTERM);
323               _exit(0);
324             });
325             let _ = raise(SIGABRT);
326             _exit(127);
327           },
328           ForkResult::Parent{..} => {
329             // parent
330             close(writing_end).map_err(nix2io)?;
331             read_await(reading_end)?;
332           },
333         };
334
335         Ok(())
336       });
337     } }
338   }
339 }
340
341 #[throws(AE)]
342 fn reinvoke_via_bwrap(_opts: &Opts, current_exe: &str) -> Void {
343   debug!("running bwrap");
344   
345   let mut bcmd = Command::new("bwrap");
346   bcmd
347     .args("--unshare-net \
348            --dev-bind / / \
349            --tmpfs /tmp \
350            --die-with-parent".split(" "))
351     .arg(current_exe)
352     .arg("--no-bwrap")
353     .args(env::args_os().skip(1));
354
355   std::io::stdout().flush().context("flush stdout")?;
356   let e : AE = bcmd.exec().into();
357   throw!(e.context("exec bwrap"));
358 }
359
360 #[throws(AE)]
361 fn prepare_tmpdir(opts: &Opts, current_exe: &str) -> DirSubst {
362   #[throws(AE)]
363   fn getcwd() -> String {
364     env::current_dir()
365       .context("getcwd")?
366       .to_str()
367       .ok_or_else(|| anyhow!("path is not UTF-8"))?
368       .to_owned()
369   }
370
371   let start_dir = getcwd()
372     .context("canonicalise our invocation directory (getcwd)")?;
373
374   (||{
375     match fs::metadata(&opts.tmp_dir) {
376       Ok(m) => {
377         if !m.is_dir() {
378           throw!(anyhow!("existing object is not a directory"));
379         }
380         if (m.st_mode() & 0o01002) != 0 {
381           throw!(anyhow!(
382             "existing directory mode {:#o} is sticky or world-writeable. \
383              We use predictable pathnames so that would be a tmp race",
384             m.st_mode()
385           ));
386         }
387       }
388       Err(e) if e.kind() == ErrorKind::NotFound => {
389         fs::create_dir(&opts.tmp_dir)
390           .context("create")?;
391       }
392       Err(e) => {
393         let e : AE = e.into();
394         throw!(e.context("stat existing directory"))
395       }
396     }
397
398     env::set_current_dir(&opts.tmp_dir)
399       .context("chdir into it")?;
400
401     Ok::<_,AE>(())
402   })()
403     .with_context(|| opts.tmp_dir.to_owned())
404     .context("prepare/create tmp-dir")?;
405
406   let leaf = current_exe.rsplitn(2, '/').next().unwrap();
407   let our_tmpdir = format!("{}/{}", &opts.tmp_dir, &leaf);
408   (||{
409     match fs::remove_dir_all(&leaf) {
410       Ok(()) => {},
411       Err(e) if e.kind() == ErrorKind::NotFound => {},
412       Err(e) => throw!(AE::from(e).context("remove previous directory")),
413     };
414
415     fs::DirBuilder::new().create(&leaf)
416       .context("create fresh subdirectory")?;
417
418     env::set_current_dir(&leaf)
419       .context("chdir into it")?;
420
421     Ok::<_,AE>(())
422   })()
423     .with_context(|| our_tmpdir.to_owned())
424     .context("prepare/create our tmp subdir")?;
425
426   let abstmp =
427     getcwd().context("canonicalise our tmp subdir (getcwd)")?;
428
429   env::set_var("HOME", &abstmp);
430   env::set_var("TMPDIR", &abstmp);
431   for v in "http_proxy https_proxy XAUTHORITY CDPATH \
432             SSH_AGENT_PID SSH_AUTH_SOCK WINDOWID WWW_HOME".split(' ')
433   {
434     env::remove_var(v);
435   }
436
437   let manifest_var = "CARGO_MANIFEST_DIR";
438   let src : String = (|| Ok::<_,AE>(match env::var(manifest_var) {
439     Ok(dir) => dir.into(),
440     Err(env::VarError::NotPresent) => start_dir.clone(),
441     e@ Err(_) => throw!(e.context(manifest_var).err().unwrap()),
442   }))()
443     .context("find source code")?;
444
445   DirSubst {
446     tmp: our_tmpdir,
447     abstmp,
448     src,
449     start_dir,
450   }
451 }
452
453 #[throws(AE)]
454 fn fork_something_which_prints(mut cmd: Command,
455                                cln: &cleanup_notify::Handle,
456                                what: &str)
457                                -> String
458 {
459   (||{
460     cmd.stdout(Stdio::piped());
461     cln.arm_hook(&mut cmd)?;
462     let mut child = cmd.spawn().context("spawn")?;
463     let mut report = BufReader::new(child.stdout.take().unwrap())
464       .lines().fuse();
465
466     let l = report.next();
467
468     let s = child.try_wait().context("check on spawned child")?;
469     if let Some(e) = s {
470       throw!(anyhow!("failed to start: wait status = {}", &e));
471     }
472
473     let l = match l {
474       Some(Ok(l)) => l,
475       None => throw!(anyhow!("EOF (but it's still running?")),
476       Some(Err(e)) => throw!(AE::from(e).context("failed to read")),
477     };
478
479     let what = what.to_owned();
480     thread::spawn(move|| (||{
481       for l in report {
482         let l : Result<String, io::Error> = l;
483         let l = l.context("reading further output")?;
484         const MAXLEN : usize = 300;
485         if l.len() <= MAXLEN {
486           println!("{} {}", what, l);
487         } else {
488           println!("{} {}...", what, &l[..MAXLEN-3]);
489         }
490       }
491       Ok::<_,AE>(())
492     })().context(what).just_warn()
493     );
494
495     Ok::<_,AE>(l)
496   })().with_context(|| what.to_owned())?
497 }
498
499 #[throws(AE)]
500 fn prepare_xserver(cln: &cleanup_notify::Handle, ds: &DirSubst) {
501   const DISPLAY : u16 = 12;
502
503   let mut xcmd = Command::new("Xvfb");
504   xcmd
505     .args("-nolisten unix \
506            -nolisten local \
507            -listen inet \
508            -listen inet6 \
509            -terminate \
510            -retro \
511            -displayfd 1".split(' '))
512     .args(&["-fbdir", &ds.abstmp])
513     .arg(format!(":{}", DISPLAY));
514
515   let l = fork_something_which_prints(xcmd, cln, "Xvfb")?;
516
517   if l != DISPLAY.to_string() {
518     throw!(anyhow!(
519       "Xfvb said {:?}, expected {:?}",
520       l, DISPLAY
521     ));
522   }
523
524   let display = format!("[::1]:{}", DISPLAY);
525   env::set_var("DISPLAY", &display);
526
527   // Doesn't do IPv6 ??
528   let v4display = format!("127.0.0.1:{}", DISPLAY);
529   let (xconn, _) = x11rb::connect(Some(&v4display))
530     .context("make keepalive connection to X server")?;
531
532   // Sadly, if we die between spawning Xfvb, and here, we will
533   // permanently leak the whole Xfvb process (and the network
534   // namespace).  There doesn't seem to a way to avoid this without
535   // editing Xvfb,
536
537   Box::leak(Box::new(xconn));
538 }
539
540 #[throws(AE)]
541 fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
542                       -> MgmtChannel {
543   let subst = ds.also(&[
544     ("command_socket", "command.socket"),
545   ]);
546   let config = subst.subst(r##"
547 change_directory = "@abstmp@"
548 base_dir = "@build@"
549 public_url = "@url@"
550
551 save_dir = "."
552 command_socket = "@command_socket@"
553 template_dir = "@src@/templates"
554 nwtemplate_dir = "@src@/nwtemplates"
555 bundled_sources = "@target@/bundled-sources"
556 wasm_dir = "@target@/packed-wasm"
557 shapelibs = [ "@src@/library/*.toml" ]
558
559 debug_js_inject_file = "@src@/templates/log-save.js"
560 check_bundled_sources = false # For testing only! see LICENCE!
561
562 [log]
563 global_level = 'debug'
564
565 [log.modules]
566 rocket = 'error'
567 _ = "error" # rocket
568 # ^ comment these two out to see Tera errors, *sigh*
569
570 'hyper::server' = 'info'
571 "game::debugreader" = 'info'
572 "game::updates" = 'trace'
573 "##)?;
574
575   fs::write(CONFIG, &config)
576     .context(CONFIG).context("create server config")?;
577
578   let server_exe = ds.subst("@target@/debug/daemon-otter")?;
579   let mut cmd = Command::new(&server_exe);
580   cmd
581     .arg("--report-startup")
582     .arg(CONFIG);
583
584   (||{
585     let l = fork_something_which_prints(cmd, cln, &server_exe)?;
586     if l != DAEMON_STARTUP_REPORT {
587       throw!(anyhow!("otter-daemon startup report {:?}, expected {:?}",
588                      &l, DAEMON_STARTUP_REPORT));
589     }
590     Ok::<_,AE>(())
591   })()
592     .context("game server")?;
593
594   let mut mgmt_conn = MgmtChannel::connect(
595     &subst.subst("@command_socket@")?
596   )?;
597
598   mgmt_conn.cmd(&MgmtCommand::SetSuperuser(true))?;
599   mgmt_conn.cmd(&MgmtCommand::SelectAccount("server:".parse()?))?;  
600
601   mgmt_conn
602 }
603
604 impl DirSubst {
605   #[throws(AE)]
606   pub fn otter<S:AsRef<str>>(&self, xargs: &[S]) {
607     let ds = self;
608     let exe = ds.subst("@target@/debug/otter")?;
609     let mut args : Vec<&str> = vec![];
610     args.extend(&["--config", CONFIG]);
611     args.extend(xargs.iter().map(AsRef::as_ref));
612     let dbg = format!("running {} {:?}", &exe, &args);
613     debug!("{}", &dbg);
614     (||{
615       let mut cmd = Command::new(&exe);
616       cmd.args(&args);
617       let st = cmd
618         .spawn().context("spawn")?
619         .wait().context("wait")?;
620       if !st.success() {
621         throw!(anyhow!("wait status {}", &st));
622       }
623       Ok::<_,AE>(())
624     })()
625       .context(dbg)
626       .context("run otter client")?;
627   }
628 }
629
630 #[throws(AE)]
631 pub fn prepare_game(ds: &DirSubst, table: &str) -> InstanceName {
632   let subst = ds.also(&[("table", &table)]);
633   ds.otter(&subst.ss(
634     "--account server:                                  \
635      reset                                              \
636      --reset-table @specs@/test.table.toml              \
637                    @table@ @specs@/demo.game.toml \
638     ")?).context("reset table")?;
639
640   let instance : InstanceName = table.parse()
641     .with_context(|| table.to_owned())
642     .context("parse table name")?;
643
644   instance
645 }
646
647 #[throws(AE)]
648 fn prepare_geckodriver(opts: &Opts, cln: &cleanup_notify::Handle) {
649   const EXPECTED : &str = "Listening on 127.0.0.1:4444";
650   let mut cmd = Command::new("geckodriver");
651   if opts.geckodriver_args != "" {
652     cmd.args(opts.geckodriver_args.split(' '));
653   }
654   let l = fork_something_which_prints(cmd, cln, "geckodriver")?;
655   let fields : Vec<_> = l.split('\t').skip(2).take(2).collect();
656   let expected = ["INFO", EXPECTED];
657   if fields != expected {
658     throw!(anyhow!("geckodriver did not report as expected \
659                     - got {:?}, expected {:?}",
660                    fields, &expected));
661   }
662 }
663
664 #[throws(AE)]
665 fn prepare_thirtyfour() -> (T4d, ScreenShotCount, Vec<String>) {
666   let mut count = 0;
667   let mut caps = t4::DesiredCapabilities::firefox();
668   let prefs : HashMap<_,_> = [
669     ("devtools.console.stdout.content", true),
670   ].iter().cloned().collect();
671   caps.add("prefs", prefs)?;
672   caps.add("stdio", "inherit")?;
673   let mut driver = t4::WebDriver::new("http://localhost:4444", &caps)
674     .context("create 34 WebDriver")?;
675
676   const FRONT : &str = "front";
677   let window_names = vec![FRONT.into()];
678   driver.set_window_name(FRONT).context("set initial window name")?;
679   screenshot(&mut driver, &mut count, "startup")?;
680   driver.get(URL).context("navigate to front page")?;
681   screenshot(&mut driver, &mut count, "front")?;
682
683   fetch_log(&driver, "front")?;
684   
685   let t = Some(5_000 * MS);
686   driver.set_timeouts(t4::TimeoutConfiguration::new(t,t,t))
687     .context("set webdriver timeouts")?;
688
689   (driver, count, window_names)
690 }
691
692 /// current window must be `name`
693 #[throws(AE)]
694 fn fetch_log(driver: &T4d, name: &str) {
695   (||{
696     let got = driver.execute_script(r#"
697       var returning = window.console.saved;
698       window.console.saved = [];
699       return returning;
700     "#).context("get log")?;
701
702     for ent in got.value().as_array()
703       .ok_or(anyhow!("saved isn't an array?"))?
704     {
705       #[derive(Deserialize)]
706       struct LogEnt(String, Vec<serde_json::Value>);
707       impl fmt::Display for LogEnt {
708         #[throws(fmt::Error)]
709         fn fmt(&self, f: &mut fmt::Formatter) {
710           write!(f, "{}:", self.0)?;
711           for a in &self.1 { write!(f, " {}", a)?; }
712         }
713       }
714
715       let ent: LogEnt = serde_json::from_value(ent.clone())
716         .context("parse log entry")?;
717
718       debug!("JS {} {}", name, &ent);
719     }
720     Ok::<_,AE>(())
721   })()
722     .with_context(|| name.to_owned())
723     .context("fetch JS log messages")?;
724 }
725
726 #[derive(Debug)]
727 pub struct Window {
728   name: String,
729   instance: InstanceName,
730 }
731
732 impl Window {
733   pub fn table(&self) -> String { self.instance.to_string() }
734 }
735
736 type ScreenCTM = ndarray::Array2::<f64>;
737
738 pub struct WindowGuard<'g> {
739   su: &'g mut Setup,
740   w: &'g Window,
741   matrix: once_cell::sync::OnceCell<ScreenCTM>,
742 }
743
744 impl Debug for WindowGuard<'_> {
745   #[throws(fmt::Error)]
746   fn fmt(&self, f: &mut fmt::Formatter) {
747     f.debug_struct("WindowGuard")
748       .field("w.name", &self.w.name)
749       .field("w.instance", &self.w.instance.to_string())
750       .finish()?
751   }
752 }
753
754 impl<'g> WindowGuard<'g> {
755   #[throws(AE)]
756   pub fn find_piece(&'g self, pieceid: &'g str) -> PieceElement<'g> {
757     let id = format!("use{}", pieceid);
758     let elem = self.su.driver.find_element(By::Id(&id))?;
759     PieceElement {
760       pieceid, elem,
761       w: self,
762     }
763   }
764 }
765
766 pub type WebCoord = i32;
767 pub type WebPos = (WebCoord, WebCoord);
768
769 pub struct PieceElement<'g> {
770   pieceid: &'g str,
771   w: &'g WindowGuard<'g>,
772   elem: t4::WebElement<'g>,
773 }
774
775 impl<'g> Deref for PieceElement<'g> {
776   type Target = t4::WebElement<'g>;
777   fn deref<'i>(&'i self) -> &'i t4::WebElement<'g> { &self.elem }
778 }
779
780 impl<'g> PieceElement<'g> {
781   #[throws(AE)]
782   pub fn posg(&self) -> Pos {
783     (||{
784       let a = |a| Ok::<_,AE>(
785         self.get_attribute(a)?.ok_or(anyhow!("{}", a))?.parse()?
786       );
787       let x = a("x")?;
788       let y = a("y")?;
789       Ok::<_,AE>(PosC([x,y]))
790     })()
791       .with_context(|| self.pieceid.to_owned())
792       .context("read position of piece out of x,y attributes")?
793   }
794
795   #[throws(AE)]
796   pub fn posw(&self) -> WebPos {
797     let posg = self.posg()?;
798
799     let mat = self.w.matrix.get_or_try_init(||{
800       let ary = self.w.su.driver.execute_script(r#"
801         let m = space.getScreenCTM();
802         return [m.a, m.b, m.c, m.d, m.e, m.f];
803       "#)?;
804       let ary = ary.value();
805       dbg!(ary);
806
807       let mat = (||{
808         let ary = ary.as_array().ok_or_else(|| anyhow!("not array"))?;
809         let mut mat : ScreenCTM = ndarray::Array2::zeros((3,3));
810         for got in itertools::Itertools::zip_longest(
811           [11, 12, 21, 22, 41, 42].iter(),
812           // ^ from https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix
813           ary.iter(),
814         ) {
815           let (mij, v) = got.both().ok_or_else(|| anyhow!("wrong length"))?;
816           let adj = |v| (if v == 4 { 3 } else { v }) - 1;
817           let i = adj(mij / 10);
818           let j = adj(mij % 10);
819           mat[(j,i)] = v.as_f64().ok_or_else(|| anyhow!("entry not f64"))?;
820         }
821         Ok::<_,AE>(mat)
822       })()
823         .with_context(|| format!("getScreenCGM script gave {:?}", &ary))?;
824
825       dbg!(&mat);
826       Ok::<_,AE>(mat)
827     })?;
828     (||{
829       let vec : ndarray::Array1<f64> =
830         posg.0.iter()
831         .cloned()
832         .map(|v| v as f64)
833         .chain(iter::once(1.))
834         .collect();
835       dbg!(&vec);
836
837       let vec = mat.dot(&vec);
838       let mut coords = vec.iter().map(
839         |v| NumCast::from(v.round()).ok_or_else(
840           || anyhow!("coordinate {} out of range in {:?}", v, &vec))
841       );
842       let mut coord = || coords.next().unwrap();
843       Ok::<WebPos,AE>((
844         coord()?,
845         coord()?,
846       ))
847     })()
848       .with_context(|| self.pieceid.to_owned())
849       .context("find piece position")?
850   }
851 }
852
853 #[throws(AE)]
854 fn check_window_name_sanity(name: &str) -> &str {
855   let e = || anyhow!("bad window name {:?}", &name);
856
857   name.chars().nth(0).ok_or(e())?
858     .is_ascii_alphanumeric().ok_or(e())?;
859
860   name.chars().all(
861     |c| c.is_ascii_alphanumeric() || c == '-' || c == '_'
862   ).ok_or(e())?;
863
864   name
865 }
866
867 impl Setup {
868   #[throws(AE)]
869   pub fn new_window<'s>(&'s mut self, instance: &Instance, name: &str)
870                         -> Window {
871     let name = check_window_name_sanity(name)?;
872     let window = (||{
873
874       self.current_window = None; // we might change the current window
875
876       match self.driver.switch_to().window_name(name) {
877         Ok(()) => throw!(anyhow!("window already exists")),
878         Err(WDE::NoSuchWindow(_)) |
879         Err(WDE::NotFound(..)) => (),
880         e@ Err(_) => {
881           eprintln!("wot {:?}", &e);
882           throw!(e
883                             .context("check for pre-existing window")
884                             .err().unwrap())
885         },
886       };
887
888       self.driver.execute_script(&format!(
889         r#"window.open('', target='{}');"#,
890         name,
891       ))
892         .context("execute script to create window")?;
893
894       Ok::<_,AE>(Window {
895         name: name.to_owned(),
896         instance: instance.0.clone(),
897       })
898     })()
899       .with_context(|| name.to_owned())
900       .context("create window")?;
901
902     self.windows_squirreled.push(name.to_owned());
903     window
904   }
905 }
906
907 impl Setup {
908   #[throws(AE)]
909   pub fn w<'s>(&'s mut self, w: &'s Window) -> WindowGuard<'s> {
910     if self.current_window.as_ref() != Some(&w.name) {
911       self.driver.switch_to().window_name(&w.name)
912         .with_context(|| w.name.to_owned())
913         .context("switch to window")?;
914       self.current_window = Some(w.name.clone());
915     }
916     WindowGuard {
917       w,
918       su: self,
919       matrix: default(),
920     }
921   }
922 }
923
924 impl<'g> Deref for WindowGuard<'g> {
925   type Target = T4d;
926   fn deref(&self) -> &T4d { &self.su.driver }
927 }
928
929 impl<'g> Drop for WindowGuard<'g> {
930   fn drop(&mut self) {
931     fetch_log(&self.su.driver, &self.w.name)
932       .just_warn();
933   }
934 }
935
936 pub trait Screenshottable {
937   fn screenshot(&mut self, slug: &str) -> Result<(),AE>;
938 }
939
940 impl<'g> Screenshottable for WindowGuard<'g> {
941   #[throws(AE)]
942   fn screenshot(&mut self, slug: &str) {
943     screenshot(&self.su.driver, &mut self.su.screenshot_count,
944                &format!("{}-{}", &self.w.name, slug))?
945   }
946 }
947
948 #[throws(AE)]
949 fn screenshot(driver: &T4d, count: &mut ScreenShotCount, slug: &str) {
950   let path = format!("{:03}{}.png", count, slug);
951   *count += 1;
952   driver.screenshot(&path::PathBuf::from(&path))
953     .with_context(|| path.clone())
954     .context("take screenshot")?;
955   debug!("screenshot {}", &path);
956 }
957
958 impl<'g> WindowGuard<'g> {
959   #[throws(AE)]
960   pub fn synch(&mut self) {
961     let cmd = MgmtCommand::AlterGame {
962       game: self.w.instance.clone(),
963       how: MgmtGameUpdateMode::Online,
964       insns: vec![ MgmtGameInstruction::Synch ],
965     };
966     let gen = if_chain!{
967       let resp = self.su.mgmt_conn.cmd(&cmd)?;
968       if let MgmtResponse::AlterGame {
969         error: None,
970         ref responses
971       } = resp;
972       if let [MgmtGameResponse::Synch(gen)] = responses[..];
973       then { gen }
974       else { throw!(anyhow!("unexpected resp to synch {:?}", resp)) }
975     };
976     trace!("{:?} gen={} ...", self, gen);
977     (|| {
978       loop {
979         let tgen = self.su.driver.execute_async_script(
980           &Subst::from(&[
981             ("wanted", &gen.to_string())
982           ]).subst(r#"
983             var done = arguments[0];
984             if (gen >= @wanted@) { done(gen); return; }
985             window.gen_update_hook = function() {
986               window.gen_update_hook = function() { };
987               done(gen);
988             };
989           "#)?
990         )
991           .context("run async script")?
992           .value().as_u64().ok_or(anyhow!("script return is not u64"))?;
993         let tgen = Generation(tgen);
994         trace!("{:?} gen={} tgen={}", self, gen, tgen);
995         if tgen >= gen { break; }
996       }
997       Ok::<(),AE>(())
998     })()
999       .context("await gen update via async js script")?;
1000   }
1001 }
1002
1003 impl Drop for FinalInfoCollection {
1004   fn drop(&mut self) {
1005     nix::unistd::linkat(None, "Xvfb_screen0",
1006                         None, "Xvfb_keep.xwd",
1007                         LinkatFlags::NoSymlinkFollow)
1008       .context("preserve Xvfb screen")
1009       .just_warn();
1010   }
1011 }
1012
1013 impl Drop for Setup {
1014   fn drop(&mut self) {
1015     (||{
1016       for name in mem::take(&mut self.windows_squirreled) {
1017         // This constructor is concurrency-hazardous.  It's only OK
1018         // here because we have &mut self.  If there is only one
1019         // Setup, there can be noone else with a Window with an
1020         // identical name to be interfered with by us.
1021         let w = Window {
1022           name: name.clone(),
1023           instance: TABLE.parse().context(TABLE)?,
1024         };
1025         self.w(&w)?.screenshot("final")
1026           .context(name)
1027           .context("final screenshot")
1028           .just_warn();
1029       }
1030       Ok::<_,AE>(())
1031     })()
1032       .context("screenshots, in Setup::drop")
1033       .just_warn();
1034   }
1035 }
1036
1037 #[throws(AE)]
1038 pub fn setup(exe_module_path: &str) -> (Setup, Instance) {
1039   env_logger::Builder::new()
1040     .format_timestamp_micros()
1041     .format_level(true)
1042     .filter_module("otter_webdriver_tests", log::LevelFilter::Debug)
1043     .filter_module(exe_module_path, log::LevelFilter::Debug)
1044     .filter_level(log::LevelFilter::Info)
1045     .parse_env("OTTER_WDT_LOG")
1046     .init();
1047   debug!("starting");
1048
1049   let current_exe : String = env::current_exe()
1050     .context("find current executable")?
1051     .to_str()
1052     .ok_or_else(|| anyhow!("current executable path is not UTF-8 !"))?
1053     .to_owned();
1054
1055   let opts = Opts::from_args();
1056   if !opts.no_bwrap {
1057     reinvoke_via_bwrap(&opts, &current_exe)
1058       .context("reinvoke via bwrap")?;
1059   }
1060
1061   info!("pid = {}", nix::unistd::getpid());
1062   sleep(opts.pause.into());
1063
1064   let cln = cleanup_notify::Handle::new()?;
1065   let ds = prepare_tmpdir(&opts, &current_exe)?;
1066
1067   prepare_xserver(&cln, &ds).always_context("setup X server")?;
1068
1069   let mgmt_conn =
1070     prepare_gameserver(&cln, &ds).always_context("setup game server")?;
1071
1072   let instance_name =
1073     prepare_game(&ds, TABLE).context("setup game")?;
1074
1075   let final_hook = FinalInfoCollection;
1076
1077   prepare_geckodriver(&opts, &cln).always_context("setup webdriver server")?;
1078   let (driver, screenshot_count, windows_squirreled) =
1079     prepare_thirtyfour().always_context("prepare web session")?;
1080
1081   (Setup {
1082     ds,
1083     mgmt_conn,
1084     driver,
1085     screenshot_count,
1086     current_window: None,
1087     windows_squirreled,
1088     final_hook,
1089   },
1090    Instance(
1091      instance_name
1092    ))
1093 }
1094
1095 impl Setup {
1096   #[throws(AE)]
1097   pub fn setup_static_users(&mut self, instance: &Instance) -> Vec<Window> {
1098     #[throws(AE)]
1099     fn mk(su: &mut Setup, instance: &Instance, u: StaticUser) -> Window {
1100       let nick: &str = u.into();
1101       let token = u.get_str("Token").expect("StaticUser missing Token");
1102       let subst = su.ds.also([("nick",  nick),
1103                               ("token", token)].iter());
1104       su.ds.otter(&subst
1105                   .ss("--super                          \
1106                        --account server:@nick@       \
1107                        --fixed-token @token@         \
1108                        join-game server::dummy")?)?;
1109       let w = su.new_window(instance, nick)?;
1110       let url = subst.subst("@url@/?@token@")?;
1111       su.w(&w)?.get(url)?;
1112       su.w(&w)?.screenshot("initial")?;
1113       w
1114     }
1115     StaticUser::iter().map(
1116       |u| mk(self, instance, u)
1117         .with_context(|| format!("{:?}", u))
1118         .context("make static user")
1119     )
1120       .collect::<Result<Vec<Window>,AE>>()?
1121   }
1122 }