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