lava_api_mock/
junit.rs

1use std::collections::BTreeMap;
2
3use junit_report::{Duration, Report, ReportBuilder, TestCaseBuilder, TestSuiteBuilder};
4use persian_rug::Accessor;
5use regex::Regex;
6use rust_decimal::prelude::ToPrimitive;
7use wiremock::{Request, Respond, ResponseTemplate};
8
9use crate::{PassFail, SharedState, State};
10
11fn get_duration(tc: &crate::TestCase<State>) -> Option<Duration> {
12    tc.measurement.as_ref().map(|m| {
13        Duration::seconds_f64(
14            m.to_f64().unwrap()
15                * match tc.unit.as_ref() {
16                    "seconds" => 1f64,
17                    "hours" => 3600f64,
18                    _ => unimplemented!("testcase unit not handled"),
19                },
20        )
21    })
22}
23
24fn create_junit(job_id: i64, data: &SharedState) -> Report {
25    let data = data.access();
26    let mut m = BTreeMap::new();
27
28    for testcase in data.get_iter::<crate::TestCase<State>>() {
29        let suite = data.get(&testcase.suite);
30        let job = data.get(&suite.job);
31        if job.id == job_id {
32            let (ty, msg) = match testcase.metadata.as_ref() {
33                Some(meta) => {
34                    let m: crate::Metadata = serde_yaml::from_str(meta).unwrap();
35                    (
36                        m.error_type.unwrap_or_default(),
37                        m.error_msg.unwrap_or_default(),
38                    )
39                }
40                None => Default::default(),
41            };
42
43            let tc = match testcase.result {
44                PassFail::Pass => TestCaseBuilder::success(
45                    &testcase.name,
46                    get_duration(testcase).unwrap_or(Duration::seconds(0)),
47                ),
48                PassFail::Fail => TestCaseBuilder::failure(
49                    &testcase.name,
50                    get_duration(testcase).unwrap_or(Duration::seconds(0)),
51                    &ty,
52                    &msg,
53                ),
54                PassFail::Skip => TestCaseBuilder::skipped(&testcase.name),
55                PassFail::Unknown => TestCaseBuilder::error(
56                    &testcase.name,
57                    get_duration(testcase).unwrap_or(Duration::seconds(0)),
58                    &ty,
59                    &msg,
60                ),
61            };
62            m.entry(testcase.suite)
63                .or_insert_with(|| TestSuiteBuilder::new(&suite.name))
64                .add_testcase(tc.build());
65        }
66    }
67
68    let mut rb = ReportBuilder::new();
69    for (_, v) in m.into_iter() {
70        rb.add_testsuite(v.build());
71    }
72    rb.build()
73}
74
75pub struct JunitEndpoint {
76    data: SharedState,
77}
78
79impl Respond for JunitEndpoint {
80    fn respond(&self, request: &Request) -> ResponseTemplate {
81        let rr = Regex::new(r"/api/v0.2/jobs/(?P<parent>[0-9]+)/junit/").unwrap();
82        if let Some(captures) = rr.captures(request.url.as_str()) {
83            let job_id = captures.get(1).unwrap().as_str().parse::<i64>().unwrap();
84            let r = create_junit(job_id, &self.data);
85            let mut v = Vec::new();
86            r.write_xml(&mut v).expect("failed to write junit xml");
87            ResponseTemplate::new(200).set_body_bytes(v)
88        } else {
89            ResponseTemplate::new(404)
90        }
91    }
92}
93
94pub fn junit_endpoint(data: SharedState) -> JunitEndpoint {
95    JunitEndpoint { data }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    use boulder::{
103        GeneratableWithPersianRug, GeneratorWithPersianRug, GeneratorWithPersianRugIterator,
104    };
105    use boulder::{Inc, Pattern, Repeat, Some as GSome, Time};
106    use chrono::{DateTime, Duration, Utc};
107    use persian_rug::Proxy;
108    use rust_decimal_macros::dec;
109    use test_log::test;
110
111    use crate::testcases::Decimal;
112    use crate::{TestCase, TestSuite};
113
114    #[test(tokio::test)]
115    async fn test_read() {
116        let mut p = SharedState::new();
117        {
118            let m = p.mutate();
119
120            let (suite, m) = Proxy::<TestSuite<State>>::generator().generate(m);
121
122            let gen = Proxy::<TestCase<State>>::generator()
123                .name(Pattern!("example-case-{}", Inc(0)))
124                .unit(Repeat!("", "seconds"))
125                .result(|| PassFail::Pass)
126                .measurement(Repeat!(None, Some(Decimal(dec!(0.1000000000)))))
127            // We hard code this here because serde_yaml isn't configurable enough to match the surface form
128            // We check the metadata generator separately
129                .metadata(GSome(Repeat!(
130                    "case: example-case-0\ndefinition: example-definition-0\nresult: pass\n",
131                    "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"
132                )))
133                .logged(Time::new(
134                    DateTime::parse_from_rfc3339("2022-04-11T16:00:00-00:00")
135                        .unwrap()
136                        .with_timezone(&Utc),
137                    Duration::minutes(30),
138                ))
139                .suite(move || suite)
140                .test_set(|| None)
141                .resource_uri(Pattern!("example-resource-uri-{}", Inc(0)));
142
143            let _ = GeneratorWithPersianRugIterator::new(gen, m)
144                .take(4)
145                .collect::<Vec<_>>();
146        }
147
148        let server = wiremock::MockServer::start().await;
149
150        let ep = junit_endpoint(p);
151
152        wiremock::Mock::given(wiremock::matchers::method("GET"))
153            .and(wiremock::matchers::path("/api/v0.2/jobs/0/junit/"))
154            .respond_with(ep)
155            .mount(&server)
156            .await;
157
158        let body = reqwest::get(&format!("{}/api/v0.2/jobs/0/junit/", server.uri()))
159            .await
160            .expect("error getting junit")
161            .bytes()
162            .await
163            .expect("error parsing utf-8 for junit");
164
165        let suites =
166            junit_parser::from_reader(std::io::Cursor::new(body)).expect("failed to parse junit");
167        assert_eq!(suites.suites.len(), 1);
168        for suite in suites.suites.iter() {
169            assert_eq!(suite.cases.len(), 4);
170            for (i, case) in suite.cases.iter().enumerate() {
171                assert!(case.status.is_success());
172                assert_eq!(case.time, if i % 2 == 0 { 0.0f64 } else { 0.1f64 });
173                assert_eq!(case.name, format!("example-case-{}", i))
174            }
175        }
176    }
177}