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 35 /// Error types for the 'test_mapping' crate. 36 #[derive(thiserror::Error, Debug)] 37 pub enum Error { 38 /// Blueprint file not found 39 #[error("Blueprint file {0} not found")] 40 BlueprintNotFound(PathBuf), 41 /// Blueprint parse error 42 #[error("Blueprint parse error: {0}")] 43 BlueprintParseError(String), 44 /// Blueprint rule has no name 45 #[error("Blueprint rule has no name")] 46 RuleWithoutName(String), 47 48 /// Error stripping JSON comments 49 #[error("Error stripping JSON comments: {0}")] 50 StripJsonCommentsError(io::Error), 51 /// JSON parse error 52 #[error(transparent)] 53 JsonParseError(#[from] serde_json::Error), 54 55 /// I/O Error 56 #[error(transparent)] 57 IoError(#[from] io::Error), 58 /// Command output not UTF-8 59 #[error(transparent)] 60 Utf8Error(#[from] Utf8Error), 61 /// Failed to split grep output on '/' 62 #[error("Failed to split grep output line {0} on '/'")] 63 GrepParseError(String), 64 } 65 66 /// A parsed TEST_MAPPING file 67 #[derive(Debug)] 68 pub struct TestMapping { 69 /// The path of the crate directory. 70 path: RootedPath, 71 /// The contents of TEST_MAPPING 72 json: TestMappingJson, 73 /// The parsed Android.bp file 74 bp: BluePrint, 75 } 76 77 impl TestMapping { 78 /// Read the TEST_MAPPING file in the specified directory. 79 /// If there is no TEST_MAPPING file, a default value is returned. 80 /// Also reads the Android.bp in the directory, because we need that 81 /// information to update the TEST_MAPPING. read(path: RootedPath) -> Result<TestMapping, Error>82 pub fn read(path: RootedPath) -> Result<TestMapping, Error> { 83 let bpfile = path.join("Android.bp").unwrap(); 84 if !bpfile.abs().exists() { 85 return Err(Error::BlueprintNotFound(bpfile.rel().to_path_buf())); 86 } 87 let bp = BluePrint::from_file(bpfile).map_err(|e: String| Error::BlueprintParseError(e))?; 88 let test_mapping_path = path.join("TEST_MAPPING").unwrap(); 89 let json = if test_mapping_path.abs().exists() { 90 TestMappingJson::parse(read_to_string(test_mapping_path)?)? 91 } else { 92 TestMappingJson::default() 93 }; 94 Ok(TestMapping { path, json, bp }) 95 } 96 /// Write the TEST_MAPPING file. write(&self) -> Result<(), Error>97 pub fn write(&self) -> Result<(), Error> { 98 if self.json.is_empty() && self.test_mapping().abs().exists() { 99 remove_file(self.test_mapping())?; 100 } else { 101 let mut contents = serde_json::to_string_pretty(&self.json)?; 102 contents.push('\n'); 103 write(self.test_mapping(), contents)?; 104 } 105 Ok(()) 106 } 107 /// Remove tests from TEST_MAPPING that are no longer in the 108 /// Android.bp file remove_unknown_tests(&mut self) -> Result<bool, Error>109 pub fn remove_unknown_tests(&mut self) -> Result<bool, Error> { 110 Ok(self.json.remove_unknown_tests(&self.bp.rust_tests()?)) 111 } 112 /// Update the presubmit and presubmit_rust fields to the 113 /// set of test targets in the Android.bp file. 114 /// Since adding new tests directly to presubmits is discouraged, 115 /// It's preferable to use add_new_tests_to_postsubmit and 116 /// convert_postsubmit_tests instead. update_presubmits(&mut self) -> Result<(), Error>117 pub fn update_presubmits(&mut self) -> Result<(), Error> { 118 self.json.set_presubmits(&self.bp.rust_tests()?); 119 Ok(()) 120 } 121 /// Add tests that aren't already mentioned in TEST_MAPPING 122 /// as post-submit tests. add_new_tests_to_postsubmit(&mut self) -> Result<bool, Error>123 pub fn add_new_tests_to_postsubmit(&mut self) -> Result<bool, Error> { 124 Ok(self.json.add_new_tests_to_postsubmit(&self.bp.rust_tests()?)) 125 } 126 /// Convert post-submit tests to run at presubmit. convert_postsubmit_tests(&mut self) -> bool127 pub fn convert_postsubmit_tests(&mut self) -> bool { 128 self.json.convert_postsubmit_tests() 129 } 130 /// Fix the import paths of TEST_MAPPING files to refer to the monorepo. 131 /// Essentially, replace external/rust/crates with external/rust/android-crates-io/crates. fix_import_paths(&mut self) -> bool132 pub fn fix_import_paths(&mut self) -> bool { 133 let mut changed = false; 134 for import in self.json.imports.iter_mut() { 135 if import.path.starts_with("external/rust/crates") { 136 let new_path = import 137 .path 138 .replace("external/rust/crates", "external/rust/android-crates-io/crates"); 139 if self.path.with_same_root(new_path.clone()).unwrap().abs().exists() { 140 import.path = new_path; 141 changed = true; 142 } 143 } 144 } 145 changed 146 } 147 /// Update the imports section of TEST_MAPPING to contain all the 148 /// paths that depend on this crate. update_imports(&mut self) -> Result<(), Error>149 pub fn update_imports(&mut self) -> Result<(), Error> { 150 let all_rdeps = ReverseDeps::for_repo(self.path.root()); 151 let mut rdeps = BTreeSet::new(); 152 for lib in self.libs()? { 153 if let Some(paths) = all_rdeps.get(lib.as_str()) { 154 rdeps.append(&mut paths.clone()); 155 } 156 } 157 let self_path = self.path.rel().to_str().unwrap(); 158 self.json.imports = rdeps 159 .iter() 160 .filter(|path| path.as_str() != self_path) 161 .map(|t| TestMappingPath { path: t.to_string() }) 162 .collect(); 163 Ok(()) 164 } test_mapping(&self) -> RootedPath165 fn test_mapping(&self) -> RootedPath { 166 self.path.join("TEST_MAPPING").unwrap() 167 } libs(&self) -> Result<Vec<String>, Error>168 fn libs(&self) -> Result<Vec<String>, Error> { 169 let mut libs = Vec::new(); 170 for module in &self.bp.modules { 171 if matches!( 172 module.typ.as_str(), 173 "rust_library" | "rust_library_rlib" | "rust_library_host" | "rust_proc_macro" 174 ) { 175 libs.push( 176 module 177 .get_string("name") 178 .ok_or(Error::RuleWithoutName(module.typ.clone()))? 179 .clone(), 180 ); 181 } 182 } 183 Ok(libs) 184 } 185 } 186