/*
* Copyright 2016 the original author or authors.
*
* 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.powermock.core.transformers.impl;
import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.CtPrimitiveType;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import org.powermock.core.IndicateReloadClass;
import org.powermock.core.testlisteners.GlobalNotificationBuildSupport;
import org.powermock.core.transformers.MockTransformer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashSet;
/**
* MockTransformer implementation that will make PowerMock test-class
* enhancements for four purposes...
* 1) Make test-class static initializer and constructor send crucial details
* (for PowerMockTestListener events) to GlobalNotificationBuildSupport so that
* this information can be forwarded to whichever
* facility is used for composing the PowerMockTestListener events.
* 2) Removal of test-method annotations as a mean to achieve test-suite
* chunking!
* 3) Restore original test-class constructors` accesses
* (in case they have all been made public by {@link
* ClassMockTransformer#setAllConstructorsToPublic(javassist.CtClass)})
* - to avoid that multiple <i>public</i> test-class constructors cause
* a delegate runner from JUnit (or 3rd party) to bail out with an
* error message such as "Test class can only have one constructor".
* 4) Set test-class defer constructor (if exist) as protected instead of public.
* Otherwise a delegate runner from JUnit (or 3rd party) might get confused by
* the presence of more than one test-class constructor and bail out with an
* error message such as "Test class can only have one constructor".
*
* The #3 and #4 enhancements will also be enforced on the constructors
* of classes that are nested within the test-class.
*/
public abstract class TestClassTransformer implements MockTransformer {
private final Class<?> testClass;
private final Class<? extends Annotation> testMethodAnnotationType;
public interface ForTestClass {
RemovesTestMethodAnnotation removesTestMethodAnnotation(Class<? extends Annotation> testMethodAnnotation);
interface RemovesTestMethodAnnotation {
TestClassTransformer fromMethods(Collection<Method> testMethodsThatRunOnOtherClassLoaders);
TestClassTransformer fromAllMethodsExcept(Method singleMethodToRunOnThisClassLoader);
}
}
public static ForTestClass forTestClass(final Class<?> testClass) {
return new ForTestClass() {
@Override
public RemovesTestMethodAnnotation removesTestMethodAnnotation(
final Class<? extends Annotation> testMethodAnnotation) {
return new RemovesTestMethodAnnotation() {
@Override
public TestClassTransformer fromMethods(
final Collection<Method> testMethodsThatRunOnOtherClassLoaders) {
return new TestClassTransformer(testClass, testMethodAnnotation) {
/**
* Is lazily initilized because of
* AbstractTestSuiteChunkerImpl#chunkClass(Class)
*/
Collection<String> methodsThatRunOnOtherClassLoaders;
@Override
boolean mustHaveTestAnnotationRemoved(CtMethod method)
throws NotFoundException {
if (null == methodsThatRunOnOtherClassLoaders) {
/* This lazy initialization is necessary - see above */
methodsThatRunOnOtherClassLoaders = new HashSet<String>();
for (Method m : testMethodsThatRunOnOtherClassLoaders) {
methodsThatRunOnOtherClassLoaders.add(
signatureOf(m));
}
testMethodsThatRunOnOtherClassLoaders.clear();
}
return methodsThatRunOnOtherClassLoaders
.contains(signatureOf(method));
}
};
}
@Override
public TestClassTransformer fromAllMethodsExcept(
Method singleMethodToRunOnTargetClassLoader) {
final String targetMethodSignature =
signatureOf(singleMethodToRunOnTargetClassLoader);
return new TestClassTransformer(testClass, testMethodAnnotation) {
@Override
boolean mustHaveTestAnnotationRemoved(CtMethod method)
throws Exception {
return !signatureOf(method).equals(targetMethodSignature);
}
};
}
};
}
};
}
private TestClassTransformer(
Class<?> testClass, Class<? extends Annotation> testMethodAnnotationType) {
this.testClass = testClass;
this.testMethodAnnotationType = testMethodAnnotationType;
}
private boolean isTestClass(CtClass clazz) {
try {
return Class.forName(clazz.getName(), false, testClass.getClassLoader())
.isAssignableFrom(testClass);
} catch (ClassNotFoundException ex) {
return false;
}
}
private boolean isNestedWithinTestClass(CtClass clazz) {
String clazzName = clazz.getName();
return clazzName.startsWith(testClass.getName())
&& '$' == clazzName.charAt(testClass.getName().length());
}
private Class<?> asOriginalClass(CtClass type) throws Exception {
try {
return type.isArray()
? Array.newInstance(asOriginalClass(type.getComponentType()), 0).getClass()
: type.isPrimitive()
? Primitives.getClassFor((CtPrimitiveType) type)
: Class.forName(type.getName(), true, testClass.getClassLoader());
} catch (Exception ex) {
throw new RuntimeException("Cannot resolve type: " + type, ex);
}
}
private Class<?>[] asOriginalClassParams(CtClass[] parameterTypes)
throws Exception {
final Class<?>[] classParams = new Class[parameterTypes.length];
for (int i = 0; i < classParams.length; ++i) {
classParams[i] = asOriginalClass(parameterTypes[i]);
}
return classParams;
}
abstract boolean mustHaveTestAnnotationRemoved(CtMethod method) throws Exception;
private void removeTestMethodAnnotationFrom(CtMethod m)
throws ClassNotFoundException {
final AnnotationsAttribute attr = (AnnotationsAttribute)
m.getMethodInfo().getAttribute(AnnotationsAttribute.visibleTag);
javassist.bytecode.annotation.Annotation[] newAnnotations =
new javassist.bytecode.annotation.Annotation[attr.numAnnotations() - 1];
int i = -1;
for (javassist.bytecode.annotation.Annotation a : attr.getAnnotations()) {
if (a.getTypeName().equals(testMethodAnnotationType.getName())) {
continue;
}
newAnnotations[++i] = a;
}
attr.setAnnotations(newAnnotations);
}
private void removeTestAnnotationsForTestMethodsThatRunOnOtherClassLoader(CtClass clazz)
throws Exception {
for (CtMethod m : clazz.getDeclaredMethods()) {
if (m.hasAnnotation(testMethodAnnotationType)
&& mustHaveTestAnnotationRemoved(m)) {
removeTestMethodAnnotationFrom(m);
}
}
}
@Override
public CtClass transform(final CtClass clazz) throws Exception {
if (clazz.isFrozen()) {
clazz.defrost();
}
if (isTestClass(clazz)) {
removeTestAnnotationsForTestMethodsThatRunOnOtherClassLoader(clazz);
addLifeCycleNotifications(clazz);
makeDeferConstructorNonPublic(clazz);
restoreOriginalConstructorsAccesses(clazz);
} else if (isNestedWithinTestClass(clazz)) {
makeDeferConstructorNonPublic(clazz);
restoreOriginalConstructorsAccesses(clazz);
}
return clazz;
}
private void addLifeCycleNotifications(CtClass clazz) {
try {
addClassInitializerNotification(clazz);
addConstructorNotification(clazz);
} catch (CannotCompileException ex) {
throw new Error("Powermock error: " + ex.getMessage(), ex);
}
}
private void addClassInitializerNotification(CtClass clazz)
throws CannotCompileException {
if (null == clazz.getClassInitializer()) {
clazz.makeClassInitializer();
}
clazz.getClassInitializer().insertBefore(
GlobalNotificationBuildSupport.class.getName()
+ ".testClassInitiated(" + clazz.getName() + ".class);");
}
private static boolean hasSuperClass(CtClass clazz) {
try {
CtClass superClazz = clazz.getSuperclass();
/*
* Being extra careful here - and backup in case the
* work-in-progress clazz doesn't cause NotFoundException ...
*/
return null != superClazz
&& !"java.lang.Object".equals(superClazz.getName());
} catch (NotFoundException noWasSuperClassFound) {
return false;
}
}
private void addConstructorNotification(final CtClass clazz)
throws CannotCompileException {
final String notificationCode =
GlobalNotificationBuildSupport.class.getName()
+ ".testInstanceCreated(this);";
final boolean asFinally = !hasSuperClass(clazz);
for (final CtConstructor constr : clazz.getDeclaredConstructors()) {
constr.insertAfter(
notificationCode,
asFinally/* unless there is a super-class, because of this
* problem: https://community.jboss.org/thread/94194*/);
}
}
private void restoreOriginalConstructorsAccesses(CtClass clazz) throws Exception {
Class<?> originalClass = testClass.getName().equals(clazz.getName())
? testClass
: Class.forName(clazz.getName(), true, testClass.getClassLoader());
for (final CtConstructor ctConstr : clazz.getConstructors()) {
int ctModifiers = ctConstr.getModifiers();
if (!Modifier.isPublic(ctModifiers)) {
/* Probably a defer-constructor */
continue;
}
int desiredAccessModifiers = originalClass.getDeclaredConstructor(
asOriginalClassParams(ctConstr.getParameterTypes())).getModifiers();
if (Modifier.isPrivate(desiredAccessModifiers)) {
ctConstr.setModifiers(Modifier.setPrivate(ctModifiers));
} else if (Modifier.isProtected(desiredAccessModifiers)) {
ctConstr.setModifiers(Modifier.setProtected(ctModifiers));
} else if (!Modifier.isPublic(desiredAccessModifiers)) {
ctConstr.setModifiers(Modifier.setPackage(ctModifiers));
} else {
/* ctConstr remains public */
}
}
}
private void makeDeferConstructorNonPublic(final CtClass clazz) {
for (final CtConstructor constr : clazz.getConstructors()) {
try {
for (CtClass paramType : constr.getParameterTypes()) {
if (IndicateReloadClass.class.getName()
.equals(paramType.getName())) {
/* Found defer constructor ... */
final int modifiers = constr.getModifiers();
if (Modifier.isPublic(modifiers)) {
constr.setModifiers(Modifier.setProtected(modifiers));
}
break;
}
}
} catch (NotFoundException thereAreNoParameters) {
/* ... but to get an exception here seems odd. */
}
}
}
private static String signatureOf(Method m) {
Class<?>[] paramTypes = m.getParameterTypes();
String[] paramTypeNames = new String[paramTypes.length];
for (int i = 0; i < paramTypeNames.length; ++i) {
paramTypeNames[i] = paramTypes[i].getSimpleName();
}
return createSignature(
m.getDeclaringClass().getSimpleName(),
m.getReturnType().getSimpleName(),
m.getName(), paramTypeNames);
}
private static String signatureOf(CtMethod m) throws NotFoundException {
CtClass[] paramTypes = m.getParameterTypes();
String[] paramTypeNames = new String[paramTypes.length];
for (int i = 0; i < paramTypeNames.length; ++i) {
paramTypeNames[i] = paramTypes[i].getSimpleName();
}
return createSignature(
m.getDeclaringClass().getSimpleName(),
m.getReturnType().getSimpleName(),
m.getName(), paramTypeNames);
}
private static String createSignature(
String testClass, String returnType, String methodName, String[] paramTypes) {
StringBuilder builder = new StringBuilder(testClass)
.append('\n').append(returnType)
.append('\n').append(methodName);
for (String param : paramTypes) {
builder.append('\n').append(param);
}
return builder.toString();
}
}