Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,131 @@ public class <Language>StepDefinitionOpener implements IStepDefinitionOpener {
- Use Eclipse's IDE.openEditor() and text editor APIs
- Handle file path resolution (relative vs. absolute)

#### 2.8 Preferences
#### 2.8 Remote Test Execution (Message Endpoint)

Implement message endpoint for real-time test execution monitoring:

##### Message Endpoint Process

Create a process that receives Cucumber messages from the test framework:

```java
public class <Framework>MessageEndpointProcess implements IProcess, EnvelopeProvider,
ISuspendResume, IDisconnect {

private static final int HANDLED_MESSAGE = 0x01;
private static final int GOOD_BY_MESSAGE = 0x00;

private ServerSocket serverSocket;
private ILaunch launch;
private List<Envelope> envelopes = new ArrayList<>();
private List<EnvelopeListener> consumers = new ArrayList<>();

public <Framework>MessageEndpointProcess(ILaunch launch) throws IOException {
this.serverSocket = new ServerSocket(0);
this.launch = launch;
launch.addProcess(this);
}

public void start() {
// Create daemon thread to listen for incoming connections
// Accept connection and read messages in loop
// Deserialize using Jackson.OBJECT_MAPPER
// Notify all registered EnvelopeListeners
// Send acknowledgment byte after each message
}

public void addBehaveArguments(List<String> args) {
// Add framework-specific arguments to inject formatter
// Include port number for socket connection
}
}
```

##### Language-Specific Formatter/Plugin

Create a formatter in the target language that sends messages to Eclipse:

**For Python/Behave:**

```python
class CucumberEclipseFormatter(Formatter):
def __init__(self, stream_opener, config):
# Read port from environment variable or config
# Connect to Eclipse socket

def _send_message(self, envelope):
# Serialize to JSON
# Send 4-byte big-endian length
# Send JSON bytes
# Wait for acknowledgment (0x01)

def step(self, step):
# Create TestStepFinished envelope
# Send to Eclipse

def eof(self):
# Send TestRunFinished
# Send 0 length
# Close connection
```

**Protocol Requirements:**

1. Connect to Eclipse on specified port
2. For each message:
- Send message length as 4-byte big-endian integer
- Send JSON-encoded Cucumber Message
- Wait for acknowledgment byte (0x01)
- If received goodbye (0x00), close connection
3. After test run:
- Send TestRunFinished message
- Send 0 length to signal end
- Wait for acknowledgment
- Close socket

**Key Points:**
- Use standard Cucumber Messages format (JSON)
- Reuse Jackson ObjectMapper for deserialization (from java.plugins bundle)
- Package formatter/plugin with bundle (e.g., in `<language>-plugins/` directory)
- Update `build.properties` to include plugin directory
- Add formatter to language runtime's plugin path (e.g., PYTHONPATH)
- Pass port via environment variable or command-line argument
- No code changes required in user's test files

##### Update Launch Delegate

Integrate message endpoint with launch configuration:

```java
@Override
public void launch(ILaunchConfiguration config, String mode,
ILaunch launch, IProgressMonitor monitor) throws CoreException {

// Create message endpoint
<Framework>MessageEndpointProcess endpoint =
new <Framework>MessageEndpointProcess(launch);

// Build launcher arguments
List<String> args = new ArrayList<>();
endpoint.addArguments(args); // Adds formatter and port

// Start endpoint listener
endpoint.start();
launch.addProcess(endpoint);

try {
// Launch test process with injected arguments
Process process = launcher.launch(pluginPath);
IProcess iProcess = DebugPlugin.newProcess(launch, process, "...");
} catch (Exception e) {
endpoint.terminate();
throw e;
}
}
```

#### 2.9 Preferences

Provide user-configurable preferences:

Expand Down
3 changes: 2 additions & 1 deletion io.cucumber.eclipse.editor/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Require-Bundle: org.eclipse.ui;bundle-version="3.5.0",
io.cucumber.tag-expressions,
org.eclipse.unittest.ui;bundle-version="1.0.0",
io.cucumber.gherkin-utils;bundle-version="9.0.0",
org.eclipse.core.expressions;bundle-version="3.9.500"
org.eclipse.core.expressions;bundle-version="3.9.500",
io.cucumber.eclipse.java.plugins;bundle-version="3.0.0"
Bundle-RequiredExecutionEnvironment: JavaSE-21
Automatic-Module-Name: io.cucumber.eclipse.editor
Bundle-ActivationPolicy: lazy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package io.cucumber.eclipse.editor.launching;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.ILog;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.model.IDisconnect;
import org.eclipse.debug.core.model.IProcess;
import org.eclipse.debug.core.model.IStreamsProxy;
import org.eclipse.debug.core.model.ISuspendResume;

import io.cucumber.eclipse.editor.Activator;
import io.cucumber.messages.types.Envelope;

/**
* Generic message endpoint process that receives Cucumber messages via socket.
*
* This class can be extended by backend-specific implementations (Java, Python, etc.)
* to provide real-time test execution monitoring in Eclipse.
*
* @author copilot
*/
public abstract class MessageEndpointProcess implements IProcess, EnvelopeProvider, ISuspendResume, IDisconnect {

private static final int HANDLED_MESSAGE = 0x01;
private static final int GOOD_BY_MESSAGE = 0x00;

private ServerSocket serverSocket;
private ILaunch launch;
private List<Envelope> envelopes = new ArrayList<>();
private List<EnvelopeListener> consumers = new ArrayList<>();
private Map<String, String> attributes = new HashMap<>();
private volatile boolean suspended;

public MessageEndpointProcess(ILaunch launch) throws IOException {
this.serverSocket = new ServerSocket(0);
this.launch = launch;
launch.addProcess(this);
}

@Override
public <T> T getAdapter(Class<T> adapter) {
return null;
}

@Override
public boolean canTerminate() {
return false;
}

@Override
public abstract String getLabel();

@Override
public ILaunch getLaunch() {
return launch;
}

@Override
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);
}

