arduino_report_size_deltas/reports/
mod.rs

1//! A module for API related to parsing of JSON data from CI artifacts.
2//! Additionally, there's a convenient [`parse_json()`] function for parsing of older
3//! JSON formats produced by the [arduino/compile-sketches] action.
4//!
5//! [arduino/compile-sketches]: https://github.com/arduino/compile-sketches
6use crate::{CommentAssemblyError, JsonError};
7use std::{fs, path::Path};
8pub mod structs;
9use structs::{Report, ReportOld};
10
11/// Deserialize a JSON file at the given `path` into a [`Report`].
12///
13/// This will automatically try to parsing old JSON formats when
14/// parsing the newer format fails syntactically.
15pub(crate) fn parse_json<P: AsRef<Path>>(path: P) -> Result<Report, JsonError> {
16    let asset = fs::read_to_string(path)?;
17    match serde_json::from_str::<Report>(&asset) {
18        Ok(report) => Ok(report),
19        Err(e) => {
20            if e.is_data() {
21                // if parsing the new format fails (for typing reasons),
22                // then try the old format and convert it.
23                match serde_json::from_str::<ReportOld>(&asset) {
24                    Ok(report) => Ok(report.into()),
25                    Err(e_old) => {
26                        eprintln!("Parsing old format failed: {e_old}");
27                        Err(JsonError::Serde(e))
28                    }
29                }
30            } else {
31                Err(JsonError::Serde(e))
32            }
33        }
34    }
35}
36
37/// Recursively scans the given `sketches_path` and parses any existing JSON files as
38/// sketch report artifacts.
39pub fn parse_artifacts<P: AsRef<Path>>(
40    sketches_path: P,
41) -> Result<Vec<Report>, CommentAssemblyError> {
42    let mut reports = vec![];
43    for entry in fs::read_dir(&sketches_path)? {
44        let path = entry?.path();
45        if path.is_dir() {
46            reports.extend(parse_artifacts(path)?);
47        } else if path
48            .extension()
49            .is_some_and(|ext| ext.to_string_lossy() == "json")
50        {
51            let report = parse_json(&path)?;
52            if report.is_valid() {
53                reports.push(report);
54            } else {
55                log::warn!("Skipping {path:?} since it does not contain sufficient information.");
56            }
57        } else {
58            log::debug!("Ignoring non-JSON file: {}", path.to_string_lossy());
59        }
60    }
61    Ok(reports)
62}
63
64#[cfg(test)]
65mod test {
66    use std::io::Write;
67
68    use super::{JsonError, parse_json};
69    use tempfile::NamedTempFile;
70
71    /// Test parsing of JSON report in newer format
72    #[test]
73    fn parse_new() {
74        for entry in std::fs::read_dir("tests/size-deltas-reports-new").unwrap() {
75            let path = entry.unwrap().path();
76            if path.extension().unwrap().to_string_lossy() == "json" {
77                println!("Parsing {path:?}");
78                let report = parse_json(&path).unwrap();
79                assert!(!report.boards.is_empty());
80                assert!(report.is_valid());
81            } else {
82                println!("Skipped parsing non-JSON file: {}", path.to_string_lossy());
83            }
84        }
85    }
86
87    /// Test parsing of JSON report in newer format
88    #[test]
89    fn parse_old() {
90        for entry in std::fs::read_dir("tests/size-deltas-reports-old").unwrap() {
91            let path = entry.unwrap().path();
92            println!("Parsing {path:?}");
93            let report = parse_json(path).unwrap();
94            assert!(!report.boards.is_empty());
95            assert!(!report.is_valid());
96        }
97    }
98
99    #[test]
100    fn absent_file() {
101        let result = parse_json("not-a-file.json");
102        assert!(result.is_err_and(|e| matches!(e, JsonError::FileReadFail(_))));
103    }
104
105    #[test]
106    fn bad_json() {
107        let bad_asset = NamedTempFile::new().unwrap();
108        let result = parse_json(&bad_asset);
109        assert!(result.is_err_and(|e| matches!(e, JsonError::Serde(_))));
110    }
111
112    #[test]
113    fn bad_report() {
114        let mut bad_asset = NamedTempFile::new().unwrap();
115        bad_asset.write_all("{}".as_bytes()).unwrap();
116        let result = parse_json(&bad_asset);
117        assert!(result.is_err_and(|e| matches!(e, JsonError::Serde(_))));
118    }
119}