Skip to main content

otter_api_tests/
apitest.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//! Otter game system (part thereeof)
6//!
7//! <https://www.chiark.greenend.org.uk/~ianmdlvl/otter/docs/README.html>
8//!
9//! This crate is intended for use only by other parts of Otter.
10
11// ==================== namespace preparation ====================
12
13pub mod crates {
14  pub use otter;
15  pub use otter::crates::*;
16
17  pub use humantime;
18}
19
20pub use crates::*;
21pub use otter::prelude::*;
22
23pub use std::cell::{RefCell, RefMut};
24
25pub use num_traits::NumCast;
26pub use serde_json::json;
27pub use structopt::StructOpt;
28pub use reqwest;
29
30pub type MgmtChannel = ClientMgmtChannel;
31
32pub type JsV = serde_json::Value;
33pub type MC = MgmtCommand;
34
35mod pi {
36  use otter::prelude::define_index_type;
37  define_index_type!{ pub struct PIA = usize; }
38  define_index_type!{ pub struct PIB = usize; }
39}
40pub use pi::*;
41
42// -------------------- private crates ----------
43
44use otter_support::config::DAEMON_STARTUP_REPORT;
45
46// ==================== public constants ====================
47
48pub const TABLE: &str = "server::dummy";
49pub const CONFIG: &str = "server-config.toml";
50
51pub const URL: &str = "http://localhost:8000";
52
53#[derive(Copy,Clone,Debug,Eq,PartialEq,Ord,PartialOrd)]
54#[derive(FromPrimitive,EnumIter,IntoStaticStr,EnumProperty)]
55#[strum(serialize_all = "snake_case")]
56pub enum StaticUser {
57  #[strum(props(Token="kmqAKPwK4TfReFjMor8MJhdRPBcwIBpe"))] Alice,
58  #[strum(props(Token="ccg9kzoTh758QrVE1xMY7BQWB36dNJTx"))] Bob,
59}
60
61// ==================== principal public structs ====================
62
63#[derive(Debug,Clone)]
64#[derive(StructOpt)]
65pub struct Opts {
66  #[structopt(long="--as-if")]
67  pub as_if: Option<String>,
68
69  #[structopt(long="--no-bwrap")]
70  pub no_bwrap: bool,
71
72  #[structopt(long="--tmp-dir", default_value="tmp")]
73  pub tmp_dir: String,
74
75  #[structopt(long="--pause", default_value="0ms")]
76  pub pause: humantime::Duration,
77
78  #[structopt(flatten)]
79  pub tests: WantedTestsOpt,
80
81  #[structopt(long="--test")]
82  test_name: Option<String>,
83}
84
85#[derive(Debug)]
86pub struct SetupCore {
87  pub ds: DirSubst,
88  pub mgmt_conn: RefCell<MgmtChannelForGame>,
89  pub server_child: Child,
90  pub wanted_tests: TrackWantedTests,
91  pub cln: cleanup_notify::Handle,
92  pub initial_pieces_cache: Option<Vec<MgmtGamePieceInfo>>,
93}
94
95#[derive(Clone,Debug)]
96pub struct DirSubst {
97  pub tmp: String,
98  pub abstmp: String,
99  pub start_dir: String,
100  pub src: String,
101}
102
103pub struct Instance(pub InstanceName);
104
105// ==================== Facilities for tests ====================
106
107impl AsRef<Opts> for Opts { fn as_ref(&self) -> &Opts { self } }
108
109#[derive(Debug)]
110pub enum Explode { }
111impl<'e, E:Into<Box<dyn Error + 'e>>> From<E> for Explode {
112  fn from(e: E) -> Explode {
113    let mut m = "exploding on error".to_string();
114    let e: Box<dyn Error> = e.into();
115    let mut e: Option<&dyn Error> = Some(&*e);
116    while let Some(te) = e {
117      m += &format!(": {}", &te);
118      e = te.source();
119    }
120    panic!("{}", m);
121  }
122}
123impl From<Explode> for anyhow::Error {
124  fn from(e: Explode) -> AE { match e { } }
125}
126#[ext(pub, name=ResultExplodeExt)]
127impl<T> Result<T,Explode> {
128  fn y(self) -> T { match self { Ok(y) => y, Err(n) => match n { } } }
129  fn did(self, msg: &'static str) -> anyhow::Result<T> {
130    ResultGenDidExt::<_,AE>::did(Ok(self.y()), msg)
131  }
132}
133  
134/*
135impl<E:Error> From<Explode> for E {
136  fn from(e: Explode) -> E { match e { } }
137}*/
138
139#[ext(pub)]
140impl JsV {
141  fn set<K: Into<String>>(&mut self, k: K, v: &JsV) {
142    self.as_object_mut().unwrap().insert(k.into(), v.clone());
143  }
144
145  fn extend<I,K,V>(&mut self, i: I)
146  where I: IntoIterator<Item=(K, V)>,
147        K: Into<String>,
148        V: Borrow<JsV>,
149  {
150    let i = i.into_iter().map(|(k,v)| (k.into(), v.borrow().clone()));
151    self.as_object_mut().unwrap().extend(i);
152  }
153
154  #[throws(E)]
155  fn tree_walk<F,E>(&self, #[allow(unused_mut,unused_variables)] mut f: F)
156  where F: FnMut(&[String], &JsV) -> Result<(),E>
157  {
158    #[throws(E)]
159    fn recurse<F,E>(kl: &mut Vec<String>, v: &JsV, f: &mut F)
160    where F: FnMut(&[String], &JsV) -> Result<(),E> {
161      f(&**kl, v)?;
162      if let Some(o) = v.as_object() {
163        for (k,v) in o {
164          kl.push(k.to_owned());
165          let y = recurse(kl, v, f);
166          kl.pop();
167          let () = y?;
168        }
169      } else if let Some(a) = v.as_array() {
170        for (k,v) in a.iter().enumerate() {
171          kl.push(k.to_string());
172          let y = recurse(kl, v, f);
173          kl.pop();
174          let () = y?;
175        }
176      }
177    }
178
179    let mut kl = vec![];
180    recurse(&mut kl, self, &mut f)?
181  }
182}
183
184// -------------------- Substition --------------------
185
186pub trait Substitutor {
187  fn get(&self, kw: &str) -> Option<String>;
188
189  fn also<L: Into<Subst>>(&self, xl: L) -> ExtendedSubst<Self, Subst>
190  where Self: Clone + Sized {
191    ExtendedSubst(self.clone(), xl.into())
192  }
193
194  #[throws(AE)]
195  fn subst(&self, s: &str) -> String {
196    let re = Regex::new(r"@(\w+)@").expect("bad re!");
197    let mut errs = vec![];
198    let out = re.replace_all(s, |caps: &regex::Captures| {
199      let kw = caps.get(1).expect("$1 missing!").as_str();
200      if kw == "" { return "".to_owned() }
201      let v = self.get(kw);
202      v.unwrap_or_else(||{
203        errs.push(kw.to_owned());
204        "".to_owned()
205      })
206    });
207    if ! errs.is_empty() {
208      throw!(anyhow!("bad substitution(s) {:?} in {:?}",
209                     &errs, s));
210    }
211    out.into()
212  }
213
214  #[throws(AE)]
215  fn ss(&self, s: &str) -> Vec<String> {
216    self.subst(s)?
217      .trim()
218      .split(' ')
219      .filter(|s| !s.is_empty())
220      .map(str::to_string)
221      .collect()
222  }
223
224  #[throws(AE)]
225  fn gss(&self, s: &str) -> Vec<String> {
226    self.ss(&format!("-g @table@ {}", s))?
227  }
228}
229
230#[derive(Clone,Debug)]
231pub struct Subst(HashMap<String,String>);
232
233impl Substitutor for Subst {
234  fn get(&self, kw: &str) -> Option<String> {
235    self.0.get(kw).map(String::clone)
236  }
237}
238
239impl<'i,
240     T: AsRef<str> + 'i,
241     U: AsRef<str> + 'i,
242     L: IntoIterator<Item=&'i (T, U)>>
243  From<L> for Subst
244{
245  fn from(l: L) -> Subst {
246    let map = l.into_iter()
247      .map(|(k,v)| (k.as_ref().to_owned(), v.as_ref().to_owned())).collect();
248    Subst(map)
249  }
250}
251
252#[derive(Clone,Debug)]
253pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
254
255impl<B:Substitutor, X:Substitutor> Substitutor for ExtendedSubst<B, X> {
256  fn get(&self, kw: &str) -> Option<String> {
257    self.1.get(kw).or_else(|| self.0.get(kw))
258  }
259}
260
261impl Substitutor for DirSubst {
262  fn get(&self, kw: &str) -> Option<String> {
263    Some(match kw {
264      "url"    => URL.to_owned(),
265      "src"    => self.src.clone(),
266      "build"  => self.start_dir.clone(),
267      "abstmp" => self.abstmp.clone(),
268      "target" => format!("{}/target", &self.start_dir),
269      "specs"  => self.specs_dir(),
270      "table"  => TABLE.to_owned(),
271      "command_socket" => "command.socket".to_owned(),
272      "examples"       => format!("{}/examples", &self.src),
273      _ => return None,
274    })
275  }
276}
277
278// ---------- requested/available test tracking ----------
279
280#[derive(Clone,Debug)]
281#[derive(StructOpt)]
282pub struct WantedTestsOpt {
283  tests: Vec<String>,
284}
285
286#[derive(Debug)]
287pub struct TrackWantedTests {
288  wanted: WantedTestsOpt,
289  found: BTreeSet<String>,
290}
291
292impl WantedTestsOpt {
293  pub fn track(&self) -> TrackWantedTests {
294    TrackWantedTests { wanted: self.clone(), found: default() }
295  }
296}
297
298impl TrackWantedTests {
299  pub fn wantp(&mut self, tname: &str) -> bool {
300    self.found.insert(tname.to_owned());
301    let y =
302      self.wanted.tests.is_empty() ||
303      self.wanted.tests.iter().any(|s| s==tname);
304    y
305  }
306}
307
308impl Drop for TrackWantedTests {
309  fn drop(&mut self) {
310    let missing_tests = self.wanted.tests.iter().cloned()
311      .filter(|s| !self.found.contains(s))
312      .collect::<Vec<_>>();
313
314    if !missing_tests.is_empty() && !self.found.is_empty() {
315      for f in &self.found {
316        eprintln!("fyi: test that exists: {}", f);
317      }
318      for m in &missing_tests {
319        eprintln!("warning: unknown test requested: {}", m);
320      }
321    }
322  }
323}
324
325#[macro_export]
326macro_rules! usual_wanted_tests {
327  ($ctx:ty, $su:ident) => {
328    impl $ctx {
329      fn wanted_tests(&mut self) -> &mut TrackWantedTests {
330        &mut self.su.wanted_tests
331      }
332    }
333  }
334}
335
336#[macro_export]
337macro_rules! test {
338  ($c:expr, $tname:expr, $s:stmt) => {
339    if $c.wanted_tests().wantp($tname) {
340      debug!("==================== {} starting ====================", $tname);
341      $s
342      info!("==================== {} completed ====================", $tname);
343    } else {
344      trace!("= = = {} skipped = = =", $tname);
345    }
346  }
347}
348
349// -------------------- Extra anyhow result handling --------------------
350
351pub trait PropagateDid {
352  fn propagate_did<T>(self, msg: &'static str) -> anyhow::Result<T>;
353}
354
355#[ext(pub, name=ResultGenDidExt)]
356impl<T,E> Result<T,E> where Result<T,E>: anyhow::Context<T,E> {
357  fn did(self, msg: &'static str) -> anyhow::Result<T>
358  {
359    match self {
360      Ok(y) => { info!("did {}.", msg); Ok(y) }
361      n@ Err(_) => n.context(msg),
362    }
363  }
364}
365
366#[ext(pub)]
367impl<T,E> Result<T,E> {
368  fn just_warn(self) -> Option<T>
369  where E: Display
370  {
371    match self {
372      Ok(x) => Some(x),
373      Err(e) => {
374        warn!("{:#}", e);
375        None
376      },
377    }
378  }
379}
380
381// -------------------- cleanup_notify (signaling) --------------------
382
383pub mod cleanup_notify {
384  use super::crates::*;
385  use otter_support::crates::*;
386  use super::AE;
387  pub use super::Void; // TODO remove the need for this
388
389  use anyhow::Context;
390  use fehler::{throw, throws};
391  use libc::_exit;
392  use nix::{unistd::*, fcntl::OFlag};
393  use nix::sys::signal::*;
394  use nix::Error as NE;
395  use std::io;
396  use std::os::unix::io::RawFd;
397  use std::panic::catch_unwind;
398  use std::process::Command;
399
400  #[derive(Debug)]
401  pub struct Handle(RawFd);
402
403  #[throws(io::Error)]
404  fn mkpipe() -> (RawFd,RawFd) {
405    pipe2(OFlag::O_CLOEXEC).map_err(nix2io)?
406  }
407
408  #[throws(io::Error)]
409  fn read_await(fd: RawFd) {
410    loop {
411      let mut buf = [0u8; 1];
412      match nix::unistd::read(fd, &mut buf) {
413        Ok(0) => break,
414        Ok(_) => throw!(io::Error::from_raw_os_error(libc::EINVAL)),
415        Err(NE::EINTR) => continue,
416        _ => throw!(io::Error::last_os_error()),
417      }
418    }
419  }
420
421  fn nix2io(_n: nix::Error) -> io::Error {
422    io::Error::last_os_error()
423  }
424
425  impl Handle {
426    #[throws(AE)]
427    pub fn new() -> Self {
428      let (reading_end, _writing_end) = mkpipe()
429        .context("create cleanup notify pipe")?;
430      // we leak the writing end, keeping it open only in this process
431      Handle(reading_end)
432    }
433
434    #[throws(AE)]
435    pub fn arm_hook(&self, cmd: &mut Command) { unsafe {
436      use std::os::unix::process::CommandExt;
437
438      let notify_writing_end = self.0;
439      let all_signals = nix::sys::signal::SigSet::all();
440
441      cmd.pre_exec(move || -> Result<(), io::Error> {
442        let semidaemon = nix::unistd::getpid();
443        let (reading_end, writing_end) = mkpipe()?;
444
445        match fork().map_err(nix2io)? {
446          ForkResult::Child => {
447            let _ = catch_unwind(move || -> Void {
448              let _ = sigprocmask(
449                SigmaskHow::SIG_BLOCK,
450                Some(&all_signals),
451                None
452              );
453
454              let _ = close(writing_end);
455              let _ = nix::unistd::dup2(2, 1);
456
457              for fd in 2.. {
458                if fd == notify_writing_end { continue }
459                let r = close(fd);
460                if fd > writing_end && matches!(r, Err(NE::EBADF)) {
461                  break;
462                }                  
463              }
464              let _ = read_await(notify_writing_end);
465              let _ = kill(semidaemon, SIGTERM);
466              let _ = kill(semidaemon, SIGCONT);
467              _exit(0);
468            });
469            let _ = raise(SIGABRT);
470            _exit(127);
471          },
472          ForkResult::Parent{..} => {
473            // parent
474            close(writing_end).map_err(nix2io)?;
475            read_await(reading_end)?;
476          },
477        };
478
479        Ok(())
480      });
481    } }
482  }
483}
484
485// -------------------- generalised daemon startup --------------------
486
487#[throws(AE)]
488pub fn fork_something_which_prints(mut cmd: Command,
489                               cln: &cleanup_notify::Handle,
490                               what: &str)
491                               -> (String, Child)
492{
493  (||{
494    cmd.stdout(Stdio::piped());
495    cln.arm_hook(&mut cmd)?;
496    let mut child = cmd.spawn().context("spawn")?;
497    let mut report = BufReader::new(child.stdout.take().unwrap())
498      .lines().fuse();
499
500    let l = report.next();
501
502    let s = child.try_wait().context("check on spawned child")?;
503    if let Some(e) = s {
504      throw!(anyhow!("failed to start: wait status = {}", &e));
505    }
506
507    let l = match l {
508      Some(Ok(l)) => l,
509      None => throw!(anyhow!("EOF (but it's still running?")),
510      Some(Err(e)) => throw!(AE::from(e).context("failed to read")),
511    };
512
513    let what = what.to_owned();
514    thread::spawn(move|| (||{
515      for l in report {
516        let l: Result<String, io::Error> = l;
517        let l = l.context("reading further output")?;
518        const MAXLEN: usize = 300;
519        if l.len() <= MAXLEN {
520          println!("{} {}", what, l);
521        } else {
522          println!("{} {}...", what, &l[..MAXLEN-3]);
523        }
524      }
525      Ok::<_,AE>(())
526    })().context(what).just_warn()
527    );
528
529    Ok::<_,AE>((l, child))
530  })().with_context(|| what.to_owned())?
531}
532
533// ==================== principal actual setup code ====================
534
535pub type EarlyArgPredicate<'f> = &'f mut dyn FnMut(&OsStr) -> bool;
536
537#[throws(AE)]
538pub fn reinvoke_via_bwrap(_opts: &Opts, current_exe: &str,
539                          early: EarlyArgPredicate<'_>) -> Void {
540  debug!("running bwrap");
541  
542  let mut bcmd = Command::new("bwrap");
543  bcmd
544    .args("--unshare-net \
545           --dev-bind / / \
546           --tmpfs /tmp \
547           --die-with-parent".split(' '))
548    .arg(current_exe);
549
550  let (early, late) = {
551    let mut still_early = true;
552    env::args_os().skip(1)
553      .partition::<Vec<_>,_>(|s| {
554        still_early &= early(s);
555        still_early
556      })
557  };
558  bcmd.args(early);
559  bcmd.arg("--no-bwrap");
560  bcmd.args(late);
561
562  std::io::stdout().flush().context("flush stdout")?;
563  let e: AE = bcmd.exec().into();
564  throw!(e.context("exec bwrap"));
565}
566
567#[throws(AE)]
568pub fn prepare_tmpdir<'x>(opts: &'x Opts, mut current_exe: &'x str) -> DirSubst {
569  #[throws(AE)]
570  fn getcwd() -> String {
571    env::current_dir()
572      .context("getcwd")?
573      .to_str()
574      .ok_or_else(|| anyhow!("path is not UTF-8"))?
575      .to_owned()
576  }
577
578  if let Some(as_if) = &opts.as_if {
579    current_exe = as_if;
580  } else if let Some(test_name) = &opts.test_name {
581    current_exe = test_name;
582  }
583
584  let start_dir = getcwd()
585    .context("canonicalise our invocation directory (getcwd)")?;
586
587  (||{
588    match fs::metadata(&opts.tmp_dir) {
589      Ok(m) => {
590        if !m.is_dir() {
591          throw!(anyhow!("existing object is not a directory"));
592        }
593        if (m.st_mode() & 0o01002) != 0 {
594          throw!(anyhow!(
595            "existing directory mode {:#o} is sticky or world-writeable. \
596             We use predictable pathnames so that would be a tmp race",
597            m.st_mode()
598          ));
599        }
600      }
601      Err(e) if e.kind() == ErrorKind::NotFound => {
602        fs::create_dir(&opts.tmp_dir)
603          .context("create")?;
604      }
605      Err(e) => {
606        let e: AE = e.into();
607        throw!(e.context("stat existing directory"))
608      }
609    }
610
611    env::set_current_dir(&opts.tmp_dir)
612      .context("chdir into it")?;
613
614    Ok::<_,AE>(())
615  })()
616    .with_context(|| opts.tmp_dir.to_owned())
617    .context("prepare/create tmp-dir")?;
618
619  let leaf = current_exe.rsplitn(2, '/').next().unwrap();
620  let our_tmpdir = format!("{}/{}", &opts.tmp_dir, &leaf);
621  (||{
622    match fs::remove_dir_all(&leaf) {
623      Ok(()) => {},
624      Err(e) if e.kind() == ErrorKind::NotFound => {},
625      Err(e) => throw!(AE::from(e).context("remove previous directory")),
626    };
627
628    fs::DirBuilder::new().create(&leaf)
629      .context("create fresh subdirectory")?;
630
631    env::set_current_dir(&leaf)
632      .context("chdir into it")?;
633
634    Ok::<_,AE>(())
635  })()
636    .with_context(|| our_tmpdir.to_owned())
637    .context("prepare/create our tmp subdir")?;
638
639  let abstmp =
640    getcwd().context("canonicalise our tmp subdir (getcwd)")?;
641
642  env::set_var("HOME", &abstmp);
643  env::set_var("TMPDIR", &abstmp);
644  env::set_var("OTTER_APITEST_START_DIR", &start_dir);
645  for v in "http_proxy https_proxy XAUTHORITY CDPATH \
646            SSH_AGENT_PID SSH_AUTH_SOCK WINDOWID WWW_HOME".split(' ')
647  {
648    env::remove_var(v);
649  }
650
651  let manifest_var = "CARGO_MANIFEST_DIR";
652  let src: String = (|| Ok::<_,AE>(match env::var(manifest_var) {
653    Ok(dir) => dir,
654    Err(env::VarError::NotPresent) => start_dir.clone(),
655    e@ Err(_) => throw!(e.context(manifest_var).err().unwrap()),
656  }))()
657    .context("find source code")?;
658
659  DirSubst {
660    tmp: our_tmpdir,
661    abstmp,
662    src,
663    start_dir,
664  }
665}
666
667#[throws(AE)]
668pub fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
669                      -> (MgmtChannelForGame, Child) {
670  let config = ds.subst(r##"
671change_directory = "@abstmp@"
672base_dir = "@build@"
673public_url = "@url@"
674
675save_dir = "."
676command_socket = "@command_socket@"
677template_dir = "@src@/templates"
678specs_dir = "@src@/specs"
679nwtemplate_dir = "@src@/nwtemplates"
680bundled_sources = "@target@/bundled-sources"
681wasm_dir = "@target@/packed-wasm"
682shapelibs = [ "@src@/library/*.toml" ]
683libexec_dir = "@target@/debug"
684usvg_bin = "@target@/release/usvg"
685
686authorized_keys = "@abstmp@/authorized_keys"
687ssh_proxy_command = "@target@/debug/otter-ssh-proxy --config @abstmp@/server-config.toml"
688
689debug_js_inject_file = "@src@/templates/test-inject.js"
690check_bundled_sources = false # For testing only! see LICENCE!
691
692fake_rng = []
693fake_time = []
694
695[log]
696global_level = 'debug'
697
698[log.modules]
699
700'hyper::server' = 'info'
701"game::debugreader" = 'info'
702"otter::updates" = 'trace'
703"otter::hidden" = 'trace'
704"##)?;
705
706  fs::write(CONFIG, &config)
707    .context(CONFIG).context("create server config")?;
708
709  start_gameserver(cln, ds)?
710}
711
712#[throws(AE)]
713fn start_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
714                    -> (MgmtChannelForGame, Child) {
715  let server_exe = ds.subst("@target@/debug/daemon-otter")?;
716  let mut cmd = Command::new(&server_exe);
717  cmd
718    .arg("--report-startup")
719    .arg(CONFIG);
720
721  let child = (||{
722    let (l,child) = fork_something_which_prints(cmd, cln, &server_exe)?;
723    if l != DAEMON_STARTUP_REPORT {
724      throw!(anyhow!("otter-daemon startup report {:?}, expected {:?}",
725                     &l, DAEMON_STARTUP_REPORT));
726    }
727    Ok::<_,AE>(child)
728  })()
729    .context("game server")?;
730
731  let mut mgmt_conn = MgmtChannel::connect(
732    &ds.subst("@command_socket@")?
733  )?;
734
735  mgmt_conn.cmd(&MgmtCommand::SetSuperuser(true))?;
736  mgmt_conn.cmd(&MgmtCommand::SelectAccount("server:".parse()?))?;  
737
738  let mgmt_conn = mgmt_conn.for_game(
739    TABLE.parse()?,
740    MgmtGameUpdateMode::Online
741  );
742
743  (mgmt_conn, child)
744}
745
746impl SetupCore {
747  #[throws(AE)]
748  pub fn restart_gameserver(&mut self) {
749    let (mgmt_conn, child) = start_gameserver(&self.cln, &self.ds)?;
750    self.mgmt_conn = RefCell::new(mgmt_conn);
751    self.server_child = child;
752  }
753}
754
755// ---------- game spec ----------
756
757#[derive(Copy,Clone,Error,Debug)]
758#[error("wait status: {0}")]
759pub struct ExitStatusError(pub std::process::ExitStatus);
760
761#[derive(Debug)]
762pub struct OtterOutput {
763  output: Option<NamedTempFile>,
764}
765impl Deref for OtterOutput {
766  type Target = fs::File;
767  fn deref(&self) -> &fs::File { self.output.as_ref().unwrap().as_file() }
768}
769impl DerefMut for OtterOutput {
770  fn deref_mut(&mut self) -> &mut fs::File {
771    self.output.as_mut().unwrap().as_file_mut()
772  }
773}
774impl From<OtterOutput> for String {
775  fn from(mut oo: OtterOutput) -> String {
776    let mut s = String::new();
777    let mut o = oo.output.take().unwrap();
778    o.rewind().unwrap();
779    o.read_to_string(&mut s).unwrap();
780    s
781  }
782}
783impl From<&mut OtterOutput> for String {
784  fn from(oo: &mut OtterOutput) -> String {
785    let mut s = String::new();
786    let o = oo.output.as_mut().unwrap();
787    o.rewind().unwrap();
788    o.read_to_string(&mut s).unwrap();
789    s
790  }
791}
792impl Drop for OtterOutput {
793  fn drop(&mut self) {
794    if let Some(mut o) = self.output.take() {
795      io::copy(&mut o, &mut io::stdout()).expect("copy otter stdout");
796    }
797  }
798}
799
800pub trait OtterArgsSpec {
801  fn to_args(&self, ds: &dyn Substitutor) -> Vec<String>;
802}
803
804impl<S> OtterArgsSpec for [S] where for <'s> &'s S: Into<String> {
805  fn to_args(&self, _: &dyn Substitutor) -> Vec<String> {
806    self.iter().map(|s| s.into()).collect()
807  }
808}
809impl<S> OtterArgsSpec for Vec<S> where for <'s> &'s S: Into<String> {
810  fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
811    self.as_slice().to_args(ds)
812  }
813}
814impl OtterArgsSpec for &str {
815  fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
816    ds.ss(self).expect(self)
817  }
818}
819impl OtterArgsSpec for G<&str> {
820  fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
821    ds.gss(self.0).expect(self.0)
822  }
823}
824#[derive(Debug,Clone)]
825pub struct G<T>(pub T);
826
827impl DirSubst {
828  pub fn specs_dir(&self) -> String {
829    format!("{}/specs" , &self.src)
830  }
831
832  pub fn example_bundle(&self) -> String {
833    self.subst("@examples@/test-bundle.zip").unwrap()
834  }
835
836  #[throws(AE)]
837  pub fn otter(&self, xargs: &dyn OtterArgsSpec) -> OtterOutput
838  {
839    self.otter_prctx(&default(), xargs)?
840  }
841
842  #[throws(AE)]
843  pub fn otter_prctx(&self, prctx: &PathResolveContext,
844                     xargs: &dyn OtterArgsSpec)
845                     -> OtterOutput
846  {
847    let ds = self;
848    let exe = ds.subst("@target@/debug/otter")?;
849    let specs = self.subst("@src@/specs")?;
850    let mut args: Vec<String> = vec![];
851    args.push("--config"  .to_owned()); args.push(prctx.resolve(CONFIG));
852    args.push("--spec-dir".to_owned()); args.push(prctx.resolve(&specs) );
853    args.extend(xargs.to_args(ds));
854    let dbg = format!("running {} {:?}", &exe, &args);
855    let mut output = NamedTempFile::new_in(
856      ds.subst("@abstmp@").unwrap()
857    ).unwrap();
858    debug!("{}", &dbg);
859    (||{
860      let mut cmd = Command::new(&exe);
861      cmd.args(&args);
862      cmd.stdout(output.as_file().try_clone().unwrap());
863      let st = cmd
864        .spawn().context("spawn")?
865        .wait().context("wait")?;
866      if !st.success() {
867        throw!(ExitStatusError(st));
868      }
869      Ok::<_,AE>(())
870    })()
871      .context(dbg)
872      .context("run otter client")?;
873
874    output.rewind().unwrap();
875    OtterOutput { output: Some(output) }
876  }
877
878  #[throws(AE)]
879  pub fn game_spec_path(&self) -> String {
880    self.subst("@specs@/demo.game.toml")?
881  }
882
883  #[throws(AE)]
884  pub fn game_spec_data(&self) -> GameSpec {
885    let path = self.game_spec_path()?;
886    (||{
887      let data = fs::read(&path).context("read")?;
888      let data = std::str::from_utf8(&data).context("convert from UTF-8")?;
889      let data: toml::Value = data.parse().context("parse TOM")?;
890      dbgc!(&data);
891      let data = toml_de::from_value(&data).context("interperet TOML")?;
892      Ok::<_,AE>(data)
893    })()
894      .context(path)
895      .context("game spec")?
896  }
897}
898
899#[throws(AE)]
900pub fn prepare_game(ds: &DirSubst, prctx: &PathResolveContext, table: &str)
901                    -> InstanceName {
902  let game_spec = ds.game_spec_path()?;
903  let subst = ds.also(&[
904    ("table",     table.to_owned()),
905    ("game_spec", prctx.resolve(&game_spec)),
906  ]);
907  ds.otter_prctx(prctx, &subst.ss(
908    "--account server: --game @table@                   \
909     reset                                              \
910     --reset-table @specs@/test.table.toml              \
911                   @game_spec@                          \
912    ")?).context("reset table")?;
913
914  let instance: InstanceName = table.parse()
915    .with_context(|| table.to_owned())
916    .context("parse table name")?;
917
918  instance
919}
920
921// ==================== post-setup facilities ====================
922
923// -------------------- static users --------------------
924
925pub struct StaticUserSetup {
926  pub nick: &'static str,
927  pub url: String,
928  pub player: PlayerId,
929}
930
931impl DirSubst {
932  #[throws(AE)]
933  pub fn setup_static_users(&self, mgmt_conn: &mut MgmtChannelForGame,
934                            layout: PresentationLayout)
935     -> Vec<StaticUserSetup>
936  {
937    #[throws(AE)]
938    fn mk(su: &DirSubst, mgmt_conn: &mut MgmtChannelForGame,
939          layout: PresentationLayout, u: StaticUser)
940          -> StaticUserSetup
941    {
942      let nick: &str = u.into();
943      let token = u.get_str("Token").expect("StaticUser missing Token");
944      let pl = AbbrevPresentationLayout(layout).to_string();
945      let subst = su.also([
946        ("nick",  nick),
947        ("token", token),
948        ("pl",    &pl),
949      ].iter());
950
951      su.otter(&subst
952                  .ss("--super -g@table@             \
953                       --account server:@nick@       \
954                       --fixed-token @token@         \
955                       join-game")?)?;
956
957      let player = mgmt_conn.has_player(
958        &subst.subst("server:@nick@")?.parse()?
959      )?.unwrap().0;
960
961      let url = subst.subst("@url@/@pl@?@token@")?;
962      StaticUserSetup { nick, url, player }
963    }
964
965    StaticUser::iter().map(
966      |u| (||{
967        let ssu = mk(self, mgmt_conn, layout, u).context("create")?;
968        Ok::<_,AE>(ssu)
969      })()
970        .with_context(|| format!("{:?}", u))
971        .context("make static user")
972    )
973      .collect::<Result<Vec<StaticUserSetup>,AE>>()?
974  }
975}
976
977// -------------------- concurrency management --------------------
978
979pub struct OtterPauseable(nix::unistd::Pid);
980pub struct OtterPaused(nix::unistd::Pid);
981
982impl SetupCore {
983  pub fn otter_pauseable(&self) -> OtterPauseable {
984    OtterPauseable(nix::unistd::Pid::from_raw(
985      self.server_child.id() as nix::libc::pid_t
986    ))
987  }
988
989  #[throws(AE)]
990  pub fn pause_otter(&self) -> OtterPaused {
991    self.otter_pauseable().pause()?
992  }
993
994  pub fn mgmt_conn<'m>(&'m self) -> RefMut<'m, MgmtChannelForGame> {
995    self.mgmt_conn.borrow_mut()
996  }
997}
998
999impl OtterPauseable {
1000  #[throws(AE)]
1001  pub fn pause(self) -> OtterPaused {
1002    nix::sys::signal::kill(self.0, nix::sys::signal::SIGSTOP)?;
1003    OtterPaused(self.0)
1004  }
1005}
1006
1007impl OtterPaused {
1008  #[throws(AE)]
1009  pub fn resume(self) -> OtterPauseable {
1010    nix::sys::signal::kill(self.0, nix::sys::signal::SIGCONT)?;
1011    OtterPauseable(self.0)
1012  }
1013}
1014
1015impl Drop for OtterPaused {
1016  fn drop(&mut self) {
1017    debug!("note, otter server pid={} was still paused", self.0);
1018  }
1019}
1020
1021// -------------------- utilities --------------------
1022
1023#[ext(pub)]
1024impl MgmtChannel {
1025  #[throws(AE)]
1026  fn game_synch(&mut self, game: InstanceName) -> Generation {
1027    let cmd = MgmtCommand::AlterGame {
1028      how: MgmtGameUpdateMode::Online,
1029      insns: vec![ MgmtGameInstruction::SynchLog ],
1030      game
1031    };
1032    let gen = if_chain!{
1033      let resp = self.cmd(&cmd)?;
1034      if let MgmtResponse::AlterGame {
1035        error: None,
1036        ref responses
1037      } = resp;
1038      if let [MgmtGameResponse::Synch(gen)] = responses[..];
1039      then { gen }
1040      else { throw!(anyhow!("unexpected resp to synch {:?}", resp)) }
1041    };
1042    trace!("gen={} ...", gen);
1043    gen
1044  }
1045
1046  fn fakerng_load(&mut self, values: &[&dyn ToString]) -> Result<(),AE> {
1047    let values = values.iter().map(|v| v.to_string()).collect();
1048    self.cmd(&MC::LoadFakeRng(values))?;
1049    Ok(())
1050  }
1051  fn fakerng_unfake(&mut self) -> Result<(),AE> {
1052    self.cmd(&MC::LoadFakeRng(vec![]))?;
1053    Ok(())
1054  }
1055}
1056
1057impl SetupCore {
1058  #[throws(AE)]
1059  pub fn initial_id_by_desc_glob(&mut self, desc_glob: &str) -> PieceId {
1060    let pieces = self.initial_pieces_cache.get_or_insert_with(||{
1061      self.mgmt_conn.borrow_mut().list_pieces().unwrap().0
1062    });
1063
1064    let glob = glob::Pattern::new(desc_glob).unwrap();
1065    pieces.iter().filter_map(|mgpi| {
1066
1067      let vis = mgpi.visible.as_ref()?;
1068      glob.matches(vis.desc_html.as_html_str()).then(||())?;
1069      Some(mgpi.piece)
1070
1071    }).exactly_one().map_err(|_| anyhow!("not exactly one {:?}", desc_glob))?
1072  }
1073
1074  #[throws(AE)]
1075  pub fn initial_vpid_by_desc_glob(&mut self, desc_glob: &str) -> String {
1076    let id = self.initial_id_by_desc_glob(desc_glob)?;
1077    VisiblePieceId::from(id.data()).to_string()
1078  }
1079}
1080
1081// ==================== core entrypoint, for wdriver too ====================
1082
1083#[throws(AE)]
1084pub fn setup_core<O>(module_paths: &[&str]) ->
1085  (O, Instance, SetupCore)
1086  where O: StructOpt + AsRef<Opts>
1087{
1088  let mut builder = env_logger::Builder::new();
1089  builder
1090    .format_timestamp_micros()
1091    .format_level(true);
1092  for too_verbose in &[
1093    "html5ever::tokenizer",
1094    "html5ever::tree_builder",
1095    "selectors::matching",
1096    "hyper::proto::h1",
1097    "hyper::client::pool",
1098  ] {
1099    builder.filter_module(too_verbose, log::LevelFilter::Info);
1100  }
1101
1102  for module in module_paths {
1103    builder
1104      .filter_module(module, log::LevelFilter::Debug);
1105  }
1106
1107  builder
1108    .filter_level(log::LevelFilter::Debug)
1109    .parse_env("OTTER_TEST_LOG")
1110    .init();
1111  debug!("starting");
1112
1113  let caller_opts = O::from_args();
1114  let opts = caller_opts.as_ref();
1115
1116  let current_exe: String = env::current_exe()
1117    .context("find current executable")?
1118    .to_str()
1119    .ok_or_else(|| anyhow!("current executable path is not UTF-8 !"))?
1120    .to_owned();
1121
1122  if !opts.no_bwrap {
1123    reinvoke_via_bwrap(
1124      opts, &current_exe,
1125      &mut |s: &OsStr| s.to_str().unwrap().starts_with("--test=")
1126    )
1127      .context("reinvoke via bwrap")?;
1128  }
1129
1130  info!("pid = {}", nix::unistd::getpid());
1131  sleep(opts.pause.into());
1132
1133  let cln = cleanup_notify::Handle::new()?;
1134  let ds = prepare_tmpdir(opts, &current_exe)?;
1135
1136  let (mgmt_conn, server_child) =
1137    prepare_gameserver(&cln, &ds).did("setup game server")?;
1138
1139  let instance_name =
1140    prepare_game(&ds, &default(), TABLE).context("setup game")?;
1141
1142  let wanted_tests = opts.tests.track();
1143
1144  (caller_opts,
1145   Instance(
1146     instance_name
1147   ),
1148   SetupCore {
1149     ds, cln,
1150     mgmt_conn: mgmt_conn.into(),
1151     server_child,
1152     wanted_tests,
1153     initial_pieces_cache: None,
1154   })
1155}
1156
1157pub struct PortmanteauMember {
1158  pub path: &'static str,
1159  pub f: fn() -> Result<(), Explode>,
1160}
1161inventory::collect!(PortmanteauMember);
1162
1163#[macro_export]
1164macro_rules! portmanteau_has {
1165  ($path:literal, $mod:ident) => {
1166    #[path = $path] mod $mod;
1167    inventory::submit!(PortmanteauMember { path: $path, f: $mod::main });
1168  }
1169}
1170
1171#[throws(AE)]
1172pub fn portmanteau_main(prefix: &str){
1173  let arg = 'arg: loop {
1174    for (ai, s) in env::args().enumerate() {
1175      let plausible = |s: &str| s.starts_with(&format!("{}-",prefix));
1176
1177      break 'arg if ai == 0 {
1178        let s = s.rsplitn(2,'/').next().unwrap();
1179        if ! plausible(s) { continue }
1180        s
1181      } else {
1182        let s = s.strip_prefix("--test=")
1183          .expect(&format!(
1184            "found non-long-option looking for --test={}-*: {:?}",
1185            prefix, s));
1186        if ! plausible(s) {
1187          panic!("found non --no-bwrap --{}-* option looking for --{}-*",
1188                 prefix,prefix);
1189        }
1190        s
1191      }.to_owned();
1192    }
1193    panic!("ran out of options looking for --test={}-*", prefix);
1194  };
1195
1196  let f = inventory::iter::<PortmanteauMember>.into_iter()
1197    .find_map(|pm| {
1198      let n = pm.path.strip_suffix(".rs").unwrap();
1199      if n == arg { Some(pm.f) } else { None }
1200    })
1201    .expect("unrecognosed {wdt,at}-* portanteau member");
1202
1203  f()?;
1204  info!("ok");
1205}