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
17package android.databinding.compilationTest;
18
19import android.databinding.tool.CompilerChef;
20import android.databinding.tool.processing.ErrorMessages;
21import android.databinding.tool.processing.ScopedErrorReport;
22import android.databinding.tool.processing.ScopedException;
23import android.databinding.tool.reflection.InjectedClass;
24import android.databinding.tool.reflection.ModelClass;
25import android.databinding.tool.reflection.ModelMethod;
26import android.databinding.tool.reflection.java.JavaAnalyzer;
27import android.databinding.tool.store.Location;
28
29import com.google.common.base.Joiner;
30
31import org.apache.commons.io.FileUtils;
32import org.apache.commons.io.IOUtils;
33import org.apache.commons.io.filefilter.PrefixFileFilter;
34import org.apache.commons.io.filefilter.SuffixFileFilter;
35import org.apache.commons.lang3.StringUtils;
36import org.junit.Test;
37
38import java.io.File;
39import java.io.FileInputStream;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.lang.reflect.Method;
43import java.lang.reflect.Modifier;
44import java.net.URISyntaxException;
45import java.net.URL;
46import java.net.URLClassLoader;
47import java.util.ArrayList;
48import java.util.Collection;
49import java.util.List;
50import java.util.jar.JarEntry;
51import java.util.jar.JarFile;
52import java.util.jar.JarOutputStream;
53import java.util.jar.Manifest;
54
55import static org.junit.Assert.assertEquals;
56import static org.junit.Assert.assertNotEquals;
57import static org.junit.Assert.assertNotNull;
58import static org.junit.Assert.assertTrue;
59import static org.junit.Assert.fail;
60
61@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
62public 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