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