lava_api_mock/
testcases.rs

1use boulder::{
2    Buildable, BuildableWithPersianRug, Builder, Generatable, GeneratableWithPersianRug, Generator,
3};
4use boulder::{Cycle, Inc, Pattern, Repeat, Some as GSome, Time};
5use chrono::{DateTime, Duration, Utc};
6use core::fmt::{Display, Formatter};
7use core::ops::{Deref, DerefMut};
8use core::str::FromStr;
9use django_query::{
10    filtering::FilterableWithPersianRug, row::IntoRowWithPersianRug,
11    sorting::SortableWithPersianRug,
12};
13use rust_decimal_macros::dec;
14use serde::{Deserialize, Serialize};
15use serde_with::{DeserializeFromStr, SerializeDisplay};
16use strum::{Display, EnumString};
17
18use crate::devices::Device;
19use crate::devicetypes::{Alias, Architecture, BitWidth, Core, DeviceType, ProcessorFamily};
20use crate::jobs::Job;
21use crate::tags::Tag;
22use crate::users::{Group, User};
23use crate::workers::Worker;
24
25use persian_rug::{contextual, Context, Proxy};
26
27/// A representation of the metadata for a test case.
28#[derive(Clone, Debug, Deserialize, Serialize, Buildable, Generatable)]
29pub struct Metadata {
30    #[boulder(default = "lava")]
31    pub definition: String,
32    #[boulder(default = "example-stage")]
33    pub case: String,
34    #[boulder(default=PassFail::Pass, generator=Repeat!(PassFail::Pass, PassFail::Fail))]
35    pub result: PassFail,
36    #[boulder(default=Some("common".to_string()), generator=Repeat!(Some("common".to_string()), None))]
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub namespace: Option<String>,
39    #[boulder(default=Some("1.1".to_string()), generator=Repeat!(Some("1.1".to_string()), None))]
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub level: Option<String>,
42    #[boulder(default=Decimal(dec!(1.234)), generator=Repeat!(Some(dec!(1.234).into()), None))]
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub duration: Option<Decimal>,
45    #[boulder(default=Some("example-definition.yaml".to_string()),
46              generator=Repeat!(Some("example-definition.yaml".to_string()), None))]
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub extra: Option<String>,
49
50    #[boulder(generator=Repeat!(None, Some("example error message".to_string())))]
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub error_msg: Option<String>,
53    #[boulder(generator=Repeat!(None, Some("Infrastructure".to_string())))]
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub error_type: Option<String>,
56}
57
58/// A suite of tests from the LAVA API.
59#[derive(
60    Clone,
61    Debug,
62    FilterableWithPersianRug,
63    SortableWithPersianRug,
64    IntoRowWithPersianRug,
65    BuildableWithPersianRug,
66    GeneratableWithPersianRug,
67)]
68#[boulder(
69    persian_rug(
70        context=C,
71        access(
72            Alias<C>,
73            Architecture<C>,
74            BitWidth<C>,
75            Core<C>,
76            Device<C>,
77            DeviceType<C>,
78            Group<C>,
79            Job<C>,
80            ProcessorFamily<C>,
81            Tag<C>,
82            User<C>,
83            Worker<C>,
84            TestSuite<C>
85        )
86    )
87)]
88#[django(
89    persian_rug(
90        context=C,
91        access(
92            Alias<C>,
93            Architecture<C>,
94            BitWidth<C>,
95            Core<C>,
96            Device<C>,
97            DeviceType<C>,
98            Group<C>,
99            Job<C>,
100            ProcessorFamily<C>,
101            Tag<C>,
102            User<C>,
103            Worker<C>,
104            TestSuite<C>
105        )
106    )
107)]
108#[contextual(C)]
109pub struct TestSuite<C: Context + 'static> {
110    #[boulder(generator=Inc(0))]
111    #[django(sort, op(lt, gt))]
112    pub id: i64,
113    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
114    #[django(traverse, foreign_key = "id")]
115    pub job: Proxy<Job<C>>,
116    #[boulder(default="Example suite name", generator=Pattern!("Example suite {}", Inc(1i32)))]
117    #[django(sort, op(in, contains, icontains, startswith, endswith))]
118    pub name: String,
119    // from v02 api
120    pub resource_uri: Option<String>,
121}
122
123/// A set of tests from the LAVA API.
124#[derive(
125    Clone,
126    Debug,
127    FilterableWithPersianRug,
128    SortableWithPersianRug,
129    IntoRowWithPersianRug,
130    BuildableWithPersianRug,
131    GeneratableWithPersianRug,
132)]
133#[boulder(
134    persian_rug(
135        context=C,
136        access(
137            Alias<C>,
138            Architecture<C>,
139            BitWidth<C>,
140            Core<C>,
141            Device<C>,
142            DeviceType<C>,
143            Group<C>,
144            Job<C>,
145            ProcessorFamily<C>,
146            Tag<C>,
147            User<C>,
148            Worker<C>,
149            TestSet<C>,
150            TestSuite<C>
151        )
152    )
153)]
154#[django(
155    persian_rug(
156        context=C,
157        access(
158            Alias<C>,
159            Architecture<C>,
160            BitWidth<C>,
161            Core<C>,
162            Device<C>,
163            DeviceType<C>,
164            Group<C>,
165            Job<C>,
166            ProcessorFamily<C>,
167            Tag<C>,
168            User<C>,
169            Worker<C>,
170            TestSet<C>,
171            TestSuite<C>
172        )
173    )
174)]
175#[contextual(C)]
176pub struct TestSet<C: Context + 'static> {
177    #[boulder(generator=Inc(0))]
178    #[django(sort, op(lt, gt))]
179    pub id: i64,
180    #[boulder(default=Some("Example test set".to_string()), generator=GSome(Pattern!("Example set {}", Inc(1i32))))]
181    #[django(sort, op(in, contains, icontains, startswith, endswith))]
182    pub name: Option<String>,
183    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
184    #[django(traverse, foreign_key = "id")]
185    pub suite: Proxy<TestSuite<C>>,
186}
187
188/// A test from the LAVA API.
189#[derive(
190    Clone,
191    Debug,
192    FilterableWithPersianRug,
193    SortableWithPersianRug,
194    IntoRowWithPersianRug,
195    BuildableWithPersianRug,
196    GeneratableWithPersianRug,
197)]
198#[boulder(
199    persian_rug(
200        context=C,
201        access(
202            Alias<C>,
203            Architecture<C>,
204            BitWidth<C>,
205            Core<C>,
206            Device<C>,
207            DeviceType<C>,
208            Group<C>,
209            Job<C>,
210            ProcessorFamily<C>,
211            Tag<C>,
212            User<C>,
213            Worker<C>,
214            TestCase<C>,
215            TestSet<C>,
216            TestSuite<C>
217        )
218    )
219)]
220#[django(
221    persian_rug(
222        context=C,
223        access(
224            Alias<C>,
225            Architecture<C>,
226            BitWidth<C>,
227            Core<C>,
228            Device<C>,
229            DeviceType<C>,
230            Group<C>,
231            Job<C>,
232            ProcessorFamily<C>,
233            Tag<C>,
234            User<C>,
235            Worker<C>,
236            TestCase<C>,
237            TestSet<C>,
238            TestSuite<C>
239        )
240    )
241)]
242#[contextual(C)]
243pub struct TestCase<C: Context + 'static> {
244    #[boulder(generator=Inc(0))]
245    #[django(sort, op(lt, gt, in))]
246    pub id: i64,
247    #[boulder(default="An example test case", generator=Pattern!("Test case {}", Inc(0usize)))]
248    #[django(sort, op(in, contains, icontains, startswith, endswith))]
249    pub name: String,
250    // Renamed in the v02 api from "units" (in the model) to "unit"
251    #[boulder(default="seconds", generator=Cycle::new(vec!["seconds".to_string(), "hours".to_string()].into_iter()))]
252    #[django(sort, op(in, contains, icontains, startswith, endswith))]
253    pub unit: String,
254    #[boulder(default=PassFail::Pass)]
255    #[django(sort)]
256    pub result: PassFail,
257    // FIXME: better default
258    #[django(sort, op(lt, lte, gt, gte))]
259    pub measurement: Option<Decimal>,
260    // Has to be a string because of the filtering and sorting
261    #[boulder(default=Some(serde_yaml::to_string(&Metadata::builder().build()).unwrap()),
262              generator=GSome(MetadataGenerator::new()))]
263    #[django(sort, op(in, contains, icontains, startswith, endswith))]
264    pub metadata: Option<String>,
265    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
266    #[django(traverse, foreign_key = "id")]
267    pub suite: Proxy<TestSuite<C>>,
268    #[django(sort, op(lt, lte, gt, gte))]
269    pub start_log_line: Option<u32>,
270    #[django(sort, op(lt, lte, gt, gte))]
271    pub end_log_line: Option<u32>,
272    #[boulder(buildable_with_persian_rug, generatable_with_persian_rug)]
273    #[django(traverse, foreign_key = "id")]
274    pub test_set: Option<Proxy<TestSet<C>>>,
275    #[boulder(default=DateTime::parse_from_rfc3339("2022-03-26T21:00:00-00:00").unwrap().with_timezone(&Utc),
276              generator=Time::new(DateTime::parse_from_rfc3339("2022-03-26T21:00:00-00:00").unwrap().with_timezone(&Utc),
277                                  Duration::minutes(1)))]
278    #[django(sort, op(lt, lte, gt, gte))]
279    pub logged: DateTime<Utc>,
280    // from v02 api
281    pub resource_uri: String,
282}
283
284/// A test result from the LAVA API
285#[derive(
286    Copy,
287    Clone,
288    Debug,
289    PartialEq,
290    Eq,
291    PartialOrd,
292    Ord,
293    EnumString,
294    Display,
295    SerializeDisplay,
296    DeserializeFromStr,
297)]
298#[strum(serialize_all = "snake_case")]
299pub enum PassFail {
300    Fail,
301    Pass,
302    Skip,
303    Unknown,
304}
305
306impl django_query::filtering::ops::Scalar for PassFail {}
307impl django_query::row::StringCellValue for PassFail {}
308
309#[doc(hidden)]
310#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
311pub struct Decimal(pub rust_decimal::Decimal);
312
313impl Deref for Decimal {
314    type Target = rust_decimal::Decimal;
315    fn deref(&self) -> &rust_decimal::Decimal {
316        &self.0
317    }
318}
319
320impl DerefMut for Decimal {
321    fn deref_mut(&mut self) -> &mut rust_decimal::Decimal {
322        &mut self.0
323    }
324}
325
326impl FromStr for Decimal {
327    type Err = <rust_decimal::Decimal as FromStr>::Err;
328    fn from_str(value: &str) -> Result<Decimal, Self::Err> {
329        Ok(Self(rust_decimal::Decimal::from_str(value)?))
330    }
331}
332
333impl Display for Decimal {
334    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
335        self.0.fmt(f)
336    }
337}
338
339impl From<rust_decimal::Decimal> for Decimal {
340    fn from(val: rust_decimal::Decimal) -> Decimal {
341        Self(val)
342    }
343}
344
345impl From<Decimal> for rust_decimal::Decimal {
346    fn from(val: Decimal) -> rust_decimal::Decimal {
347        val.0
348    }
349}
350
351impl django_query::filtering::ops::Scalar for Decimal {}
352impl django_query::row::StringCellValue for Decimal {}
353
354/// YAML encoded [`Metadata`] objects.
355pub struct MetadataGenerator(<Metadata as Generatable>::Generator);
356
357impl MetadataGenerator {
358    /// Create a new generator
359    ///
360    /// Note that this generator can only contain a default
361    /// [`Metadata`] generator at present. It's convenient only when
362    /// you aren't particularly interested in the actual data, you
363    /// just need something parseable.
364    pub fn new() -> Self {
365        Self(Metadata::generator())
366    }
367}
368
369impl Generator for MetadataGenerator {
370    type Output = String;
371    fn generate(&mut self) -> Self::Output {
372        serde_yaml::to_string(&self.0.generate()).unwrap()
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::{SharedState, State};
380
381    use boulder::GeneratorWithPersianRugIterator;
382    use boulder::{BuilderWithPersianRug, GeneratableWithPersianRug};
383    use django_query::row::{CellValue, IntoRowWithContext, Serializer};
384    use persian_rug::Proxy;
385    use serde_json::Number;
386    use test_log::test;
387
388    #[test]
389    fn test_builder() {
390        let mut p = SharedState::new();
391        let tc = {
392            let m = p.mutate();
393
394            let (tc, _) = TestCase::builder().build(m);
395            tc
396        };
397        let map = TestCase::get_serializer(p.access()).to_row(&tc);
398        assert_eq!(map["id"], CellValue::Number(Number::from(0)));
399        assert_eq!(
400            map["name"],
401            CellValue::String("An example test case".to_string())
402        );
403        assert_eq!(map["unit"], CellValue::String("seconds".to_string()));
404        assert_eq!(map["result"], CellValue::String("pass".to_string()));
405        assert_eq!(map["measurement"], CellValue::Null);
406        // assert_eq!(map["metadata"], CellValue::String(""));
407        assert_eq!(map["suite"], CellValue::Number(Number::from(0)));
408        assert_eq!(map["start_log_line"], CellValue::Null);
409        assert_eq!(map["end_log_line"], CellValue::Null);
410        assert_eq!(map["test_set"], CellValue::Number(Number::from(0)));
411    }
412
413    #[test]
414    fn test_generator() {
415        let mut p = SharedState::new();
416        let gen = TestCase::<State>::generator();
417
418        let tcs = GeneratorWithPersianRugIterator::new(gen, p.mutate())
419            .take(5)
420            .collect::<Vec<_>>();
421
422        let ser = TestCase::get_serializer(p.access());
423        for (i, tc) in tcs.iter().enumerate() {
424            let map = ser.to_row(tc);
425            let units = ["seconds".to_string(), "hours".to_string()];
426            assert_eq!(map["id"], CellValue::Number(Number::from(i)));
427            assert_eq!(map["name"], CellValue::String(format!("Test case {}", i)));
428            assert_eq!(map["unit"], CellValue::String(units[i % 2].clone()));
429            assert_eq!(map["result"], CellValue::String("pass".to_string()));
430            assert_eq!(map["measurement"], CellValue::Null);
431            // assert_eq!(map["metadata"], CellValue::String(""));
432            assert_eq!(map["suite"], CellValue::Number(Number::from(i)));
433            assert_eq!(map["start_log_line"], CellValue::Null);
434            assert_eq!(map["end_log_line"], CellValue::Null);
435            assert_eq!(map["test_set"], CellValue::Number(Number::from(i)));
436        }
437    }
438
439    #[test]
440    fn test_metadata_output() {
441        let mut mgen = MetadataGenerator(
442            Metadata::generator()
443                .case(Pattern!("example-case-{}", Inc(0)))
444                .definition(Pattern!("example-definition-{}", Inc(0)))
445                .result(|| PassFail::Pass)
446                .level(Repeat!(None, Some("1.1.1".to_string())))
447                .extra(Repeat!(None, Some("example-extra-data".to_string())))
448                .namespace(Repeat!(None, Some("example-namespace".to_string())))
449                .duration(Repeat!(None, Some(Decimal(dec!(0.10)))))
450                .error_msg(|| None)
451                .error_type(|| None),
452        );
453
454        let cases = vec![
455            "case: example-case-0\ndefinition: example-definition-0\nresult: pass\n",
456            "case: example-case-1\ndefinition: example-definition-1\nduration: '0.10'\nextra: example-extra-data\nlevel: 1.1.1\nnamespace: example-namespace\nresult: pass\n",
457        ];
458
459        for case in cases {
460            let control: serde_yaml::Value =
461                serde_yaml::from_str(case).expect("failed to parse control input");
462            let test: serde_yaml::Value =
463                serde_yaml::from_str(&mgen.generate()).expect("failed to generate test data");
464            assert_eq!(test, control);
465        }
466    }
467
468    #[test(tokio::test)]
469    async fn test_output() {
470        let mut p = SharedState::new();
471        {
472            let m = p.mutate();
473
474            let gen = Proxy::<TestCase<State>>::generator()
475                .name(Pattern!("example-case-{}", Inc(0)))
476                .unit(Repeat!("", "seconds"))
477                .result(|| PassFail::Pass)
478                .measurement(Repeat!(None, Some(Decimal(dec!(0.1000000000)))))
479            // We hard code this here because serde_yaml isn't configurable enough to match the surface form
480            // We check the metadata generator separately
481                .metadata(GSome(Repeat!(
482                    "case: example-case-0\ndefinition: example-definition-0\nresult: pass\n",
483                    "case: example-case-1\ndefinition: example-definition-1\nduration: '0.10'\nextra: example-extra-data\nlevel: 1.1.1\nnamespace: example-namespace\nresult: pass\n"
484                )))
485                .logged(Time::new(
486                    DateTime::parse_from_rfc3339("2022-04-11T16:00:00-00:00")
487                        .unwrap()
488                        .with_timezone(&Utc),
489                    Duration::minutes(30),
490                ))
491                .suite(Proxy::<TestSuite<State>>::generator())
492                .test_set(|| None)
493                .resource_uri(Pattern!("example-resource-uri-{}", Inc(0)));
494
495            let _ = GeneratorWithPersianRugIterator::new(gen, m)
496                .take(4)
497                .collect::<Vec<_>>();
498        }
499
500        let server = wiremock::MockServer::start().await;
501
502        let ep = p.endpoint::<TestCase<State>>(Some(&server.uri()), None);
503
504        wiremock::Mock::given(wiremock::matchers::method("GET"))
505            .and(wiremock::matchers::path("/api/v0.2/jobs/0/tests/"))
506            .respond_with(ep)
507            .mount(&server)
508            .await;
509
510        let body: serde_json::Value =
511            reqwest::get(&format!("{}/api/v0.2/jobs/0/tests/?limit=2", server.uri()))
512                .await
513                .expect("error getting tests")
514                .json()
515                .await
516                .expect("error parsing tests");
517
518        let next = format!("{}/api/v0.2/jobs/0/tests/?limit=2&offset=2", server.uri());
519
520        assert_eq!(
521            body,
522            serde_json::json! {
523                {
524                    "count": 4,
525                    "next": next,
526                    "previous": null,
527                    "results": [
528                        {
529                            "id": 0,
530                            "result": "pass",
531                            "resource_uri": "example-resource-uri-0",
532                            "unit": "",
533                            "name": "example-case-0",
534                            "measurement": null,
535                            "metadata": "case: example-case-0\ndefinition: example-definition-0\nresult: pass\n",
536                            "start_log_line": null,
537                            "end_log_line": null,
538                            "logged": "2022-04-11T16:00:00.000000Z",
539                            "suite": 0,
540                            "test_set": null
541                        },
542                        {
543                            "id": 1,
544                            "result": "pass",
545                            "resource_uri": "example-resource-uri-1",
546                            "unit": "seconds",
547                            "name": "example-case-1",
548                            "measurement": "0.1000000000",
549                            "metadata": "case: example-case-1\ndefinition: example-definition-1\nduration: '0.10'\nextra: example-extra-data\nlevel: 1.1.1\nnamespace: example-namespace\nresult: pass\n",
550                            "start_log_line": null,
551                            "end_log_line": null,
552                            "logged": "2022-04-11T16:30:00.000000Z",
553                            "suite": 1,
554                            "test_set": null
555                        }
556                    ]
557                }
558            }
559        );
560    }
561}