• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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