Home | History | Annotate | Download | only in compilationTest
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
      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 android.databinding.compilationTest;
     18 
     19 import android.databinding.tool.CompilerChef;
     20 import android.databinding.tool.processing.ErrorMessages;
     21 import android.databinding.tool.processing.ScopedErrorReport;
     22 import android.databinding.tool.processing.ScopedException;
     23 import android.databinding.tool.reflection.InjectedClass;
     24 import android.databinding.tool.reflection.ModelClass;
     25 import android.databinding.tool.reflection.ModelMethod;
     26 import android.databinding.tool.reflection.java.JavaAnalyzer;
     27 import android.databinding.tool.store.Location;
     28 
     29 import com.google.common.base.Joiner;
     30 
     31 import org.apache.commons.io.FileUtils;
     32 import org.apache.commons.io.IOUtils;
     33 import org.apache.commons.io.filefilter.PrefixFileFilter;
     34 import org.apache.commons.io.filefilter.SuffixFileFilter;
     35 import org.apache.commons.lang3.StringUtils;
     36 import org.junit.Test;
     37 
     38 import java.io.File;
     39 import java.io.FileInputStream;
     40 import java.io.FileOutputStream;
     41 import java.io.IOException;
     42 import java.lang.reflect.Method;
     43 import java.lang.reflect.Modifier;
     44 import java.net.URISyntaxException;
     45 import java.net.URL;
     46 import java.net.URLClassLoader;
     47 import java.util.ArrayList;
     48 import java.util.Collection;
     49 import java.util.List;
     50 import java.util.jar.JarEntry;
     51 import java.util.jar.JarFile;
     52 import java.util.jar.JarOutputStream;
     53 import java.util.jar.Manifest;
     54 
     55 import static org.junit.Assert.assertEquals;
     56 import static org.junit.Assert.assertNotEquals;
     57 import static org.junit.Assert.assertNotNull;
     58 import static org.junit.Assert.assertTrue;
     59 import static org.junit.Assert.fail;
     60 
     61 @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
     62 public class SimpleCompilationTest extends BaseCompilationTest {
     63 
     64     @Test
     65     public void listTasks() throws IOException, URISyntaxException, InterruptedException {
     66         prepareProject();
     67         CompilationResult result = runGradle("tasks");
     68         assertEquals(0, result.resultCode);
     69         assertTrue("there should not be any errors", StringUtils.isEmpty(result.error));
     70         assertTrue("Test sanity, empty project tasks",
     71                 result.resultContainsText("All tasks runnable from root project"));
     72     }
     73 
     74     @Test
     75     public void testEmptyCompilation() throws IOException, URISyntaxException, InterruptedException {
     76         prepareProject();
     77         CompilationResult result = runGradle("assembleDebug");
     78         assertEquals(result.error, 0, result.resultCode);
     79         assertTrue("there should not be any errors " + result.error,
     80                 StringUtils.isEmpty(result.error));
     81         assertTrue("Test sanity, should compile fine",
     82                 result.resultContainsText("BUILD SUCCESSFUL"));
     83     }
     84 
     85     @Test
     86     public void testMultipleConfigs() throws IOException, URISyntaxException, InterruptedException {
     87         prepareProject();
     88         copyResourceTo("/layout/basic_layout.xml",
     89                 "/app/src/main/res/layout/main.xml");
     90         copyResourceTo("/layout/basic_layout.xml",
     91                 "/app/src/main/res/layout-sw100dp/main.xml");
     92         CompilationResult result = runGradle("assembleDebug");
     93         assertEquals(result.error, 0, result.resultCode);
     94         File debugOut = new File(testFolder,
     95                 "app/build/intermediates/data-binding-layout-out/debug");
     96         Collection<File> layoutFiles = FileUtils.listFiles(debugOut, new SuffixFileFilter(".xml"),
     97                 new PrefixFileFilter("layout"));
     98         assertTrue("test sanity", layoutFiles.size() > 1);
     99         for (File layout : layoutFiles) {
    100             final String contents = FileUtils.readFileToString(layout);
    101             if (layout.getParent().contains("sw100")) {
    102                 assertTrue("File has wrong tag:" + layout.getPath(),
    103                         contents.indexOf("android:tag=\"layout-sw100dp/main_0\"") > 0);
    104             } else {
    105                 assertTrue("File has wrong tag:" + layout.getPath() + "\n" + contents,
    106                         contents.indexOf("android:tag=\"layout/main_0\"")
    107                                 > 0);
    108             }
    109         }
    110     }
    111 
    112     private ScopedException singleFileErrorTest(String resource, String targetFile,
    113             String expectedExtract, String errorMessage)
    114             throws IOException, URISyntaxException, InterruptedException {
    115         prepareProject();
    116         copyResourceTo(resource, targetFile);
    117         CompilationResult result = runGradle("assembleDebug");
    118         assertNotEquals(0, result.resultCode);
    119         ScopedException scopedException = result.getBindingException();
    120         assertNotNull(result.error, scopedException);
    121         ScopedErrorReport report = scopedException.getScopedErrorReport();
    122         assertNotNull(report);
    123         assertEquals(1, report.getLocations().size());
    124         Location loc = report.getLocations().get(0);
    125         if (expectedExtract != null) {
    126             String extract = extract(targetFile, loc);
    127             assertEquals(expectedExtract, extract);
    128         }
    129         final File errorFile = new File(report.getFilePath());
    130         assertTrue(errorFile.exists());
    131         assertEquals(new File(testFolder, targetFile).getCanonicalFile(),
    132                 errorFile.getCanonicalFile());
    133         if (errorMessage != null) {
    134             assertEquals(errorMessage, scopedException.getBareMessage());
    135         }
    136         return scopedException;
    137     }
    138 
    139     private void singleFileWarningTest(String resource, String targetFile,
    140             String expectedMessage)
    141             throws IOException, URISyntaxException, InterruptedException {
    142         prepareProject();
    143         copyResourceTo(resource, targetFile);
    144         CompilationResult result = runGradle("assembleDebug");
    145         assertEquals(0, result.resultCode);
    146         final List<String> warnings = result.getBindingWarnings();
    147         boolean found = false;
    148         for (String warning : warnings) {
    149             found |= warning.contains(expectedMessage);
    150         }
    151         assertTrue(Joiner.on("\n").join(warnings),found);
    152     }
    153 
    154     @Test
    155     public void testMultipleExceptionsInDifferentFiles()
    156             throws IOException, URISyntaxException, InterruptedException {
    157         prepareProject();
    158         copyResourceTo("/layout/undefined_variable_binding.xml",
    159                 "/app/src/main/res/layout/broken.xml");
    160         copyResourceTo("/layout/invalid_setter_binding.xml",
    161                 "/app/src/main/res/layout/invalid_setter.xml");
    162         CompilationResult result = runGradle("assembleDebug");
    163         assertNotEquals(result.output, 0, result.resultCode);
    164         List<ScopedException> bindingExceptions = result.getBindingExceptions();
    165         assertEquals(result.error, 2, bindingExceptions.size());
    166         File broken = new File(testFolder, "/app/src/main/res/layout/broken.xml");
    167         File invalidSetter = new File(testFolder, "/app/src/main/res/layout/invalid_setter.xml");
    168         for (ScopedException exception : bindingExceptions) {
    169             ScopedErrorReport report = exception.getScopedErrorReport();
    170             final File errorFile = new File(report.getFilePath());
    171             String message = null;
    172             String expectedErrorFile = null;
    173             if (errorFile.getCanonicalPath().equals(broken.getCanonicalPath())) {
    174                 message = String.format(ErrorMessages.UNDEFINED_VARIABLE, "myVariable");
    175                 expectedErrorFile = "/app/src/main/res/layout/broken.xml";
    176             } else if (errorFile.getCanonicalPath().equals(invalidSetter.getCanonicalPath())) {
    177                 message = String.format(ErrorMessages.CANNOT_FIND_SETTER_CALL, "android:textx",
    178                         String.class.getCanonicalName(), "android.widget.TextView");
    179                 expectedErrorFile = "/app/src/main/res/layout/invalid_setter.xml";
    180             } else {
    181                 fail("unexpected exception " + exception.getBareMessage());
    182             }
    183             assertEquals(1, report.getLocations().size());
    184             Location loc = report.getLocations().get(0);
    185             String extract = extract(expectedErrorFile, loc);
    186             assertEquals("myVariable", extract);
    187             assertEquals(message, exception.getBareMessage());
    188         }
    189     }
    190 
    191     @Test
    192     public void testBadSyntax() throws IOException, URISyntaxException, InterruptedException {
    193         singleFileErrorTest("/layout/layout_with_bad_syntax.xml",
    194                 "/app/src/main/res/layout/broken.xml",
    195                 "myVar.length())",
    196                 String.format(ErrorMessages.SYNTAX_ERROR,
    197                         "extraneous input ')' expecting {<EOF>, ',', '.', '::', '[', '+', '-', " +
    198                                 "'*', '/', '%', '<<', '>>>', '>>', '<=', '>=', '>', '<', " +
    199                                 "'instanceof', '==', '!=', '&', '^', '|', '&&', '||', '?', '??'}"));
    200     }
    201 
    202     @Test
    203     public void testBrokenSyntax() throws IOException, URISyntaxException, InterruptedException {
    204         singleFileErrorTest("/layout/layout_with_completely_broken_syntax.xml",
    205                 "/app/src/main/res/layout/broken.xml",
    206                 "new String()",
    207                 String.format(ErrorMessages.SYNTAX_ERROR,
    208                         "mismatched input 'String' expecting {<EOF>, ',', '.', '::', '[', '+', " +
    209                                 "'-', '*', '/', '%', '<<', '>>>', '>>', '<=', '>=', '>', '<', " +
    210                                 "'instanceof', '==', '!=', '&', '^', '|', '&&', '||', '?', '??'}"));
    211     }
    212 
    213     @Test
    214     public void testUndefinedVariable() throws IOException, URISyntaxException,
    215             InterruptedException {
    216         ScopedException ex = singleFileErrorTest("/layout/undefined_variable_binding.xml",
    217                 "/app/src/main/res/layout/broken.xml", "myVariable",
    218                 String.format(ErrorMessages.UNDEFINED_VARIABLE, "myVariable"));
    219     }
    220 
    221     @Test
    222     public void testInvalidSetterBinding() throws IOException, URISyntaxException,
    223             InterruptedException {
    224         prepareProject();
    225         ScopedException ex = singleFileErrorTest("/layout/invalid_setter_binding.xml",
    226                 "/app/src/main/res/layout/invalid_setter.xml", "myVariable",
    227                 String.format(ErrorMessages.CANNOT_FIND_SETTER_CALL, "android:textx",
    228                         String.class.getCanonicalName(), "android.widget.TextView"));
    229     }
    230 
    231     @Test
    232     public void testCallbackArgumentCountMismatch() throws Throwable {
    233         singleFileErrorTest("/layout/layout_with_missing_callback_args.xml",
    234                 "/app/src/main/res/layout/broken.xml",
    235                 "(seekBar, progress) -> obj.length()",
    236                 String.format(ErrorMessages.CALLBACK_ARGUMENT_COUNT_MISMATCH,
    237                         "android.databinding.adapters.SeekBarBindingAdapter.OnProgressChanged",
    238                         "onProgressChanged", 3, 2));
    239     }
    240 
    241     @Test
    242     public void testDuplicateCallbackArgument() throws Throwable {
    243         singleFileErrorTest("/layout/layout_with_duplicate_callback_identifier.xml",
    244                 "/app/src/main/res/layout/broken.xml",
    245                 "(seekBar, progress, progress) -> obj.length()",
    246                 String.format(ErrorMessages.DUPLICATE_CALLBACK_ARGUMENT,
    247                         "progress"));
    248     }
    249 
    250     @Test
    251     public void testConflictWithVariableName() throws Throwable {
    252         singleFileWarningTest("/layout/layout_with_same_name_for_var_and_callback.xml",
    253                 "/app/src/main/res/layout/broken.xml",
    254                 String.format(ErrorMessages.CALLBACK_VARIABLE_NAME_CLASH,
    255                         "myVar", "myVar", "String"));
    256 
    257     }
    258 
    259     @Test
    260     public void testRootTag() throws IOException, URISyntaxException,
    261             InterruptedException {
    262         prepareProject();
    263         copyResourceTo("/layout/root_tag.xml", "/app/src/main/res/layout/root_tag.xml");
    264         CompilationResult result = runGradle("assembleDebug");
    265         assertNotEquals(0, result.resultCode);
    266         assertNotNull(result.error);
    267         final String expected = String.format(ErrorMessages.ROOT_TAG_NOT_SUPPORTED, "hello");
    268         assertTrue(result.error.contains(expected));
    269     }
    270 
    271     @Test
    272     public void testInvalidVariableType() throws IOException, URISyntaxException,
    273             InterruptedException {
    274         prepareProject();
    275         ScopedException ex = singleFileErrorTest("/layout/invalid_variable_type.xml",
    276                 "/app/src/main/res/layout/invalid_variable.xml", "myVariable",
    277                 String.format(ErrorMessages.CANNOT_RESOLVE_TYPE, "myVariable"));
    278     }
    279 
    280     @Test
    281     public void testSingleModule() throws IOException, URISyntaxException, InterruptedException {
    282         prepareApp(toMap(KEY_DEPENDENCIES, "compile project(':module1')",
    283                 KEY_SETTINGS_INCLUDES, "include ':app'\ninclude ':module1'"));
    284         prepareModule("module1", "com.example.module1", toMap());
    285         copyResourceTo("/layout/basic_layout.xml", "/module1/src/main/res/layout/module_layout.xml");
    286         copyResourceTo("/layout/basic_layout.xml", "/app/src/main/res/layout/app_layout.xml");
    287         CompilationResult result = runGradle("assembleDebug");
    288         assertEquals(result.error, 0, result.resultCode);
    289     }
    290 
    291     @Test
    292     public void testModuleDependencyChange() throws IOException, URISyntaxException,
    293             InterruptedException {
    294         prepareApp(toMap(KEY_DEPENDENCIES, "compile project(':module1')",
    295                 KEY_SETTINGS_INCLUDES, "include ':app'\ninclude ':module1'"));
    296         prepareModule("module1", "com.example.module1", toMap(
    297                 KEY_DEPENDENCIES, "compile 'com.android.support:appcompat-v7:23.1.1'"
    298         ));
    299         copyResourceTo("/layout/basic_layout.xml", "/module1/src/main/res/layout/module_layout.xml");
    300         copyResourceTo("/layout/basic_layout.xml", "/app/src/main/res/layout/app_layout.xml");
    301         CompilationResult result = runGradle("assembleDebug");
    302         assertEquals(result.error, 0, result.resultCode);
    303         File moduleFolder = new File(testFolder, "module1");
    304         copyResourceTo("/module_build.gradle", new File(moduleFolder, "build.gradle"),
    305                 toMap());
    306         result = runGradle("assembleDebug");
    307         assertEquals(result.error, 0, result.resultCode);
    308     }
    309 
    310     @Test
    311     public void testTwoLevelDependency() throws IOException, URISyntaxException, InterruptedException {
    312         prepareApp(toMap(KEY_DEPENDENCIES, "compile project(':module1')",
    313                 KEY_SETTINGS_INCLUDES, "include ':app'\ninclude ':module1'\n"
    314                         + "include ':module2'"));
    315         prepareModule("module1", "com.example.module1", toMap(KEY_DEPENDENCIES,
    316                 "compile project(':module2')"));
    317         prepareModule("module2", "com.example.module2", toMap());
    318         copyResourceTo("/layout/basic_layout.xml",
    319                 "/module2/src/main/res/layout/module2_layout.xml");
    320         copyResourceTo("/layout/basic_layout.xml", "/module1/src/main/res/layout/module1_layout.xml");
    321         copyResourceTo("/layout/basic_layout.xml", "/app/src/main/res/layout/app_layout.xml");
    322         CompilationResult result = runGradle("assembleDebug");
    323         assertEquals(result.error, 0, result.resultCode);
    324     }
    325 
    326     @Test
    327     public void testIncludeInMerge() throws Throwable {
    328         prepareProject();
    329         copyResourceTo("/layout/merge_include.xml", "/app/src/main/res/layout/merge_include.xml");
    330         CompilationResult result = runGradle("assembleDebug");
    331         assertNotEquals(0, result.resultCode);
    332         List<ScopedException> errors = ScopedException.extractErrors(result.error);
    333         assertEquals(result.error, 1, errors.size());
    334         final ScopedException ex = errors.get(0);
    335         final ScopedErrorReport report = ex.getScopedErrorReport();
    336         final File errorFile = new File(report.getFilePath());
    337         assertTrue(errorFile.exists());
    338         assertEquals(
    339                 new File(testFolder, "/app/src/main/res/layout/merge_include.xml")
    340                         .getCanonicalFile(),
    341                 errorFile.getCanonicalFile());
    342         assertEquals("Merge shouldn't support includes as root. Error message was '" + result.error,
    343                 ErrorMessages.INCLUDE_INSIDE_MERGE, ex.getBareMessage());
    344     }
    345 
    346     @Test
    347     public void testAssignTwoWayEvent() throws Throwable {
    348         prepareProject();
    349         copyResourceTo("/layout/layout_with_two_way_event_attribute.xml",
    350                 "/app/src/main/res/layout/layout_with_two_way_event_attribute.xml");
    351         CompilationResult result = runGradle("assembleDebug");
    352         assertNotEquals(0, result.resultCode);
    353         List<ScopedException> errors = ScopedException.extractErrors(result.error);
    354         assertEquals(result.error, 1, errors.size());
    355         final ScopedException ex = errors.get(0);
    356         final ScopedErrorReport report = ex.getScopedErrorReport();
    357         final File errorFile = new File(report.getFilePath());
    358         assertTrue(errorFile.exists());
    359         assertEquals(new File(testFolder,
    360                 "/app/src/main/res/layout/layout_with_two_way_event_attribute.xml")
    361                         .getCanonicalFile(),
    362                 errorFile.getCanonicalFile());
    363         assertEquals("The attribute android:textAttrChanged is a two-way binding event attribute " +
    364                 "and cannot be assigned.", ex.getBareMessage());
    365     }
    366 
    367     @SuppressWarnings("deprecated")
    368     @Test
    369     public void testDynamicUtilMembers() throws Throwable {
    370         prepareProject();
    371         CompilationResult result = runGradle("assembleDebug");
    372         assertEquals(result.error, 0, result.resultCode);
    373         assertTrue("there should not be any errors " + result.error,
    374                 StringUtils.isEmpty(result.error));
    375         assertTrue("Test sanity, should compile fine",
    376                 result.resultContainsText("BUILD SUCCESSFUL"));
    377         File classFile = new File(testFolder,
    378                 "app/build/intermediates/classes/debug/android/databinding/DynamicUtil.class");
    379         assertTrue(classFile.exists());
    380 
    381         File root = new File(testFolder, "app/build/intermediates/classes/debug/");
    382         URL[] urls = new URL[] {root.toURL()};
    383         JavaAnalyzer.initForTests();
    384         JavaAnalyzer analyzer = (JavaAnalyzer) JavaAnalyzer.getInstance();
    385         ClassLoader classLoader = new URLClassLoader(urls, analyzer.getClassLoader());
    386         Class dynamicUtilClass = classLoader.loadClass("android.databinding.DynamicUtil");
    387 
    388         InjectedClass injectedClass = CompilerChef.pushDynamicUtilToAnalyzer();
    389 
    390         // test methods
    391         for (Method method : dynamicUtilClass.getMethods()) {
    392             // look for the method in the injected class
    393             ArrayList<ModelClass> args = new ArrayList<ModelClass>();
    394             for (Class<?> param : method.getParameterTypes()) {
    395                 args.add(analyzer.findClass(param));
    396             }
    397             ModelMethod modelMethod = injectedClass.getMethod(
    398                     method.getName(), args, Modifier.isStatic(method.getModifiers()), false);
    399             assertNotNull("Method " + method + " not found", modelMethod);
    400         }
    401     }
    402 }
    403