blackjack/
test_spec.rs

1// Copyright 2024 Ole Kliemann
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::error::Result;
5use display_json::{DebugAsJson, DisplayAsJsonPretty};
6use envsubst;
7use schemars::{schema::RootSchema, schema_for, JsonSchema};
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, HashMap, HashSet};
10use std::ffi::OsStr;
11use std::path::{Path, PathBuf};
12use tokio::fs::read_to_string;
13
14pub type Env = HashMap<String, String>;
15
16pub trait EnvSubst {
17    fn subst_env(self, env: &Env) -> Self;
18}
19
20#[derive(
21    Default,
22    Clone,
23    Serialize,
24    Deserialize,
25    JsonSchema,
26    Eq,
27    PartialEq,
28    Hash,
29    DisplayAsJsonPretty,
30    DebugAsJson,
31)]
32#[serde(rename_all = "lowercase")]
33pub enum TestType {
34    Cluster,
35    #[default]
36    User,
37}
38
39#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
40pub struct TestSpec {
41    #[serde(default)]
42    pub name: String,
43    #[serde(default, rename = "type")]
44    pub test_type: TestType,
45    #[serde(default)]
46    pub ordering: Option<String>,
47    #[serde(default)]
48    pub steps: Vec<StepSpec>,
49    #[serde(skip_deserializing)]
50    pub dir: PathBuf,
51    #[serde(default)]
52    pub attempts: Option<u16>,
53}
54
55impl TestSpec {
56    pub async fn new_from_file(dirname: PathBuf) -> Result<TestSpec> {
57        let path = dirname.join(Path::new("test.yaml"));
58        let data = read_to_string(path).await?;
59        let mut testspec: TestSpec = serde_yaml::from_str(&data)?;
60        if testspec.name == "" {
61            let mut it = dirname.components();
62            let n2 = it.next_back().map_or_else(
63                || "".to_string(),
64                |x| {
65                    let x: &OsStr = x.as_ref();
66                    x.to_str().unwrap_or_default().to_string()
67                },
68            );
69            let n1 = it.next_back().map_or_else(
70                || "".to_string(),
71                |x| {
72                    let x: &OsStr = x.as_ref();
73                    x.to_str().unwrap_or_default().to_string()
74                },
75            );
76            testspec.name = format!("{n1}-{n2}");
77        }
78        testspec.dir = dirname;
79        Ok(testspec)
80    }
81
82    pub fn schema() -> RootSchema {
83        schema_for!(TestSpec)
84    }
85}
86
87#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
88pub struct StepSpec {
89    pub name: String,
90    #[serde(default)]
91    pub bucket: Vec<BucketSpec>,
92    #[serde(default)]
93    pub watch: Vec<WatchSpec>,
94    #[serde(default)]
95    pub apply: Vec<ApplySpec>,
96    #[serde(default)]
97    pub delete: Vec<ApplySpec>,
98    #[serde(default)]
99    pub script: Vec<ScriptSpec>,
100    #[serde(default)]
101    pub sleep: u16,
102    #[serde(default)]
103    pub wait: Vec<WaitSpec>,
104}
105
106pub type ScriptSpec = String;
107
108#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
109pub struct BucketSpec {
110    pub name: String,
111    pub operations: HashSet<BucketOperation>,
112}
113
114#[derive(
115    Clone, Serialize, Deserialize, JsonSchema, Eq, Hash, PartialEq, DisplayAsJsonPretty, DebugAsJson,
116)]
117#[serde(rename_all = "lowercase")]
118pub enum BucketOperation {
119    Create,
120    Patch,
121    Delete,
122}
123
124#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
125pub struct WatchSpec {
126    pub name: String,
127    #[serde(default)]
128    pub kind: String,
129    #[serde(default)]
130    pub group: String,
131    #[serde(default)]
132    pub version: String,
133    #[serde(default = "default_namespace")]
134    pub namespace: String,
135    #[serde(default)]
136    pub labels: Option<BTreeMap<String, String>>,
137    #[serde(default)]
138    pub fields: Option<BTreeMap<String, String>>,
139}
140
141impl EnvSubst for WatchSpec {
142    fn subst_env(self, env: &Env) -> Self {
143        WatchSpec {
144            name: self.name,
145            kind: subst_or_not(self.kind, env),
146            group: subst_or_not(self.group, env),
147            version: subst_or_not(self.version, env),
148            namespace: subst_or_not(self.namespace, env),
149            labels: self.labels,
150            fields: self.fields,
151        }
152    }
153}
154
155#[derive(Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
156pub struct ApplySpec {
157    pub path: String,
158    #[serde(default = "default_namespace")]
159    pub namespace: String,
160    #[serde(default = "default_override_namespace", rename = "override-namespace")]
161    pub override_namespace: bool,
162}
163
164fn default_override_namespace() -> bool {
165    true
166}
167fn default_namespace() -> String {
168    "${BLACKJACK_NAMESPACE}".to_string()
169}
170
171impl EnvSubst for ApplySpec {
172    fn subst_env(self, env: &Env) -> Self {
173        ApplySpec {
174            path: subst_or_not(self.path, env),
175            namespace: subst_or_not(self.namespace, env),
176            override_namespace: self.override_namespace,
177        }
178    }
179}
180
181#[derive(Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
182pub struct WaitSpec {
183    pub target: String,
184    pub condition: Expr,
185    pub timeout: u16,
186}
187
188impl EnvSubst for WaitSpec {
189    fn subst_env(self, env: &Env) -> Self {
190        WaitSpec {
191            target: self.target,
192            condition: self.condition.subst_env(env),
193            timeout: self.timeout,
194        }
195    }
196}
197
198#[derive(Clone, Serialize, Deserialize, JsonSchema, DebugAsJson)]
199#[serde(untagged)]
200pub enum Expr {
201    AndExpr { and: Vec<Expr> },
202    OrExpr { or: Vec<Expr> },
203    NotExpr { not: Box<Expr> },
204    SizeExpr { size: usize },
205    OneExpr { one: serde_json::Value },
206    AllExpr { all: serde_json::Value },
207}
208
209impl EnvSubst for Expr {
210    fn subst_env(self, env: &Env) -> Self {
211        match self {
212            Expr::AndExpr { and } => Expr::AndExpr {
213                and: and.into_iter().map(|expr| expr.subst_env(env)).collect(),
214            },
215            Expr::OrExpr { or } => Expr::OrExpr {
216                or: or.into_iter().map(|expr| expr.subst_env(env)).collect(),
217            },
218            Expr::NotExpr { not } => Expr::NotExpr {
219                not: Box::new(not.subst_env(env)),
220            },
221            Expr::SizeExpr { size } => Expr::SizeExpr { size },
222            Expr::OneExpr { one } => Expr::OneExpr {
223                one: env_subst_json(one, env),
224            },
225            Expr::AllExpr { all } => Expr::AllExpr {
226                all: env_subst_json(all, env),
227            },
228        }
229    }
230}
231
232impl std::fmt::Display for Expr {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        match self {
235            Expr::AndExpr { and } => {
236                let exprs: Vec<String> = and.iter().map(|e| format!("{}", e)).collect();
237                write!(f, "AND({})", exprs.join(", "))
238            }
239            Expr::OrExpr { or } => {
240                let exprs: Vec<String> = or.iter().map(|e| format!("{}", e)).collect();
241                write!(f, "OR({})", exprs.join(", "))
242            }
243            Expr::NotExpr { not } => {
244                write!(f, "NOT({})", not)
245            }
246            Expr::SizeExpr { size } => {
247                write!(f, "size == {}", size)
248            }
249            Expr::OneExpr { one } => {
250                write!(f, "ANY({})", one)
251            }
252            Expr::AllExpr { all } => {
253                write!(f, "ALL({})", all)
254            }
255        }
256    }
257}
258
259fn env_subst_json(value: serde_json::Value, env: &Env) -> serde_json::Value {
260    match value {
261        serde_json::Value::String(s) => serde_json::Value::String(subst_or_not(s, env)),
262        serde_json::Value::Array(arr) => {
263            let new_arr = arr.into_iter().map(|v| env_subst_json(v, env)).collect();
264            serde_json::Value::Array(new_arr)
265        }
266        serde_json::Value::Object(obj) => {
267            let new_obj = obj
268                .into_iter()
269                .map(|(k, v)| (k, env_subst_json(v, env)))
270                .collect();
271            serde_json::Value::Object(new_obj)
272        }
273        other => other,
274    }
275}
276
277fn subst_or_not(s: String, env: &Env) -> String {
278    envsubst::substitute(&s, env).or::<String>(Ok(s)).unwrap()
279}