diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml
index 977fba877c..4a4cdce18b 100644
--- a/testsuite/integration-arquillian/pom.xml
+++ b/testsuite/integration-arquillian/pom.xml
@@ -119,6 +119,7 @@
test-apps
+ test-utils
servers
tests
diff --git a/testsuite/integration-arquillian/test-utils/pom.xml b/testsuite/integration-arquillian/test-utils/pom.xml
new file mode 100644
index 0000000000..c03c56b334
--- /dev/null
+++ b/testsuite/integration-arquillian/test-utils/pom.xml
@@ -0,0 +1,34 @@
+
+
+
+ integration-arquillian
+ org.keycloak.testsuite
+ 2.2.0-SNAPSHOT
+
+ 4.0.0
+
+ integration-arquillian-test-utils
+ jar
+
+ Test utils
+
+
+
+ junit
+ junit
+ compile
+
+
+ org.jboss.logging
+ jboss-logging
+
+
+ commons-configuration
+ commons-configuration
+ 1.10
+
+
+
+
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-utils/src/main/java/org/keycloak/testsuite/util/junit/AggregateResultsReporter.java b/testsuite/integration-arquillian/test-utils/src/main/java/org/keycloak/testsuite/util/junit/AggregateResultsReporter.java
new file mode 100644
index 0000000000..a0809c126c
--- /dev/null
+++ b/testsuite/integration-arquillian/test-utils/src/main/java/org/keycloak/testsuite/util/junit/AggregateResultsReporter.java
@@ -0,0 +1,269 @@
+package org.keycloak.testsuite.util.junit;
+
+import org.apache.commons.configuration.PropertiesConfiguration;
+
+import org.jboss.logging.Logger;
+
+import org.junit.Ignore;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Aggregates jUnit test results into a single report - XML file.
+ */
+public class AggregateResultsReporter extends RunListener {
+
+ private static final Logger LOGGER = Logger.getLogger(AggregateResultsReporter.class);
+
+ private final Document xml;
+ private final File reportFile;
+ private final boolean working;
+
+ private final AtomicInteger tests = new AtomicInteger(0);
+ private final AtomicInteger errors = new AtomicInteger(0);
+ private final AtomicInteger failures = new AtomicInteger(0);
+ private final AtomicInteger ignored = new AtomicInteger(0);
+ private final AtomicLong suiteStartTime = new AtomicLong(0L);
+
+ private final AtomicReference testsuite = new AtomicReference();
+
+ private final Map testTimes = new HashMap();
+
+ public AggregateResultsReporter() {
+ boolean working = true;
+ Document xml = null;
+ try {
+ xml = createEmptyDocument();
+ } catch (ParserConfigurationException ex) {
+ LOGGER.error("Failed to create XML DOM - reporting will not be done", ex);
+ working = false;
+ }
+
+ File reportFile = null;
+ try {
+ reportFile = createReportFile();
+ } catch (Exception ex) {
+ LOGGER.error("Failed to create log file - reporting will not be done", ex);
+ working = false;
+ }
+
+ this.working = working;
+ this.xml = xml;
+ this.reportFile = reportFile;
+ }
+
+ private Document createEmptyDocument() throws ParserConfigurationException {
+ DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+ return builder.newDocument();
+ }
+
+ private File createReportFile() throws Exception {
+ PropertiesConfiguration config = new PropertiesConfiguration(System.getProperty("testsuite.constants"));
+ config.setThrowExceptionOnMissing(true);
+
+ final File logDir = new File(config.getString("log-dir"));
+ logDir.mkdirs();
+
+ final File reportFile = new File(logDir, "junit-report.xml").getAbsoluteFile();
+ reportFile.createNewFile();
+
+ return reportFile;
+ }
+
+ @Override
+ public void testRunStarted(Description description) throws Exception {
+ if (working) {
+ suiteStartTime.set(System.currentTimeMillis());
+
+ Element testsuite = xml.createElement("testsuite");
+
+ if (description.getChildren().size() == 1) {
+ testsuite.setAttribute("name", safeString(description.getChildren().get(0).getDisplayName()));
+ }
+
+ xml.appendChild(testsuite);
+ this.testsuite.set(testsuite);
+ writeXml();
+ }
+ }
+
+ @Override
+ public void testStarted(Description description) throws Exception {
+ if (working) {
+ testTimes.put(description.getDisplayName(), System.currentTimeMillis());
+ }
+ }
+
+ @Override
+ public void testFinished(Description description) throws Exception {
+ if (working) {
+ if (testTimes.containsKey(description.getDisplayName())) {
+ testsuite.get().appendChild(createTestCase(description));
+ writeXml();
+ }
+ }
+ }
+
+ @Override
+ public void testAssumptionFailure(Failure failure) {
+ if (working) {
+ ignored.incrementAndGet();
+
+ Element testcase = createTestCase(failure.getDescription());
+ Element skipped = xml.createElement("skipped");
+ skipped.setAttribute("message", safeString(failure.getMessage()));
+
+ testcase.appendChild(skipped);
+
+ testsuite.get().appendChild(testcase);
+ writeXml();
+ }
+ }
+
+ @Override
+ public void testFailure(Failure failure) throws Exception {
+ if (working) {
+ if (failure.getDescription().getMethodName() == null) {
+ // before class failed
+ for (Description child : failure.getDescription().getChildren()) {
+ // mark all methods failed
+ testFailure(new Failure(child, failure.getException()));
+ }
+ } else {
+ // normal failure
+ Element testcase = createTestCase(failure.getDescription());
+
+ Element element;
+ if (failure.getException() instanceof AssertionError) {
+ failures.incrementAndGet();
+ element = xml.createElement("failure");
+ } else {
+ errors.incrementAndGet();
+ element = xml.createElement("error");
+ }
+
+ testcase.appendChild(element);
+
+ element.setAttribute("type", safeString(failure.getException().getClass().getName()));
+ element.setAttribute("message", safeString(failure.getMessage()));
+ element.appendChild(xml.createCDATASection(safeString(failure.getTrace())));
+
+ testsuite.get().appendChild(testcase);
+ writeXml();
+ }
+ }
+ }
+
+ @Override
+ public void testIgnored(Description description) throws Exception {
+ if (working) {
+ ignored.incrementAndGet();
+
+ Element testcase = createTestCase(description);
+
+ Element skipped = xml.createElement("skipped");
+ skipped.setAttribute("message", safeString(description.getAnnotation(Ignore.class).value()));
+
+ testcase.appendChild(skipped);
+
+ testsuite.get().appendChild(testcase);
+ writeXml();
+ }
+ }
+
+ @Override
+ public void testRunFinished(Result result) throws Exception {
+ if (working) {
+ writeXml();
+ }
+ }
+
+ private void writeXml() {
+ Element testsuite = this.testsuite.get();
+
+ testsuite.setAttribute("tests", Integer.toString(tests.get()));
+ testsuite.setAttribute("errors", Integer.toString(errors.get()));
+ testsuite.setAttribute("skipped", Integer.toString(ignored.get()));
+ testsuite.setAttribute("failures", Integer.toString(failures.get()));
+ testsuite.setAttribute("time", computeTestTime(suiteStartTime.get()));
+
+ try {
+ Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(reportFile, false), Charset.forName("UTF-8")));
+ try {
+ Transformer t = TransformerFactory.newInstance().newTransformer();
+ t.setOutputProperty(OutputKeys.INDENT, "yes");
+ t.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
+ t.transform(new DOMSource(xml), new StreamResult(writer));
+ } catch (TransformerConfigurationException ex) {
+ LOGGER.error("Misconfigured transformer", ex);
+ } catch (TransformerException ex) {
+ LOGGER.error("Unable to save XML file", ex);
+ } finally {
+ writer.close();
+ }
+ } catch (IOException ex) {
+ LOGGER.warn("Unable to open report file", ex);
+ }
+ }
+
+ private String computeTestTime(Long startTime) {
+ if (startTime == null) {
+ return "0";
+ } else {
+ long amount = System.currentTimeMillis() - startTime;
+ return String.format("%.3f", amount / 1000F);
+ }
+ }
+
+ private Element createTestCase(Description description) {
+ tests.incrementAndGet();
+
+ Element testcase = xml.createElement("testcase");
+
+ testcase.setAttribute("name", safeString(description.getMethodName()));
+ testcase.setAttribute("classname", safeString(description.getClassName()));
+ testcase.setAttribute("time", computeTestTime(testTimes.remove(description.getDisplayName())));
+
+ return testcase;
+ }
+
+ private String safeString(String input) {
+ if (input == null) {
+ return "null";
+ }
+
+ return input
+ // first remove color coding (all of it)
+ .replaceAll("\u001b\\[\\d+m", "")
+ // then remove control characters that are not whitespaces
+ .replaceAll("[\\p{Cntrl}&&[^\\p{Space}]]", "");
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml
index b2ee848b26..3220be5292 100755
--- a/testsuite/integration-arquillian/tests/pom.xml
+++ b/testsuite/integration-arquillian/tests/pom.xml
@@ -37,7 +37,7 @@
base
other
-
+
undertow
true
@@ -178,7 +178,7 @@
listener
- org.keycloak.testsuite.util.TestEventsLogger
+ org.keycloak.testsuite.util.TestEventsLogger,org.keycloak.testsuite.util.junit.AggregateResultsReporter
@@ -645,6 +645,13 @@
+
+
+ org.keycloak.testsuite
+ integration-arquillian-test-utils
+ ${project.version}
+
+
junit