1use std::collections::BTreeMap;
2use std::path::{Path, PathBuf};
3use std::{env, fs};
4
5use anyhow::{bail, Context, Result};
6use indexmap::IndexMap;
7use itertools::{self, Itertools};
8use serde::{Deserialize, Serialize};
9use serde_yaml::Value;
10
11use crate::{app, util};
12
13const FILE_NAME: &str = "test.yaml";
14
15pub const INTEGRATION_TESTS_DIR: &str = "integration";
16pub const E2E_TESTS_DIR: &str = "e2e";
17
18pub type Environment = BTreeMap<String, Option<String>>;
19
20#[derive(Deserialize, Debug)]
21pub struct RustToolchainRootConfig {
22 pub toolchain: RustToolchainConfig,
23}
24
25#[derive(Deserialize, Debug)]
26pub struct RustToolchainConfig {
27 pub channel: String,
28}
29
30impl RustToolchainConfig {
31 fn parse() -> Result<Self> {
32 let repo_path = app::path();
33 let config_file: PathBuf = [repo_path, "rust-toolchain.toml"].iter().collect();
34 let contents = fs::read_to_string(&config_file)
35 .with_context(|| format!("failed to read {}", config_file.display()))?;
36 let config: RustToolchainRootConfig = toml::from_str(&contents)
37 .with_context(|| format!("failed to parse {}", config_file.display()))?;
38
39 Ok(config.toolchain)
40 }
41
42 pub fn rust_version() -> String {
43 match RustToolchainConfig::parse() {
44 Ok(config) => config.channel,
45 Err(error) => fatal!("Could not read `rust-toolchain.toml` file: {error}"),
46 }
47 }
48}
49
50#[derive(Debug, Deserialize, Serialize)]
51#[serde(untagged)]
52pub enum VolumeMount {
53 Short(String),
54 Long {
55 #[serde(default)]
56 r#type: Option<String>,
57 source: String,
58 target: String,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 read_only: Option<bool>,
61 },
62}
63
64#[derive(Debug, Deserialize, Serialize)]
65#[serde(untagged)]
66pub enum VolumeDefinition {
67 Empty,
68 WithOptions(BTreeMap<String, Value>),
69}
70
71#[derive(Debug, Deserialize, Serialize)]
72pub struct ComposeConfig {
73 pub services: BTreeMap<String, ComposeService>,
74 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
75 pub volumes: BTreeMap<String, VolumeDefinition>,
76 #[serde(default)]
77 pub networks: BTreeMap<String, BTreeMap<String, String>>,
78}
79
80#[derive(Debug, Deserialize, Serialize)]
81pub struct ComposeService {
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub image: Option<String>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub hostname: Option<String>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub container_name: Option<String>,
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub build: Option<Value>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub command: Option<Command>,
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub ports: Option<Vec<String>>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub env_file: Option<Vec<String>>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub volumes: Option<Vec<VolumeMount>>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub environment: Option<Vec<String>>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub depends_on: Option<Vec<String>>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub healthcheck: Option<Value>,
104}
105
106#[derive(Debug, Deserialize, Serialize)]
107#[serde(untagged)]
108pub enum Command {
109 Single(String),
110 Multiple(Vec<String>),
111}
112
113impl ComposeConfig {
114 pub fn parse(path: &Path) -> Result<Self> {
115 let contents = fs::read_to_string(path)
116 .with_context(|| format!("failed to read {}", path.display()))?;
117 serde_yaml::from_str(&contents)
118 .with_context(|| format!("failed to parse {}", path.display()))
119 }
120}
121
122#[derive(Clone, Debug, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct ComposeTestConfig {
125 pub args: Option<Vec<String>>,
127 #[serde(default)]
131 pub env: Environment,
132 matrix: IndexMap<String, Vec<String>>,
134 #[serde(default)]
136 pub runner: IntegrationRunnerConfig,
137
138 pub features: Vec<String>,
139
140 pub test: Option<String>,
141
142 pub test_filter: Option<String>,
143
144 pub paths: Option<Vec<String>>,
145}
146
147#[derive(Clone, Debug, Default, Deserialize)]
148#[serde(deny_unknown_fields)]
149pub struct IntegrationRunnerConfig {
150 #[serde(default)]
153 pub env: Environment,
154 #[serde(default)]
156 pub volumes: BTreeMap<String, String>,
157 #[serde(default)]
159 pub needs_docker_socket: bool,
160}
161
162impl ComposeTestConfig {
163 fn parse_file(config_file: &Path) -> Result<Self> {
164 let contents = fs::read_to_string(config_file)
165 .with_context(|| format!("failed to read {}", config_file.display()))?;
166 let config: Self = serde_yaml::from_str(&contents).with_context(|| {
167 format!(
168 "failed to parse integration test configuration file {}",
169 config_file.display()
170 )
171 })?;
172
173 Ok(config)
174 }
175
176 pub fn environments(&self) -> IndexMap<String, Environment> {
177 self.matrix
178 .values()
179 .multi_cartesian_product()
180 .map(|product| {
181 let key = product.iter().join("-");
182 let config: Environment = self
183 .matrix
184 .keys()
185 .zip(product)
186 .map(|(variable, value)| (variable.clone(), Some(value.clone())))
187 .collect();
188 (key, config)
189 })
190 .collect()
191 }
192
193 pub fn load(root_dir: &str, integration: &str) -> Result<(PathBuf, Self)> {
194 let test_dir: PathBuf = [app::path(), "scripts", root_dir, integration]
195 .iter()
196 .collect();
197
198 if !test_dir.is_dir() {
199 bail!("unknown integration: {}", integration);
200 }
201
202 let config = Self::parse_file(&test_dir.join(FILE_NAME))?;
203 Ok((test_dir, config))
204 }
205
206 fn collect_all_dir(tests_dir: &Path, configs: &mut BTreeMap<String, Self>) -> Result<()> {
207 for entry in tests_dir.read_dir()? {
208 let entry = entry?;
209 if entry.path().is_dir() {
210 let config_file: PathBuf =
211 [entry.path().to_str().unwrap(), FILE_NAME].iter().collect();
212 if util::exists(&config_file)? {
213 let config = Self::parse_file(&config_file)?;
214 configs.insert(entry.file_name().into_string().unwrap(), config);
215 }
216 }
217 }
218 Ok(())
219 }
220
221 pub fn collect_all(root_dir: &str) -> Result<BTreeMap<String, Self>> {
222 let mut configs = BTreeMap::new();
223
224 let tests_dir: PathBuf = [app::path(), "scripts", root_dir].iter().collect();
225
226 Self::collect_all_dir(&tests_dir, &mut configs)?;
227
228 Ok(configs)
229 }
230
231 pub fn check_required(&self) -> Result<()> {
233 let missing: Vec<_> = self
234 .env
235 .iter()
236 .chain(self.runner.env.iter())
237 .filter_map(|(key, value)| value.is_none().then_some(key))
238 .filter(|var| env::var(var).is_err())
239 .collect();
240 if missing.is_empty() {
241 Ok(())
242 } else {
243 let missing = missing.into_iter().join(", ");
244 bail!("Required environment variables are not set: {missing}");
245 }
246 }
247}