diff options
| author | Lucas Bullen | 2018-07-17 18:21:35 +0000 |
|---|---|---|
| committer | Lucas Bullen | 2018-07-20 16:35:46 +0000 |
| commit | 21df6c58ad51eafc007fb99b53e71ba72d229839 (patch) | |
| tree | 4f0259960409d58eeed5490f3101c7b588c90121 | |
| parent | 399676240364eb7f00333803278c613b2eb42162 (diff) | |
| download | eclipse.platform.releng-21df6c58ad51eafc007fb99b53e71ba72d229839.tar.gz eclipse.platform.releng-21df6c58ad51eafc007fb99b53e71ba72d229839.tar.xz eclipse.platform.releng-21df6c58ad51eafc007fb99b53e71ba72d229839.zip | |
Bug 531057- 2nd Attempt JUnit 5 support for testsI20180720-2000
- 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>
6 files changed, 1060 insertions, 482 deletions
diff --git a/bundles/org.eclipse.test/META-INF/MANIFEST.MF b/bundles/org.eclipse.test/META-INF/MANIFEST.MF index d9d11e99..5b9f6073 100644 --- a/bundles/org.eclipse.test/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.test/META-INF/MANIFEST.MF @@ -7,11 +7,16 @@ Eclipse-BundleShape: dir 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 00000000..5d681aa0 --- /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 00000000..28fe5380 --- /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 1a6b77d7..a653ebee 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.InputStream; 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.graphics.ImageLoader; 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 @@ import junit.framework.TestResult; * 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 class EclipseTestRunner implements TestListener { */ 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 @@ public class EclipseTestRunner implements TestListener { 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 @@ public class EclipseTestRunner implements TestListener { 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 @@ public class EclipseTestRunner implements TestListener { 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 @@ public class EclipseTestRunner implements TestListener { // 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(); + + 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; + } - EclipseTestRunner runner = new EclipseTestRunner(t, testPluginName, haltError, haltFail); - transferFormatters(runner); - runner.run(); - return runner.getRetCode(); + 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 @@ public class EclipseTestRunner implements TestListener { // 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 @@ public class EclipseTestRunner implements TestListener { 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 class EclipseTestRunner implements TestListener { } } - 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 @@ public class EclipseTestRunner implements TestListener { 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 00000000..31e1da3c --- /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 966b0d04..62b12887 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 java.io.IOException; - -import junit.framework.Assert; +import static org.junit.jupiter.api.Assertions.assertNotNull; -import org.eclipse.equinox.app.IApplication; -import org.eclipse.equinox.app.IApplicationContext; +import java.io.IOException; 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 @@ import org.eclipse.ui.testing.TestableObject; /** * 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 @@ public class UITestApplication implements IPlatformRunnable, ITestHarness, IApp } return Integer.valueOf(fTestRunnerResult); } - - + + /* * return the application to run, or null if not even the default application * is found. @@ -77,11 +75,11 @@ public class UITestApplication implements IPlatformRunnable, ITestHarness, IApp // 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 @@ public class UITestApplication implements IPlatformRunnable, ITestHarness, IApp 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 @@ public class UITestApplication implements IPlatformRunnable, ITestHarness, IApp } 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 @@ public class UITestApplication implements IPlatformRunnable, ITestHarness, IApp } 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 @@ public class UITestApplication implements IPlatformRunnable, ITestHarness, IApp * 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 @@ public class UITestApplication implements IPlatformRunnable, ITestHarness, IApp @Override public void stop() { // TODO Auto-generated method stub - + } - + } |
