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#[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#[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 pub resource_uri: Option<String>,
121}
122
123#[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#[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 #[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 #[django(sort, op(lt, lte, gt, gte))]
259 pub measurement: Option<Decimal>,
260 #[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 pub resource_uri: String,
282}
283
284#[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
354pub struct MetadataGenerator(<Metadata as Generatable>::Generator);
356
357impl MetadataGenerator {
358 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["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["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 .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}