Add dtcompare tool and device tree validation test changes
Created dtcompare tool to compare 2 device trees. The tool traverses the
trees depth-first, and allows ignoring values of properties, and
skipping properties completely.
Avoid using diff.
Using FDT allows more flexibility in comparing device trees, such as
skipping over fields or validating their presence.
Some fields in the device tree are expected to exist and contain
separate data, and it should be validated that such fields exist. Other
fields can be ignored completely.
Bug: 360388014
Test: atest avf_backcompat_tests
Change-Id: Ib4d5f89bbc25e90bd47dd422f7966ab3f2910433
diff --git a/tests/dtcompare/Android.bp b/tests/dtcompare/Android.bp
new file mode 100644
index 0000000..988f420
--- /dev/null
+++ b/tests/dtcompare/Android.bp
@@ -0,0 +1,18 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+ name: "dtcompare",
+ crate_root: "src/main.rs",
+ srcs: ["src/main.rs"],
+ edition: "2021",
+ rustlibs: [
+ "libanyhow",
+ "libclap",
+ "libhex_nostd",
+ "liblibfdt_nostd",
+ "liblog_rust",
+ ],
+ visibility: ["//packages/modules/Virtualization:__subpackages__"],
+}
diff --git a/tests/dtcompare/src/main.rs b/tests/dtcompare/src/main.rs
new file mode 100644
index 0000000..db3aac2
--- /dev/null
+++ b/tests/dtcompare/src/main.rs
@@ -0,0 +1,192 @@
+// Copyright 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.
+
+//! Compare device tree contents.
+//! Allows skipping over fields provided.
+
+use anyhow::anyhow;
+use anyhow::Context;
+use anyhow::Result;
+use clap::Parser;
+use hex::encode;
+use libfdt::Fdt;
+use libfdt::FdtNode;
+
+use std::collections::BTreeMap;
+use std::collections::BTreeSet;
+use std::fs::read;
+use std::path::PathBuf;
+
+#[derive(Debug, Parser)]
+/// Device Tree Compare arguments.
+struct DtCompareArgs {
+ /// first device tree
+ #[arg(long)]
+ dt1: PathBuf,
+ /// second device tree
+ #[arg(long)]
+ dt2: PathBuf,
+ /// list of properties that should exist but are expected to hold different values in the
+ /// trees.
+ #[arg(short = 'I', long)]
+ ignore_path_value: Vec<String>,
+ /// list of paths that will be ignored, whether added, removed, or changed.
+ /// Paths can be nodes, subnodes, or even properties:
+ /// Ex: /avf/unstrusted // this is a path to a subnode. All properties and subnodes underneath
+ /// // it will also be ignored.
+ /// /avf/name // This is a path for a property. Only this property will be ignored.
+ #[arg(short = 'S', long)]
+ ignore_path: Vec<String>,
+}
+
+fn main() -> Result<()> {
+ let args = DtCompareArgs::parse();
+ let dt1: Vec<u8> = read(args.dt1)?;
+ let dt2: Vec<u8> = read(args.dt2)?;
+ let ignore_value_set = BTreeSet::from_iter(args.ignore_path_value);
+ let ignore_set = BTreeSet::from_iter(args.ignore_path);
+ compare_device_trees(dt1.as_slice(), dt2.as_slice(), ignore_value_set, ignore_set)
+}
+
+// Compare device trees by doing a pre-order traversal of the trees.
+fn compare_device_trees(
+ dt1: &[u8],
+ dt2: &[u8],
+ ignore_value_set: BTreeSet<String>,
+ ignore_set: BTreeSet<String>,
+) -> Result<()> {
+ let fdt1 = Fdt::from_slice(dt1).context("invalid device tree: Dt1")?;
+ let fdt2 = Fdt::from_slice(dt2).context("invalid device tree: Dt2")?;
+ let mut errors = Vec::new();
+ compare_subnodes(
+ &fdt1.root(),
+ &fdt2.root(),
+ &ignore_value_set,
+ &ignore_set,
+ /* path */ &mut ["".to_string()],
+ &mut errors,
+ )?;
+ if !errors.is_empty() {
+ return Err(anyhow!(
+ "Following properties had different values: [\n{}\n]\ndetected {} diffs",
+ errors.join("\n"),
+ errors.len()
+ ));
+ }
+ Ok(())
+}
+
+fn compare_props(
+ root1: &FdtNode,
+ root2: &FdtNode,
+ ignore_value_set: &BTreeSet<String>,
+ ignore_set: &BTreeSet<String>,
+ path: &mut [String],
+ errors: &mut Vec<String>,
+) -> Result<()> {
+ let mut prop_map: BTreeMap<String, &[u8]> = BTreeMap::new();
+ for prop in root1.properties().context("Error getting properties")? {
+ let prop_path =
+ path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?;
+ // Do not add to prop map if skipping
+ if ignore_set.contains(&prop_path) {
+ continue;
+ }
+ let value = prop.value().context("Error getting value")?;
+ if prop_map.insert(prop_path.clone(), value).is_some() {
+ return Err(anyhow!("Duplicate property detected in subnode: {}", prop_path));
+ }
+ }
+ for prop in root2.properties().context("Error getting properties")? {
+ let prop_path =
+ path.join("/") + "/" + prop.name().context("Error getting property name")?.to_str()?;
+ if ignore_set.contains(&prop_path) {
+ continue;
+ }
+ let Some(prop1_value) = prop_map.remove(&prop_path) else {
+ errors.push(format!("added prop_path: {}", prop_path));
+ continue;
+ };
+ let prop_compare = prop1_value == prop.value().context("Error getting value")?;
+ // Check if value should be ignored. If yes, skip field.
+ if ignore_value_set.contains(&prop_path) {
+ continue;
+ }
+ if !prop_compare {
+ errors.push(format!(
+ "prop {} value mismatch: old: {} -> new: {}",
+ prop_path,
+ encode(prop1_value),
+ encode(prop.value().context("Error getting value")?)
+ ));
+ }
+ }
+ if !prop_map.is_empty() {
+ errors.push(format!("missing properties: {:?}", prop_map));
+ }
+ Ok(())
+}
+
+fn compare_subnodes(
+ node1: &FdtNode,
+ node2: &FdtNode,
+ ignore_value_set: &BTreeSet<String>,
+ ignore_set: &BTreeSet<String>,
+ path: &mut [String],
+ errors: &mut Vec<String>,
+) -> Result<()> {
+ let mut subnodes_map: BTreeMap<String, FdtNode> = BTreeMap::new();
+ for subnode in node1.subnodes().context("Error getting subnodes of first FDT")? {
+ let sn_path = path.join("/")
+ + "/"
+ + subnode.name().context("Error getting property name")?.to_str()?;
+ // Do not add to subnode map if skipping
+ if ignore_set.contains(&sn_path) {
+ continue;
+ }
+ if subnodes_map.insert(sn_path.clone(), subnode).is_some() {
+ return Err(anyhow!("Duplicate subnodes detected: {}", sn_path));
+ }
+ }
+ for sn2 in node2.subnodes().context("Error getting subnodes of second FDT")? {
+ let sn_path =
+ path.join("/") + "/" + sn2.name().context("Error getting subnode name")?.to_str()?;
+ let sn1 = subnodes_map.remove(&sn_path);
+ match sn1 {
+ Some(sn) => {
+ compare_props(
+ &sn,
+ &sn2,
+ ignore_value_set,
+ ignore_set,
+ &mut [sn_path.clone()],
+ errors,
+ )?;
+ compare_subnodes(
+ &sn,
+ &sn2,
+ ignore_value_set,
+ ignore_set,
+ &mut [sn_path.clone()],
+ errors,
+ )?;
+ }
+ None => errors.push(format!("added node: {}", sn_path)),
+ }
+ }
+ if !subnodes_map.is_empty() {
+ errors.push(format!("missing nodes: {:?}", subnodes_map));
+ }
+ Ok(())
+}