1use chrono::{DateTime, Utc};
4use serde::de::Visitor;
5use serde::{Deserialize, Deserializer};
6use serde_with::DeserializeFromStr;
7use std::fmt;
8use strum::{Display, EnumString};
9
10#[derive(Copy, DeserializeFromStr, Clone, Debug, Display, EnumString, PartialEq, Eq)]
13#[strum(serialize_all = "snake_case")]
14pub enum PassFail {
15 Pass,
16 Fail,
17 Skip,
18 Unknown,
19}
20
21#[derive(Copy, DeserializeFromStr, Clone, Debug, Display, EnumString, PartialEq, Eq)]
24pub enum ErrorType {
25 None,
26 Infrastructure,
27 Configuration,
28 Bug,
29 Canceled,
30 Job,
31 Test,
32 #[strum(serialize = "LAVATimeout")]
33 LavaTimeout,
34 MultinodeTimeout,
35 ObjectNotPersisted,
36 #[strum(serialize = "Unexisting permission codename.")]
37 UnexistingPermissionCodename,
38}
39
40#[derive(Clone, Debug, Deserialize)]
53pub struct Metadata {
54 pub definition: String,
57 pub case: String,
58 pub result: PassFail,
59
60 pub namespace: Option<String>,
62 pub level: Option<String>,
63 pub duration: Option<String>,
65 pub extra: Option<String>,
66
67 pub error_msg: Option<String>,
71 pub error_type: Option<ErrorType>,
72}
73
74#[derive(Clone, Debug, Deserialize)]
78pub struct TestCase {
79 pub id: i64,
80 pub name: String,
81 pub unit: String,
83 pub result: PassFail,
84 pub measurement: Option<String>,
85 #[serde(deserialize_with = "nested_yaml")]
86 pub metadata: Option<Metadata>,
87 pub suite: i64,
88 pub start_log_line: Option<u32>,
89 pub end_log_line: Option<u32>,
90 pub test_set: Option<i64>,
91 pub logged: DateTime<Utc>,
92 pub resource_uri: String,
94}
95
96fn nested_yaml<'de, D, T>(deser: D) -> Result<T, D::Error>
97where
98 D: Deserializer<'de>,
99 T: for<'de3> Deserialize<'de3>,
100{
101 struct StrVisitor<U> {
102 _marker: core::marker::PhantomData<U>,
103 }
104
105 impl<U> Default for StrVisitor<U> {
106 fn default() -> Self {
107 Self {
108 _marker: Default::default(),
109 }
110 }
111 }
112
113 impl<'de, U> Visitor<'de> for StrVisitor<U>
114 where
115 U: for<'de2> Deserialize<'de2>,
116 {
117 type Value = U;
118
119 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
120 formatter.write_str("nested YAML")
121 }
122
123 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
124 where
125 E: serde::de::Error,
126 {
127 serde_yaml::from_str(value).map_err(|e| serde::de::Error::custom(e))
128 }
129
130 fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
131 where
132 E: serde::de::Error,
133 {
134 serde_yaml::from_str(&value).map_err(|e| serde::de::Error::custom(e))
135 }
136 }
137
138 deser.deserialize_str(StrVisitor::default())
139}
140
141#[cfg(test)]
142mod tests {
143 use super::{ErrorType, Metadata, PassFail, TestCase};
144
145 use crate::Lava;
146 use boulder::{Buildable, Builder};
147 use futures::TryStreamExt;
148 use lava_api_mock::{Job, LavaMock, PaginationLimits, PopulationParams, SharedState, State};
149 use persian_rug::Accessor;
150 use std::collections::BTreeMap;
151 use test_log::test;
152
153 #[test]
154 fn test_meta() {
155 let yaml = r#"
156case: http-download
157definition: lava
158duration: '0.35'
159extra: /var/lib/lava-server/default/media/job-output/2022/02/28/5790643/metadata/lava-http-download-1.2.1.yaml
160level: 1.2.1
161namespace: common
162result: pass"#;
163 let meta: Metadata = serde_yaml::from_str(yaml).expect("failed to deserialize metadata");
164 assert_eq!(meta.case, "http-download");
165 assert_eq!(meta.definition, "lava");
166 assert_eq!(meta.duration, Some("0.35".to_string()));
167 assert_eq!(meta.extra, Some("/var/lib/lava-server/default/media/job-output/2022/02/28/5790643/metadata/lava-http-download-1.2.1.yaml".to_string()));
168 assert_eq!(meta.level, Some("1.2.1".to_string()));
169 assert_eq!(meta.namespace, Some("common".to_string()));
170 assert_eq!(meta.result, PassFail::Pass);
171 assert_eq!(meta.error_msg, None);
172 assert_eq!(meta.error_type, None);
173
174 let yaml = r#"
175case: job
176definition: lava
177error_msg: bootloader-interrupt timed out after 30 seconds
178error_type: Infrastructure
179result: fail
180"#;
181 let meta: Metadata = serde_yaml::from_str(yaml).expect("failed to deserialize metadata");
182 assert_eq!(meta.case, "job");
183 assert_eq!(meta.definition, "lava");
184 assert_eq!(meta.duration, None);
185 assert_eq!(meta.extra, None);
186 assert_eq!(meta.level, None);
187 assert_eq!(meta.namespace, None);
188 assert_eq!(meta.result, PassFail::Fail);
189 assert_eq!(
190 meta.error_msg,
191 Some("bootloader-interrupt timed out after 30 seconds".to_string())
192 );
193 assert_eq!(meta.error_type, Some(ErrorType::Infrastructure));
194 }
195
196 #[test]
197 fn test_test_case() {
198 let json = r#"
199{
200 "id": 207021205,
201 "result": "pass",
202 "resource_uri": "http://lava.collabora.co.uk/api/v0.2/jobs/5790643/suites/10892144/tests/207021205/",
203 "unit": "seconds",
204 "name": "http-download",
205 "measurement": "0.2600000000",
206 "metadata": "case: http-download\ndefinition: lava\nduration: '0.26'\nextra: /var/lib/lava-server/default/media/job-output/2022/02/28/5790643/metadata/lava-http-download-1.1.1.yaml\nlevel: 1.1.1\nnamespace: common\nresult: pass\n",
207 "start_log_line": null,
208 "end_log_line": null,
209 "logged": "2022-02-28T19:29:01.998922Z",
210 "suite": 10892144,
211 "test_set": null
212}"#;
213 let tc: TestCase = serde_json::from_str(json).expect("failed to deserialize testcase");
214 assert_eq!(tc.id, 207021205i64);
215 assert_eq!(tc.result, PassFail::Pass);
216 assert_eq!(
217 tc.resource_uri,
218 "http://lava.collabora.co.uk/api/v0.2/jobs/5790643/suites/10892144/tests/207021205/"
219 );
220 assert_eq!(tc.unit, "seconds");
221 assert_eq!(tc.name, "http-download");
222 assert_eq!(tc.measurement, Some("0.2600000000".to_string()));
223 assert!(tc.metadata.is_some());
224 if let Some(ref meta) = tc.metadata {
225 assert_eq!(meta.case, "http-download");
226 assert_eq!(meta.definition, "lava");
227 assert_eq!(meta.duration, Some("0.26".to_string()));
228 assert_eq!(meta.extra, Some("/var/lib/lava-server/default/media/job-output/2022/02/28/5790643/metadata/lava-http-download-1.1.1.yaml".to_string()));
229 assert_eq!(meta.level, Some("1.1.1".to_string()));
230 assert_eq!(meta.namespace, Some("common".to_string()));
231 assert_eq!(meta.result, PassFail::Pass);
232 }
233 assert_eq!(tc.start_log_line, None);
234 assert_eq!(tc.end_log_line, None);
235 assert_eq!(
236 tc.logged,
237 chrono::DateTime::parse_from_rfc3339("2022-02-28T19:29:01.998922Z")
238 .expect("parsing date")
239 );
240 assert_eq!(tc.suite, 10892144i64);
241 assert_eq!(tc.test_set, None);
242 }
243
244 #[test(tokio::test)]
248 async fn test_basic() {
249 let pop = PopulationParams::builder()
250 .jobs(3usize)
251 .test_suites(6usize)
252 .test_cases(20usize)
253 .build();
254 let state = SharedState::new_populated(pop);
255 let server = LavaMock::new(
256 state.clone(),
257 PaginationLimits::builder().test_cases(Some(6)).build(),
258 )
259 .await;
260
261 let mut map = BTreeMap::new();
262 let start = state.access();
263 for t in start.get_iter::<lava_api_mock::TestCase<State>>() {
264 map.insert(t.id, t.clone());
265 }
266
267 let lava = Lava::new(&server.uri(), None).expect("failed to make lava server");
268
269 let mut seen = BTreeMap::new();
270
271 for job in start.get_iter::<Job<State>>() {
272 let mut lt = lava.test_cases(job.id);
273
274 while let Some(test) = lt.try_next().await.expect("failed to get test") {
275 assert!(!seen.contains_key(&test.id));
276 assert!(map.contains_key(&test.id));
277 let tt = map.get(&test.id).unwrap();
278 assert_eq!(test.id, tt.id);
279 assert_eq!(test.name, tt.name);
280 assert_eq!(test.unit, tt.unit);
281 assert_eq!(test.result.to_string(), tt.result.to_string());
282 assert_eq!(
283 test.measurement,
284 tt.measurement.as_ref().map(|m| m.to_string())
285 );
286 assert_eq!(test.suite, start.get(&tt.suite).id);
287 assert_eq!(job.id, start.get(&start.get(&tt.suite).job).id);
288 assert_eq!(test.start_log_line, tt.start_log_line);
289 assert_eq!(test.end_log_line, tt.end_log_line);
290 assert_eq!(test.test_set, tt.test_set.as_ref().map(|t| start.get(t).id));
291 assert_eq!(test.logged, tt.logged);
292 assert_eq!(test.resource_uri, tt.resource_uri);
293
294 seen.insert(test.id, test.clone());
295 }
296 }
297 assert_eq!(seen.len(), 60);
298 }
299}