/* * 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.classloader; import javassist.ClassClassPath; import javassist.ClassPool; import javassist.CtClass; import org.powermock.core.ClassReplicaCreator; import org.powermock.core.WildcardMatcher; import org.powermock.core.classloader.annotations.UseClassPathAdjuster; import org.powermock.core.spi.PowerMockPolicy; import org.powermock.core.spi.support.InvocationSubstitute; import org.powermock.core.transformers.MockTransformer; import java.security.ProtectionDomain; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Mock all classes except system classes. * <p/> * Notice that there are two different types of classes that are not mocked: * <ol> * <li>system classes are deferred to another classloader</li> * <li>testing framework classes are loaded, but not modified</li> * </ol> * * @author Johan Haleby * @author Jan Kronquist */ public class MockClassLoader extends DeferSupportingClassLoader { /** * Pass this string to the constructor to indicate that all classes should * be modified. */ public static final String MODIFY_ALL_CLASSES = "*"; private static final String CGLIB_ENHANCER = "net.sf.cglib.proxy.Enhancer$EnhancerKey$$KeyFactoryByCGLIB$$"; private static final String CGLIB_METHOD_WRAPPER = "net.sf.cglib.core.MethodWrapper$MethodWrapperKey$$KeyFactoryByCGLIB"; private final JavaAssistClassMarker javaAssistClassMarker; private List<MockTransformer> mockTransformerChain; private final Set<String> modify = Collections.synchronizedSet(new HashSet<String>()); /* * Classes not deferred but loaded by the mock class loader but they're not * modified. */ private final String[] packagesToLoadButNotModify = new String[]{"org.junit.", "junit.", "org.testng.", "org.easymock.", "net.sf.cglib.", "javassist.", "org.powermock.modules.junit4.internal.", "org.powermock.modules.junit4.legacy.internal.", "org.powermock.modules.junit3.internal.", "org.powermock"}; private final String[] specificClassesToLoadButNotModify = new String[]{InvocationSubstitute.class.getName(), PowerMockPolicy.class.getName(), ClassReplicaCreator.class.getName()}; /* * Classes that should always be deferred regardless of what the user * specifies in annotations etc. */ private static final String[] packagesToBeDeferred = new String[]{"org.hamcrest.*", "java.*", "javax.accessibility.*", "sun.*", "org.junit.*", "org.testng.*", "junit.*", "org.pitest.*", "org.powermock.modules.junit4.common.internal.*", "org.powermock.modules.junit3.internal.PowerMockJUnit3RunnerDelegate*", "org.powermock.core*", "org.jacoco.agent.rt.*"}; private final ClassPool classPool = new ClassPool(); /** * Creates a new instance of the {@link MockClassLoader} based on the * following parameters: * * @param classesToMock The classes that must be modified to prepare for testability. * @param packagesToDefer Classes in these packages will be defered to the system * class-loader. */ public MockClassLoader(String[] classesToMock, String[] packagesToDefer, UseClassPathAdjuster useClassPathAdjuster) { super(MockClassLoader.class.getClassLoader(), getPackagesToDefer(packagesToDefer)); addClassesToModify(classesToMock); classPool.appendClassPath(new ClassClassPath(this.getClass())); if (useClassPathAdjuster != null) { try { Class<? extends ClassPathAdjuster> value = useClassPathAdjuster.value(); ClassPathAdjuster classPathAdjuster = value.newInstance(); classPathAdjuster.adjustClassPath(classPool); } catch (Exception e) { throw new RuntimeException("Error instantiating class path adjuster", e); } } javaAssistClassMarker = JavaAssistClassMarkerFactory.createClassMarker(classPool); } MockClassLoader() { this(new String[0], new String[0]); } private static String[] getPackagesToDefer(final String[] additionalDeferPackages) { final int additionalIgnorePackagesLength = additionalDeferPackages == null ? 0 : additionalDeferPackages.length; final int defaultDeferPackagesLength = packagesToBeDeferred.length; final int allIgnoreLength = defaultDeferPackagesLength + additionalIgnorePackagesLength; final String[] allPackagesToBeIgnored = new String[allIgnoreLength]; if (allIgnoreLength > defaultDeferPackagesLength) { System.arraycopy(packagesToBeDeferred, 0, allPackagesToBeIgnored, 0, defaultDeferPackagesLength); System.arraycopy(additionalDeferPackages != null ? additionalDeferPackages : new String[0], 0, allPackagesToBeIgnored, defaultDeferPackagesLength, additionalIgnorePackagesLength); return allPackagesToBeIgnored; } return packagesToBeDeferred; } /** * Creates a new instance of the {@link MockClassLoader} based on the * following parameters: * * @param classesToMock The classes that must be modified to prepare for testability. * @param packagesToDefer Classes in these packages will be defered to the system * class-loader. */ public MockClassLoader(String[] classesToMock, String[] packagesToDefer) { this(classesToMock, packagesToDefer, null); } /** * Creates a new instance of the {@link MockClassLoader} based on the * following parameters: * * @param classesToMock The classes that must be modified to prepare for testability. */ public MockClassLoader(String[] classesToMock, UseClassPathAdjuster useClassPathAdjuster) { this(classesToMock, new String[0], useClassPathAdjuster); } /** * Creates a new instance of the {@link MockClassLoader} based on the * following parameters: * * @param classesToMock The classes that must be modified to prepare for testability. */ public MockClassLoader(String[] classesToMock) { this(classesToMock, new String[0], null); } /** * Add classes that will be loaded by the mock classloader, i.e. these * classes will be byte-code manipulated to allow for testing. Any classes * contained in the {@link #packagesToBeDeferred} will be ignored. How ever * classes added here have precedence over additionally deferred (ignored) * packages (those ignored by the user using @PrepareForTest). * * @param classes The fully qualified name of the classes that will be appended * to the list of classes that will be byte-code modified to * enable testability. */ public final void addClassesToModify(String... classes) { if (classes != null) { for (String clazz : classes) { if (!shouldDefer(packagesToBeDeferred, clazz)) { modify.add(clazz); } } } } @Override protected Class<?> loadModifiedClass(String s) throws ClassFormatError, ClassNotFoundException { Class<?> loadedClass; Class<?> deferClass = deferTo.loadClass(s); if (shouldModify(s) && !shouldLoadWithMockClassloaderWithoutModifications(s)) { loadedClass = loadMockClass(s, deferClass.getProtectionDomain()); } else { loadedClass = loadUnmockedClass(s, deferClass.getProtectionDomain()); } return loadedClass; } private boolean shouldModify(String className) { final boolean shouldIgnoreClass = shouldIgnore(deferPackages, className); final boolean shouldModifyAll = shouldModifyAll(); if (shouldModifyAll) { return !shouldIgnoreClass; } else { /* * Never mind if we should ignore the class here since * classes added by prepared for test should (i.e. those added in "modify") * have precedence over ignored packages. */ return WildcardMatcher.matchesAny(modify, className); } } private boolean shouldModifyAll() { return (modify.size() == 1 && modify.iterator().next().equals(MODIFY_ALL_CLASSES)); } private Class<?> loadUnmockedClass(String name, ProtectionDomain protectionDomain) throws ClassFormatError, ClassNotFoundException { byte bytes[] = null; try { /* * TODO This if-statement is a VERY ugly hack to avoid the * java.lang.ExceptionInInitializerError caused by * "javassist.NotFoundException: * net.sf.cglib.proxy.Enhancer$EnhancerKey$$KeyFactoryByCGLIB$$7fb24d72 * ". This happens after the * se.jayway.examples.tests.privatefield. * SimplePrivateFieldServiceClassTest#testUseService(..) tests has * been run and all other tests will fail if this class is tried to * be loaded. Atm I have found no solution other than this ugly hack * to make it work. We really need to investigate the real cause of * this behavior. */ if (!name.startsWith(CGLIB_ENHANCER) && !name.startsWith(CGLIB_METHOD_WRAPPER)) { final CtClass ctClass = classPool.get(name); if (ctClass.isFrozen()) { ctClass.defrost(); } bytes = ctClass.toBytecode(); } } catch (Exception e) { if (e instanceof javassist.NotFoundException) { throw new ClassNotFoundException(); } else { throw new RuntimeException(e); } } return bytes == null ? null : defineClass(name, bytes, 0, bytes.length, protectionDomain); } /** * Load a mocked version of the class. */ private Class<?> loadMockClass(String name, ProtectionDomain protectionDomain) { final byte[] clazz; ClassPool.doPruning = false; try { CtClass type = classPool.get(name); for (MockTransformer transformer : mockTransformerChain) { type = transformer.transform(type); } javaAssistClassMarker.mark(type); /* * ClassPool may cause huge memory consumption if the number of CtClass * objects becomes amazingly large (this rarely happens since Javassist * tries to reduce memory consumption in various ways). To avoid this * problem, you can explicitly remove an unnecessary CtClass object from * the ClassPool. If you call detach() on a CtClass object, then that * CtClass object is removed from the ClassPool. */ type.detach(); clazz = type.toBytecode(); } catch (Exception e) { throw new IllegalStateException("Failed to transform class with name " + name + ". Reason: " + e.getMessage(), e); } return defineClass(name, clazz, 0, clazz.length, protectionDomain); } public void setMockTransformerChain(List<MockTransformer> mockTransformerChain) { this.mockTransformerChain = mockTransformerChain; } @Override protected boolean shouldModifyClass(String s) { return modify.contains(s); } @Override protected boolean shouldLoadUnmodifiedClass(String className) { for (String classNameToLoadButNotModify : specificClassesToLoadButNotModify) { if (className.equals(classNameToLoadButNotModify)) { return true; } } return false; } private boolean shouldLoadWithMockClassloaderWithoutModifications(String className) { if (className.startsWith("org.powermock.example")) { return false; } for (String packageToLoadButNotModify : packagesToLoadButNotModify) { if (className.startsWith(packageToLoadButNotModify)) { return true; } } return false; } }