/*
* 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.infrastructure;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.DuplicateDataException;
import com.android.ide.common.res2.MergingException;
import com.android.ide.common.res2.ResourceFile;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.res2.ResourceMerger;
import com.android.ide.common.res2.ResourceRepository;
import com.android.ide.common.res2.ResourceSet;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.IAndroidTarget;
import com.android.testutils.SdkTestCase;
import com.android.tools.lint.ExternalAnnotationRepository;
import com.android.tools.lint.LintCliClient;
import com.android.tools.lint.LintCliFlags;
import com.android.tools.lint.Reporter;
import com.android.tools.lint.TextReporter;
import com.android.tools.lint.Warning;
import com.android.tools.lint.checks.BuiltinIssueRegistry;
import com.android.tools.lint.client.api.Configuration;
import com.android.tools.lint.client.api.DefaultConfiguration;
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.client.api.LintRequest;
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.LintUtils;
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 com.android.tools.lint.detector.api.TextFormat;
import com.android.utils.ILogger;
import com.android.utils.SdkUtils;
import com.android.utils.StdLogger;
import com.android.utils.XmlUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Charsets;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import org.intellij.lang.annotations.Language;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Test case for lint detectors.
* <p>
* <b>NOTE: This is not a public or final API; if you rely on this be prepared
* to adjust your code for the next tools release.</b>
*/
@Beta
@SuppressWarnings("javadoc")
public abstract class LintDetectorTest extends SdkTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
BuiltinIssueRegistry.reset();
}
protected abstract Detector getDetector();
private Detector mDetector;
protected final Detector getDetectorInstance() {
if (mDetector == null) {
mDetector = getDetector();
}
return mDetector;
}
protected abstract List<Issue> getIssues();
public class CustomIssueRegistry extends IssueRegistry {
@NonNull
@Override
public List<Issue> getIssues() {
return LintDetectorTest.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);
}
Collections.sort(files, new Comparator<File>() {
@Override
public int compare(File file1, File file2) {
ResourceFolderType folder1 = ResourceFolderType.getFolderType(
file1.getParentFile().getName());
ResourceFolderType folder2 = ResourceFolderType.getFolderType(
file2.getParentFile().getName());
if (folder1 != null && folder2 != null && folder1 != folder2) {
return folder1.compareTo(folder2);
}
return file1.compareTo(file2);
}
});
addManifestFile(targetDir);
return checkLint(files);
}
protected String checkLint(List<File> files) throws Exception {
TestLintClient lintClient = createClient();
return checkLint(lintClient, files);
}
protected String checkLint(TestLintClient lintClient, List<File> files) throws Exception {
if (System.getenv("ANDROID_BUILD_TOP") != null) {
fail("Don't run the lint tests with $ANDROID_BUILD_TOP set; that enables lint's "
+ "special support for detecting AOSP projects (looking for .class "
+ "files in $ANDROID_HOST_OUT etc), and this confuses lint.");
}
mOutput = new StringBuilder();
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 void checkReportedError(
@NonNull Context context,
@NonNull Issue issue,
@NonNull Severity severity,
@Nullable Location location,
@NonNull String message) {
}
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));
}
protected String lintProjectIncrementally(String currentFile, String... relativePaths)
throws Exception {
File projectDir = getProjectDir(null, relativePaths);
File current = new File(projectDir, currentFile.replace('/', File.separatorChar));
assertTrue(current.exists());
TestLintClient client = createClient();
client.setIncremental(current);
return checkLint(client, Collections.singletonList(projectDir));
}
protected String lintProjectIncrementally(String currentFile, TestFile... files)
throws Exception {
File projectDir = getProjectDir(null, files);
File current = new File(projectDir, currentFile.replace('/', File.separatorChar));
assertTrue(current.exists());
TestLintClient client = createClient();
client.setIncremental(current);
return checkLint(client, Collections.singletonList(projectDir));
}
/**
* 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(TestFile... files) throws Exception {
File projectDir = getProjectDir(null, files);
return checkLint(Collections.singletonList(projectDir));
}
@Override
protected File getTargetDir() {
File targetDir = new File(getTempDir(), getClass().getSimpleName() + "_" + getName());
addCleanupDir(targetDir);
return targetDir;
}
@NonNull
public TestFile file() {
return new TestFile();
}
@NonNull
public TestFile source(@NonNull String to, @NonNull String source) {
return file().to(to).withSource(source);
}
@NonNull
public TestFile java(@NonNull String to, @NonNull @Language("JAVA") String source) {
return file().to(to).withSource(source);
}
@NonNull
public TestFile xml(@NonNull String to, @NonNull @Language("XML") String source) {
return file().to(to).withSource(source);
}
@NonNull
public TestFile copy(@NonNull String from, @NonNull String to) {
return file().from(from).to(to);
}
@NonNull
public TestFile copy(@NonNull String from) {
return file().from(from).to(from);
}
/** 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()));
List<TestFile> testFiles = Lists.newArrayList();
for (String relativePath : relativePaths) {
testFiles.add(file().copy(relativePath));
}
return getProjectDir(name, testFiles.toArray(new TestFile[testFiles.size()]));
}
/** Creates a project directory structure from the given files */
protected File getProjectDir(String name, TestFile... testFiles) 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());
}
for (TestFile fp : testFiles) {
File file = fp.createFile(projectDir);
assertNotNull(file);
}
addManifestFile(projectDir);
return projectDir;
}
private static 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 = LintDetectorTest.class.getResourceAsStream(path);
if (!expectExists && stream == null) {
return null;
}
return stream;
}
protected boolean isEnabled(Issue issue) {
Class<? extends Detector> detectorClass = getDetectorInstance().getClass();
if (issue.getImplementation().getDetectorClass() == detectorClass) {
return true;
}
if (issue == IssueRegistry.LINT_ERROR || issue == IssueRegistry.PARSER_ERROR) {
return !ignoreSystemErrors();
}
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;
}
protected boolean ignoreSystemErrors() {
return true;
}
public class TestLintClient extends LintCliClient {
private StringWriter mWriter = new StringWriter();
private File mIncrementalCheck;
public TestLintClient() {
super(new LintCliFlags());
mFlags.getReporters().add(new TextReporter(this, mFlags, mWriter, false));
}
@Override
public String getSuperClass(@NonNull Project project, @NonNull String name) {
String superClass = LintDetectorTest.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);
LintRequest request = new LintRequest(this, files);
if (mIncrementalCheck != null) {
assertEquals(1, files.size());
File projectDir = files.get(0);
assertTrue(isProjectDirectory(projectDir));
Project project = createProject(projectDir, projectDir);
project.addFile(mIncrementalCheck);
List<Project> projects = Collections.singletonList(project);
request.setProjects(projects);
}
mDriver.analyze(request.setScope(getLintScope(files)));
// Check compare contract
Warning prev = null;
for (Warning warning : mWarnings) {
if (prev != null) {
boolean equals = warning.equals(prev);
assertEquals(equals, prev.equals(warning));
int compare = warning.compareTo(prev);
assertEquals(equals, compare == 0);
assertEquals(-compare, prev.compareTo(warning));
}
prev = warning;
}
Collections.sort(mWarnings);
// Check compare contract & transitivity
Warning prev2 = prev;
prev = null;
for (Warning warning : mWarnings) {
if (prev != null && prev2 != null) {
assertTrue(warning.compareTo(prev) >= 0);
assertTrue(prev.compareTo(prev2) >= 0);
assertTrue(warning.compareTo(prev2) >= 0);
assertTrue(prev.compareTo(warning) <= 0);
assertTrue(prev2.compareTo(prev) <= 0);
assertTrue(prev2.compareTo(warning) <= 0);
}
prev2 = prev;
prev = warning;
}
for (Reporter reporter : mFlags.getReporters()) {
reporter.write(mErrorCount, mWarningCount, mWarnings);
}
mOutput.append(mWriter.toString());
if (mOutput.length() == 0) {
mOutput.append("No warnings.");
}
String result = mOutput.toString();
if (result.equals("No issues found.\n")) {
result = "No warnings.";
}
result = cleanup(result);
return result;
}
public String getErrors() throws Exception {
return mWriter.toString();
}
@Override
public void report(
@NonNull Context context,
@NonNull Issue issue,
@NonNull Severity severity,
@Nullable Location location,
@NonNull String message,
@NonNull TextFormat format) {
if (ignoreSystemErrors() && (issue == IssueRegistry.LINT_ERROR)) {
return;
}
// Use plain ascii in the test golden files for now. (This also ensures
// that the markup is wellformed, e.g. if we have a ` without a matching
// closing `, the ` would show up in the plain text.)
message = format.convertTo(message, TextFormat.TEXT);
checkReportedError(context, issue, severity, location, message);
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();
if (l == location) {
fail("Location link cycle");
}
while (l != null) {
if (l.getMessage() == null) {
l.setMessage("<No location-specific message");
}
if (l == l.getSecondary()) {
fail("Location link cycle");
}
l = l.getSecondary();
}
}
super.report(context, issue, severity, location, message, format);
// Make sure errors are unique!
Warning prev = null;
for (Warning warning : mWarnings) {
assertNotSame(warning, prev);
assert prev == null || !warning.equals(prev) : warning;
prev = warning;
}
}
@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());
}
}
@NonNull
@Override
public Configuration getConfiguration(@NonNull Project project,
@Nullable LintDriver driver) {
return LintDetectorTest.this.getConfiguration(this, project);
}
@Override
public File findResource(@NonNull String relativePath) {
if (relativePath.equals("platform-tools/api/api-versions.xml")) {
// Look in the current Git repository and try to find it there
File rootDir = getRootDir();
if (rootDir != null) {
File file = new File(rootDir, "development" + File.separator + "sdk"
+ File.separator + "api-versions.xml");
if (file.exists()) {
return file;
}
}
// Look in an SDK install, if found
File home = getSdkHome();
if (home != null) {
return new File(home, relativePath);
}
} else if (relativePath.equals(ExternalAnnotationRepository.SDK_ANNOTATIONS_PATH)) {
// Look in the current Git repository and try to find it there
File rootDir = getRootDir();
if (rootDir != null) {
File file = new File(rootDir,
"tools" + File.separator
+ "adt" + File.separator
+ "idea" + File.separator
+ "android" + File.separator
+ "annotations");
if (file.exists()) {
return file;
}
}
// Look in an SDK install, if found
File home = getSdkHome();
if (home != null) {
File file = new File(home, relativePath);
return file.exists() ? file : null;
}
} else if (relativePath.startsWith("tools/support/")) {
// Look in the current Git repository and try to find it there
String base = relativePath.substring("tools/support/".length());
File rootDir = getRootDir();
if (rootDir != null) {
File file = new File(rootDir, "tools"
+ File.separator + "base"
+ File.separator + "files"
+ File.separator + "typos"
+ File.separator + base);
if (file.exists()) {
return file;
}
}
// Look in an SDK install, if found
File home = getSdkHome();
if (home != null) {
return new File(home, relativePath);
}
} else {
fail("Unit tests don't support arbitrary resource lookup yet.");
}
return super.findResource(relativePath);
}
@NonNull
@Override
public List<File> findGlobalRuleJars() {
// Don't pick up random custom rules in ~/.android/lint when running unit tests
return Collections.emptyList();
}
public void setIncremental(File currentFile) {
mIncrementalCheck = currentFile;
}
@Override
public boolean supportsProjectResources() {
return mIncrementalCheck != null;
}
@Nullable
@Override
public AbstractResourceRepository getProjectResources(Project project,
boolean includeDependencies) {
if (mIncrementalCheck == null) {
return null;
}
ResourceRepository repository = new ResourceRepository(false);
ILogger logger = new StdLogger(StdLogger.Level.INFO);
ResourceMerger merger = new ResourceMerger();
ResourceSet resourceSet = new ResourceSet(getName()) {
@Override
protected void checkItems() throws DuplicateDataException {
// No checking in ProjectResources; duplicates can happen, but
// the project resources shouldn't abort initialization
}
};
// Only support 1 resource folder in test setup right now
int size = project.getResourceFolders().size();
assertTrue("Found " + size + " test resources folders", size <= 1);
if (size == 1) {
resourceSet.addSource(project.getResourceFolders().get(0));
}
try {
resourceSet.loadFromFiles(logger);
merger.addDataSet(resourceSet);
merger.mergeData(repository.createMergeConsumer(), true);
// Make tests stable: sort the item lists!
Map<ResourceType, ListMultimap<String, ResourceItem>> map = repository.getItems();
for (Map.Entry<ResourceType, ListMultimap<String, ResourceItem>> entry : map.entrySet()) {
Map<String, List<ResourceItem>> m = Maps.newHashMap();
ListMultimap<String, ResourceItem> value = entry.getValue();
List<List<ResourceItem>> lists = Lists.newArrayList();
for (Map.Entry<String, ResourceItem> e : value.entries()) {
String key = e.getKey();
ResourceItem item = e.getValue();
List<ResourceItem> list = m.get(key);
if (list == null) {
list = Lists.newArrayList();
lists.add(list);
m.put(key, list);
}
list.add(item);
}
for (List<ResourceItem> list : lists) {
Collections.sort(list, new Comparator<ResourceItem>() {
@Override
public int compare(ResourceItem o1, ResourceItem o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
}
// Store back in list multi map in new sorted order
value.clear();
for (Map.Entry<String, List<ResourceItem>> e : m.entrySet()) {
String key = e.getKey();
List<ResourceItem> list = e.getValue();
for (ResourceItem item : list) {
value.put(key, item);
}
}
}
// Workaround: The repository does not insert ids from layouts! We need
// to do that here.
Map<ResourceType,ListMultimap<String,ResourceItem>> items = repository.getItems();
ListMultimap<String, ResourceItem> layouts = items
.get(ResourceType.LAYOUT);
if (layouts != null) {
for (ResourceItem item : layouts.values()) {
ResourceFile source = item.getSource();
if (source == null) {
continue;
}
File file = source.getFile();
try {
String xml = Files.toString(file, Charsets.UTF_8);
Document document = XmlUtils.parseDocumentSilently(xml, true);
assertNotNull(document);
Set<String> ids = Sets.newHashSet();
addIds(ids, document); // TODO: pull parser
if (!ids.isEmpty()) {
ListMultimap<String, ResourceItem> idMap =
items.get(ResourceType.ID);
if (idMap == null) {
idMap = ArrayListMultimap.create();
items.put(ResourceType.ID, idMap);
}
for (String id : ids) {
ResourceItem idItem = new ResourceItem(id, ResourceType.ID,
null);
String qualifiers = file.getParentFile().getName();
if (qualifiers.startsWith("layout-")) {
qualifiers = qualifiers.substring("layout-".length());
} else if (qualifiers.equals("layout")) {
qualifiers = "";
}
idItem.setSource(new ResourceFile(file, item, qualifiers));
idMap.put(id, idItem);
}
}
} catch (IOException e) {
fail(e.toString());
}
}
}
}
catch (MergingException e) {
fail(e.getMessage());
}
return repository;
}
private void addIds(Set<String> ids, Node node) {
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
String id = element.getAttributeNS(ANDROID_URI, ATTR_ID);
if (id != null && !id.isEmpty()) {
ids.add(LintUtils.stripIdPrefix(id));
}
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
String value = attribute.getValue();
if (value.startsWith(NEW_ID_PREFIX)) {
ids.add(value.substring(NEW_ID_PREFIX.length()));
}
}
}
NodeList children = node.getChildNodes();
for (int i = 0, n = children.getLength(); i < n; i++) {
Node child = children.item(i);
addIds(ids, child);
}
}
@Nullable
@Override
public IAndroidTarget getCompileTarget(@NonNull Project project) {
IAndroidTarget compileTarget = super.getCompileTarget(project);
if (compileTarget == null) {
IAndroidTarget[] targets = getTargets();
for (int i = targets.length - 1; i >= 0; i--) {
IAndroidTarget target = targets[i];
if (target.isPlatform()) {
return target;
}
}
}
return compileTarget;
}
@NonNull
@Override
public List<File> getTestSourceFolders(@NonNull Project project) {
List<File> testSourceFolders = super.getTestSourceFolders(project);
//List<File> tests = new ArrayList<File>();
File tests = new File(project.getDir(), "test");
if (tests.exists()) {
List<File> all = Lists.newArrayList(testSourceFolders);
all.add(tests);
testSourceFolders = all;
}
return testSourceFolders;
}
}
/**
* Returns the Android source tree root dir.
* @return the root dir or null if it couldn't be computed.
*/
protected File getRootDir() {
CodeSource source = getClass().getProtectionDomain().getCodeSource();
if (source != null) {
URL location = source.getLocation();
try {
File dir = SdkUtils.urlToFile(location);
assertTrue(dir.getPath(), dir.exists());
while (dir != null) {
File settingsGradle = new File(dir, "settings.gradle"); //$NON-NLS-1$
if (settingsGradle.exists()) {
return dir.getParentFile().getParentFile();
}
File lint = new File(dir, "lint"); //$NON-NLS-1$
if (lint.exists() && new File(lint, "cli").exists()) { //$NON-NLS-1$
return dir.getParentFile().getParentFile();
}
dir = dir.getParentFile();
}
return null;
} catch (MalformedURLException 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) {
if (issue.getDefaultSeverity() != Severity.IGNORE) {
return issue.getDefaultSeverity();
}
return Severity.WARNING;
}
return severity;
}
@Override
public boolean isEnabled(@NonNull Issue issue) {
return LintDetectorTest.this.isEnabled(issue);
}
@Override
public void ignore(@NonNull Context context, @NonNull Issue issue,
@Nullable Location location, @NonNull String message) {
fail("Not supported in tests.");
}
@Override
public void setSeverity(@NonNull Issue issue, @Nullable Severity severity) {
fail("Not supported in tests.");
}
}
}