ch.inftec.ju.testing.db.DbTestAnnotationHandler Maven / Gradle / Ivy
Show all versions of ju-testing Show documentation
package ch.inftec.ju.testing.db;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.persistence.EntityManager;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.junit.internal.AssumptionViolatedException;
import org.junit.runner.Description;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import ch.inftec.ju.db.JuEmUtil;
import ch.inftec.ju.testing.db.DbDataUtil.ExportBuilder;
import ch.inftec.ju.util.AssertUtil;
import ch.inftec.ju.util.IOUtil;
import ch.inftec.ju.util.JuException;
import ch.inftec.ju.util.JuObjectUtils;
import ch.inftec.ju.util.JuRuntimeException;
import ch.inftec.ju.util.JuStringUtils;
import ch.inftec.ju.util.JuUrl;
import ch.inftec.ju.util.JuUtils;
import ch.inftec.ju.util.ReflectUtils;
import ch.inftec.ju.util.XString;
import ch.inftec.ju.util.xml.XmlUtils;
/**
* Helper class to handle test annotations like @DataSet and @DataVerify.
*
* When calling the execute... methods, the client is responsible that a valid transaction is present.
* @author Martin
*
*/
public class DbTestAnnotationHandler implements Serializable {
private final Logger logger = LoggerFactory.getLogger(DbTestAnnotationHandler.class);
private final List dataSetAnnos;
private final List dataSetExportAnnos;
private final List postServerCodeAnnos;
private final List dataVerifyAnnos;
protected final String testClassName;
protected final String testMethodName;
/**
* Readable name of the test method, may be testMethod[0] for parameterized tests.
*/
private final String testMethodReadableName;
public DbTestAnnotationHandler(Method method, Description description) {
// Get all annotations for the method and the declaring class (including super classes, but
// excluding overridden methods)
this.dataSetAnnos = ReflectUtils.getAnnotations(method, DataSet.class, false, true, true);
// Reverse the list as we want to start with the base class, then class and method last
Collections.reverse(this.dataSetAnnos);
this.dataSetExportAnnos = ReflectUtils.getAnnotations(method, DataSetExport.class, true, true, true);
this.postServerCodeAnnos = ReflectUtils.getAnnotations(method, PostServerCode.class, true, false, false);
this.dataVerifyAnnos = ReflectUtils.getAnnotations(method, DataVerify.class, true, false, false);
this.testClassName = method.getDeclaringClass().getName();
this.testMethodName = method.getName();
this.testMethodReadableName = description.getMethodName();
}
private Class> getTestClass() {
try {
return Class.forName(this.testClassName);
} catch (Exception ex) {
throw new JuRuntimeException("Couldn't get test class. Make sure it's on the classpath: " + this.testClassName);
}
}
// private Method getTestMethod() {
// return ReflectUtils.getMethod(this.getTestClass(), this.testMethodName, new Class>[0]);
// }
public final void executePreTestAnnotations(JuEmUtil emUtil) throws Exception {
// Load test data as defined by annotations
DbDataUtil du = new DbDataUtil(emUtil);
Integer sequenceValue = null;
for (DataSet dataSet : this.dataSetAnnos) {
// Run pre initializer
this.runInitializer(dataSet.preInitializer(), emUtil.getEm());
if (DataSet.NO_CLEAN_INSERT.equals(dataSet.value())) {
// Skip clean-insert
} else {
// Perform clean-insert of value resource
URL resourceUrl = this.resourceToUrl(dataSet.value(), dataSet.resourceDir());
du.buildImport()
.from(resourceUrl)
.executeCleanInsert();
}
// Perform inserts for inserts resources
for (String insertResource : dataSet.inserts()) {
URL resourceUrl = this.resourceToUrl(insertResource, dataSet.resourceDir());
du.buildImport()
.from(resourceUrl)
.executeInsert();
}
sequenceValue = dataSet.sequenceValue();
// Run post initializer
this.runInitializer(dataSet.postInitializer(), emUtil.getEm());
}
// Reset the sequences
if (sequenceValue != null) {
emUtil.resetIdentityGenerationOrSequences(sequenceValue);
}
}
private void runInitializer(Class extends ServerCode> clazz, EntityManager em) throws Exception {
ServerCode initializer = ReflectUtils.newInstance(clazz, false);
initializer.init(em);
initializer.execute();
}
/**
* Converts a resourceUrl string to an URL. This also performs paramterized placeholder replacement
* if necessary.
* @param resource Resource path
* @param resourceDir Resource directory in case we need to lookup the resource in the file system
* @return Actual resource URL
* @throws JuRuntimeException If the resource is not valid
*/
private URL resourceToUrl(String resource, String resourceDir) {
String actualResource = resource;
// Perform {param} placeholder replacement
if (resource.indexOf(DataSet.PARAM_POSTFIX) > 0) {
String parameterizedTestName = this.getParameterizedTestName();
AssertUtil.assertNotNull("Doesn't seem to be parameterized test: " + this.testMethodReadableName, parameterizedTestName);
actualResource = actualResource.replace(DataSet.PARAM_POSTFIX, "[" + parameterizedTestName + "]");
}
URL url = null;
if (!JuUtils.getJuPropertyChain().get("ju-testing.export.compareToResource", Boolean.class)
&& !StringUtils.isEmpty(resourceDir)) {
// Lookup resource in file system
Path p = Paths.get(this.getLocalRoot(), resourceDir, actualResource);
url = JuUrl.toUrl(p);
} else {
// Lookup resource as (classpath) resource
url = JuUrl.resource().relativeTo(this.getTestClass()).get(actualResource);
if (url == null) url = JuUrl.resource(actualResource);
}
if (url == null) {
throw new JuRuntimeException(String.format("Couldn't find resource %s, relative to class %s"
, actualResource
, this.getTestClass()));
}
return url;
}
/**
* Gets the local root directory used to resolve resource locations.
*
* Can be overridden by extending classes to provide a different root.
* @return Root location for resource lookup on the filesystem
*/
protected String getLocalRoot() {
return ".";
}
/**
* Get the name of the parameterized test.
* @return Parameterized test name or null if the test is not parameterized.
*/
private String getParameterizedTestName() {
if (this.testMethodReadableName.indexOf("[") < 0 || !this.testMethodReadableName.endsWith("]")) {
return null;
} else {
return this.testMethodReadableName.substring(this.testMethodReadableName.indexOf("[") + 1
, this.testMethodReadableName.length() - 1);
}
}
/**
* Extending classes can override this method to perform initialization on the
* test class before the test method is invoked.
* @param instance
*/
protected void initTestClass(Object instance) {
}
public final void executePostServerCode(JuEmUtil emUtil) throws Exception {
// Execute post server code
for (PostServerCode code : this.postServerCodeAnnos) {
Class> codeClass = null;
if (code.value() == PostServerCode.DEFAULT_SERVER_CODE.class) {
String verifierName = StringUtils.capitalize(this.testMethodName + "_code");
Class> defaultVerifier = ReflectUtils.getInnerClass(this.getTestClass(), verifierName);
AssertUtil.assertNotNull(String.format("Couldn't find Verifier %s as inner class of %s. Make sure it exists and is public static."
, verifierName, this.getTestClass())
, defaultVerifier);
codeClass = defaultVerifier;
} else {
codeClass = code.value();
}
this.runServerCode(codeClass, emUtil.getEm());
}
}
public final void executePostTestAnnotations(JuEmUtil emUtil) throws Exception {
// Process DataSetExport annotation. We'll just consider the first annotation.
Document doc = null;
if (this.dataSetExportAnnos.size() > 0) {
DataSetExport dataSetExport = this.dataSetExportAnnos.get(0);
// Get file name
String targetFileName = dataSetExport.exportName();
if (StringUtils.isEmpty(targetFileName)) {
// Construct name using class and method name
targetFileName = String.format("%s_%s.xml"
,this.getTestClass().getSimpleName()
, JuStringUtils.removeNonAlphabeticalLeadingCharacters(this.testMethodReadableName));
}
URL tablesDataSestUrl = JuUrl.resource().relativeTo(this.getTestClass()).get(dataSetExport.tablesDataSet());
if (tablesDataSestUrl == null) tablesDataSestUrl = JuUrl.resource(dataSetExport.tablesDataSet());
ExportBuilder eb = new DbDataUtil(emUtil).buildExport()
.addTablesByDataSet(tablesDataSestUrl, true);
doc = eb.writeToXmlDocument();
if (dataSetExport.doPhysicalExport()) {
if (JuUtils.getJuPropertyChain().get("ju-testing.export.compareToResource", Boolean.class, true)) {
// Perform export in-memory and compare to resource
String resourcePrefix = dataSetExport.resourcePrefix();
String resourcePath = resourcePrefix + "/" + targetFileName;
URL resourceUrl = JuUrl.singleResource(resourcePath);
String resourceString = new IOUtil().loadTextFromUrl(resourceUrl);
String xmlString = eb.writeToXmlString();
logger.debug("Comparing DB export to resource {}", resourceUrl);
Assert.assertEquals(resourceString, xmlString);
} else {
// Perform export to file
String targetDirName = dataSetExport.targetDir();
// Create target directory
Path targetDirPath = Paths.get(this.getLocalRoot(), targetDirName);
Files.createDirectories(targetDirPath);
// Build file path
Path targetFilePath = targetDirPath.resolve(targetFileName);
eb.writeToXmlFile(targetFilePath);
}
} else {
// Log XML
if (logger.isInfoEnabled()) {
XString xs = new XString(targetFileName);
xs.newLine();
xs.addLine(XmlUtils.toString(doc, true, true));
logger.info(xs.toString());
}
}
if (this.dataSetExportAnnos.size() > 1) {
logger.warn("Ignoring DataSetExport annotations as only first is processed");
}
}
// Run data verifiers (provided the test method and data set export has succeeded)
List verifiers = new ArrayList();
// Check for programmatic verifiers
for (DataVerify verify : this.dataVerifyAnnos) {
Class> verifierClass = null;
if (verify.value() == DataVerify.DEFAULT_DATA_VERIFIER.class) {
String verifierName = StringUtils.capitalize(JuStringUtils.removeNonAlphabeticalLeadingCharacters(this.testMethodName));
Class> defaultVerifier = ReflectUtils.getInnerClass(this.getTestClass(), verifierName);
AssertUtil.assertNotNull(
String.format("Couldn't find Verifier %s as inner class of %s. Make sure it exists and is public static."
, verifierName, this.getTestClass())
, defaultVerifier);
verifierClass = defaultVerifier;
} else {
verifierClass = verify.value();
}
verifiers.add(this.createVerifier(verifierClass, emUtil.getEm(), doc));
}
// Run verifiers
for (DataVerifier verifier : verifiers) {
verifier.verify();
}
}
private void runServerCode(Class> codeClass, EntityManager em) throws Exception {
AssertUtil.assertTrue("Code class must be of type ServerCode: " + codeClass.getName(), ServerCode.class.isAssignableFrom(codeClass));
ServerCode code = (ServerCode) ReflectUtils.newInstance(codeClass, false);
code.init(em);
try {
code.execute();
} catch (Exception ex) {
this.handleServerThrowable(ex);
}
}
/**
* Handle Server throwables to make sure we can send them to the client.
* @param t Throwable
* @throws T Handled throwable (may be the same or a converted Throwable)
*/
protected final void handleServerThrowable(T t) throws T {
// Handle non-serializable exceptions
if (!IOUtil.isSerializable(t)) {
// If we have an assumption failure wrapped in an InvocationTargetException, rethrow a new
// AssumptionViolatedException that just containing the message
InvocationTargetException ite = JuObjectUtils.as(t, InvocationTargetException.class);
if (ite != null && ite.getTargetException() instanceof AssumptionViolatedException) {
throw new AssumptionViolatedException(ite.getTargetException().getMessage());
}
// Use cause (if possible / serializable)
Throwable cause = IOUtil.isSerializable(t.getCause())
? t.getCause()
: new JuException("Original cause was non-serializable. Check server for details.");
throw new JuRuntimeException("%s (Original Exception %s was non-serializable)", cause, t.getMessage(), t.getClass().getName());
} else {
throw t;
}
}
private DataVerifier createVerifier(Class> verifierClass, EntityManager em, Document doc) {
AssertUtil.assertTrue("Verifier must be of type DataVerifier: " + verifierClass.getName(), DataVerifier.class.isAssignableFrom(verifierClass));
DataVerifier verifier = (DataVerifier) ReflectUtils.newInstance(verifierClass, false);
verifier.init(em, doc);
this.initVerifier(verifier);
return verifier;
}
/**
* Extending classes can override this method to perform additional initialization on the DataVerifier.
* @param verifier DataVerifier
*/
protected void initVerifier(DataVerifier verifier) {
}
@Override
public String toString() {
return String.format("%s.%s()", this.testClassName, this.testMethodReadableName);
}
}