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=&lt;testSuiteName&gt;
+	 * -testPluginName&lt;containingpluginName&gt;
+	 * -formatter=&lt;classname&gt;(,&lt;path&gt;)
 	 * </pre>
+	 * Where &lt;classname&gt; 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
-		
+
 	}
-	
+
 }