blob: b9a16db56a4619e73b401fa675622b834417f467 [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Read, update, and write TEST_MAPPING files.
mod blueprint;
mod json;
mod rdeps;
use std::{
collections::BTreeSet,
fs::{read_to_string, remove_file, write},
io,
path::PathBuf,
str::Utf8Error,
};
use android_bp::BluePrint;
use blueprint::RustTests;
use json::{TestMappingJson, TestMappingPath};
use rdeps::ReverseDeps;
use rooted_path::RootedPath;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum TestMappingError {
#[error("Blueprint file {0} not found")]
BlueprintNotFound(PathBuf),
#[error("Blueprint parse error: {0}")]
BlueprintParseError(String),
#[error("Blueprint rule has no name")]
RuleWithoutName(String),
#[error("Error stripping JSON comments")]
StripJsonCommentsError(io::Error),
#[error("JSON parse error")]
JsonParseError(#[from] serde_json::Error),
#[error("IO error")]
IoError(#[from] io::Error),
#[error("Command output not UTF-8")]
Utf8Error(#[from] Utf8Error),
#[error("Failed to split grep output line {0} on '/'")]
GrepParseError(String),
}
#[derive(Debug)]
pub struct TestMapping {
path: RootedPath,
json: TestMappingJson,
bp: BluePrint,
}
impl TestMapping {
/// Read the TEST_MAPPING file in the specified directory.
/// If there is no TEST_MAPPING file, a default value is returned.
/// Also reads the Android.bp in the directory, because we need that
/// information to update the TEST_MAPPING.
pub fn read(path: RootedPath) -> Result<TestMapping, TestMappingError> {
let bpfile = path.join("Android.bp").unwrap();
if !bpfile.abs().exists() {
return Err(TestMappingError::BlueprintNotFound(bpfile.rel().to_path_buf()));
}
let bp = BluePrint::from_file(bpfile)
.map_err(|e: String| TestMappingError::BlueprintParseError(e))?;
let test_mapping_path = path.join("TEST_MAPPING").unwrap();
let json = if test_mapping_path.abs().exists() {
TestMappingJson::parse(read_to_string(test_mapping_path)?)?
} else {
TestMappingJson::default()
};
Ok(TestMapping { path, json, bp })
}
/// Write the TEST_MAPPING file.
pub fn write(&self) -> Result<(), TestMappingError> {
if self.json.is_empty() && self.test_mapping().abs().exists() {
remove_file(self.test_mapping())?;
} else {
let mut contents = serde_json::to_string_pretty(&self.json)?;
contents.push('\n');
write(self.test_mapping(), contents)?;
}
Ok(())
}
/// Remove tests from TEST_MAPPING that are no longer in the
/// Android.bp file
pub fn remove_unknown_tests(&mut self) -> Result<bool, TestMappingError> {
Ok(self.json.remove_unknown_tests(&self.bp.rust_tests()?))
}
/// Update the presubmit and presubmit_rust fields to the
/// set of test targets in the Android.bp file.
/// Since adding new tests directly to presubmits is discouraged,
/// It's preferable to use add_new_tests_to_postsubmit and
/// convert_postsubmit_tests instead.
pub fn update_presubmits(&mut self) -> Result<(), TestMappingError> {
self.json.set_presubmits(&self.bp.rust_tests()?);
Ok(())
}
/// Add tests that aren't already mentioned in TEST_MAPPING
/// as post-submit tests.
pub fn add_new_tests_to_postsubmit(&mut self) -> Result<bool, TestMappingError> {
Ok(self.json.add_new_tests_to_postsubmit(&self.bp.rust_tests()?))
}
/// Convert post-submit tests to run at presubmit.
pub fn convert_postsubmit_tests(&mut self) -> bool {
self.json.convert_postsubmit_tests()
}
/// Fix the import paths of TEST_MAPPING files to refer to the monorepo.
/// Essentially, replace external/rust/crates with external/rust/android-crates-io/crates.
pub fn fix_import_paths(&mut self) -> bool {
let mut changed = false;
for import in self.json.imports.iter_mut() {
if import.path.starts_with("external/rust/crates") {
let new_path = import
.path
.replace("external/rust/crates", "external/rust/android-crates-io/crates");
if self.path.with_same_root(new_path.clone()).unwrap().abs().exists() {
import.path = new_path;
changed = true;
}
}
}
changed
}
/// Update the imports section of TEST_MAPPING to contain all the
/// paths that depend on this crate.
pub fn update_imports(&mut self) -> Result<(), TestMappingError> {
let all_rdeps = ReverseDeps::for_repo(self.path.root());
let mut rdeps = BTreeSet::new();
for lib in self.libs()? {
if let Some(paths) = all_rdeps.get(lib.as_str()) {
rdeps.append(&mut paths.clone());
}
}
let self_path = self.path.rel().to_str().unwrap();
self.json.imports = rdeps
.iter()
.filter(|path| path.as_str() != self_path)
.map(|t| TestMappingPath { path: t.to_string() })
.collect();
Ok(())
}
fn test_mapping(&self) -> RootedPath {
self.path.join("TEST_MAPPING").unwrap()
}
fn libs(&self) -> Result<Vec<String>, TestMappingError> {
let mut libs = Vec::new();
for module in &self.bp.modules {
if matches!(
module.typ.as_str(),
"rust_library" | "rust_library_rlib" | "rust_library_host" | "rust_proc_macro"
) {
libs.push(
module
.get_string("name")
.ok_or(TestMappingError::RuleWithoutName(module.typ.clone()))?
.clone(),
);
}
}
Ok(libs)
}
}