From 8e7e21afedbb83a1952a9c0a157e2b4b06219dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Mon, 26 Feb 2024 13:25:53 +0100 Subject: [PATCH 1/3] Trace test execution directories in the remote maven execution Currently running tests in maven via m2e is possible but not very convenient as one still needs to navigate to the results then open the correct file. Another pitfall is that even if one has opened the file once, the "classic" JUnit view not update the view even when the file changes afterwards. This now adds a new process tracking of test executions directories that then can be watched on the m2e side and display the new advanced JUnit view when the run has finished. --- .../META-INF/MANIFEST.MF | 4 +- org.eclipse.m2e.launching/plugin.xml | 7 + .../eclipse/m2e/actions/ExecutePomAction.java | 7 +- .../launch/MavenBuildConnectionProcess.java | 160 +++++++++++++++ .../MavenBuildProjectDataConnection.java | 74 ++++--- .../launch/MavenConsoleLineTracker.java | 2 +- .../internal/launch/MavenLaunchDelegate.java | 12 +- .../launch/testing/MavenTestRunnerClient.java | 189 ++++++++++++++++++ .../launch/testing/MavenTestViewSupport.java | 65 ++++++ org.eclipse.m2e.maven.runtime/pom.xml | 2 +- .../listener/M2EMavenBuildDataBridge.java | 132 +++++------- .../internal/maven/listener/M2eEventSpy.java | 71 +++++++ .../listener/M2eMojoExecutionListener.java | 148 ++++++++++++++ .../maven/listener/MavenBuildListener.java | 27 +++ .../maven/listener/MavenProjectBuildData.java | 79 ++++++++ .../maven/listener/MavenTestEvent.java | 43 ++++ pom.xml | 2 +- target-platform/target-platform.target | 1 + 18 files changed, 895 insertions(+), 130 deletions(-) create mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java create mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java create mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestViewSupport.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2eEventSpy.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2eMojoExecutionListener.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildListener.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java create mode 100644 org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenTestEvent.java diff --git a/org.eclipse.m2e.launching/META-INF/MANIFEST.MF b/org.eclipse.m2e.launching/META-INF/MANIFEST.MF index 523d712280..b8ee041a76 100644 --- a/org.eclipse.m2e.launching/META-INF/MANIFEST.MF +++ b/org.eclipse.m2e.launching/META-INF/MANIFEST.MF @@ -18,7 +18,9 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.27.0,4.0.0)", org.eclipse.m2e.maven.runtime;bundle-version="[3.8.6,4.0.0)", org.eclipse.m2e.core;bundle-version="[2.0.0,3.0.0)", org.eclipse.m2e.core.ui;bundle-version="[2.0.0,3.0.0)", - org.eclipse.m2e.workspace.cli;bundle-version="0.1.0" + org.eclipse.m2e.workspace.cli;bundle-version="0.1.0", + org.eclipse.jdt.junit.core, + org.eclipse.unittest.ui;bundle-version="1.1.200" Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-Vendor: %Bundle-Vendor diff --git a/org.eclipse.m2e.launching/plugin.xml b/org.eclipse.m2e.launching/plugin.xml index fec1e03370..661e235d4b 100644 --- a/org.eclipse.m2e.launching/plugin.xml +++ b/org.eclipse.m2e.launching/plugin.xml @@ -243,5 +243,12 @@ class="org.eclipse.m2e.internal.launch.MavenConsoleLineTracker" processType="java"/> + + + + diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/actions/ExecutePomAction.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/actions/ExecutePomAction.java index 69c2ef633f..b5819df36e 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/actions/ExecutePomAction.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/actions/ExecutePomAction.java @@ -56,6 +56,7 @@ import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.dialogs.ElementListSelectionDialog; +import org.eclipse.unittest.ui.ConfigureViewerSupport; import org.eclipse.m2e.core.MavenPlugin; import org.eclipse.m2e.core.embedder.IMavenExecutableLocation; @@ -75,6 +76,10 @@ * @author Eugene Kuleshov */ public class ExecutePomAction implements ILaunchShortcut, IExecutableExtension, ILaunchShortcut2 { + + public static final ConfigureViewerSupport TEST_RESULT_LISTENER_CONFIGURER = new ConfigureViewerSupport( + "org.eclipse.m2e.launching.testViewSupport"); + private static final Logger log = LoggerFactory.getLogger(ExecutePomAction.class); private boolean showDialog = false; @@ -212,7 +217,6 @@ private ILaunchConfiguration createLaunchConfiguration(IContainer basedir, Strin workingCopy.setAttribute(IDebugUIConstants.ATTR_PRIVATE, true); workingCopy.setAttribute(RefreshTab.ATTR_REFRESH_SCOPE, "${project}"); //$NON-NLS-1$ workingCopy.setAttribute(RefreshTab.ATTR_REFRESH_RECURSIVE, true); - setProjectConfiguration(workingCopy, basedir); // TODO when launching Maven with debugger consider to add the following property @@ -237,6 +241,7 @@ private void setProjectConfiguration(ILaunchConfigurationWorkingCopy workingCopy workingCopy.setAttribute(MavenLaunchConstants.ATTR_PROFILES, selectedProfiles); } } + TEST_RESULT_LISTENER_CONFIGURER.apply(workingCopy); } private ILaunchConfiguration getLaunchConfiguration(IContainer basedir, String mode) { diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java new file mode 100644 index 0000000000..91e24a1ed9 --- /dev/null +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java @@ -0,0 +1,160 @@ +/******************************************************************************** + * Copyright (c) 2024 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + ********************************************************************************/ + +package org.eclipse.m2e.internal.launch; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.core.runtime.Status; +import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.ILaunch; +import org.eclipse.debug.core.model.IProcess; +import org.eclipse.debug.core.model.IStreamsProxy; + +import org.eclipse.m2e.core.embedder.ArtifactKey; +import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge; +import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge.MavenBuildConnection; +import org.eclipse.m2e.internal.maven.listener.MavenBuildListener; +import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData; +import org.eclipse.m2e.internal.maven.listener.MavenTestEvent; + + +/** + * A process that represents the remote connection to the maven process + */ +public class MavenBuildConnectionProcess implements IProcess { + + private ILaunch launch; + + private Map attributes = new HashMap<>(); + + private Map projects = new ConcurrentHashMap<>(); + + private MavenBuildConnection connection; + + private List buildListeners = new CopyOnWriteArrayList<>(); + + public MavenBuildConnectionProcess(ILaunch launch) { + this.launch = launch; + launch.addProcess(this); + attributes.put(IProcess.ATTR_PROCESS_TYPE, "m2e-build-endpoint"); + } + + public T getAdapter(Class adapter) { + return null; + } + + public boolean canTerminate() { + return true; + } + + public boolean isTerminated() { + return connection == null || connection.isReadCompleted(); + } + + public void terminate() throws DebugException { + if(connection != null) { + try { + connection.close(); + } catch(IOException ex) { + throw new DebugException(Status.error("Terminate failed", ex)); + } + connection = null; + } + } + + public String getLabel() { + return "M2E Build Listener"; + } + + public ILaunch getLaunch() { + return launch; + } + + public IStreamsProxy getStreamsProxy() { + return null; + } + + @Override + public void setAttribute(String key, String value) { + attributes.put(key, value); + } + + @Override + public String getAttribute(String key) { + return attributes.get(key); + } + + public void addMavenBuildListener(MavenBuildListener listener) { + buildListeners.add(listener); + } + + /** + * @param mavenTestRunnerClient + * @return + */ + public void removeMavenBuildListener(MavenBuildListener listener) { + buildListeners.remove(listener); + } + + @Override + public int getExitValue() { + return 0; + } + + /** + * @return the projects + */ + public Map getProjects() { + return this.projects; + } + + void connect() throws IOException { + connection = M2EMavenBuildDataBridge.prepareConnection(launch.getLaunchConfiguration().getName(), + new MavenBuildListener() { + + @Override + public void projectStarted(MavenProjectBuildData data) { + projects.put(new ArtifactKey(data.groupId, data.artifactId, data.version, null), data); + for(MavenBuildListener mavenBuildListener : buildListeners) { + mavenBuildListener.projectStarted(data); + } + } + + @Override + public void onTestEvent(MavenTestEvent mavenTestEvent) { + for(MavenBuildListener mavenBuildListener : buildListeners) { + mavenBuildListener.onTestEvent(mavenTestEvent); + } + } + + public void close() { + for(MavenBuildListener mavenBuildListener : buildListeners) { + mavenBuildListener.close(); + } + buildListeners.clear(); + launch.removeProcess(MavenBuildConnectionProcess.this); + } + }); + } + + String getMavenVMArguments() throws IOException { + return connection.getMavenVMArguments(); + } + +} diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java index cd9abfbcd0..3ae45480fc 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java @@ -16,50 +16,39 @@ import java.io.IOException; import java.util.Arrays; import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Stream; +import java.util.Optional; import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchesListener2; +import org.eclipse.debug.core.model.IProcess; import org.eclipse.m2e.core.embedder.ArtifactKey; import org.eclipse.m2e.core.internal.launch.MavenEmbeddedRuntime; import org.eclipse.m2e.internal.launch.MavenRuntimeLaunchSupport.VMArguments; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge.MavenBuildConnection; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge.MavenProjectBuildData; +import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData; public class MavenBuildProjectDataConnection { - private static record MavenBuildConnectionData(Map projects, - MavenBuildConnection connection) { - } - - private static final Map LAUNCH_PROJECT_DATA = new ConcurrentHashMap<>(); - static { DebugPlugin.getDefault().getLaunchManager().addLaunchListener(new ILaunchesListener2() { public void launchesRemoved(ILaunch[] launches) { - closeServers(Arrays.stream(launches).map(LAUNCH_PROJECT_DATA::remove)); - } - - public void launchesTerminated(ILaunch[] launches) { - closeServers(Arrays.stream(launches).map(LAUNCH_PROJECT_DATA::get)); - } - - private static void closeServers(Stream connectionData) { - connectionData.filter(Objects::nonNull).forEach(c -> { + Arrays.stream(launches).flatMap(launch -> getConnection(launch).stream()).forEach(con -> { try { - c.connection().close(); - } catch(IOException ex) { // ignore + con.terminate(); + } catch(DebugException ex) { + //ignore } }); } + public void launchesTerminated(ILaunch[] launches) { + launchesRemoved(launches); + } + public void launchesAdded(ILaunch[] launches) { // ignore } @@ -71,33 +60,42 @@ public void launchesChanged(ILaunch[] launches) { // ignore static void openListenerConnection(ILaunch launch, VMArguments arguments) { try { if(MavenLaunchUtils.getMavenRuntime(launch.getLaunchConfiguration()) instanceof MavenEmbeddedRuntime) { - - Map projects = new ConcurrentHashMap<>(); - - MavenBuildConnection connection = M2EMavenBuildDataBridge.prepareConnection( - launch.getLaunchConfiguration().getName(), - d -> projects.put(new ArtifactKey(d.groupId, d.artifactId, d.version, null), d)); - - if(LAUNCH_PROJECT_DATA.putIfAbsent(launch, new MavenBuildConnectionData(projects, connection)) != null) { - connection.close(); + getConnection(launch).ifPresent(existing -> { + try { + existing.terminate(); + } catch(DebugException ex) { + } throw new IllegalStateException( "Maven bridge already created for launch of" + launch.getLaunchConfiguration().getName()); - } - arguments.append(connection.getMavenVMArguments()); + }); + MavenBuildConnectionProcess process = new MavenBuildConnectionProcess(launch); + process.connect(); + arguments.append(process.getMavenVMArguments()); } } catch(CoreException | IOException ex) { // ignore } } + public static Optional getConnection(ILaunch launch) { + for(IProcess process : launch.getProcesses()) { + if(process instanceof MavenBuildConnectionProcess connection) { + return Optional.of(connection); + } + } + return Optional.empty(); + } + static MavenProjectBuildData getBuildProject(ILaunch launch, String groupId, String artifactId, String version) { - MavenBuildConnectionData build = LAUNCH_PROJECT_DATA.get(launch); - if(build == null) { + Optional connection = getConnection(launch); + if(connection.isEmpty()) { return null; } + MavenBuildConnectionProcess process = connection.get(); + Map projects = process.getProjects(); ArtifactKey key = new ArtifactKey(groupId, artifactId, version, null); while(true) { - MavenProjectBuildData buildProject = build.projects().get(key); - if(buildProject != null || build.connection().isReadCompleted()) { + MavenProjectBuildData buildProject = projects.get(key); + if(buildProject != null || process.isTerminated()) { return buildProject; } Thread.onSpinWait(); // Await completion of project data read. It has to become available soon, since its GAV was printed on the console diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java index 923df503c7..cca16682eb 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenConsoleLineTracker.java @@ -65,7 +65,7 @@ import org.eclipse.m2e.core.internal.IMavenConstants; import org.eclipse.m2e.core.project.IBuildProjectFileResolver; -import org.eclipse.m2e.internal.maven.listener.M2EMavenBuildDataBridge.MavenProjectBuildData; +import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData; /** diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchDelegate.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchDelegate.java index 3fad7104e1..1b81a02493 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchDelegate.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenLaunchDelegate.java @@ -26,6 +26,8 @@ import java.util.SortedMap; import java.util.TreeMap; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,6 +123,15 @@ public void launch(ILaunchConfiguration configuration, String mode, ILaunch laun this.programArguments = null; try { + Bundle bundle = Platform.getBundle("org.eclipse.unittest.ui"); + if(bundle != null) { + try { + bundle.start(); + } catch(BundleException ex) { + //we tried our best... + } + } + System.out.println(bundle); this.launchSupport = MavenRuntimeLaunchSupport.create(configuration, monitor); this.extensionsSupport = MavenLaunchExtensionsSupport.create(configuration, launch); this.preferencesService = Platform.getPreferencesService(); @@ -129,7 +140,6 @@ public void launch(ILaunchConfiguration configuration, String mode, ILaunch laun log.info(" mvn {}", getProgramArguments(configuration)); //$NON-NLS-1$ extensionsSupport.configureSourceLookup(configuration, launch, monitor); - super.launch(configuration, mode, launch, monitor); } finally { this.launch = null; diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java new file mode 100644 index 0000000000..3bdeab71aa --- /dev/null +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java @@ -0,0 +1,189 @@ +/******************************************************************************** + * Copyright (c) 2024 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + ********************************************************************************/ + +package org.eclipse.m2e.internal.launch.testing; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; + +import org.xml.sax.SAXException; + +import org.eclipse.debug.core.ILaunch; +import org.eclipse.unittest.internal.junitXmlReport.TestRunHandler; +import org.eclipse.unittest.launcher.ITestRunnerClient; +import org.eclipse.unittest.model.ITestCaseElement; +import org.eclipse.unittest.model.ITestElement; +import org.eclipse.unittest.model.ITestElement.FailureTrace; +import org.eclipse.unittest.model.ITestElement.Result; +import org.eclipse.unittest.model.ITestRunSession; +import org.eclipse.unittest.model.ITestSuiteElement; + +import org.apache.maven.execution.ExecutionEvent.Type; + +import org.eclipse.m2e.internal.launch.MavenBuildProjectDataConnection; +import org.eclipse.m2e.internal.maven.listener.MavenBuildListener; +import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData; +import org.eclipse.m2e.internal.maven.listener.MavenTestEvent; + + +/** + * + */ +public class MavenTestRunnerClient implements ITestRunnerClient, MavenBuildListener { + + private ITestRunSession session; + + private ThreadLocal projectData = new ThreadLocal<>(); + + private Map projectElementMap = new ConcurrentHashMap<>(); + + /** + * @param session + */ + public MavenTestRunnerClient(ITestRunSession session) { + this.session = session; + } + + public void startMonitoring() { + MavenBuildProjectDataConnection.getConnection(session.getLaunch()) + .ifPresent(con -> con.addMavenBuildListener(this)); + + } + + public void stopTest() { + stopMonitoring(); + ILaunch launch = session.getLaunch(); + try { + launch.terminate(); + } catch(Exception ex) { + } + } + + public void stopMonitoring() { + MavenBuildProjectDataConnection.getConnection(session.getLaunch()) + .ifPresent(con -> con.removeMavenBuildListener(this)); + } + + /** + * @return the session + */ + public ITestRunSession getSession() { + return this.session; + } + + public void projectStarted(MavenProjectBuildData data) { + this.projectData.set(data); + } + + public void onTestEvent(MavenTestEvent mavenTestEvent) { + if(mavenTestEvent.getType() == Type.MojoSucceeded || mavenTestEvent.getType() == Type.MojoFailed) { + //in any case look for the tests... + Path reportDirectory = mavenTestEvent.getReportDirectory(); + if(Files.isDirectory(reportDirectory)) { + SAXParser parser = getParser(); + if(parser == null) { + return; + } + try (Stream list = Files.list(reportDirectory)) { + Iterator iterator = list.iterator(); + while(iterator.hasNext()) { + Path path = iterator.next(); + System.out.println("Scan result file " + path); + ITestRunSession importedSession = parseFile(path, parser); + if(importedSession != null) { + ITestSuiteElement project = getProjectSuite(); + ITestSuiteElement file = session.newTestSuite(path.toString(), path.getFileName().toString(), null, + project, path.getFileName().toString(), null); + for(ITestElement element : importedSession.getChildren()) { + importTestElement(element, file); + } + } + } + } catch(IOException ex) { + } + } + } + } + + /** + * @param element + * @param file + */ + private void importTestElement(ITestElement element, ITestSuiteElement parent) { + if(element instanceof ITestCaseElement testcase) { + ITestCaseElement importedTestCase = session.newTestCase(parent.getId() + "." + testcase.getId(), + testcase.getTestName(), parent, testcase.getDisplayName(), testcase.getData()); + session.notifyTestStarted(importedTestCase); + FailureTrace failureTrace = testcase.getFailureTrace(); + if(failureTrace == null) { + session.notifyTestEnded(importedTestCase, testcase.isIgnored()); + } else { + session.notifyTestFailed(importedTestCase, Result.ERROR/*TODO how do we know?*/, false /*TODO how do we know?*/, + failureTrace); + } + } else if(element instanceof ITestSuiteElement suite) { + ITestSuiteElement importedTestSuite = session.newTestSuite(parent.getId() + "." + suite.getId(), + suite.getTestName(), null, parent, suite.getDisplayName(), suite.getData()); + session.notifyTestStarted(importedTestSuite); + for(ITestElement child : suite.getChildren()) { + importTestElement(child, importedTestSuite); + } + session.notifyTestEnded(importedTestSuite, false); + } + } + + /** + * @return + */ + private ITestSuiteElement getProjectSuite() { + return projectElementMap.computeIfAbsent(projectData.get(), data -> { + Path basedir = data.projectBasedir; + return session.newTestSuite(basedir.toString(), basedir.getFileName().toString(), null, null, + data.groupId + ":" + data.artifactId, null); + }); + } + + private SAXParser getParser() { + try { + return org.eclipse.core.internal.runtime.XmlProcessorFactory.createSAXParserWithErrorOnDOCTYPE(); + } catch(ParserConfigurationException ex) { + } catch(SAXException ex) { + } + return null; + } + + private ITestRunSession parseFile(Path path, SAXParser parser) { + //TODO Currently NOT working as this is internal API that is not exported! + final TestRunHandler handler = new TestRunHandler(); + try { + parser.parse(Files.newInputStream(path), handler); + return handler.getTestRunSession(); + } catch(SAXException | IOException ex) { + //can't read then... + return null; + } + } + + public void close() { + // nothing to do yet... + } + +} diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestViewSupport.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestViewSupport.java new file mode 100644 index 0000000000..ae3c2c4928 --- /dev/null +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestViewSupport.java @@ -0,0 +1,65 @@ +/******************************************************************************** + * Copyright (c) 2024 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + ********************************************************************************/ + +package org.eclipse.m2e.internal.launch.testing; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.core.text.StringMatcher; +import org.eclipse.debug.core.ILaunchConfiguration; +import org.eclipse.jface.action.IAction; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.unittest.launcher.ITestRunnerClient; +import org.eclipse.unittest.model.ITestCaseElement; +import org.eclipse.unittest.model.ITestElement; +import org.eclipse.unittest.model.ITestRunSession; +import org.eclipse.unittest.model.ITestSuiteElement; +import org.eclipse.unittest.ui.ITestViewSupport; + + +public class MavenTestViewSupport implements ITestViewSupport { + + public ITestRunnerClient newTestRunnerClient(ITestRunSession session) { + return new MavenTestRunnerClient(session); + } + + public Collection getTraceExclusionFilterPatterns() { + return null; + } + + public IAction getOpenTestAction(Shell shell, ITestCaseElement testCase) { + return null; + } + + public IAction getOpenTestAction(Shell shell, ITestSuiteElement testSuite) { + return null; + } + + public IAction createOpenEditorAction(Shell shell, ITestElement failure, String traceLine) { + return null; + } + + public Runnable createShowStackTraceInConsoleViewActionDelegate(ITestElement failedTest) { + return null; + } + + public ILaunchConfiguration getRerunLaunchConfiguration(List testElements) { + return null; + } + + public String getDisplayName() { + return "Maven"; + } + +} diff --git a/org.eclipse.m2e.maven.runtime/pom.xml b/org.eclipse.m2e.maven.runtime/pom.xml index b1146a7ed1..15d8e92376 100644 --- a/org.eclipse.m2e.maven.runtime/pom.xml +++ b/org.eclipse.m2e.maven.runtime/pom.xml @@ -20,7 +20,7 @@ org.eclipse.m2e.maven.runtime - 3.9.600-SNAPSHOT + 3.9.601-SNAPSHOT jar M2E Embedded Maven Runtime (includes Incubating components) diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java index 72d113fe06..13e0b27ff3 100644 --- a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/M2EMavenBuildDataBridge.java @@ -21,36 +21,26 @@ import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; -import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.StringJoiner; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; import javax.inject.Named; import javax.inject.Singleton; -import org.apache.maven.eventspy.EventSpy; -import org.apache.maven.execution.ExecutionEvent; +import org.apache.maven.AbstractMavenLifecycleParticipant; +import org.apache.maven.MavenExecutionException; import org.apache.maven.execution.ExecutionEvent.Type; -import org.apache.maven.project.MavenProject; +import org.apache.maven.execution.MavenSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * This {@link EventSpy} listens to certain events within a Maven build JVM and - * sends certain data (e.g. about projects being built) to the JVM of the - * Eclipse IDE that launched the Maven build JVM. - * - * @author Hannes Wellmann - * + * Bridge between the remote running maven and the local m2e to exchange event + * messages and information. */ -@Named +@Named("m2e") @Singleton -public class M2EMavenBuildDataBridge implements EventSpy { +public class M2EMavenBuildDataBridge extends AbstractMavenLifecycleParticipant { private static final String SOCKET_FILE_PROPERTY_NAME = "m2e.build.project.data.socket.port"; private static final String DATA_SET_SEPARATOR = ";;"; @@ -60,7 +50,7 @@ public class M2EMavenBuildDataBridge implements EventSpy { private SocketChannel writeChannel; @Override - public void init(Context context) throws IOException { + public void afterSessionStart(MavenSession session) throws MavenExecutionException { String socketPort = System.getProperty(SOCKET_FILE_PROPERTY_NAME); if (socketPort != null) { try { @@ -76,94 +66,51 @@ public void init(Context context) throws IOException { } @Override - public void close() throws IOException { - writeChannel.close(); + public void afterSessionEnd(MavenSession session) throws MavenExecutionException { + try { + writeChannel.close(); + } catch (IOException e) { + // we can't do much here anyways... + } } - @Override - public void onEvent(Object event) throws Exception { - if (writeChannel != null && event instanceof ExecutionEvent - && ((ExecutionEvent) event).getType() == Type.ProjectStarted) { - - String message = serializeProjectData(((ExecutionEvent) event).getProject()); + boolean isActive() { + return writeChannel != null; + } - ByteBuffer buffer = ByteBuffer.wrap(message.getBytes()); - synchronized (writeChannel) { + void sendMessage(String msg) { + SocketChannel channel = writeChannel; + if (channel != null) { + ByteBuffer buffer = ByteBuffer.wrap((msg + DATA_SET_SEPARATOR).getBytes()); + synchronized (channel) { while (buffer.hasRemaining()) { - writeChannel.write(buffer); + try { + channel.write(buffer); + } catch (IOException e) { + LOGGER.warn("Can't forward message to m2e: " + e); + } } } } } - private static String serializeProjectData(MavenProject project) { - StringJoiner data = new StringJoiner(","); - add(data, "groupId", project.getGroupId()); - add(data, "artifactId", project.getArtifactId()); - add(data, "version", project.getVersion()); - add(data, "file", project.getFile()); - add(data, "basedir", project.getBasedir()); - add(data, "build.directory", project.getBuild().getDirectory()); - return data.toString() + DATA_SET_SEPARATOR; - } - private static void add(StringJoiner data, String key, Object value) { - data.add(key + "=" + value); - } - /** - *

