dockertest 0.3.1

A library to control docker containers when running your integration tests.
Documentation
//! Configure a DockerTest to run.

use crate::composition::Composition;
use crate::image::Source;
use crate::runner::{DockerOperations, Runner};
use crate::DockerTestError;

use futures::future::Future;
use tokio::runtime::Runtime;
use tracing::{event, span, Instrument, Level};

/// The internal configuration object of a DockerTest instance.
pub struct DockerTest {
    /// All Compositions that have been added to this test run.
    /// They are stored in the order they where added by `add_composition`.
    pub(crate) compositions: Vec<Composition>,
    /// The namespace of all started containers,
    /// this is essentially only a prefix on each container.
    /// Used to more easily identify which containers was
    /// started by DockerTest.
    pub(crate) namespace: String,
    /// The default pull source to use for all images.
    /// Images with a specified source will override this default.
    pub(crate) default_source: Source,
    /// The name of an external network to use for this test, if any.
    /// If not provided, DockerTest will determine its own networking needs.
    pub(crate) external_network: Option<String>,
    /// Retrieved internally by an env variable the user has to set.
    /// Will only be used in environments where dockertest itself is running inside a container.
    pub(crate) container_id: Option<String>,
}

impl DockerTest {
    /// Configure a new instance of [DockerTest].
    pub fn new() -> Self {
        Self {
            default_source: Source::Local,
            compositions: Vec::new(),
            namespace: "dockertest-rs".to_string(),
            container_id: None,
            external_network: None,
        }
    }

    /// Sets the default source for all images.
    ///
    /// All images without a specified source will be pulled from the default source.
    /// DockerTest will default to Local if no default source is provided.
    pub fn with_default_source(self, default_source: Source) -> Self {
        Self {
            default_source,
            ..self
        }
    }

    /// Sets the namespace for all containers created by [DockerTest].
    ///
    /// All container names will be prefixed with this namespace.
    /// DockerTest defaults to the namespace "dockertest-rs".
    pub fn with_namespace<T: ToString>(self, name: T) -> Self {
        Self {
            namespace: name.to_string(),
            ..self
        }
    }

    /// Test will use an externally managed docker network.
    ///
    /// All created containers will attach itself to the existing, externally managed network.
    ///
    /// If the container is created with a [crate::composition::StaticManagementPolicy::External],
    /// it is assumed that the container is already part of this network.
    ///
    /// For [crate::composition::StaticManagementPolicy::Internal], the container will be included
    /// into the network before test starts, and dropped once the statically managed container
    /// is removed.
    pub fn with_external_network<T: ToString>(self, network: T) -> Self {
        Self {
            external_network: Some(network.to_string()),
            ..self
        }
    }

    /// Add a Composition to this DockerTest.
    pub fn add_composition(&mut self, instance: Composition) {
        self.compositions.push(instance);
    }

    /// Retrieve the default source for Images unless explicitly specified per Image.
    pub fn source(&self) -> &Source {
        &self.default_source
    }

    /// Execute the test with the constructed environment in full operation.
    ///
    /// # Synchronous
    /// This synchronous version of executes the test with its own runtime.
    // NOTE(clippy): tracing generates cognitive complexity due to macro expansion.
    #[allow(clippy::cognitive_complexity)]
    pub fn run<T, Fut>(self, test: T)
    where
        T: FnOnce(DockerOperations) -> Fut,
        Fut: Future<Output = ()> + Send + 'static,
    {
        let span = span!(Level::ERROR, "run");
        let _guard = span.enter();

        // Allocate a new runtime for this test.
        let rt = match Runtime::new() {
            Ok(r) => r,
            Err(e) => {
                event!(Level::ERROR, "failed to allocate tokio runtime: {}", e);
                panic!("{}", e);
            }
        };

        let runner = Runner::new(self);
        process_run(rt.block_on(runner.run_impl(test).in_current_span()))
    }

    /// Async version of [DockerTest::run].
    ///
    /// # Asynchronous
    /// This version allows the caller to provide the runtime to execute this test within.
    /// This can be useful if the test executable is wrapped with a runtime macro, e.g.,
    /// `#[tokio::test]`.
    pub async fn run_async<T, Fut>(self, test: T)
    where
        T: FnOnce(DockerOperations) -> Fut,
        Fut: Future<Output = ()> + Send + 'static,
    {
        let span = span!(Level::ERROR, "run");
        let _guard = span.enter();

        let runner = Runner::new(self);
        process_run(runner.run_impl(test).in_current_span().await);
    }
}

impl Default for DockerTest {
    fn default() -> Self {
        Self::new()
    }
}

fn process_run(result: Result<(), DockerTestError>) {
    match result {
        Ok(_) => event!(Level::DEBUG, "dockertest successfully executed"),
        Err(e) => {
            event!(
                Level::ERROR,
                "internal dockertest condition failure: {:?}",
                e
            );
            event!(Level::WARN, "dockertest failure");
            panic!("{}", e);
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::{DockerTest, Source};

    // The default DockerTest constructor produces a valid instance with the correct values set
    #[test]
    fn test_default_constructor() {
        let test = DockerTest::new();
        assert_eq!(
            test.compositions.len(),
            0,
            "should not have any strict instances after creation"
        );

        assert_eq!(
            test.namespace,
            "dockertest-rs".to_string(),
            "default namespace was not set correctly"
        );

        let equal = match *test.source() {
            Source::Local => true,
            _ => false,
        };

        assert!(equal, "source not set to local by default");
    }

    // The `with_namespace` builder method sets the namespace correctly
    #[test]
    fn test_with_namespace() {
        let namespace = "this_is_a_test_namespace".to_string();
        let test = DockerTest::new().with_namespace(&namespace);

        assert_eq!(
            test.namespace, namespace,
            "default namespace was not set correctly"
        );
    }

    // The `with_default_source` builder method sets the default_source_correctly
    #[test]
    fn test_with_default_source() {
        let test = DockerTest::new().with_default_source(Source::DockerHub);

        let equal = match test.default_source {
            Source::DockerHub => true,
            _ => false,
        };

        assert!(equal, "default_source was not set correctly");
    }
}