chiark / gitweb /
changelog: document further make-release changes
[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   pub initial_pieces_cache: Option<Vec<MgmtGamePieceInfo>>,
93 }
94
95 #[derive(Clone,Debug)]
96 pub struct DirSubst {
97   pub tmp: String,
98   pub abstmp: String,
99   pub start_dir: String,
100   pub src: String,
101 }
102
103 pub struct Instance(pub InstanceName);
104
105 // ==================== Facilities for tests ====================
106
107 impl AsRef<Opts> for Opts { fn as_ref(&self) -> &Opts { self } }
108
109 #[derive(Debug)]
110 pub enum Explode { }
111 impl<'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 }
123 impl From<Explode> for anyhow::Error {
124   fn from(e: Explode) -> AE { match e { } }
125 }
126 #[ext(pub, name=ResultExplodeExt)]
127 impl<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 /*
135 impl<E:Error> From<Explode> for E {
136   fn from(e: Explode) -> E { match e { } }
137 }*/
138
139 #[ext(pub)]
140 impl 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
186 pub 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)]
231 pub struct Subst(HashMap<String,String>);
232
233 impl Substitutor for Subst {
234   fn get(&self, kw: &str) -> Option<String> {
235     self.0.get(kw).map(String::clone)
236   }
237 }
238
239 impl<'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)]
253 pub struct ExtendedSubst<B: Substitutor, X: Substitutor>(B, X);
254
255 impl<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
261 impl 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)]
282 pub struct WantedTestsOpt {
283   tests: Vec<String>,
284 }
285
286 #[derive(Debug)]
287 pub struct TrackWantedTests {
288   wanted: WantedTestsOpt,
289   found: BTreeSet<String>,
290 }
291
292 impl WantedTestsOpt {
293   pub fn track(&self) -> TrackWantedTests {
294     TrackWantedTests { wanted: self.clone(), found: default() }
295   }
296 }
297
298 impl 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
308 impl 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]
326 macro_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]
337 macro_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
351 pub trait PropagateDid {
352   fn propagate_did<T>(self, msg: &'static str) -> anyhow::Result<T>;
353 }
354
355 #[ext(pub, name=ResultGenDidExt)]
356 impl<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)]
367 impl<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
383 pub 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)]
488 pub 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
535 pub type EarlyArgPredicate<'f> = &'f mut dyn FnMut(&OsStr) -> bool;
536
537 #[throws(AE)]
538 pub 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)]
568 pub 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)]
668 pub fn prepare_gameserver(cln: &cleanup_notify::Handle, ds: &DirSubst)
669                       -> (MgmtChannelForGame, Child) {
670   let config = ds.subst(r##"
671 change_directory = "@abstmp@"
672 base_dir = "@build@"
673 public_url = "@url@"
674
675 save_dir = "."
676 command_socket = "@command_socket@"
677 template_dir = "@src@/templates"
678 specs_dir = "@src@/specs"
679 nwtemplate_dir = "@src@/nwtemplates"
680 bundled_sources = "@target@/bundled-sources"
681 wasm_dir = "@target@/packed-wasm"
682 shapelibs = [ "@src@/library/*.toml" ]
683 libexec_dir = "@target@/debug"
684 usvg_bin = "@target@/release/usvg"
685
686 authorized_keys = "@abstmp@/authorized_keys"
687 ssh_proxy_command = "@target@/debug/otter-ssh-proxy --config @abstmp@/server-config.toml"
688
689 debug_js_inject_file = "@src@/templates/test-inject.js"
690 check_bundled_sources = false # For testing only! see LICENCE!
691
692 fake_rng = []
693 fake_time = []
694
695 [log]
696 global_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)]
713 fn 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
746 impl 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}")]
759 pub struct ExitStatusError(pub std::process::ExitStatus);
760
761 #[derive(Debug)]
762 pub struct OtterOutput {
763   output: Option<NamedTempFile>,
764 }
765 impl Deref for OtterOutput {
766   type Target = fs::File;
767   fn deref(&self) -> &fs::File { self.output.as_ref().unwrap().as_file() }
768 }
769 impl DerefMut for OtterOutput {
770   fn deref_mut(&mut self) -> &mut fs::File {
771     self.output.as_mut().unwrap().as_file_mut()
772   }
773 }
774 impl 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 }
783 impl 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 }
792 impl 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
800 pub trait OtterArgsSpec {
801   fn to_args(&self, ds: &dyn Substitutor) -> Vec<String>;
802 }
803
804 impl<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 }
809 impl<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 }
814 impl OtterArgsSpec for &str {
815   fn to_args(&self, ds: &dyn Substitutor) -> Vec<String> {
816     ds.ss(self).expect(self)
817   }
818 }
819 impl 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)]
825 pub struct G<T>(pub T);
826
827 impl 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)]
900 pub 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
925 pub struct StaticUserSetup {
926   pub nick: &'static str,
927   pub url: String,
928   pub player: PlayerId,
929 }
930
931 impl 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
979 pub struct OtterPauseable(nix::unistd::Pid);
980 pub struct OtterPaused(nix::unistd::Pid);
981
982 impl 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
999 impl 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
1007 impl 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
1015 impl 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)]
1024 impl 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
1057 impl 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)]
1084 pub 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
1157 pub struct PortmanteauMember {
1158   pub path: &'static str,
1159   pub f: fn() -> Result<(), Explode>,
1160 }
1161 inventory::collect!(PortmanteauMember);
1162
1163 #[macro_export]
1164 macro_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)]
1172 pub 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 }