Skip to main content

terraform_wrapper/commands/
test.rs

1use crate::Terraform;
2use crate::command::TerraformCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5
6/// Command for running Terraform module integration tests.
7///
8/// Runs `.tftest.hcl` test files. Available in Terraform 1.6+.
9///
10/// ```no_run
11/// # async fn example() -> terraform_wrapper::error::Result<()> {
12/// use terraform_wrapper::{Terraform, TerraformCommand};
13/// use terraform_wrapper::commands::test::TestCommand;
14///
15/// let tf = Terraform::builder().working_dir("/tmp/infra").build()?;
16/// let output = TestCommand::new()
17///     .filter("my_test")
18///     .verbose()
19///     .execute(&tf)
20///     .await?;
21/// # Ok(())
22/// # }
23/// ```
24#[derive(Debug, Clone, Default)]
25pub struct TestCommand {
26    filter: Option<String>,
27    json: bool,
28    test_directory: Option<String>,
29    verbose: bool,
30    vars: Vec<(String, String)>,
31    var_files: Vec<String>,
32    parallelism: Option<u32>,
33    junit_xml: Option<String>,
34    raw_args: Vec<String>,
35}
36
37impl TestCommand {
38    /// Create a new test command with default options.
39    #[must_use]
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Filter to a specific test (`-filter`).
45    #[must_use]
46    pub fn filter(mut self, name: &str) -> Self {
47        self.filter = Some(name.to_string());
48        self
49    }
50
51    /// Enable machine-readable JSON output (`-json`).
52    #[must_use]
53    pub fn json(mut self) -> Self {
54        self.json = true;
55        self
56    }
57
58    /// Set the directory containing test files (`-test-directory`).
59    ///
60    /// Defaults to `tests` if not specified.
61    #[must_use]
62    pub fn test_directory(mut self, path: &str) -> Self {
63        self.test_directory = Some(path.to_string());
64        self
65    }
66
67    /// Enable verbose output (`-verbose`).
68    #[must_use]
69    pub fn verbose(mut self) -> Self {
70        self.verbose = true;
71        self
72    }
73
74    /// Set a variable value (`-var="name=value"`).
75    #[must_use]
76    pub fn var(mut self, name: &str, value: &str) -> Self {
77        self.vars.push((name.to_string(), value.to_string()));
78        self
79    }
80
81    /// Add a variable definitions file (`-var-file`).
82    #[must_use]
83    pub fn var_file(mut self, path: &str) -> Self {
84        self.var_files.push(path.to_string());
85        self
86    }
87
88    /// Limit the number of concurrent operations (`-parallelism`).
89    #[must_use]
90    pub fn parallelism(mut self, n: u32) -> Self {
91        self.parallelism = Some(n);
92        self
93    }
94
95    /// Write test results to a JUnit XML file (`-junit-xml`).
96    #[must_use]
97    pub fn junit_xml(mut self, path: &str) -> Self {
98        self.junit_xml = Some(path.to_string());
99        self
100    }
101
102    /// Add a raw argument (escape hatch for unsupported options).
103    #[must_use]
104    pub fn arg(mut self, arg: impl Into<String>) -> Self {
105        self.raw_args.push(arg.into());
106        self
107    }
108}
109
110impl TerraformCommand for TestCommand {
111    type Output = CommandOutput;
112
113    fn args(&self) -> Vec<String> {
114        let mut args = vec!["test".to_string()];
115        if let Some(ref filter) = self.filter {
116            args.push(format!("-filter={filter}"));
117        }
118        if self.json {
119            args.push("-json".to_string());
120        }
121        if let Some(ref dir) = self.test_directory {
122            args.push(format!("-test-directory={dir}"));
123        }
124        if self.verbose {
125            args.push("-verbose".to_string());
126        }
127        for (name, value) in &self.vars {
128            args.push(format!("-var={name}={value}"));
129        }
130        for file in &self.var_files {
131            args.push(format!("-var-file={file}"));
132        }
133        if let Some(n) = self.parallelism {
134            args.push(format!("-parallelism={n}"));
135        }
136        if let Some(ref path) = self.junit_xml {
137            args.push(format!("-junit-xml={path}"));
138        }
139        args.extend(self.raw_args.clone());
140        args
141    }
142
143    async fn execute(&self, tf: &Terraform) -> Result<CommandOutput> {
144        exec::run_terraform(tf, self.args()).await
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn default_args() {
154        let cmd = TestCommand::new();
155        assert_eq!(cmd.args(), vec!["test"]);
156    }
157
158    #[test]
159    fn with_filter() {
160        let cmd = TestCommand::new().filter("my_test");
161        assert_eq!(cmd.args(), vec!["test", "-filter=my_test"]);
162    }
163
164    #[test]
165    fn with_json_and_verbose() {
166        let cmd = TestCommand::new().json().verbose();
167        let args = cmd.args();
168        assert_eq!(args[0], "test");
169        assert!(args.contains(&"-json".to_string()));
170        assert!(args.contains(&"-verbose".to_string()));
171    }
172
173    #[test]
174    fn with_test_directory() {
175        let cmd = TestCommand::new().test_directory("integration");
176        assert_eq!(cmd.args(), vec!["test", "-test-directory=integration"]);
177    }
178
179    #[test]
180    fn with_vars() {
181        let cmd = TestCommand::new()
182            .var("region", "us-west-2")
183            .var("env", "staging");
184        let args = cmd.args();
185        assert!(args.contains(&"-var=region=us-west-2".to_string()));
186        assert!(args.contains(&"-var=env=staging".to_string()));
187    }
188
189    #[test]
190    fn with_var_files() {
191        let cmd = TestCommand::new()
192            .var_file("prod.tfvars")
193            .var_file("overrides.tfvars");
194        let args = cmd.args();
195        assert!(args.contains(&"-var-file=prod.tfvars".to_string()));
196        assert!(args.contains(&"-var-file=overrides.tfvars".to_string()));
197    }
198
199    #[test]
200    fn with_parallelism() {
201        let cmd = TestCommand::new().parallelism(4);
202        let args = cmd.args();
203        assert!(args.contains(&"-parallelism=4".to_string()));
204    }
205
206    #[test]
207    fn with_junit_xml() {
208        let cmd = TestCommand::new().junit_xml("results.xml");
209        let args = cmd.args();
210        assert!(args.contains(&"-junit-xml=results.xml".to_string()));
211    }
212
213    #[test]
214    fn all_options() {
215        let cmd = TestCommand::new()
216            .filter("vpc_test")
217            .json()
218            .test_directory("e2e")
219            .verbose()
220            .var("region", "us-west-2")
221            .var_file("prod.tfvars")
222            .parallelism(8)
223            .junit_xml("results.xml");
224        let args = cmd.args();
225        assert_eq!(args[0], "test");
226        assert!(args.contains(&"-filter=vpc_test".to_string()));
227        assert!(args.contains(&"-json".to_string()));
228        assert!(args.contains(&"-test-directory=e2e".to_string()));
229        assert!(args.contains(&"-verbose".to_string()));
230        assert!(args.contains(&"-var=region=us-west-2".to_string()));
231        assert!(args.contains(&"-var-file=prod.tfvars".to_string()));
232        assert!(args.contains(&"-parallelism=8".to_string()));
233        assert!(args.contains(&"-junit-xml=results.xml".to_string()));
234    }
235}