Home | History | Annotate | Download | only in manifmerger
      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 &lt; 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