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