/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.server.execution;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import org.diqube.executionenv.cache.ColumnShardCache;
import org.diqube.threads.ExecutorManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.Factory;
import org.testng.annotations.Test;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.MethodDelegation;
/**
* Utility to execute test methods of a test twice by only using a single bean context.
*
* <p>
* This is meaningful to Test the {@link ColumnShardCache} - the first execution of a test method fills the cache, the
* second is then executed with intermediate results taken from the cache.
*
* <p>
* Use this utility in a method annotated with TestNGs {@link Factory} annotation.
*
* <p>
* The test classes that are generated dynamically will have a name that ends with {@link #CACHE_DOUBLE_IDENTIFIER}.
*
* @author Bastian Gloeckle
*/
public class CacheDoubleTestUtil {
/** Identifier for dynamically created classes and methods (suffix). */
private static final String CACHE_DOUBLE_IDENTIFIER = "CacheDouble";
/**
* Creates a object of a dynamically created class for each test method.
*
* When executing the tests on the returned objects, this will result in the test methods being called twice:
*
* <pre>
* {@link AbstractDiqlExecutionTest#setUp()};
* testMethod();
* testMethod();
* {@link AbstractDiqlExecutionTest#cleanup()};
* </pre>
*
* @param baseTest
* The test class object which defines the test methods.
* @return For each test method a object which will, when executed by TestNG execute that test method twice.
*/
public static Object[] createTestObjects(AbstractDiqlExecutionTest<?> baseTest) {
List<Object> res = new ArrayList<>();
for (Method m : baseTest.getClass().getMethods()) {
if (m.isAnnotationPresent(Test.class) && !m.isAnnotationPresent(IgnoreInCacheDoubleTestUtil.class)) {
res.add(createTestClassObject(baseTest, m));
}
}
return res.toArray();
}
/**
* Creates a test class dynamically for a single test method and returns an instance thereof.
*
* @param baseTest
* @param testMethod
* @return
*/
private static Object createTestClassObject(AbstractDiqlExecutionTest<?> baseTest, Method testMethod) {
// define the method that the returned class should have
MethodDescription.Latent testMethodDescription = new MethodDescription.Latent( //
testMethod.getName() + CACHE_DOUBLE_IDENTIFIER, //
null, // not important, as this is ignored by ByteBuddy (defineMethod(.) call below).
TypeDescription.VOID, //
new ArrayList<>(), //
Modifier.PUBLIC, //
new ArrayList<>());
Test testAnnotation = testMethod.getAnnotation(Test.class);
String newClassName = baseTest.getClass().getName() + CACHE_DOUBLE_IDENTIFIER;
Class<?> testClass = new ByteBuddy() //
.subclass(Object.class) //
.name(newClassName) // class name
.defineMethod(testMethodDescription) // define our method
// intercept calls of our method and call the method in CacheDoubleTestInterceptor instead.
.intercept(MethodDelegation.to(new CacheDoubleTestInterceptor(baseTest, testMethod, newClassName))) //
// let the new method have a Test annotation - use the original annotation to preserve all annotation properties
.annotateMethod(testAnnotation) //
.make() // make class
.load(CacheDoubleTestUtil.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) //
.getLoaded();
try {
return testClass.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("Cannot instantiate dynamic test class", e);
}
}
/**
* Intercepts calls to the method of the dynamically created test classes.
*/
protected static class CacheDoubleTestInterceptor {
private Logger logger;
private AbstractDiqlExecutionTest<?> baseTest;
private Method testMethod;
public CacheDoubleTestInterceptor(AbstractDiqlExecutionTest<?> baseTest, Method testMethod, String loggerName) {
this.baseTest = baseTest;
this.testMethod = testMethod;
this.logger = LoggerFactory.getLogger(loggerName);
}
/**
* This method will be executed when the {@link Test}-annotated method of a dynamically created test class is
* called.
*
* This implements the logic of the test.
*
* Take care when changing the signature of this method that ByteBuddy keeps accepting it as target method of our
* interception.
*/
public void testImplementationIntercept() throws Throwable {
try {
baseTest.setUp();
try {
logger.info("Executing test method first time: {} (see {} for details)", testMethod,
CacheDoubleTestUtil.class.getName());
testMethod.invoke(baseTest);
ExecutorManager executorManager = baseTest.getDataContext().getBean(ExecutorManager.class);
if (executorManager != null) {
logger.info("Shutting down the execution of all queries of the first run of the test method {}",
testMethod);
// as second execution of test method will effectively use the very same queryUuid, we need to enforce the
// cleanup here.
executorManager.shutdownEverythingOfAllQueries();
}
logger.info(
"Executing test method second time (this time there should be something in the cache): {} (see {} for details)",
testMethod, CacheDoubleTestUtil.class.getName());
testMethod.invoke(baseTest);
} catch (InvocationTargetException e) {
logger.error("Exception while double-executing test method {}: {}. Test executed by {}.",
testMethod.toString(), e.getTargetException().toString(), CacheDoubleTestUtil.class.getName(), e);
// re-throw inner exception. The test could expect a specific exception to be thrown - take care of that!
throw e.getTargetException();
} finally {
baseTest.cleanup();
}
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot invoke test method. Test executed by " + CacheDoubleTestUtil.class.getName(),
e);
}
}
}
/**
* Annotate a {@link Test} method with this annotation to <b>NOT</b> let {@link CacheDoubleTestUtil} create a
* double-test for this method!
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public static @interface IgnoreInCacheDoubleTestUtil {
}
}