test_output_directory/
v1.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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
99/// Saves a summary of test results in the experimental format.
100pub(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
150/// Retrieve a test result summary from the given directory.
151pub(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        // This verifies version is serialized.
196
197        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}