1use crate::{
3 baseline::Baseline, impl_extra, insights::run_insights::RunInsights, results::Results,
4};
5
6use std::{
8 collections::HashMap,
9 io::{Read, Write},
10 str::FromStr,
11 time::SystemTime,
12};
13
14use semver::Version;
16use serde::{de::Error, Deserialize, Deserializer, Serialize};
17use serde_json::{Result, Value};
18use uuid::Uuid;
19
20pub const REPORT_FORMAT: &str = "CTRF";
21pub const SPEC_VERSION: Version = Version::new(0, 0, 0);
22
23#[derive(Deserialize, Serialize, Debug, PartialEq)]
26#[serde(rename_all = "camelCase")]
27pub struct Report {
28 #[serde(deserialize_with = "deserialize_format")]
29 report_format: String,
30 spec_version: Version,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub report_id: Option<Uuid>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub timestamp: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub generated_by: Option<String>,
37 results: Results,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub insights: Option<RunInsights>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 pub baseline: Option<Baseline>,
42 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
43 extra: HashMap<String, Value>,
44}
45
46impl Report {
47 pub fn new(
49 report_id: Option<Uuid>,
50 timestamp: Option<SystemTime>,
51 generated_by: Option<String>,
52 results: Results,
53 insights: Option<RunInsights>,
54 baseline: Option<Baseline>,
55 ) -> Self {
56 Report {
57 report_format: String::from(REPORT_FORMAT),
58 spec_version: SPEC_VERSION,
59 report_id,
60 timestamp: timestamp.map(|ts| format!("{ts:?}")),
61 generated_by,
62 results,
63 insights,
64 baseline,
65 extra: HashMap::new(),
66 }
67 }
68
69 pub fn from_slice(s: &[u8]) -> Result<Self> {
71 serde_json::from_slice(s)
72 }
73
74 pub fn from_value(v: Value) -> Result<Self> {
76 serde_json::from_value(v)
77 }
78
79 pub fn from_reader(r: impl Read) -> Result<Self> {
81 serde_json::from_reader(r)
82 }
83
84 pub fn results(&self) -> &Results {
86 &self.results
87 }
88
89 pub fn to_string(&self) -> Result<String> {
91 serde_json::to_string(self)
92 }
93
94 pub fn to_string_pretty(&self) -> Result<String> {
96 serde_json::to_string_pretty(self)
97 }
98
99 pub fn to_vec(&self) -> Result<Vec<u8>> {
101 serde_json::to_vec(self)
102 }
103
104 pub fn to_vec_pretty(&self) -> Result<Vec<u8>> {
106 serde_json::to_vec_pretty(self)
107 }
108
109 pub fn to_writer(&self, writer: impl Write) -> Result<()> {
111 serde_json::to_writer(writer, self)
112 }
113
114 pub fn to_writer_pretty(&self, writer: impl Write) -> Result<()> {
116 serde_json::to_writer_pretty(writer, self)
117 }
118}
119
120impl FromStr for Report {
121 type Err = serde_json::Error;
122
123 fn from_str(s: &str) -> Result<Self> {
125 serde_json::from_str(s)
126 }
127}
128
129fn deserialize_format<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
130where
131 D: Deserializer<'de>,
132{
133 let s: String = Deserialize::deserialize(deserializer)?;
134
135 if s == REPORT_FORMAT {
136 Ok(s)
137 } else {
138 Err(D::Error::custom(format!(
139 "unrecognized report format '{s}'"
140 )))
141 }
142}
143
144impl_extra!(Report);
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 use crate::{results::ResultsBuilder, tool::Tool};
151
152 use std::time::{Duration, UNIX_EPOCH};
153
154 const TEMPLATE_JSON: &str = r#"{
155 "reportFormat": "CTRF",
156 "specVersion": "0.0.0",
157 "results": {
158 "tool": {
159 "name": "ctrf-rs",
160 "version": "VERSION"
161 },
162 "summary": {
163 "tests": 0,
164 "passed": 0,
165 "failed": 0,
166 "pending": 0,
167 "skipped": 0,
168 "other": 0,
169 "start": START,
170 "stop": STOP,
171 "duration": 0
172 },
173 "tests": []
174 }
175}"#;
176
177 #[test]
178 fn create_empty_report() {
179 let time = SystemTime::now();
180 let results = ResultsBuilder::new(Tool::new()).build(time, time);
181 let report = Report::new(None, None, None, results, None, None);
182
183 assert_eq!(report.report_format, REPORT_FORMAT);
184 assert_eq!(report.spec_version, SPEC_VERSION);
185 assert_eq!(report.report_id, None);
186 assert_eq!(report.timestamp, None);
187 assert_eq!(report.generated_by, None);
188 }
189
190 #[test]
191 fn create_report_with_id() {
192 let time = SystemTime::now();
193 let results = ResultsBuilder::new(Tool::new()).build(time, time);
194 let id = Some(Uuid::new_v4());
195 let report = Report::new(id, None, None, results, None, None);
196
197 assert_eq!(report.report_format, REPORT_FORMAT);
198 assert_eq!(report.spec_version, SPEC_VERSION);
199 assert_eq!(report.report_id, id);
200 assert_eq!(report.timestamp, None);
201 assert_eq!(report.generated_by, None);
202 }
203
204 #[test]
205 fn create_report_with_timestamp() {
206 let time = SystemTime::now();
207 let results = ResultsBuilder::new(Tool::new()).build(time, time);
208 let ts = Some(time);
209 let report = Report::new(None, ts, None, results, None, None);
210
211 assert_eq!(report.report_format, REPORT_FORMAT);
212 assert_eq!(report.spec_version, SPEC_VERSION);
213 assert_eq!(report.report_id, None);
214 assert_eq!(report.timestamp, Some(format!("{time:?}")));
215 assert_eq!(report.generated_by, None);
216 }
217
218 #[test]
219 fn create_report_with_generated_by() {
220 let time = SystemTime::now();
221 let results = ResultsBuilder::new(Tool::new()).build(time, time);
222 let gen_by = Some(String::from("ctrf-rs"));
223 let report = Report::new(None, None, gen_by, results, None, None);
224
225 assert_eq!(report.report_format, REPORT_FORMAT);
226 assert_eq!(report.spec_version, SPEC_VERSION);
227 assert_eq!(report.report_id, None);
228 assert_eq!(report.timestamp, None);
229 assert_eq!(report.generated_by, Some(String::from("ctrf-rs")));
230 }
231
232 #[test]
233 fn serialize_to_string() {
234 let time = SystemTime::now();
235 let results = ResultsBuilder::new(Tool::new()).build(time, time);
236 let report = Report::new(None, None, None, results, None, None);
237
238 assert_eq!(report.report_format, REPORT_FORMAT);
239 assert_eq!(report.spec_version, SPEC_VERSION);
240
241 let report_text = report.to_string().expect("report generation failed");
242 let exp_text = r#"{"reportFormat":"CTRF","specVersion":"0.0.0","results":{"tool":{"name":"ctrf-rs","version":"VERSION"},"summary":{"tests":0,"passed":0,"failed":0,"pending":0,"skipped":0,"other":0,"start":START,"stop":STOP,"duration":0},"tests":[]}}"#
243 .replace("VERSION", env!("CARGO_PKG_VERSION"));
244 let time_str = time
245 .duration_since(UNIX_EPOCH)
246 .expect("time conversion error")
247 .as_millis()
248 .to_string();
249
250 assert_eq!(
251 report_text,
252 exp_text
253 .replace("START", &time_str)
254 .replace("STOP", &time_str)
255 );
256 }
257
258 #[test]
259 fn serialize_to_string_pretty() {
260 let time = SystemTime::now();
261 let results = ResultsBuilder::new(Tool::new()).build(time, time);
262 let report = Report::new(None, None, None, results, None, None);
263
264 assert_eq!(report.report_format, REPORT_FORMAT);
265 assert_eq!(report.spec_version, SPEC_VERSION);
266
267 let report_text = report.to_string_pretty().expect("report generation failed");
268 let time_str = time
269 .duration_since(UNIX_EPOCH)
270 .expect("time conversion error")
271 .as_millis()
272 .to_string();
273
274 assert_eq!(
275 report_text,
276 TEMPLATE_JSON
277 .replace("START", &time_str)
278 .replace("STOP", &time_str)
279 .replace("VERSION", env!("CARGO_PKG_VERSION"))
280 );
281 }
282
283 #[test]
286 fn deserialize_happy_path() -> Result<()> {
287 let time = 1234567890000_u64;
288 let time_str = time.to_string();
289 let json = TEMPLATE_JSON
290 .replace("START", &time_str)
291 .replace("STOP", &time_str)
292 .replace("VERSION", env!("CARGO_PKG_VERSION"));
293
294 let report = Report::from_str(&json)?;
295
296 assert_eq!(report.report_format, REPORT_FORMAT);
297 assert_eq!(report.spec_version, SPEC_VERSION);
298
299 let time_sys = SystemTime::UNIX_EPOCH + Duration::from_millis(time);
300 let results = ResultsBuilder::new(Tool::new()).build(time_sys, time_sys);
301 let report_exp = Report::new(None, None, None, results, None, None);
302
303 assert_eq!(report, report_exp);
304
305 Ok(())
306 }
307
308 #[test]
309 fn deserialize_bad_format() {
310 let bad_format = "INVALID";
311 let json = TEMPLATE_JSON.replace("CTRF", bad_format);
312
313 let report_result = Report::from_str(&json);
314 let exp_msg = format!("unrecognized report format '{bad_format}'");
315
316 match report_result {
317 Ok(_) => panic!("report deserialization should have failed"),
318 Err(e) => {
319 if !e.to_string().contains(&exp_msg) {
320 panic!(
321 "deserialization result did not contain expected message \"{}\"",
322 exp_msg
323 );
324 }
325 }
326 }
327 }
328
329 }