ephemeris_report/
report.rs1use std::fs;
2use std::path::Path;
3
4use facet::Facet;
5
6use crate::{Details, Outcome, ReportError, SkipReason};
7
8const FILE: &str = "report.json";
9
10#[derive(Clone, Debug, Eq, Facet, PartialEq)]
12pub struct Attachment {
13 pub name: String,
15 pub path: String,
17}
18
19impl Attachment {
20 pub const OUTPUT: &str = "output";
22}
23
24#[derive(Clone, Debug, Eq, Facet, PartialEq)]
26pub struct Report {
27 pub check: String,
29 pub outcome: Outcome,
31 #[facet(default, skip_serializing_if = Option::is_none)]
33 pub skipped: Option<SkipReason>,
34 #[facet(default, skip_serializing_if = Option::is_none)]
36 pub status: Option<i32>,
37 #[facet(default, skip_serializing_if = Option::is_none)]
39 pub details: Option<Details>,
40 #[facet(default)]
42 pub attachments: Vec<Attachment>,
43}
44
45impl Report {
46 #[must_use]
48 pub fn plain(check: &str, outcome: Outcome, status: Option<i32>) -> Self {
49 Self {
50 check: check.to_owned(),
51 outcome,
52 skipped: None,
53 status,
54 details: None,
55 attachments: Vec::new(),
56 }
57 }
58
59 #[must_use]
61 pub fn skipped(check: &str, reason: SkipReason) -> Self {
62 Self {
63 skipped: Some(reason),
64 ..Self::plain(check, Outcome::Skip, None)
65 }
66 }
67
68 #[must_use]
70 pub fn passed(&self) -> bool {
71 self.outcome != Outcome::Fail
72 }
73
74 pub fn attach(
82 &mut self,
83 directory: &Path,
84 name: &str,
85 file: &str,
86 contents: &[u8],
87 ) -> Result<(), ReportError> {
88 fs::create_dir_all(directory).map_err(|source| ReportError::io(directory, source))?;
89 let path = directory.join(file);
90 fs::write(&path, contents).map_err(|source| ReportError::io(path, source))?;
91 self.attachments.push(Attachment {
92 name: name.to_owned(),
93 path: file.to_owned(),
94 });
95 Ok(())
96 }
97
98 pub fn write(&self, directory: &Path) -> Result<(), ReportError> {
104 fs::create_dir_all(directory).map_err(|source| ReportError::io(directory, source))?;
105 let path = directory.join(FILE);
106 let mut text =
107 facet_json::to_string_pretty(self).map_err(|source| ReportError::Encode {
108 source: Box::new(source),
109 })?;
110 text.push('\n');
111 fs::write(&path, text).map_err(|source| ReportError::io(path, source))
112 }
113
114 pub fn read(directory: &Path) -> Result<Self, ReportError> {
120 let path = directory.join(FILE);
121 let text = fs::read_to_string(&path).map_err(|source| ReportError::io(&path, source))?;
122 facet_json::from_str(&text).map_err(|source| ReportError::Decode {
123 path,
124 source: Box::new(source),
125 })
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use std::fs;
132
133 use super::Report;
134 use crate::{Outcome, ReportError};
135
136 #[test]
137 fn a_tests_report_round_trips_through_its_json() {
138 let json = r#"{
139 "check": "nextest",
140 "outcome": "pass",
141 "details": {
142 "kind": "tests",
143 "summary": {
144 "passed": 1,
145 "failed": 1,
146 "skipped": 0
147 },
148 "items": [
149 {
150 "name": "a::b",
151 "outcome": "pass"
152 },
153 {
154 "name": "a::c",
155 "outcome": "fail",
156 "message": "boom"
157 }
158 ]
159 },
160 "attachments": [
161 {
162 "name": "junit.xml",
163 "path": "junit.xml"
164 }
165 ]
166}"#;
167 let report: Report = facet_json::from_str(json).expect("decodes");
168 let directory = tempfile::tempdir().expect("directory");
169 report.write(directory.path()).expect("writes");
170 assert_eq!(Report::read(directory.path()).expect("reads"), report);
171 let written = fs::read_to_string(directory.path().join("report.json")).expect("file");
172 assert_eq!(written, format!("{json}\n"));
173 }
174
175 #[test]
176 fn a_plain_report_round_trips_without_optional_fields() {
177 let report = Report::plain("lint", Outcome::Fail, Some(42));
178 let directory = tempfile::tempdir().expect("directory");
179 let nested = directory.path().join("reports").join("lint");
180 report.write(&nested).expect("writes");
181 let written = fs::read_to_string(nested.join("report.json")).expect("file");
182 assert_eq!(
183 written,
184 "{\n \"check\": \"lint\",\n \"outcome\": \"fail\",\n \"status\": 42,\n \
185 \"attachments\": []\n}\n"
186 );
187 assert_eq!(Report::read(&nested).expect("reads"), report);
188 }
189
190 #[test]
191 fn reading_a_missing_directory_is_an_io_error() {
192 let directory = tempfile::tempdir().expect("directory");
193 let error = Report::read(&directory.path().join("missing")).unwrap_err();
194 assert!(
195 matches!(error, ReportError::Io { path: _, source: _ }),
196 "{error:?}"
197 );
198 }
199}