/** * 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.collect.Lists.newArrayList; import static com.google.common.collect.Maps.newHashMap; 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.assertTrue; import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import jdepend.framework.JDepend; import jdepend.framework.JavaClass; import jdepend.framework.JavaPackage; import junit.framework.AssertionFailedError; import com.google.common.collect.Sets; public class DependencyTestRunner extends AbstractDeclarativeTestRunner<DependencyTestRunner.Dependencies> { @Target(TYPE) @Retention(RUNTIME) public @interface Dependencies { public int minClasses(); public String[] forPackages(); public CheckPackage[] ensure(); public String[] binDirectories() default "bin"; public String binDirectoryProperty() default "kawala.bin_directories"; } @Retention(RUNTIME) @Target({}) public @interface CheckPackage { public String name(); public String[] mayDependOn(); } public DependencyTestRunner(Class<?> testClass) { super(testClass, Dependencies.class); } @Override @SuppressWarnings("unchecked") protected void runTest(Dependencies 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); jDepend.analyze(); /* 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(), jDepend.countClasses()), dependencies.minClasses() < jDepend.countClasses()); DependenciesBuilder builder = new DependenciesBuilder() .forPackages(dependencies.forPackages()); for (CheckPackage checkPackage : dependencies.ensure()) { builder.check(checkPackage.name()) .mayDependOn(checkPackage.mayDependOn()); } builder.assertIsVerified(jDepend.getPackages()); } static class DependenciesBuilder { private Set<String> forPackages; private final Map<String, Set<String>> mayDepend = newHashMap(); private Set<String> currentDependencies; public DependenciesBuilder forPackages(String... packageExpressions) { if (forPackages != null) { throw new IllegalStateException(); } forPackages = newHashSet(); for (String packageExpression : packageExpressions) { forPackages.add(packageExpression); } return this; } DependenciesBuilder check(String packageExpression) { if (forPackages == null) { throw new IllegalStateException(); } currentDependencies = newHashSet(); mayDepend.put(packageExpression, currentDependencies); return this; } DependenciesBuilder mayDependOn(String... packageExpressions) { if (currentDependencies == null || forPackages == null) { throw new IllegalStateException(); } for (String packageExpression : packageExpressions) { currentDependencies.add(packageExpression); } currentDependencies = null; return this; } @SuppressWarnings("unchecked") void assertIsVerified(Collection<JavaPackage> packages) { if (forPackages == null) { throw new IllegalStateException(); } Set<String> flattenedForPackages = flattenForPackages(packages); Map<String, Set<String>> flattened = flattenMayDepend(packages); List<Violation> violations = newArrayList(); for (JavaPackage package1 : packages) { if (!flattenedForPackages.contains(package1.getName())) { continue; } for (JavaPackage package2 : (Collection<JavaPackage>) package1.getEfferents()) { if (package1 != package2) { assertIsVerified( package1, package2, flattened, violations); } } } if (!violations.isEmpty()) { StringBuilder errors = new StringBuilder(); errors.append(format("%s violation(s):", violations.size())); for (Violation violation : violations) { errors.append("\n"); errors.append(violation.toString()); } throw new AssertionFailedError(errors.toString()); } } private Set<String> flattenForPackages(Collection<JavaPackage> packages) { Set<String> flattened = newHashSet(); for (JavaPackage javaPackage : packages) { for (String expression : forPackages) { if (packageNameMatchesExpression(javaPackage.getName(), expression)) { flattened.add(javaPackage.getName()); } } } return flattened; } private Map<String, Set<String>> flattenMayDepend(Collection<JavaPackage> packages) { Map<String, Set<String>> flattened = newHashMap(); for (JavaPackage javaPackage : packages) { for (Map.Entry<String, Set<String>> entry : mayDepend.entrySet()) { String name = javaPackage.getName(); if (packageNameMatchesExpression(name, entry.getKey())) { if (!flattened.containsKey(name)) { flattened.put(name, Sets.<String>newHashSet()); } flattened.get(name).addAll(entry.getValue()); } } } return flattened; } private void assertIsVerified(JavaPackage package1, JavaPackage package2, Map<String, Set<String>> flattened, List<Violation> violations) { boolean matched = false; Set<String> expressions = flattened.get(package1.getName()); if (expressions != null) { for (String expression : expressions) { if (packageNameMatchesExpression(package2.getName(), expression)) { matched = true; } } } if (!matched) { violations.add(new Violation(package1, package2)); } } /* visible for testing */ static boolean packageNameMatchesExpression(String name, String expression) { return "*".equals(expression) || name.equals(expression) || (expression.endsWith(".*") && name.startsWith(expression.substring(0, expression.length() - 2))); } } static class Violation { final JavaPackage javaPackage; final JavaPackage efferent; Violation(JavaPackage javaPackage, JavaPackage efferent) { this.javaPackage = javaPackage; this.efferent = efferent; } @Override @SuppressWarnings("unchecked") public String toString() { String baseMessage = format("package %s cannot depend on package %s", javaPackage.getName(), efferent.getName()); Collection<JavaClass> classes = javaPackage.getClasses(); if (!classes.isEmpty()) { int javaPackageNameLength = javaPackage.getName().length() + 1; StringBuilder stringBuilder = new StringBuilder(baseMessage); stringBuilder.append(" (classes "); boolean first = true; for (JavaClass javaClass : classes) { if (javaClass.getImportedPackages().contains(efferent)) { if (first) { first = false; } else { stringBuilder.append(", "); } stringBuilder.append(javaClass.getName().substring(javaPackageNameLength)); } } stringBuilder.append(")"); return stringBuilder.toString(); } else { return baseMessage; } } } }