cargo_difftests/
index_data.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//! Holds the [`TestIndex`] struct, and logic for indexing [`CoverageData`] into
18//! a [`TestIndex`].
19
20use std::collections::BTreeMap;
21use std::fs;
22use std::fs::File;
23use std::io::BufWriter;
24use std::path::{Path, PathBuf};
25
26use crate::analysis_data::CoverageData;
27use crate::difftest::TestInfo;
28use crate::{Difftest, DifftestsResult};
29
30#[derive(serde::Serialize, serde::Deserialize)]
31#[serde(transparent)]
32struct IndexRegionSerDe([usize; 6]);
33
34/// A region in a [`TestIndex`].
35#[derive(serde::Serialize, serde::Deserialize, Copy, Clone, Debug)]
36#[serde(from = "IndexRegionSerDe", into = "IndexRegionSerDe")]
37pub struct IndexRegion {
38    /// The line number of the first line of the region.
39    pub l1: usize,
40    /// The column number of the first column of the region.
41    pub c1: usize,
42    /// The line number of the last line of the region.
43    pub l2: usize,
44    /// The column number of the last column of the region.
45    pub c2: usize,
46    /// The number of times the region was executed.
47    pub count: usize,
48    /// The index of the file in the [`TestIndex`].
49    pub file_id: usize,
50}
51
52impl From<IndexRegionSerDe> for IndexRegion {
53    fn from(IndexRegionSerDe([l1, c1, l2, c2, count, file_id]): IndexRegionSerDe) -> Self {
54        Self {
55            l1,
56            c1,
57            l2,
58            c2,
59            count,
60            file_id,
61        }
62    }
63}
64
65impl From<IndexRegion> for IndexRegionSerDe {
66    fn from(
67        IndexRegion {
68            l1,
69            c1,
70            l2,
71            c2,
72            count,
73            file_id,
74        }: IndexRegion,
75    ) -> Self {
76        Self([l1, c1, l2, c2, count, file_id])
77    }
78}
79
80/// A test index, which is a more compact representation of [`CoverageData`],
81/// and contains only the information needed for analysis.
82#[derive(serde::Serialize, serde::Deserialize)]
83pub struct TestIndex {
84    /// The regions in all the files.
85    #[serde(default, skip_serializing_if = "Vec::is_empty")]
86    pub regions: Vec<IndexRegion>,
87    /// The paths to all the files.
88    pub files: Vec<PathBuf>,
89    /// The time the test was run.
90    pub test_run: chrono::DateTime<chrono::Utc>,
91    /// The test description.
92    pub test_info: TestInfo,
93}
94
95impl TestIndex {
96    /// Indexes/compiles the [`CoverageData`] into a [`TestIndex`].
97    pub fn index(
98        difftest: &Difftest,
99        profdata: CoverageData,
100        mut index_data_compiler_config: IndexDataCompilerConfig,
101    ) -> DifftestsResult<Self> {
102        let mut index_data = Self {
103            regions: vec![],
104            files: vec![],
105            test_run: difftest.test_run_time().into(),
106            test_info: difftest.test_info()?,
107        };
108
109        if index_data_compiler_config.remove_bin_path {
110            index_data.test_info.test_binary = PathBuf::new();
111        }
112
113        let mut mapping_files = BTreeMap::<PathBuf, usize>::new();
114
115        for mapping in &profdata.data {
116            for f in &mapping.functions {
117                for region in &f.regions {
118                    if region.execution_count == 0 {
119                        continue;
120                    }
121
122                    let filename = &f.filenames[region.file_id];
123
124                    if !(index_data_compiler_config.accept_file)(filename) {
125                        continue;
126                    }
127
128                    let file_id = *mapping_files.entry(filename.clone()).or_insert_with(|| {
129                        let id = index_data.files.len();
130                        index_data
131                            .files
132                            .push((index_data_compiler_config.index_filename_converter)(
133                                filename,
134                            ));
135                        id
136                    });
137
138                    if index_data_compiler_config.index_size == IndexSize::Full {
139                        index_data.regions.push(IndexRegion {
140                            l1: region.l1,
141                            c1: region.c1,
142                            l2: region.l2,
143                            c2: region.c2,
144                            count: region.execution_count,
145                            file_id,
146                        });
147                    }
148                }
149            }
150        }
151
152        Ok(index_data)
153    }
154
155    /// Writes the [`TestIndex`] to a file.
156    pub fn write_to_file(&self, path: &Path) -> DifftestsResult {
157        let mut file = File::create(path)?;
158        let mut writer = BufWriter::new(&mut file);
159        serde_json::to_writer(&mut writer, self)?;
160        Ok(())
161    }
162
163    /// Reads a [`TestIndex`] from a file.
164    pub fn read_from_file(path: &Path) -> DifftestsResult<Self> {
165        Ok(serde_json::from_str(&fs::read_to_string(path)?)?)
166    }
167}
168
169/// Configuration for the [`TestIndex::index`] function.
170pub struct IndexDataCompilerConfig {
171    /// Whether to ignore files in the cargo registry.
172    pub ignore_registry_files: bool,
173    /// Whether or not to remove the binary path from the index.
174    pub remove_bin_path: bool,
175    /// A conversion function for the file names in the index.
176    /// This is useful for converting absolute paths to paths
177    /// relative to the repository root for example.
178    pub index_filename_converter: Box<dyn FnMut(&Path) -> PathBuf>,
179    /// A function that determines whether a file should be indexed.
180    /// This is useful for excluding files that are not part of the
181    /// project, such as files in the cargo registry.
182    pub accept_file: Box<dyn FnMut(&Path) -> bool>,
183    /// The desired size of the index.
184    ///
185    /// This is useful for reducing the size of the index,
186    /// at the cost of losing some information.
187    ///
188    /// Refer to [`IndexSize`] for more information.
189    pub index_size: IndexSize,
190}
191
192/// The size of the index.
193///
194/// This is useful for reducing the size of the index,
195/// at the cost of losing some information.
196#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Default)]
197pub enum IndexSize {
198    /// The smallest size, which only contains the file names.
199    ///
200    /// Tests indexes created with this size cannot be used for
201    /// [`DirtyAlgorithm::GitDiff`] with the [`GitDiffStrategy::Hunks`] strategy,
202    /// as it requires the regions to be present.
203    ///
204    /// [`DirtyAlgorithm::GitDiff`]: crate::dirty_algorithm::DirtyAlgorithm
205    /// [`GitDiffStrategy::Hunks`]: crate::dirty_algorithm::GitDiffStrategy
206    #[default]
207    Tiny,
208    /// The full size, which contains all the information, including regions.
209    Full,
210}