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
listTasks()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
testEmptyCompilation()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
testMultipleConfigs()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 
singleFileErrorTest(String resource, String targetFile, String expectedExtract, String errorMessage)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 
singleFileWarningTest(String resource, String targetFile, String expectedMessage)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
testMultipleExceptionsInDifferentFiles()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
testBadSyntax()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
testBrokenSyntax()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
testUndefinedVariable()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
testInvalidSetterBinding()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
testCallbackArgumentCountMismatch()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
testDuplicateCallbackArgument()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
testConflictWithVariableName()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
testRootTag()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
testInvalidVariableType()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
testSingleModule()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
testModuleDependencyChange()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
testTwoLevelDependency()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
testIncludeInMerge()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
testAssignTwoWayEvent()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
testDynamicUtilMembers()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