/*
* 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.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.testutils.SdkTestCase;
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.DefaultConfiguration;
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.LintClient;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
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.Collections;
import java.util.EnumSet;
import java.util.List;
/** Common utility methods for the various lint check tests */
@SuppressWarnings("javadoc")
public abstract class AbstractCheckTest extends SdkTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
BuiltinIssueRegistry.reset();
}
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 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(LintClient client, Project project) {
return new TestConfiguration(client, project, null);
}
protected void configureDriver(LintDriver driver) {
}
/**
* 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));
}
@Override
protected File getTargetDir() {
File targetDir = new File(getTempDir(), getClass().getSimpleName() + "_" + getName());
addCleanupDir(targetDir);
return targetDir;
}
/** 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);
}
if (!projectDir.exists()) {
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;
@Override
protected InputStream getTestResource(String relativePath, boolean expectExists) {
String path = "data" + File.separator + relativePath; //$NON-NLS-1$
InputStream stream =
AbstractCheckTest.class.getResourceAsStream(path);
if (!expectExists && stream == null) {
return null;
}
return 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 EnumSet<Scope> getLintScope(List<File> file) {
return null;
}
public String getSuperClass(Project project, String name) {
return null;
}
public class TestLintClient extends Main {
private StringWriter mWriter = new StringWriter();
TestLintClient() {
mReporters.add(new TextReporter(this, mWriter, false));
}
@Override
public String getSuperClass(Project project, String name) {
String superClass = AbstractCheckTest.this.getSuperClass(project, name);
if (superClass != null) {
return superClass;
}
return super.getSuperClass(project, name);
}
public String analyze(List<File> files) throws Exception {
mDriver = new LintDriver(new CustomIssueRegistry(), this);
configureDriver(mDriver);
mDriver.analyze(files, getLintScope(files));
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.";
}
result = cleanup(result);
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(this, project);
}
@Override
public File findResource(String relativePath) {
if (relativePath.equals("platform-tools/api/api-versions.xml")) {
File rootDir = getRootDir();
if (rootDir != null) {
File file = new File(rootDir, "development" + File.separator + "sdk"
+ File.separator + "api-versions.xml");
return file;
}
} else if (relativePath.startsWith("tools/support/")) {
String base = relativePath.substring("tools/support/".length());
File rootDir = getRootDir();
if (rootDir != null) {
File file = new File(rootDir, "sdk" + File.separator + "files"
+ File.separator + "typos"
+ File.separator + base);
return file;
}
} else {
fail("Unit tests don't support arbitrary resource lookup yet.");
}
return super.findResource(relativePath);
}
}
/**
* Returns the Android source tree root dir.
* @return the root dir or null if it couldn't be computed.
*/
private File getRootDir() {
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 rootDir = dir.getParentFile().getParentFile().getParentFile()
.getParentFile().getParentFile().getParentFile();
// check if "settings.gradle" is there. This will let us know if we need
// to go up one extra level, which is the case when running the tests
// from gradle.
File settingsGradle = new File(rootDir, "settings.gradle"); //$NON-NLS-1$
if (settingsGradle.isFile()) {
rootDir = rootDir.getParentFile();
}
return rootDir;
} catch (URISyntaxException e) {
fail(e.getLocalizedMessage());
}
}
return null;
}
public class TestConfiguration extends DefaultConfiguration {
protected TestConfiguration(
@NonNull LintClient client,
@NonNull Project project,
@Nullable Configuration parent) {
super(client, project, parent);
}
public TestConfiguration(
@NonNull LintClient client,
@Nullable Project project,
@Nullable Configuration parent,
@NonNull File configFile) {
super(client, project, parent, configFile);
}
@Override
@NonNull
protected Severity getDefaultSeverity(@NonNull Issue issue) {
// In unit tests, include issues that are ignored by default
Severity severity = super.getDefaultSeverity(issue);
if (severity == Severity.IGNORE) {
return Severity.WARNING;
}
return severity;
}
@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.");
}
}
}