1 // Copyright 2020 Ian Jackson
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
5 #![feature(unboxed_closures)]
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;
25 pub use t4::WebDriverCommands;
29 pub use std::fmt::{self, Debug};
30 pub use std::collections::hash_map::HashMap;
31 pub use std::convert::TryInto;
33 pub use std::io::{self, BufRead, BufReader, ErrorKind, Write};
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??
42 pub use std::process::{Command, Stdio};
43 pub use std::thread::{self, sleep};
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 pub use otter::ui::{AbbrevPresentationLayout, PresentationLayout};
56 pub type T4d = t4::WebDriver;
57 pub type WDE = t4::error::WebDriverError;
59 pub const MS : time::Duration = time::Duration::from_millis(1);
60 pub type AE = anyhow::Error;
62 pub const URL : &str = "http://localhost:8000";
64 pub fn default<T:Default>() -> T { Default::default() }
67 use otter::config::DAEMON_STARTUP_REPORT;
69 const TABLE : &str = "server::dummy";
70 const CONFIG : &str = "server-config.toml";
72 #[derive(Copy,Clone,Debug,Eq,PartialEq,Ord,PartialOrd)]
73 #[derive(FromPrimitive,EnumIter,IntoStaticStr,EnumProperty)]
74 #[strum(serialize_all = "snake_case")]
76 #[strum(props(Token="kmqAKPwK4TfReFjMor8MJhdRPBcwIBpe"))] Alice,
77 #[strum(props(Token="ccg9kzoTh758QrVE1xMY7BQWB36dNJTx"))] Bob,
80 pub trait AlwaysContext<T,E> {
81 fn always_context(self, msg: &'static str) -> anyhow::Result<T>;
84 impl<T,E> AlwaysContext<T,E> for Result<T,E>
85 where Self: anyhow::Context<T,E>,
87 fn always_context(self, msg: &'static str) -> anyhow::Result<T> {
88 let x = self.context(msg);
89 if x.is_ok() { info!("completed {}.", msg) };
94 pub trait JustWarn<T> {
95 fn just_warn(self) -> Option<T>;
98 impl<T> JustWarn<T> for Result<T,AE> {
99 fn just_warn(self) -> Option<T> {
110 #[derive(Debug,Clone)]
113 #[structopt(long="--no-bwrap")]
116 #[structopt(long="--tmp-dir", default_value="tmp")]
119 #[structopt(long="--as-if")]
120 as_if: Option<String>,
122 #[structopt(long="--pause", default_value="0ms")]
123 pause: humantime::Duration,
125 #[structopt(long="--layout", default_value="Portrait")]
126 layout: PresentationLayout,
128 #[structopt(long="--geckodriver-args", default_value="")]
129 geckodriver_args: String,
133 pub struct FinalInfoCollection;
135 type ScreenShotCount = u32;
136 type WindowState = Option<String>;
141 pub mgmt_conn: MgmtChannel,
144 current_window: WindowState,
145 screenshot_count: ScreenShotCount,
146 final_hook: FinalInfoCollection,
147 windows_squirreled: Vec<String>, // see Drop impl
150 #[derive(Clone,Debug)]
151 pub struct DirSubst {
154 pub start_dir: String,
158 pub struct Instance(InstanceName);
160 #[derive(Clone,Debug)]
161 pub struct Subst(HashMap<String,String>);
163 #[derive(Clone,Debug)]
164 pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
169 L: IntoIterator<Item=&'i (T, U)>>
172 fn from(l: L) -> Subst {
173 let map = l.into_iter()
174 .map(|(k,v)| (k.as_ref().to_owned(), v.as_ref().to_owned())).collect();
179 pub trait Substitutor {
180 fn get(&self, kw: &str) -> Option<String>;
182 fn also<L: Into<Subst>>(&self, xl: L) -> ExtendedSubst<Self, Subst>
183 where Self: Clone + Sized {
184 ExtendedSubst(self.clone(), xl.into())
188 fn subst<S: AsRef<str>>(&self, s: S) -> String
191 fn inner(self_: &dyn Substitutor, s: &dyn AsRef<str>) -> String {
193 let re = Regex::new(r"@(\w+)@").expect("bad re!");
194 let mut errs = vec![];
195 let out = re.replace_all(s, |caps: ®ex::Captures| {
196 let kw = caps.get(1).expect("$1 missing!").as_str();
197 if kw == "" { return "".to_owned() }
198 let v = self_.get(kw);
200 errs.push(kw.to_owned());
204 if ! errs.is_empty() {
205 throw!(anyhow!("bad substitution(s) {:?} in {:?}",
214 fn ss(&self, s: &str) -> Vec<String>
219 .filter(|s| !s.is_empty())
225 impl Substitutor for Subst {
226 fn get(&self, kw: &str) -> Option<String> {
227 self.0.get(kw).map(String::clone)
231 impl<B:Substitutor, X:Substitutor> Substitutor for ExtendedSubst<B, X> {
232 fn get(&self, kw: &str) -> Option<String> {
233 self.1.get(kw).or_else(|| self.0.get(kw))
237 impl Substitutor for DirSubst {
238 fn get(&self, kw: &str) -> Option<String> {
240 "url" => URL.to_owned(),
241 "src" => self.src.clone(),
242 "build" => self.start_dir.clone(),
243 "abstmp" => self.abstmp.clone(),
244 "target" => format!("{}/target", &self.start_dir),
245 "specs" => format!("{}/specs" , &self.src ),
253 use fehler::{throw, throws};
255 use nix::errno::Errno::*;
256 use nix::{unistd::*, fcntl::OFlag};
257 use nix::sys::signal::*;
261 use std::os::unix::io::RawFd;
262 use std::panic::catch_unwind;
263 use std::process::Command;
264 type AE = anyhow::Error;
266 pub struct Handle(RawFd);
269 fn mkpipe() -> (RawFd,RawFd) {
270 pipe2(OFlag::O_CLOEXEC).map_err(nix2io)?
274 fn read_await(fd: RawFd) {
276 let mut buf = [0u8; 1];
277 match nix::unistd::read(fd, &mut buf) {
279 Ok(_) => throw!(io::Error::from_raw_os_error(libc::EINVAL)),
280 Err(Sys(EINTR)) => continue,
281 _ => throw!(io::Error::last_os_error()),
286 fn nix2io(_n: nix::Error) -> io::Error {
287 io::Error::last_os_error()
292 pub fn new() -> Self {
293 let (reading_end, _writing_end) = mkpipe()
294 .context("create cleanup notify pipe")?;
295 // we leak the writing end, keeping it open only in this process
300 pub fn arm_hook(&self, cmd: &mut Command) { unsafe {
301 use std::os::unix::process::CommandExt;
303 let notify_writing_end = self.0;
304 let all_signals = nix::sys::signal::SigSet::all();
306 cmd.pre_exec(move || -> Result<(), io::Error> {
307 let semidaemon = nix::unistd::getpid();
308 let (reading_end, writing_end) = mkpipe()?;
310 match fork().map_err(nix2io)? {
311 ForkResult::Child => {
312 let _ = catch_unwind(move || -> Void {
314 SigmaskHow::SIG_BLOCK,
319 let _ = close(writing_end);
320 let _ = nix::unistd::dup2(2, 1);
323 if fd == notify_writing_end { continue }
325 if fd >= writing_end && matches!(r, Err(Sys(EBADF))) {
329 let _ = read_await(notify_writing_end);
330 let _ = kill(semidaemon, SIGTERM);
333 let _ = raise(SIGABRT);
336 ForkResult::Parent{..} => {
338 close(writing_end).map_err(nix2io)?;
339 read_await(reading_end)?;
350 fn reinvoke_via_bwrap(_opts: &Opts, current_exe: &str) -> Void {
351 debug!("running bwrap");
353 let mut bcmd = Command::new("bwrap");
355 .args("--unshare-net \
358 --die-with-parent".split(" "))
361 .args(env::args_os().skip(1));
363 std::io::stdout().flush().context("flush stdout")?;
364 let e : AE = bcmd.exec().into();
365 throw!(e.context("exec bwrap"));
369 fn prepare_tmpdir<'x>(opts: &'x Opts, mut current_exe: &'x str) -> DirSubst {
371 fn getcwd() -> String {
375 .ok_or_else(|| anyhow!("path is not UTF-8"))?
379 if let Some(as_if) = &opts.as_if {
383 let start_dir = getcwd()
384 .context("canonicalise our invocation directory (getcwd)")?;
387 match fs::metadata(&opts.tmp_dir) {
390 throw!(anyhow!("existing object is not a directory"));
392 if (m.st_mode() & 0o01002) != 0 {
394 "existing directory mode {:#o} is sticky or world-writeable. \
395 We use predictable pathnames so that would be a tmp race",
400 Err(e) if e.kind() == ErrorKind::NotFound => {
401 fs::create_dir(&opts.tmp_dir)
405 let e : AE = e.into();
406 throw!(e.context("stat existing directory"))
410 env::set_current_dir(&opts.tmp_dir)
411 .context("chdir into it")?;
415 .with_context(|| opts.tmp_dir.to_owned())
416 .context("prepare/create tmp-dir")?;
418 let leaf = current_exe.rsplitn(2, '/').next().unwrap();
419 let our_tmpdir = format!("{}/{}", &opts.tmp_dir, &leaf);
421 match fs::remove_dir_all(&leaf) {
423 Err(e) if e.kind() == ErrorKind::NotFound => {},
424 Err(e) => throw!(AE::from(e).context("remove previous directory")),
427 fs::DirBuilder::new().create(&leaf)
428 .context("create fresh subdirectory")?;
430 env::set_current_dir(&leaf)
431 .context("chdir into it")?;
435 .with_context(|| our_tmpdir.to_owned())
436 .context("prepare/create our tmp subdir")?;
439 getcwd().context("canonicalise our tmp subdir (getcwd)")?;
441 env::set_var("HOME", &abstmp);
442 env::set_var("TMPDIR", &abstmp);
443 for v in "http_proxy https_proxy XAUTHORITY CDPATH \
444 SSH_AGENT_PID SSH_AUTH_SOCK WINDOWID WWW_HOME".split(' ')
449 let manifest_var = "CARGO_MANIFEST_DIR";
450 let src : String = (|| Ok::<_,AE>(match env::var(manifest_var) {
451 Ok(dir) => dir.into(),
452 Err(env::VarError::NotPresent) => start_dir.clone(),
453 e@ Err(_) => throw!(e.context(manifest_var).err().unwrap()),
455 .context("find source code")?;
466 fn fork_something_which_prints(mut cmd: Command,
467 cln: &cleanup_notify::Handle,
472 cmd.stdout(Stdio::piped());
473 cln.arm_hook(&mut cmd)?;
474 let mut child = cmd.spawn().context("spawn")?;
475 let mut report = BufReader::new(child.stdout.take().unwrap())
478 let l = report.next();
480 let s = child.try_wait().context("check on spawned child")?;
482 throw!(anyhow!("failed to start: wait status = {}", &e));
487 None => throw!(anyhow!("EOF (but it's still running?")),
488 Some(Err(e)) => throw!(AE::from(e).context("failed to read")),
491 let what = what.to_owned();
492 thread::spawn(move|| (||{
494 let l : Result<String, io::Error> = l;
495 let l = l.context("reading further output")?;
496 const MAXLEN : usize = 300;
497 if l.len() <= MAXLEN {
498 println!("{} {}", what, l);
500 println!("{} {}...", what, &l[..MAXLEN-3]);
504 })().context(what).just_warn()
508 })().with_context(|| what.to_owned())?
512 fn prepare_xserver(cln: &cleanup_notify::Handle, ds: &DirSubst) {
513 const DISPLAY : u16 = 12;
515 let mut xcmd = Command::new("Xvfb");
517 .args("-nolisten unix \
523 -displayfd 1".split(' '))
524 .args(&["-fbdir", &ds.abstmp])
525 .arg(format!(":{}", DISPLAY));
527 let l = fork_something_which_prints(xcmd, cln, "Xvfb")?;
529 if l != DISPLAY.to_string() {
531 "Xfvb said {:?}, expected {:?}",
536 let display = format!("[::1]:{}", DISPLAY);
537 env::set_var("DISPLAY", &display);
539 // Doesn't do IPv6 ??
540 let v4display = format!("127.0.0.1:{}", DISPLAY);
541 let (xconn, _) = x11rb::connect(Some(&v4display))
542 .context("make keepalive connection to X server")?;
544 // Sadly, if we die between spawning Xfvb, and here, we will
545 // permanently leak the whole Xfvb process (and the network
546 // namespace). There doesn't seem to a way to avoid this without
549 Box::leak(Box::new(xconn));
553 fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
555 let subst = ds.also(&[
556 ("command_socket", "command.socket"),
558 let config = subst.subst(r##"
559 change_directory = "@abstmp@"
564 command_socket = "@command_socket@"
565 template_dir = "@src@/templates"
566 nwtemplate_dir = "@src@/nwtemplates"
567 bundled_sources = "@target@/bundled-sources"
568 wasm_dir = "@target@/packed-wasm"
569 shapelibs = [ "@src@/library/*.toml" ]
571 debug_js_inject_file = "@src@/templates/log-save.js"
572 check_bundled_sources = false # For testing only! see LICENCE!
575 global_level = 'debug'
580 # ^ comment these two out to see Tera errors, *sigh*
582 'hyper::server' = 'info'
583 "game::debugreader" = 'info'
584 "game::updates" = 'trace'
587 fs::write(CONFIG, &config)
588 .context(CONFIG).context("create server config")?;
590 let server_exe = ds.subst("@target@/debug/daemon-otter")?;
591 let mut cmd = Command::new(&server_exe);
593 .arg("--report-startup")
597 let l = fork_something_which_prints(cmd, cln, &server_exe)?;
598 if l != DAEMON_STARTUP_REPORT {
599 throw!(anyhow!("otter-daemon startup report {:?}, expected {:?}",
600 &l, DAEMON_STARTUP_REPORT));
604 .context("game server")?;
606 let mut mgmt_conn = MgmtChannel::connect(
607 &subst.subst("@command_socket@")?
610 mgmt_conn.cmd(&MgmtCommand::SetSuperuser(true))?;
611 mgmt_conn.cmd(&MgmtCommand::SelectAccount("server:".parse()?))?;
618 pub fn otter<S:AsRef<str>>(&self, xargs: &[S]) {
620 let exe = ds.subst("@target@/debug/otter")?;
621 let mut args : Vec<&str> = vec![];
622 args.extend(&["--config", CONFIG]);
623 args.extend(xargs.iter().map(AsRef::as_ref));
624 let dbg = format!("running {} {:?}", &exe, &args);
627 let mut cmd = Command::new(&exe);
630 .spawn().context("spawn")?
631 .wait().context("wait")?;
633 throw!(anyhow!("wait status {}", &st));
638 .context("run otter client")?;
643 pub fn prepare_game(ds: &DirSubst, table: &str) -> InstanceName {
644 let subst = ds.also(&[("table", &table)]);
648 --reset-table @specs@/test.table.toml \
649 @table@ @specs@/demo.game.toml \
650 ")?).context("reset table")?;
652 let instance : InstanceName = table.parse()
653 .with_context(|| table.to_owned())
654 .context("parse table name")?;
660 fn prepare_geckodriver(opts: &Opts, cln: &cleanup_notify::Handle) {
661 const EXPECTED : &str = "Listening on 127.0.0.1:4444";
662 let mut cmd = Command::new("geckodriver");
663 if opts.geckodriver_args != "" {
664 cmd.args(opts.geckodriver_args.split(' '));
666 let l = fork_something_which_prints(cmd, cln, "geckodriver")?;
667 let fields : Vec<_> = l.split('\t').skip(2).take(2).collect();
668 let expected = ["INFO", EXPECTED];
669 if fields != expected {
670 throw!(anyhow!("geckodriver did not report as expected \
671 - got {:?}, expected {:?}",
677 fn prepare_thirtyfour() -> (T4d, ScreenShotCount, Vec<String>) {
679 let mut caps = t4::DesiredCapabilities::firefox();
680 let prefs : HashMap<_,_> = [
681 ("devtools.console.stdout.content", true),
682 ].iter().cloned().collect();
683 caps.add("prefs", prefs)?;
684 caps.add("stdio", "inherit")?;
685 let mut driver = t4::WebDriver::new("http://localhost:4444", &caps)
686 .context("create 34 WebDriver")?;
688 const FRONT : &str = "front";
689 let window_names = vec![FRONT.into()];
690 driver.set_window_name(FRONT).context("set initial window name")?;
691 screenshot(&mut driver, &mut count, "startup")?;
692 driver.get(URL).context("navigate to front page")?;
693 screenshot(&mut driver, &mut count, "front")?;
695 fetch_log(&driver, "front")?;
697 let t = Some(5_000 * MS);
698 driver.set_timeouts(t4::TimeoutConfiguration::new(t,t,t))
699 .context("set webdriver timeouts")?;
701 (driver, count, window_names)
704 /// current window must be `name`
706 fn fetch_log(driver: &T4d, name: &str) {
708 let got = driver.execute_script(r#"
709 var returning = window.console.saved;
710 window.console.saved = [];
712 "#).context("get log")?;
714 for ent in got.value().as_array()
715 .ok_or(anyhow!("saved isn't an array?"))?
717 #[derive(Deserialize)]
718 struct LogEnt(String, Vec<serde_json::Value>);
719 impl fmt::Display for LogEnt {
720 #[throws(fmt::Error)]
721 fn fmt(&self, f: &mut fmt::Formatter) {
722 write!(f, "{}:", self.0)?;
723 for a in &self.1 { write!(f, " {}", a)?; }
727 let ent: LogEnt = serde_json::from_value(ent.clone())
728 .context("parse log entry")?;
730 debug!("JS {} {}", name, &ent);
734 .with_context(|| name.to_owned())
735 .context("fetch JS log messages")?;
741 instance: InstanceName,
745 pub fn table(&self) -> String { self.instance.to_string() }
748 type ScreenCTM = ndarray::Array2::<f64>;
750 pub struct WindowGuard<'g> {
753 matrix: once_cell::sync::OnceCell<ScreenCTM>,
756 impl Debug for WindowGuard<'_> {
757 #[throws(fmt::Error)]
758 fn fmt(&self, f: &mut fmt::Formatter) {
759 f.debug_struct("WindowGuard")
760 .field("w.name", &self.w.name)
761 .field("w.instance", &self.w.instance.to_string())
766 impl<'g> WindowGuard<'g> {
768 pub fn find_piece(&'g self, pieceid: &'g str) -> PieceElement<'g> {
769 let id = format!("use{}", pieceid);
770 let elem = self.su.driver.find_element(By::Id(&id))?;
778 pub type WebCoord = i32;
779 pub type WebPos = (WebCoord, WebCoord);
781 pub struct PieceElement<'g> {
783 w: &'g WindowGuard<'g>,
784 elem: t4::WebElement<'g>,
787 impl<'g> Deref for PieceElement<'g> {
788 type Target = t4::WebElement<'g>;
789 fn deref<'i>(&'i self) -> &'i t4::WebElement<'g> { &self.elem }
792 impl<'g> PieceElement<'g> {
794 pub fn posg(&self) -> Pos {
796 let a = |a| Ok::<_,AE>(
797 self.get_attribute(a)?.ok_or(anyhow!("{}", a))?.parse()?
801 Ok::<_,AE>(PosC([x,y]))
803 .with_context(|| self.pieceid.to_owned())
804 .context("read position of piece out of x,y attributes")?
808 pub fn posw(&self) -> WebPos {
809 let posg = self.posg()?;
811 let mat = self.w.matrix.get_or_try_init(||{
812 let ary = self.w.su.driver.execute_script(r#"
813 let m = space.getScreenCTM();
814 return [m.a, m.b, m.c, m.d, m.e, m.f];
816 let ary = ary.value();
820 let ary = ary.as_array().ok_or_else(|| anyhow!("not array"))?;
821 let mut mat : ScreenCTM = ndarray::Array2::zeros((3,3));
822 for got in itertools::Itertools::zip_longest(
823 [11, 12, 21, 22, 41, 42].iter(),
824 // ^ from https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrix
827 let (mij, v) = got.both().ok_or_else(|| anyhow!("wrong length"))?;
828 let adj = |v| (if v == 4 { 3 } else { v }) - 1;
829 let i = adj(mij / 10);
830 let j = adj(mij % 10);
831 mat[(j,i)] = v.as_f64().ok_or_else(|| anyhow!("entry not f64"))?;
835 .with_context(|| format!("getScreenCGM script gave {:?}", &ary))?;
841 let vec : ndarray::Array1<f64> =
845 .chain(iter::once(1.))
849 let vec = mat.dot(&vec);
850 let mut coords = vec.iter().map(
851 |v| NumCast::from(v.round()).ok_or_else(
852 || anyhow!("coordinate {} out of range in {:?}", v, &vec))
854 let mut coord = || coords.next().unwrap();
860 .with_context(|| self.pieceid.to_owned())
861 .context("find piece position")?
866 fn check_window_name_sanity(name: &str) -> &str {
867 let e = || anyhow!("bad window name {:?}", &name);
869 name.chars().nth(0).ok_or(e())?
870 .is_ascii_alphanumeric().ok_or(e())?;
873 |c| c.is_ascii_alphanumeric() || c == '-' || c == '_'
881 pub fn new_window<'s>(&'s mut self, instance: &Instance, name: &str)
883 let name = check_window_name_sanity(name)?;
886 self.current_window = None; // we might change the current window
888 match self.driver.switch_to().window_name(name) {
889 Ok(()) => throw!(anyhow!("window already exists")),
890 Err(WDE::NoSuchWindow(_)) |
891 Err(WDE::NotFound(..)) => (),
893 eprintln!("wot {:?}", &e);
895 .context("check for pre-existing window")
900 self.driver.execute_script(&format!(
901 r#"window.open('', target='{}');"#,
904 .context("execute script to create window")?;
907 name: name.to_owned(),
908 instance: instance.0.clone(),
911 .with_context(|| name.to_owned())
912 .context("create window")?;
914 self.windows_squirreled.push(name.to_owned());
921 pub fn w<'s>(&'s mut self, w: &'s Window) -> WindowGuard<'s> {
922 if self.current_window.as_ref() != Some(&w.name) {
923 self.driver.switch_to().window_name(&w.name)
924 .with_context(|| w.name.to_owned())
925 .context("switch to window")?;
926 self.current_window = Some(w.name.clone());
936 impl<'g> Deref for WindowGuard<'g> {
938 fn deref(&self) -> &T4d { &self.su.driver }
941 impl<'g> Drop for WindowGuard<'g> {
943 fetch_log(&self.su.driver, &self.w.name)
948 pub trait Screenshottable {
949 fn screenshot(&mut self, slug: &str) -> Result<(),AE>;
952 impl<'g> Screenshottable for WindowGuard<'g> {
954 fn screenshot(&mut self, slug: &str) {
955 screenshot(&self.su.driver, &mut self.su.screenshot_count,
956 &format!("{}-{}", &self.w.name, slug))?
961 fn screenshot(driver: &T4d, count: &mut ScreenShotCount, slug: &str) {
962 let path = format!("{:03}{}.png", count, slug);
964 driver.screenshot(&path::PathBuf::from(&path))
965 .with_context(|| path.clone())
966 .context("take screenshot")?;
967 debug!("screenshot {}", &path);
970 impl<'g> WindowGuard<'g> {
972 pub fn synch(&mut self) {
973 let cmd = MgmtCommand::AlterGame {
974 game: self.w.instance.clone(),
975 how: MgmtGameUpdateMode::Online,
976 insns: vec![ MgmtGameInstruction::Synch ],
979 let resp = self.su.mgmt_conn.cmd(&cmd)?;
980 if let MgmtResponse::AlterGame {
984 if let [MgmtGameResponse::Synch(gen)] = responses[..];
986 else { throw!(anyhow!("unexpected resp to synch {:?}", resp)) }
988 trace!("{:?} gen={} ...", self, gen);
991 let tgen = self.su.driver.execute_async_script(
993 ("wanted", &gen.to_string())
995 var done = arguments[0];
996 if (gen >= @wanted@) { done(gen); return; }
997 window.gen_update_hook = function() {
998 window.gen_update_hook = function() { };
1003 .context("run async script")?
1004 .value().as_u64().ok_or(anyhow!("script return is not u64"))?;
1005 let tgen = Generation(tgen);
1006 trace!("{:?} gen={} tgen={}", self, gen, tgen);
1007 if tgen >= gen { break; }
1011 .context("await gen update via async js script")?;
1014 let errors = self.su.driver.execute_script(r#"
1015 let e = document.getElementById('error');
1019 console.log('wdt-*: no errors element, no trapped errors check');
1023 .context("get errors")?;
1027 .ok_or_else(|| anyhow!("errors script gave non-string"))?;
1028 if ! errors.is_empty() {
1029 throw!(anyhow!("JS errors - HTML: {}", errors));
1033 .context("check for in-client trapped errors")?;
1037 impl Drop for FinalInfoCollection {
1038 fn drop(&mut self) {
1039 nix::unistd::linkat(None, "Xvfb_screen0",
1040 None, "Xvfb_keep.xwd",
1041 LinkatFlags::NoSymlinkFollow)
1042 .context("preserve Xvfb screen")
1047 impl Drop for Setup {
1048 fn drop(&mut self) {
1050 for name in mem::take(&mut self.windows_squirreled) {
1051 // This constructor is concurrency-hazardous. It's only OK
1052 // here because we have &mut self. If there is only one
1053 // Setup, there can be noone else with a Window with an
1054 // identical name to be interfered with by us.
1057 instance: TABLE.parse().context(TABLE)?,
1059 self.w(&w)?.screenshot("final")
1061 .context("final screenshot")
1066 .context("screenshots, in Setup::drop")
1072 pub fn setup(exe_module_path: &str) -> (Setup, Instance) {
1073 env_logger::Builder::new()
1074 .format_timestamp_micros()
1076 .filter_module("otter_webdriver_tests", log::LevelFilter::Debug)
1077 .filter_module(exe_module_path, log::LevelFilter::Debug)
1078 .filter_level(log::LevelFilter::Info)
1079 .parse_env("OTTER_WDT_LOG")
1083 let current_exe : String = env::current_exe()
1084 .context("find current executable")?
1086 .ok_or_else(|| anyhow!("current executable path is not UTF-8 !"))?
1089 let opts = Opts::from_args();
1091 reinvoke_via_bwrap(&opts, ¤t_exe)
1092 .context("reinvoke via bwrap")?;
1095 info!("pid = {}", nix::unistd::getpid());
1096 sleep(opts.pause.into());
1098 let cln = cleanup_notify::Handle::new()?;
1099 let ds = prepare_tmpdir(&opts, ¤t_exe)?;
1101 prepare_xserver(&cln, &ds).always_context("setup X server")?;
1104 prepare_gameserver(&cln, &ds).always_context("setup game server")?;
1107 prepare_game(&ds, TABLE).context("setup game")?;
1109 let final_hook = FinalInfoCollection;
1111 prepare_geckodriver(&opts, &cln).always_context("setup webdriver server")?;
1112 let (driver, screenshot_count, windows_squirreled) =
1113 prepare_thirtyfour().always_context("prepare web session")?;
1121 current_window: None,
1132 pub fn setup_static_users(&mut self, instance: &Instance) -> Vec<Window> {
1134 fn mk(su: &mut Setup, instance: &Instance, u: StaticUser) -> Window {
1135 let nick: &str = u.into();
1136 let token = u.get_str("Token").expect("StaticUser missing Token");
1137 let pl = AbbrevPresentationLayout(su.opts.layout).to_string();
1138 let subst = su.ds.also([
1145 --account server:@nick@ \
1146 --fixed-token @token@ \
1147 join-game server::dummy")?)?;
1148 let w = su.new_window(instance, nick)?;
1149 let url = subst.subst("@url@/@pl@?@token@")?;
1150 su.w(&w)?.get(url)?;
1151 su.w(&w)?.screenshot("initial")?;
1154 StaticUser::iter().map(
1155 |u| mk(self, instance, u)
1156 .with_context(|| format!("{:?}", u))
1157 .context("make static user")
1159 .collect::<Result<Vec<Window>,AE>>()?