1use crate::judge::{CaseResult, Status};
4use crate::model::RunId;
5use crate::plan::{NotRun, NotRunReason};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeSet;
8use std::fmt::Write as _;
9
10#[derive(Clone, Debug, Default, Serialize, Deserialize)]
12pub struct EnvironmentRecord {
13 pub game_version: Option<String>,
14 pub dlc: Vec<String>,
15 pub mods: Vec<String>,
16 pub runner: Option<String>,
17}
18
19#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
20pub struct Summary {
21 pub passed: usize,
22 pub smoke: usize,
23 pub xfailed: usize,
24 pub xpassed: usize,
25 pub failed: usize,
26 pub setup_error: usize,
27 pub incomplete: usize,
28 pub protocol_error: usize,
29 pub runtime_error: usize,
30 pub skipped: usize,
31 pub ignored: usize,
32 pub deselected: usize,
33 pub wall_ms: u64,
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct Report {
39 pub run_id: RunId,
40 pub results: Vec<CaseResult>,
41 pub not_run: Vec<NotRun>,
42 pub environment: EnvironmentRecord,
43}
44
45impl Report {
46 pub fn new(
47 run_id: RunId,
48 results: Vec<CaseResult>,
49 not_run: Vec<NotRun>,
50 environment: EnvironmentRecord,
51 ) -> Self {
52 Self {
53 run_id,
54 results,
55 not_run,
56 environment,
57 }
58 }
59
60 pub fn summary(&self) -> Summary {
61 let mut summary = Summary::default();
62 for result in &self.results {
63 let counter = match result.status {
64 Status::Passed => &mut summary.passed,
65 Status::Smoke => &mut summary.smoke,
66 Status::XFailed => &mut summary.xfailed,
67 Status::XPassed { .. } => &mut summary.xpassed,
68 Status::Failed => &mut summary.failed,
69 Status::SetupError => &mut summary.setup_error,
70 Status::Incomplete => &mut summary.incomplete,
71 Status::ProtocolError => &mut summary.protocol_error,
72 Status::RuntimeError => &mut summary.runtime_error,
73 Status::Skipped => &mut summary.skipped,
74 Status::Ignored => &mut summary.ignored,
75 };
76 *counter += 1;
77 }
78 for not_run in &self.not_run {
79 match not_run.reason {
80 NotRunReason::Deselected => summary.deselected += 1,
81 NotRunReason::Skipped { .. } => summary.skipped += 1,
82 NotRunReason::Ignored { .. } => summary.ignored += 1,
83 }
84 }
85 let mut sessions = BTreeSet::new();
87 for result in &self.results {
88 if sessions.insert(result.session.as_str()) {
89 summary.wall_ms += result.timing.wall_ms;
90 }
91 }
92 summary
93 }
94
95 pub fn slowest(&self, count: usize) -> Vec<&CaseResult> {
96 let mut results: Vec<_> = self.results.iter().collect();
97 results.sort_by_key(|result| std::cmp::Reverse(result.timing.wall_ms));
98 results.truncate(count);
99 results
100 }
101
102 pub fn failed_nodes(&self) -> BTreeSet<String> {
104 self.results
105 .iter()
106 .filter(|result| !result.status.is_success())
107 .map(|result| result.node.to_string())
108 .collect()
109 }
110
111 pub fn exit_code(&self) -> i32 {
113 let summary = self.summary();
114 let anything = !self.results.is_empty() || summary.skipped + summary.ignored > 0;
115 i32::from(
116 !anything
117 || self
118 .results
119 .iter()
120 .any(|result| !result.status.is_success()),
121 )
122 }
123
124 pub fn to_json(&self) -> String {
125 serde_json::to_string_pretty(self).expect("reports serialize")
126 }
127
128 pub fn to_junit(&self) -> String {
130 let summary = self.summary();
131 let failures = summary.failed
132 + self
133 .results
134 .iter()
135 .filter(|result| result.status == Status::XPassed { strict: true })
136 .count();
137 let errors = summary.setup_error
138 + summary.incomplete
139 + summary.protocol_error
140 + summary.runtime_error;
141 let skipped = summary.skipped + summary.ignored;
142 let tests = self.results.len() + skipped;
143 let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
144 let _ = writeln!(
145 xml,
146 "<testsuites><testsuite name=\"foch\" tests=\"{tests}\" failures=\"{failures}\" errors=\"{errors}\" skipped=\"{skipped}\" time=\"{:.3}\">",
147 summary.wall_ms as f64 / 1000.0
148 );
149 for result in &self.results {
150 let _ = write!(
151 xml,
152 "<testcase classname=\"{}\" name=\"{}\" time=\"{:.3}\">",
153 escape(result.node.path.as_str()),
154 escape(&result.node.local()),
155 result.timing.wall_ms as f64 / 1000.0
156 );
157 let detail: Vec<String> = result
158 .failures
159 .iter()
160 .map(|failure| match &failure.span {
161 Some(span) => format!("{span} {}: {}", failure.check, failure.message),
162 None => format!("{}: {}", failure.check, failure.message),
163 })
164 .chain(result.notes.iter().cloned())
165 .collect();
166 let detail = escape(&detail.join("\n"));
167 match result.status {
168 Status::Failed | Status::XPassed { strict: true } => {
169 let _ = write!(
170 xml,
171 "<failure message=\"{:?}\">{detail}</failure>",
172 result.status
173 );
174 }
175 Status::SetupError
176 | Status::Incomplete
177 | Status::ProtocolError
178 | Status::RuntimeError => {
179 let _ = write!(
180 xml,
181 "<error message=\"{:?}\">{detail}</error>",
182 result.status
183 );
184 }
185 _ => {}
186 }
187 xml.push_str("</testcase>\n");
188 }
189 for not_run in &self.not_run {
190 let message = match ¬_run.reason {
191 NotRunReason::Deselected => continue,
192 NotRunReason::Skipped { message } | NotRunReason::Ignored { message } => {
193 message.clone().unwrap_or_default()
194 }
195 };
196 let _ = writeln!(
197 xml,
198 "<testcase classname=\"{}\" name=\"{}\"><skipped message=\"{}\"/></testcase>",
199 escape(not_run.node.path.as_str()),
200 escape(¬_run.node.local()),
201 escape(&message)
202 );
203 }
204 xml.push_str("</testsuite></testsuites>\n");
205 xml
206 }
207}
208
209fn escape(text: &str) -> String {
210 text.replace('&', "&")
211 .replace('<', "<")
212 .replace('>', ">")
213 .replace('"', """)
214}