Bug 531057- 2nd Attempt JUnit 5 support for tests
- Changes
- Include currentThread classLoader in the PluginClassLoader to access
all required classes, This caused the formatter to be unable to print
results
- Removed infinite loop when invalid plugin is given
- Use the given formatter output path making the results in the right
location
- Include exception stack trace in the formatted xml output
Change-Id: I12239c5c18daab6bf79d0c4fd35190f8abfde386
Signed-off-by: Lucas Bullen <lbullen@redhat.com>
diff --git a/bundles/org.eclipse.test/META-INF/MANIFEST.MF b/bundles/org.eclipse.test/META-INF/MANIFEST.MF
index d9d11e9..5b9f607 100644
--- a/bundles/org.eclipse.test/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.test/META-INF/MANIFEST.MF
@@ -7,11 +7,16 @@
Bundle-Vendor: %providerName
Bundle-Localization: plugin
Require-Bundle: org.apache.ant,
- org.junit;bundle-version="4.12.0",
org.eclipse.ui,
org.eclipse.core.runtime,
org.eclipse.ui.ide.application,
- org.eclipse.equinox.app
+ org.eclipse.equinox.app,
+ org.junit.jupiter.api,
+ org.junit.jupiter.engine,
+ org.junit.platform.commons,
+ org.junit.platform.engine,
+ org.junit.platform.launcher,
+ org.junit.vintage.engine
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Export-Package: org.eclipse.test
diff --git a/bundles/org.eclipse.test/src/org/eclipse/test/AbstractJUnitResultFormatter.java b/bundles/org.eclipse.test/src/org/eclipse/test/AbstractJUnitResultFormatter.java
new file mode 100644
index 0000000..5d681aa
--- /dev/null
+++ b/bundles/org.eclipse.test/src/org/eclipse/test/AbstractJUnitResultFormatter.java
@@ -0,0 +1,307 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Red Hat Inc. and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Lucas Bullen (Red Hat Inc.) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.test;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.taskdefs.optional.junitlauncher.TestExecutionContext;
+import org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter;
+import org.apache.tools.ant.util.FileUtils;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+/**
+ * Contains some common behaviour that's used by our internal {@link TestResultFormatter}s
+ */
+abstract class AbstractJUnitResultFormatter implements TestResultFormatter {
+
+
+ protected static String NEW_LINE = System.getProperty("line.separator");
+ protected TestExecutionContext context;
+
+ private SysOutErrContentStore sysOutStore;
+ private SysOutErrContentStore sysErrStore;
+
+ @Override
+ public void sysOutAvailable(final byte[] data) {
+ if (this.sysOutStore == null) {
+ this.sysOutStore = new SysOutErrContentStore(true);
+ }
+ try {
+ this.sysOutStore.store(data);
+ } catch (IOException e) {
+ handleException(e);
+ return;
+ }
+ }
+
+ @Override
+ public void sysErrAvailable(final byte[] data) {
+ if (this.sysErrStore == null) {
+ this.sysErrStore = new SysOutErrContentStore(false);
+ }
+ try {
+ this.sysErrStore.store(data);
+ } catch (IOException e) {
+ handleException(e);
+ return;
+ }
+ }
+
+ @Override
+ public void setContext(final TestExecutionContext context) {
+ this.context = context;
+ }
+
+ /**
+ * @return Returns true if there's any stdout data, that was generated during the
+ * tests, is available for use. Else returns false.
+ */
+ boolean hasSysOut() {
+ return this.sysOutStore != null && this.sysOutStore.hasData();
+ }
+
+ /**
+ * @return Returns true if there's any stderr data, that was generated during the
+ * tests, is available for use. Else returns false.
+ */
+ boolean hasSysErr() {
+ return this.sysErrStore != null && this.sysErrStore.hasData();
+ }
+
+ /**
+ * @return Returns a {@link Reader} for reading any stdout data that was generated
+ * during the test execution. It is expected that the {@link #hasSysOut()} be first
+ * called to see if any such data is available and only if there is, then this method
+ * be called
+ * @throws IOException If there's any I/O problem while creating the {@link Reader}
+ */
+ Reader getSysOutReader() throws IOException {
+ return this.sysOutStore.getReader();
+ }
+
+ /**
+ * @return Returns a {@link Reader} for reading any stderr data that was generated
+ * during the test execution. It is expected that the {@link #hasSysErr()} be first
+ * called to see if any such data is available and only if there is, then this method
+ * be called
+ * @throws IOException If there's any I/O problem while creating the {@link Reader}
+ */
+ Reader getSysErrReader() throws IOException {
+ return this.sysErrStore.getReader();
+ }
+
+ /**
+ * Writes out any stdout data that was generated during the
+ * test execution. If there was no such data then this method just returns.
+ *
+ * @param writer The {@link Writer} to use. Cannot be null.
+ * @throws IOException If any I/O problem occurs during writing the data
+ */
+ void writeSysOut(final Writer writer) throws IOException {
+ Objects.requireNonNull(writer, "Writer cannot be null");
+ this.writeFrom(this.sysOutStore, writer);
+ }
+
+ /**
+ * Writes out any stderr data that was generated during the
+ * test execution. If there was no such data then this method just returns.
+ *
+ * @param writer The {@link Writer} to use. Cannot be null.
+ * @throws IOException If any I/O problem occurs during writing the data
+ */
+ void writeSysErr(final Writer writer) throws IOException {
+ Objects.requireNonNull(writer, "Writer cannot be null");
+ this.writeFrom(this.sysErrStore, writer);
+ }
+
+ static Optional<TestIdentifier> traverseAndFindTestClass(final TestPlan testPlan, final TestIdentifier testIdentifier) {
+ if (isTestClass(testIdentifier).isPresent()) {
+ return Optional.of(testIdentifier);
+ }
+ final Optional<TestIdentifier> parent = testPlan.getParent(testIdentifier);
+ return parent.isPresent() ? traverseAndFindTestClass(testPlan, parent.get()) : Optional.empty();
+ }
+
+ static Optional<ClassSource> isTestClass(final TestIdentifier testIdentifier) {
+ if (testIdentifier == null) {
+ return Optional.empty();
+ }
+ final Optional<TestSource> source = testIdentifier.getSource();
+ if (!source.isPresent()) {
+ return Optional.empty();
+ }
+ final TestSource testSource = source.get();
+ if (testSource instanceof ClassSource) {
+ return Optional.of((ClassSource) testSource);
+ }
+ return Optional.empty();
+ }
+
+ private void writeFrom(final SysOutErrContentStore store, final Writer writer) throws IOException {
+ final char[] chars = new char[1024];
+ int numRead = -1;
+ try (final Reader reader = store.getReader()) {
+ while ((numRead = reader.read(chars)) != -1) {
+ writer.write(chars, 0, numRead);
+ }
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ FileUtils.close(this.sysOutStore);
+ FileUtils.close(this.sysErrStore);
+ }
+
+ protected void handleException(final Throwable t) {
+ // we currently just log it and move on.
+ this.context.getProject().ifPresent((p) -> p.log("Exception in listener "
+ + AbstractJUnitResultFormatter.this.getClass().getName(), t, Project.MSG_DEBUG));
+ }
+
+
+ /*
+ A "store" for sysout/syserr content that gets sent to the AbstractJUnitResultFormatter.
+ This store first uses a relatively decent sized in-memory buffer for storing the sysout/syserr
+ content. This in-memory buffer will be used as long as it can fit in the new content that
+ keeps coming in. When the size limit is reached, this store switches to a file based store
+ by creating a temporarily file and writing out the already in-memory held buffer content
+ and any new content that keeps arriving to this store. Once the file has been created,
+ the in-memory buffer will never be used any more and in fact is destroyed as soon as the
+ file is created.
+ Instances of this class are not thread-safe and users of this class are expected to use necessary thread
+ safety guarantees, if they want to use an instance of this class by multiple threads.
+ */
+ private static final class SysOutErrContentStore implements Closeable {
+ private static final int DEFAULT_CAPACITY_IN_BYTES = 50 * 1024; // 50 KB
+ private static final Reader EMPTY_READER = new Reader() {
+ @Override
+ public int read(final char[] cbuf, final int off, final int len) throws IOException {
+ return -1;
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+ };
+
+ private final String tmpFileSuffix;
+ private ByteBuffer inMemoryStore = ByteBuffer.allocate(DEFAULT_CAPACITY_IN_BYTES);
+ private boolean usingFileStore = false;
+ private Path filePath;
+ private FileOutputStream fileOutputStream;
+
+ SysOutErrContentStore(final boolean isSysOut) {
+ this.tmpFileSuffix = isSysOut ? ".sysout" : ".syserr";
+ }
+
+ void store(final byte[] data) throws IOException {
+ if (this.usingFileStore) {
+ this.storeToFile(data, 0, data.length);
+ return;
+ }
+ // we haven't yet created a file store and the data can fit in memory,
+ // so we write it in our buffer
+ try {
+ this.inMemoryStore.put(data);
+ return;
+ } catch (BufferOverflowException boe) {
+ // the buffer capacity can't hold this incoming data, so this
+ // incoming data hasn't been transferred to the buffer. let's
+ // now fall back to a file store
+ this.usingFileStore = true;
+ }
+ // since the content couldn't be transferred into in-memory buffer,
+ // we now create a file and transfer already (previously) stored in-memory
+ // content into that file, before finally transferring this new content
+ // into the file too. We then finally discard this in-memory buffer and
+ // just keep using the file store instead
+ this.fileOutputStream = createFileStore();
+ // first the existing in-memory content
+ storeToFile(this.inMemoryStore.array(), 0, this.inMemoryStore.position());
+ storeToFile(data, 0, data.length);
+ // discard the in-memory store
+ this.inMemoryStore = null;
+ }
+
+ private void storeToFile(final byte[] data, final int offset, final int length) throws IOException {
+ if (this.fileOutputStream == null) {
+ // no backing file was created so we can't do anything
+ return;
+ }
+ this.fileOutputStream.write(data, offset, length);
+ }
+
+ private FileOutputStream createFileStore() throws IOException {
+ this.filePath = Files.createTempFile(null, this.tmpFileSuffix);
+ this.filePath.toFile().deleteOnExit();
+ return new FileOutputStream(this.filePath.toFile());
+ }
+
+ /*
+ * Returns a Reader for reading the sysout/syserr content. If there's no data
+ * available in this store, then this returns a Reader which when used for read operations,
+ * will immediately indicate an EOF.
+ */
+ Reader getReader() throws IOException {
+ if (this.usingFileStore && this.filePath != null) {
+ // we use a FileReader here so that we can use the system default character
+ // encoding for reading the contents on sysout/syserr stream, since that's the
+ // encoding that System.out/System.err uses to write out the messages
+ return new BufferedReader(new FileReader(this.filePath.toFile()));
+ }
+ if (this.inMemoryStore != null) {
+ return new InputStreamReader(new ByteArrayInputStream(this.inMemoryStore.array(), 0, this.inMemoryStore.position()));
+ }
+ // no data to read, so we return an "empty" reader
+ return EMPTY_READER;
+ }
+
+ /*
+ * Returns true if this store has any data (either in-memory or in a file). Else
+ * returns false.
+ */
+ boolean hasData() {
+ if (this.inMemoryStore != null && this.inMemoryStore.position() > 0) {
+ return true;
+ }
+ if (this.usingFileStore && this.filePath != null) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.inMemoryStore = null;
+ FileUtils.close(this.fileOutputStream);
+ FileUtils.delete(this.filePath.toFile());
+ }
+ }
+}
diff --git a/bundles/org.eclipse.test/src/org/eclipse/test/ClassLoaderTools.java b/bundles/org.eclipse.test/src/org/eclipse/test/ClassLoaderTools.java
new file mode 100644
index 0000000..28fe538
--- /dev/null
+++ b/bundles/org.eclipse.test/src/org/eclipse/test/ClassLoaderTools.java
@@ -0,0 +1,187 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Red Hat Inc. and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Lucas Bullen (Red Hat Inc.) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.test;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Vector;
+
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.osgi.internal.framework.EquinoxBundle;
+import org.osgi.framework.Bundle;
+
+@SuppressWarnings("restriction")
+class ClassLoaderTools {
+ public static ClassLoader getPluginClassLoader(String getfTestPluginName, ClassLoader currentTCCL) {
+ Bundle bundle = Platform.getBundle(getfTestPluginName);
+ if (bundle == null) {
+ throw new IllegalArgumentException("Bundle \"" + getfTestPluginName + "\" not found. Possible causes include missing dependencies, too restrictive version ranges, or a non-matching required execution environment."); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ return new TestBundleClassLoader(bundle, currentTCCL);
+ }
+
+ public static String getClassPlugin(String className) {
+ int index = className.lastIndexOf('.');
+ String plugin = null;
+ while (index != -1) {
+ plugin = className.substring(0, index);
+ if(Platform.getBundle(plugin) != null) {
+ break;
+ }
+ index = className.lastIndexOf('.', index-1);
+ }
+ return plugin;
+ }
+
+ public static ClassLoader getJUnit5Classloader(List<String> platformEngine) {
+ List<Bundle> platformEngineBundles = new ArrayList<>();
+ for (Iterator<String> iterator = platformEngine.iterator(); iterator.hasNext();) {
+ String string = iterator.next();
+ Bundle bundle = Platform.getBundle(string);
+ platformEngineBundles.add(bundle);
+ }
+ return new MultiBundleClassLoader(platformEngineBundles);
+ }
+
+ static class TestBundleClassLoader extends ClassLoader {
+ protected Bundle bundle;
+ protected ClassLoader currentTCCL;
+
+ public TestBundleClassLoader(Bundle target, ClassLoader currentTCCL) {
+ this.bundle = target;
+ this.currentTCCL = currentTCCL;
+ }
+
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ try {
+ return bundle.loadClass(name);
+ } catch (ClassNotFoundException e) {
+ return currentTCCL.loadClass(name);
+ }
+ }
+
+ @Override
+ protected URL findResource(String name) {
+ URL url = bundle.getResource(name);
+ if(url == null) {
+ url = currentTCCL.getResource(name);
+ }
+ return url;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ protected Enumeration findResources(String name) throws IOException {
+ Enumeration enumeration = bundle.getResources(name);
+ if(enumeration == null) {
+ enumeration = currentTCCL.getResources(name);
+ }
+ return enumeration;
+ }
+
+ @Override
+ public Enumeration<URL> getResources(String res) throws IOException {
+ Enumeration<URL> urls = currentTCCL.getResources(res);
+ if(urls.hasMoreElements())
+ return urls;
+
+ List<URL> resources = new ArrayList<>(6);
+ String location = null;
+ URL url = null;
+ if (bundle instanceof EquinoxBundle) {
+ location = ((EquinoxBundle) bundle).getLocation();
+ }
+ if (location != null && location.startsWith("reference:")) { //$NON-NLS-1$
+ location = location.substring(10, location.length());
+ URI uri = URI.create(location);
+ String newPath =( uri.getPath() == null ? "" : uri.getPath()) + "bin" + '/' + res; //$NON-NLS-1$
+ URI newUri = uri.resolve(newPath).normalize();
+ if(newUri.isAbsolute())
+ url = newUri.toURL();
+ }
+ if (url != null) {
+ File f = new File(url.getFile());
+ if (f.exists())
+ resources.add(url);
+ }
+ else
+ return Collections.emptyEnumeration();
+
+ return Collections.enumeration(resources);
+ }
+ }
+
+ static class MultiBundleClassLoader extends ClassLoader {
+ private List<Bundle> bundleList;
+
+ public MultiBundleClassLoader(List<Bundle> platformEngineBundles) {
+ this.bundleList = platformEngineBundles;
+
+ }
+ public Class<?> findClasss(String name) throws ClassNotFoundException {
+ return findClass(name);
+ }
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ Class<?> c = null;
+ for (Bundle temp : bundleList) {
+ try {
+ c = temp.loadClass(name);
+ if (c != null)
+ return c;
+ } catch (ClassNotFoundException e) {
+ }
+ }
+ return c;
+ }
+
+ @Override
+ protected URL findResource(String name) {
+ URL url = null;
+ for (Bundle temp : bundleList) {
+ url = temp.getResource(name);
+ if (url != null)
+ return url;
+ }
+ return url;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ protected Enumeration findResources(String name) throws IOException {
+ Enumeration enumFinal = null;
+ for (int i = 0; i < bundleList.size(); i++) {
+ if (i == 0) {
+ enumFinal = bundleList.get(i).getResources(name);
+ continue;
+ }
+ Enumeration e2 = bundleList.get(i).getResources(name);
+ Vector temp = new Vector();
+ while (enumFinal != null && enumFinal.hasMoreElements()) {
+ temp.add(enumFinal.nextElement());
+ }
+ while (e2 != null && e2.hasMoreElements()) {
+ temp.add(e2.nextElement());
+ }
+ enumFinal = temp.elements();
+ }
+ return enumFinal;
+ }
+ }
+}
diff --git a/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java b/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java
index 1a6b77d..a653ebe 100644
--- a/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java
+++ b/bundles/org.eclipse.test/src/org/eclipse/test/EclipseTestRunner.java
@@ -8,9 +8,12 @@
* Contributors:
* IBM Corporation - initial API and implementation
* Anthony Dahanne <anthony.dahanne@compuware.com> - enhance ETF to be able to launch several tests in several bundles - https://bugs.eclipse.org/330613
+ * Lucas Bullen (Red Hat Inc.) - JUnit 5 support
*******************************************************************************/
package org.eclipse.test;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -21,33 +24,31 @@
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
-import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Date;
-import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;
-import java.util.Vector;
-import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
-import org.apache.tools.ant.taskdefs.optional.junit.JUnitResultFormatter;
-import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest;
+import org.apache.tools.ant.taskdefs.optional.junitlauncher.TestExecutionContext;
import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
-import org.eclipse.osgi.util.ManifestElement;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.GC;
@@ -57,14 +58,16 @@
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleException;
-import org.osgi.framework.Constants;
-
-import junit.framework.AssertionFailedError;
-import junit.framework.Test;
-import junit.framework.TestListener;
-import junit.framework.TestResult;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.wiring.BundleWiring;
/**
* A TestRunner for JUnit that supports Ant JUnitResultFormatters and running
@@ -73,20 +76,7 @@
* formatter=org.apache.tools.ant.taskdefs.optional.junit
* .XMLJUnitResultFormatter
*/
-public class EclipseTestRunner implements TestListener {
- class TestFailedException extends Exception {
-
- private static final long serialVersionUID = 6009335074727417445L;
-
- TestFailedException(String message) {
- super(message);
- }
-
- TestFailedException(Throwable e) {
- super(e);
- }
- }
-
+public class EclipseTestRunner {
static class ThreadDump extends Exception {
private static final long serialVersionUID = 1L;
@@ -132,7 +122,6 @@
*/
public static final int ERRORS = 2;
- private static final String SUITE_METHODNAME = "suite";
/**
* SECONDS_BEFORE_TIMEOUT_BUFFER is the time we allow ourselves to take stack
* traces, get a screen shot, delay "SECONDS_BETWEEN_DUMPS", then do it again.
@@ -150,68 +139,20 @@
private static final int SECONDS_BETWEEN_DUMPS = 5;
/**
- * The current test result
- */
- private TestResult fTestResult;
- /**
- * The name of the plugin containing the test
- */
- private String fTestPluginName;
- /**
- * The corresponding testsuite.
- */
- private Test fSuite;
- /**
- * Formatters from the command line.
- */
- private static Vector<JUnitResultFormatter> fgFromCmdLine = new Vector<>();
- /**
- * Holds the registered formatters.
- */
- private Vector<JUnitResultFormatter> formatters = new Vector<>();
- /**
- * Do we stop on errors.
- */
- private boolean fHaltOnError = false;
- /**
- * Do we stop on test failures.
- */
- private boolean fHaltOnFailure = false;
- /**
- * The TestSuite we are currently running.
- */
- private JUnitTest fJunitTest;
- /**
- * output written during the test
- */
- private PrintStream fSystemError;
- /**
- * Error output during the test
- */
- private PrintStream fSystemOut;
- /**
- * Exception caught in constructor.
- */
- private Exception fException;
- /**
- * Returncode
- */
- private int fRetCode = SUCCESS;
-
- /**
* The main entry point (the parameters are not yet consistent with the Ant
* JUnitTestRunner, but eventually they should be). Parameters
*
* <pre>
- * -className: the name of the testSuite
- * -testPluginName: the name of the containing plugin
- * haltOnError: halt test on errors?
- * haltOnFailure: halt test on failures?
- * -testlistener listenerClass: deprecated
- * print a warning that this option is deprecated
- * formatter: a JUnitResultFormatter given as classname,filename.
- * If filename is ommitted, System.out is assumed.
+ * -className=<testSuiteName>
+ * -testPluginName<containingpluginName>
+ * -formatter=<classname>(,<path>)
* </pre>
+ * Where <classname> is the formatter classname, currently ignored as only
+ * LegacyXmlResultFormatter is used. The path is either the path to the
+ * result file and should include the file extension (xml) if a single test
+ * is being run or should be the path to the result directory where result
+ * files should be created if multiple tests are being run. If no path is
+ * given, the standard output is used.
*/
public static void main(String[] args) throws IOException {
System.exit(run(args));
@@ -222,13 +163,10 @@
String classesNames = null;
String testPluginName = null;
String testPluginsNames = null;
- String formatterString = null;
+ String resultPathString = null;
String timeoutString = null;
String junitReportOutput = null;
- boolean haltError = false;
- boolean haltFail = false;
-
Properties props = new Properties();
int startArgs = 0;
@@ -262,19 +200,19 @@
junitReportOutput = args[i + 1];
i++;
} else if (args[i].startsWith("haltOnError=")) {
- haltError = Project.toBoolean(args[i].substring(12));
+ System.err.println("The haltOnError option is no longer supported");
} else if (args[i].startsWith("haltOnFailure=")) {
- haltFail = Project.toBoolean(args[i].substring(14));
+ System.err.println("The haltOnFailure option is no longer supported");
} else if (args[i].startsWith("formatter=")) {
- formatterString = args[i].substring(10);
+ String formatterString = args[i].substring(10);
+ int seperatorIndex = formatterString.indexOf(',');
+ resultPathString = formatterString.substring(seperatorIndex + 1);
} else if (args[i].startsWith("propsfile=")) {
try (FileInputStream in = new FileInputStream(args[i].substring(10))) {
props.load(in);
}
} else if (args[i].equals("-testlistener")) {
- System.err
- .println("The -testlistener option is no longer supported\nuse the formatter= option instead");
- return ERRORS;
+ System.err.println("The testlistener option is no longer supported");
} else if (args[i].equals("-timeout")) {
if (i < args.length - 1)
timeoutString = args[i + 1];
@@ -307,44 +245,133 @@
// names
String[] testPlugins = testPluginsNames.split(",");
String[] suiteClasses = classesNames.split(",");
- try {
- createAndStoreFormatter(formatterString, suiteClasses);
- } catch (BuildException be) {
- System.err.println(be.getMessage());
- return ERRORS;
- }
int returnCode = 0;
int j = 0;
+ EclipseTestRunner runner = new EclipseTestRunner();
for (String oneClassName : suiteClasses) {
- JUnitTest t = new JUnitTest(oneClassName);
- t.setProperties(props);
- EclipseTestRunner runner = new EclipseTestRunner(t, testPlugins[j], haltError, haltFail);
- transferFormatters(runner, j);
- runner.run();
+ int result = runner.runTests(props, testPlugins[j], oneClassName, resultPathString, true);
j++;
- if (runner.getRetCode() != 0) {
- returnCode = runner.getRetCode();
+ if(result != 0) {
+ returnCode = result;
}
}
return returnCode;
}
- try {
- createAndStoreFormatter(formatterString);
- } catch (BuildException be) {
- System.err.println(be.getMessage());
- return ERRORS;
- }
if (className == null)
throw new IllegalArgumentException("Test class name not specified");
+ EclipseTestRunner runner = new EclipseTestRunner();
+ return runner.runTests(props, testPluginName, className, resultPathString, false);
+ }
- JUnitTest t = new JUnitTest(className);
+ private int runTests(Properties props, String testPluginName, String testClassName, String resultPath, boolean multiTest) {
+ ClassLoader currentTCCL = Thread.currentThread().getContextClassLoader();
+ ExecutionListener executionListener = new ExecutionListener();
+ if(testPluginName == null) {
+ testPluginName = ClassLoaderTools.getClassPlugin(testClassName);
+ }
+ if(testPluginName == null)
+ throw new IllegalArgumentException("Test class not found");
+ LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
+ .selectors(selectClass(testClassName))
+ .build();
- t.setProperties(props);
+ try {
+ Thread.currentThread().setContextClassLoader(ClassLoaderTools.getJUnit5Classloader(getPlatformEngines()));
+ final Launcher launcher = LauncherFactory.create();
- EclipseTestRunner runner = new EclipseTestRunner(t, testPluginName, haltError, haltFail);
- transferFormatters(runner);
- runner.run();
- return runner.getRetCode();
+ Thread.currentThread().setContextClassLoader(ClassLoaderTools.getPluginClassLoader(testPluginName, currentTCCL));
+ try(LegacyXmlResultFormatter legacyXmlResultFormatter = new LegacyXmlResultFormatter()){
+ try (OutputStream fileOutputStream = getResultOutputStream(resultPath,testClassName,multiTest)){
+ legacyXmlResultFormatter.setDestination(fileOutputStream);
+ legacyXmlResultFormatter.setContext(new ExecutionContext(props));
+ launcher.execute(request, legacyXmlResultFormatter, executionListener);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return ERRORS;
+ }
+ } finally {
+ Thread.currentThread().setContextClassLoader(currentTCCL);
+ }
+ return executionListener.didExecutionContainedFailures() ? FAILURES : SUCCESS;
+ }
+
+ private OutputStream getResultOutputStream(String resultPathString, String testClassName, boolean multiTest) throws IOException {
+ if(resultPathString == null)
+ return System.out;
+ File resultFile;
+ if(multiTest) {
+ Path resultDirectoryPath = new Path(resultPathString);
+ File testDirectory = resultDirectoryPath.toFile();
+ if(!testDirectory.exists())
+ testDirectory.mkdirs();
+ resultFile = resultDirectoryPath.append("TEST-"+testClassName+".xml").toFile();
+ }else {
+ resultFile = new Path(resultPathString).toFile();
+ File resultDirectory = resultFile.getParentFile();
+ if(!resultDirectory.exists())
+ resultDirectory.mkdirs();
+ }
+ if(!resultFile.exists()) {
+ resultFile.createNewFile();
+ }
+ return new FileOutputStream(resultFile);
+ }
+
+
+ private List<String> getPlatformEngines(){
+ List<String> platformEngines = new ArrayList<>();
+ Bundle bundle = FrameworkUtil.getBundle(getClass());
+ Bundle[] bundles = bundle.getBundleContext().getBundles();
+ for (Bundle iBundle : bundles) {
+ try {
+ BundleWiring bundleWiring = Platform.getBundle(iBundle.getSymbolicName()).adapt(BundleWiring.class);
+ Collection<String> listResources = bundleWiring.listResources("META-INF/services", "org.junit.platform.engine.TestEngine", BundleWiring.LISTRESOURCES_LOCAL);
+ if (!listResources.isEmpty())
+ platformEngines.add(iBundle.getSymbolicName());
+ } catch (Exception e) {
+ // check the next bundle
+ }
+ }
+ return platformEngines;
+ }
+
+ private final class ExecutionListener implements TestExecutionListener {
+ private boolean executionContainedFailures;
+
+ public ExecutionListener() {
+ this.executionContainedFailures = false;
+ }
+
+ public boolean didExecutionContainedFailures() {
+ return executionContainedFailures;
+ }
+
+ @Override
+ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
+ if(testExecutionResult.getStatus() == org.junit.platform.engine.TestExecutionResult.Status.FAILED) {
+ executionContainedFailures = true;
+ }
+ }
+ }
+
+ private final class ExecutionContext implements TestExecutionContext {
+
+ private final Properties props;
+
+ ExecutionContext(Properties props) {
+ this.props = props;
+ }
+
+ @Override
+ public Properties getProperties() {
+ return this.props;
+ }
+
+ @Override
+ public Optional<Project> getProject() {
+ return null;
+ }
}
/**
@@ -404,7 +431,7 @@
// Dump all stacks:
dumpStackTraces(num, System.err);
dumpStackTraces(num, System.out); // System.err could be blocked, see
- // https://bugs.eclipse.org/506304
+ // https://bugs.eclipse.org/506304
logStackTraces(num); // make this available in the log, see bug 533367
if (!dumpSwtDisplay(num)) {
@@ -495,7 +522,7 @@
dumpDisplayState(System.err);
dumpDisplayState(System.out); // System.err could be blocked, see
- // https://bugs.eclipse.org/506304
+ // https://bugs.eclipse.org/506304
// Take a screenshot:
GC gc = new GC(display);
@@ -552,307 +579,6 @@
}
}
- public EclipseTestRunner(JUnitTest test, String testPluginName, boolean haltOnError, boolean haltOnFailure) {
- fJunitTest = test;
- fTestPluginName = testPluginName;
- fHaltOnError = haltOnError;
- fHaltOnFailure = haltOnFailure;
-
- try {
- fSuite = getTest(test.getName());
- } catch (Exception e) {
- fRetCode = ERRORS;
- fException = e;
- }
- }
-
- protected Test getTest(String suiteClassName) throws TestFailedException {
- if (suiteClassName.isEmpty()) {
- clearStatus();
- return null;
- }
- Class<?> testClass = null;
- try {
- testClass = loadSuiteClass(suiteClassName);
- } catch (ClassNotFoundException e) {
- if (e.getCause() != null) {
- runFailed(e.getCause());
- }
- String clazz = e.getMessage();
- if (clazz == null)
- clazz = suiteClassName;
- runFailed("Class not found \"" + clazz + "\"");
- return null;
- } catch (Exception e) {
- runFailed(e);
- return null;
- }
- Method suiteMethod = null;
- try {
- suiteMethod = testClass.getMethod(SUITE_METHODNAME);
- } catch (Exception e) {
- // try to extract a test suite automatically
- clearStatus();
- return new junit.framework.JUnit4TestAdapter(testClass);
- }
- if (!Modifier.isStatic(suiteMethod.getModifiers())) {
- runFailed("suite() method must be static");
- return null;
- }
- Test test = null;
- try {
- test = (Test) suiteMethod.invoke(null); // static method
- if (test == null)
- return test;
- } catch (InvocationTargetException e) {
- runFailed("Failed to invoke suite():" + e.getTargetException().toString());
- return null;
- } catch (IllegalAccessException e) {
- runFailed("Failed to invoke suite():" + e.toString());
- return null;
- }
- clearStatus();
- return test;
- }
-
- protected void runFailed(String message) throws TestFailedException {
- System.err.println(message);
- throw new TestFailedException(message);
- }
-
- protected void runFailed(Throwable e) throws TestFailedException {
- e.printStackTrace();
- throw new TestFailedException(e);
- }
-
- protected void clearStatus() {
- }
-
- /**
- * Loads the class either with the system class loader or a plugin class loader
- * if a plugin name was specified
- */
- protected Class<?> loadSuiteClass(String suiteClassName) throws ClassNotFoundException {
- if (fTestPluginName == null)
- return Class.forName(suiteClassName);
- Bundle bundle = Platform.getBundle(fTestPluginName);
- if (bundle == null) {
- throw new ClassNotFoundException(suiteClassName,
- new Exception("Could not find plugin \"" + fTestPluginName + "\""));
- }
-
- // is the plugin a fragment?
- Dictionary<String, String> headers = bundle.getHeaders();
- String hostHeader = headers.get(Constants.FRAGMENT_HOST);
- if (hostHeader != null) {
- // we are a fragment for sure
- // we need to find which is our host
- ManifestElement[] hostElement = null;
- try {
- hostElement = ManifestElement.parseHeader(Constants.FRAGMENT_HOST, hostHeader);
- } catch (BundleException e) {
- throw new RuntimeException("Could not find host for fragment:" + fTestPluginName, e);
- }
- Bundle host = Platform.getBundle(hostElement[0].getValue());
- // we really want to get the host not the fragment
- bundle = host;
- }
-
- return bundle.loadClass(suiteClassName);
- }
-
- public void run() {
- fTestResult = new TestResult();
- fTestResult.addListener(this);
- for (int i = 0; i < formatters.size(); i++) {
- fTestResult.addListener(formatters.elementAt(i));
- }
-
- long start = System.currentTimeMillis();
- fireStartTestSuite();
-
- if (fException != null) { // had an exception in the constructor
- for (int i = 0; i < formatters.size(); i++) {
- formatters.elementAt(i).addError(null, fException);
- }
- fJunitTest.setCounts(1, 0, 1);
- fJunitTest.setRunTime(0);
- } else {
- ByteArrayOutputStream errStrm = new ByteArrayOutputStream();
- fSystemError = new PrintStream(errStrm);
-
- ByteArrayOutputStream outStrm = new ByteArrayOutputStream();
- fSystemOut = new PrintStream(outStrm);
-
- try {
- // pm.snapshot(1); // before
- fSuite.run(fTestResult);
- } finally {
- // pm.snapshot(2); // after
- fSystemError.close();
- fSystemError = null;
- fSystemOut.close();
- fSystemOut = null;
- sendOutAndErr(new String(outStrm.toByteArray()), new String(errStrm.toByteArray()));
- fJunitTest.setCounts(fTestResult.runCount(), fTestResult.failureCount(), fTestResult.errorCount());
- fJunitTest.setRunTime(System.currentTimeMillis() - start);
- }
- }
- fireEndTestSuite();
-
- if (fRetCode != SUCCESS || fTestResult.errorCount() != 0) {
- fRetCode = ERRORS;
- } else if (fTestResult.failureCount() != 0) {
- fRetCode = FAILURES;
- }
-
- // pm.upload(getClass().getName());
- }
-
- /**
- * Returns what System.exit() would return in the standalone version.
- *
- * @return 2 if errors occurred, 1 if tests failed else 0.
- */
- public int getRetCode() {
- return fRetCode;
- }
-
- @Override
- public void startTest(Test t) {
- }
-
- @Override
- public void endTest(Test test) {
- }
-
- @Override
- public void addFailure(Test test, AssertionFailedError t) {
- if (fHaltOnFailure) {
- fTestResult.stop();
- }
- }
-
- @Override
- public void addError(Test test, Throwable t) {
- if (fHaltOnError) {
- fTestResult.stop();
- }
- }
-
- private void fireStartTestSuite() {
- for (int i = 0; i < formatters.size(); i++) {
- formatters.elementAt(i).startTestSuite(fJunitTest);
- }
- }
-
- private void fireEndTestSuite() {
- for (int i = 0; i < formatters.size(); i++) {
- formatters.elementAt(i).endTestSuite(fJunitTest);
- }
- }
-
- public void addFormatter(JUnitResultFormatter f) {
- formatters.addElement(f);
- }
-
- /**
- * Line format is: formatter=<classname>(,<pathname>)?
- */
- private static void createAndStoreFormatter(String line) throws BuildException {
- String formatterClassName = null;
- File formatterFile = null;
-
- int pos = line.indexOf(',');
- if (pos == -1) {
- formatterClassName = line;
- } else {
- formatterClassName = line.substring(0, pos);
- formatterFile = new File(line.substring(pos + 1)); // the method is
- // package
- // visible
- }
- fgFromCmdLine.addElement(createFormatter(formatterClassName, formatterFile));
- }
-
- /**
- * Line format is: formatter=<pathname>
- */
- private static void createAndStoreFormatter(String line, String... suiteClassesNames) throws BuildException {
- String formatterClassName = null;
- File formatterFile = null;
-
- int pos = line.indexOf(',');
- if (pos == -1) {
- formatterClassName = line;
- } else {
- formatterClassName = line.substring(0, pos);
- }
- File outputDirectory = new File(line.substring(pos + 1));
- outputDirectory.mkdir();
- for (String suiteClassName : suiteClassesNames) {
-
- String pathname = "TEST-" + suiteClassName + ".xml";
- if (outputDirectory.exists()) {
- pathname = outputDirectory.getAbsolutePath() + "/" + pathname;
- }
- formatterFile = new File(pathname);
- fgFromCmdLine.addElement(createFormatter(formatterClassName, formatterFile));
- }
-
- }
-
- private static void transferFormatters(EclipseTestRunner runner, int j) {
- runner.addFormatter(fgFromCmdLine.elementAt(j));
- }
-
- private static void transferFormatters(EclipseTestRunner runner) {
- for (int i = 0; i < fgFromCmdLine.size(); i++) {
- runner.addFormatter(fgFromCmdLine.elementAt(i));
- }
- }
-
- /*
- * DUPLICATED from FormatterElement, since it is package visible only
- */
- private static JUnitResultFormatter createFormatter(String classname, File outfile) throws BuildException {
- OutputStream out = System.out;
-
- if (classname == null) {
- throw new BuildException("you must specify type or classname");
- }
- Class<?> f = null;
- try {
- f = EclipseTestRunner.class.getClassLoader().loadClass(classname);
- } catch (ClassNotFoundException e) {
- throw new BuildException(e);
- }
-
- Object o = null;
- try {
- o = f.getDeclaredConstructor().newInstance();
- } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
- | NoSuchMethodException | SecurityException e) {
- throw new BuildException(e);
- }
-
- if (!(o instanceof JUnitResultFormatter)) {
- throw new BuildException(classname + " is not a JUnitResultFormatter");
- }
-
- JUnitResultFormatter r = (JUnitResultFormatter) o;
-
- if (outfile != null) {
- try {
- out = new FileOutputStream(outfile);
- } catch (java.io.IOException e) {
- throw new BuildException(e);
- }
- }
- r.setOutput(out);
- return r;
- }
-
public static void dumpAwtScreenshot(String screenshotFile) {
try {
URL location = AwtScreenshot.class.getProtectionDomain().getCodeSource().getLocation();
@@ -896,23 +622,4 @@
e.printStackTrace();
}
}
-
- private void sendOutAndErr(String out, String err) {
- for (JUnitResultFormatter formatter : formatters) {
- formatter.setSystemOutput(out);
- formatter.setSystemError(err);
- }
- }
-
- protected void handleOutput(String line) {
- if (fSystemOut != null) {
- fSystemOut.println(line);
- }
- }
-
- protected void handleErrorOutput(String line) {
- if (fSystemError != null) {
- fSystemError.println(line);
- }
- }
}
diff --git a/bundles/org.eclipse.test/src/org/eclipse/test/LegacyXmlResultFormatter.java b/bundles/org.eclipse.test/src/org/eclipse/test/LegacyXmlResultFormatter.java
new file mode 100644
index 0000000..31e1da3
--- /dev/null
+++ b/bundles/org.eclipse.test/src/org/eclipse/test/LegacyXmlResultFormatter.java
@@ -0,0 +1,374 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Red Hat Inc. and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Lucas Bullen (Red Hat Inc.) - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.test;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.util.Date;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
+import org.apache.tools.ant.taskdefs.optional.junitlauncher.TestResultFormatter;
+import org.apache.tools.ant.util.DOMElementWriter;
+import org.apache.tools.ant.util.DateUtils;
+import org.junit.platform.commons.util.ExceptionUtils;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+/**
+ * A {@link TestResultFormatter} which generates an XML report of the tests. The generated XML reports
+ * conforms to the schema of the XML that was generated by the {@code junit} task's XML
+ * report formatter and can be used by the {@code junitreport} task
+ */
+public class LegacyXmlResultFormatter extends AbstractJUnitResultFormatter {
+
+ private static final double ONE_SECOND = 1000.0;
+
+ OutputStream outputStream;
+ final Map<TestIdentifier, Stats> testIds = new ConcurrentHashMap<>();
+ final Map<TestIdentifier, Optional<String>> skipped = new ConcurrentHashMap<>();
+ final Map<TestIdentifier, Optional<Throwable>> failed = new ConcurrentHashMap<>();
+ final Map<TestIdentifier, Optional<Throwable>> aborted = new ConcurrentHashMap<>();
+
+ TestPlan testPlan;
+ long testPlanStartedAt = -1;
+ long testPlanEndedAt = -1;
+ final AtomicLong numTestsRun = new AtomicLong(0);
+ final AtomicLong numTestsFailed = new AtomicLong(0);
+ final AtomicLong numTestsSkipped = new AtomicLong(0);
+ final AtomicLong numTestsAborted = new AtomicLong(0);
+
+
+ @Override
+ public void testPlanExecutionStarted(final TestPlan plan) {
+ this.testPlan = plan;
+ this.testPlanStartedAt = System.currentTimeMillis();
+ }
+
+ @Override
+ public void testPlanExecutionFinished(final TestPlan plan) {
+ this.testPlanEndedAt = System.currentTimeMillis();
+ // format and print out the result
+ try {
+ new XMLReportWriter().write();
+ } catch (IOException | XMLStreamException e) {
+ handleException(e);
+ return;
+ }
+ }
+
+ @Override
+ public void dynamicTestRegistered(final TestIdentifier testIdentifier) {
+ // nothing to do
+ }
+
+ @Override
+ public void executionSkipped(final TestIdentifier testIdentifier, final String reason) {
+ final long currentTime = System.currentTimeMillis();
+ this.numTestsSkipped.incrementAndGet();
+ this.skipped.put(testIdentifier, Optional.ofNullable(reason));
+ // a skipped test is considered started and ended now
+ final Stats stats = new Stats(testIdentifier, currentTime);
+ stats.endedAt = currentTime;
+ this.testIds.put(testIdentifier, stats);
+ }
+
+ @Override
+ public void executionStarted(final TestIdentifier testIdentifier) {
+ final long currentTime = System.currentTimeMillis();
+ this.testIds.putIfAbsent(testIdentifier, new Stats(testIdentifier, currentTime));
+ if (testIdentifier.isTest()) {
+ this.numTestsRun.incrementAndGet();
+ }
+ }
+
+ @Override
+ public void executionFinished(final TestIdentifier testIdentifier, final TestExecutionResult testExecutionResult) {
+ final long currentTime = System.currentTimeMillis();
+ final Stats stats = this.testIds.get(testIdentifier);
+ if (stats != null) {
+ stats.endedAt = currentTime;
+ }
+ switch (testExecutionResult.getStatus()) {
+ case SUCCESSFUL: {
+ break;
+ }
+ case ABORTED: {
+ this.numTestsAborted.incrementAndGet();
+ this.aborted.put(testIdentifier, testExecutionResult.getThrowable());
+ break;
+ }
+ case FAILED: {
+ this.numTestsFailed.incrementAndGet();
+ this.failed.put(testIdentifier, testExecutionResult.getThrowable());
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void reportingEntryPublished(final TestIdentifier testIdentifier, final ReportEntry entry) {
+ // nothing to do
+ }
+
+ @Override
+ public void setDestination(final OutputStream os) {
+ this.outputStream = os;
+ }
+
+ private final class Stats {
+ final long startedAt;
+ long endedAt;
+
+ Stats(final TestIdentifier testIdentifier, final long startedAt) {
+ this.startedAt = startedAt;
+ }
+ }
+
+ private final class XMLReportWriter {
+
+ private static final String ELEM_TESTSUITE = "testsuite";
+ private static final String ELEM_PROPERTIES = "properties";
+ private static final String ELEM_PROPERTY = "property";
+ private static final String ELEM_TESTCASE = "testcase";
+ private static final String ELEM_SKIPPED = "skipped";
+ private static final String ELEM_FAILURE = "failure";
+ private static final String ELEM_ABORTED = "aborted";
+ private static final String ELEM_SYSTEM_OUT = "system-out";
+ private static final String ELEM_SYSTEM_ERR = "system-err";
+
+
+ private static final String ATTR_CLASSNAME = "classname";
+ private static final String ATTR_NAME = "name";
+ private static final String ATTR_VALUE = "value";
+ private static final String ATTR_TIME = "time";
+ private static final String ATTR_TIMESTAMP = "timestamp";
+ private static final String ATTR_NUM_ABORTED = "aborted";
+ private static final String ATTR_NUM_FAILURES = "failures";
+ private static final String ATTR_NUM_TESTS = "tests";
+ private static final String ATTR_NUM_SKIPPED = "skipped";
+ private static final String ATTR_MESSAGE = "message";
+ private static final String ATTR_TYPE = "type";
+
+ public XMLReportWriter() {
+ // TODO Auto-generated constructor stub
+ }
+
+ void write() throws XMLStreamException, IOException {
+ final XMLStreamWriter writer = XMLOutputFactory.newFactory().createXMLStreamWriter(outputStream, "UTF-8");
+ try {
+ writer.writeStartDocument();
+ writeTestSuite(writer);
+ writer.writeEndDocument();
+ } finally {
+ writer.close();
+ }
+ }
+
+ void writeTestSuite(final XMLStreamWriter writer) throws XMLStreamException, IOException {
+ // write the testsuite element
+ writer.writeStartElement(ELEM_TESTSUITE);
+ final String testsuiteName = determineTestSuiteName();
+ writer.writeAttribute(ATTR_NAME, testsuiteName);
+ // time taken for the tests execution
+ writer.writeAttribute(ATTR_TIME, String.valueOf((testPlanEndedAt - testPlanStartedAt) / ONE_SECOND));
+ // add the timestamp of report generation
+ final String timestamp = DateUtils.format(new Date(), DateUtils.ISO8601_DATETIME_PATTERN);
+ writer.writeAttribute(ATTR_TIMESTAMP, timestamp);
+ writer.writeAttribute(ATTR_NUM_TESTS, String.valueOf(numTestsRun.longValue()));
+ writer.writeAttribute(ATTR_NUM_FAILURES, String.valueOf(numTestsFailed.longValue()));
+ writer.writeAttribute(ATTR_NUM_SKIPPED, String.valueOf(numTestsSkipped.longValue()));
+ writer.writeAttribute(ATTR_NUM_ABORTED, String.valueOf(numTestsAborted.longValue()));
+
+ // write the properties
+ writeProperties(writer);
+ // write the tests
+ writeTestCase(writer);
+ writeSysOut(writer);
+ writeSysErr(writer);
+ // end the testsuite
+ writer.writeEndElement();
+ }
+
+ void writeProperties(final XMLStreamWriter writer) throws XMLStreamException {
+ final Properties properties = LegacyXmlResultFormatter.this.context.getProperties();
+ if (properties == null || properties.isEmpty()) {
+ return;
+ }
+ writer.writeStartElement(ELEM_PROPERTIES);
+ for (final String prop : properties.stringPropertyNames()) {
+ writer.writeStartElement(ELEM_PROPERTY);
+ writer.writeAttribute(ATTR_NAME, prop);
+ writer.writeAttribute(ATTR_VALUE, properties.getProperty(prop));
+ writer.writeEndElement();
+ }
+ writer.writeEndElement();
+ }
+
+ void writeTestCase(final XMLStreamWriter writer) throws XMLStreamException {
+ for (final Map.Entry<TestIdentifier, Stats> entry : testIds.entrySet()) {
+ final TestIdentifier testId = entry.getKey();
+ if (!testId.isTest()) {
+ // only interested in test methods
+ continue;
+ }
+ // find the parent class of this test method
+ final Optional<TestIdentifier> parent = testPlan.getParent(testId);
+ if (!parent.isPresent()) {
+ continue;
+ }
+ final String classname = parent.get().getLegacyReportingName();
+ writer.writeStartElement(ELEM_TESTCASE);
+ writer.writeAttribute(ATTR_CLASSNAME, classname);
+ writer.writeAttribute(ATTR_NAME, testId.getDisplayName());
+ final Stats stats = entry.getValue();
+ writer.writeAttribute(ATTR_TIME, String.valueOf((stats.endedAt - stats.startedAt) / ONE_SECOND));
+ // skipped element if the test was skipped
+ writeSkipped(writer, testId);
+ // failed element if the test failed
+ writeFailed(writer, testId);
+ // aborted element if the test was aborted
+ writeAborted(writer, testId);
+
+ writer.writeEndElement();
+ }
+ }
+
+ private void writeSkipped(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
+ if (!skipped.containsKey(testIdentifier)) {
+ return;
+ }
+ writer.writeStartElement(ELEM_SKIPPED);
+ final Optional<String> reason = skipped.get(testIdentifier);
+ if (reason.isPresent()) {
+ writer.writeAttribute(ATTR_MESSAGE, reason.get());
+ }
+ writer.writeEndElement();
+ }
+
+ private void writeFailed(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
+ if (!failed.containsKey(testIdentifier)) {
+ return;
+ }
+ writer.writeStartElement(ELEM_FAILURE);
+ final Optional<Throwable> cause = failed.get(testIdentifier);
+ if (cause.isPresent()) {
+ final Throwable t = cause.get();
+ final String message = t.getMessage();
+ if (message != null && !message.trim().isEmpty()) {
+ writer.writeAttribute(ATTR_MESSAGE, message);
+ }
+ writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
+ writer.writeCharacters(ExceptionUtils.readStackTrace(t));
+ }
+ writer.writeEndElement();
+ }
+
+ private void writeAborted(final XMLStreamWriter writer, final TestIdentifier testIdentifier) throws XMLStreamException {
+ if (!aborted.containsKey(testIdentifier)) {
+ return;
+ }
+ writer.writeStartElement(ELEM_ABORTED);
+ final Optional<Throwable> cause = aborted.get(testIdentifier);
+ if (cause.isPresent()) {
+ final Throwable t = cause.get();
+ final String message = t.getMessage();
+ if (message != null && !message.trim().isEmpty()) {
+ writer.writeAttribute(ATTR_MESSAGE, message);
+ }
+ writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
+ }
+ writer.writeEndElement();
+ }
+
+ private void writeSysOut(final XMLStreamWriter writer) throws XMLStreamException, IOException {
+ if (!LegacyXmlResultFormatter.this.hasSysOut()) {
+ return;
+ }
+ writer.writeStartElement(ELEM_SYSTEM_OUT);
+ try (final Reader reader = LegacyXmlResultFormatter.this.getSysOutReader()) {
+ writeCharactersFrom(reader, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ private void writeSysErr(final XMLStreamWriter writer) throws XMLStreamException, IOException {
+ if (!LegacyXmlResultFormatter.this.hasSysErr()) {
+ return;
+ }
+ writer.writeStartElement(ELEM_SYSTEM_ERR);
+ try (final Reader reader = LegacyXmlResultFormatter.this.getSysErrReader()) {
+ writeCharactersFrom(reader, writer);
+ }
+ writer.writeEndElement();
+ }
+
+ private void writeCharactersFrom(final Reader reader, final XMLStreamWriter writer) throws IOException, XMLStreamException {
+ final char[] chars = new char[1024];
+ int numRead = -1;
+ while ((numRead = reader.read(chars)) != -1) {
+ // although it's called a DOMElementWriter, the encode method is just a
+ // straight forward XML util method which doesn't concern about whether
+ // DOM, SAX, StAX semantics.
+ // TODO: Perhaps make it a static method
+ final String encoded = new DOMElementWriter().encode(new String(chars, 0, numRead));
+ writer.writeCharacters(encoded);
+ }
+ }
+
+ private String determineTestSuiteName() {
+ // this is really a hack to try and match the expectations of the XML report in JUnit4.x
+ // world. In JUnit5, the TestPlan doesn't have a name and a TestPlan (for which this is a
+ // listener) can have numerous tests within it
+ final Set<TestIdentifier> roots = testPlan.getRoots();
+ if (roots.isEmpty()) {
+ return "UNKNOWN";
+ }
+ for (final TestIdentifier root : roots) {
+ final Optional<ClassSource> classSource = findFirstClassSource(root);
+ if (classSource.isPresent()) {
+ return classSource.get().getClassName();
+ }
+ }
+ return "UNKNOWN";
+ }
+
+ private Optional<ClassSource> findFirstClassSource(final TestIdentifier root) {
+ if (root.getSource().isPresent()) {
+ final TestSource source = root.getSource().get();
+ if (source instanceof ClassSource) {
+ return Optional.of((ClassSource) source);
+ }
+ }
+ for (final TestIdentifier child : testPlan.getChildren(root)) {
+ final Optional<ClassSource> classSource = findFirstClassSource(child);
+ if (classSource.isPresent()) {
+ return classSource;
+ }
+ }
+ return Optional.empty();
+ }
+ }
+
+}
diff --git a/bundles/org.eclipse.test/src/org/eclipse/test/UITestApplication.java b/bundles/org.eclipse.test/src/org/eclipse/test/UITestApplication.java
index 966b0d0..62b1288 100644
--- a/bundles/org.eclipse.test/src/org/eclipse/test/UITestApplication.java
+++ b/bundles/org.eclipse.test/src/org/eclipse/test/UITestApplication.java
@@ -10,19 +10,17 @@
*******************************************************************************/
package org.eclipse.test;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
import java.io.IOException;
-import junit.framework.Assert;
-
-import org.eclipse.equinox.app.IApplication;
-import org.eclipse.equinox.app.IApplicationContext;
-
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IPlatformRunnable;
import org.eclipse.core.runtime.Platform;
-
+import org.eclipse.equinox.app.IApplication;
+import org.eclipse.equinox.app.IApplicationContext;
import org.eclipse.ui.IWindowListener;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
@@ -32,27 +30,27 @@
/**
* A Workbench that runs a test suite specified in the command line arguments.
- *
+ *
* @deprecated As using deprecated materials
- */
+ */
@Deprecated
public class UITestApplication implements IPlatformRunnable, ITestHarness, IApplication {
private static final String DEFAULT_APP_3_0 = "org.eclipse.ui.ide.workbench"; //$NON-NLS-1$
private static final String DEFAULT_APP_PRE_3_0 = "org.eclipse.ui.workbench"; //$NON-NLS-1$
-
+
private boolean fInDeprecatedMode = false;
private TestableObject fTestableObject;
int fTestRunnerResult = -1;
private IApplicationContext appContext;
-
-
+
+
@Override
public Object run(final Object args) throws Exception {
// Get the application to test
Object application = getApplication((String[])args);
- Assert.assertNotNull(application);
-
+ assertNotNull(application);
+
Object result;
if (fInDeprecatedMode && (application instanceof IPlatformRunnable)) {
result = runDeprecatedApplication((IPlatformRunnable)application, args);
@@ -65,8 +63,8 @@
}
return Integer.valueOf(fTestRunnerResult);
}
-
-
+
+
/*
* return the application to run, or null if not even the default application
* is found.
@@ -77,11 +75,11 @@
// If no application is specified, the 3.0 default workbench application
// is returned.
IExtension extension =
- Platform.getExtensionRegistry().getExtension(
- Platform.PI_RUNTIME,
- Platform.PT_APPLICATIONS,
- getApplicationToRun(args));
-
+ Platform.getExtensionRegistry().getExtension(
+ Platform.PI_RUNTIME,
+ Platform.PT_APPLICATIONS,
+ getApplicationToRun(args));
+
// If no 3.0 extension can be found, search the registry
// for the pre-3.0 default workbench application, i.e. org.eclipse ui.workbench
// Set the deprecated flag to true
@@ -92,9 +90,9 @@
DEFAULT_APP_PRE_3_0);
fInDeprecatedMode = true;
}
-
- Assert.assertNotNull(extension);
-
+
+ assertNotNull(extension);
+
// If the extension does not have the correct grammar, return null.
// Otherwise, return the application object.
IConfigurationElement[] elements = extension.getConfigurationElements();
@@ -110,13 +108,13 @@
}
return null;
}
-
+
/**
* The -testApplication argument specifies the application to be run.
* If the PDE JUnit launcher did not set this argument, then return
* the name of the default application.
* In 3.0, the default is the "org.eclipse.ui.ide.worbench" application.
- *
+ *
*/
private String getApplicationToRun(String[] args) {
for (int i = 0; i < args.length; i++) {
@@ -125,21 +123,21 @@
}
return DEFAULT_APP_3_0;
}
-
+
/**
* In 3.0 mode
- *
+ *
*/
private Object runApplication(Object application, Object args) throws Exception {
fTestableObject = PlatformUI.getTestableObject();
fTestableObject.setTestHarness(this);
if (application instanceof IPlatformRunnable) {
return ((IPlatformRunnable) application).run(args);
- }
+ }
return ((IApplication) application).start(appContext);
-
+
}
-
+
/*
* If we are in pre-3.0 mode, then the application to run is
* "org.eclipse.ui.workbench" Therefore, we safely cast the runnable object
@@ -148,11 +146,11 @@
* done, we explicitly call close() on the workbench.
*/
private Object runDeprecatedApplication(
- IPlatformRunnable object,
- final Object args)
- throws Exception {
+ IPlatformRunnable object,
+ final Object args)
+ throws Exception {
- Assert.assertTrue(object instanceof IWorkbench);
+ assertNotNull(object instanceof IWorkbench);
final IWorkbench workbench = (IWorkbench) object;
// the 'started' flag is used so that we only run tests when the window
@@ -214,8 +212,8 @@
@Override
public void stop() {
// TODO Auto-generated method stub
-
+
}
-
+
}