cargo_difftests/
lib.rs

1/*
2 *        Copyright (c) 2023-2024 Dinu Blanovschi
3 *
4 *    Licensed under the Apache License, Version 2.0 (the "License");
5 *    you may not use this file except in compliance with the License.
6 *    You may obtain a copy of the License at
7 *
8 *        https://www.apache.org/licenses/LICENSE-2.0
9 *
10 *    Unless required by applicable law or agreed to in writing, software
11 *    distributed under the License is distributed on an "AS IS" BASIS,
12 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 *    See the License for the specific language governing permissions and
14 *    limitations under the License.
15 */
16
17#![feature(let_chains)]
18
19use std::collections::BTreeSet;
20use std::path::PathBuf;
21
22use difftest::TestInfo;
23
24use crate::analysis::AnalysisResult;
25use crate::difftest::Difftest;
26use crate::index_data::TestIndex;
27
28pub mod analysis;
29pub mod analysis_data;
30pub mod difftest;
31pub mod index_data;
32pub mod test_rerunner_core;
33pub mod bin_context;
34
35/// Errors that can occur when running `cargo difftests`.
36#[derive(thiserror::Error, Debug)]
37pub enum DifftestsError {
38    /// IO error.
39    #[error("IO error: {0}")]
40    IO(#[from] std::io::Error),
41    /// JSON error (during the deserialization of the
42    /// file at the given [PathBuf], if any).
43    #[error(
44        "JSON error: {0}{}",
45        match &.1 {
46            Some(it) => format!(" (in {it:?})"),
47            None => "".to_owned(),
48        }
49    )]
50    Json(#[source] serde_json::Error, Option<PathBuf>),
51    /// The `self.json` file does not exist.
52    #[error("Self json does not exist: {0:?}")]
53    SelfJsonDoesNotExist(PathBuf),
54    /// The `cargo_difftests_version` file does not exist.
55    #[error("cargo_difftests_version file does not exist: {0:?}")]
56    CargoDifftestsVersionDoesNotExist(PathBuf),
57    /// The content of the `cargo_difftests_version` file indicates
58    /// a mismatch between the version of `cargo_difftests_testclient`
59    /// that generated the difftest and the version of `cargo_difftests`.
60    #[error("cargo difftests version mismatch: {0} (file) != {1} (cargo difftests)")]
61    CargoDifftestsVersionMismatch(String, String),
62    /// The process failed.
63    #[error("process failed: {name}")]
64    ProcessFailed { name: &'static str },
65    /// A [git2::Error] occurred.
66    #[error("git error: {0}")]
67    Git(#[from] git2::Error),
68    /// The difftest has been cleaned.
69    #[error("difftest has been cleaned")]
70    DifftestCleaned,
71
72    #[error("multiple main group binaries: {1:?} (group at {0:?})")]
73    MultipleMainGroupBinaries(PathBuf, BTreeSet<PathBuf>),
74
75    #[error("no difftests found for group {0:?}")]
76    EmptyGroup(PathBuf),
77
78    #[error("invalid config: {0}")]
79    InvalidConfig(analysis::InvalidConfigError),
80}
81
82impl From<serde_json::Error> for DifftestsError {
83    fn from(e: serde_json::Error) -> Self {
84        DifftestsError::Json(e, None)
85    }
86}
87
88pub type DifftestsResult<T = ()> = Result<T, DifftestsError>;
89
90/// Compares two indexes, returning an error consisting of their deltas
91/// if they are different, or [Ok] if they are the same.
92///
93/// This only looks at the files that are touched by the indexes,
94/// and not at individual code regions.
95pub fn compare_indexes_touch_same_files(
96    index_a: &TestIndex,
97    index_b: &TestIndex,
98) -> Result<(), IndexCompareDifferences<TouchSameFilesDifference>> {
99    let mut diffs = IndexCompareDifferences {
100        differences: vec![],
101    };
102
103    let a_files = index_a.files.iter().collect::<BTreeSet<_>>();
104    let b_files = index_b.files.iter().collect::<BTreeSet<_>>();
105
106    diffs.differences.extend(
107        a_files
108            .difference(&b_files)
109            .map(|f| TouchSameFilesDifference::TouchedByFirstOnly((*f).clone())),
110    );
111
112    diffs.differences.extend(
113        b_files
114            .difference(&a_files)
115            .map(|f| TouchSameFilesDifference::TouchedBySecondOnly((*f).clone())),
116    );
117
118    if diffs.differences.is_empty() {
119        Ok(())
120    } else {
121        Err(diffs)
122    }
123}
124
125/// A list of differences between two indexes.
126#[derive(Clone, Debug)]
127pub struct IndexCompareDifferences<D> {
128    differences: Vec<D>,
129}
130
131impl<D> IndexCompareDifferences<D> {
132    pub fn differences(&self) -> &[D] {
133        &self.differences
134    }
135}
136
137/// A difference between two indexes, given by comparing the list
138/// of files that the [Difftest]s they come from touched.
139#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
140pub enum TouchSameFilesDifference {
141    /// A file that was touched by the first index, but not by the second.
142    #[serde(rename = "first_only")]
143    TouchedByFirstOnly(PathBuf),
144    /// A file that was touched by the second index, but not by the first.
145    #[serde(rename = "second_only")]
146    TouchedBySecondOnly(PathBuf),
147}
148
149/// When using `analyze-all`, the output is a JSON stream of type
150/// [Vec]<[AnalyzeAllSingleTest]>.
151///
152/// This is in the library so that it can be used by consumers of the
153/// `cargo difftests` binary.
154#[derive(serde::Serialize, serde::Deserialize)]
155pub struct AnalyzeAllSingleTest {
156    /// The [Difftest] that was analyzed, or [None] if
157    /// the analysis was performed on the index data alone,
158    /// with no [Difftest] associated.
159    pub difftest: Option<Difftest>,
160    /// The description of the test that was analyzed.
161    pub test_info: TestInfo,
162    /// The result of the analysis.
163    pub verdict: AnalysisVerdict,
164}
165
166/// An analysis verdict.
167#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, PartialEq, Eq, Debug)]
168pub enum AnalysisVerdict {
169    /// The analysis found no modifications to the source files used by the test,
170    /// and it should probably not be rerun (unless something else changed, like
171    /// non-source code file system inputs).
172    #[serde(rename = "clean")]
173    Clean,
174    /// The analysis found some modifications to the source files used by the test,
175    /// and the it should be rerun.
176    #[serde(rename = "dirty")]
177    Dirty,
178}
179
180impl From<AnalysisResult> for AnalysisVerdict {
181    fn from(r: AnalysisResult) -> Self {
182        match r {
183            AnalysisResult::Clean => AnalysisVerdict::Clean,
184            AnalysisResult::Dirty => AnalysisVerdict::Dirty,
185        }
186    }
187}