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