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}