/* * 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. */ #[cfg(test)] mod tests { use anyhow::{anyhow, bail, ensure, Result}; use itertools::Itertools; use std::fmt::Debug; use std::io::Read; use xml::attribute::OwnedAttribute; use xml::reader::{ParserConfig, XmlEvent}; fn get_attribute(attributes: &[OwnedAttribute], tag: &str, key: &str) -> Result { attributes .iter() .find_map( |attr| { if attr.name.local_name == key { Some(attr.value.clone()) } else { None } }, ) .ok_or_else(|| anyhow!("tag {}: missing attribute {}", tag, key)) } fn verify_xml(mut source: R) -> Result<()> { #[derive(Debug)] struct Sdk { id: String, shortname: String, name: String, reference: String, } #[derive(Debug)] struct Symbol { #[allow(dead_code)] jar: String, #[allow(dead_code)] pattern: String, sdks: Vec, } // this will error out on XML syntax errors let reader = ParserConfig::new().create_reader(&mut source); let events: Vec<_> = reader.into_iter().collect::, _>>()?; // parse XML let mut sdks = vec![]; let mut symbols = vec![]; for (name, attributes) in events.into_iter().filter_map(|e| match e { XmlEvent::StartElement { name, attributes, namespace: _ } => { Some((name.local_name, attributes)) } _ => None, }) { match name.as_str() { "sdk-extensions-info" => {} "sdk" => { let sdk = Sdk { id: get_attribute(&attributes, "sdk", "id")?, shortname: get_attribute(&attributes, "sdk", "shortname")?, name: get_attribute(&attributes, "sdk", "name")?, reference: get_attribute(&attributes, "sdk", "reference")?, }; sdks.push(sdk); } "symbol" => { let symbol = Symbol { jar: get_attribute(&attributes, "symbol", "jar")?, pattern: get_attribute(&attributes, "symbol", "pattern")?, sdks: get_attribute(&attributes, "symbol", "sdks")? .split(',') .map(|s| s.to_owned()) .collect(), }; symbols.push(symbol); } _ => bail!("unknown tag '{}'", name), } } // verify all Sdk fields are unique across all Sdk items let dupes = sdks.iter().duplicates_by(|sdk| &sdk.id).collect::>(); ensure!(dupes.is_empty(), "{:?}: multiple sdk entries with identical id value", dupes); let dupes = sdks.iter().duplicates_by(|sdk| &sdk.shortname).collect::>(); ensure!( dupes.is_empty(), "{:?}: multiple sdk entries with identical shortname value", dupes ); let dupes = sdks.iter().duplicates_by(|sdk| &sdk.name).collect::>(); ensure!(dupes.is_empty(), "{:?}: multiple sdk entries with identical name value", dupes); let dupes = sdks.iter().duplicates_by(|sdk| &sdk.reference).collect::>(); ensure!( dupes.is_empty(), "{:?}: multiple sdk entries with identical reference value", dupes ); // verify Sdk id field has the expected format (positive integer) for sdk in sdks.iter() { ensure!(sdk.id.parse::().is_ok(), "{:?}: id not a positive int", sdk); } // verify individual Symbol elements let sdk_shortnames: Vec<_> = sdks.iter().map(|sdk| &sdk.shortname).collect(); for symbol in symbols.iter() { ensure!( symbol.sdks.iter().duplicates().collect::>().is_empty(), "{:?}: symbol contains duplicate references to the same sdk", symbol ); ensure!( !symbol.jar.contains(char::is_whitespace), "{:?}: jar contains whitespace", symbol ); ensure!( !symbol.pattern.contains(char::is_whitespace), "{:?}: pattern contains whitespace", symbol ); for id in symbol.sdks.iter() { ensure!( sdk_shortnames.contains(&id), "{:?}: symbol refers to non-existent sdk {}", symbol, id ); } } Ok(()) } #[test] fn test_get_attribute() { use xml::EventReader; let mut iter = EventReader::from_str(r#""#).into_iter(); let _ = iter.next().unwrap(); // skip start of doc let Ok(XmlEvent::StartElement { attributes, .. }) = iter.next().unwrap() else { panic!(); }; assert_eq!(get_attribute(&attributes, "tag", "a").unwrap(), "A"); assert!(get_attribute(&attributes, "tag", "no-such-attribute").is_err()); } #[test] fn test_verify_xml_correct_input() { verify_xml(&include_bytes!("testdata/correct.xml")[..]).unwrap(); } #[test] fn test_verify_xml_incorrect_input() { macro_rules! assert_err { ($input_path:expr, $expected_error:expr) => { let error = verify_xml(&include_bytes!($input_path)[..]).unwrap_err().to_string(); assert_eq!(error, $expected_error); }; } assert_err!( "testdata/corrupt-xml.xml", "25:1 Unexpected end of stream: still inside the root element" ); assert_err!( "testdata/duplicate-sdk-id.xml", r#"[Sdk { id: "1", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical id value"# ); assert_err!( "testdata/duplicate-sdk-shortname.xml", r#"[Sdk { id: "2", shortname: "foo", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical shortname value"# ); assert_err!( "testdata/duplicate-sdk-name.xml", r#"[Sdk { id: "2", shortname: "bar", name: "The foo extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical name value"# ); assert_err!( "testdata/duplicate-sdk-reference.xml", r#"[Sdk { id: "2", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$FOO" }]: multiple sdk entries with identical reference value"# ); assert_err!( "testdata/incorrect-sdk-id-format.xml", r#"Sdk { id: "1.0", shortname: "foo", name: "The foo extensions", reference: "android/os/Build$FOO" }: id not a positive int"# ); assert_err!( "testdata/duplicate-symbol-sdks.xml", r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "bar", "bar"] }: symbol contains duplicate references to the same sdk"# ); assert_err!( "testdata/symbol-refers-to-non-existent-sdk.xml", r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "does-not-exist", "bar"] }: symbol refers to non-existent sdk does-not-exist"# ); assert_err!( "testdata/whitespace-in-jar.xml", r#"Symbol { jar: "framework something", pattern: "*", sdks: ["foo", "bar"] }: jar contains whitespace"# ); assert_err!( "testdata/whitespace-in-pattern.xml", r#"Symbol { jar: "framework-something-else", pattern: "android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder\n .addIndexableNestedProperties ", sdks: ["bar"] }: pattern contains whitespace"# ); } #[test] fn test_actual_sdk_extensions_info_contents() { verify_xml(&include_bytes!("../sdk-extensions-info.xml")[..]).unwrap(); } }