1use std::collections::BTreeMap;
15use std::net::{SocketAddr, TcpStream};
16use std::process::{Child, Stdio};
17use std::time::{Duration, Instant};
18
19pub trait Service: Send {
21 fn name(&self) -> &str;
23
24 fn unavailable(&self) -> Option<String> {
26 None
27 }
28
29 fn start(&self) -> Result<Box<dyn Running>, String>;
31}
32
33pub trait Running: Send {
35 fn alive(&mut self) -> bool {
37 true
38 }
39
40 fn endpoint(&self, key: &str) -> Option<String> {
42 let _ = key;
43 None
44 }
45}
46
47#[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 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 pub fn is_empty(&self) -> bool {
71 self.known.is_empty()
72 }
73
74 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 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 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 pub fn endpoint(&self, name: &str, key: &str) -> Option<String> {
119 self.up.iter().find(|(n, _)| n == name)?.1.endpoint(key)
120 }
121
122 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#[derive(Clone, Debug)]
136pub enum Ready {
137 Started,
139 Tcp(u16),
141 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
171fn port_answers(port: u16) -> bool {
173 TcpStream::connect_timeout(&SocketAddr::from(([127, 0, 0, 1], port)), Duration::from_millis(200)).is_ok()
174}
175
176fn 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#[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 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 pub fn patience(mut self, patience: Duration) -> Self {
240 self.patience = patience;
241 self
242 }
243
244 pub fn hint(mut self, hint: &str) -> Self {
246 self.hint = Some(hint.into());
247 self
248 }
249
250 pub fn program(&self) -> &str {
252 &self.program
253 }
254
255 pub fn argv(&self) -> &[String] {
257 &self.args
258 }
259
260 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
338pub 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 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#[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 pub fn container(mut self, name: &str) -> Self {
427 self.container = name.into();
428 self
429 }
430
431 pub fn build_from(mut self, dir: &str) -> Self {
433 self.build_dir = Some(dir.into());
434 self
435 }
436
437 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 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 pub fn docker_program(mut self, program: &str) -> Self {
466 self.program = program.into();
467 self
468 }
469
470 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 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 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 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
597pub 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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[test]
786 fn a_command_is_ready_when_its_port_answers() {
787 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 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 #[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 #[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 #[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 #[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 #[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}