Merge "Allow user to specify semver compatibility logic for suggested updates." into main
diff --git a/tools/cargo_embargo/src/main.rs b/tools/cargo_embargo/src/main.rs
index decc42a..7e80775 100644
--- a/tools/cargo_embargo/src/main.rs
+++ b/tools/cargo_embargo/src/main.rs
@@ -806,6 +806,7 @@
         // Other cases.
         "MIT OR LGPL-3.0-or-later" => vec!["MIT"],
         "MIT/BSD-3-Clause" => vec!["MIT"],
+        "MIT AND (MIT OR Apache-2.0)" => vec!["MIT"],
 
         "LGPL-2.1-only OR BSD-2-Clause" => vec!["BSD-2-Clause"],
         _ => {
diff --git a/tools/external_crates/crate_tool/src/managed_crate.rs b/tools/external_crates/crate_tool/src/managed_crate.rs
index b4bb827..f823234 100644
--- a/tools/external_crates/crate_tool/src/managed_crate.rs
+++ b/tools/external_crates/crate_tool/src/managed_crate.rs
@@ -232,7 +232,10 @@
     pub fn fix_test_mapping(&self) -> Result<()> {
         let mut tm = TestMapping::read(self.android_crate_path().clone())?;
         println!("{}", self.name());
-        if tm.fix_import_paths() || tm.add_new_tests_to_postsubmit()? {
+        let mut changed = tm.fix_import_paths();
+        changed |= tm.add_new_tests_to_postsubmit()?;
+        changed |= tm.remove_unknown_tests()?;
+        if changed {
             tm.write()?;
         }
         Ok(())
@@ -435,6 +438,7 @@
         let android_crate_dir = staged.android_crate.path();
         remove_dir_all(android_crate_dir)?;
         rename(staged.staging_path(), android_crate_dir)?;
+        staged.fix_test_mapping()?;
         checksum::generate(android_crate_dir.abs())?;
 
         Ok(staged)
diff --git a/tools/external_crates/crate_tool/src/managed_repo.rs b/tools/external_crates/crate_tool/src/managed_repo.rs
index 6935988..4771537 100644
--- a/tools/external_crates/crate_tool/src/managed_repo.rs
+++ b/tools/external_crates/crate_tool/src/managed_repo.rs
@@ -22,7 +22,7 @@
     str::from_utf8,
 };
 
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use crates_index::DependencyKind;
 use glob::glob;
 use google_metadata::GoogleMetadata;
@@ -599,10 +599,21 @@
                 self.managed_dir(), managed_dirs.difference(&deps).join(", "), deps.difference(&managed_dirs).join(", ")));
         }
 