@Override
public int getExitValue() throws DebugException {
return 0;
}

protected void handleMessage(Envelope envelope) throws InterruptedException {
synchronized (this) {
while (suspended) {
wait();
}
envelopes.add(envelope);
for (EnvelopeListener consumer : consumers) {
try {
consumer.handleEnvelope(envelope);
} catch (RuntimeException e) {
ILog.get().error("Listener throws RuntimeException while handling Envelope " + envelope, e);
}
}
}
}

/**
* Start listening for incoming messages
*/
public void start() {
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
try {
Socket socket = serverSocket.accept();
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
OutputStream outputStream = socket.getOutputStream()) {
int framelength;
byte[] buffer = new byte[1024 * 1024 * 10];
while ((framelength = inputStream.readInt()) > 0) {
if (buffer.length < framelength) {
buffer = new byte[framelength];
}
inputStream.readFully(buffer, 0, framelength);
Envelope envelope = deserializeEnvelope(buffer, framelength);
try {
handleMessage(envelope);
} catch (InterruptedException e) {
break;
}
outputStream.write(HANDLED_MESSAGE);
outputStream.flush();
if (envelope.getTestRunFinished().isPresent()) {
break;
}
}
outputStream.write(GOOD_BY_MESSAGE);
outputStream.flush();
}
socket.close();
} catch (IOException e) {
// Connection closed or error
} finally {
try {
serverSocket.close();
} catch (IOException e) {
}
}
}
});
thread.setDaemon(true);
thread.start();
}

/**
* Deserialize an envelope from bytes.
* Default implementation uses Jackson from the java.plugins bundle.
* Backend-specific implementations can override this to use a different JSON library if needed.
*
* @param buffer byte buffer containing the message
* @param length length of the message in the buffer
* @return deserialized Envelope
* @throws IOException if deserialization fails
*/
protected Envelope deserializeEnvelope(byte[] buffer, int length) throws IOException {
return io.cucumber.eclipse.java.plugins.Jackson.OBJECT_MAPPER.readerFor(Envelope.class).readValue(buffer, 0, length);
}

/**
* Get the port number that the server socket is listening on
*
* @return port number
*/
public int getPort() {
return serverSocket.getLocalPort();
}

public boolean isTerminated() {
return serverSocket.isClosed();
}

public void terminate() {
try {
serverSocket.close();
} catch (IOException e) {
}
}

@Override
public void addEnvelopeListener(EnvelopeListener listener) {
synchronized (this) {
if (!consumers.contains(listener)) {
consumers.add(listener);
for (Envelope envelope : envelopes) {
listener.handleEnvelope(envelope);
}
}
}
}

@Override
public synchronized void removeEnvelopeListener(EnvelopeListener listener) {
synchronized (this) {
consumers.remove(listener);
}
}

@Override
public boolean canDisconnect() {
return false;
}

@Override
public void disconnect() throws DebugException {
// Not implemented
}

@Override
public boolean isDisconnected() {
return false;
}

@Override
public boolean canResume() {
return isSuspended();
}

@Override
public boolean canSuspend() {
return !isSuspended();
}

@Override
public boolean isSuspended() {
return suspended;
}

@Override
public void resume() throws DebugException {
suspended = false;
synchronized (this) {
notifyAll();
}
}

@Override
public void suspend() throws DebugException {
suspended = true;
}
}
Loading
Loading