blob: 8469644071ca4d7c121dd0326f3645241980b430 [file] [log] [blame]
/*
* 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.checks;
import com.android.tools.lint.LintCliXmlParser;
import com.android.tools.lint.LombokParser;
import com.android.tools.lint.Main;
import com.android.tools.lint.Reporter;
import com.android.tools.lint.TextReporter;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.IDomParser;
import com.android.tools.lint.client.api.IJavaParser;
import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.client.api.LintDriver;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Severity;
import com.google.common.io.Files;
import com.google.common.io.InputSupplier;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
import junit.framework.TestCase;
/** Common utility methods for the various lint check tests */
@SuppressWarnings("javadoc")
public abstract class AbstractCheckTest extends TestCase {
protected abstract Detector getDetector();
private Detector mDetector;
private Detector getDetectorInstance() {
if (mDetector == null) {
mDetector = getDetector();
}
return mDetector;
}
protected List<Issue> getIssues() {
List<Issue> issues = new ArrayList<Issue>();
Class<? extends Detector> detectorClass = getDetectorInstance().getClass();
// Get the list of issues from the registry and filter out others, to make sure
// issues are properly registered
List<Issue> candidates = new BuiltinIssueRegistry().getIssues();
for (Issue issue : candidates) {
if (issue.getDetectorClass() == detectorClass) {
issues.add(issue);
}
}
return issues;
}
private class CustomIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
return AbstractCheckTest.this.getIssues();
}
}
protected String lintFiles(String... relativePaths) throws Exception {
List<File> files = new ArrayList<File>();
File targetDir = getTargetDir();
for (String relativePath : relativePaths) {
File file = getTestfile(targetDir, relativePath);
assertNotNull(file);
files.add(file);
}
addManifestFile(targetDir);
return checkLint(files);
}
protected void deleteFile(File dir) {
if (dir.isDirectory()) {
for (File f : dir.listFiles()) {
deleteFile(f);
}
} else if (dir.isFile()) {
assertTrue(dir.getPath(), dir.delete());
}
}
protected String checkLint(List<File> files) throws Exception {
mOutput = new StringBuilder();
TestLintClient lintClient = createClient();
String result = lintClient.analyze(files);
// The output typically contains a few directory/filenames.
// On Windows we need to change the separators to the unix-style
// forward slash to make the test as OS-agnostic as possible.
if (File.separatorChar != '/') {
result = result.replace(File.separatorChar, '/');
}
for (File f : files) {
deleteFile(f);
}
return result;
}
protected TestLintClient createClient() {
return new TestLintClient();
}
protected TestConfiguration getConfiguration(Project project) {
return new TestConfiguration();
}
/**
* Run lint on the given files when constructed as a separate project
* @return The output of the lint check. On Windows, this transforms all directory
* separators to the unix-style forward slash.
*/
protected String lintProject(String... relativePaths) throws Exception {
File projectDir = getProjectDir(null, relativePaths);
return checkLint(Collections.singletonList(projectDir));
}
/** Creates a project directory structure from the given files */
protected File getProjectDir(String name, String ...relativePaths) throws Exception {
assertFalse("getTargetDir must be overridden to make a unique directory",
getTargetDir().equals(getTempDir()));
File projectDir = getTargetDir();
if (name != null) {
projectDir = new File(projectDir, name);
}
assertTrue(projectDir.getPath(), projectDir.mkdirs());
List<File> files = new ArrayList<File>();
for (String relativePath : relativePaths) {
File file = getTestfile(projectDir, relativePath);
assertNotNull(file);
files.add(file);
}
addManifestFile(projectDir);
return projectDir;
}
private void addManifestFile(File projectDir) throws IOException {
// Ensure that there is at least a manifest file there to make it a valid project
// as far as Lint is concerned:
if (!new File(projectDir, "AndroidManifest.xml").exists()) {
File manifest = new File(projectDir, "AndroidManifest.xml");
FileWriter fw = new FileWriter(manifest);
fw.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" +
" package=\"foo.bar2\"\n" +
" android:versionCode=\"1\"\n" +
" android:versionName=\"1.0\" >\n" +
"</manifest>\n");
fw.close();
}
}
private StringBuilder mOutput = null;
protected static File sTempDir = null;
protected File getTempDir() {
if (sTempDir == null) {
File base = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$
String os = System.getProperty("os.name"); //$NON-NLS-1$
if (os.startsWith("Mac OS")) { //$NON-NLS-1$
base = new File("/tmp");
}
Calendar c = Calendar.getInstance();
String name = String.format("lintTests/%1$tF_%1$tT", c).replace(':', '-'); //$NON-NLS-1$
File tmpDir = new File(base, name);
if (!tmpDir.exists() && tmpDir.mkdirs()) {
sTempDir = tmpDir;
} else {
sTempDir = base;
}
}
return sTempDir;
}
protected File getTargetDir() {
return new File(getTempDir(), getClass().getSimpleName() + "_" + getName());
}
private File makeTestFile(String name, String relative,
final InputStream contents) throws IOException {
return makeTestFile(getTargetDir(), name, relative, contents);
}
private File makeTestFile(File dir, String name, String relative,
final InputStream contents) throws IOException {
if (relative != null) {
dir = new File(dir, relative);
if (!dir.exists()) {
boolean mkdir = dir.mkdirs();
assertTrue(dir.getPath(), mkdir);
}
} else if (!dir.exists()) {
boolean mkdir = dir.mkdirs();
assertTrue(dir.getPath(), mkdir);
}
File tempFile = new File(dir, name);
if (tempFile.exists()) {
tempFile.delete();
}
Files.copy(new InputSupplier<InputStream>() {
public InputStream getInput() throws IOException {
return contents;
}
}, tempFile);
return tempFile;
}
private File getTestfile(File targetDir, String relativePath) throws IOException {
// Support replacing filenames and paths with a => syntax, e.g.
// dir/file.txt=>dir2/dir3/file2.java
// will read dir/file.txt from the test data and write it into the target
// directory as dir2/dir3/file2.java
String targetPath = relativePath;
int replaceIndex = relativePath.indexOf("=>"); //$NON-NLS-1$
if (replaceIndex != -1) {
// foo=>bar
targetPath = relativePath.substring(replaceIndex + "=>".length());
relativePath = relativePath.substring(0, replaceIndex);
}
String path = "data" + File.separator + relativePath; //$NON-NLS-1$
InputStream stream =
AbstractCheckTest.class.getResourceAsStream(path);
assertNotNull(relativePath + " does not exist", stream);
int index = targetPath.lastIndexOf('/');
String relative = null;
String name = targetPath;
if (index != -1) {
name = targetPath.substring(index + 1);
relative = targetPath.substring(0, index);
}
return makeTestFile(targetDir, name, relative, stream);
}
protected boolean isEnabled(Issue issue) {
Class<? extends Detector> detectorClass = getDetectorInstance().getClass();
if (issue.getDetectorClass() == detectorClass) {
return true;
}
return false;
}
protected boolean includeParentPath() {
return false;
}
protected static String cleanup(String result) throws IOException {
if (sTempDir != null && result.contains(sTempDir.getPath())) {
result = result.replace(sTempDir.getCanonicalFile().getPath(), "/TESTROOT");
result = result.replace(sTempDir.getAbsoluteFile().getPath(), "/TESTROOT");
result = result.replace(sTempDir.getPath(), "/TESTROOT");
}
// The output typically contains a few directory/filenames.
// On Windows we need to change the separators to the unix-style
// forward slash to make the test as OS-agnostic as possible.
if (File.separatorChar != '/') {
result = result.replace(File.separatorChar, '/');
}
return result;
}
public class TestLintClient extends Main {
private StringWriter mWriter = new StringWriter();
TestLintClient() {
mReporters.add(new TextReporter(this, mWriter, false));
}
public String analyze(List<File> files) throws Exception {
mDriver = new LintDriver(new CustomIssueRegistry(), this);
mDriver.analyze(files, null /* scope */);
Collections.sort(mWarnings);
for (Reporter reporter : mReporters) {
reporter.write(mErrorCount, mWarningCount, mWarnings);
}
mOutput.append(mWriter.toString());
if (mOutput.length() == 0) {
mOutput.append("No warnings.");
}
String result = mOutput.toString();
if (result.equals("\nNo issues found.\n")) {
result = "No warnings.";
}
if (sTempDir != null && result.contains(sTempDir.getPath())) {
result = result.replace(sTempDir.getCanonicalFile().getPath(), "/TESTROOT");
result = result.replace(sTempDir.getAbsoluteFile().getPath(), "/TESTROOT");
result = result.replace(sTempDir.getPath(), "/TESTROOT");
}
return result;
}
public String getErrors() throws Exception {
return mWriter.toString();
}
@Override
public void report(Context context, Issue issue, Severity severity, Location location,
String message, Object data) {
if (issue == IssueRegistry.LINT_ERROR) {
return;
}
if (severity == Severity.FATAL) {
// Treat fatal errors like errors in the golden files.
severity = Severity.ERROR;
}
// For messages into all secondary locations to ensure they get
// specifically included in the text report
if (location != null && location.getSecondary() != null) {
Location l = location.getSecondary();
while (l != null) {
if (l.getMessage() == null) {
l.setMessage("<No location-specific message");
}
l = l.getSecondary();
}
}
super.report(context, issue, severity, location, message, data);
}
@Override
public void log(Throwable exception, String format, Object... args) {
if (exception != null) {
exception.printStackTrace();
}
StringBuilder sb = new StringBuilder();
if (format != null) {
sb.append(String.format(format, args));
}
if (exception != null) {
sb.append(exception.toString());
}
System.err.println(sb);
if (exception != null) {
fail(exception.toString());
}
}
@Override
public IDomParser getDomParser() {
return new LintCliXmlParser();
}
@Override
public IJavaParser getJavaParser() {
return new LombokParser();
}
@Override
public Configuration getConfiguration(Project project) {
return AbstractCheckTest.this.getConfiguration(project);
}
@Override
public File findResource(String relativePath) {
if (relativePath.equals("platform-tools/api/api-versions.xml")) {
CodeSource source = getClass().getProtectionDomain().getCodeSource();
if (source != null) {
URL location = source.getLocation();
try {
File dir = new File(location.toURI());
assertTrue(dir.getPath(), dir.exists());
File sdkDir = dir.getParentFile().getParentFile().getParentFile()
.getParentFile().getParentFile().getParentFile();
File file = new File(sdkDir, "development" + File.separator + "sdk"
+ File.separator + "api-versions.xml");
return file;
} catch (URISyntaxException e) {
fail(e.getLocalizedMessage());
}
}
} else if (relativePath.startsWith("tools/support/")) {
String base = relativePath.substring("tools/support/".length());
CodeSource source = getClass().getProtectionDomain().getCodeSource();
if (source != null) {
URL location = source.getLocation();
try {
File dir = new File(location.toURI());
assertTrue(dir.getPath(), dir.exists());
File sdkDir = dir.getParentFile().getParentFile().getParentFile()
.getParentFile().getParentFile().getParentFile();
File file = new File(sdkDir, "sdk" + File.separator + "files"
+ File.separator + "typos"
+ File.separator + base);
return file;
} catch (URISyntaxException e) {
fail(e.getLocalizedMessage());
}
}
} else {
fail("Unit tests don't support arbitrary resource lookup yet.");
}
return super.findResource(relativePath);
}
}
public class TestConfiguration extends Configuration {
@Override
public boolean isEnabled(Issue issue) {
return AbstractCheckTest.this.isEnabled(issue);
}
@Override
public void ignore(Context context, Issue issue, Location location, String message,
Object data) {
fail("Not supported in tests.");
}
@Override
public void setSeverity(Issue issue, Severity severity) {
fail("Not supported in tests.");
}
}
}