xref: /aosp_15_r20/development/tools/external_crates/test_mapping/src/lib.rs (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1 // Copyright (C) 2024 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! Read, update, and write TEST_MAPPING files.
16 
17 mod blueprint;
18 mod json;
19 mod rdeps;
20 
21 use std::{
22     collections::BTreeSet,
23     fs::{read_to_string, remove_file, write},
24     io,
25     path::PathBuf,
26     str::Utf8Error,
27 };
28 
29 use android_bp::BluePrint;
30 use blueprint::RustTests;
31 use json::{TestMappingJson, TestMappingPath};
32 use rdeps::ReverseDeps;
33 use rooted_path::RootedPath;
34 use thiserror::Error;
35 
36 #[derive(Error, Debug)]
37 pub enum TestMappingError {
38     #[error("Blueprint file {0} not found")]
39     BlueprintNotFound(PathBuf),
40     #[error("Blueprint parse error: {0}")]
41     BlueprintParseError(String),
42     #[error("Blueprint rule has no name")]
43     RuleWithoutName(String),
44 
45     #[error("Error stripping JSON comments")]
46     StripJsonCommentsError(io::Error),
47     #[error("JSON parse error")]
48     JsonParseError(#[from] serde_json::Error),
49 
50     #[error("IO error")]
51     IoError(#[from] io::Error),
52     #[error("Command output not UTF-8")]
53     Utf8Error(#[from] Utf8Error),
54     #[error("Failed to split grep output line {0} on '/'")]
55     GrepParseError(String),
56 }
57 
58 #[derive(Debug)]
59 pub struct TestMapping {
60     path: RootedPath,
61     json: TestMappingJson,
62     bp: BluePrint,
63 }
64 
65 impl TestMapping {
66     /// Read the TEST_MAPPING file in the specified directory.
67     /// If there is no TEST_MAPPING file, a default value is returned.
68     /// Also reads the Android.bp in the directory, because we need that
69     /// information to update the TEST_MAPPING.
read(path: RootedPath) -> Result<TestMapping, TestMappingError>70     pub fn read(path: RootedPath) -> Result<TestMapping, TestMappingError> {
71         let bpfile = path.join("Android.bp").unwrap();
72         if !bpfile.abs().exists() {
73             return Err(TestMappingError::BlueprintNotFound(bpfile.rel().to_path_buf()));
74         }
75         let bp = BluePrint::from_file(bpfile)
76             .map_err(|e: String| TestMappingError::BlueprintParseError(e))?;
77         let test_mapping_path = path.join("TEST_MAPPING").unwrap();
78         let json = if path.abs().exists() {
79             TestMappingJson::parse(read_to_string(test_mapping_path)?)?
80         } else {
81             TestMappingJson::default()
82         };
83         Ok(TestMapping { path, json, bp })
84     }
85     /// Write the TEST_MAPPING file.
write(&self) -> Result<(), TestMappingError>86     pub fn write(&self) -> Result<(), TestMappingError> {
87         if self.json.is_empty() && self.test_mapping().abs().exists() {
88             remove_file(self.test_mapping())?;
89         } else {
90             let mut contents = serde_json::to_string_pretty(&self.json)?;
91             contents.push('\n');
92             write(self.test_mapping(), contents)?;
93         }
94         Ok(())
95     }
96     /// Update the presubmit and presubmit_rust fields to the
97     /// set of test targets in the Android.bp file.
98     /// Since adding new tests directly to presubmits is discouraged,
99     /// It's preferable to use add_new_tests_to_postsubmit and
100     /// convert_postsubmit_tests instead.
update_presubmits(&mut self) -> Result<(), TestMappingError>101     pub fn update_presubmits(&mut self) -> Result<(), TestMappingError> {
102         self.json.set_presubmits(&self.bp.rust_tests()?);
103         Ok(())
104     }
105     /// Add tests that aren't already mentioned in TEST_MAPPING
106     /// as post-submit tests.
add_new_tests_to_postsubmit(&mut self) -> Result<bool, TestMappingError>107     pub fn add_new_tests_to_postsubmit(&mut self) -> Result<bool, TestMappingError> {
108         Ok(self.json.add_new_tests_to_postsubmit(&self.bp.rust_tests()?))
109     }
110     /// Convert post-submit tests to run at presubmit.
convert_postsubmit_tests(&mut self) -> bool111     pub fn convert_postsubmit_tests(&mut self) -> bool {
112         self.json.convert_postsubmit_tests()
113     }
114     /// Fix the import paths of TEST_MAPPING files to refer to the monorepo.
115     /// Essentially, replace external/rust/crates with external/rust/android-crates-io/crates.
fix_import_paths(&mut self) -> bool116     pub fn fix_import_paths(&mut self) -> bool {
117         let mut changed = false;
118         for import in self.json.imports.iter_mut() {
119             if import.path.starts_with("external/rust/crates") {
120                 let new_path = import
121                     .path
122                     .replace("external/rust/crates", "external/rust/android-crates-io/crates");
123                 if self.path.with_same_root(new_path.clone()).unwrap().abs().exists() {
124                     import.path = new_path;
125                     changed = true;
126                 }
127             }
128         }
129         changed
130     }
131     /// Update the imports section of TEST_MAPPING to contain all the
132     /// paths that depend on this crate.
update_imports(&mut self) -> Result<(), TestMappingError>133     pub fn update_imports(&mut self) -> Result<(), TestMappingError> {
134         let all_rdeps = ReverseDeps::for_repo(self.path.root());
135         let mut rdeps = BTreeSet::new();
136         for lib in self.libs()? {
137             if let Some(paths) = all_rdeps.get(lib.as_str()) {
138                 rdeps.append(&mut paths.clone());
139             }
140         }
141         let self_path = self.path.rel().to_str().unwrap();
142         self.json.imports = rdeps
143             .iter()
144             .filter(|path| path.as_str() != self_path)
145             .map(|t| TestMappingPath { path: t.to_string() })
146             .collect();
147         Ok(())
148     }
test_mapping(&self) -> RootedPath149     fn test_mapping(&self) -> RootedPath {
150         self.path.join("TEST_MAPPING").unwrap()
151     }
libs(&self) -> Result<Vec<String>, TestMappingError>152     fn libs(&self) -> Result<Vec<String>, TestMappingError> {
153         let mut libs = Vec::new();
154         for module in &self.bp.modules {
155             if matches!(
156                 module.typ.as_str(),
157                 "rust_library" | "rust_library_rlib" | "rust_library_host" | "rust_proc_macro"
158             ) {
159                 libs.push(
160                     module
161                         .get_string("name")
162                         .ok_or(TestMappingError::RuleWithoutName(module.typ.clone()))?
163                         .clone(),
164                 );
165             }
166         }
167         Ok(libs)
168     }
169 }
170