blob: 1ea575b998e529708c634e2909399fba241912e8 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{anyhow, format_err, Result};
use camino::{Utf8Path, Utf8PathBuf};
use fidl::unpersist;
use fidl_fuchsia_component_decl::Component;
use fidl_fuchsia_data as fdata;
use fidl_fuchsia_data::Dictionary;
use fuchsia_url::AbsoluteComponentUrl;
use rayon::prelude::*;
use std::{
collections::{BTreeSet, HashMap},
sync::Mutex,
};
use crate::{
CategorizedTestInfo, FailureReason, HermeticityStatus, TestPackageInfo, ValidationStatus,
};
const META_FAR_PREFIX: &'static str = "meta/";
const TEST_REALM_FACET_NAME: &'static str = "fuchsia.test.type";
const TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY: &'static str =
"fuchsia.test.deprecated-allowed-packages";
const HERMETIC_TEST_REALM: &'static str = "hermetic";
/// Validate that every test in the list of tests is a packaged fuchsia test, and
/// that the test is hermetic.
pub(crate) fn validate_hermeticity(
categorized_tests: Vec<CategorizedTestInfo>,
component_test_realms: &HashMap<String, String>,
build_dir: &Utf8Path,
inputs_for_depfile: &mut BTreeSet<Utf8PathBuf>,
) -> Vec<ValidationStatus> {
let inputs_for_depfile = Mutex::new(inputs_for_depfile);
categorized_tests
.into_par_iter()
.map(|categorized_test_info| {
match categorized_test_info {
// It's a test package, so validate that the component in the test package is
// not listed in the lookup table of components that run in test realms:
CategorizedTestInfo::Package(test) => {
let specified_test_realm = match &test.component_label {
None => None,
Some(component_label) => component_test_realms.get(component_label),
};
if specified_test_realm.is_some() {
// It has a specified test realm, so it's not hermetic.
ValidationStatus::failed_not_hermetic(test.into())
} else {
// It doesn't specify a test realm in test_components.json, but it might
// still be non-hermetic.
//
// So we also check the manifest to see if it lists a test realm. This in a
// separate function so that it is easy to remove when we are done with
// migrations.
match check_manifest_hermeticity(&test, build_dir, &inputs_for_depfile) {
Ok(status) => match status {
HermeticityStatus::Hermetic => ValidationStatus::Passed,
HermeticityStatus::NotHermetic => {
ValidationStatus::failed_not_hermetic(test.into())
}
},
Err(error) => ValidationStatus::failed_with_error(test.into(), error),
}
}
}
CategorizedTestInfo::Host(test) | CategorizedTestInfo::DisabledHost(test) => {
ValidationStatus::Failed { test: test.into(), reason: FailureReason::HostTest }
}
CategorizedTestInfo::E2e(test) => {
ValidationStatus::Failed { test: test.into(), reason: FailureReason::E2eTest }
}
}
})
.collect()
}
/// Given a TestPackageInfo, locate the package manifest, and from it, the
/// meta.far, and from it, the component manifest, and then validate that it
/// doesn't specify a non-hermetic test realm in the component facets.
// TODO(https://fxbug.dev/42069253), TODO(https://fxbug.dev/42068721): Remove these checks when hermeticity no longer
// depends on component manifest
fn check_manifest_hermeticity(
test: &TestPackageInfo,
build_dir: &Utf8Path,
inputs_for_depfile: &Mutex<&mut BTreeSet<Utf8PathBuf>>,
) -> Result<HermeticityStatus> {
if test.package_manifests.len() > 0 {
let pkg_manifest = &test.package_manifests[0];
inputs_for_depfile.lock().expect("Failed to get depfile lock.").insert(pkg_manifest.into());
let res = find_meta_far(build_dir, pkg_manifest);
if res.is_err() {
return Err(format_err!(
"error finding meta.far file in package manifest {}: {:?}",
&pkg_manifest,
res.unwrap_err()
));
}
let meta_far_path = res.unwrap();
let pkg_url = AbsoluteComponentUrl::parse(&test.package_url)?;
let cm_path = pkg_url.resource();
inputs_for_depfile
.lock()
.expect("Failed to get depfile lock.")
.insert(meta_far_path.to_owned());
let decl = cm_decl_from_meta_far(&meta_far_path, cm_path)?;
let facets = decl.facets.unwrap_or(fdata::Dictionary::default());
return check_facet_hermeticity(&facets).map_err(Into::into);
}
Err(anyhow!("Missing package_manifests[] entry"))
}
/// Given the path to a package_manifest.json, find the meta.far itself.
fn find_meta_far(build_dir: &Utf8Path, manifest_path: &String) -> Result<Utf8PathBuf> {
let package_manifest =
fuchsia_pkg::PackageManifest::try_load_from(build_dir.join(manifest_path))?;
for blob in package_manifest.blobs() {
if blob.path.eq(META_FAR_PREFIX) {
return Ok(build_dir.join(&blob.source_path));
}
}
Err(anyhow!("Missing blob for meta.far"))
}
/// Given the path to a meta.far, and the path of a compiled component manifest
/// within it, return the Component decl parsed.
fn cm_decl_from_meta_far(meta_far_path: &Utf8PathBuf, cm_path: &str) -> Result<Component> {
let mut meta_far = std::fs::File::open(meta_far_path)?;
let mut far_reader = fuchsia_archive::Utf8Reader::new(&mut meta_far)?;
let cm_contents = far_reader.read_file(cm_path)?;
let decl: Component = unpersist(&cm_contents)?;
Ok(decl)
}
/// Given a the facets of a component decl, see if it has a test-realm facet,
/// and if that facet is the hermetic test realm or not.
fn check_facet_hermeticity(facets: &Dictionary) -> Result<HermeticityStatus> {
for facet in facets.entries.as_ref().unwrap_or(&vec![]) {
if facet.key.eq(TEST_REALM_FACET_NAME) {
// It's using the test realm facet, so validate that the realm it
// specifies is the hermetic one.
let val = facet.value.as_ref().ok_or(anyhow!("Null facet"))?;
match &**val {
fdata::DictionaryValue::Str(s) => {
if s.ne(HERMETIC_TEST_REALM) {
return Ok(HermeticityStatus::NotHermetic);
}
}
_ => {
return Err(anyhow!(
"Invalid facet value for {}: {:?}",
facet.key.clone(),
val
));
}
}
} else if facet.key.eq(TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY) {
// It's using the deprecated key, so if it specifies a value other
// than the empty string, it's a non-hermetic test.
let val = facet.value.as_ref().ok_or(anyhow!("Null facet"))?;
match &**val {
fdata::DictionaryValue::StrVec(s) => {
if !s.is_empty() {
return Ok(HermeticityStatus::NotHermetic);
}
}
_ => {
return Err(anyhow!(
"Invalid facet value for {}: {:?}",
facet.key.clone(),
val
));
}
}
}
}
// It didn't make any claims around test realms, so can only run in the
// hermetic test realm.
Ok(HermeticityStatus::Hermetic)
}
#[cfg(test)]
mod tests {
use super::*;
mod fn_check_facet_hermeticity {
use super::*;
#[test]
fn empty_facet() {
let facets = Dictionary::default();
let status = check_facet_hermeticity(&facets).unwrap();
assert_eq!(status, HermeticityStatus::Hermetic);
}
#[test]
fn hermetic_fuchsia_facet() {
let mut facets = Dictionary::default();
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_REALM_FACET_NAME.to_string(),
value: Some(Box::new(fdata::DictionaryValue::Str(HERMETIC_TEST_REALM.to_string()))),
}]);
let status = check_facet_hermeticity(&facets).unwrap();
assert_eq!(status, HermeticityStatus::Hermetic);
}
#[test]
fn non_hermetic_fuchsia_facet() {
let mut facets = Dictionary::default();
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_REALM_FACET_NAME.to_string(),
value: Some(Box::new(fdata::DictionaryValue::Str("some_realm".to_string()))),
}]);
let status = check_facet_hermeticity(&facets).unwrap();
assert_eq!(status, HermeticityStatus::NotHermetic);
}
#[test]
fn invalid_fuchsia_type_facet() {
let mut facets = Dictionary::default();
let val = fdata::DictionaryValue::StrVec(vec!["some_realm".to_string()]);
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_REALM_FACET_NAME.to_string(),
value: Some(Box::new(val.clone())),
}]);
let result = check_facet_hermeticity(&facets);
let error = result.unwrap_err();
assert_eq!(
error.to_string(),
format!("Invalid facet value for {}: {:?}", TEST_REALM_FACET_NAME, val)
);
}
#[test]
fn null_fuchsia_type_facet() {
let mut facets = Dictionary::default();
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_REALM_FACET_NAME.to_string(),
value: None,
}]);
let result = check_facet_hermeticity(&facets);
let error = result.unwrap_err();
assert_eq!(error.to_string(), "Null facet");
}
#[test]
fn empty_deprecated_allowed_facet() {
let mut facets = Dictionary::default();
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY.to_string(),
value: Some(Box::new(fdata::DictionaryValue::StrVec(vec![]))),
}]);
let status = check_facet_hermeticity(&facets).unwrap();
assert_eq!(status, HermeticityStatus::Hermetic);
}
#[test]
fn with_deprecated_allowed_facet() {
let mut facets = Dictionary::default();
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY.to_string(),
value: Some(Box::new(fdata::DictionaryValue::StrVec(vec!["some_pkg".into()]))),
}]);
let status = check_facet_hermeticity(&facets).unwrap();
assert_eq!(status, HermeticityStatus::NotHermetic);
}
#[test]
fn invalid_deprecated_allowed_facet() {
let mut facets = Dictionary::default();
let val = fdata::DictionaryValue::Str("some_pkg".into());
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY.to_string(),
value: Some(Box::new(val.clone())),
}]);
let result = check_facet_hermeticity(&facets);
let error = result.unwrap_err();
assert_eq!(
error.to_string(),
format!(
"Invalid facet value for {}: {:?}",
TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY, val
)
);
}
#[test]
fn null_deprecated_allowed_facet() {
let mut facets = Dictionary::default();
facets.entries = Some(vec![fdata::DictionaryEntry {
key: TEST_DEPRECATED_ALLOWED_PACKAGES_FACET_KEY.to_string(),
value: None,
}]);
let result = check_facet_hermeticity(&facets);
let error = result.unwrap_err();
assert_eq!(error.to_string(), "Null facet");
}
}
#[test]
fn arbitrary_facet() {
let mut facets = Dictionary::default();
facets.entries = Some(vec![fdata::DictionaryEntry {
key: "fuchsia.test.some_key".to_string(),
value: Some(Box::new(fdata::DictionaryValue::StrVec(vec!["some_pkg".into()]))),
}]);
let status = check_facet_hermeticity(&facets).unwrap();
assert_eq!(status, HermeticityStatus::Hermetic);
}
mod fn_check_hermeticity {
use crate::{TestComponentEntry, TestComponentsJsonEntry, TestEntry, TestsJsonEntry};
use super::*;
use assert_matches::assert_matches;
#[test]
fn not_hermetic() {
let test_component_entry = TestComponentsJsonEntry {
test_component: TestComponentEntry {
component_label: "component_label".to_string(),
moniker: "/some/moniker".to_string(),
},
};
let test_components_map =
TestComponentsJsonEntry::convert_to_map(vec![test_component_entry]).unwrap();
let test_entry = TestsJsonEntry {
test: TestEntry {
name: "test name".to_string(),
test_label: "//gn/label/for:test".to_string(),
package_url: Some(
"fuchsia-pkg://fuchsia.com/test_package#meta/test_component.cm".to_string(),
),
package_label: Some("//gn/label/for:pkg".to_string()),
component_label: Some("component_label".to_string()),
..Default::default()
},
..Default::default()
};
let test_package_infos =
vec![TestPackageInfo::try_from(test_entry.test).unwrap().into()];
let mut inputs_for_depfile = BTreeSet::new();
let statuses = validate_hermeticity(
test_package_infos,
&test_components_map,
"igored".into(),
&mut inputs_for_depfile,
);
assert_matches!(
statuses.get(0).unwrap(),
ValidationStatus::Failed { reason: FailureReason::NotHermetic, .. }
);
}
}
}