/**
* Copyright 2015 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.collect.Sets.newHashSet;
import static java.lang.String.format;
import static java.lang.System.getProperty;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jdepend.framework.JDepend;
import jdepend.framework.JavaPackage;
import jdepend.framework.PackageFilter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.kaching.platform.testing.CyclicDependencyTestRunner.PackagesBuilder.Result;
/**
* @see <a href="http://clarkware.com/software/JDepend.html#junit">JDepend and JUnit</a>
*/
public class CyclicDependencyTestRunner extends AbstractDeclarativeTestRunner<CyclicDependencyTestRunner.Packages> {
@Target(TYPE)
@Retention(RUNTIME)
public @interface Packages {
public int minClasses() default 10;
public String[] forPackages();
public String[] binDirectories() default "target/test-classes";
public String binDirectoryProperty() default "kawala.bin_directories";
}
public CyclicDependencyTestRunner(Class<?> testClass) {
super(testClass, Packages.class);
}
@Override
@SuppressWarnings("unchecked")
protected void runTest(final Packages dependencies) throws IOException {
Result results = getTestResults(dependencies);
/* This assertion is meant as a sanity check. It makes sure that when
* run, JDepend can correctly find the project's classes. Otherwise, the
* test could pass simply because the path is incorrect or the build
* assembles classes in a different directory.
*/
assertTrue(
format(
"project does not contain more than %s classes, only %s found",
dependencies.minClasses(), results.numClasses),
dependencies.minClasses() < results.numClasses);
assertFalse(results.hasCycle());
}
Result getTestResults(final Packages dependencies) throws IOException {
JDepend jDepend = new JDepend();
String binDirectoryProperty = getProperty(dependencies.binDirectoryProperty());
String[] binDirectories = binDirectoryProperty != null ?
binDirectoryProperty.split(":") :
dependencies.binDirectories();
for (String binDirectory : binDirectories) {
if (new File(binDirectory).isDirectory()) {
jDepend.addDirectory(binDirectory);
}
}
jDepend.analyzeInnerClasses(true);
PackageFilter checkedPackagesFilter = new PackageFilter() {
@Override
public boolean accept(String scannedPackageName) {
for (String expectedPackageName : dependencies.forPackages()) {
if (scannedPackageName.startsWith(expectedPackageName)) {
return true;
}
}
return false;
}
};
jDepend.setFilter(checkedPackagesFilter);
jDepend.analyze();
Result result = new Result(jDepend.countClasses());
if (jDepend.containsCycles()) {
for (Object o : jDepend.getPackages()) {
JavaPackage pkg = (JavaPackage) o;
List<JavaPackage> cycles = Lists.newArrayList();
pkg.collectAllCycles(cycles);
result.addCycles(cycles);
}
}
return result;
}
static class PackagesBuilder {
private Set<String> forPackages;
public PackagesBuilder forPackages(String... packageExpressions) {
if (forPackages != null) {
throw new IllegalStateException();
}
forPackages = newHashSet();
for (String packageExpression : packageExpressions) {
forPackages.add(packageExpression);
}
return this;
}
PackagesBuilder check(String packageExpression) {
if (forPackages == null) {
throw new IllegalStateException();
}
return this;
}
static class Result {
final int numClasses;
final Map<JavaPackage, Set<JavaPackage>> sccs = Maps.newHashMap();
Result(int numClasses) {
this.numClasses = numClasses;
}
boolean hasCycle() {
return !sccs.isEmpty();
}
void addCycles(List<JavaPackage> cycles) {
Set<JavaPackage> scc = Sets.newHashSet(cycles);
for (JavaPackage pkg : cycles) {
sccs.put(pkg, scc);
}
}
Set<Set<JavaPackage>> getUniqueCycles() {
return Sets.newHashSet(sccs.values());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Strongly connected components: {\n");
for (Set<JavaPackage> scc : getUniqueCycles()) {
boolean first = true;
sb.append("[");
for (JavaPackage jp : scc) {
if (first) {
first = false;
} else {
sb.append(",\n ");
}
sb.append(jp.getName());
}
sb.append("]\n");
}
return sb.append("}").toString();
}
}
}
}