• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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. */
TestFiles( boolean shouldFail, @NonNull File main, @NonNull File[] libs, @NonNull File actualResult, @NonNull String expectedResult, @NonNull String expectedErrors)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 
getShouldFail()96         public boolean getShouldFail() {
97             return mShouldFail;
98         }
99 
100         @NonNull
getMain()101         public File getMain() {
102             return mMain;
103         }
104 
105         @NonNull
getLibs()106         public File[] getLibs() {
107             return mLibs;
108         }
109 
110         @NonNull
getActualResult()111         public File getActualResult() {
112             return mActualResult;
113         }
114 
115         @NonNull
getExpectedResult()116         public String getExpectedResult() {
117             return mExpectedResult;
118         }
119 
getExpectedErrors()120         public String getExpectedErrors() {
121             return mExpectedErrors;
122         }
123 
124         // Try to delete any temp file potentially created.
cleanup()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
loadTestData()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 &lt; 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
loadTestData(@onNull String filename)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      */
processTestFiles()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      */
processTestFiles(TestFiles testFiles)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