1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! test-patience is a utility to synchronize the startup of applications that are part of an integration test and the test itself.
//!
//! When writing integration tests, it is often necessary to launch applications that are part of the test.
//! 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.
//! Also waiting for a fixed duration can still lead to test failures without producing a clear error message.
//! test-patience waits exactly until the starting application signals that it's ready or a specified timeout period has passed.
//!
//! # Using test-patience
//!
//! The test has to create an instance of the `Server` struct, which starts a TCP server and returns a port number.
//! That port number needs to be sent to the application that is needed to execute the test.
//! This could be done using an environment variable, an argument or a configuration file.
//! After the start of the application has been initiated, the `wait` method needs to be called.
//! It blocks the currently running thread until either the starting application has signaled its successful start or the `timeout` period has passed.
//!
//! 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.
//! After that the thread of the test continues executing.
//!
//! 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)).
//!
//! # Examples
//!
//! Application
//!
//! ```no_run
//! use std::env;
//!
//! // initialize application (eg. connect to database server)
//! # fn get_db_connection() {}
//! # #[allow(unused_variables)]
//! let db_connection = get_db_connection();
//!
//! // notify test in case the environment variable TEST_PATIENCE_PORT is set
//! if let Some(port) = env::var("TEST_PATIENCE_PORT").ok()
//!                     .and_then(|s| s.parse::<u16>().ok()) {
//!     test_patience::Client::notify(port).unwrap();
//! }
//! ```
//!
//! Test
//!
//! ```no_run
//! use std::time::Duration;
//! use std::process;
//!
//! let server = test_patience::Server::new().unwrap();
//! let port = server.port().unwrap();
//!
//! # #[allow(unused_variables)]
//! let process = process::Command::new("path/to/application")
//!     .env("TEST_PATIENCE_PORT", format!("{}", port))
//!     .spawn()
//!     .unwrap();
//!
//! server.wait(Duration::from_secs(5)).unwrap();
//! ```
#![warn(missing_docs)]

use std::net::{TcpListener, TcpStream};
use std::time::{Instant, Duration};
use std::io::{Result, Error, ErrorKind};
use std::io::prelude::*;
use std::thread;

/// Entry point for the application that needs to be synchronized
pub struct Client;

impl Client {
    /// Notify the server that the client has started successfully
    pub fn notify(port: u16) -> Result<()> {
        let mut stream = TcpStream::connect(("127.0.0.1", port))?;
        stream.write_all(b"done")?;
        Ok(())
    }
}

/// Entry point for the test, waiting for the application to start
pub struct Server {
    listener: TcpListener,
}

impl Server {
    /// Start new TCP server, waiting for the application's startup notification
    pub fn new() -> Result<Server> {
        Ok(Server {
            listener: TcpListener::bind(("127.0.0.1", 0))?
        })
    }

    /// Get the port number of the TCP Server
    ///
    /// This port number has to sent to the application.
    pub fn port(&self) -> Result<u16> {
        Ok(self.listener.local_addr()?.port())
    }

    /// Block the currently running thread until either the starting application has signaled its successful start or the `timeout` period has expired
    ///
    /// Returns the duration for which was waited or an error in case of a timeout or invalid startup notification.
    pub fn wait(self, timeout: Duration) -> Result<Duration> {
        self.listener.set_nonblocking(true)?;

        let start = Instant::now();
        while start.elapsed() < timeout {
            match self.listener.accept() {
                Ok((mut stream, _)) => {
                    let mut buf = Vec::new();
                    stream.read_to_end(&mut buf)?;
                    if buf == b"done" {
                        return Ok(start.elapsed());
                    } else {
                        return Err(Error::new(ErrorKind::Other, "wrong startup notification received"));
                    }
                }
                Err(ref e) if e.kind() == ErrorKind::WouldBlock => {}
                Err(e) => return Err(e)
            }
            thread::sleep(Duration::from_millis(1));
        }
        Err(Error::new(ErrorKind::TimedOut, "did not receive startup notification"))
    }
}