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 .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}