package unittesting;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.JUnitCore;
import org.junit.runners.JUnit4;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import unittesting.proxies.TestSuite;
import unittesting.proxies.UnitTest;
import unittesting.proxies.UnitTestResult;
import com.mendix.core.Core;
import com.mendix.core.CoreException;
import com.mendix.logging.ILogNode;
import com.mendix.systemwideinterfaces.core.IContext;
import com.mendix.systemwideinterfaces.core.IDataType;
import communitycommons.XPath;
/**
* @author mwe
*
*/
public class TestManager
{
/** Test manager introduces its own exception, because the AssertionExceptions from JUnit are not picked up properly by
* the runtime in 4.1.1 and escape all exception handling defined inside microflows :-S
* @author mwe
*
*/
public static class AssertionException extends Exception
{
private static final long serialVersionUID = -3115796226784699883L;
public AssertionException(String message)
{
super(message);
}
}
private static final String TEST_MICROFLOW_PREFIX_1 = "Test_";
private static final String TEST_MICROFLOW_PREFIX_2 = "UT_";
static final String CLOUD_SECURITY_ERROR = "Unable to find JUnit test classes or methods. \n\n";
private static TestManager instance;
public static ILogNode LOG = Core.getLogger("UnitTestRunner");
private static final Map<String, Object> emptyArguments = new HashMap<String, Object>();
private static final Map<String, Class<?>[]> classCache = new HashMap<String, Class<?>[]>();
private String lastStep;
public static TestManager instance()
{
if (instance == null)
instance = new TestManager();
return instance;
}
private static Class<?>[] getUnitTestClasses(TestSuite testRun) throws ZipException, IOException {
if (!classCache.containsKey(testRun.getModule().toLowerCase())) {
ArrayList<Class<?>> classlist = getClassesForPackage(testRun.getModule());
Class<?>[] clazzez = classlist.toArray(new Class<?>[classlist.size()]);
classCache.put(testRun.getModule().toLowerCase(), clazzez);
}
return classCache.get(testRun.getModule().toLowerCase());
}
public synchronized void runTest(IContext context, UnitTest unitTest) throws ClassNotFoundException, CoreException
{
TestSuite testSuite = unitTest.getUnitTest_TestSuite();
/**
* Is Mf
*/
if (unitTest.getIsMf()) {
try {
runMfSetup(testSuite);
runMicroflowTest(unitTest.getName(), unitTest);
}
finally {
runMfTearDown(testSuite);
}
}
/**
* Is java
*/
else {
Class<?> test = Class.forName(unitTest.getName().split("/")[0]);
JUnitCore junit = new JUnitCore();
junit.addListener(new UnitTestRunListener(context, testSuite));
junit.run(test);
}
}
private void runMfSetup(TestSuite testSuite)
{
if (Core.getMicroflowNames().contains(testSuite.getModule() + ".Setup")) {
try {
LOG.info("Running Setup microflow..");
Core.execute(Core.createSystemContext(), testSuite.getModule() + ".Setup", emptyArguments);
}
catch(Exception e) {
LOG.error("Exception during SetUp microflow: " + e.getMessage(), e);
throw new RuntimeException(e);
}
}
}
private void runMfTearDown(TestSuite testSuite)
{
if (Core.getMicroflowNames().contains(testSuite.getModule() + ".TearDown")) {
try
{
LOG.info("Running TearDown microflow..");
Core.execute(Core.createSystemContext(), testSuite.getModule() + ".TearDown", emptyArguments);
}
catch (Exception e)
{
LOG.error("Severe: exception in unittest TearDown microflow '" + testSuite.getModule() + ".Setup': " +e.getMessage(), e);
throw new RuntimeException(e);
}
}
}
public synchronized void runTestSuites() throws CoreException {
LOG.info("Starting testrun on all suites");
//Context without transaction!
IContext context = Core.createSystemContext();
for(TestSuite suite : XPath.create(context, TestSuite.class).all()) {
suite.setResult(null);
suite.commit();
}
for(TestSuite suite : XPath.create(context, TestSuite.class).all()) {
runTestSuite(context, suite);
}
LOG.info("Finished testrun on all suites");
}
public synchronized boolean runTestSuite(IContext context, TestSuite testSuite) throws CoreException
{
LOG.info("Starting testrun on " + testSuite.getModule());
/**
* Reset state
*/
testSuite.setLastRun(new Date());
testSuite.setLastRunTime(0L);
testSuite.setTestFailedCount(0L);
testSuite.setResult(UnitTestResult._1_Running);
testSuite.commit();
for(UnitTest test : XPath.create(context, UnitTest.class).eq(UnitTest.MemberNames.UnitTest_TestSuite, testSuite).all()) {
test.setResult(null);
test.commit();
}
long start = System.currentTimeMillis();
/**
* Run java unit tests
*/
if(unittesting.proxies.constants.Constants.getFindJUnitTests())
{
Class<?>[] clazzez = null;
try {
clazzez = getUnitTestClasses(testSuite);
}
catch(Exception e) {
LOG.error(CLOUD_SECURITY_ERROR + e.getMessage(), e);
}
if (clazzez != null && clazzez.length > 0) {
JUnitCore junit = new JUnitCore();
junit.addListener(new UnitTestRunListener(context, testSuite));
junit.run(clazzez);
}
}
/**
* Run microflow tests
*
*/
try {
runMfSetup(testSuite);
List<String> mfnames = findMicroflowUnitTests(testSuite);
for (String mf : mfnames){
if (!runMicroflowTest(mf, getUnitTest(context, testSuite, mf, true), testSuite))
{
testSuite.setTestFailedCount(testSuite.getTestFailedCount() + 1);
testSuite.commit();
}
}
}
finally {
runMfTearDown(testSuite);
}
/**
* Aggregate
*/
testSuite.setLastRunTime((System.currentTimeMillis() - start) / 1000);
testSuite.setResult(testSuite.getTestFailedCount() == 0L ? UnitTestResult._3_Success : UnitTestResult._2_Failed);
testSuite.commit();
LOG.info("Finished testrun on " + testSuite.getModule());
return true;
}
public List<String> findMicroflowUnitTests(TestSuite testRun)
{
List<String> mfnames = new ArrayList<String>();
String basename1 = (testRun.getModule() + "." + TEST_MICROFLOW_PREFIX_1).toLowerCase();
String basename2 = (testRun.getModule() + "." + TEST_MICROFLOW_PREFIX_2).toLowerCase();
//Find microflownames
for (String mf : Core.getMicroflowNames())
if (mf.toLowerCase().startsWith(basename1) || mf.toLowerCase().startsWith(basename2))
mfnames.add(mf);
//Sort microflow names
Collections.sort(mfnames);
return mfnames;
}
private boolean runMicroflowTest(String mf, UnitTest test) throws CoreException
{
/**
* Prepare...
*/
TestSuite testSuite = test.getUnitTest_TestSuite();
return runMicroflowTest(mf, test, testSuite);
}
private boolean runMicroflowTest(String mf, UnitTest test, TestSuite testSuite) throws CoreException
{
/**
* Prepare...
*/
LOG.info("Starting unittest for microflow " + mf);
reportStep("Starting microflow test '" + mf + "'");
test.setResult(UnitTestResult._1_Running);
test.setName(mf);
test.setResultMessage("");
test.setLastRun(new Date());
if (Core.getInputParameters(mf).size() != 0) {
test.setResultMessage("Unable to start test '" + mf + "', microflow has parameters");
test.setResult(UnitTestResult._2_Failed);
}
else if (Core.getReturnType(mf).getType() != IDataType.DataTypeEnum.Boolean &&
Core.getReturnType(mf).getType() != IDataType.DataTypeEnum.String &&
Core.getReturnType(mf).getType() != IDataType.DataTypeEnum.Nothing) {
test.setResultMessage("Unable to start test '" + mf + "', microflow should return either a boolean or a string or nothing at all");
test.setResult(UnitTestResult._2_Failed);
}
commitSilent(test);
IContext mfContext = Core.createSystemContext();
if (testSuite.getAutoRollbackMFs())
mfContext.startTransaction();
long start = System.currentTimeMillis();
try {
Object resultObject = Core.execute(mfContext, mf, emptyArguments);
start = System.currentTimeMillis() - start;
boolean res = resultObject == null || Boolean.TRUE.equals(resultObject) || "".equals(resultObject);
test.setResult(res ? UnitTestResult._3_Success : UnitTestResult._2_Failed);
if (res) {
test.setResultMessage("Microflow completed successfully");
}
return res;
}
catch(Exception e) {
start = System.currentTimeMillis() - start;
test.setResult(UnitTestResult._2_Failed);
Throwable cause = ExceptionUtils.getRootCause(e);
if (cause != null && cause instanceof AssertionException)
test.setResultMessage(cause.getMessage());
else
test.setResultMessage("Exception: " + e.getMessage() + "\n\n" + ExceptionUtils.getStackTrace(e));
return false;
}
finally {
if (testSuite.getAutoRollbackMFs())
mfContext.rollbackTransAction();
test.setLastStep(lastStep);
test.setReadableTime((start > 10000 ? Math.round(start / 1000) + " seconds" : start + " milliseconds"));
commitSilent(test);
LOG.info("Finished unittest " + mf + ": " + test.getResult());
}
}
private void commitSilent(UnitTest test)
{
try
{
test.commit();
}
catch (CoreException e)
{
throw new RuntimeException(e);
}
}
UnitTest getUnitTest(IContext context, TestSuite testSuite, Description description, boolean isMF) {
return getUnitTest(context, testSuite, description.getClassName() + "/" + description.getMethodName(), isMF);
}
private UnitTest getUnitTest(IContext context, TestSuite testSuite, String name, boolean isMF) {
UnitTest res;
try
{
res = XPath.create(context, UnitTest.class)
.eq(UnitTest.MemberNames.UnitTest_TestSuite, testSuite)
.and()
.eq(UnitTest.MemberNames.Name, name)
.and()
.eq(UnitTest.MemberNames.IsMf, isMF)
.first();
}
catch (CoreException e)
{
throw new RuntimeException(e);
}
if (res == null) {
res = new UnitTest(context);
res.setName(name);
res.setUnitTest_TestSuite(testSuite);
res.setIsMf(isMF);
}
return res;
}
/**
*
*
* Find runabble classes
*
* https://github.com/ddopson/java-class-enumerator/blob/master/src/pro/ddopson/ClassEnumerator.java
*
*/
private static Class<?> loadClass(String className) {
try {
return TestManager.instance().getClass().getClassLoader().loadClass(className);
}
catch (ClassNotFoundException e) {
throw new RuntimeException("Unexpected ClassNotFoundException loading class '" + className + "'");
}
}
private static void processProjectJar(File projectJar, String pkgname, ArrayList<Class<?>> classes) throws IOException {
// Get the list of the files contained in the package
ZipFile zipFile = new ZipFile(projectJar);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
System.out.println("Starting processProjec");
while(entries.hasMoreElements()){
ZipEntry zipEntry = entries.nextElement();
String fileName = zipEntry.getName();
String className = null;
if (fileName.startsWith(pkgname) && fileName.endsWith(".class")) {
fileName = fileName.replace("/", ".");
// removes the .class extension
className = fileName.substring(0, fileName.length() - 6);
}
if (className != null) {
Class<?> clazz = loadClass(className);
if (isProperUnitTest(clazz))
classes.add(clazz);
}
}
zipFile.close();
}
private static boolean isProperUnitTest(Class<?> clazz)
{
for (Method m : clazz.getMethods())
if (m.getAnnotation(org.junit.Test.class) != null)
return true;
return false;
}
public static ArrayList<Class<?>> getClassesForPackage(String path /*Package pkg*/) throws ZipException, IOException {
ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
//String pkgname = pkg.getName();
//String relPath = pkgname.replace('.', '/');
//Lowercased Mendix Module names equals their package names
String pkgname = path.toLowerCase();
// Get a File object containing the classes. This file is expected to be located at [deploymentdir]/model/bundles/project.jar
File projectjar = new File(Core.getConfiguration().getBasePath() + File.separator + "model" + File.separator + "bundles" + File.separator + "project.jar");
processProjectJar(projectjar, pkgname, classes);
return classes;
}
public void reportStep(String lastStep1)
{
lastStep = lastStep1;
LOG.debug("UnitTest reportStep: '" + lastStep1 + "'");
}
public synchronized void findAllTests(IContext context) throws CoreException {
/*
* Find modules
*/
Set<String> modules = new HashSet<String>();
for(String name : Core.getMicroflowNames())
modules.add(name.split("\\.")[0]);
/*
* Update modules
*/
for(String module : modules) {
TestSuite testSuite = XPath.create(context, TestSuite.class).findOrCreate(TestSuite.MemberNames.Module, module);
updateUnitTestList(context, testSuite);
}
/*
* Remove all modules without tests
*/
XPath.create(context, TestSuite.class).not().hasReference(UnitTest.MemberNames.UnitTest_TestSuite, UnitTest.entityName).close().deleteAll();
}
public synchronized void updateUnitTestList(IContext context, TestSuite testSuite)
{
try {
/*
* Mark all dirty
*/
for(UnitTest test : XPath.create(context, UnitTest.class)
.eq(UnitTest.MemberNames.UnitTest_TestSuite, testSuite)
.all()) {
test.set_dirty(true);
test.commit();
}
/*
* Find microflow tests
*/
for (String mf : findMicroflowUnitTests(testSuite)) {
UnitTest test = getUnitTest(context, testSuite, mf, true);
test.set_dirty(false);
test.setUnitTest_TestSuite(testSuite);
test.commit();
}
if(unittesting.proxies.constants.Constants.getFindJUnitTests())
{
/*
* Find Junit tests
*/
for (String jtest : findJUnitTests(testSuite)) {
UnitTest test = getUnitTest(context, testSuite, jtest, false);
test.set_dirty(false);
test.setUnitTest_TestSuite(testSuite);
test.commit();
}
}
/*
* Delete dirty tests
*/
for(UnitTest test : XPath.create(context, UnitTest.class)
.eq(UnitTest.MemberNames._dirty, true)
.all()) {
test.delete();
}
/*
* Update count
*/
testSuite.setTestCount(XPath.create(context, UnitTest.class).eq(UnitTest.MemberNames.UnitTest_TestSuite, testSuite).count());
testSuite.commit();
}
catch(Exception e) {
LOG.error("Failed to update unit test list: " + e.getMessage(), e);
}
}
public List<String> findJUnitTests(TestSuite testSuite)
{
List<String> junitTests = new ArrayList<String>();
try {
Class<?>[] clazzez = getUnitTestClasses(testSuite);
if (clazzez != null && clazzez.length > 0) {
for (Class<?> clazz : clazzez) {
//From https://github.com/KentBeck/junit/blob/master/src/main/java/org/junit/runners/BlockJUnit4ClassRunner.java method computeTestMethods
try {
List<FrameworkMethod> methods = new JUnit4(clazz).getTestClass().getAnnotatedMethods(Test.class);
if (methods != null && !methods.isEmpty())
for (FrameworkMethod method: methods)
junitTests.add(clazz.getName() + "/" + method.getName());
}
catch(InitializationError e2) {
StringBuilder errors = new StringBuilder();
for(Throwable cause : e2.getCauses())
errors.append("\n").append(cause.getMessage());
LOG.error("Failed to recognize class '" + clazz + "' as unitTestClass: " + errors.toString());
}
}
}
}
catch(Exception e) {
LOG.error(CLOUD_SECURITY_ERROR + e.getMessage(), e);
}
return junitTests;
}
public String getLastReportedStep() {
//MWE: this system is problematic weird if used from multiple simultanously used threads..
return lastStep;
}
}