Skip to main content

expectation_service/
lib.rs

1//! External things a test needs: a server, a database, a container. The suite names
2//! what it needs and the harness starts it, once for the run, in the order the tests ask, stops it
3//! when the run is over, and starts it again if it died on the way. What cannot be had on this
4//! machine is not a failure: the tests that need it are "not supported yet", with how to get it.
5//!
6//! A [`Service`] says whether it can run here ([`Service::unavailable`]) and how to start
7//! ([`Service::start`]), which gives a [`Running`] that stops when it is dropped. There are
8//! providers for a [`Command`] (a program, ready when a port answers) and a [`Docker`] container
9//! (built from a directory if its image is not there), and a [`Fallback`] takes the first of
10//! several that is available: a native build, else a container. A database is a container.
11//!
12//! No dependencies beyond the standard library.
13
14use std::collections::BTreeMap;
15use std::net::{SocketAddr, TcpStream};
16use std::process::{Child, Stdio};
17use std::time::{Duration, Instant};
18
19/// Something that can be started for a test run.
20pub trait Service: Send {
21    /// What the tests call it.
22    fn name(&self) -> &str;
23
24    /// Why it cannot run on this machine, and how to get it so that it can; `None` when it can.
25    fn unavailable(&self) -> Option<String> {
26        None
27    }
28
29    /// Starts it and waits until it is ready.
30    fn start(&self) -> Result<Box<dyn Running>, String>;
31}
32
33/// A started service. Dropping it stops it.
34pub trait Running: Send {
35    /// Whether it is still up.
36    fn alive(&mut self) -> bool {
37        true
38    }
39
40    /// Where to find it: `"port"`, `"pid"`, `"container"`, whatever the service tells.
41    fn endpoint(&self, key: &str) -> Option<String> {
42        let _ = key;
43        None
44    }
45}
46
47/// The services of a run: those that are known, and those that are up.
48#[derive(Default)]
49pub struct Services {
50    known: BTreeMap<String, Box<dyn Service>>,
51    up: Vec<(String, Box<dyn Running>)>,
52}
53
54impl Services {
55    pub fn new() -> Self {
56        Services::default()
57    }
58
59    /// Makes a service known (by its name), without starting it.
60    pub fn add(&mut self, service: impl Service + 'static) {
61        self.known.insert(service.name().to_string(), Box::new(service));
62    }
63
64    pub fn with(mut self, service: impl Service + 'static) -> Self {
65        self.add(service);
66        self
67    }
68
69    /// Whether anything is known.
70    pub fn is_empty(&self) -> bool {
71        self.known.is_empty()
72    }
73
74    /// The reason each of `names` cannot be had here, one line each: empty when all can.
75    pub fn missing(&self, names: &[&str]) -> Vec<String> {
76        names
77            .iter()
78            .filter_map(|n| match self.known.get(*n) {
79                None => Some(format!("{n}: no such service is set up for this run")),
80                Some(s) => s.unavailable().map(|why| format!("{n}: {why}")),
81            })
82            .collect()
83    }
84
85    /// Makes sure each of `names` is up, in that order: one that is up stays, one that died is
86    /// started again. The error says what cannot be had, or what failed to start.
87    pub fn start(&mut self, names: &[&str]) -> Result<(), String> {
88        let missing = self.missing(names);
89        if !missing.is_empty() {
90            return Err(missing.join("; "));
91        }
92        for name in names {
93            if let Some(i) = self.up.iter().position(|(n, _)| n == name) {
94                if self.up[i].1.alive() {
95                    continue;
96                }
97                self.up.remove(i);
98            }
99            let service = &self.known[*name];
100            let running = service.start().map_err(|e| format!("{name} would not start: {e}"))?;
101            self.up.push((name.to_string(), running));
102        }
103        Ok(())
104    }
105
106    /// Starts these afresh: stopped if they are up, then started. For a service that keeps state
107    /// a test must not find (a database the last test wrote to).
108    pub fn restart(&mut self, names: &[&str]) -> Result<(), String> {
109        for name in names {
110            if let Some(i) = self.up.iter().position(|(n, _)| n == name) {
111                self.up.remove(i);
112            }
113        }
114        self.start(names)
115    }
116
117    /// Where a running service says it is (see [`Running::endpoint`]).
118    pub fn endpoint(&self, name: &str, key: &str) -> Option<String> {
119        self.up.iter().find(|(n, _)| n == name)?.1.endpoint(key)
120    }
121
122    /// Stops everything, the last started first (what depends on a database stops before it).
123    pub fn stop_all(&mut self) {
124        while self.up.pop().is_some() {}
125    }
126}
127
128impl Drop for Services {
129    fn drop(&mut self) {
130        self.stop_all();
131    }
132}
133
134/// How to tell that a started service is ready to be used.
135#[derive(Clone, Debug)]
136pub enum Ready {
137    /// As soon as it has started.
138    Started,
139    /// When something accepts a connection on this port of this machine.
140    Tcp(u16),
141    /// After this long.
142    After(Duration),
143}
144
145fn wait_until_ready(name: &str, ready: &Ready, patience: Duration, mut still_up: impl FnMut() -> bool) -> Result<(), String> {
146    match ready {
147        Ready::Started => Ok(()),
148        Ready::After(d) => {
149            std::thread::sleep(*d);
150            if still_up() { Ok(()) } else { Err(format!("{name} exited while it was starting")) }
151        }
152        Ready::Tcp(port) => {
153            let addr = SocketAddr::from(([127, 0, 0, 1], *port));
154            let start = Instant::now();
155            loop {
156                if TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok() {
157                    return Ok(());
158                }
159                if !still_up() {
160                    return Err(format!("{name} exited before port {port} answered"));
161                }
162                if start.elapsed() > patience {
163                    return Err(format!("{name}: nothing answered on port {port} within {} s", patience.as_secs_f32()));
164                }
165                std::thread::sleep(Duration::from_millis(100));
166            }
167        }
168    }
169}
170
171/// Whether something accepts a connection on `port` of this machine.
172fn port_answers(port: u16) -> bool {
173    TcpStream::connect_timeout(&SocketAddr::from(([127, 0, 0, 1], port)), Duration::from_millis(200)).is_ok()
174}
175
176/// Whether `program` can be run: a path that exists, or a name found on `PATH`.
177fn installed(program: &str) -> bool {
178    let p = std::path::Path::new(program);
179    if p.components().count() > 1 {
180        return p.exists();
181    }
182    std::env::var_os("PATH").is_some_and(|path| std::env::split_paths(&path).any(|d| d.join(program).is_file()))
183}
184
185/// A program run for the duration of a test run.
186#[derive(Clone)]
187pub struct Command {
188    name: String,
189    program: String,
190    args: Vec<String>,
191    dir: Option<std::path::PathBuf>,
192    env: Vec<(String, String)>,
193    ready: Ready,
194    patience: Duration,
195    hint: Option<String>,
196}
197
198impl Command {
199    pub fn new(name: &str, program: &str) -> Self {
200        Command {
201            name: name.into(),
202            program: program.into(),
203            args: vec![],
204            dir: None,
205            env: vec![],
206            ready: Ready::Started,
207            patience: Duration::from_secs(30),
208            hint: None,
209        }
210    }
211
212    pub fn arg(mut self, arg: &str) -> Self {
213        self.args.push(arg.into());
214        self
215    }
216
217    pub fn args<I: IntoIterator<Item = S>, S: AsRef<str>>(mut self, args: I) -> Self {
218        self.args.extend(args.into_iter().map(|a| a.as_ref().to_string()));
219        self
220    }
221
222    /// Runs in this directory (a program that keeps its files where it runs).
223    pub fn dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
224        self.dir = Some(dir.into());
225        self
226    }
227
228    pub fn env(mut self, key: &str, value: &str) -> Self {
229        self.env.push((key.into(), value.into()));
230        self
231    }
232
233    pub fn ready(mut self, ready: Ready) -> Self {
234        self.ready = ready;
235        self
236    }
237
238    /// How long to wait for it to be ready.
239    pub fn patience(mut self, patience: Duration) -> Self {
240        self.patience = patience;
241        self
242    }
243
244    /// What to say when the program is not there: how to get it.
245    pub fn hint(mut self, hint: &str) -> Self {
246        self.hint = Some(hint.into());
247        self
248    }
249
250    /// The program that is run.
251    pub fn program(&self) -> &str {
252        &self.program
253    }
254
255    /// Its arguments.
256    pub fn argv(&self) -> &[String] {
257        &self.args
258    }
259
260    /// The directory it runs in, if it has one of its own.
261    pub fn working_dir(&self) -> Option<&std::path::Path> {
262        self.dir.as_deref()
263    }
264}
265
266impl Service for Command {
267    fn name(&self) -> &str {
268        &self.name
269    }
270
271    fn unavailable(&self) -> Option<String> {
272        if installed(&self.program) {
273            return None;
274        }
275        Some(match &self.hint {
276            Some(h) => format!("{} is not installed: {h}", self.program),
277            None => format!("{} is not installed", self.program),
278        })
279    }
280
281    fn start(&self) -> Result<Box<dyn Running>, String> {
282        if let Ready::Tcp(port) = self.ready {
283            if port_answers(port) {
284                return Err(format!("{}: port {port} is already in use (a leftover of an earlier run? stop it first)", self.name));
285            }
286        }
287        let mut cmd = std::process::Command::new(&self.program);
288        cmd.args(&self.args).envs(self.env.iter().map(|(k, v)| (k, v))).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null());
289        if let Some(dir) = &self.dir {
290            std::fs::create_dir_all(dir).map_err(|e| format!("cannot make {}: {e}", dir.display()))?;
291            cmd.current_dir(dir);
292        }
293        let mut child = cmd.spawn().map_err(|e| format!("cannot run {}: {e}", self.program))?;
294        let name = self.name.clone();
295        let port = if let Ready::Tcp(p) = self.ready { Some(p) } else { None };
296        let up = |c: &mut Child| c.try_wait().map(|s| s.is_none()).unwrap_or(false);
297        let waited = wait_until_ready(&self.name, &self.ready, self.patience, || up(&mut child));
298        if let Err(e) = waited {
299            let exited = child.try_wait().ok().flatten();
300            let _ = child.kill();
301            let _ = child.wait();
302            return Err(match exited {
303                Some(status) => format!("{name} exited ({status}) before it was ready"),
304                None => e,
305            }
306            .replace("exited while it was starting", "exited before it was ready"));
307        }
308        Ok(Box::new(Process { child, port }))
309    }
310}
311
312struct Process {
313    child: Child,
314    port: Option<u16>,
315}
316
317impl Running for Process {
318    fn alive(&mut self) -> bool {
319        self.child.try_wait().map(|s| s.is_none()).unwrap_or(false)
320    }
321
322    fn endpoint(&self, key: &str) -> Option<String> {
323        match key {
324            "pid" => Some(self.child.id().to_string()),
325            "port" => self.port.map(|p| p.to_string()),
326            _ => None,
327        }
328    }
329}
330
331impl Drop for Process {
332    fn drop(&mut self) {
333        let _ = self.child.kill();
334        let _ = self.child.wait();
335    }
336}
337
338/// Something that must be there but is not ours to start: a build of a program, a data file. It
339/// is available when it exists; starting it is nothing, and it is at its `path`.
340pub struct Present {
341    name: String,
342    path: std::path::PathBuf,
343    hint: Option<String>,
344}
345
346impl Present {
347    pub fn new(name: &str, path: &std::path::Path) -> Self {
348        Present { name: name.into(), path: path.to_path_buf(), hint: None }
349    }
350
351    /// How to get it.
352    pub fn hint(mut self, hint: &str) -> Self {
353        self.hint = Some(hint.into());
354        self
355    }
356}
357
358impl Service for Present {
359    fn name(&self) -> &str {
360        &self.name
361    }
362
363    fn unavailable(&self) -> Option<String> {
364        if self.path.exists() {
365            return None;
366        }
367        Some(match &self.hint {
368            Some(h) => format!("{} is not there: {h}", self.path.display()),
369            None => format!("{} is not there", self.path.display()),
370        })
371    }
372
373    fn start(&self) -> Result<Box<dyn Running>, String> {
374        match self.unavailable() {
375            Some(why) => Err(why),
376            None => Ok(Box::new(Here(self.path.clone()))),
377        }
378    }
379}
380
381struct Here(std::path::PathBuf);
382
383impl Running for Here {
384    fn alive(&mut self) -> bool {
385        self.0.exists()
386    }
387
388    fn endpoint(&self, key: &str) -> Option<String> {
389        (key == "path").then(|| self.0.display().to_string())
390    }
391}
392
393/// A Docker container: its image is built from a directory when it is not there, it runs
394/// detached, and it is removed when it stops. A database for the tests is one of these.
395#[derive(Clone)]
396pub struct Docker {
397    name: String,
398    image: String,
399    container: String,
400    build_dir: Option<String>,
401    ports: Vec<(u16, u16)>,
402    env: Vec<(String, String)>,
403    args: Vec<String>,
404    ready: Ready,
405    patience: Duration,
406    program: String,
407}
408
409impl Docker {
410    pub fn new(name: &str, image: &str) -> Self {
411        Docker {
412            name: name.into(),
413            image: image.into(),
414            container: format!("expectation-{name}"),
415            build_dir: None,
416            ports: vec![],
417            env: vec![],
418            args: vec![],
419            ready: Ready::Started,
420            patience: Duration::from_secs(60),
421            program: "docker".into(),
422        }
423    }
424
425    /// The container's name, which a stale one of the same name is removed by.
426    pub fn container(mut self, name: &str) -> Self {
427        self.container = name.into();
428        self
429    }
430
431    /// Where the image is built from, if it is not there.
432    pub fn build_from(mut self, dir: &str) -> Self {
433        self.build_dir = Some(dir.into());
434        self
435    }
436
437    /// Publishes the container's `inside` port as `outside` on this machine.
438    pub fn port(mut self, inside: u16, outside: u16) -> Self {
439        self.ports.push((inside, outside));
440        self
441    }
442
443    pub fn env(mut self, key: &str, value: &str) -> Self {
444        self.env.push((key.into(), value.into()));
445        self
446    }
447
448    /// An argument for the image's command, after the image.
449    pub fn arg(mut self, arg: &str) -> Self {
450        self.args.push(arg.into());
451        self
452    }
453
454    pub fn ready(mut self, ready: Ready) -> Self {
455        self.ready = ready;
456        self
457    }
458
459    pub fn patience(mut self, patience: Duration) -> Self {
460        self.patience = patience;
461        self
462    }
463
464    /// Which program is Docker (for tests, and for `podman`).
465    pub fn docker_program(mut self, program: &str) -> Self {
466        self.program = program.into();
467        self
468    }
469
470    /// The arguments of `docker run`.
471    pub fn run_args(&self) -> Vec<String> {
472        let mut a: Vec<String> = ["run", "-d", "--rm", "--name"].iter().map(|s| s.to_string()).collect();
473        a.push(self.container.clone());
474        for (inside, outside) in &self.ports {
475            a.extend(["-p".to_string(), format!("{outside}:{inside}")]);
476        }
477        for (k, v) in &self.env {
478            a.extend(["-e".to_string(), format!("{k}={v}")]);
479        }
480        a.push(self.image.clone());
481        a.extend(self.args.iter().cloned());
482        a
483    }
484
485    /// The arguments of `docker build`, when there is a directory to build from.
486    pub fn build_args(&self) -> Option<Vec<String>> {
487        let dir = self.build_dir.as_ref()?;
488        Some(["build", "-q", "-t", &self.image, dir].iter().map(|s| s.to_string()).collect())
489    }
490
491    fn docker(&self, args: &[String]) -> std::io::Result<std::process::Output> {
492        std::process::Command::new(&self.program).args(args).stdin(Stdio::null()).output()
493    }
494
495    fn daemon_up(&self) -> bool {
496        self.docker(&["info".to_string()]).is_ok_and(|o| o.status.success())
497    }
498
499    /// Docker Desktop on a Mac does not run until it is opened: open it and wait for it.
500    fn wake_daemon(&self) -> Result<(), String> {
501        if self.daemon_up() {
502            return Ok(());
503        }
504        if cfg!(target_os = "macos") {
505            let _ = std::process::Command::new("open").args(["-ga", "Docker"]).status();
506        }
507        let start = Instant::now();
508        while start.elapsed() < Duration::from_secs(120) {
509            if self.daemon_up() {
510                return Ok(());
511            }
512            std::thread::sleep(Duration::from_secs(2));
513        }
514        Err("the Docker daemon is not running".into())
515    }
516}
517
518impl Service for Docker {
519    fn name(&self) -> &str {
520        &self.name
521    }
522
523    fn unavailable(&self) -> Option<String> {
524        if installed(&self.program) {
525            return None;
526        }
527        Some("Docker is not installed: install Docker Desktop (https://www.docker.com/products/docker-desktop/)".into())
528    }
529
530    fn start(&self) -> Result<Box<dyn Running>, String> {
531        self.wake_daemon()?;
532        let have_image = self.docker(&["image".into(), "inspect".into(), self.image.clone()]).is_ok_and(|o| o.status.success());
533        if !have_image {
534            let build = self.build_args().ok_or_else(|| format!("the image {} is not built, and there is nothing to build it from", self.image))?;
535            let out = self.docker(&build).map_err(|e| format!("docker build: {e}"))?;
536            if !out.status.success() {
537                return Err(format!("docker build failed: {}", String::from_utf8_lossy(&out.stderr).trim()));
538            }
539        }
540        // A container of the same name from an earlier run would stop this one starting.
541        let _ = self.docker(&["rm".into(), "-f".into(), self.container.clone()]);
542        if let Some((_, outside)) = self.ports.first().filter(|_| matches!(self.ready, Ready::Tcp(_))) {
543            if port_answers(*outside) {
544                return Err(format!("{}: port {outside} is already in use (a leftover of an earlier run? stop it first)", self.name));
545            }
546        }
547        let out = self.docker(&self.run_args()).map_err(|e| format!("docker run: {e}"))?;
548        if !out.status.success() {
549            return Err(format!("docker run failed: {}", String::from_utf8_lossy(&out.stderr).trim()));
550        }
551        let container = self.container.clone();
552        let program = self.program.clone();
553        let running = Container { program, container: container.clone(), port: self.ports.first().map(|p| p.1) };
554        let up = || running.is_up();
555        wait_until_ready(&self.name, &self.ready, self.patience, up)?;
556        Ok(Box::new(running))
557    }
558}
559
560struct Container {
561    program: String,
562    container: String,
563    port: Option<u16>,
564}
565
566impl Container {
567    fn is_up(&self) -> bool {
568        std::process::Command::new(&self.program)
569            .args(["inspect", "-f", "{{.State.Running}}", &self.container])
570            .stdin(Stdio::null())
571            .stderr(Stdio::null())
572            .output()
573            .is_ok_and(|o| String::from_utf8_lossy(&o.stdout).trim() == "true")
574    }
575}
576
577impl Running for Container {
578    fn alive(&mut self) -> bool {
579        self.is_up()
580    }
581
582    fn endpoint(&self, key: &str) -> Option<String> {
583        match key {
584            "container" => Some(self.container.clone()),
585            "port" => self.port.map(|p| p.to_string()),
586            _ => None,
587        }
588    }
589}
590
591impl Drop for Container {
592    fn drop(&mut self) {
593        let _ = std::process::Command::new(&self.program).args(["rm", "-f", &self.container]).stdout(Stdio::null()).stderr(Stdio::null()).status();
594    }
595}
596
597/// One service, made of several ways to have it: the first that can run here runs.
598pub struct Fallback {
599    name: String,
600    ways: Vec<Box<dyn Service>>,
601}
602
603impl Fallback {
604    pub fn new(name: &str, ways: Vec<Box<dyn Service>>) -> Self {
605        Fallback { name: name.into(), ways }
606    }
607}
608
609impl Service for Fallback {
610    fn name(&self) -> &str {
611        &self.name
612    }
613
614    fn unavailable(&self) -> Option<String> {
615        let reasons: Vec<String> = self.ways.iter().map(|w| w.unavailable()).collect::<Option<_>>()?;
616        Some(reasons.join("; or ").to_string())
617    }
618
619    fn start(&self) -> Result<Box<dyn Running>, String> {
620        self.ways.iter().find(|w| w.unavailable().is_none()).ok_or("no way to start it here")?.start()
621    }
622}
623
624#[cfg(test)]
625mod tests {
626    use super::*;
627    use std::sync::Mutex;
628    use std::net::TcpListener;
629    use std::sync::Arc;
630
631    /// A service that counts how it is used.
632    #[derive(Clone, Default)]
633    struct Fake {
634        name: &'static str,
635        starts: Arc<Mutex<u32>>,
636        stops: Arc<Mutex<u32>>,
637        dead: Arc<Mutex<bool>>,
638        why_not: Option<&'static str>,
639    }
640
641    struct FakeRunning(Fake);
642
643    impl Service for Fake {
644        fn name(&self) -> &str {
645            self.name
646        }
647        fn unavailable(&self) -> Option<String> {
648            self.why_not.map(String::from)
649        }
650        fn start(&self) -> Result<Box<dyn Running>, String> {
651            *self.starts.lock().unwrap() += 1;
652            *self.dead.lock().unwrap() = false;
653            Ok(Box::new(FakeRunning(self.clone())))
654        }
655    }
656
657    impl Running for FakeRunning {
658        fn alive(&mut self) -> bool {
659            !*self.0.dead.lock().unwrap()
660        }
661        fn endpoint(&self, key: &str) -> Option<String> {
662            (key == "port").then(|| "5760".to_string())
663        }
664    }
665
666    impl Drop for FakeRunning {
667        fn drop(&mut self) {
668            *self.0.stops.lock().unwrap() += 1;
669        }
670    }
671
672    fn fake(name: &'static str) -> Fake {
673        Fake { name, ..Default::default() }
674    }
675
676    /// A test that needs something that cannot be had here is not run and not failed: it says
677    /// what is missing and how to get it.
678    #[test]
679    fn a_service_that_cannot_start_here_says_how_to_get_it() {
680        let mut s = Services::new();
681        s.add(Fake { why_not: Some("build it with `make server`"), ..fake("server") });
682        s.add(fake("database"));
683        assert_eq!(s.start(&["database"]), Ok(()));
684        let err = s.start(&["database", "server"]).unwrap_err();
685        assert!(err.contains("server") && err.contains("make server"), "{err}");
686        let unknown = s.start(&["nothing"]).unwrap_err();
687        assert!(unknown.contains("nothing") && unknown.contains("no such service"), "{unknown}");
688    }
689
690    /// Several plans want the same service: it is started once, for all of them, and stopped
691    /// when the run is over.
692    #[test]
693    fn a_service_starts_once_and_stops_when_the_run_ends() {
694        let f = fake("db");
695        let (starts, stops) = (f.starts.clone(), f.stops.clone());
696        {
697            let mut s = Services::new();
698            s.add(f);
699            s.start(&["db"]).unwrap();
700            s.start(&["db"]).unwrap();
701            assert_eq!(*starts.lock().unwrap(), 1);
702            assert_eq!(s.endpoint("db", "port"), Some("5760".to_string()));
703            assert_eq!(s.endpoint("db", "nothing"), None);
704            assert_eq!(*stops.lock().unwrap(), 0);
705        }
706        assert_eq!(*stops.lock().unwrap(), 1, "dropped, so stopped");
707    }
708
709    /// A service that died between tests (a crash it did not survive) is started again for the next.
710    #[test]
711    fn a_service_that_died_is_started_again() {
712        let f = fake("server");
713        let (starts, stops, dead) = (f.starts.clone(), f.stops.clone(), f.dead.clone());
714        let mut s = Services::new();
715        s.add(f);
716        s.start(&["server"]).unwrap();
717        *dead.lock().unwrap() = true;
718        s.start(&["server"]).unwrap();
719        assert_eq!((*starts.lock().unwrap(), *stops.lock().unwrap()), (2, 1));
720    }
721
722    /// A restart is a stop and a start, so that what the last test left in a service is gone.
723    #[test]
724    fn a_restart_is_a_fresh_start() {
725        let f = fake("server");
726        let (starts, stops) = (f.starts.clone(), f.stops.clone());
727        let mut s = Services::new();
728        s.add(f);
729        s.start(&["server"]).unwrap();
730        s.restart(&["server"]).unwrap();
731        assert_eq!((*starts.lock().unwrap(), *stops.lock().unwrap()), (2, 1));
732        // A service that is not up is just started.
733        let mut other = Services::new();
734        other.add(fake("db"));
735        other.restart(&["db"]).unwrap();
736        assert!(other.endpoint("db", "port").is_some());
737    }
738
739    #[test]
740    fn services_stop_in_the_opposite_order_they_started() {
741        let order = Arc::new(Mutex::new(Vec::<&str>::new()));
742        struct Ordered(&'static str, Arc<Mutex<Vec<&'static str>>>);
743        struct OrderedRunning(&'static str, Arc<Mutex<Vec<&'static str>>>);
744        impl Service for Ordered {
745            fn name(&self) -> &str {
746                self.0
747            }
748            fn start(&self) -> Result<Box<dyn Running>, String> {
749                Ok(Box::new(OrderedRunning(self.0, self.1.clone())))
750            }
751        }
752        impl Running for OrderedRunning {}
753        impl Drop for OrderedRunning {
754            fn drop(&mut self) {
755                self.1.lock().unwrap().push(self.0);
756            }
757        }
758        {
759            let mut s = Services::new();
760            s.add(Ordered("database", order.clone()));
761            s.add(Ordered("app", order.clone()));
762            s.start(&["database", "app"]).unwrap();
763        }
764        assert_eq!(*order.lock().unwrap(), vec!["app", "database"], "what depends on the database stops first");
765    }
766
767    /// The first service of a list that can run here is the one that runs: a native build, else a container.
768    #[test]
769    fn a_fallback_takes_the_first_that_is_available() {
770        let (native, docker) = (Fake { why_not: Some("not built"), ..fake("native") }, fake("docker"));
771        let docker_starts = docker.starts.clone();
772        let mut s = Services::new();
773        s.add(Fallback::new("server", vec![Box::new(native.clone()), Box::new(docker.clone())]));
774        s.start(&["server"]).unwrap();
775        assert_eq!(*docker_starts.lock().unwrap(), 1);
776        assert_eq!(*native.starts.lock().unwrap(), 0);
777
778        let mut none = Services::new();
779        none.add(Fallback::new("server", vec![Box::new(native.clone()), Box::new(Fake { why_not: Some("no docker"), ..fake("docker") })]));
780        let err = none.start(&["server"]).unwrap_err();
781        assert!(err.contains("not built") && err.contains("no docker"), "every way is named: {err}");
782    }
783
784    /// A command is a service when it can be run here; its readiness is a port answering.
785    #[test]
786    fn a_command_is_ready_when_its_port_answers() {
787        // The program opens its port a moment after it starts (here, a thread does).
788        let port = {
789            let l = TcpListener::bind("127.0.0.1:0").unwrap();
790            l.local_addr().unwrap().port()
791        };
792        let opener = std::thread::spawn(move || {
793            std::thread::sleep(std::time::Duration::from_millis(400));
794            let listener = TcpListener::bind(("127.0.0.1", port)).unwrap();
795            std::thread::sleep(std::time::Duration::from_secs(3));
796            drop(listener);
797        });
798        let sleeper = Command::new("sleeper", "sleep").arg("30").ready(Ready::Tcp(port)).patience(std::time::Duration::from_secs(5));
799        let began = std::time::Instant::now();
800        let mut running = sleeper.start().expect("starts and is ready");
801        assert!(began.elapsed() >= std::time::Duration::from_millis(300), "it did not wait for the port");
802        assert!(running.alive());
803        assert_eq!(running.endpoint("port"), Some(port.to_string()));
804        opener.join().unwrap();
805
806        // Nothing answers on a port that is closed: the wait ends, and says so.
807        let closed = {
808            let l = TcpListener::bind("127.0.0.1:0").unwrap();
809            l.local_addr().unwrap().port()
810        };
811        let slow = Command::new("slow", "sleep").arg("30").ready(Ready::Tcp(closed)).patience(std::time::Duration::from_millis(300));
812        let err = slow.start().err().expect("never ready");
813        assert!(err.contains("slow") && err.contains(&closed.to_string()), "{err}");
814    }
815
816    /// A port that something else already answers on is not ours to wait for: what answers there
817    /// is a leftover of an earlier run, and a test that talked to it would be testing that.
818    #[test]
819    fn a_port_somebody_else_holds_is_not_ours_to_wait_for() {
820        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
821        let port = listener.local_addr().unwrap().port();
822        let c = Command::new("server", "sleep").arg("30").ready(Ready::Tcp(port));
823        let err = c.start().err().expect("the port is taken");
824        assert!(err.contains("server") && err.contains(&port.to_string()) && err.contains("already"), "{err}");
825        drop(listener);
826    }
827
828    #[test]
829    fn a_command_is_stopped_when_it_is_dropped() {
830        let sleeper = Command::new("sleeper", "sleep").arg("30");
831        let running = sleeper.start().unwrap();
832        let pid = running.endpoint("pid").expect("a pid");
833        let gone = |pid: &str| !std::process::Command::new("kill").args(["-0", pid]).stderr(std::process::Stdio::null()).status().unwrap().success();
834        assert!(!gone(&pid), "it is running");
835        drop(running);
836        let mut tries = 0;
837        while !gone(&pid) && tries < 50 {
838            std::thread::sleep(std::time::Duration::from_millis(20));
839            tries += 1;
840        }
841        assert!(gone(&pid), "it is still running");
842    }
843
844    #[test]
845    fn a_command_that_is_not_installed_is_unavailable_with_a_hint() {
846        let c = Command::new("thing", "definitely-not-a-program-expectation").hint("install it from example.org");
847        let why = c.unavailable().expect("not there");
848        assert!(why.contains("definitely-not-a-program-expectation") && why.contains("example.org"), "{why}");
849        assert!(Command::new("sleeper", "sleep").unavailable().is_none());
850    }
851
852    /// Something that must be there but is not ours to start (a build, a file): available when
853    /// it exists, and then it is simply there, at its path.
854    #[test]
855    fn a_thing_that_must_be_there_is_available_when_it_exists() {
856        let here = std::env::current_exe().unwrap();
857        let present = Present::new("tool", &here).hint("build it first");
858        assert_eq!(present.unavailable(), None);
859        let mut running = present.start().unwrap();
860        assert!(running.alive());
861        assert_eq!(running.endpoint("path"), Some(here.display().to_string()));
862
863        let gone = Present::new("tool", std::path::Path::new("/definitely/not/here/tool")).hint("build it first");
864        let why = gone.unavailable().expect("missing");
865        assert!(why.contains("/definitely/not/here/tool") && why.contains("build it first"), "{why}");
866        assert!(gone.start().is_err());
867    }
868
869    #[test]
870    fn a_command_that_exits_at_once_is_not_ready() {
871        let c = Command::new("quitter", "true").ready(Ready::Tcp(1)).patience(std::time::Duration::from_secs(2));
872        let err = c.start().err().expect("it quit");
873        assert!(err.contains("quitter") && err.contains("exited"), "{err}");
874    }
875
876    /// What `docker run` is told for a container: it is detached, removed when it stops, named,
877    /// with its ports, environment and image.
878    #[test]
879    fn a_container_runs_detached_with_its_ports_and_environment() {
880        let postgres = Docker::new("database", "postgres:16").container("expectation-test-db").port(5432, 55432).env("POSTGRES_PASSWORD", "test").ready(Ready::Tcp(55432));
881        assert_eq!(
882            postgres.run_args(),
883            vec!["run", "-d", "--rm", "--name", "expectation-test-db", "-p", "55432:5432", "-e", "POSTGRES_PASSWORD=test", "postgres:16"]
884        );
885        let with_command = Docker::new("x", "img").container("c").arg("--flag");
886        assert_eq!(with_command.run_args().last().map(String::as_str), Some("--flag"), "arguments go after the image");
887    }
888
889    /// Without Docker installed, a container service is unavailable and says where to get it.
890    #[test]
891    fn a_container_needs_docker() {
892        let d = Docker::new("database", "postgres:16").docker_program("definitely-not-docker-expectation");
893        let why = d.unavailable().expect("no docker");
894        assert!(why.contains("Docker") && why.contains("docker.com"), "{why}");
895    }
896
897    /// An image that is not there is built from its directory, when it has one, and the
898    /// service is not available when it has neither.
899    #[test]
900    fn an_image_is_built_from_its_directory() {
901        let d = Docker::new("server", "expectation-server").build_from("server");
902        assert_eq!(d.build_args().unwrap(), vec!["build", "-q", "-t", "expectation-server", "server"]);
903        assert!(Docker::new("x", "img").build_args().is_none());
904    }
905}