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.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 &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
    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