1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 #[cfg(test)] 18 mod tests { 19 use anyhow::{anyhow, bail, ensure, Result}; 20 use itertools::Itertools; 21 use std::fmt::Debug; 22 use std::io::Read; 23 use xml::attribute::OwnedAttribute; 24 use xml::reader::{ParserConfig, XmlEvent}; 25 get_attribute(attributes: &[OwnedAttribute], tag: &str, key: &str) -> Result<String>26 fn get_attribute(attributes: &[OwnedAttribute], tag: &str, key: &str) -> Result<String> { 27 attributes 28 .iter() 29 .find_map( 30 |attr| { 31 if attr.name.local_name == key { 32 Some(attr.value.clone()) 33 } else { 34 None 35 } 36 }, 37 ) 38 .ok_or_else(|| anyhow!("tag {}: missing attribute {}", tag, key)) 39 } 40 verify_xml<R: Read>(mut source: R) -> Result<()>41 fn verify_xml<R: Read>(mut source: R) -> Result<()> { 42 #[derive(Debug)] 43 struct Sdk { 44 id: String, 45 shortname: String, 46 name: String, 47 reference: String, 48 } 49 50 #[derive(Debug)] 51 struct Symbol { 52 #[allow(dead_code)] 53 jar: String, 54 #[allow(dead_code)] 55 pattern: String, 56 sdks: Vec<String>, 57 } 58 59 // this will error out on XML syntax errors 60 let reader = ParserConfig::new().create_reader(&mut source); 61 let events: Vec<_> = reader.into_iter().collect::<Result<Vec<_>, _>>()?; 62 63 // parse XML 64 let mut sdks = vec![]; 65 let mut symbols = vec![]; 66 for (name, attributes) in events.into_iter().filter_map(|e| match e { 67 XmlEvent::StartElement { name, attributes, namespace: _ } => { 68 Some((name.local_name, attributes)) 69 } 70 _ => None, 71 }) { 72 match name.as_str() { 73 "sdk-extensions-info" => {} 74 "sdk" => { 75 let sdk = Sdk { 76 id: get_attribute(&attributes, "sdk", "id")?, 77 shortname: get_attribute(&attributes, "sdk", "shortname")?, 78 name: get_attribute(&attributes, "sdk", "name")?, 79 reference: get_attribute(&attributes, "sdk", "reference")?, 80 }; 81 sdks.push(sdk); 82 } 83 "symbol" => { 84 let symbol = Symbol { 85 jar: get_attribute(&attributes, "symbol", "jar")?, 86 pattern: get_attribute(&attributes, "symbol", "pattern")?, 87 sdks: get_attribute(&attributes, "symbol", "sdks")? 88 .split(',') 89 .map(|s| s.to_owned()) 90 .collect(), 91 }; 92 symbols.push(symbol); 93 } 94 _ => bail!("unknown tag '{}'", name), 95 } 96 } 97 98 // verify all Sdk fields are unique across all Sdk items 99 let dupes = sdks.iter().duplicates_by(|sdk| &sdk.id).collect::<Vec<_>>(); 100 ensure!(dupes.is_empty(), "{:?}: multiple sdk entries with identical id value", dupes); 101 102 let dupes = sdks.iter().duplicates_by(|sdk| &sdk.shortname).collect::<Vec<_>>(); 103 ensure!( 104 dupes.is_empty(), 105 "{:?}: multiple sdk entries with identical shortname value", 106 dupes 107 ); 108 109 let dupes = sdks.iter().duplicates_by(|sdk| &sdk.name).collect::<Vec<_>>(); 110 ensure!(dupes.is_empty(), "{:?}: multiple sdk entries with identical name value", dupes); 111 112 let dupes = sdks.iter().duplicates_by(|sdk| &sdk.reference).collect::<Vec<_>>(); 113 ensure!( 114 dupes.is_empty(), 115 "{:?}: multiple sdk entries with identical reference value", 116 dupes 117 ); 118 119 // verify Sdk id field has the expected format (positive integer) 120 for sdk in sdks.iter() { 121 ensure!(sdk.id.parse::<usize>().is_ok(), "{:?}: id not a positive int", sdk); 122 } 123 124 // verify individual Symbol elements 125 let sdk_shortnames: Vec<_> = sdks.iter().map(|sdk| &sdk.shortname).collect(); 126 for symbol in symbols.iter() { 127 ensure!( 128 symbol.sdks.iter().duplicates().collect::<Vec<_>>().is_empty(), 129 "{:?}: symbol contains duplicate references to the same sdk", 130 symbol 131 ); 132 ensure!( 133 !symbol.jar.contains(char::is_whitespace), 134 "{:?}: jar contains whitespace", 135 symbol 136 ); 137 ensure!( 138 !symbol.pattern.contains(char::is_whitespace), 139 "{:?}: pattern contains whitespace", 140 symbol 141 ); 142 for id in symbol.sdks.iter() { 143 ensure!( 144 sdk_shortnames.contains(&id), 145 "{:?}: symbol refers to non-existent sdk {}", 146 symbol, 147 id 148 ); 149 } 150 } 151 152 Ok(()) 153 } 154 155 #[test] test_get_attribute()156 fn test_get_attribute() { 157 use xml::EventReader; 158 159 let mut iter = EventReader::from_str(r#"<tag a="A" b="B" c="C"/>"#).into_iter(); 160 let _ = iter.next().unwrap(); // skip start of doc 161 let Ok(XmlEvent::StartElement { attributes, .. }) = iter.next().unwrap() else { 162 panic!(); 163 }; 164 assert_eq!(get_attribute(&attributes, "tag", "a").unwrap(), "A"); 165 assert!(get_attribute(&attributes, "tag", "no-such-attribute").is_err()); 166 } 167 168 #[test] test_verify_xml_correct_input()169 fn test_verify_xml_correct_input() { 170 verify_xml(&include_bytes!("testdata/correct.xml")[..]).unwrap(); 171 } 172 173 #[test] test_verify_xml_incorrect_input()174 fn test_verify_xml_incorrect_input() { 175 macro_rules! assert_err { 176 ($input_path:expr, $expected_error:expr) => { 177 let error = verify_xml(&include_bytes!($input_path)[..]).unwrap_err().to_string(); 178 assert_eq!(error, $expected_error); 179 }; 180 } 181 182 assert_err!( 183 "testdata/corrupt-xml.xml", 184 "25:1 Unexpected end of stream: still inside the root element" 185 ); 186 assert_err!( 187 "testdata/duplicate-sdk-id.xml", 188 r#"[Sdk { id: "1", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical id value"# 189 ); 190 assert_err!( 191 "testdata/duplicate-sdk-shortname.xml", 192 r#"[Sdk { id: "2", shortname: "foo", name: "The bar extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical shortname value"# 193 ); 194 assert_err!( 195 "testdata/duplicate-sdk-name.xml", 196 r#"[Sdk { id: "2", shortname: "bar", name: "The foo extensions", reference: "android/os/Build$BAR" }]: multiple sdk entries with identical name value"# 197 ); 198 assert_err!( 199 "testdata/duplicate-sdk-reference.xml", 200 r#"[Sdk { id: "2", shortname: "bar", name: "The bar extensions", reference: "android/os/Build$FOO" }]: multiple sdk entries with identical reference value"# 201 ); 202 assert_err!( 203 "testdata/incorrect-sdk-id-format.xml", 204 r#"Sdk { id: "1.0", shortname: "foo", name: "The foo extensions", reference: "android/os/Build$FOO" }: id not a positive int"# 205 ); 206 assert_err!( 207 "testdata/duplicate-symbol-sdks.xml", 208 r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "bar", "bar"] }: symbol contains duplicate references to the same sdk"# 209 ); 210 assert_err!( 211 "testdata/symbol-refers-to-non-existent-sdk.xml", 212 r#"Symbol { jar: "framework-something", pattern: "*", sdks: ["foo", "does-not-exist", "bar"] }: symbol refers to non-existent sdk does-not-exist"# 213 ); 214 assert_err!( 215 "testdata/whitespace-in-jar.xml", 216 r#"Symbol { jar: "framework something", pattern: "*", sdks: ["foo", "bar"] }: jar contains whitespace"# 217 ); 218 assert_err!( 219 "testdata/whitespace-in-pattern.xml", 220 r#"Symbol { jar: "framework-something-else", pattern: "android.app.appsearch.AppSearchSchema.DocumentPropertyConfig.Builder\n .addIndexableNestedProperties ", sdks: ["bar"] }: pattern contains whitespace"# 221 ); 222 } 223 224 #[test] test_actual_sdk_extensions_info_contents()225 fn test_actual_sdk_extensions_info_contents() { 226 verify_xml(&include_bytes!("../sdk-extensions-info.xml")[..]).unwrap(); 227 } 228 } 229