1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php 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 package com.android.manifmerger; 18 19 import com.android.annotations.NonNull; 20 import com.android.manifmerger.IMergerLog.FileAndLine; 21 import com.android.sdklib.mock.MockLog; 22 23 import org.w3c.dom.Document; 24 25 import java.io.BufferedReader; 26 import java.io.BufferedWriter; 27 import java.io.File; 28 import java.io.FileWriter; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.io.UnsupportedEncodingException; 33 import java.util.ArrayList; 34 import java.util.List; 35 36 import junit.framework.TestCase; 37 38 /** 39 * Some utilities to reduce repetitions in the {@link ManifestMergerTest}s. 40 * <p/> 41 * See {@link #loadTestData(String)} for an explanation of the data file format. 42 */ 43 abstract class ManifestMergerTestCase extends TestCase { 44 45 /** 46 * Delimiter that indicates the test must fail. 47 * An XML output and errors are still generated and checked. 48 */ 49 private static final String DELIM_FAILS = "fails"; 50 /** 51 * Delimiter that starts a library XML content. 52 * The delimiter name must be in the form {@code @libSomeName} and it will be 53 * used as the base for the test file name. Using separate lib names is encouraged 54 * since it makes the error output easier to read. 55 */ 56 private static final String DELIM_LIB = "lib"; 57 /** 58 * Delimiter that starts the main manifest XML content. 59 */ 60 private static final String DELIM_MAIN = "main"; 61 /** 62 * Delimiter that starts the resulting XML content, whatever is generated by the merge. 63 */ 64 private static final String DELIM_RESULT = "result"; 65 /** 66 * Delimiter that starts the SdkLog output. 67 * The logger prints each entry on its lines, prefixed with E for errors, 68 * W for warnings and P for regular printfs. 69 */ 70 private static final String DELIM_ERRORS = "errors"; 71 72 static class TestFiles { 73 private final File mMain; 74 private final File[] mLibs; 75 private final File mActualResult; 76 private final String mExpectedResult; 77 private final String mExpectedErrors; 78 private final boolean mShouldFail; 79 80 /** Files used by a given test case. */ 81 public TestFiles( 82 boolean shouldFail, 83 @NonNull File main, 84 @NonNull File[] libs, 85 @NonNull File actualResult, 86 @NonNull String expectedResult, 87 @NonNull String expectedErrors) { 88 mShouldFail = shouldFail; 89 mMain = main; 90 mLibs = libs; 91 mActualResult = actualResult; 92 mExpectedResult = expectedResult; 93 mExpectedErrors = expectedErrors; 94 } 95 96 public boolean getShouldFail() { 97 return mShouldFail; 98 } 99 100 @NonNull 101 public File getMain() { 102 return mMain; 103 } 104 105 @NonNull 106 public File[] getLibs() { 107 return mLibs; 108 } 109 110 @NonNull 111 public File getActualResult() { 112 return mActualResult; 113 } 114 115 @NonNull 116 public String getExpectedResult() { 117 return mExpectedResult; 118 } 119 120 public String getExpectedErrors() { 121 return mExpectedErrors; 122 } 123 124 // Try to delete any temp file potentially created. 125 public void cleanup() { 126 if (mMain != null && mMain.isFile()) { 127 mMain.delete(); 128 } 129 130 if (mActualResult != null && mActualResult.isFile()) { 131 mActualResult.delete(); 132 } 133 134 for (File f : mLibs) { 135 if (f != null && f.isFile()) { 136 f.delete(); 137 } 138 } 139 } 140 } 141 142 /** 143 * Calls {@link #loadTestData(String)} by 144 * inferring the data filename from the caller's method name. 145 * <p/> 146 * The caller method name must be composed of "test" + the leaf filename. 147 * Extensions ".xml" or ".txt" are implied. 148 * <p/> 149 * E.g. to use the data file "12_foo.xml", simply call this from a method 150 * named "test12_foo". 151 * 152 * @return A new {@link TestFiles} instance. Never null. 153 * @throws Exception when things go wrong. 154 * @see #loadTestData(String) 155 */ 156 @NonNull 157 TestFiles loadTestData() throws Exception { 158 StackTraceElement[] stack = Thread.currentThread().getStackTrace(); 159 for (int i = 0, n = stack.length; i < n; i++) { 160 StackTraceElement caller = stack[i]; 161 String name = caller.getMethodName(); 162 if (name.startsWith("test")) { 163 return loadTestData(name.substring(4)); 164 } 165 } 166 167 throw new IllegalArgumentException("No caller method found which name started with 'test'"); 168 } 169 170 /** 171 * Loads test data for a given test case. 172 * The input (main + libs) are stored in temp files. 173 * A new destination temp file is created to store the actual result output. 174 * The expected result is actually kept in a string. 175 * <p/> 176 * Data File Syntax: 177 * <ul> 178 * <li> Lines starting with # are ignored (anywhere, as long as # is the first char). 179 * <li> Lines before the first {@code @delimiter} are ignored. 180 * <li> Empty lines just after the {@code @delimiter} 181 * and before the first < XML line are ignored. 182 * <li> Valid delimiters are {@code @main} for the XML of the main app manifest. 183 * <li> Following delimiters are {@code @libXYZ}, read in the order of definition. 184 * The name can be anything as long as it starts with "{@code @lib}". 185 * </ul> 186 * 187 * @param filename The test data filename. If no extension is provided, this will 188 * try with .xml or .txt. Must not be null. 189 * @return A new {@link TestFiles} instance. Must not be null. 190 * @throws Exception when things fail to load properly. 191 */ 192 @NonNull 193 TestFiles loadTestData(@NonNull String filename) throws Exception { 194 195 String resName = "data" + File.separator + filename; 196 InputStream is = null; 197 BufferedReader reader = null; 198 BufferedWriter writer = null; 199 200 try { 201 is = this.getClass().getResourceAsStream(resName); 202 if (is == null && !filename.endsWith(".xml")) { 203 String resName2 = resName + ".xml"; 204 is = this.getClass().getResourceAsStream(resName2); 205 if (is != null) { 206 filename = resName2; 207 } 208 } 209 if (is == null && !filename.endsWith(".txt")) { 210 String resName3 = resName + ".txt"; 211 is = this.getClass().getResourceAsStream(resName3); 212 if (is != null) { 213 filename = resName3; 214 } 215 } 216 assertNotNull("Test data file not found for " + filename, is); 217 218 reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); 219 220 // Get the temporary directory to use. Just create a temp file, extracts its 221 // directory and remove the file. 222 File tempFile = File.createTempFile(this.getClass().getSimpleName(), ".tmp"); 223 File tempDir = tempFile.getParentFile(); 224 if (!tempFile.delete()) { 225 tempFile.deleteOnExit(); 226 } 227 228 String line = null; 229 String delimiter = null; 230 boolean skipEmpty = true; 231 232 boolean shouldFail = false; 233 StringBuilder expectedResult = new StringBuilder(); 234 StringBuilder expectedErrors = new StringBuilder(); 235 File mainFile = null; 236 File actualResultFile = null; 237 List<File> libFiles = new ArrayList<File>(); 238 int tempIndex = 0; 239 240 while ((line = reader.readLine()) != null) { 241 if (skipEmpty && line.trim().length() == 0) { 242 continue; 243 } 244 if (line.length() > 0 && line.charAt(0) == '#') { 245 continue; 246 } 247 if (line.length() > 0 && line.charAt(0) == '@') { 248 delimiter = line.substring(1); 249 assertTrue( 250 "Unknown delimiter @" + delimiter + " in " + filename, 251 delimiter.startsWith(DELIM_LIB) || 252 delimiter.equals(DELIM_MAIN) || 253 delimiter.equals(DELIM_RESULT) || 254 delimiter.equals(DELIM_ERRORS) || 255 delimiter.equals(DELIM_FAILS)); 256 257 skipEmpty = true; 258 259 if (writer != null) { 260 try { 261 writer.close(); 262 } catch (IOException ignore) {} 263 writer = null; 264 } 265 266 if (delimiter.equals(DELIM_FAILS)) { 267 shouldFail = true; 268 269 } else if (!delimiter.equals(DELIM_ERRORS)) { 270 tempFile = new File(tempDir, String.format("%1$s%2$d_%3$s.xml", 271 this.getClass().getSimpleName(), 272 tempIndex++, 273 delimiter.replaceAll("[^a-zA-Z0-9_-]", "") 274 )); 275 tempFile.deleteOnExit(); 276 277 if (delimiter.startsWith(DELIM_LIB)) { 278 libFiles.add(tempFile); 279 280 } else if (delimiter.equals(DELIM_MAIN)) { 281 mainFile = tempFile; 282 283 } else if (delimiter.equals(DELIM_RESULT)) { 284 actualResultFile = tempFile; 285 286 } else { 287 fail("Unexpected data file delimiter @" + delimiter + 288 " in " + filename); 289 } 290 291 if (!delimiter.equals(DELIM_RESULT)) { 292 writer = new BufferedWriter(new FileWriter(tempFile)); 293 } 294 } 295 296 continue; 297 } 298 if (delimiter != null && 299 skipEmpty && 300 line.length() > 0 && 301 line.charAt(0) != '#' && 302 line.charAt(0) != '@') { 303 skipEmpty = false; 304 } 305 if (writer != null) { 306 writer.write(line); 307 writer.write('\n'); 308 } else if (DELIM_RESULT.equals(delimiter)) { 309 expectedResult.append(line).append('\n'); 310 } else if (DELIM_ERRORS.equals(delimiter)) { 311 expectedErrors.append(line).append('\n'); 312 } 313 } 314 315 assertNotNull("Missing @" + DELIM_MAIN + " in " + filename, mainFile); 316 assertNotNull("Missing @" + DELIM_RESULT + " in " + filename, actualResultFile); 317 318 assert mainFile != null; 319 assert actualResultFile != null; 320 321 return new TestFiles( 322 shouldFail, 323 mainFile, 324 libFiles.toArray(new File[libFiles.size()]), 325 actualResultFile, 326 expectedResult.toString(), 327 expectedErrors.toString()); 328 329 } catch (UnsupportedEncodingException e) { 330 // BufferedReader failed to decode UTF-8, O'RLY? 331 throw e; 332 333 } finally { 334 if (writer != null) { 335 try { 336 writer.close(); 337 } catch (IOException ignore) {} 338 } 339 if (reader != null) { 340 try { 341 reader.close(); 342 } catch (IOException ignore) {} 343 } 344 if (is != null) { 345 try { 346 is.close(); 347 } catch (IOException ignore) {} 348 } 349 } 350 } 351 352 /** 353 * Loads the data test files using {@link #loadTestData()} and then 354 * invokes {@link #processTestFiles(TestFiles)} to test them. 355 * 356 * @see #loadTestData() 357 * @see #processTestFiles(TestFiles) 358 */ 359 void processTestFiles() throws Exception { 360 processTestFiles(loadTestData()); 361 } 362 363 /** 364 * Processes the data from the given {@link TestFiles} by 365 * invoking {@link ManifestMerger#process(File, File, File[])}: 366 * the given library files are applied consecutively to the main XML 367 * document and the output is generated. 368 * <p/> 369 * Then the expected and actual outputs are loaded into a DOM, 370 * dumped again to a String using an XML transform and compared. 371 * This makes sure only the structure is checked and that any 372 * formatting is ignored in the comparison. 373 * 374 * @param testFiles The test files to process. Must not be null. 375 * @throws Exception when this go wrong. 376 */ 377 void processTestFiles(TestFiles testFiles) throws Exception { 378 MockLog log = new MockLog(); 379 IMergerLog mergerLog = MergerLog.wrapSdkLog(log); 380 ManifestMerger merger = new ManifestMerger(mergerLog, new ICallback() { 381 @Override 382 public int queryCodenameApiLevel(@NonNull String codename) { 383 if ("ApiCodename1".equals(codename)) { 384 return 1; 385 } else if ("ApiCodename10".equals(codename)) { 386 return 10; 387 } 388 return ICallback.UNKNOWN_CODENAME; 389 } 390 }); 391 boolean processOK = merger.process(testFiles.getActualResult(), 392 testFiles.getMain(), 393 testFiles.getLibs()); 394 395 String expectedErrors = testFiles.getExpectedErrors().trim(); 396 StringBuilder actualErrors = new StringBuilder(); 397 for (String s : log.getMessages()) { 398 actualErrors.append(s); 399 if (!s.endsWith("\n")) { 400 actualErrors.append('\n'); 401 } 402 } 403 assertEquals("Error generated during merging", 404 expectedErrors, actualErrors.toString().trim()); 405 406 if (testFiles.getShouldFail()) { 407 assertFalse("Merge process() returned true, expected false", processOK); 408 } else { 409 assertTrue("Merge process() returned false, expected true", processOK); 410 } 411 412 // Test result XML. There should always be one created 413 // since the process action does not stop on errors. 414 log.clear(); 415 Document document = XmlUtils.parseDocument(testFiles.getActualResult(), mergerLog); 416 assertNotNull(document); 417 assert document != null; // for Eclipse null analysis 418 String actual = XmlUtils.printXmlString(document, mergerLog); 419 assertEquals("Error parsing actual result XML", "[]", log.toString()); 420 log.clear(); 421 document = XmlUtils.parseDocument( 422 testFiles.getExpectedResult(), 423 mergerLog, 424 new FileAndLine("<expected-result>", 0)); 425 assertNotNull(document); 426 assert document != null; 427 String expected = XmlUtils.printXmlString(document, mergerLog); 428 assertEquals("Error parsing expected result XML", "[]", log.toString()); 429 assertEquals("Error comparing expected to actual result", expected, actual); 430 431 testFiles.cleanup(); 432 } 433 434 } 435