- * This method is supposed to be called from M2E within the Eclipse-IDE JVM. - *

- * - * @param dataSet the data-set to parse - * @return the {@link MavenProjectBuildData} parsed from the given string - */ - private static MavenProjectBuildData parseMavenBuildProject(String dataSet) { - Map data = new HashMap<>(8); - for (String entry : dataSet.split(",")) { - String[] keyValue = entry.split("="); - if (keyValue.length != 2) { - throw new IllegalStateException("Invalid data-set format" + dataSet); - } - data.put(keyValue[0], keyValue[1]); - } - return new MavenProjectBuildData(data); - } - public static final class MavenProjectBuildData { - public final String groupId; - public final String artifactId; - public final String version; - public final Path projectBasedir; - public final Path projectFile; - public final Path projectBuildDirectory; - - MavenProjectBuildData(Map data) { - if (data.size() != 6) { - throw new IllegalArgumentException(); - } - this.groupId = Objects.requireNonNull(data.get("groupId")); - this.artifactId = Objects.requireNonNull(data.get("artifactId")); - this.version = Objects.requireNonNull(data.get("version")); - this.projectBasedir = Paths.get(data.get("basedir")); - this.projectFile = Paths.get(data.get("file")); - this.projectBuildDirectory = Paths.get(data.get("build.directory")); - } - } + /** * Prepares the connection to a {@code Maven build JVM} to be launched and is * intended to be called from the Eclipse IDE JVM. * * @param label the label of the listener thread - * @param datasetListener the listener, which is notified whenever a new + * @param buildListener the listener, which is notified whenever a new * {@link MavenProjectBuildData MavenProjectBuildDataSet} * has arived from the Maven VM in the Eclipse-IDE VM. * @return the preapre {@link MavenBuildConnection} * @throws IOException */ - public static MavenBuildConnection prepareConnection(String label, Consumer datasetListener) + public static MavenBuildConnection prepareConnection(String label, MavenBuildListener buildListener) throws IOException { // TODO: use UNIX domain socket once Java-17 is required by Maven @@ -187,9 +134,21 @@ public static MavenBuildConnection prepareConnection(String label, Consumer= 0;) { String dataSet = message.substring(0, terminatorIndex); message.delete(0, terminatorIndex + DATA_SET_SEPARATOR.length()); - - MavenProjectBuildData buildData = parseMavenBuildProject(dataSet); - datasetListener.accept(buildData); + if (dataSet.startsWith(M2eEventSpy.PROJECT_START_EVENT)) { + MavenProjectBuildData buildData = MavenProjectBuildData + .parseMavenBuildProject( + dataSet.substring(M2eEventSpy.PROJECT_START_EVENT.length())); + buildListener.projectStarted(buildData); + } else if (dataSet.startsWith(M2eMojoExecutionListener.TEST_START_EVENT)) { + String path = dataSet.substring(M2eMojoExecutionListener.TEST_START_EVENT.length()); + buildListener.onTestEvent(new MavenTestEvent(Type.MojoStarted, Paths.get(path))); + } else if (dataSet.startsWith(M2eMojoExecutionListener.TEST_END_EVENT)) { + String path = dataSet.substring(M2eMojoExecutionListener.TEST_END_EVENT.length()); + buildListener.onTestEvent(new MavenTestEvent(Type.MojoSucceeded, Paths.get(path))); + } else if (dataSet.startsWith(M2eMojoExecutionListener.TEST_END_FAILED_EVENT)) { + String path = dataSet.substring(M2eMojoExecutionListener.TEST_END_EVENT.length()); + buildListener.onTestEvent(new MavenTestEvent(Type.MojoFailed, Paths.get(path))); + } } // Explicit cast for compatibility with covariant return type on JDK 9's // ByteBuffer @@ -198,6 +157,7 @@ public static MavenBuildConnection prepareConnection(String label, Consumer testExecutionPathMap = new ConcurrentHashMap<>(); + private BuildPluginManager buildPluginManager; + + @Inject + public M2eMojoExecutionListener(M2EMavenBuildDataBridge bridge, BuildPluginManager buildPluginManager) { + this.bridge = bridge; + this.buildPluginManager = buildPluginManager; + } + + @Override + public void beforeMojoExecution(MojoExecutionEvent event) throws MojoExecutionException { + if (!bridge.isActive()) { + return; + } + MojoExecution execution = event.getExecution(); + // it would be better if we have a common interface in plexus-build api that + // mojos can implement instead then we just need to query for the interface + boolean bndTest = isBndTest(execution); + if (isSurefireTest(execution) || isFailsafeTest(execution) || bndTest || isTychoSurefire(execution)) { + File reportsDirectory = getMojoParameterValue(execution, bndTest ? "reportsDir" : "reportsDirectory", + File.class, event.getSession()); + if (reportsDirectory != null) { + testExecutionPathMap.put(execution, reportsDirectory.toPath()); + bridge.sendMessage(TEST_START_EVENT + reportsDirectory.getAbsolutePath()); + } + } + } + + private static boolean isTychoSurefire(MojoExecution execution) { + return execution.getPlugin().getId().startsWith("org.eclipse.tycho:tycho-surefire-plugin:") + && ("test".equals(execution.getGoal()) || "plugin-test".equals(execution.getGoal()) + || "bnd-test".equals(execution.getGoal())); + + } + + private static boolean isFailsafeTest(MojoExecution execution) { + return execution.getPlugin().getId().startsWith("org.apache.maven.plugins:maven-failsafe-plugin:") + && "integration-test".equals(execution.getGoal()); + } + + private static boolean isSurefireTest(MojoExecution execution) { + return execution.getPlugin().getId().startsWith("org.apache.maven.plugins:maven-surefire-plugin:") + && "test".equals(execution.getGoal()); + } + + private static boolean isBndTest(MojoExecution execution) { + return execution.getPlugin().getId().startsWith("biz.aQute.bnd:bnd-testing-maven-plugin:") + && "testing".equals(execution.getGoal()); + } + + private void afterMojoExecution(MojoExecutionEvent event, String type) { + if (!bridge.isActive()) { + return; + } + Path testPath = testExecutionPathMap.remove(event.getExecution()); + if (testPath != null) { + bridge.sendMessage(type + testPath.toAbsolutePath().toString()); + } + } + + private T getMojoParameterValue(MojoExecution mojoExecution, String parameter, Class asType, + MavenSession session) { + Xpp3Dom dom = mojoExecution.getConfiguration(); + if (dom == null) { + return null; + } + PlexusConfiguration configuration = new XmlPlexusConfiguration(dom).getChild(parameter); + if (configuration == null) { + return null; + } + MojoDescriptor mojoDescriptor = mojoExecution.getMojoDescriptor(); + ClassRealm pluginRealm; + try { + pluginRealm = buildPluginManager.getPluginRealm(session, mojoDescriptor.getPluginDescriptor()); + ExpressionEvaluator expressionEvaluator = new PluginParameterExpressionEvaluator(session, mojoExecution); + ConfigurationConverter typeConverter = converterLookup.lookupConverterForType(asType); + + Object value = typeConverter.fromConfiguration(converterLookup, configuration, asType, + mojoDescriptor.getImplementationClass(), pluginRealm, expressionEvaluator, null); + return asType.cast(value); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + @Override + public void afterMojoExecutionSuccess(MojoExecutionEvent event) throws MojoExecutionException { + afterMojoExecution(event, TEST_END_EVENT); + } + + @Override + public void afterExecutionFailure(MojoExecutionEvent event) { + afterMojoExecution(event, TEST_END_FAILED_EVENT); + + } + +} diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildListener.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildListener.java new file mode 100644 index 0000000000..dce5b75ae9 --- /dev/null +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenBuildListener.java @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + ********************************************************************************/ +package org.eclipse.m2e.internal.maven.listener; + +/** + * Interface to implement when wanted to be informed about remote maven events + */ +public interface MavenBuildListener extends AutoCloseable { + + void projectStarted(MavenProjectBuildData data); + + void onTestEvent(MavenTestEvent mavenTestEvent); + + @Override + void close(); + +} diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java new file mode 100644 index 0000000000..02b2bddecd --- /dev/null +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenProjectBuildData.java @@ -0,0 +1,79 @@ +/******************************************************************************** + * Copyright (c) 2022, 2024 Hannes Wellmann and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Hannes Wellmann - initial API and implementation + * Christoph Läubrich - factored out + ********************************************************************************/ +package org.eclipse.m2e.internal.maven.listener; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.StringJoiner; + +import org.apache.maven.project.MavenProject; + +public final class MavenProjectBuildData { + public final String groupId; + public final String artifactId; + public final String version; + public final Path projectBasedir; + public final Path projectFile; + public final Path projectBuildDirectory; + + MavenProjectBuildData(Map data) { + if (data.size() != 6) { + throw new IllegalArgumentException(); + } + this.groupId = Objects.requireNonNull(data.get("groupId")); + this.artifactId = Objects.requireNonNull(data.get("artifactId")); + this.version = Objects.requireNonNull(data.get("version")); + this.projectBasedir = Paths.get(data.get("basedir")); + this.projectFile = Paths.get(data.get("file")); + this.projectBuildDirectory = Paths.get(data.get("build.directory")); + } + + static String serializeProjectData(MavenProject project) { + StringJoiner data = new StringJoiner(","); + add(data, "groupId", project.getGroupId()); + add(data, "artifactId", project.getArtifactId()); + add(data, "version", project.getVersion()); + add(data, "file", project.getFile()); + add(data, "basedir", project.getBasedir()); + add(data, "build.directory", project.getBuild().getDirectory()); + return data.toString(); + } + + /** + *

+ * This method is supposed to be called from M2E within the Eclipse-IDE JVM. + *

+ * + * @param dataSet the data-set to parse + * @return the {@link MavenProjectBuildData} parsed from the given string + */ + static MavenProjectBuildData parseMavenBuildProject(String dataSet) { + Map data = new HashMap<>(8); + for (String entry : dataSet.split(",")) { + String[] keyValue = entry.split("="); + if (keyValue.length != 2) { + throw new IllegalStateException("Invalid data-set format" + dataSet); + } + data.put(keyValue[0], keyValue[1]); + } + return new MavenProjectBuildData(data); + } + + private static void add(StringJoiner data, String key, Object value) { + data.add(key + "=" + value); + } +} \ No newline at end of file diff --git a/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenTestEvent.java b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenTestEvent.java new file mode 100644 index 0000000000..bf18b02295 --- /dev/null +++ b/org.eclipse.m2e.maven.runtime/src/main/java/org/eclipse/m2e/internal/maven/listener/MavenTestEvent.java @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Christoph Läubrich and others + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + ********************************************************************************/ +package org.eclipse.m2e.internal.maven.listener; + +import java.nio.file.Path; + +import org.apache.maven.execution.ExecutionEvent; + +public class MavenTestEvent { + + + private Path reportDirectory; + private ExecutionEvent.Type type; + + public MavenTestEvent(ExecutionEvent.Type type, Path reportDirectory) { + this.type = type; + this.reportDirectory = reportDirectory; + } + + public Path getReportDirectory() { + return reportDirectory; + } + + public ExecutionEvent.Type getType() { + return type; + } + + @Override + public String toString() { + return "MavenTestEvent [type=" + type + ", reportDirectory=" + reportDirectory + "]"; + } + +} diff --git a/pom.xml b/pom.xml index 4950bcd1b8..c39883b38e 100644 --- a/pom.xml +++ b/pom.xml @@ -103,7 +103,7 @@ org.eclipse.m2e org.eclipse.m2e.maven.runtime - 3.9.600-SNAPSHOT + 3.9.601-SNAPSHOT diff --git a/target-platform/target-platform.target b/target-platform/target-platform.target index f2730674ef..03656ce2b3 100644 --- a/target-platform/target-platform.target +++ b/target-platform/target-platform.target @@ -12,6 +12,7 @@ + From 8676e14d665135a4c0da9eb6b821238d1dbac980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 27 Feb 2024 07:30:00 +0100 Subject: [PATCH 2/3] Copy some code from internal platform code --- .../launch/testing/copied/IXMLTags.java | 112 ++++++ .../launch/testing/copied/TestRunHandler.java | 365 ++++++++++++++++++ 2 files changed, 477 insertions(+) create mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java create mode 100644 org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java new file mode 100644 index 0000000000..efe388a47e --- /dev/null +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2007, 2017 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.unittest.internal.junitXmlReport; + +interface IXMLTags { + + String NODE_TESTRUN = "testrun"; //$NON-NLS-1$ + String NODE_TESTSUITES = "testsuites"; //$NON-NLS-1$ + String NODE_TESTSUITE = "testsuite"; //$NON-NLS-1$ + String NODE_PROPERTIES = "properties"; //$NON-NLS-1$ + String NODE_PROPERTY = "property"; //$NON-NLS-1$ + String NODE_TESTCASE = "testcase"; //$NON-NLS-1$ + String NODE_ERROR = "error"; //$NON-NLS-1$ + String NODE_FAILURE = "failure"; //$NON-NLS-1$ + String NODE_EXPECTED = "expected"; //$NON-NLS-1$ + String NODE_ACTUAL = "actual"; //$NON-NLS-1$ + String NODE_SYSTEM_OUT = "system-out"; //$NON-NLS-1$ + String NODE_SYSTEM_ERR = "system-err"; //$NON-NLS-1$ + String NODE_SKIPPED = "skipped"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_NAME = "name"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_LAUNCH_CONFIG_NAME = "launch_config_name"; //$NON-NLS-1$ + /** + * value: Integer + */ + String ATTR_TESTS = "tests"; //$NON-NLS-1$ + /** + * value: Integer + */ + String ATTR_STARTED = "started"; //$NON-NLS-1$ + /** + * value: Integer + */ + String ATTR_FAILURES = "failures"; //$NON-NLS-1$ + /** + * value: Integer + */ + String ATTR_ERRORS = "errors"; //$NON-NLS-1$ + /** + * value: Boolean + */ + String ATTR_IGNORED = "ignored"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_PACKAGE = "package"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_ID = "id"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_CLASSNAME = "classname"; //$NON-NLS-1$ + /** + * value: Boolean + */ + String ATTR_INCOMPLETE = "incomplete"; //$NON-NLS-1$ + /** + * value: Duration.toString() + */ + String ATTR_START_TIME = "startTime"; //$NON-NLS-1$ + /** + * value: Double (duration in seconds) + */ + String ATTR_DURATION = "time"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_MESSAGE = "message"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_DISPLAY_NAME = "displayname"; //$NON-NLS-1$ + /** + * value: Boolean + */ + String ATTR_DYNAMIC_TEST = "dynamicTest"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_DATA = "data"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_INCLUDE_TAGS = "include_tags"; //$NON-NLS-1$ + /** + * value: String + */ + String ATTR_EXCLUDE_TAGS = "exclude_tags"; //$NON-NLS-1$ + +// public static final String ATTR_TYPE= "type"; //$NON-NLS-1$ +} diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java new file mode 100644 index 0000000000..fc9dad0ba7 --- /dev/null +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java @@ -0,0 +1,365 @@ +/******************************************************************************* + * Copyright (c) 2007, 2017 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.unittest.internal.junitXmlReport; + +import java.time.Duration; +import java.time.Instant; +import java.util.Stack; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import org.eclipse.osgi.util.NLS; +import org.eclipse.unittest.internal.UnitTestPlugin; +import org.eclipse.unittest.internal.model.ModelMessages; +import org.eclipse.unittest.internal.model.TestCaseElement; +import org.eclipse.unittest.internal.model.TestElement; +import org.eclipse.unittest.internal.model.TestRunSession; +import org.eclipse.unittest.internal.model.TestSuiteElement; +import org.eclipse.unittest.model.ITestElement; +import org.eclipse.unittest.model.ITestElement.FailureTrace; +import org.eclipse.unittest.model.ITestElement.Result; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.OperationCanceledException; + +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.ILaunchConfiguration; + +public class TestRunHandler extends DefaultHandler { + + /* + * TODO: validate (currently assumes correct XML) + */ + + private int fId; + + private TestRunSession fTestRunSession; + private TestSuiteElement fTestSuite; + private TestCaseElement fTestCase; + private final Stack fNotRun = new Stack<>(); + + private StringBuilder fFailureBuffer; + private boolean fInExpected; + private boolean fInActual; + private StringBuilder fExpectedBuffer; + private StringBuilder fActualBuffer; + + private Locator fLocator; + + private Result fStatus; + + private final IProgressMonitor fMonitor; + private int fLastReportedLine; + + /** + * Constructs a default {@link TestRunHandler} object instance + */ + public TestRunHandler() { + fMonitor = new NullProgressMonitor(); + } + + /** + * Constructs a {@link TestRunHandler} object instance + * + * @param monitor a progress monitor + */ + public TestRunHandler(IProgressMonitor monitor) { + fMonitor = monitor != null ? monitor : new NullProgressMonitor(); + } + + @Override + public void setDocumentLocator(Locator locator) { + fLocator = locator; + } + + @Override + public void startDocument() throws SAXException { + // Nothing to do + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + if (fMonitor.isCanceled()) + throw new OperationCanceledException(); + + if (fLocator != null) { + int line = fLocator.getLineNumber(); + if (line - 20 >= fLastReportedLine) { + line -= line % 20; + fLastReportedLine = line; + fMonitor.subTask(NLS.bind(ModelMessages.TestRunHandler_lines_read, Integer.valueOf(line))); + } + } + + if (Thread.interrupted()) + throw new OperationCanceledException(); + + switch (qName) { + case IXMLTags.NODE_TESTRUN: + if (fTestRunSession == null) { + String name = attributes.getValue(IXMLTags.ATTR_NAME); + String launchConfigName = attributes.getValue(IXMLTags.ATTR_LAUNCH_CONFIG_NAME); + ILaunchConfiguration launchConfiguration = null; + if (launchConfigName != null) { + try { + for (ILaunchConfiguration config : DebugPlugin.getDefault().getLaunchManager() + .getLaunchConfigurations()) { + if (config.getName().equals(launchConfigName)) { + launchConfiguration = config; + } + } + } catch (CoreException e) { + UnitTestPlugin.log(e); + } + } + fTestRunSession = new TestRunSession(name, Instant.parse(attributes.getValue(IXMLTags.ATTR_START_TIME)), + launchConfiguration); + readDuration(fTestRunSession, attributes); + // TODO: read counts? + } else { + fTestRunSession.reset(); + } + fTestSuite = fTestRunSession; + break; + case IXMLTags.NODE_TESTSUITES: + break; + case IXMLTags.NODE_TESTSUITE: { + String name = attributes.getValue(IXMLTags.ATTR_NAME); + String pack = attributes.getValue(IXMLTags.ATTR_PACKAGE); + String suiteName = pack == null ? name : pack + "." + name; //$NON-NLS-1$ + String displayName = attributes.getValue(IXMLTags.ATTR_DISPLAY_NAME); + String data = attributes.getValue(IXMLTags.ATTR_DATA); + if (data != null && data.isBlank()) { + data = null; + } + fTestSuite = (TestSuiteElement) fTestRunSession.createTestElement(fTestSuite, getNextId(), suiteName, true, + null, false, displayName, data); + readDuration(fTestSuite, attributes); + fNotRun.push(Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_INCOMPLETE))); + break; + } + // not interested + case IXMLTags.NODE_PROPERTIES: + case IXMLTags.NODE_PROPERTY: + break; + case IXMLTags.NODE_TESTCASE: { + String name = attributes.getValue(IXMLTags.ATTR_NAME); + String classname = attributes.getValue(IXMLTags.ATTR_CLASSNAME); + String testName = name + '(' + classname + ')'; + boolean isDynamicTest = Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_DYNAMIC_TEST)).booleanValue(); + String displayName = attributes.getValue(IXMLTags.ATTR_DISPLAY_NAME); + String data = attributes.getValue(IXMLTags.ATTR_DATA); + if (data != null && data.isBlank()) { + data = null; + } + fTestCase = (TestCaseElement) fTestRunSession.createTestElement(fTestSuite, getNextId(), testName, false, 1, + isDynamicTest, displayName, data); + fNotRun.push(Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_INCOMPLETE))); + fTestCase.setIgnored(Boolean.parseBoolean(attributes.getValue(IXMLTags.ATTR_IGNORED))); + readDuration(fTestCase, attributes); + break; + } + case IXMLTags.NODE_ERROR: + // TODO: multiple failures: https://bugs.eclipse.org/bugs/show_bug.cgi?id=125296 + fStatus = Result.ERROR; + fFailureBuffer = new StringBuilder(); + break; + case IXMLTags.NODE_FAILURE: + // TODO: multiple failures: https://bugs.eclipse.org/bugs/show_bug.cgi?id=125296 + fStatus = Result.FAILURE; + fFailureBuffer = new StringBuilder(); + break; + case IXMLTags.NODE_EXPECTED: + fInExpected = true; + fExpectedBuffer = new StringBuilder(); + break; + case IXMLTags.NODE_ACTUAL: + fInActual = true; + fActualBuffer = new StringBuilder(); + break; + // not interested + case IXMLTags.NODE_SYSTEM_OUT: + case IXMLTags.NODE_SYSTEM_ERR: + break; + case IXMLTags.NODE_SKIPPED: + // before Ant 1.9.0: not an Ant JUnit tag, see + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=276068 + // later: child of or , see + // https://issues.apache.org/bugzilla/show_bug.cgi?id=43969 + fStatus = Result.OK; + fFailureBuffer = new StringBuilder(); + String message = attributes.getValue(IXMLTags.ATTR_MESSAGE); + if (message != null) { + fFailureBuffer.append(message).append('\n'); + } + break; + default: + throw new SAXParseException("unknown node '" + qName + "'", fLocator); //$NON-NLS-1$//$NON-NLS-2$ + } + } + + private void readDuration(ITestElement testElement, Attributes attributes) { + if (testElement instanceof TestElement) { + TestElement element = (TestElement) testElement; + String timeString = attributes.getValue(IXMLTags.ATTR_DURATION); + if (timeString != null) { + try { + double seconds = Double.parseDouble(timeString); + long millis = (long) (seconds * 1000); + element.setDuration(Duration.ofMillis(millis)); + } catch (NumberFormatException e) { + // Ignore + } + } + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (fInExpected) { + fExpectedBuffer.append(ch, start, length); + + } else if (fInActual) { + fActualBuffer.append(ch, start, length); + + } else if (fFailureBuffer != null) { + fFailureBuffer.append(ch, start, length); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + switch (qName) { + // OK + case IXMLTags.NODE_TESTRUN: + break; + // OK + case IXMLTags.NODE_TESTSUITES: + break; + case IXMLTags.NODE_TESTSUITE: + handleTestElementEnd(fTestSuite); + fTestSuite = fTestSuite.getParent(); + // TODO: end suite: compare counters? + break; + // OK + case IXMLTags.NODE_PROPERTIES: + case IXMLTags.NODE_PROPERTY: + break; + case IXMLTags.NODE_TESTCASE: + handleTestElementEnd(fTestCase); + fTestCase = null; + break; + case IXMLTags.NODE_FAILURE: + case IXMLTags.NODE_ERROR: { + ITestElement testElement = fTestCase; + if (testElement == null) + testElement = fTestSuite; + handleFailure(testElement); + break; + } + case IXMLTags.NODE_EXPECTED: + fInExpected = false; + if (fFailureBuffer != null) { + // skip whitespace from before and nodes + fFailureBuffer.setLength(0); + } + break; + case IXMLTags.NODE_ACTUAL: + fInActual = false; + if (fFailureBuffer != null) { + // skip whitespace from before and nodes + fFailureBuffer.setLength(0); + } + break; + // OK + case IXMLTags.NODE_SYSTEM_OUT: + case IXMLTags.NODE_SYSTEM_ERR: + break; + case IXMLTags.NODE_SKIPPED: { + TestElement testElement = fTestCase; + if (testElement == null) + testElement = fTestSuite; + if (fFailureBuffer != null && fFailureBuffer.length() > 0) { + handleFailure(testElement); + testElement.setAssumptionFailed(true); + } else if (fTestCase != null) { + fTestCase.setIgnored(true); + } else { // not expected + testElement.setAssumptionFailed(true); + } + break; + } + default: + handleUnknownNode(qName); + break; + } + } + + private void handleTestElementEnd(ITestElement testElement) { + boolean completed = !fNotRun.pop().booleanValue(); + fTestRunSession.registerTestEnded((TestElement) testElement, completed); + } + + private void handleFailure(ITestElement testElement) { + if (fFailureBuffer != null) { + fTestRunSession.registerTestFailureStatus((TestElement) testElement, fStatus, + new FailureTrace(fFailureBuffer.toString(), toString(fExpectedBuffer), toString(fActualBuffer))); + fFailureBuffer = null; + fExpectedBuffer = null; + fActualBuffer = null; + fStatus = null; + } + } + + private String toString(StringBuilder buffer) { + return buffer != null ? buffer.toString() : null; + } + + private void handleUnknownNode(String qName) throws SAXException { + // TODO: just log if debug option is enabled? + String msg = "unknown node '" + qName + "'"; //$NON-NLS-1$//$NON-NLS-2$ + if (fLocator != null) { + msg += " at line " + fLocator.getLineNumber() + ", column " + fLocator.getColumnNumber(); //$NON-NLS-1$//$NON-NLS-2$ + } + throw new SAXException(msg); + } + + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + + @Override + public void warning(SAXParseException e) throws SAXException { + throw e; + } + + private String getNextId() { + return Integer.toString(fId++); + } + + /** + * @return the parsed test run session, or null + */ + public TestRunSession getTestRunSession() { + return fTestRunSession; + } +} From d3deebc9fd481ce03a5847ab852c5170dd87c43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 27 Feb 2024 21:00:29 +0100 Subject: [PATCH 3/3] Fix process handling --- .../launch/MavenBuildConnectionProcess.java | 46 +- .../MavenBuildProjectDataConnection.java | 22 +- .../launch/testing/MavenTestRunnerClient.java | 77 +-- .../launch/testing/copied/IXMLTags.java | 213 +++--- .../launch/testing/copied/TestRunHandler.java | 612 ++++++++---------- 5 files changed, 467 insertions(+), 503 deletions(-) diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java index 91e24a1ed9..37010abeba 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildConnectionProcess.java @@ -19,9 +19,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicBoolean; -import org.eclipse.core.runtime.Status; -import org.eclipse.debug.core.DebugException; +import org.eclipse.debug.core.DebugEvent; +import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.model.IProcess; import org.eclipse.debug.core.model.IStreamsProxy; @@ -49,10 +50,13 @@ public class MavenBuildConnectionProcess implements IProcess { private List buildListeners = new CopyOnWriteArrayList<>(); + private AtomicBoolean terminated = new AtomicBoolean(); + public MavenBuildConnectionProcess(ILaunch launch) { this.launch = launch; launch.addProcess(this); attributes.put(IProcess.ATTR_PROCESS_TYPE, "m2e-build-endpoint"); + fireEvent(new DebugEvent(this, DebugEvent.CREATE)); } public T getAdapter(Class adapter) { @@ -60,21 +64,27 @@ public T getAdapter(Class adapter) { } public boolean canTerminate() { - return true; + return !isTerminated(); } public boolean isTerminated() { - return connection == null || connection.isReadCompleted(); + return terminated.get() || connection == null; } - public void terminate() throws DebugException { - if(connection != null) { - try { - connection.close(); - } catch(IOException ex) { - throw new DebugException(Status.error("Terminate failed", ex)); + public void terminate() { + if(terminated.compareAndSet(false, true)) { + if(connection != null) { + try { + connection.close(); + } catch(IOException ex) { + } + connection = null; + } + for(MavenBuildListener mavenBuildListener : buildListeners) { + mavenBuildListener.close(); } - connection = null; + buildListeners.clear(); + fireEvent(new DebugEvent(this, DebugEvent.TERMINATE)); } } @@ -93,6 +103,7 @@ public IStreamsProxy getStreamsProxy() { @Override public void setAttribute(String key, String value) { attributes.put(key, value); + fireEvent(new DebugEvent(this, DebugEvent.CHANGE)); } @Override @@ -144,11 +155,7 @@ public void onTestEvent(MavenTestEvent mavenTestEvent) { } public void close() { - for(MavenBuildListener mavenBuildListener : buildListeners) { - mavenBuildListener.close(); - } - buildListeners.clear(); - launch.removeProcess(MavenBuildConnectionProcess.this); + MavenBuildConnectionProcess.this.terminate(); } }); } @@ -157,4 +164,11 @@ String getMavenVMArguments() throws IOException { return connection.getMavenVMArguments(); } + private void fireEvent(DebugEvent event) { + DebugPlugin manager = DebugPlugin.getDefault(); + if(manager != null) { + manager.fireDebugEventSet(new DebugEvent[] {event}); + } + } + } diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java index 3ae45480fc..919caa77f1 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/MavenBuildProjectDataConnection.java @@ -19,7 +19,6 @@ import java.util.Optional; import org.eclipse.core.runtime.CoreException; -import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchesListener2; @@ -36,17 +35,11 @@ public class MavenBuildProjectDataConnection { static { DebugPlugin.getDefault().getLaunchManager().addLaunchListener(new ILaunchesListener2() { public void launchesRemoved(ILaunch[] launches) { - Arrays.stream(launches).flatMap(launch -> getConnection(launch).stream()).forEach(con -> { - try { - con.terminate(); - } catch(DebugException ex) { - //ignore - } - }); + cleanupConnections(launches); } public void launchesTerminated(ILaunch[] launches) { - launchesRemoved(launches); + cleanupConnections(launches); } public void launchesAdded(ILaunch[] launches) { // ignore @@ -54,6 +47,12 @@ public void launchesAdded(ILaunch[] launches) { // ignore public void launchesChanged(ILaunch[] launches) { // ignore } + + private void cleanupConnections(ILaunch[] launches) { + Arrays.stream(launches).flatMap(launch -> getConnection(launch).stream()).forEach(con -> { + con.terminate(); + }); + } }); } @@ -61,10 +60,7 @@ static void openListenerConnection(ILaunch launch, VMArguments arguments) { try { if(MavenLaunchUtils.getMavenRuntime(launch.getLaunchConfiguration()) instanceof MavenEmbeddedRuntime) { getConnection(launch).ifPresent(existing -> { - try { - existing.terminate(); - } catch(DebugException ex) { - } + existing.terminate(); throw new IllegalStateException( "Maven bridge already created for launch of" + launch.getLaunchConfiguration().getName()); }); diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java index 3bdeab71aa..e64b98d76f 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/MavenTestRunnerClient.java @@ -27,18 +27,14 @@ import org.xml.sax.SAXException; import org.eclipse.debug.core.ILaunch; -import org.eclipse.unittest.internal.junitXmlReport.TestRunHandler; import org.eclipse.unittest.launcher.ITestRunnerClient; -import org.eclipse.unittest.model.ITestCaseElement; -import org.eclipse.unittest.model.ITestElement; -import org.eclipse.unittest.model.ITestElement.FailureTrace; -import org.eclipse.unittest.model.ITestElement.Result; import org.eclipse.unittest.model.ITestRunSession; import org.eclipse.unittest.model.ITestSuiteElement; import org.apache.maven.execution.ExecutionEvent.Type; import org.eclipse.m2e.internal.launch.MavenBuildProjectDataConnection; +import org.eclipse.m2e.internal.launch.testing.copied.TestRunHandler; import org.eclipse.m2e.internal.maven.listener.MavenBuildListener; import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData; import org.eclipse.m2e.internal.maven.listener.MavenTestEvent; @@ -63,6 +59,7 @@ public MavenTestRunnerClient(ITestRunSession session) { } public void startMonitoring() { + System.out.println("---- start monitoring ---"); MavenBuildProjectDataConnection.getConnection(session.getLaunch()) .ifPresent(con -> con.addMavenBuildListener(this)); @@ -78,6 +75,7 @@ public void stopTest() { } public void stopMonitoring() { + System.out.println("---- stop Monitoring ----\r\n"); MavenBuildProjectDataConnection.getConnection(session.getLaunch()) .ifPresent(con -> con.removeMavenBuildListener(this)); } @@ -93,8 +91,14 @@ public void projectStarted(MavenProjectBuildData data) { this.projectData.set(data); } + boolean started; + public void onTestEvent(MavenTestEvent mavenTestEvent) { + System.out.println("MavenTestRunnerClient.onTestEvent()"); if(mavenTestEvent.getType() == Type.MojoSucceeded || mavenTestEvent.getType() == Type.MojoFailed) { + MavenProjectBuildData buildData = projectData.get(); +// Display.getDefault().execute(() -> { + //in any case look for the tests... Path reportDirectory = mavenTestEvent.getReportDirectory(); if(Files.isDirectory(reportDirectory)) { @@ -102,65 +106,43 @@ public void onTestEvent(MavenTestEvent mavenTestEvent) { if(parser == null) { return; } + ensureStarted(); + ITestSuiteElement projectSuite = getProjectSuite(buildData); try (Stream list = Files.list(reportDirectory)) { Iterator iterator = list.iterator(); while(iterator.hasNext()) { Path path = iterator.next(); System.out.println("Scan result file " + path); - ITestRunSession importedSession = parseFile(path, parser); - if(importedSession != null) { - ITestSuiteElement project = getProjectSuite(); - ITestSuiteElement file = session.newTestSuite(path.toString(), path.getFileName().toString(), null, - project, path.getFileName().toString(), null); - for(ITestElement element : importedSession.getChildren()) { - importTestElement(element, file); - } - } + parseFile(path, parser, projectSuite); } } catch(IOException ex) { } } +// }); } } - /** - * @param element - * @param file - */ - private void importTestElement(ITestElement element, ITestSuiteElement parent) { - if(element instanceof ITestCaseElement testcase) { - ITestCaseElement importedTestCase = session.newTestCase(parent.getId() + "." + testcase.getId(), - testcase.getTestName(), parent, testcase.getDisplayName(), testcase.getData()); - session.notifyTestStarted(importedTestCase); - FailureTrace failureTrace = testcase.getFailureTrace(); - if(failureTrace == null) { - session.notifyTestEnded(importedTestCase, testcase.isIgnored()); - } else { - session.notifyTestFailed(importedTestCase, Result.ERROR/*TODO how do we know?*/, false /*TODO how do we know?*/, - failureTrace); - } - } else if(element instanceof ITestSuiteElement suite) { - ITestSuiteElement importedTestSuite = session.newTestSuite(parent.getId() + "." + suite.getId(), - suite.getTestName(), null, parent, suite.getDisplayName(), suite.getData()); - session.notifyTestStarted(importedTestSuite); - for(ITestElement child : suite.getChildren()) { - importTestElement(child, importedTestSuite); - } - session.notifyTestEnded(importedTestSuite, false); + private synchronized void ensureStarted() { + if(!started) { + session.notifyTestSessionStarted(null); + started = true; } } /** * @return */ - private ITestSuiteElement getProjectSuite() { - return projectElementMap.computeIfAbsent(projectData.get(), data -> { + private ITestSuiteElement getProjectSuite(MavenProjectBuildData buildData) { + return projectElementMap.computeIfAbsent(buildData, data -> { Path basedir = data.projectBasedir; - return session.newTestSuite(basedir.toString(), basedir.getFileName().toString(), null, null, - data.groupId + ":" + data.artifactId, null); + ITestSuiteElement suite = session.newTestSuite(System.currentTimeMillis() + basedir.toString(), + basedir.getFileName().toString(), null, null, data.groupId + ":" + data.artifactId, null); + session.notifyTestStarted(suite); + return suite; }); } + @SuppressWarnings("restriction") private SAXParser getParser() { try { return org.eclipse.core.internal.runtime.XmlProcessorFactory.createSAXParserWithErrorOnDOCTYPE(); @@ -170,20 +152,19 @@ private SAXParser getParser() { return null; } - private ITestRunSession parseFile(Path path, SAXParser parser) { - //TODO Currently NOT working as this is internal API that is not exported! - final TestRunHandler handler = new TestRunHandler(); + private void parseFile(Path path, SAXParser parser, ITestSuiteElement parent) { + final TestRunHandler handler = new TestRunHandler(session, parent); try { parser.parse(Files.newInputStream(path), handler); - return handler.getTestRunSession(); } catch(SAXException | IOException ex) { //can't read then... - return null; } } public void close() { - // nothing to do yet... + if(started) { + session.notifyTestSessionCompleted(null); + } } } diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java index efe388a47e..aaa636ee4d 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/IXMLTags.java @@ -12,101 +12,130 @@ * IBM Corporation - initial API and implementation *******************************************************************************/ -package org.eclipse.unittest.internal.junitXmlReport; +package org.eclipse.m2e.internal.launch.testing.copied; interface IXMLTags { - String NODE_TESTRUN = "testrun"; //$NON-NLS-1$ - String NODE_TESTSUITES = "testsuites"; //$NON-NLS-1$ - String NODE_TESTSUITE = "testsuite"; //$NON-NLS-1$ - String NODE_PROPERTIES = "properties"; //$NON-NLS-1$ - String NODE_PROPERTY = "property"; //$NON-NLS-1$ - String NODE_TESTCASE = "testcase"; //$NON-NLS-1$ - String NODE_ERROR = "error"; //$NON-NLS-1$ - String NODE_FAILURE = "failure"; //$NON-NLS-1$ - String NODE_EXPECTED = "expected"; //$NON-NLS-1$ - String NODE_ACTUAL = "actual"; //$NON-NLS-1$ - String NODE_SYSTEM_OUT = "system-out"; //$NON-NLS-1$ - String NODE_SYSTEM_ERR = "system-err"; //$NON-NLS-1$ - String NODE_SKIPPED = "skipped"; //$NON-NLS-1$ - - /** - * value: String - */ - String ATTR_NAME = "name"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_LAUNCH_CONFIG_NAME = "launch_config_name"; //$NON-NLS-1$ - /** - * value: Integer - */ - String ATTR_TESTS = "tests"; //$NON-NLS-1$ - /** - * value: Integer - */ - String ATTR_STARTED = "started"; //$NON-NLS-1$ - /** - * value: Integer - */ - String ATTR_FAILURES = "failures"; //$NON-NLS-1$ - /** - * value: Integer - */ - String ATTR_ERRORS = "errors"; //$NON-NLS-1$ - /** - * value: Boolean - */ - String ATTR_IGNORED = "ignored"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_PACKAGE = "package"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_ID = "id"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_CLASSNAME = "classname"; //$NON-NLS-1$ - /** - * value: Boolean - */ - String ATTR_INCOMPLETE = "incomplete"; //$NON-NLS-1$ - /** - * value: Duration.toString() - */ - String ATTR_START_TIME = "startTime"; //$NON-NLS-1$ - /** - * value: Double (duration in seconds) - */ - String ATTR_DURATION = "time"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_MESSAGE = "message"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_DISPLAY_NAME = "displayname"; //$NON-NLS-1$ - /** - * value: Boolean - */ - String ATTR_DYNAMIC_TEST = "dynamicTest"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_DATA = "data"; //$NON-NLS-1$ - - /** - * value: String - */ - String ATTR_INCLUDE_TAGS = "include_tags"; //$NON-NLS-1$ - /** - * value: String - */ - String ATTR_EXCLUDE_TAGS = "exclude_tags"; //$NON-NLS-1$ + String NODE_TESTRUN = "testrun"; //$NON-NLS-1$ + + String NODE_TESTSUITES = "testsuites"; //$NON-NLS-1$ + + String NODE_TESTSUITE = "testsuite"; //$NON-NLS-1$ + + String NODE_PROPERTIES = "properties"; //$NON-NLS-1$ + + String NODE_PROPERTY = "property"; //$NON-NLS-1$ + + String NODE_TESTCASE = "testcase"; //$NON-NLS-1$ + + String NODE_ERROR = "error"; //$NON-NLS-1$ + + String NODE_FAILURE = "failure"; //$NON-NLS-1$ + + String NODE_EXPECTED = "expected"; //$NON-NLS-1$ + + String NODE_ACTUAL = "actual"; //$NON-NLS-1$ + + String NODE_SYSTEM_OUT = "system-out"; //$NON-NLS-1$ + + String NODE_SYSTEM_ERR = "system-err"; //$NON-NLS-1$ + + String NODE_SKIPPED = "skipped"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_NAME = "name"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_LAUNCH_CONFIG_NAME = "launch_config_name"; //$NON-NLS-1$ + + /** + * value: Integer + */ + String ATTR_TESTS = "tests"; //$NON-NLS-1$ + + /** + * value: Integer + */ + String ATTR_STARTED = "started"; //$NON-NLS-1$ + + /** + * value: Integer + */ + String ATTR_FAILURES = "failures"; //$NON-NLS-1$ + + /** + * value: Integer + */ + String ATTR_ERRORS = "errors"; //$NON-NLS-1$ + + /** + * value: Boolean + */ + String ATTR_IGNORED = "ignored"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_PACKAGE = "package"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_ID = "id"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_CLASSNAME = "classname"; //$NON-NLS-1$ + + /** + * value: Boolean + */ + String ATTR_INCOMPLETE = "incomplete"; //$NON-NLS-1$ + + /** + * value: Duration.toString() + */ + String ATTR_START_TIME = "startTime"; //$NON-NLS-1$ + + /** + * value: Double (duration in seconds) + */ + String ATTR_DURATION = "time"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_MESSAGE = "message"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_DISPLAY_NAME = "displayname"; //$NON-NLS-1$ + + /** + * value: Boolean + */ + String ATTR_DYNAMIC_TEST = "dynamicTest"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_DATA = "data"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_INCLUDE_TAGS = "include_tags"; //$NON-NLS-1$ + + /** + * value: String + */ + String ATTR_EXCLUDE_TAGS = "exclude_tags"; //$NON-NLS-1$ // public static final String ATTR_TYPE= "type"; //$NON-NLS-1$ } diff --git a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java index fc9dad0ba7..71a961d378 100644 --- a/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java +++ b/org.eclipse.m2e.launching/src/org/eclipse/m2e/internal/launch/testing/copied/TestRunHandler.java @@ -12,11 +12,10 @@ * IBM Corporation - initial API and implementation *******************************************************************************/ -package org.eclipse.unittest.internal.junitXmlReport; +package org.eclipse.m2e.internal.launch.testing.copied; -import java.time.Duration; -import java.time.Instant; import java.util.Stack; +import java.util.concurrent.atomic.AtomicInteger; import org.xml.sax.Attributes; import org.xml.sax.Locator; @@ -24,342 +23,287 @@ import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; -import org.eclipse.osgi.util.NLS; -import org.eclipse.unittest.internal.UnitTestPlugin; -import org.eclipse.unittest.internal.model.ModelMessages; -import org.eclipse.unittest.internal.model.TestCaseElement; -import org.eclipse.unittest.internal.model.TestElement; -import org.eclipse.unittest.internal.model.TestRunSession; -import org.eclipse.unittest.internal.model.TestSuiteElement; +import org.eclipse.core.runtime.OperationCanceledException; +import org.eclipse.unittest.model.ITestCaseElement; import org.eclipse.unittest.model.ITestElement; import org.eclipse.unittest.model.ITestElement.FailureTrace; import org.eclipse.unittest.model.ITestElement.Result; +import org.eclipse.unittest.model.ITestRunSession; +import org.eclipse.unittest.model.ITestSuiteElement; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IProgressMonitor; -import org.eclipse.core.runtime.NullProgressMonitor; -import org.eclipse.core.runtime.OperationCanceledException; - -import org.eclipse.debug.core.DebugPlugin; -import org.eclipse.debug.core.ILaunchConfiguration; public class TestRunHandler extends DefaultHandler { - /* - * TODO: validate (currently assumes correct XML) - */ - - private int fId; - - private TestRunSession fTestRunSession; - private TestSuiteElement fTestSuite; - private TestCaseElement fTestCase; - private final Stack fNotRun = new Stack<>(); - - private StringBuilder fFailureBuffer; - private boolean fInExpected; - private boolean fInActual; - private StringBuilder fExpectedBuffer; - private StringBuilder fActualBuffer; - - private Locator fLocator; - - private Result fStatus; - - private final IProgressMonitor fMonitor; - private int fLastReportedLine; - - /** - * Constructs a default {@link TestRunHandler} object instance - */ - public TestRunHandler() { - fMonitor = new NullProgressMonitor(); - } - - /** - * Constructs a {@link TestRunHandler} object instance - * - * @param monitor a progress monitor - */ - public TestRunHandler(IProgressMonitor monitor) { - fMonitor = monitor != null ? monitor : new NullProgressMonitor(); - } - - @Override - public void setDocumentLocator(Locator locator) { - fLocator = locator; - } - - @Override - public void startDocument() throws SAXException { - // Nothing to do - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { - if (fMonitor.isCanceled()) - throw new OperationCanceledException(); - - if (fLocator != null) { - int line = fLocator.getLineNumber(); - if (line - 20 >= fLastReportedLine) { - line -= line % 20; - fLastReportedLine = line; - fMonitor.subTask(NLS.bind(ModelMessages.TestRunHandler_lines_read, Integer.valueOf(line))); - } - } - - if (Thread.interrupted()) - throw new OperationCanceledException(); - - switch (qName) { - case IXMLTags.NODE_TESTRUN: - if (fTestRunSession == null) { - String name = attributes.getValue(IXMLTags.ATTR_NAME); - String launchConfigName = attributes.getValue(IXMLTags.ATTR_LAUNCH_CONFIG_NAME); - ILaunchConfiguration launchConfiguration = null; - if (launchConfigName != null) { - try { - for (ILaunchConfiguration config : DebugPlugin.getDefault().getLaunchManager() - .getLaunchConfigurations()) { - if (config.getName().equals(launchConfigName)) { - launchConfiguration = config; - } - } - } catch (CoreException e) { - UnitTestPlugin.log(e); - } - } - fTestRunSession = new TestRunSession(name, Instant.parse(attributes.getValue(IXMLTags.ATTR_START_TIME)), - launchConfiguration); - readDuration(fTestRunSession, attributes); - // TODO: read counts? - } else { - fTestRunSession.reset(); - } - fTestSuite = fTestRunSession; - break; - case IXMLTags.NODE_TESTSUITES: - break; - case IXMLTags.NODE_TESTSUITE: { - String name = attributes.getValue(IXMLTags.ATTR_NAME); - String pack = attributes.getValue(IXMLTags.ATTR_PACKAGE); - String suiteName = pack == null ? name : pack + "." + name; //$NON-NLS-1$ - String displayName = attributes.getValue(IXMLTags.ATTR_DISPLAY_NAME); - String data = attributes.getValue(IXMLTags.ATTR_DATA); - if (data != null && data.isBlank()) { - data = null; - } - fTestSuite = (TestSuiteElement) fTestRunSession.createTestElement(fTestSuite, getNextId(), suiteName, true, - null, false, displayName, data); - readDuration(fTestSuite, attributes); - fNotRun.push(Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_INCOMPLETE))); - break; - } - // not interested - case IXMLTags.NODE_PROPERTIES: - case IXMLTags.NODE_PROPERTY: - break; - case IXMLTags.NODE_TESTCASE: { - String name = attributes.getValue(IXMLTags.ATTR_NAME); - String classname = attributes.getValue(IXMLTags.ATTR_CLASSNAME); - String testName = name + '(' + classname + ')'; - boolean isDynamicTest = Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_DYNAMIC_TEST)).booleanValue(); - String displayName = attributes.getValue(IXMLTags.ATTR_DISPLAY_NAME); - String data = attributes.getValue(IXMLTags.ATTR_DATA); - if (data != null && data.isBlank()) { - data = null; - } - fTestCase = (TestCaseElement) fTestRunSession.createTestElement(fTestSuite, getNextId(), testName, false, 1, - isDynamicTest, displayName, data); - fNotRun.push(Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_INCOMPLETE))); - fTestCase.setIgnored(Boolean.parseBoolean(attributes.getValue(IXMLTags.ATTR_IGNORED))); - readDuration(fTestCase, attributes); - break; - } - case IXMLTags.NODE_ERROR: - // TODO: multiple failures: https://bugs.eclipse.org/bugs/show_bug.cgi?id=125296 - fStatus = Result.ERROR; - fFailureBuffer = new StringBuilder(); - break; - case IXMLTags.NODE_FAILURE: - // TODO: multiple failures: https://bugs.eclipse.org/bugs/show_bug.cgi?id=125296 - fStatus = Result.FAILURE; - fFailureBuffer = new StringBuilder(); - break; - case IXMLTags.NODE_EXPECTED: - fInExpected = true; - fExpectedBuffer = new StringBuilder(); - break; - case IXMLTags.NODE_ACTUAL: - fInActual = true; - fActualBuffer = new StringBuilder(); - break; - // not interested - case IXMLTags.NODE_SYSTEM_OUT: - case IXMLTags.NODE_SYSTEM_ERR: - break; - case IXMLTags.NODE_SKIPPED: - // before Ant 1.9.0: not an Ant JUnit tag, see - // https://bugs.eclipse.org/bugs/show_bug.cgi?id=276068 - // later: child of or , see - // https://issues.apache.org/bugzilla/show_bug.cgi?id=43969 - fStatus = Result.OK; - fFailureBuffer = new StringBuilder(); - String message = attributes.getValue(IXMLTags.ATTR_MESSAGE); - if (message != null) { - fFailureBuffer.append(message).append('\n'); - } - break; - default: - throw new SAXParseException("unknown node '" + qName + "'", fLocator); //$NON-NLS-1$//$NON-NLS-2$ - } - } - - private void readDuration(ITestElement testElement, Attributes attributes) { - if (testElement instanceof TestElement) { - TestElement element = (TestElement) testElement; - String timeString = attributes.getValue(IXMLTags.ATTR_DURATION); - if (timeString != null) { - try { - double seconds = Double.parseDouble(timeString); - long millis = (long) (seconds * 1000); - element.setDuration(Duration.ofMillis(millis)); - } catch (NumberFormatException e) { - // Ignore - } - } - } - } - - @Override - public void characters(char[] ch, int start, int length) throws SAXException { - if (fInExpected) { - fExpectedBuffer.append(ch, start, length); - - } else if (fInActual) { - fActualBuffer.append(ch, start, length); - - } else if (fFailureBuffer != null) { - fFailureBuffer.append(ch, start, length); - } - } - - @Override - public void endElement(String uri, String localName, String qName) throws SAXException { - switch (qName) { - // OK - case IXMLTags.NODE_TESTRUN: - break; - // OK - case IXMLTags.NODE_TESTSUITES: - break; - case IXMLTags.NODE_TESTSUITE: - handleTestElementEnd(fTestSuite); - fTestSuite = fTestSuite.getParent(); - // TODO: end suite: compare counters? - break; - // OK - case IXMLTags.NODE_PROPERTIES: - case IXMLTags.NODE_PROPERTY: - break; - case IXMLTags.NODE_TESTCASE: - handleTestElementEnd(fTestCase); - fTestCase = null; - break; - case IXMLTags.NODE_FAILURE: - case IXMLTags.NODE_ERROR: { - ITestElement testElement = fTestCase; - if (testElement == null) - testElement = fTestSuite; - handleFailure(testElement); - break; - } - case IXMLTags.NODE_EXPECTED: - fInExpected = false; - if (fFailureBuffer != null) { - // skip whitespace from before and nodes - fFailureBuffer.setLength(0); - } - break; - case IXMLTags.NODE_ACTUAL: - fInActual = false; - if (fFailureBuffer != null) { - // skip whitespace from before and nodes - fFailureBuffer.setLength(0); - } - break; - // OK - case IXMLTags.NODE_SYSTEM_OUT: - case IXMLTags.NODE_SYSTEM_ERR: - break; - case IXMLTags.NODE_SKIPPED: { - TestElement testElement = fTestCase; - if (testElement == null) - testElement = fTestSuite; - if (fFailureBuffer != null && fFailureBuffer.length() > 0) { - handleFailure(testElement); - testElement.setAssumptionFailed(true); - } else if (fTestCase != null) { - fTestCase.setIgnored(true); - } else { // not expected - testElement.setAssumptionFailed(true); - } - break; - } - default: - handleUnknownNode(qName); - break; - } - } - - private void handleTestElementEnd(ITestElement testElement) { - boolean completed = !fNotRun.pop().booleanValue(); - fTestRunSession.registerTestEnded((TestElement) testElement, completed); - } - - private void handleFailure(ITestElement testElement) { - if (fFailureBuffer != null) { - fTestRunSession.registerTestFailureStatus((TestElement) testElement, fStatus, - new FailureTrace(fFailureBuffer.toString(), toString(fExpectedBuffer), toString(fActualBuffer))); - fFailureBuffer = null; - fExpectedBuffer = null; - fActualBuffer = null; - fStatus = null; - } - } - - private String toString(StringBuilder buffer) { - return buffer != null ? buffer.toString() : null; - } - - private void handleUnknownNode(String qName) throws SAXException { - // TODO: just log if debug option is enabled? - String msg = "unknown node '" + qName + "'"; //$NON-NLS-1$//$NON-NLS-2$ - if (fLocator != null) { - msg += " at line " + fLocator.getLineNumber() + ", column " + fLocator.getColumnNumber(); //$NON-NLS-1$//$NON-NLS-2$ - } - throw new SAXException(msg); - } - - @Override - public void error(SAXParseException e) throws SAXException { - throw e; - } - - @Override - public void warning(SAXParseException e) throws SAXException { - throw e; - } - - private String getNextId() { - return Integer.toString(fId++); - } - - /** - * @return the parsed test run session, or null - */ - public TestRunSession getTestRunSession() { - return fTestRunSession; - } + private static final AtomicInteger fid = new AtomicInteger(); + + private ITestRunSession fTestRunSession; + + private ITestSuiteElement fTestSuite; + + private ITestCaseElement fTestCase; + + private final Stack fNotRun = new Stack<>(); + + private StringBuilder fFailureBuffer; + + private boolean fInExpected; + + private boolean fInActual; + + private StringBuilder fExpectedBuffer; + + private StringBuilder fActualBuffer; + + private Locator fLocator; + + private Result fStatus; + + private int fLastReportedLine; + + /** + * Constructs a default {@link TestRunHandler} object instance + */ + public TestRunHandler(ITestRunSession session, ITestSuiteElement parent) { + fTestRunSession = session; + this.fTestSuite = parent; + } + + @Override + public void setDocumentLocator(Locator locator) { + fLocator = locator; + } + + @Override + public void startDocument() throws SAXException { + // Nothing to do + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { + try { + if(fLocator != null) { + int line = fLocator.getLineNumber(); + if(line - 20 >= fLastReportedLine) { + line -= line % 20; + fLastReportedLine = line; + } + } + + if(Thread.interrupted()) + throw new OperationCanceledException(); + + switch(qName) { + case IXMLTags.NODE_TESTRUN: + break; + case IXMLTags.NODE_TESTSUITES: + break; + case IXMLTags.NODE_TESTSUITE: { + String name = attributes.getValue(IXMLTags.ATTR_NAME); + String pack = attributes.getValue(IXMLTags.ATTR_PACKAGE); + String suiteName = pack == null ? name : pack + "." + name; //$NON-NLS-1$ + String displayName = attributes.getValue(IXMLTags.ATTR_DISPLAY_NAME); + String data = attributes.getValue(IXMLTags.ATTR_DATA); + if(data != null && data.isBlank()) { + data = null; + } + fTestSuite = fTestRunSession.newTestSuite(getNextId(), suiteName, null, fTestSuite, displayName, data); + fNotRun.push(Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_INCOMPLETE))); + fTestRunSession.notifyTestStarted(fTestSuite); + break; + } + // not interested + case IXMLTags.NODE_PROPERTIES: + case IXMLTags.NODE_PROPERTY: + break; + case IXMLTags.NODE_TESTCASE: { + String name = attributes.getValue(IXMLTags.ATTR_NAME); + String classname = attributes.getValue(IXMLTags.ATTR_CLASSNAME); + String testName = name + '(' + classname + ')'; + //TODO dynamic test not supported by API! +// boolean isDynamicTest = Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_DYNAMIC_TEST)).booleanValue(); + String displayName = attributes.getValue(IXMLTags.ATTR_DISPLAY_NAME); + String data = attributes.getValue(IXMLTags.ATTR_DATA); + if(data != null && data.isBlank()) { + data = null; + } + fTestCase = fTestRunSession.newTestCase(getNextId(), testName, fTestSuite, displayName, data); + fNotRun.push(Boolean.valueOf(attributes.getValue(IXMLTags.ATTR_INCOMPLETE))); + //TODO incomplete versus ignored?!? +// fTestCase.setIgnored(Boolean.parseBoolean(attributes.getValue(IXMLTags.ATTR_IGNORED))); + fTestRunSession.notifyTestStarted(fTestCase); + break; + } + case IXMLTags.NODE_ERROR: + // TODO: multiple failures: https://bugs.eclipse.org/bugs/show_bug.cgi?id=125296 + fStatus = Result.ERROR; + fFailureBuffer = new StringBuilder(); + break; + case IXMLTags.NODE_FAILURE: + // TODO: multiple failures: https://bugs.eclipse.org/bugs/show_bug.cgi?id=125296 + fStatus = Result.FAILURE; + fFailureBuffer = new StringBuilder(); + break; + case IXMLTags.NODE_EXPECTED: + fInExpected = true; + fExpectedBuffer = new StringBuilder(); + break; + case IXMLTags.NODE_ACTUAL: + fInActual = true; + fActualBuffer = new StringBuilder(); + break; + // not interested + case IXMLTags.NODE_SYSTEM_OUT: + case IXMLTags.NODE_SYSTEM_ERR: + break; + case IXMLTags.NODE_SKIPPED: + // before Ant 1.9.0: not an Ant JUnit tag, see + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=276068 + // later: child of or , see + // https://issues.apache.org/bugzilla/show_bug.cgi?id=43969 + fStatus = Result.OK; + fFailureBuffer = new StringBuilder(); + String message = attributes.getValue(IXMLTags.ATTR_MESSAGE); + if(message != null) { + fFailureBuffer.append(message).append('\n'); + } + break; + default: + throw new SAXParseException("unknown node '" + qName + "'", fLocator); //$NON-NLS-1$//$NON-NLS-2$ + } + } catch(RuntimeException e) { + e.printStackTrace(); + } + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if(fInExpected) { + fExpectedBuffer.append(ch, start, length); + + } else if(fInActual) { + fActualBuffer.append(ch, start, length); + + } else if(fFailureBuffer != null) { + fFailureBuffer.append(ch, start, length); + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + try { + switch(qName) { + // OK + case IXMLTags.NODE_TESTRUN: + break; + // OK + case IXMLTags.NODE_TESTSUITES: + break; + case IXMLTags.NODE_TESTSUITE: + handleTestElementEnd(fTestSuite); + fTestSuite = fTestSuite.getParent(); + // TODO: end suite: compare counters? + break; + // OK + case IXMLTags.NODE_PROPERTIES: + case IXMLTags.NODE_PROPERTY: + break; + case IXMLTags.NODE_TESTCASE: + handleTestElementEnd(fTestCase); + fTestCase = null; + break; + case IXMLTags.NODE_FAILURE: + case IXMLTags.NODE_ERROR: { + ITestElement testElement = fTestCase; + if(testElement == null) + testElement = fTestSuite; + handleFailure(testElement, false); + break; + } + case IXMLTags.NODE_EXPECTED: + fInExpected = false; + if(fFailureBuffer != null) { + // skip whitespace from before and nodes + fFailureBuffer.setLength(0); + } + break; + case IXMLTags.NODE_ACTUAL: + fInActual = false; + if(fFailureBuffer != null) { + // skip whitespace from before and nodes + fFailureBuffer.setLength(0); + } + break; + // OK + case IXMLTags.NODE_SYSTEM_OUT: + case IXMLTags.NODE_SYSTEM_ERR: + break; + case IXMLTags.NODE_SKIPPED: { + ITestElement testElement = fTestCase; + if(testElement == null) { + testElement = fTestSuite; + } + if(fFailureBuffer != null && fFailureBuffer.length() > 0) { + handleFailure(testElement, true); + } else if(fTestCase != null) { + fStatus = Result.IGNORED; + } else { // not expected + fTestRunSession.notifyTestFailed(testElement, Result.FAILURE, false, null); + } + break; + } + default: + handleUnknownNode(qName); + break; + } + } catch(RuntimeException e) { + e.printStackTrace(); + } + } + + private void handleTestElementEnd(ITestElement testElement) { + boolean notrun = fNotRun.pop().booleanValue(); + if(notrun) { + return; + } + fTestRunSession.notifyTestEnded(testElement, fStatus == Result.IGNORED); + } + + private void handleFailure(ITestElement testElement, boolean assumptionFailed) { + if(fFailureBuffer != null) { + fTestRunSession.notifyTestFailed(testElement, fStatus, assumptionFailed, + new FailureTrace(fFailureBuffer.toString(), toString(fExpectedBuffer), toString(fActualBuffer))); + fFailureBuffer = null; + fExpectedBuffer = null; + fActualBuffer = null; + fStatus = null; + } + } + + private String toString(StringBuilder buffer) { + return buffer != null ? buffer.toString() : null; + } + + private void handleUnknownNode(String qName) throws SAXException { + // TODO: just log if debug option is enabled? + String msg = "unknown node '" + qName + "'"; //$NON-NLS-1$//$NON-NLS-2$ + if(fLocator != null) { + msg += " at line " + fLocator.getLineNumber() + ", column " + fLocator.getColumnNumber(); //$NON-NLS-1$//$NON-NLS-2$ + } + throw new SAXException(msg); + } + + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + + @Override + public void warning(SAXParseException e) throws SAXException { + throw e; + } + + private String getNextId() { + return fTestSuite.getId() + "." + fid.incrementAndGet(); + } + }