/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.tools.lint; import static com.android.tools.lint.LintCliFlags.ERRNO_EXISTS; import static com.android.tools.lint.LintCliFlags.ERRNO_INVALID_ARGS; import static com.android.tools.lint.LintCliFlags.ERRNO_SUCCESS; import com.android.tools.lint.checks.AbstractCheckTest; import com.android.tools.lint.checks.AccessibilityDetector; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Issue; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.PrintStream; import java.security.Permission; import java.util.List; @SuppressWarnings("javadoc") public class MainTest extends AbstractCheckTest { protected String checkLint(String[] args, List<File> files) throws Exception { PrintStream previousOut = System.out; try { final ByteArrayOutputStream output = new ByteArrayOutputStream(); System.setOut(new PrintStream(output)); Main.main(args); return output.toString(); } finally { System.setOut(previousOut); } } private void checkDriver(String expectedOutput, String expectedError, int expectedExitCode, String[] args) throws Exception { PrintStream previousOut = System.out; PrintStream previousErr = System.err; try { // Trap System.exit calls: System.setSecurityManager(new SecurityManager() { @Override public void checkPermission(Permission perm) { // allow anything. } @Override public void checkPermission(Permission perm, Object context) { // allow anything. } @Override public void checkExit(int status) { throw new ExitException(status); } }); final ByteArrayOutputStream output = new ByteArrayOutputStream(); System.setOut(new PrintStream(output)); final ByteArrayOutputStream error = new ByteArrayOutputStream(); System.setErr(new PrintStream(error)); int exitCode = 0xCAFEBABE; // not set try { Main.main(args); } catch (ExitException e) { // Allow exitCode = e.getStatus(); } assertEquals(expectedError, cleanup(error.toString())); assertEquals(expectedOutput, cleanup(output.toString())); assertEquals(expectedExitCode, exitCode); } finally { // Re-enable system exit for unit test System.setSecurityManager(null); System.setOut(previousOut); System.setErr(previousErr); } } public void testArguments() throws Exception { checkDriver( // Expected output "\n" + "Scanning MainTest_testArguments: .\n" + "res/layout/accessibility.xml:4: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageView android:id=\"@+id/android_logo\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "res/layout/accessibility.xml:5: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageButton android:importantForAccessibility=\"yes\" android:id=\"@+id/android_logo2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "0 errors, 2 warnings\n", // Expected error "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--check", "ContentDescription", "--disable", "LintError", getProjectDir(null, "res/layout/accessibility.xml").getPath() }); } public void testShowDescription() throws Exception { checkDriver( // Expected output "NewApi\n" + "------\n" + "Summary: Calling new methods on older versions\n" + "\n" + "Priority: 6 / 10\n" + "Severity: Error\n" + "Category: Correctness\n" + "\n" + "This check scans through all the Android API calls in the application and\n" + "warns about any calls that are not available on all versions targeted by this\n" + "application (according to its minimum SDK attribute in the manifest).\n" + "\n" + "If you really want to use this API and don't need to support older devices\n" + "just set the minSdkVersion in your build.gradle or AndroidManifest.xml files.\n" + "\n" + "If your code is deliberately accessing newer APIs, and you have ensured (e.g.\n" + "with conditional execution) that this code will only ever be called on a\n" + "supported platform, then you can annotate your class or method with the\n" + "@TargetApi annotation specifying the local minimum SDK to apply, such as\n" + "@TargetApi(11), such that this check considers 11 rather than your manifest\n" + "file's minimum SDK as the required API level.\n" + "\n" + "If you are deliberately setting android: attributes in style definitions, make\n" + "sure you place this in a values-vNN folder in order to avoid running into\n" + "runtime conflicts on certain devices where manufacturers have added custom\n" + "attributes whose ids conflict with the new ones on later platforms.\n" + "\n" + "Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that\n" + "the element will only be inflated in an adequate context.\n" + "\n" + "\n", // Expected error "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--show", "NewApi" }); } public void testShowDescriptionWithUrl() throws Exception { checkDriver("" // Expected output + "SdCardPath\n" + "----------\n" + "Summary: Hardcoded reference to /sdcard\n" + "\n" + "Priority: 6 / 10\n" + "Severity: Warning\n" + "Category: Correctness\n" + "\n" + "Your code should not reference the /sdcard path directly; instead use\n" + "Environment.getExternalStorageDirectory().getPath().\n" + "\n" + "Similarly, do not reference the /data/data/ path directly; it can vary in\n" + "multi-user scenarios. Instead, use Context.getFilesDir().getPath().\n" + "\n" + "More information: \n" + "http://developer.android.com/guide/topics/data/data-storage.html#filesExternal\n" + "\n", // Expected error "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--show", "SdCardPath" }); } public void testNonexistentLibrary() throws Exception { checkDriver( "", "Library foo.jar does not exist.\n", // Expected exit code ERRNO_INVALID_ARGS, // Args new String[] { "--libraries", "foo.jar", "prj" }); } public void testMultipleProjects() throws Exception { File project = getProjectDir(null, "bytecode/classes.jar=>libs/classes.jar"); checkDriver( "", "The --sources, --classpath, --libraries and --resources arguments can only be used with a single project\n", // Expected exit code ERRNO_INVALID_ARGS, // Args new String[] { "--libraries", new File(project, "libs/classes.jar").getPath(), "--disable", "LintError", project.getPath(), project.getPath() }); } public void testCustomResourceDirs() throws Exception { File project = getProjectDir(null, "res/layout/accessibility.xml=>myres1/layout/accessibility1.xml", "res/layout/accessibility.xml=>myres2/layout/accessibility1.xml" ); checkDriver( "\n" + "Scanning MainTest_testCustomResourceDirs: ..\n" + "myres1/layout/accessibility1.xml:4: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageView android:id=\"@+id/android_logo\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "myres2/layout/accessibility1.xml:4: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageView android:id=\"@+id/android_logo\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "myres1/layout/accessibility1.xml:5: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageButton android:importantForAccessibility=\"yes\" android:id=\"@+id/android_logo2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "myres2/layout/accessibility1.xml:5: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageButton android:importantForAccessibility=\"yes\" android:id=\"@+id/android_logo2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "0 errors, 4 warnings\n", // Expected output "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--check", "ContentDescription", "--disable", "LintError", "--resources", new File(project, "myres1").getPath(), "--resources", new File(project, "myres2").getPath(), project.getPath(), }); } public void testPathList() throws Exception { File project = getProjectDir(null, "res/layout/accessibility.xml=>myres1/layout/accessibility1.xml", "res/layout/accessibility.xml=>myres2/layout/accessibility1.xml" ); checkDriver( "\n" + "Scanning MainTest_testPathList: ..\n" + "myres1/layout/accessibility1.xml:4: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageView android:id=\"@+id/android_logo\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "myres2/layout/accessibility1.xml:4: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageView android:id=\"@+id/android_logo\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "myres1/layout/accessibility1.xml:5: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageButton android:importantForAccessibility=\"yes\" android:id=\"@+id/android_logo2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "myres2/layout/accessibility1.xml:5: Warning: [Accessibility] Missing contentDescription attribute on image [ContentDescription]\n" + " <ImageButton android:importantForAccessibility=\"yes\" android:id=\"@+id/android_logo2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\" android:src=\"@drawable/android_button\" android:focusable=\"false\" android:clickable=\"false\" android:layout_weight=\"1.0\" />\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "0 errors, 4 warnings\n", // Expected output "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--check", "ContentDescription", "--disable", "LintError", "--resources", // Combine two paths with a single separator here new File(project, "myres1").getPath() + ':' + new File(project, "myres2").getPath(), project.getPath(), }); } public void testClassPath() throws Exception { File project = getProjectDir(null, "apicheck/minsdk1.xml=>AndroidManifest.xml", "bytecode/GetterTest.java.txt=>src/test/bytecode/GetterTest.java", "bytecode/GetterTest.jar.data=>bin/classes.jar" ); checkDriver( "\n" + "Scanning MainTest_testClassPath: \n" + "src/test/bytecode/GetterTest.java:47: Warning: Calling getter method getFoo1() on self is slower than field access (mFoo1) [FieldGetter]\n" + " getFoo1();\n" + " ~~~~~~~\n" + "src/test/bytecode/GetterTest.java:48: Warning: Calling getter method getFoo2() on self is slower than field access (mFoo2) [FieldGetter]\n" + " getFoo2();\n" + " ~~~~~~~\n" + "src/test/bytecode/GetterTest.java:52: Warning: Calling getter method isBar1() on self is slower than field access (mBar1) [FieldGetter]\n" + " isBar1();\n" + " ~~~~~~\n" + "src/test/bytecode/GetterTest.java:54: Warning: Calling getter method getFoo1() on self is slower than field access (mFoo1) [FieldGetter]\n" + " this.getFoo1();\n" + " ~~~~~~~\n" + "src/test/bytecode/GetterTest.java:55: Warning: Calling getter method getFoo2() on self is slower than field access (mFoo2) [FieldGetter]\n" + " this.getFoo2();\n" + " ~~~~~~~\n" + "0 errors, 5 warnings\n", "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--check", "FieldGetter", "--classpath", new File(project, "bin/classes.jar").getPath(), "--disable", "LintError", project.getPath() }); } public void testLibraries() throws Exception { File project = getProjectDir(null, "apicheck/minsdk1.xml=>AndroidManifest.xml", "bytecode/GetterTest.java.txt=>src/test/bytecode/GetterTest.java", "bytecode/GetterTest.jar.data=>bin/classes.jar" ); checkDriver( "\n" + "Scanning MainTest_testLibraries: \n" + "No issues found.\n", "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--check", "FieldGetter", "--libraries", new File(project, "bin/classes.jar").getPath(), "--disable", "LintError", project.getPath() }); } @Override protected Detector getDetector() { // Sample issue to check by the main driver return new AccessibilityDetector(); } private static class ExitException extends SecurityException { private static final long serialVersionUID = 1L; private final int mStatus; public ExitException(int status) { super("Unit test"); mStatus = status; } public int getStatus() { return mStatus; } } public void test_getCleanPath() throws Exception { assertEquals("foo", LintCliClient.getCleanPath(new File("foo"))); String sep = File.separator; assertEquals("foo" + sep + "bar", LintCliClient.getCleanPath(new File("foo" + sep + "bar"))); assertEquals(sep, LintCliClient.getCleanPath(new File(sep))); assertEquals("foo" + sep + "bar", LintCliClient.getCleanPath(new File("foo" + sep + "." + sep + "bar"))); assertEquals("bar", LintCliClient.getCleanPath(new File("foo" + sep + ".." + sep + "bar"))); assertEquals("", LintCliClient.getCleanPath(new File("foo" + sep + ".."))); assertEquals("foo", LintCliClient.getCleanPath(new File("foo" + sep + "bar" + sep + ".."))); assertEquals("foo" + sep + ".foo" + sep + "bar", LintCliClient.getCleanPath(new File("foo" + sep + ".foo" + sep + "bar"))); assertEquals("foo" + sep + "bar", LintCliClient.getCleanPath(new File("foo" + sep + "bar" + sep + "."))); assertEquals("foo" + sep + "...", LintCliClient.getCleanPath(new File("foo" + sep + "..."))); assertEquals(".." + sep + "foo", LintCliClient.getCleanPath(new File(".." + sep + "foo"))); assertEquals(sep + "foo", LintCliClient.getCleanPath(new File(sep + "foo"))); assertEquals(sep, LintCliClient.getCleanPath(new File(sep + "foo" + sep + ".."))); assertEquals(sep + "foo", LintCliClient.getCleanPath(new File(sep + "foo" + sep + "bar " + sep + ".."))); assertEquals(sep + "c:", LintCliClient.getCleanPath(new File(sep + "c:"))); assertEquals(sep + "c:" + sep + "foo", LintCliClient.getCleanPath(new File(sep + "c:" + sep + "foo"))); } public void testGradle() throws Exception { File project = getProjectDir(null, "apicheck/minsdk1.xml=>AndroidManifest.xml", "multiproject/library.properties=>build.gradle", // dummy; only name counts "apicheck/ApiCallTest.class.data=>bin/classes/foo/bar/ApiCallTest.class" ); checkDriver("" + "\n" + "MainTest_testGradle: Error: \"MainTest_testGradle\" is a Gradle project. To correctly analyze Gradle projects, you should run \"gradlew :lint\" instead. [LintError]\n" + "1 errors, 0 warnings\n", "", // Expected exit code ERRNO_SUCCESS, // Args new String[] { "--check", "HardcodedText", project.getPath() }); } public void testValidateOutput() throws Exception { File project = getProjectDir(null, "res/layout/accessibility.xml=>myres1/layout/accessibility1.xml" ); File outputDir = new File(project, "build"); outputDir.mkdirs(); assertTrue(outputDir.setWritable(true)); checkDriver( "\n" + "Scanning MainTest_testValidateOutput: .\n" + "Scanning MainTest_testValidateOutput (Phase 2): \n", // Expected output "", // Expected exit code ERRNO_SUCCESS, // Args new String[]{ "--text", new File(outputDir, "foo2.text").getPath(), project.getPath(), }); //noinspection ResultOfMethodCallIgnored boolean disabledWrite = outputDir.setWritable(false); assertTrue(disabledWrite); checkDriver( "", // Expected output "Cannot write XML output file /TESTROOT/build/foo.xml\n", // Expected error // Expected exit code ERRNO_EXISTS, // Args new String[] { "--xml", new File(outputDir, "foo.xml").getPath(), project.getPath(), }); checkDriver( "", // Expected output "Cannot write HTML output file /TESTROOT/build/foo.html\n", // Expected error // Expected exit code ERRNO_EXISTS, // Args new String[] { "--html", new File(outputDir, "foo.html").getPath(), project.getPath(), }); checkDriver( "", // Expected output "Cannot write text output file /TESTROOT/build/foo.text\n", // Expected error // Expected exit code ERRNO_EXISTS, // Args new String[] { "--text", new File(outputDir, "foo.text").getPath(), project.getPath(), }); } @Override protected boolean isEnabled(Issue issue) { return true; } }