package spoon.test.architecture; import org.junit.Test; import spoon.Launcher; import spoon.SpoonAPI; import spoon.processing.AbstractManualProcessor; import spoon.reflect.code.CtConstructorCall; import spoon.reflect.declaration.CtClass; import spoon.reflect.declaration.CtConstructor; import spoon.reflect.declaration.CtElement; import spoon.reflect.declaration.CtField; import spoon.reflect.declaration.CtInterface; import spoon.reflect.declaration.CtMethod; import spoon.reflect.declaration.CtPackage; import spoon.reflect.declaration.CtType; import spoon.reflect.declaration.ModifierKind; import spoon.reflect.factory.Factory; import spoon.reflect.factory.FactoryImpl; import spoon.reflect.visitor.filter.AbstractFilter; import spoon.reflect.visitor.filter.TypeFilter; import java.util.List; import java.util.TreeSet; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class SpoonArchitectureEnforcerTest { @Test public void statelessFactory() throws Exception { // the factories must be stateless SpoonAPI spoon = new Launcher(); spoon.addInputResource("src/main/java/spoon/reflect/factory"); spoon.buildModel(); for (CtType t : spoon.getFactory().Package().getRootPackage().getElements(new AbstractFilter<CtType>() { @Override public boolean matches(CtType element) { return super.matches(element) && element.getSimpleName().contains("Factory"); }; })) { for (Object o : t.getFields()) { CtField f=(CtField)o; if (f.getSimpleName().equals("factory")) { continue; } if (f.hasModifier(ModifierKind.FINAL) || f.hasModifier(ModifierKind.TRANSIENT) ) { continue; } fail("architectural constraint: a factory must be stateless"); } } } @Test public void testFactorySubFactory() throws Exception { // contract:: all subfactory methods must also be in the main factory // this is very important for usability and discoverability final Launcher launcher = new Launcher(); launcher.addInputResource("./src/main/java/spoon/reflect/factory"); class SanityCheck { int val = 0; }; SanityCheck sanityCheck = new SanityCheck(); launcher.addProcessor(new AbstractManualProcessor() { @Override public void process() { CtType factoryImpl = getFactory().Interface().get(Factory.class); CtPackage factoryPackage = getFactory().Package().getOrCreate("spoon.reflect.factory"); CtInterface itf = getFactory().Interface().create("MegaFactoryItf"); CtClass impl = getFactory().Class().create("MegaFactory"); for (CtType<?> t : factoryPackage.getTypes()) { if (t.getSimpleName().startsWith("Mega")) continue; // for (CtMethod<?> m : t.getMethods()) { // we check only public methods if (m.hasModifier(ModifierKind.PUBLIC) == false) continue; // we only consider factory methods if (!m.getSimpleName().startsWith("create")) continue; // too generic, what should we create?? if (m.getSimpleName().equals("create")) continue; // too generic, is it a fieldref? an execref? etc if (m.getSimpleName().equals("createReference")) continue; if (m.getModifiers().contains(ModifierKind.ABSTRACT)) continue; sanityCheck.val++; // the core assertion assertTrue(factoryImpl.hasMethod(m)); } } } }); launcher.run(); assertTrue(sanityCheck.val > 100); } @Test public void noTreeSetInSpoon() throws Exception { // we don't use TreeSet, because they implicitly depend on Comparable (no static check, only dynamic checks) SpoonAPI spoon = new Launcher(); spoon.addInputResource("src/main/java/"); spoon.buildModel(); List<CtConstructorCall> treeSetWithoutComparators = spoon.getFactory().Package().getRootPackage().filterChildren(new AbstractFilter<CtConstructorCall>() { @Override public boolean matches(CtConstructorCall element) { return element.getType().getActualClass().equals(TreeSet.class) && element.getArguments().size() == 0; } }).list(); assertEquals(0, treeSetWithoutComparators.size()); } @Test public void metamodelPackageRule() throws Exception { // all implementations of the metamodel classes have a corresponding interface in the appropriate package SpoonAPI implementations = new Launcher(); implementations.addInputResource("src/main/java/spoon/support/reflect/declaration"); implementations.addInputResource("src/main/java/spoon/support/reflect/code"); implementations.addInputResource("src/main/java/spoon/support/reflect/reference"); implementations.buildModel(); SpoonAPI interfaces = new Launcher(); interfaces.addInputResource("src/main/java/spoon/reflect/declaration"); interfaces.addInputResource("src/main/java/spoon/reflect/code"); interfaces.addInputResource("src/main/java/spoon/reflect/reference"); interfaces.addInputResource("src/main/java/spoon/support/DefaultCoreFactory.java"); interfaces.buildModel(); for (CtType<?> implType : implementations.getModel().getAllTypes()) { String impl = implType.getQualifiedName().replace(".support", "").replace("Impl", ""); CtType interfaceType = interfaces.getFactory().Type().get(impl); // the implementation is a subtype of the superinterface assertTrue(implType.getReference().isSubtypeOf(interfaceType.getReference())); } } @Test public void testGoodTestClassNames() throws Exception { // contract: to be run by Maven surefire, all test classes must be called Test* or *Test // reference: "By default, the Surefire Plugin will automatically include all test classes with the following wildcard patterns:" // "**/Test*.java" and "**/*Test.java" // http://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html SpoonAPI spoon = new Launcher(); spoon.addInputResource("src/test/java/"); spoon.buildModel(); for (CtMethod<?> meth : spoon.getModel().getRootPackage().getElements(new TypeFilter<CtMethod>(CtMethod.class) { @Override public boolean matches(CtMethod element) { return super.matches(element) && element.getAnnotation(Test.class) != null; } })) { assertTrue("naming contract violated for "+meth.getParent(CtClass.class).getSimpleName(), meth.getParent(CtClass.class).getSimpleName().startsWith("Test") || meth.getParent(CtClass.class).getSimpleName().endsWith("Test")); } } @Test public void testStaticClasses() throws Exception { // contract: helper classes only have static methods and a private constructor // spoon.compiler.SpoonResourceHelper // spoon.reflect.visitor.Query // spoon.support.compiler.jdt.JDTTreeBuilderQuery // spoon.support.compiler.SnippetCompilationHelper // spoon.support.util.ByteSerialization // spoon.support.util.RtHelper // spoon.support.visitor.equals.CloneHelper // spoon.template.Substitution // spoon.testing.utils.Check // spoon.testing.utils.ProcessorUtils // spoon.testing.Assert SpoonAPI spoon = new Launcher(); spoon.addInputResource("src/main/java/"); spoon.buildModel(); for (CtClass<?> klass : spoon.getModel().getRootPackage().getElements(new TypeFilter<CtClass>(CtClass.class) { @Override public boolean matches(CtClass element) { return element.getSuperclass() == null && super.matches(element) && element.getMethods().size()>0 && element.getElements(new TypeFilter<>(CtMethod.class)).stream().allMatch( x -> x.hasModifier(ModifierKind.STATIC)); } })) { assertTrue(klass.getElements(new TypeFilter<>(CtConstructor.class)).stream().allMatch(x -> x.hasModifier(ModifierKind.PRIVATE))); } } }