| /* |
| * 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_CREATED_BASELINE; |
| import static com.android.tools.lint.LintCliFlags.ERRNO_ERRORS; |
| 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.SdkConstants; |
| import com.android.Version; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.testutils.TestUtils; |
| import com.android.tools.lint.checks.AbstractCheckTest; |
| import com.android.tools.lint.checks.AccessibilityDetector; |
| import com.android.tools.lint.checks.infrastructure.TestFile; |
| import com.android.tools.lint.client.api.LintDriver; |
| import com.android.tools.lint.client.api.LintListener; |
| 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.nio.file.Files; |
| import java.nio.file.StandardOpenOption; |
| import java.util.stream.Collectors; |
| import kotlin.text.StringsKt; |
| import org.intellij.lang.annotations.Language; |
| |
| @SuppressWarnings("javadoc") |
| public class MainTest extends AbstractCheckTest { |
| public interface Cleanup { |
| String cleanup(String s); |
| } |
| |
| @Override |
| public String cleanup(String result) { |
| return super.cleanup(result); |
| } |
| |
| private void checkDriver( |
| String expectedOutput, String expectedError, int expectedExitCode, String[] args) { |
| checkDriver( |
| expectedOutput, |
| expectedError, |
| expectedExitCode, |
| args, |
| MainTest.this::cleanup, |
| null); |
| } |
| |
| public static void checkDriver( |
| String expectedOutput, |
| String expectedError, |
| int expectedExitCode, |
| String[] args, |
| @Nullable Cleanup cleanup, |
| @Nullable LintListener listener) { |
| |
| PrintStream previousOut = System.out; |
| PrintStream previousErr = System.err; |
| try { |
| final ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| System.setOut(new PrintStream(output)); |
| final ByteArrayOutputStream error = new ByteArrayOutputStream(); |
| System.setErr(new PrintStream(error)); |
| |
| Main main = |
| new Main() { |
| @Override |
| protected void initializeDriver(@NonNull LintDriver driver) { |
| super.initializeDriver(driver); |
| if (listener != null) { |
| driver.addLintListener(listener); |
| } |
| } |
| }; |
| int exitCode = main.run(args); |
| |
| String stderr = error.toString(); |
| if (cleanup != null) { |
| stderr = cleanup.cleanup(stderr); |
| } |
| if (expectedError != null && !expectedError.trim().equals(stderr.trim())) { |
| assertEquals(expectedError, stderr); // instead of fail: get difference in output |
| } |
| if (expectedOutput != null) { |
| String stdout = output.toString(); |
| expectedOutput = StringsKt.trimIndent(expectedOutput); |
| stdout = StringsKt.trimIndent(stdout); |
| if (cleanup != null) { |
| expectedOutput = cleanup.cleanup(expectedOutput); |
| stdout = cleanup.cleanup(stdout); |
| } |
| if (!expectedOutput |
| .replace('\\', '/') |
| .trim() |
| .equals(stdout.replace('\\', '/').trim())) { |
| assertEquals(expectedOutput.trim(), stdout.trim()); |
| } |
| } |
| assertEquals(expectedExitCode, exitCode); |
| } finally { |
| System.setOut(previousOut); |
| System.setErr(previousErr); |
| } |
| } |
| |
| public void testArguments() throws Exception { |
| checkDriver( |
| // Expected output |
| "\n" |
| + "Scanning MainTest_testArguments: .\n" |
| + "res/layout/accessibility.xml:4: Error: 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: Error: 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" |
| + "2 errors, 0 warnings\n", |
| |
| // Expected error |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "--check", |
| "ContentDescription", |
| "--error", |
| "ContentDescription", |
| "--disable", |
| "LintError", |
| getProjectDir(null, mAccessibility).getPath() |
| }); |
| } |
| |
| public void testShowDescription() { |
| 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" |
| + "Vendor: Android Open Source Project\n" |
| + "Contact: https://groups.google.com/g/lint-dev\n" |
| + "Feedback: https://issuetracker.google.com/issues/new?component=192708\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() { |
| checkDriver( |
| "" |
| // Expected output |
| + "SdCardPath\n" |
| + "----------\n" |
| + "Summary: Hardcoded reference to /sdcard\n" |
| + "\n" |
| + "Priority: 6 / 10\n" |
| + "Severity: Warning\n" |
| + "Category: Correctness\n" |
| + "Vendor: Android Open Source Project\n" |
| + "Contact: https://groups.google.com/g/lint-dev\n" |
| + "Feedback: https://issuetracker.google.com/issues/new?component=192708\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" |
| + "https://developer.android.com/training/data-storage#filesExternal\n" |
| + "\n", |
| |
| // Expected error |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] {"--show", "SdCardPath"}); |
| } |
| |
| public void testNonexistentLibrary() { |
| File fooJar = new File(getTempDir(), "foo.jar"); |
| checkDriver( |
| "", |
| "Library /TESTROOT/foo.jar does not exist.\n", |
| |
| // Expected exit code |
| ERRNO_INVALID_ARGS, |
| |
| // Args |
| new String[] {"--libraries", fooJar.getPath(), "prj"}); |
| } |
| |
| public void testMultipleProjects() throws Exception { |
| File project = getProjectDir(null, 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, mAccessibility2, mAccessibility3); |
| |
| checkDriver( |
| "" |
| + "\n" |
| + "Scanning MainTest_testCustomResourceDirs: ..\n" |
| + "myres1/layout/accessibility1.xml:4: Warning: 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: 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: 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: 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(), |
| "--compile-sdk-version", |
| "15", |
| "--java-language-level", |
| "11", |
| project.getPath(), |
| }); |
| } |
| |
| public void testPathList() throws Exception { |
| File project = getProjectDir(null, mAccessibility2, mAccessibility3); |
| |
| checkDriver( |
| "" |
| + "\n" |
| + "Scanning MainTest_testPathList: ..\n" |
| + "myres1/layout/accessibility1.xml:4: Warning: 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: 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: 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: 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, manifest().minSdk(1), cipherTestSource, cipherTestClass); |
| checkDriver( |
| "\n" |
| + "Scanning MainTest_testClassPath: \n" |
| + "src/test/pkg/CipherTest1.java:11: Warning: Potentially insecure random numbers on Android 4.3 and older. Read https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html for more info. [TrulyRandom]\n" |
| + " cipher.init(Cipher.WRAP_MODE, key); // FLAG\n" |
| + " ~~~~\n" |
| + "0 errors, 1 warnings\n", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "--check", |
| "TrulyRandom", |
| "--classpath", |
| new File(project, "bin/classes.jar").getPath(), |
| "--disable", |
| "LintError", |
| project.getPath() |
| }); |
| } |
| |
| public void testLibraries() throws Exception { |
| File project = getProjectDir(null, manifest().minSdk(1), cipherTestSource, cipherTestClass); |
| checkDriver( |
| "\nScanning MainTest_testLibraries: \nNo issues found.\n", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "--check", |
| "TrulyRandom", |
| "--libraries", |
| new File(project, "bin/classes.jar").getPath(), |
| "--disable", |
| "LintError", |
| project.getPath() |
| }); |
| } |
| |
| public void testCreateBaseline() throws Exception { |
| File baseline = File.createTempFile("baseline", "xml"); |
| //noinspection ResultOfMethodCallIgnored |
| baseline.delete(); // shouldn't exist |
| assertFalse(baseline.exists()); |
| //noinspection ConcatenationWithEmptyString |
| checkDriver( |
| // Expected output |
| null, |
| |
| // Expected error |
| "" |
| + "Created baseline file " |
| + cleanup(baseline.getPath()) |
| + "\n" |
| + "\n" |
| + "Also breaking the build in case this was not intentional. If you\n" |
| + "deliberately created the baseline file, re-run the build and this\n" |
| + "time it should succeed without warnings.\n" |
| + "\n" |
| + "If not, investigate the baseline path in the lintOptions config\n" |
| + "or verify that the baseline file has been checked into version\n" |
| + "control.\n" |
| + "\n", |
| |
| // Expected exit code |
| ERRNO_CREATED_BASELINE, |
| |
| // Args |
| new String[] { |
| "--check", |
| "ContentDescription", |
| "--baseline", |
| baseline.getPath(), |
| "--sdk-home", // SDK is needed to get version number for the baseline |
| TestUtils.getSdk().toString(), |
| "--disable", |
| "LintError", |
| getProjectDir(null, mAccessibility).getPath() |
| }); |
| assertTrue(baseline.exists()); |
| //noinspection ResultOfMethodCallIgnored |
| baseline.delete(); |
| } |
| |
| public void testUpdateBaseline() throws Exception { |
| File baseline = File.createTempFile("baseline", "xml"); |
| Files.write( |
| baseline.toPath(), |
| // language=XML |
| ("<issues></issues>").getBytes(), |
| StandardOpenOption.TRUNCATE_EXISTING); |
| |
| checkDriver( |
| // Expected output |
| "\n" |
| + "Scanning MainTest_testUpdateBaseline: .\n" |
| + "res/layout/accessibility.xml:4: Information: 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: Information: 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, 0 warnings\n", |
| |
| // Expected error |
| "", |
| |
| // Expected exit code |
| ERRNO_CREATED_BASELINE, |
| |
| // Args |
| new String[] { |
| "--check", |
| "ContentDescription", |
| "--info", |
| "ContentDescription", |
| "--baseline", |
| baseline.getPath(), |
| "--update-baseline", |
| "--disable", |
| "LintError", |
| getProjectDir(null, mAccessibility).getPath() |
| }); |
| |
| // Skip the first three lines that contain just the version which can change. |
| String newBaseline = |
| Files.readAllLines(baseline.toPath()).stream() |
| .skip(3) |
| .collect(Collectors.joining("\n")); |
| |
| String expected = |
| " <issue\n" |
| + " id=\"ContentDescription\"\n" |
| + " message=\"Missing `contentDescription` attribute on image\">\n" |
| + " <location\n" |
| + " file=\"res/layout/accessibility.xml\"\n" |
| + " line=\"4\"/>\n" |
| + " </issue>\n" |
| + "\n" |
| + " <issue\n" |
| + " id=\"ContentDescription\"\n" |
| + " message=\"Missing `contentDescription` attribute on image\">\n" |
| + " <location\n" |
| + " file=\"res/layout/accessibility.xml\"\n" |
| + " line=\"5\"/>\n" |
| + " </issue>\n" |
| + "\n" |
| + "</issues>"; |
| |
| assertEquals(expected, newBaseline); |
| |
| baseline.delete(); |
| } |
| |
| /** |
| * This test emulates Google3's `android_lint` setup, and catches regression caused by relative |
| * path for JAR files. |
| * |
| * @throws Exception |
| */ |
| public void testRelativePaths() throws Exception { |
| // Project with source only |
| File project = getProjectDir(null, manifest().minSdk(1), cipherTestSource); |
| |
| // Create external jar somewhere outside of project dir. |
| File pwd = new File(System.getProperty("user.dir")); |
| assertTrue(pwd.isDirectory()); |
| File classFile = cipherTestClass.createFile(pwd); |
| |
| try { |
| checkDriver( |
| "\n" |
| + "Scanning MainTest_testRelativePaths: \n" |
| + "src/test/pkg/CipherTest1.java:11: Warning: Potentially insecure random numbers on Android 4.3 and older. Read https://android-developers.blogspot.com/2013/08/some-securerandom-thoughts.html for more info. [TrulyRandom]\n" |
| + " cipher.init(Cipher.WRAP_MODE, key); // FLAG\n" |
| + " ~~~~\n" |
| + "0 errors, 1 warnings\n", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "--check", |
| "TrulyRandom", |
| "--classpath", |
| cipherTestClass.targetRelativePath, |
| "--disable", |
| "LintError", |
| project.getPath() |
| }); |
| } finally { |
| classFile.delete(); |
| } |
| } |
| |
| @Override |
| protected Detector getDetector() { |
| // Sample issue to check by the main driver |
| return new AccessibilityDetector(); |
| } |
| |
| public void test_getCleanPath() { |
| 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 + ".."))); |
| |
| if (SdkConstants.CURRENT_PLATFORM != SdkConstants.PLATFORM_WINDOWS) { |
| 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, |
| manifest().minSdk(1), |
| source("build.gradle", ""), // placeholder; only name counts |
| // placeholder to ensure we have .class files |
| source("bin/classes/foo/bar/ApiCallTest.class", "")); |
| checkDriver( |
| "" |
| + "\n" |
| + "build.gradle: 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 testGradleKts() throws Exception { |
| File project = |
| getProjectDir( |
| null, |
| manifest().minSdk(1), |
| source("build.gradle.kts", ""), // placeholder; only name counts |
| // placeholder to ensure we have .class files |
| source("bin/classes/foo/bar/ApiCallTest.class", "")); |
| checkDriver( |
| "" |
| + "\n" |
| + "build.gradle.kts: Error: \"MainTest_testGradleKts\" 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 testWall() throws Exception { |
| File project = getProjectDir(null, java("class Test {\n // STOPSHIP\n}")); |
| checkDriver( |
| "" |
| + "Scanning MainTest_testWall: ..\n" |
| + "Scanning MainTest_testWall (Phase 2): .\n" |
| + "src/Test.java:2: Error: STOPSHIP comment found; points to code which must be fixed prior to release [StopShip]\n" |
| + " // STOPSHIP\n" |
| + " ~~~~~~~~\n" |
| + "1 errors, 0 warnings", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "-Wall", "--disable", "LintError,UsesMinSdkAttributes", project.getPath() |
| }); |
| } |
| |
| public void testWerror() throws Exception { |
| File project = |
| getProjectDir(null, java("class Test {\n String s = \"/sdcard/path\";\n}")); |
| checkDriver( |
| "" |
| + "Scanning MainTest_testWerror: ..\n" |
| + "src/Test.java:2: Error: Do not hardcode \"/sdcard/\"; use Environment.getExternalStorageDirectory().getPath() instead [SdCardPath]\n" |
| + " String s = \"/sdcard/path\";\n" |
| + " ~~~~~~~~~~~~~~\n" |
| + "1 errors, 0 warnings", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "-Werror", "--disable", "LintError,UsesMinSdkAttributes", project.getPath() |
| }); |
| } |
| |
| public void testNoWarn() throws Exception { |
| File project = |
| getProjectDir( |
| null, |
| java("" + "class Test {\n String s = \"/sdcard/path\";\n}"), |
| xml( |
| "res/layout/test.xml", |
| "" |
| + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n" |
| + " <Button android:id='@+id/duplicated'/>\n" |
| + " <Button android:id='@+id/duplicated'/>\n" |
| + "</LinearLayout>\n")); |
| checkDriver( |
| "" |
| + "Scanning MainTest_testNoWarn: ....\n" |
| + "res/layout/test.xml:3: Error: Duplicate id @+id/duplicated, already defined earlier in this layout [DuplicateIds]\n" |
| + " <Button android:id='@+id/duplicated'/>\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + " res/layout/test.xml:2: Duplicate id @+id/duplicated originally defined here\n" |
| + "1 errors, 0 warnings", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "-w", "--disable", "LintError,UsesMinSdkAttributes", project.getPath() |
| }); |
| } |
| |
| public void testWrongThreadOff() throws Exception { |
| // Make sure the wrong thread interprocedural check is not included with -Wall |
| File project = |
| getProjectDir( |
| null, |
| java( |
| "" |
| + "package test.pkg;\n" |
| + "\n" |
| + "import android.support.annotation.UiThread;\n" |
| + "import android.support.annotation.WorkerThread;\n" |
| + "\n" |
| + "@FunctionalInterface\n" |
| + "public interface Runnable {\n" |
| + " public abstract void run();\n" |
| + "}\n" |
| + "\n" |
| + "class Test {\n" |
| + " @UiThread static void uiThreadStatic() { unannotatedStatic(); }\n" |
| + " static void unannotatedStatic() { workerThreadStatic(); }\n" |
| + " @WorkerThread static void workerThreadStatic() {}\n" |
| + "\n" |
| + " @UiThread void uiThread() { unannotated(); }\n" |
| + " void unannotated() { workerThread(); }\n" |
| + " @WorkerThread void workerThread() {}\n" |
| + "\n" |
| + " @UiThread void runUi() {}\n" |
| + " void runIt(Runnable r) { r.run(); }\n" |
| + " @WorkerThread void callRunIt() {\n" |
| + " runIt(() -> runUi());\n" |
| + " }\n" |
| + "\n" |
| + " public static void main(String[] args) {\n" |
| + " Test instance = new Test();\n" |
| + " instance.uiThread();\n" |
| + " }\n" |
| + "}\n"), |
| SUPPORT_ANNOTATIONS_CLASS_PATH, |
| SUPPORT_ANNOTATIONS_JAR); |
| checkDriver( |
| "" |
| + "Scanning MainTest_testWrongThreadOff: ..\n" |
| + "Scanning MainTest_testWrongThreadOff (Phase 2): .\n" |
| + "No issues found.", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "-Wall", "--disable", "LintError,UsesMinSdkAttributes", project.getPath() |
| }); |
| } |
| |
| public void testInvalidLintXmlId() throws Exception { |
| // Regression test for |
| // 37070812: Lint does not fail when invalid issue ID is referenced in XML |
| File project = |
| getProjectDir( |
| null, |
| manifest().minSdk(1), |
| xml( |
| "lint.xml", |
| "" |
| + "<lint>\n" |
| + " <issue id=\"all\" severity=\"warning\" />\n" |
| + " <issue id=\"UnknownIssueId\" severity=\"error\" />\n" |
| + " <issue id=\"SomeUnknownId\" severity=\"fatal\" />\n" |
| + " <issue id=\"Security\" severity=\"fatal\" />\n" |
| + " <issue id=\"Interoperability\" severity=\"ignore\" />\n" |
| + " <issue id=\"IconLauncherFormat\">\n" |
| + " <ignore path=\"src/main/res/mipmap-anydpi-v26/ic_launcher.xml\" />\n" |
| + " <ignore path=\"src/main/res/drawable/ic_launcher_foreground.xml\" />\n" |
| + " <ignore path=\"src/main/res/drawable/ic_launcher_background.xml\" />\n" |
| + " </issue>" |
| + "</lint>"), |
| // placeholder to ensure we have .class files |
| source("bin/classes/foo/bar/ApiCallTest.class", "")); |
| checkDriver( |
| "" |
| + "Scanning MainTest_testInvalidLintXmlId: \n" |
| + "lint.xml:4: Error: Unknown issue id \"SomeUnknownId\". Did you mean 'UnknownId' (Reference to an unknown id) ? [UnknownIssueId]\n" |
| + " <issue id=\"SomeUnknownId\" severity=\"fatal\" />\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + "1 errors, 0 warnings", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] {"--check", "HardcodedText", project.getPath()}); |
| } |
| |
| public void testFatalOnly() throws Exception { |
| // This is a lint infrastructure test to make sure we correctly include issues |
| // with fatal only |
| File project = |
| getProjectDir( |
| null, |
| manifest().minSdk(1), |
| xml( |
| "lint.xml", |
| "" |
| + "<lint>\n" |
| + " <issue id=\"DuplicateDefinition\" severity=\"fatal\"/>\n" |
| + "</lint>\n"), |
| xml( |
| "res/layout/test.xml", |
| "" |
| + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n" |
| + " <Button android:id='@+id/duplicated'/>\n" |
| + " <Button android:id='@+id/duplicated'/>\n" |
| + "</LinearLayout>\n"), |
| xml( |
| "res/values/duplicates.xml", |
| "" |
| + "<resources>\n" |
| + " <item type=\"id\" name=\"name\" />\n" |
| + " <item type=\"id\" name=\"name\" />\n" |
| + "</resources>\n"), |
| kotlin("val path = \"/sdcard/path\"")); |
| |
| // Without --fatalOnly: Both errors and warnings are reported. |
| checkDriver( |
| "" |
| + "res/layout/test.xml:3: Error: Duplicate id @+id/duplicated, already defined earlier in this layout [DuplicateIds]\n" |
| + " <Button android:id='@+id/duplicated'/>\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + " res/layout/test.xml:2: Duplicate id @+id/duplicated originally defined here\n" |
| + "res/values/duplicates.xml:3: Error: name has already been defined in this folder [DuplicateDefinition]\n" |
| + " <item type=\"id\" name=\"name\" />\n" |
| + " ~~~~~~~~~~~\n" |
| + " res/values/duplicates.xml:2: Previously defined here\n" |
| + "src/test.kt:1: Warning: Do not hardcode \"/sdcard/\"; use Environment.getExternalStorageDirectory().getPath() instead [SdCardPath]\n" |
| + "val path = \"/sdcard/path\"\n" |
| + " ~~~~~~~~~~~~\n" |
| + "res/layout/test.xml:1: Warning: The resource R.layout.test appears to be unused [UnusedResources]\n" |
| + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\">\n" |
| + "^\n" |
| + "2 errors, 2 warnings", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "--quiet", |
| "--disable", |
| "LintError,UsesMinSdkAttributes,ButtonStyle,AllowBackup", |
| project.getPath() |
| }); |
| |
| // WITH --fatalOnly: Only the DuplicateDefinition issue is flagged, since it is fatal. |
| checkDriver( |
| // Both an implicitly fatal issue (DuplicateIds) and an error severity issue |
| // configured to be fatal via lint.xml (DuplicateDefinition) |
| "" |
| + "res/layout/test.xml:3: Error: Duplicate id @+id/duplicated, already defined earlier in this layout [DuplicateIds]\n" |
| + " <Button android:id='@+id/duplicated'/>\n" |
| + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" |
| + " res/layout/test.xml:2: Duplicate id @+id/duplicated originally defined here\n" |
| + "res/values/duplicates.xml:3: Error: name has already been defined in this folder [DuplicateDefinition]\n" |
| + " <item type=\"id\" name=\"name\" />\n" |
| + " ~~~~~~~~~~~\n" |
| + " res/values/duplicates.xml:2: Previously defined here\n" |
| + "2 errors, 0 warnings", |
| "", |
| ERRNO_ERRORS, |
| |
| // Args |
| new String[] { |
| "--quiet", |
| "--disable", |
| "LintError", |
| "--disable", |
| "UsesMinSdkAttributes", |
| "--fatalOnly", |
| "--exitcode", |
| project.getPath() |
| }); |
| } |
| |
| public void testValidateOutput() throws Exception { |
| if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { |
| // This test relies on making directories not writable, then |
| // running lint pointing the output to that directory |
| // and checking that error messages make sense. This isn't |
| // supported on Windows; calling file.setWritable(false) returns |
| // false; so skip this entire test on Windows. |
| return; |
| } |
| File project = getProjectDir(null, mAccessibility2); |
| |
| File outputDir = new File(project, "build"); |
| assertTrue(outputDir.mkdirs()); |
| assertTrue(outputDir.setWritable(true)); |
| |
| checkDriver( |
| "Scanning MainTest_testValidateOutput: .\n", // Expected output |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] { |
| "--sdk-home", // SDK is needed to get version number for the baseline |
| TestUtils.getSdk().toString(), |
| "--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(), |
| }); |
| } |
| |
| public void testVersion() throws Exception { |
| File project = getProjectDir(null, manifest().minSdk(1)); |
| checkDriver( |
| "lint: version " + Version.ANDROID_GRADLE_PLUGIN_VERSION + "\n", |
| "", |
| |
| // Expected exit code |
| ERRNO_SUCCESS, |
| |
| // Args |
| new String[] {"--version", "--check", "HardcodedText", project.getPath()}); |
| } |
| |
| @Override |
| protected boolean isEnabled(Issue issue) { |
| return true; |
| } |
| |
| @Language("XML") |
| private static final String ACCESSIBILITY_XML = |
| "" |
| + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" |
| + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\" android:id=\"@+id/newlinear\" android:orientation=\"vertical\" android:layout_width=\"match_parent\" android:layout_height=\"match_parent\">\n" |
| + " <Button android:text=\"Button\" android:id=\"@+id/button1\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\"></Button>\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" |
| + " <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" |
| + " <Button android:text=\"Button\" android:id=\"@+id/button2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\"></Button>\n" |
| + " <Button android:id=\"@+android:id/summary\" android:contentDescription=\"@string/label\" />\n" |
| + " <ImageButton android:importantForAccessibility=\"no\" 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" |
| + "</LinearLayout>\n"; |
| |
| private final TestFile mAccessibility = xml("res/layout/accessibility.xml", ACCESSIBILITY_XML); |
| |
| private final TestFile mAccessibility2 = |
| xml("myres1/layout/accessibility1.xml", ACCESSIBILITY_XML); |
| |
| private final TestFile mAccessibility3 = |
| xml("myres2/layout/accessibility1.xml", ACCESSIBILITY_XML); |
| |
| @SuppressWarnings("all") // Sample code |
| private TestFile cipherTestSource = |
| java( |
| "" |
| + "package test.pkg;\n" |
| + "\n" |
| + "import java.security.Key;\n" |
| + "import java.security.SecureRandom;\n" |
| + "\n" |
| + "import javax.crypto.Cipher;\n" |
| + "\n" |
| + "@SuppressWarnings(\"all\")\n" |
| + "public class CipherTest1 {\n" |
| + " public void test1(Cipher cipher, Key key) {\n" |
| + " cipher.init(Cipher.WRAP_MODE, key); // FLAG\n" |
| + " }\n" |
| + "\n" |
| + " public void test2(Cipher cipher, Key key, SecureRandom random) {\n" |
| + " cipher.init(Cipher.ENCRYPT_MODE, key, random);\n" |
| + " }\n" |
| + "\n" |
| + " public void setup(String transform) {\n" |
| + " Cipher cipher = Cipher.getInstance(transform);\n" |
| + " }\n" |
| + "}\n"); |
| |
| @SuppressWarnings("all") // Sample code |
| private TestFile cipherTestClass = |
| jar( |
| "bin/classes.jar", |
| base64gzip( |
| "test/pkg/CipherTest1.class", |
| "" |
| + "H4sIAAAAAAAAAI1S227TQBA92zh2kgZSmrTlUqBAgSROa5U+BvFScYkIRSJV" |
| + "3x13m7pNbMveoOazeCmIBz6Aj0LM2FYSgrnY2pnZ2Zk5M2f3+4+v3wDsY7cE" |
| + "HVslPMBDFo9YbBt4bOCJgP7c9Vz1QiBXbxwLaAf+iRSodF1PHo5HfRke2f0h" |
| + "eVa7vmMPj+3Q5X3q1NSZGwmsd5WMlBVcDKwDNzijHNrutQXy7N8TMOvdc/uj" |
| + "fWk54SRQfhrVjp1WJJ1x6KqJ9VZO2tyD7sTHAmuZWdTqhZwIVDPSBUovLx0Z" |
| + "KNf3IgNP0xaeCbz+7xYWXD025AfbO/FHSXthbAts/i2SkCOpxgFNkSBbQ9sb" |
| + "WD0Vut4grlNUVCg69cMRs/tbCI3S88ehI1+5TPXKHLO7HFyGgYKBehkNNFmY" |
| + "ZbSwI1DLugwqMEN43z+XjiIGZ64pa6l3gSe6an4mAhv1zh9ubT/z5F9kLg+k" |
| + "6niRsj2HhmxkUZV5b/SE8/Sq+dMgmAqSRdpZpAXpfPMzxCcyllAiqcfOIpZJ" |
| + "lpMA0tdIC1xHhaI4uYMc/YBh6q0rLM3SS6RByTolcYmtJCwtwdYKbsRlDayi" |
| + "StG1tLM1WuvYSAGOyKeRLphaa+cKuUWESlyJEZpJ3BShMEUopAhs3cQt6mQe" |
| + "6zbupFhvaMdd6uYXaO8WkapEQG1uFn2KpGMTdyk3T4sxf53lXlzn/k9RvT9I" |
| + "XQQAAA==")); |
| } |