1use crate::{
6 ArtifactMetadata, ArtifactSubDirectory, CommonResult, MaybeUnknown, Outcome, SchemaVersion,
7 SuiteResult, TestCaseResult, TestRunResult, RUN_NAME, RUN_SUMMARY_NAME,
8};
9use serde::{Deserialize, Serialize};
10use std::borrow::Cow;
11use std::collections::HashMap;
12use std::fs::File;
13use std::io::{BufReader, BufWriter, Error, Write};
14use std::path::{Path, PathBuf};
15use test_list::TestTag;
16
17#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
18struct SerializableCommon<'a> {
19 #[serde(skip_serializing_if = "Option::is_none")]
20 name: Option<Cow<'a, str>>,
21 artifacts: Cow<'a, HashMap<PathBuf, ArtifactMetadata>>,
22 artifact_dir: Cow<'a, Path>,
23 outcome: MaybeUnknown<Outcome>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 start_time: Option<u64>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 duration_milliseconds: Option<u64>,
28}
29
30#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
31struct SerializableTestRun<'a> {
32 #[serde(flatten)]
33 common: SerializableCommon<'a>,
34 suites: Vec<SerializableSuite<'a>>,
35}
36
37#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
38struct SerializableSuite<'a> {
39 #[serde(flatten)]
40 common: SerializableCommon<'a>,
41 cases: Vec<SerializableTestCase<'a>>,
42 tags: Cow<'a, Vec<TestTag>>,
43}
44
45#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
46struct SerializableTestCase<'a> {
47 #[serde(flatten)]
48 common: SerializableCommon<'a>,
49}
50
51#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
52enum SchemaId {
53 #[serde(rename = "https://fuchsia.dev/schema/ffx_test/run_summary-8d1dd964.json")]
54 V1,
55}
56
57#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
58struct VersionedEnvelope<'a> {
59 data: SerializableTestRun<'a>,
60 schema_id: SchemaId,
61}
62
63enum NameOption {
64 Omit,
65 Include,
66}
67
68fn make_serializable_common<'a>(
69 common: &'a CommonResult,
70 omit_name: NameOption,
71) -> SerializableCommon<'a> {
72 SerializableCommon {
73 name: match omit_name {
74 NameOption::Omit => None,
75 NameOption::Include => Some(Cow::Borrowed(&common.name)),
76 },
77 artifacts: Cow::Borrowed(&common.artifact_dir.artifacts),
78 artifact_dir: Cow::Borrowed(&common.artifact_dir.root.file_name().unwrap().as_ref()),
79 outcome: common.outcome.clone(),
80 start_time: common.start_time,
81 duration_milliseconds: common.duration_milliseconds,
82 }
83}
84
85fn make_serializable_suite<'a, 'b>(suite: &'a SuiteResult<'b>) -> SerializableSuite<'a> {
86 SerializableSuite {
87 common: make_serializable_common(&*suite.common, NameOption::Include),
88 cases: suite
89 .cases
90 .iter()
91 .map(|case| SerializableTestCase {
92 common: make_serializable_common(&*case.common, NameOption::Include),
93 })
94 .collect(),
95 tags: Cow::Borrowed(&suite.tags),
96 }
97}
98
99pub(crate) fn save_summary<'a, 'b>(
101 root_path: &'a Path,
102 result: &TestRunResult<'b>,
103) -> Result<(), Error> {
104 let serializable_run = SerializableTestRun {
105 common: make_serializable_common(&*result.common, NameOption::Omit),
106 suites: result.suites.iter().map(make_serializable_suite).collect(),
107 };
108
109 let enveloped = VersionedEnvelope { data: serializable_run, schema_id: SchemaId::V1 };
110
111 let mut file = BufWriter::new(File::create(root_path.join(RUN_SUMMARY_NAME))?);
112 serde_json::to_writer_pretty(&mut file, &enveloped)?;
113 file.flush()
114}
115
116fn from_serializable_common(
117 root_path: &Path,
118 serializable: SerializableCommon<'static>,
119) -> CommonResult {
120 CommonResult {
121 name: serializable.name.unwrap_or(Cow::Borrowed(RUN_NAME)).into_owned(),
122 artifact_dir: ArtifactSubDirectory {
123 version: SchemaVersion::V1,
124 root: root_path.join(serializable.artifact_dir),
125 artifacts: serializable.artifacts.into_owned(),
126 },
127 outcome: serializable.outcome,
128 start_time: serializable.start_time,
129 duration_milliseconds: serializable.duration_milliseconds,
130 }
131}
132
133fn from_serializable_suite(
134 root_path: &Path,
135 serializable: SerializableSuite<'static>,
136) -> SuiteResult<'static> {
137 SuiteResult {
138 common: Cow::Owned(from_serializable_common(root_path, serializable.common)),
139 cases: serializable
140 .cases
141 .into_iter()
142 .map(|case| TestCaseResult {
143 common: Cow::Owned(from_serializable_common(root_path, case.common)),
144 })
145 .collect(),
146 tags: serializable.tags,
147 }
148}
149
150pub(crate) fn parse_from_directory(root_path: &Path) -> Result<TestRunResult<'static>, Error> {
152 let summary_file = BufReader::new(File::open(root_path.join(RUN_SUMMARY_NAME))?);
153 let envelope: VersionedEnvelope<'static> = serde_json::from_reader(summary_file)?;
154
155 Ok(TestRunResult {
156 common: Cow::Owned(from_serializable_common(root_path, envelope.data.common)),
157 suites: envelope
158 .data
159 .suites
160 .into_iter()
161 .map(|suite| from_serializable_suite(root_path, suite))
162 .collect(),
163 })
164}
165
166#[cfg(test)]
167pub fn validate_against_schema(root_path: &Path) {
168 const RUN_SCHEMA: &str =
169 include_str!("../../../../../sdk/schema/ffx_test/run_summary-8d1dd964.json");
170 const COMMON_SCHEMA: &str = include_str!("../../../../../sdk/schema/common-00000000.json");
171 let mut run_scope = valico::json_schema::Scope::new();
172 let common_schema_json = serde_json::from_str(COMMON_SCHEMA).expect("parse common schema");
173 let _ = run_scope.compile(common_schema_json, false).expect("compile common schema");
174 let run_schema_json = serde_json::from_str(RUN_SCHEMA).expect("parse json schema");
175 let run_schema =
176 run_scope.compile_and_return(run_schema_json, false).expect("compile json schema");
177
178 let summary_file =
179 BufReader::new(File::open(root_path.join(RUN_SUMMARY_NAME)).expect("open summary file"));
180 let run_result_value: serde_json::Value =
181 serde_json::from_reader(summary_file).expect("deserialize run from file");
182 let validation = run_schema.validate(&run_result_value);
183 if !validation.is_strictly_valid() {
184 panic!("Run file does not conform with schema: {:#?}", validation);
185 }
186}
187
188#[cfg(test)]
189mod test {
190 use super::*;
191 use serde_json::{from_str, json, to_string, Value};
192
193 #[test]
194 fn run_version_serialized() {
195 let envelope = VersionedEnvelope {
198 data: SerializableTestRun {
199 common: SerializableCommon {
200 name: None,
201 artifacts: Cow::Owned(HashMap::new()),
202 artifact_dir: Cow::Owned(Path::new("artifacts").to_path_buf()),
203 outcome: MaybeUnknown::Known(Outcome::Passed),
204 start_time: None,
205 duration_milliseconds: None,
206 },
207 suites: vec![],
208 },
209 schema_id: SchemaId::V1,
210 };
211
212 let serialized = to_string(&envelope).expect("serialize result");
213 let value = from_str::<Value>(&serialized).expect("deserialize result");
214
215 let expected = json!({
216 "schema_id": "https://fuchsia.dev/schema/ffx_test/run_summary-8d1dd964.json",
217 "data": {
218 "artifacts": {},
219 "artifact_dir": "artifacts",
220 "outcome": "PASSED",
221 "suites": []
222 }
223 });
224
225 assert_eq!(value, expected);
226 }
227
228 #[test]
229 fn run_version_mismatch() {
230 let wrong_version_json = json!({
231 "schema_id": "https://fuchsia.dev/schema/fake-schema",
232 "data": {
233 "artifacts": {},
234 "artifact_dir": "artifacts",
235 "outcome": "PASSED",
236 "suites": []
237 }
238 });
239
240 let serialized = to_string(&wrong_version_json).expect("serialize result");
241
242 assert!(from_str::<SerializableTestRun<'static>>(&serialized).unwrap_err().is_data());
243 }
244}