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}