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