lava_api/
test.rs

1//! Retrieve test data
2
3use chrono::{DateTime, Utc};
4use serde::de::Visitor;
5use serde::{Deserialize, Deserializer};
6use serde_with::DeserializeFromStr;
7use std::fmt;
8use strum::{Display, EnumString};
9
10/// The result of running a [`TestCase`], as stored by LAVA
11// From lava/lava_results_app/models.py in TestCase::RESULT_CHOICES
12#[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/// The type of an error that occurred running a test
22// From lava/lava_common/exceptions.py as the error_type fields of the classes
23#[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/// The metadata available for a [`TestCase`] from the LAVA API
41// This structure is an amalgam of things handed to
42// - lava/lava_common/log.py YAMLLogger::results
43// particularly by
44// - lava/lava_dispatcher/action.py Action::log_action_results
45// - lava/lava_dispatcher/job.py Job::validate
46// - lava/lava/dispatcher/lava-run main
47// In particular the failure case is defined in lava-run.
48// These results are then propagated back to
49// - lava/lava_scheduler_app/views.py internal_v1_jobs_logs
50// And then from there to
51// - lava/lava_results_app/dbutils.py map_scanned_results
52#[derive(Clone, Debug, Deserialize)]
53pub struct Metadata {
54    // These three fields are present or the results would have been
55    // rejected earlier by map_scanned_results.
56    pub definition: String,
57    pub case: String,
58    pub result: PassFail,
59
60    // Success case
61    pub namespace: Option<String>,
62    pub level: Option<String>,
63    // This is just a float formatted with "%0.2f"
64    pub duration: Option<String>,
65    pub extra: Option<String>,
66
67    // Failure case
68    // These are not present in the success case, and are added by
69    // lava-run based on the contents of the fault thrown.
70    pub error_msg: Option<String>,
71    pub error_type: Option<ErrorType>,
72}
73
74/// The data available for a test case for a [`Job`](crate::job::Job)
75/// from the LAVA API
76// From lava/lava_results_app/models.py in TestCase
77#[derive(Clone, Debug, Deserialize)]
78pub struct TestCase {
79    pub id: i64,
80    pub name: String,
81    // Renamed in the v02 api from "units" (in the model) to "unit"
82    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    // from v02 api
93    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    /// Stream 20 tests each from 3 jobs with a page limit of 6 from
245    /// the server checking that they are all accounted for (that
246    /// pagination is handled properly)
247    #[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}