-        let crate_list = pseudo_crate.read_crate_list()?;
-        if deps.iter().ne(crate_list.iter()) {
-            return Err(anyhow!("Deps in pseudo_crate/Cargo.toml don't match deps in crate-list.txt\nCargo.toml: {}\ncrate-list.txt: {}",
-                deps.iter().join(", "), crate_list.iter().join(", ")));
+        let crate_list = pseudo_crate.read_crate_list("crate-list.txt")?;
+        if !deps.is_subset(&crate_list) {
+            bail!("Deps in pseudo_crate/Cargo.toml don't match deps in crate-list.txt\nCargo.toml: {}\ncrate-list.txt: {}",
+                deps.iter().join(", "), crate_list.iter().join(", "));
+        }
+
+        let expected_deleted_crates =
+            crate_list.difference(&deps).cloned().collect::<BTreeSet<_>>();
+        let deleted_crates = pseudo_crate.read_crate_list("deleted-crates.txt")?;
+        if deleted_crates != expected_deleted_crates {
+            bail!(
+                "Deleted crate list is inconsistent. Expected: {}, Found: {}",
+                expected_deleted_crates.iter().join(", "),
+                deleted_crates.iter().join(", ")
+            );
         }
 
         // Per https://android.googlesource.com/platform/tools/repohooks/,
diff --git a/tools/external_crates/crate_tool/src/pseudo_crate.rs b/tools/external_crates/crate_tool/src/pseudo_crate.rs
index ee0ce67..204feb0 100644
--- a/tools/external_crates/crate_tool/src/pseudo_crate.rs
+++ b/tools/external_crates/crate_tool/src/pseudo_crate.rs
@@ -14,7 +14,7 @@
 
 use std::{
     cell::OnceCell,
-    collections::BTreeMap,
+    collections::{BTreeMap, BTreeSet},
     env,
     fs::{read, write},
     io::BufRead,
@@ -22,7 +22,7 @@
     str::from_utf8,
 };
 
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
 use itertools::Itertools;
 use name_and_version::{NameAndVersionMap, NamedAndVersioned};
 use rooted_path::RootedPath;
@@ -51,10 +51,13 @@
     pub fn get_path(&self) -> &RootedPath {
         &self.path
     }
-    pub fn read_crate_list(&self) -> Result<Vec<String>> {
-        let mut lines = Vec::new();
-        for line in read(self.path.join("crate-list.txt")?)?.lines() {
-            lines.push(line?);
+    pub fn read_crate_list(&self, filename: &str) -> Result<BTreeSet<String>> {
+        let mut lines = BTreeSet::new();
+        for line in read(self.path.join(filename)?)?.lines() {
+            let line = line?;
+            if !lines.insert(line.clone()) {
+                bail!("Duplicate entry {line} in crate list {filename}");
+            }
         }
         Ok(lines)
     }
@@ -62,7 +65,16 @@
 
 impl PseudoCrate<CargoVendorClean> {
     pub fn regenerate_crate_list(&self) -> Result<()> {
-        write(self.path.join("crate-list.txt")?, format!("{}\n", self.deps().keys().join("\n")))?;
+        let old_crate_list = self.read_crate_list("crate-list.txt")?;
+        let current_crates = self.deps().keys().cloned().collect::<BTreeSet<_>>();
+        write(
+            self.path.join("crate-list.txt")?,
+            format!("{}\n", old_crate_list.union(&current_crates).join("\n")),
+        )?;
+        write(
+            self.path.join("deleted-crates.txt")?,
+            format!("{}\n", old_crate_list.difference(&current_crates).join("\n")),
+        )?;
         Ok(())
     }
     fn version_of(&self, crate_name: &str) -> Result<Version> {
@@ -179,6 +191,7 @@
 "#,
         )?;
         write(self.path.join("crate-list.txt")?, "")?;
+        write(self.path.join("deleted-crates.txt")?, "")?;
         write(self.path.join(".gitignore")?, "target/\nvendor/\n")?;
 
         ensure_exists_and_empty(&self.path.join("src")?)?;
diff --git a/tools/external_crates/license_checker/src/expression_parser.rs b/tools/external_crates/license_checker/src/expression_parser.rs
index 2754710..5337cd5 100644
--- a/tools/external_crates/license_checker/src/expression_parser.rs
+++ b/tools/external_crates/license_checker/src/expression_parser.rs
@@ -138,6 +138,11 @@
             "Apache preferred to MIT"
         );
         assert_eq!(
+            get_chosen_licenses("foo", Some("MIT AND (MIT OR Apache-2.0)"))?,
+            BTreeSet::from([Licensee::parse("MIT").unwrap().into_req()]),
+            "Complex expression from libm 0.2.11"
+        );
+        assert_eq!(
             get_chosen_licenses("webpki", None)?,
             BTreeSet::from([
                 Licensee::parse("ISC").unwrap().into_req(),
diff --git a/tools/external_crates/test_mapping/src/json.rs b/tools/external_crates/test_mapping/src/json.rs
index de199ca..d741b3f 100644
--- a/tools/external_crates/test_mapping/src/json.rs
+++ b/tools/external_crates/test_mapping/src/json.rs
@@ -56,6 +56,22 @@
             && self.presubmit_rust.is_empty()
             && self.postsubmit.is_empty()
     }
+    pub fn remove_unknown_tests(&mut self, tests: &BTreeSet<String>) -> bool {
+        let mut changed = false;
+        if self.presubmit.iter().any(|t| !tests.contains(&t.name)) {
+            self.presubmit.retain(|t| tests.contains(&t.name));
+            changed = true;
+        }
+        if self.presubmit_rust.iter().any(|t| !tests.contains(&t.name)) {
+            self.presubmit_rust.retain(|t| tests.contains(&t.name));
+            changed = true;
+        }
+        if self.postsubmit.iter().any(|t| !tests.contains(&t.name)) {
+            self.postsubmit.retain(|t| tests.contains(&t.name));
+            changed = true;
+        }
+        changed
+    }
     pub fn set_presubmits(&mut self, tests: &BTreeSet<String>) {
         self.presubmit = tests.iter().map(|t| TestMappingName { name: t.to_string() }).collect();
         self.presubmit_rust = self.presubmit.clone();
diff --git a/tools/external_crates/test_mapping/src/lib.rs b/tools/external_crates/test_mapping/src/lib.rs
index 185af51..b9a16db 100644
--- a/tools/external_crates/test_mapping/src/lib.rs
+++ b/tools/external_crates/test_mapping/src/lib.rs
@@ -93,6 +93,11 @@
         }
         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,