test_patience/lib.rs
1//! test-patience is a utility to synchronize the startup of applications that are part of an integration test and the test itself.
2//!
3//! When writing integration tests, it is often necessary to launch applications that are part of the test.
4//! This can take some time and if the test has to wait until the application is running, valuable time is lost when waiting for a fixed duration.
5//! Also waiting for a fixed duration can still lead to test failures without producing a clear error message.
6//! test-patience waits exactly until the starting application signals that it's ready or a specified timeout period has passed.
7//!
8//! # Using test-patience
9//!
10//! The test has to create an instance of the `Server` struct, which starts a TCP server and returns a port number.
11//! That port number needs to be sent to the application that is needed to execute the test.
12//! This could be done using an environment variable, an argument or a configuration file.
13//! After the start of the application has been initiated, the `wait` method needs to be called.
14//! It blocks the currently running thread until either the starting application has signaled its successful start or the `timeout` period has passed.
15//!
16//! When the application is ready, it has to create an instance of the `Client` struct and call the `notify` method with the correct port number.
17//! After that the thread of the test continues executing.
18//!
19//! In order to disable startup notifications in release builds, use `cfg!(debug_assertions)` (see [conditional compilation](https://doc.rust-lang.org/reference.html#conditional-compilation)).
20//!
21//! # Examples
22//!
23//! Application
24//!
25//! ```no_run
26//! use std::env;
27//!
28//! // initialize application (eg. connect to database server)
29//! # fn get_db_connection() {}
30//! # #[allow(unused_variables)]
31//! let db_connection = get_db_connection();
32//!
33//! // notify test in case the environment variable TEST_PATIENCE_PORT is set
34//! if let Some(port) = env::var("TEST_PATIENCE_PORT").ok()
35//! .and_then(|s| s.parse::<u16>().ok()) {
36//! test_patience::Client::notify(port).unwrap();
37//! }
38//! ```
39//!
40//! Test
41//!
42//! ```no_run
43//! use std::time::Duration;
44//! use std::process;
45//!
46//! let server = test_patience::Server::new().unwrap();
47//! let port = server.port().unwrap();
48//!
49//! # #[allow(unused_variables)]
50//! let process = process::Command::new("path/to/application")
51//! .env("TEST_PATIENCE_PORT", format!("{}", port))
52//! .spawn()
53//! .unwrap();
54//!
55//! server.wait(Duration::from_secs(5)).unwrap();
56//! ```
57#![warn(missing_docs)]
58
59use std::net::{TcpListener, TcpStream};
60use std::time::{Instant, Duration};
61use std::io::{Result, Error, ErrorKind};
62use std::io::prelude::*;
63use std::thread;
64
65/// Entry point for the application that needs to be synchronized
66pub struct Client;
67
68impl Client {
69 /// Notify the server that the client has started successfully
70 pub fn notify(port: u16) -> Result<()> {
71 let mut stream = TcpStream::connect(("127.0.0.1", port))?;
72 stream.write_all(b"done")?;
73 Ok(())
74 }
75}
76
77/// Entry point for the test, waiting for the application to start
78pub struct Server {
79 listener: TcpListener,
80}
81
82impl Server {
83 /// Start new TCP server, waiting for the application's startup notification
84 pub fn new() -> Result<Server> {
85 Ok(Server {
86 listener: TcpListener::bind(("127.0.0.1", 0))?
87 })
88 }
89
90 /// Get the port number of the TCP Server
91 ///
92 /// This port number has to sent to the application.
93 pub fn port(&self) -> Result<u16> {
94 Ok(self.listener.local_addr()?.port())
95 }
96
97 /// Block the currently running thread until either the starting application has signaled its successful start or the `timeout` period has expired
98 ///
99 /// Returns the duration for which was waited or an error in case of a timeout or invalid startup notification.
100 pub fn wait(self, timeout: Duration) -> Result<Duration> {
101 self.listener.set_nonblocking(true)?;
102
103 let start = Instant::now();
104 while start.elapsed() < timeout {
105 match self.listener.accept() {
106 Ok((mut stream, _)) => {
107 let mut buf = Vec::new();
108 stream.read_to_end(&mut buf)?;
109 if buf == b"done" {
110 return Ok(start.elapsed());
111 } else {
112 return Err(Error::new(ErrorKind::Other, "wrong startup notification received"));
113 }
114 }
115 Err(ref e) if e.kind() == ErrorKind::WouldBlock => {}
116 Err(e) => return Err(e)
117 }
118 thread::sleep(Duration::from_millis(1));
119 }
120 Err(Error::new(ErrorKind::TimedOut, "did not receive startup notification"))
121 }
122}