/* * Copyright 2011 SpringSource * * 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 org.grails.compiler.injection.test; import grails.compiler.ast.GrailsArtefactClassInjector; import grails.test.mixin.TestFor; import grails.test.mixin.domain.DomainClassUnitTestMixin; import grails.test.mixin.support.MixinMethod; import grails.test.mixin.support.TestMixinRegistrar; import grails.test.mixin.support.TestMixinRegistry; import grails.util.BuildSettings; import grails.util.GrailsNameUtils; import org.codehaus.groovy.ast.*; import org.codehaus.groovy.ast.expr.*; import org.codehaus.groovy.ast.stmt.*; import org.codehaus.groovy.control.CompilePhase; import org.codehaus.groovy.control.SourceUnit; import org.codehaus.groovy.syntax.Token; import org.codehaus.groovy.transform.GroovyASTTransformation; import org.grails.compiler.injection.GrailsASTUtils; import org.grails.compiler.logging.LoggingTransformer; import org.grails.core.io.DefaultResourceLocator; import org.grails.core.io.ResourceLocator; import org.grails.core.io.support.GrailsFactoriesLoader; import org.grails.io.support.FileSystemResource; import org.grails.io.support.GrailsResourceUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import java.io.IOException; import java.lang.reflect.Modifier; import java.util.*; /** * Transformation used by the {@link grails.test.mixin.TestFor} annotation to signify the * class under test. * * @author Graeme Rocher * @since 2.0 */ @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) @SuppressWarnings("rawtypes") public class TestForTransformation extends TestMixinTransformation implements TestMixinRegistry { private static final ClassNode MY_TYPE = new ClassNode(TestFor.class); private static final Token ASSIGN = Token.newSymbol("=", -1, -1); protected static final Map<String, Class> artefactTypeToTestMap = new HashMap<String, Class>(); static { List<TestMixinRegistrar> registrars = GrailsFactoriesLoader.loadFactories(TestMixinRegistrar.class); TestMixinRegistry thisRegistry = new TestMixinRegistry() { @Override public void registerMixin(String artefactType, Class mixin) { artefactTypeToTestMap.put(artefactType, mixin); } }; for(TestMixinRegistrar registrar : registrars) { registrar.registerTestMixins(thisRegistry); } } public static final String DOMAIN_TYPE = "Domain"; public static final ClassNode BEFORE_CLASS_NODE = new ClassNode(Before.class); public static final AnnotationNode BEFORE_ANNOTATION = new AnnotationNode(BEFORE_CLASS_NODE); public static final ClassNode AFTER_CLASS_NODE = new ClassNode(After.class); public static final AnnotationNode AFTER_ANNOTATION = new AnnotationNode(AFTER_CLASS_NODE); public static final AnnotationNode TEST_ANNOTATION = new AnnotationNode(new ClassNode(Test.class)); private static final String GROOVY_TEST_CASE_CLASS_NAME = "groovy.util.GroovyTestCase"; public static final String VOID_TYPE = "void"; private ResourceLocator resourceLocator; public ResourceLocator getResourceLocator() { if (resourceLocator == null) { resourceLocator = new DefaultResourceLocator(); String basedir = BuildSettings.BASE_DIR.getAbsolutePath(); resourceLocator.setSearchLocation(basedir); } return resourceLocator; } @Override public void visit(ASTNode[] astNodes, SourceUnit source) { if (!(astNodes[0] instanceof AnnotationNode) || !(astNodes[1] instanceof AnnotatedNode)) { throw new RuntimeException("Internal error: wrong types: $node.class / $parent.class"); } AnnotatedNode parent = (AnnotatedNode) astNodes[1]; AnnotationNode node = (AnnotationNode) astNodes[0]; if (!MY_TYPE.equals(node.getClassNode()) || !(parent instanceof ClassNode)) { return; } ClassNode classNode = (ClassNode) parent; if (classNode.isInterface() || Modifier.isAbstract(classNode.getModifiers())) { return; } boolean junit3Test = isJunit3Test(classNode); boolean spockTest = isSpockTest(classNode); boolean isJunit = !junit3Test && !spockTest; if (!junit3Test && !spockTest && !isJunit) return; handleTestForAnnotation(classNode, source, node, junit3Test); } protected void handleTestForAnnotation(ClassNode classNode, SourceUnit source, AnnotationNode testForAnnotationNode, boolean junit3Test) { Expression value = testForAnnotationNode.getMember("value"); ClassExpression ce; if (value instanceof ClassExpression) { ce = (ClassExpression) value; testFor(classNode, ce); return; } if (junit3Test) { return; } List<AnnotationNode> annotations = classNode.getAnnotations(MY_TYPE); if (annotations.size()>0) return; // bail out, in this case it was already applied as a local transform annotations = classNode.getAnnotations(TestMixinTransformation.MY_TYPE); if (annotations.size()>0) return; // bail out, another TestMixin transform already defines behavior // no explicit class specified try by convention String fileName = source.getName(); String className = GrailsResourceUtils.getClassName(new FileSystemResource(fileName)); if (className == null) { return; } String targetClassName = null; if (className.endsWith("Tests")) { targetClassName = className.substring(0, className.indexOf("Tests")); } else if (className.endsWith("Test")) { targetClassName = className.substring(0, className.indexOf("Test")); } else if (className.endsWith("Spec")) { targetClassName = className.substring(0, className.indexOf("Spec")); } if (targetClassName == null) { return; } Resource targetResource = getResourceLocator().findResourceForClassName(targetClassName); if (targetResource == null) { return; } try { if (GrailsResourceUtils.isDomainClass(targetResource.getURL())) { testFor(classNode, new ClassExpression(new ClassNode(targetClassName, 0, ClassHelper.OBJECT_TYPE))); } else { for (String artefactType : artefactTypeToTestMap.keySet()) { if (targetClassName.endsWith(artefactType)) { testFor(classNode, new ClassExpression(new ClassNode(targetClassName, 0, ClassHelper.OBJECT_TYPE))); break; } } } } catch (IOException e) { // ignore } } /** * Main entry point for the calling the TestForTransformation programmatically. * * @param classNode The class node that represents th test * @param ce The class expression that represents the class to test */ public void testFor(ClassNode classNode, ClassExpression ce) { autoAnnotateSetupTeardown(classNode); boolean isJunit3Test = isJunit3Test(classNode); // make sure the 'log' property is not the one from GroovyTestCase FieldNode log = classNode.getField("log"); if (log == null || log.getDeclaringClass().getName().equals(GROOVY_TEST_CASE_CLASS_NAME)) { new LoggingTransformer().performInjectionOnAnnotatedClass(classNode.getModule().getContext(), classNode); } boolean isSpockTest = isSpockTest(classNode); boolean isJunit4 = !isSpockTest && !isJunit3Test; if (isJunit4) { // assume JUnit 4 Map<String, MethodNode> declaredMethodsMap = classNode.getDeclaredMethodsMap(); boolean hasTestMethods = false; for (String methodName : declaredMethodsMap.keySet()) { MethodNode methodNode = declaredMethodsMap.get(methodName); ClassNode testAnnotationClassNode = TEST_ANNOTATION.getClassNode(); List<AnnotationNode> existingTestAnnotations = methodNode.getAnnotations(testAnnotationClassNode); if (isCandidateMethod(methodNode) && (methodNode.getName().startsWith("test") || existingTestAnnotations.size()>0)) { if (existingTestAnnotations.size()==0) { ClassNode returnType = methodNode.getReturnType(); if (returnType.getName().equals(VOID_TYPE)) { methodNode.addAnnotation(TEST_ANNOTATION); } } hasTestMethods = true; } } if (!hasTestMethods) { isJunit4 = false; } } if (isJunit4 || isJunit3Test || isSpockTest) { final MethodNode methodToAdd = weaveMock(classNode, ce, true); if (methodToAdd != null && isJunit3Test) { addMethodCallsToMethod(classNode,SET_UP_METHOD, Arrays.asList(methodToAdd)); } } } private Map<ClassNode, List<Class>> wovenMixins = new HashMap<ClassNode, List<Class>>(); protected MethodNode weaveMock(ClassNode classNode, ClassExpression value, boolean isClassUnderTest) { ClassNode testTarget = value.getType(); String className = testTarget.getName(); MethodNode testForMethod = null; for (String artefactType : artefactTypeToTestMap.keySet()) { if (className.endsWith(artefactType)) { Class mixinClass = artefactTypeToTestMap.get(artefactType); if (!isAlreadyWoven(classNode, mixinClass)) { weaveMixinClass(classNode, mixinClass); if (isClassUnderTest) { testForMethod = addClassUnderTestMethod(classNode, value, artefactType); } else { addMockCollaboratorToSetup(classNode, value, artefactType); } return testForMethod; } addMockCollaboratorToSetup(classNode, value, artefactType); return null; } } // must be a domain class boolean isDataTest = GrailsASTUtils.isSubclassOf(classNode, "grails.test.hibernate.HibernateSpec") || GrailsASTUtils.isSubclassOf(classNode, "grails.test.mongodb.MongoSpec"); if(!isDataTest) { Class<?> domainClassPresent = null; try { domainClassPresent = Class.forName("org.grails.plugins.domain.DomainClassGrailsPlugin", true, TestForTransformation.class.getClassLoader()); } catch (ClassNotFoundException e) { // not on classpath ignore } catch (NoClassDefFoundError e) { // ignore } if(domainClassPresent != null) { weaveMixinClass(classNode, DomainClassUnitTestMixin.class); if (isClassUnderTest) { testForMethod = addClassUnderTestMethod(classNode, value, DOMAIN_TYPE); } else { addMockCollaboratorToSetup(classNode, value, DOMAIN_TYPE); } return testForMethod; } } return null; } protected Class getMixinClassForArtefactType(ClassNode classNode) { String className = classNode.getName(); for (String artefactType : artefactTypeToTestMap.keySet()) { if (className.endsWith(artefactType)) { Class mixinClass = artefactTypeToTestMap.get(artefactType); if (mixinClass != null) { return mixinClass; } } } return null; } private void addMockCollaboratorToSetup(ClassNode classNode, ClassExpression targetClassExpression, String artefactType) { BlockStatement methodBody = getOrCreateTestSetupMethod(classNode); addMockCollaborator(artefactType, targetClassExpression,methodBody); } protected BlockStatement getOrCreateTestSetupMethod(ClassNode classNode) { BlockStatement methodBody; if (isJunit3Test(classNode)) { methodBody = getJunit3Setup(classNode); } else { methodBody = getExistingOrCreateJUnit4Setup(classNode); } return methodBody; } protected BlockStatement getExistingOrCreateJUnit4Setup(ClassNode classNode) { Statement code = getExistingJUnit4BeforeMethod(classNode); if (code instanceof BlockStatement) { return (BlockStatement) code; } return getJunit4Setup(classNode); } protected Statement getExistingJUnit4BeforeMethod(ClassNode classNode) { Statement code = null; Map<String, MethodNode> declaredMethodsMap = classNode.getDeclaredMethodsMap(); for (MethodNode methodNode : declaredMethodsMap.values()) { if (isDeclaredBeforeMethod(methodNode)) { code = getMethodBody(methodNode); } } return code; } private Statement getMethodBody(MethodNode methodNode) { Statement code = methodNode.getCode(); if (!(code instanceof BlockStatement)) { BlockStatement body = new BlockStatement(); body.addStatement(code); code = body; } return code; } private boolean isDeclaredBeforeMethod(MethodNode methodNode) { return isPublicInstanceMethod(methodNode) && hasAnnotation(methodNode, Before.class) && !hasAnnotation(methodNode, MixinMethod.class); } private boolean isPublicInstanceMethod(MethodNode methodNode) { return !methodNode.isSynthetic() && !methodNode.isStatic() && methodNode.isPublic(); } private BlockStatement getJunit4Setup(ClassNode classNode) { MethodNode setupMethod = classNode.getDeclaredMethod(SET_UP_METHOD, GrailsArtefactClassInjector.ZERO_PARAMETERS); if (setupMethod == null) { setupMethod = new MethodNode(SET_UP_METHOD,Modifier.PUBLIC,ClassHelper.VOID_TYPE,GrailsArtefactClassInjector.ZERO_PARAMETERS,null,new BlockStatement()); setupMethod.addAnnotation(MIXIN_METHOD_ANNOTATION); classNode.addMethod(setupMethod); } if (setupMethod.getAnnotations(BEFORE_CLASS_NODE).size() == 0) { setupMethod.addAnnotation(BEFORE_ANNOTATION); } return getOrCreateMethodBody(classNode, setupMethod, SET_UP_METHOD); } private BlockStatement getJunit3Setup(ClassNode classNode) { boolean hasExistingSetupMethod = classNode.hasDeclaredMethod(SET_UP_METHOD, Parameter.EMPTY_ARRAY); BlockStatement setUpMethodBody = getOrCreateNoArgsMethodBody(classNode, SET_UP_METHOD); if(!hasExistingSetupMethod) { setUpMethodBody.getStatements().add(new ExpressionStatement(new MethodCallExpression(new VariableExpression("super"), SET_UP_METHOD, GrailsArtefactClassInjector.ZERO_ARGS))); } return setUpMethodBody; } private boolean isAlreadyWoven(ClassNode classNode, Class mixinClass) { List<Class> mixinClasses = wovenMixins.get(classNode); if (mixinClasses == null) { mixinClasses = new ArrayList<Class>(); mixinClasses.add(mixinClass); wovenMixins.put(classNode, mixinClasses); } else { if (mixinClasses.contains(mixinClass)) { return true; } mixinClasses.add(mixinClass); } return false; } protected void weaveMixinClass(ClassNode classNode, Class mixinClass) { ListExpression listExpression = new ListExpression(); listExpression.addExpression(new ClassExpression(new ClassNode(mixinClass))); weaveMixinsIntoClass(classNode,listExpression); } protected MethodNode addClassUnderTestMethod(ClassNode classNode, ClassExpression targetClass, String type) { String methodName = "setup" + type + "UnderTest"; String fieldName = GrailsNameUtils.getPropertyName(type); String getterName = GrailsNameUtils.getGetterName(fieldName); fieldName = '$' +fieldName; if (classNode.getField(fieldName) == null) { classNode.addField(fieldName, Modifier.PRIVATE, targetClass.getType(),null); } MethodNode methodNode = classNode.getDeclaredMethod(methodName,GrailsArtefactClassInjector.ZERO_PARAMETERS); VariableExpression fieldExpression = new VariableExpression(fieldName, targetClass.getType()); if (methodNode == null) { BlockStatement setupMethodBody = new BlockStatement(); addMockCollaborator(type, targetClass, setupMethodBody); methodNode = new MethodNode(methodName, Modifier.PUBLIC, ClassHelper.VOID_TYPE, GrailsArtefactClassInjector.ZERO_PARAMETERS,null, setupMethodBody); methodNode.addAnnotation(BEFORE_ANNOTATION); methodNode.addAnnotation(MIXIN_METHOD_ANNOTATION); classNode.addMethod(methodNode); GrailsASTUtils.addCompileStaticAnnotation(methodNode); } MethodNode getter = classNode.getDeclaredMethod(getterName, GrailsArtefactClassInjector.ZERO_PARAMETERS); if (getter == null) { BlockStatement getterBody = new BlockStatement(); getter = new MethodNode(getterName, Modifier.PUBLIC, targetClass.getType().getPlainNodeReference(),GrailsArtefactClassInjector.ZERO_PARAMETERS,null, getterBody); BinaryExpression testTargetAssignment = new BinaryExpression(fieldExpression, ASSIGN, new ConstructorCallExpression(targetClass.getType(), GrailsArtefactClassInjector.ZERO_ARGS)); IfStatement autowiringIfStatement = getAutowiringIfStatement(targetClass,fieldExpression, testTargetAssignment); getterBody.addStatement(autowiringIfStatement); getterBody.addStatement(new ReturnStatement(fieldExpression)); classNode.addMethod(getter); GrailsASTUtils.addCompileStaticAnnotation(getter); } return methodNode; } private IfStatement getAutowiringIfStatement(ClassExpression targetClass, VariableExpression fieldExpression, BinaryExpression testTargetAssignment) { VariableExpression appCtxVar = new VariableExpression("applicationContext", ClassHelper.make(ApplicationContext.class)); BooleanExpression applicationContextCheck = new BooleanExpression( new BinaryExpression( new BinaryExpression(fieldExpression, GrailsASTUtils.EQUALS_OPERATOR, GrailsASTUtils.NULL_EXPRESSION), Token.newSymbol("&&",0,0), new BinaryExpression(appCtxVar, GrailsASTUtils.NOT_EQUALS_OPERATOR, GrailsASTUtils.NULL_EXPRESSION))); BlockStatement performAutowireBlock = new BlockStatement(); ArgumentListExpression arguments = new ArgumentListExpression(); arguments.addExpression(fieldExpression); arguments.addExpression(new ConstantExpression(1)); arguments.addExpression(new ConstantExpression(false)); BlockStatement assignFromApplicationContext = new BlockStatement(); ArgumentListExpression argWithClassName = new ArgumentListExpression(); final PropertyExpression classNamePropertyExpression = new PropertyExpression(targetClass, new ConstantExpression("name")); argWithClassName.addExpression(classNamePropertyExpression); assignFromApplicationContext.addStatement(new ExpressionStatement(new BinaryExpression(fieldExpression, ASSIGN, new MethodCallExpression(appCtxVar, "getBean", argWithClassName)))); BlockStatement elseBlock = new BlockStatement(); elseBlock.addStatement(new ExpressionStatement(testTargetAssignment)); performAutowireBlock.addStatement(new IfStatement(new BooleanExpression(new MethodCallExpression(appCtxVar, "containsBean", argWithClassName)), assignFromApplicationContext, elseBlock)); performAutowireBlock.addStatement(new ExpressionStatement(new MethodCallExpression(new PropertyExpression(appCtxVar,"autowireCapableBeanFactory"), "autowireBeanProperties", arguments))); return new IfStatement(applicationContextCheck, performAutowireBlock, new BlockStatement()); } protected void addMockCollaborator(String mockType, ClassExpression targetClass, BlockStatement methodBody) { ArgumentListExpression args = new ArgumentListExpression(); args.addExpression(targetClass); methodBody.getStatements().add(0, new ExpressionStatement(new MethodCallExpression(new VariableExpression("this"), "mock" + mockType, args))); } protected void addMockCollaborators(ClassNode classNode, String mockType, List<ClassExpression> targetClasses) { addMockCollaborators(mockType, targetClasses, getOrCreateTestSetupMethod(classNode)); } protected void addMockCollaborators(String mockType, List<ClassExpression> targetClasses, BlockStatement methodBody) { ArgumentListExpression args = new ArgumentListExpression(); for(ClassExpression ce : targetClasses) { args.addExpression(ce); } methodBody.getStatements().add(0, new ExpressionStatement(new MethodCallExpression(new VariableExpression("this"), "mock" + mockType + 's', args))); } @Override public void registerMixin(String artefactType, Class mixin) { artefactTypeToTestMap.put(artefactType, mixin); } }