Skip to content

Commit

Permalink
Trace test execution directories in the remote maven execution
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
laeubi committed Feb 26, 2024
1 parent 8789e4c commit 50518ae
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
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.MavenBuildListener;
import org.eclipse.m2e.internal.maven.listener.MavenProjectBuildData;
import org.eclipse.m2e.internal.maven.listener.MavenTestEvent;


public class MavenBuildProjectDataConnection {
Expand Down Expand Up @@ -74,9 +76,21 @@ static void openListenerConnection(ILaunch launch, VMArguments arguments) {

Map<ArtifactKey, MavenProjectBuildData> projects = new ConcurrentHashMap<>();

MavenBuildConnection connection = M2EMavenBuildDataBridge.prepareConnection(
launch.getLaunchConfiguration().getName(),
d -> projects.put(new ArtifactKey(d.groupId, d.artifactId, d.version, null), d));
MavenBuildConnection 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);
}

@Override
public void onTestEvent(MavenTestEvent mavenTestEvent) {
System.out.println(
"Test event " + mavenTestEvent.getType() + " for directory " + mavenTestEvent.getReportDirectory());

}
});

if(LAUNCH_PROJECT_DATA.putIfAbsent(launch, new MavenBuildConnectionData(projects, connection)) != null) {
connection.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;


/**
Expand Down
2 changes: 1 addition & 1 deletion org.eclipse.m2e.maven.runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</parent>

<artifactId>org.eclipse.m2e.maven.runtime</artifactId>
<version>3.9.600-SNAPSHOT</version>
<version>3.9.601-SNAPSHOT</version>
<packaging>jar</packaging>

<name>M2E Embedded Maven Runtime (includes Incubating components)</name>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ";;";
Expand All @@ -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 {
Expand All @@ -76,81 +66,38 @@ 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);
}

/**
* <p>
* This method is supposed to be called from M2E within the Eclipse-IDE JVM.
* </p>
*
* @param dataSet the data-set to parse
* @return the {@link MavenProjectBuildData} parsed from the given string
*/
private static MavenProjectBuildData parseMavenBuildProject(String dataSet) {
Map<String, String> 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<String, String> 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
Expand All @@ -163,7 +110,7 @@ public static final class MavenProjectBuildData {
* @return the preapre {@link MavenBuildConnection}
* @throws IOException
*/
public static MavenBuildConnection prepareConnection(String label, Consumer<MavenProjectBuildData> datasetListener)
public static MavenBuildConnection prepareConnection(String label, MavenBuildListener datasetListener)
throws IOException {

// TODO: use UNIX domain socket once Java-17 is required by Maven
Expand All @@ -187,9 +134,21 @@ public static MavenBuildConnection prepareConnection(String label, Consumer<Mave
for (int terminatorIndex; (terminatorIndex = message.indexOf(DATA_SET_SEPARATOR)) >= 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()));
datasetListener.projectStarted(buildData);
} else if (dataSet.startsWith(M2eMojoExecutionListener.TEST_START_EVENT)) {
String path = dataSet.substring(M2eMojoExecutionListener.TEST_START_EVENT.length());
datasetListener.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());
datasetListener.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());
datasetListener.onTestEvent(new MavenTestEvent(Type.MojoFailed, Paths.get(path)));
}
}
// Explicit cast for compatibility with covariant return type on JDK 9's
// ByteBuffer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/********************************************************************************
* 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 javax.inject.Named;
import javax.inject.Singleton;

import org.apache.maven.eventspy.EventSpy;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.ExecutionEvent.Type;

import com.google.inject.Inject;

/**
* 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
*
*/
@Named("m2e")
@Singleton
public class M2eEventSpy implements EventSpy {

private M2EMavenBuildDataBridge bridge;
static final String PROJECT_START_EVENT = "PSE#";

@Inject
public M2eEventSpy(M2EMavenBuildDataBridge bridge) {
this.bridge = bridge;
}

@Override
public void init(Context context) throws Exception {

}

@Override
public void onEvent(Object event) throws Exception {
if (!bridge.isActive()) {
return;
}
if (event instanceof ExecutionEvent) {
ExecutionEvent executionEvent = (ExecutionEvent) event;
if (executionEvent.getType() == Type.ProjectStarted) {
String message = M2eEventSpy.PROJECT_START_EVENT
+ MavenProjectBuildData.serializeProjectData(executionEvent.getProject());
bridge.sendMessage(message);
}
}
}

@Override
public void close() throws Exception {

}

}
Loading

0 comments on commit 50518ae

Please sign in to comment.