/**
* Copyright 2010 Wealthfront Inc. 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.kaching.platform.testing;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Sets.newHashSet;
import static com.kaching.platform.testing.BadCodeSnippetsRunner.VerificationMode.BOTH;
import static java.lang.String.format;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import com.google.common.base.Joiner;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.io.Files;
public class BadCodeSnippetsRunner extends AbstractDeclarativeTestRunner<BadCodeSnippetsRunner.CodeSnippets> {
/**
* Top level annotation used to describe the bad code snippet test.
*/
@Target(TYPE)
@Retention(RUNTIME)
public @interface CodeSnippets {
/**
* Lists all the checks that must be performed by this bad code snippet
* test.
*/
public Check[] value();
/**
* Specifies the file extension to check for bad code snippet. The default
* is {@code java}.
*/
public String fileExtension() default "java";
}
@Retention(RUNTIME)
@Target({})
public @interface Check {
public String[] paths();
public Snippet[] snippets();
}
@Retention(RUNTIME)
@Target({})
public @interface Snippet {
public String value();
public String[] exceptions() default {};
public VerificationMode verificationMode() default BOTH;
public String rationale() default "";
}
public enum VerificationMode {
ONLY_MATCHES(false, true),
ONLY_MISSING_MATCHES(true, false),
BOTH(true, true);
private final boolean reportMissing;
private final boolean reportMatches;
private VerificationMode(boolean missing, boolean matches) {
this.reportMissing = missing;
this.reportMatches = matches;
}
}
/**
* Internal use only.
*/
public BadCodeSnippetsRunner(Class<?> klass) {
super(klass, CodeSnippets.class);
}
@Override
protected void runTest(CodeSnippets codeSnippets) throws IOException {
for (Check check : codeSnippets.value()) {
checkBadCodeSnippet(check, codeSnippets.fileExtension());
}
}
private void checkBadCodeSnippet(
Check check, String fileExtension) throws IOException {
LoadingCache<Snippet, Set<File>> snippetsToUses = CacheBuilder.newBuilder().build(
new CacheLoader<Snippet, Set<File>>() {
@Override
public Set<File> load(Snippet key) {
return newHashSet();
}
});
LoadingCache<Snippet, Pattern> compiledPatterns = CacheBuilder.newBuilder().build(
new CacheLoader<Snippet, Pattern>() {
@Override
public Pattern load(Snippet key) {
return Pattern.compile(key.value());
}
});
Map<Snippet, Set<File>> snippetsToExceptions = snippetsToExceptions(check.snippets());
Set<Snippet> snippets = snippetsToExceptions.keySet();
for (String path : check.paths()) {
collectUses(fileExtension, new File(path), snippets, snippetsToUses, compiledPatterns);
}
CombinedAssertionFailedError error =
new CombinedAssertionFailedError("bad code uses");
for (Snippet snippet : snippets) {
Set<File> exceptions = snippetsToExceptions.get(snippet);
Set<File> uses = snippetsToUses.getUnchecked(snippet);
List<File> spuriousExceptions = newArrayList(exceptions);
spuriousExceptions.removeAll(uses);
if (snippet.verificationMode().reportMissing && !spuriousExceptions.isEmpty()) {
error.addError(format(
"%s: marked as exception to snippet but didn't occur:\n %s",
snippet.value(), Joiner.on("\n ").join(spuriousExceptions)));
continue;
}
uses.removeAll(exceptions);
if (snippet.verificationMode().reportMatches && !uses.isEmpty()) {
String rationale = snippet.rationale().isEmpty() ? ""
: format("\nrationale: %s", snippet.rationale());
error.addError(format(
"%s: found %s bad snippets in:\n %s%s",
snippet.value(), uses.size(), Joiner.on("\n ").join(uses), rationale));
}
}
error.throwIfHasErrors();
}
private Map<Snippet, Set<File>> snippetsToExceptions(Snippet[] snippets) {
Map<Snippet, Set<File>> patternsToExceptions = newHashMap();
for (Snippet snippet : snippets) {
try {
Set<File> files = newHashSet();
for (int i = 0; i < snippet.exceptions().length; i++) {
files.add(new File(snippet.exceptions()[i]));
}
patternsToExceptions.put(snippet, files);
} catch (PatternSyntaxException e) {
throw new AssertionError(e.getMessage());
}
}
return patternsToExceptions;
}
private void collectUses(
String fileExtension, File f, Iterable<Snippet> patterns,
LoadingCache<Snippet, Set<File>> uses, LoadingCache<Snippet, Pattern> compiledPatterns)
throws IOException {
if (f.isFile()) {
if (f.getName().endsWith("." + fileExtension)) {
String code = Files.toString(f, UTF_8);
for (Snippet p : patterns) {
if (compiledPatterns.getUnchecked(p).matcher(code).find()) {
uses.getUnchecked(p).add(f);
}
}
}
} else if (f.isDirectory()) {
for (File c : f.listFiles()) {
collectUses(fileExtension, c, patterns, uses, compiledPatterns);
}
}
}
}