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