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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
//! Configure a DockerTest to run.

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

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

/// The main entry point to specify a test.
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,
    /// 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>,
    /// Network configuration, defaults to [Network::Singular] if not specified by
    /// user.
    pub(crate) network: Network,
}

/// Configure how the docker network should be applied to the containers within this test.
///
/// The default value for a [DockerTest], if not provided, is [Network::Singular].
#[derive(Debug)]
pub enum Network {
    /// A single statically named network, with the namespace of the [DockerTest] as a prefix.
    ///
    /// This network will be shared between each test using the same namespace, and the network
    /// itself will never be deleted. This network is created on demand, but is never deleted.
    /// This is to facilitate reuse between tests, to avoid the cost of creating a new
    /// docker network when not necessary.
    ///
    /// In the event that multiple networks matching the same name exists, the most recently
    /// created network is selected. This situation might occur when multiple tests are running
    /// in parallel while there are no pre-existing network. No locking is performed to avoid
    /// this race condition, since it has no impact on test performance.
    Singular,
    /// Test will use an externally managed docker network.
    ///
    /// All created containers will attach itself to the existing, externally managed network.
    External(String),
    /// Each [DockerTest] instance will create and manage its own isolated docker network.
    ///
    /// The network will be deleted once the test body exits.
    Isolated,
}

impl DockerTest {
    /// Start the configuration process of a new [DockerTest] instance.
    pub fn new() -> Self {
        Self {
            default_source: Source::Local,
            compositions: Vec::new(),
            namespace: "dockertest-rs".to_string(),
            container_id: None,
            network: Network::Singular,
        }
    }

    /// Sets the default [Source] for all [Image]s.
    ///
    /// All images without a specified source will be pulled from the default source.
    /// DockerTest will default to [Source::Local] if not configured.
    ///
    /// [Image]: crate::image::Image
    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
        }
    }

    /// Sets the network configuration
    pub fn with_network(self, network: Network) -> Self {
        Self { network, ..self }
    }

    /// Append a container specification as part of this specific test.
    ///
    /// The order of which container specifications are added to DockerTest is significant
    /// for the start-up order for strict order dependencies.
    ///
    /// Please refer to one of the following variants to construct your container specification:
    /// * [TestBodySpecification]
    /// * [DynamicSpecification]
    /// * [ExternalSpecification]
    ///
    /// [TestBodySpecification]: crate::specification::TestBodySpecification
    /// [DynamicSpecification]: crate::specification::DynamicSpecification
    /// [ExternalSpecification]: crate::specification::ExternalSpecification
    pub fn provide_container(
        &mut self,
        specification: impl ContainerSpecification,
    ) -> &mut DockerTest {
        let composition = specification.into_composition();
        self.compositions.push(composition);
        self
    }

    /// 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 non-async version creates its own runtime to execute the test.
    // 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 = rt.block_on(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).await;
        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 = matches!(*test.source(), Source::Local);

        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 = matches!(test.default_source, Source::DockerHub);

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