From 54d5f41440b2dd2792504d561ba4df22d1fbc868 Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Mon, 9 Jun 2025 12:29:31 +0100 Subject: [PATCH 1/8] first test debugging a fix title expression --- .../epsilon/eol/dap/EpsilonDebugAdapter.java | 9 +- .../src/org/eclipse/epsilon/evl/dom/Fix.java | 19 +++- .../epsilon/11-validation.evl | 6 ++ .../test/AbstractEpsilonDebugAdapterTest.java | 26 +++--- .../dap/test/AbstractExecutionQueueTest.java | 2 +- .../test/EpsilonDebugAdapterTestSuite.java | 2 + .../eol/dap/test/evl/EvlFixDebugTest.java | 90 +++++++++++++++++++ 7 files changed, 135 insertions(+), 19 deletions(-) create mode 100644 tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java diff --git a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java index cae952f42a..b086246070 100644 --- a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java +++ b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java @@ -132,7 +132,7 @@ protected class ModuleCompletionListener implements IExecutionListener { @Override public void aboutToExecute(ModuleElement ast, IEolContext context) { - if (ast.getParent() == null) { + if (ast.getParent() == null || runningRoots.isEmpty()) { runningRoots.add(ast); if (topElement == null) { @@ -159,8 +159,8 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { } } - if (ast instanceof IEolModule) { - ThreadState threadState = attachTo((IEolModule) ast); + if (ast.getModule() instanceof IEolModule) { + ThreadState threadState = attachTo((IEolModule) ast.getModule()); sendThreadEvent(threadState.getThreadId(), ThreadEventArgumentsReason.STARTED); } } @@ -168,8 +168,7 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { @Override public void finishedExecuting(ModuleElement ast, Object result, IEolContext context) { - if (ast.getParent() == null) { - runningRoots.remove(ast); + if (runningRoots.remove(ast)) { if (ast instanceof IEolModule) { final IEolModule eolModule = (IEolModule) ast; eolModule.getContext().getOutputStream().flush(); diff --git a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/dom/Fix.java b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/dom/Fix.java index 2b71c7bf9d..b6890c2a73 100644 --- a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/dom/Fix.java +++ b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/dom/Fix.java @@ -17,6 +17,7 @@ import org.eclipse.epsilon.eol.exceptions.EolRuntimeException; import org.eclipse.epsilon.eol.execute.context.FrameType; import org.eclipse.epsilon.eol.execute.context.Variable; +import org.eclipse.epsilon.evl.IEvlModule; import org.eclipse.epsilon.evl.execute.context.IEvlContext; import org.eclipse.epsilon.evl.parse.EvlParser; @@ -35,19 +36,31 @@ public void build(AST cst, IModule module) { } public String getTitle(Object self, IEvlContext context) throws EolRuntimeException{ - return titleBlock.execute(context, true, FrameType.UNPROTECTED); + // Using the executor factory is needed to allow debugging the fix expression + return (String) getModule().getContext().getExecutorFactory().execute(titleBlock, context); } public void execute(Object self, IEvlContext context) throws EolRuntimeException { - bodyBlock.execute(context, true, FrameType.UNPROTECTED); + getModule().getContext().getExecutorFactory().execute(bodyBlock, context); } public boolean appliesTo(Object self, IEvlContext context) throws EolRuntimeException { if (guardBlock != null) { - return guardBlock.execute(context, Variable.createReadOnlyVariable("self", self)); + // Using the executor factory is needed to allow debugging the guard expression + try { + context.getFrameStack().enterLocal(FrameType.UNPROTECTED, bodyBlock, Variable.createReadOnlyVariable("self", self)); + return (Boolean) getModule().getContext().getExecutorFactory().execute(guardBlock, context); + } finally { + context.getFrameStack().leaveLocal(bodyBlock); + } } else return true; } + + @Override + public IEvlModule getModule() { + return (IEvlModule) super.getModule(); + } public void accept(IEvlVisitor visitor) { visitor.visit(this); diff --git a/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl b/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl index 48fb77429d..54fd6d12ac 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl +++ b/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl @@ -9,5 +9,11 @@ context Person { var l = self.lastName; return l.isDefined() and l.trim().length() > 0; } + fix { + title: 'Add "NoLastName" as placeholder' + do { + self.lastName = 'NoLastName'; + } + } } } \ No newline at end of file diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java index 8b5d46a797..ac637a91b4 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java @@ -22,6 +22,9 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; @@ -64,6 +67,8 @@ public abstract class AbstractEpsilonDebugAdapterTest { /** Timeout used for various assertions in this base class. */ protected static final int TIMEOUT_SECONDS = 10; + protected ExecutorService executor; + @Rule public Timeout globalTimeout = Timeout.seconds(TIMEOUT_SECONDS * 2); @@ -132,7 +137,7 @@ public List getBreakpointEvents() { protected IEolModule module; protected EpsilonDebugAdapter adapter; protected TestClient client; - protected Thread epsilonThread; + protected Future runModuleResult; protected abstract void setupModule() throws Exception; @@ -145,6 +150,7 @@ protected void setupInitializeArguments(InitializeRequestArguments args) { @Before public void setup() throws Exception { + executor = Executors.newCachedThreadPool(); setupModule(); this.adapter = new EpsilonDebugAdapter(); @@ -165,7 +171,12 @@ public void setup() throws Exception { @After public void teardown() { + if (module != null) { + module.getContext().getModelRepository().dispose(); + module.getContext().dispose(); + } adapter.disconnect(new DisconnectArguments()); + executor.shutdownNow(); } protected void assertStoppedBecauseOf(final String expectedReason) throws InterruptedException { @@ -256,20 +267,15 @@ protected SourceBreakpoint createBreakpoint(final int line) { } protected void onAttach() { - epsilonThread = new Thread(this::runModule); - epsilonThread.setName("EpsilonDebuggee"); - epsilonThread.start(); + runModuleResult = executor.submit(this::runModule); } - protected void runModule() { + protected Object runModule() { try { - module.execute(); + return module.execute(); } catch (Throwable e) { e.printStackTrace(); - } finally { - module.getContext().getModelRepository().dispose(); - module.getContext().dispose(); - module = null; + return null; } } diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java index 1595c95edc..ce738543e8 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java @@ -34,7 +34,7 @@ protected void shutdown() throws Exception { // Ensures the program has finished running, and that the script thread has died assertProgramCompletedSuccessfully(); - epsilonThread.join(); + runModuleResult.get(); } } \ No newline at end of file diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/EpsilonDebugAdapterTestSuite.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/EpsilonDebugAdapterTestSuite.java index 4ab1a2aab9..5ea24fa1ca 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/EpsilonDebugAdapterTestSuite.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/EpsilonDebugAdapterTestSuite.java @@ -42,6 +42,7 @@ import org.eclipse.epsilon.eol.dap.test.etl.EtlDebugTest; import org.eclipse.epsilon.eol.dap.test.eunit.EUnitDebugTest; import org.eclipse.epsilon.eol.dap.test.evl.EvlDebugTest; +import org.eclipse.epsilon.eol.dap.test.evl.EvlFixDebugTest; import org.eclipse.epsilon.eol.dap.test.flock.FlockDebugTest; import org.eclipse.epsilon.eol.dap.test.pinset.PinsetDebugTest; import org.junit.runner.RunWith; @@ -77,6 +78,7 @@ ClasspathEgxTest.class, EgxErrorInEglTest.class, EvlDebugTest.class, + EvlFixDebugTest.class, EtlDebugTest.class, EclDebugTest.class, EmlDebugTest.class, diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java new file mode 100644 index 0000000000..7458334185 --- /dev/null +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2024 The University of York. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.epsilon.eol.dap.test.evl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.Future; + +import org.eclipse.epsilon.eol.dap.test.AbstractEpsilonDebugAdapterTest; +import org.eclipse.epsilon.eol.dap.test.metamodel.Person; +import org.eclipse.epsilon.eol.models.java.JavaModel; +import org.eclipse.epsilon.evl.EvlModule; +import org.eclipse.epsilon.evl.execute.UnsatisfiedConstraint; +import org.eclipse.lsp4j.debug.ContinueArguments; +import org.eclipse.lsp4j.debug.ScopesResponse; +import org.eclipse.lsp4j.debug.SetBreakpointsResponse; +import org.eclipse.lsp4j.debug.StackTraceResponse; +import org.eclipse.lsp4j.debug.StoppedEventArgumentsReason; +import org.eclipse.lsp4j.debug.Variable; +import org.eclipse.lsp4j.debug.VariablesResponse; +import org.junit.Test; + +public class EvlFixDebugTest extends AbstractEpsilonDebugAdapterTest { + + private static final File SCRIPT_FILE = new File(BASE_RESOURCE_FOLDER, "11-validation.evl"); + + @Override + protected void setupModule() throws Exception { + this.module = new EvlModule(); + module.parse(SCRIPT_FILE); + + final ArrayList objects = new ArrayList(); + objects.add(new Person("John", null)); + final JavaModel model = new JavaModel("M", objects, new ArrayList<>(Arrays.asList(Person.class))); + module.getContext().getModelRepository().addModel(model); + } + + @Test + public void canStopInsideFixTitleExpression() throws Exception { + SetBreakpointsResponse breakpoints = adapter.setBreakpoints( + createBreakpoints(createBreakpoint(13))).get(); + assertTrue("The breakpoint on the file should be recognised", + breakpoints.getBreakpoints()[0].isVerified()); + assertEquals("The breakpoint should be verified on the fix expression", + (Integer) 13, breakpoints.getBreakpoints()[0].getLine()); + attach(); + + // Wait for the EVL script to run + runModuleResult.get(); + + // Try to get the title of the first fix + Future futureTitle = executor.submit(() -> { + Collection allUnsatisfied = + ((EvlModule) module).getContext().getUnsatisfiedConstraints(); + UnsatisfiedConstraint unsatisfied = allUnsatisfied.iterator().next(); + return unsatisfied.getFixes().get(0).getTitle(); + }); + + assertStoppedBecauseOf(StoppedEventArgumentsReason.BREAKPOINT); + assertFalse("Computing the title should not be done yet", futureTitle.isDone()); + StackTraceResponse stackTrace = getStackTrace(); + assertEquals("Shold be stopped at the fix title expression line", + 13, stackTrace.getStackFrames()[0].getLine()); + + ScopesResponse scopes = getScopes(stackTrace.getStackFrames()[1]); + VariablesResponse variables = getVariables(scopes.getScopes()[0]); + Map varsByName = getVariablesByName(variables); + Variable personVariable = varsByName.get("self"); + assertNotNull("The second stack frame should have a 'self' variable", personVariable); + + adapter.continue_(new ContinueArguments()).get(); + assertNotNull(futureTitle.get()); + assertProgramCompletedSuccessfully(); + } +} From cde5290c54604f0a3bc912372f8fee176bbea535 Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Mon, 9 Jun 2025 13:28:03 +0100 Subject: [PATCH 2/8] Further tweaks to pass DAP test suite --- .../src/org/eclipse/epsilon/emc/emf/AbstractEmfModel.java | 2 +- .../org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java | 6 +++--- .../eol/dap/test/AbstractEpsilonDebugAdapterTest.java | 8 +++++--- .../epsilon/eol/dap/test/eol/StandaloneEolTest.java | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/org.eclipse.epsilon.emc.emf/src/org/eclipse/epsilon/emc/emf/AbstractEmfModel.java b/plugins/org.eclipse.epsilon.emc.emf/src/org/eclipse/epsilon/emc/emf/AbstractEmfModel.java index aa15f74a3c..710b80e8d9 100644 --- a/plugins/org.eclipse.epsilon.emc.emf/src/org/eclipse/epsilon/emc/emf/AbstractEmfModel.java +++ b/plugins/org.eclipse.epsilon.emc.emf/src/org/eclipse/epsilon/emc/emf/AbstractEmfModel.java @@ -363,7 +363,7 @@ protected boolean deleteElementInModel(Object instance) throws EolRuntimeExcepti @Override public boolean owns(Object instance) { - if (instance instanceof EObject) { + if (instance instanceof EObject && modelImpl != null) { EObject eObject = (EObject) instance; Resource eObjectResource = eObject.eResource(); diff --git a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java index b086246070..260214352c 100644 --- a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java +++ b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java @@ -169,11 +169,11 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { @Override public void finishedExecuting(ModuleElement ast, Object result, IEolContext context) { if (runningRoots.remove(ast)) { - if (ast instanceof IEolModule) { - final IEolModule eolModule = (IEolModule) ast; + if (ast.getModule() instanceof IEolModule) { + final IEolModule eolModule = (IEolModule) ast.getModule(); eolModule.getContext().getOutputStream().flush(); eolModule.getContext().getErrorStream().flush(); - removeThreadFor((IEolModule) ast); + removeThreadFor(eolModule); } } if (ast == topElement) { diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java index ac637a91b4..7be2cde3c3 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java @@ -170,13 +170,15 @@ public void setup() throws Exception { } @After - public void teardown() { + public void teardown() throws InterruptedException { + adapter.disconnect(new DisconnectArguments()); + executor.shutdown(); + executor.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (module != null) { module.getContext().getModelRepository().dispose(); module.getContext().dispose(); } - adapter.disconnect(new DisconnectArguments()); - executor.shutdownNow(); } protected void assertStoppedBecauseOf(final String expectedReason) throws InterruptedException { diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eol/StandaloneEolTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eol/StandaloneEolTest.java index 8c51c0eecd..f173b46aa5 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eol/StandaloneEolTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eol/StandaloneEolTest.java @@ -156,7 +156,7 @@ public void breakThenTerminate() throws Exception { attach(); assertStoppedBecauseOf(StoppedEventArgumentsReason.BREAKPOINT); - adapter.terminate(new TerminateArguments()); + adapter.terminate(new TerminateArguments()).get(); assertProgramFailed(); } From d8ed9883fc13c377c6f0c38f67ee69efa22fea92 Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Mon, 9 Jun 2025 15:27:46 +0100 Subject: [PATCH 3/8] Fix most test regressions except EML+ECL --- .../epsilon/eol/dap/EpsilonDebugAdapter.java | 46 +++++++++++-------- .../epsilon/eol/debug/EolDebugger.java | 8 +++- .../epsilon/eol/debug/IEolDebugger.java | 6 +++ .../epsilon/evl/debug/EvlDebugger.java | 23 ++++++++++ .../test/AbstractEpsilonDebugAdapterTest.java | 3 +- .../eol/dap/test/eml/EmlDebugTest.java | 1 - .../eol/dap/test/evl/EvlFixDebugTest.java | 8 +++- 7 files changed, 72 insertions(+), 23 deletions(-) diff --git a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java index 260214352c..d2133025d2 100644 --- a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java +++ b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java @@ -59,6 +59,7 @@ import org.eclipse.epsilon.eol.execute.context.FrameType; import org.eclipse.epsilon.eol.execute.context.IEolContext; import org.eclipse.epsilon.eol.execute.context.SingleFrame; +import org.eclipse.epsilon.eol.execute.control.ExecutionController; import org.eclipse.epsilon.eol.execute.control.IExecutionListener; import org.eclipse.lsp4j.debug.Breakpoint; import org.eclipse.lsp4j.debug.BreakpointEventArguments; @@ -127,7 +128,7 @@ public class EpsilonDebugAdapter implements IDebugProtocolServer { * be propagated across all scripts being launched (e.g. from EGX to EGL scripts). */ protected class ModuleCompletionListener implements IExecutionListener { - private ModuleElement topElement; + private IEolModule topModule; private final Set runningRoots = new HashSet<>(); @Override @@ -135,13 +136,13 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { if (ast.getParent() == null || runningRoots.isEmpty()) { runningRoots.add(ast); - if (topElement == null) { + if (topModule == null && ast.getModule() instanceof IEolModule) { /* - * The very first node we will run is assumed to be the "top" element of our - * program. When this completes, the program will be assumed to have completed - * its execution. + * The very first module we will run is assumed to be the "main" module of our + * program. When this is considered terminated, the whole program will be considered + * terminated. */ - topElement = ast; + topModule = (IEolModule) ast.getModule(); } /* @@ -174,13 +175,17 @@ public void finishedExecuting(ModuleElement ast, Object result, IEolContext cont eolModule.getContext().getOutputStream().flush(); eolModule.getContext().getErrorStream().flush(); removeThreadFor(eolModule); + + if (topModule != null && ast.getModule() == topModule) { + ExecutionController execController = topModule.getContext().getExecutorFactory().getExecutionController(); + if (execController instanceof IEolDebugger && ((IEolDebugger) execController).isDoneAfterModuleElement(ast)) { + sendTerminated(); + sendExited(0); + topModule = null; + } + } } } - if (ast == topElement) { - sendTerminated(); - sendExited(0); - topElement = null; - } } @Override @@ -191,15 +196,18 @@ public void finishedExecutingWithException(ModuleElement ast, EolRuntimeExceptio eolModule.getContext().getOutputStream().flush(); eolModule.getContext().getErrorStream().flush(); removeThreadFor(eolModule); - } - if (ast == topElement) { - if (ast instanceof IEolModule) { + + if (topModule != null && ast == topModule) { // Report the exception that was propagated to the top module - ((IEolModule) ast).getContext().getErrorStream().println(exception.toString()); + topModule.getContext().getErrorStream().println(exception.toString()); + + ExecutionController execController = topModule.getContext().getExecutorFactory().getExecutionController(); + if (execController instanceof IEolDebugger && ((IEolDebugger) execController).isDoneAfterModuleElement(ast)) { + sendTerminated(); + sendExited(1); + topModule = null; + } } - sendTerminated(); - sendExited(1); - topElement = null; } } @@ -802,6 +810,8 @@ public CompletableFuture terminate(TerminateArguments args) { return CompletableFuture.runAsync(() -> { this.isTerminated = true; resumeAllThreads(); + sendTerminated(); + sendExited(2); }); } diff --git a/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/EolDebugger.java b/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/EolDebugger.java index 5cfe11658a..c7d45415d3 100644 --- a/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/EolDebugger.java +++ b/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/EolDebugger.java @@ -101,8 +101,6 @@ public void control(ModuleElement ast, IEolContext context) { } catch (InterruptedException ex) { ex.printStackTrace(); } - - if (isTerminated()) return; } @Override @@ -137,6 +135,12 @@ public void stepReturn() { stopAfterFrameStackSizeDropsBelow = frameStackSize(); } + @Override + public boolean isDoneAfterModuleElement(ModuleElement ast) { + // Default behaviour: execution is done after running the module + return ast == getModule(); + } + private boolean controls(ModuleElement ast) { // Top level element or block if (ast.getParent() == null || ast instanceof StatementBlock) { diff --git a/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/IEolDebugger.java b/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/IEolDebugger.java index 47b2771658..a35e78da24 100644 --- a/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/IEolDebugger.java +++ b/plugins/org.eclipse.epsilon.eol.engine/src/org/eclipse/epsilon/eol/debug/IEolDebugger.java @@ -32,4 +32,10 @@ public interface IEolDebugger extends ExecutionController { Integer getStopAfterFrameStackSizeDropsBelow(); + /** + * Returns true iff the execution will complete after the + * given module element completes (i.e. no further elements + * will be run from this execution). + */ + boolean isDoneAfterModuleElement(ModuleElement ast); } \ No newline at end of file diff --git a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java index 0701a43134..4c1b1ea62b 100644 --- a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java +++ b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java @@ -11,9 +11,11 @@ import org.eclipse.epsilon.common.module.ModuleElement; import org.eclipse.epsilon.eol.debug.EolDebugger; +import org.eclipse.epsilon.evl.EvlModule; import org.eclipse.epsilon.evl.dom.Constraint; import org.eclipse.epsilon.evl.dom.ConstraintContext; import org.eclipse.epsilon.evl.dom.Fix; +import org.eclipse.epsilon.evl.execute.UnsatisfiedConstraint; public class EvlDebugger extends EolDebugger { @@ -21,5 +23,26 @@ public class EvlDebugger extends EolDebugger { protected boolean isStructuralBlock(ModuleElement ast) { return super.isStructuralBlock(ast) || ast instanceof ConstraintContext || ast instanceof Constraint || ast instanceof Fix; } + + @Override + public boolean isDoneAfterModuleElement(ModuleElement ast) { + if (super.isDoneAfterModuleElement(ast)) { + for (UnsatisfiedConstraint unsatisfied : getModule().getContext().getUnsatisfiedConstraints()) { + if (!unsatisfied.getFixes().isEmpty()) { + // There is at least one unsatisfied constraint with fixes: leave it running + return false; + } + } + + // no unsatisfied constraints with fixes: EVL script will end as usual + return true; + } + return false; + } + + @Override + public EvlModule getModule() { + return (EvlModule) super.getModule(); + } } diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java index 7be2cde3c3..69a37ddbab 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractEpsilonDebugAdapterTest.java @@ -10,6 +10,7 @@ package org.eclipse.epsilon.eol.dap.test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -198,7 +199,7 @@ protected void assertProgramCompletedSuccessfully() throws InterruptedException protected void assertProgramFailed() throws InterruptedException { client.isExited.tryAcquire(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertNotNull("The script should have exited within " + TIMEOUT_SECONDS + " seconds", client.exitedArgs); - assertEquals("The script should have completed its execution with an error", 1, client.exitedArgs.getExitCode()); + assertNotEquals("The script should have completed its execution with an error", 0, client.exitedArgs.getExitCode()); } protected StackTraceResponse getStackTrace() throws Exception { diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eml/EmlDebugTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eml/EmlDebugTest.java index 87f0ae5ead..b891b33780 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eml/EmlDebugTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/eml/EmlDebugTest.java @@ -28,7 +28,6 @@ import org.eclipse.lsp4j.debug.SetBreakpointsResponse; import org.eclipse.lsp4j.debug.StoppedEventArgumentsReason; import org.eclipse.lsp4j.debug.Variable; -import org.junit.Before; import org.junit.Test; public class EmlDebugTest extends AbstractEpsilonDebugAdapterTest { diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java index 7458334185..acad7eb8f3 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java @@ -31,6 +31,7 @@ import org.eclipse.lsp4j.debug.SetBreakpointsResponse; import org.eclipse.lsp4j.debug.StackTraceResponse; import org.eclipse.lsp4j.debug.StoppedEventArgumentsReason; +import org.eclipse.lsp4j.debug.TerminateArguments; import org.eclipse.lsp4j.debug.Variable; import org.eclipse.lsp4j.debug.VariablesResponse; import org.junit.Test; @@ -85,6 +86,11 @@ public void canStopInsideFixTitleExpression() throws Exception { adapter.continue_(new ContinueArguments()).get(); assertNotNull(futureTitle.get()); - assertProgramCompletedSuccessfully(); + + // We need to explicitly terminate as there is a lingering fix that could be applied + adapter.terminate(new TerminateArguments()).get(); + + // Terminated programs exit with a non-zero status + assertProgramFailed(); } } From 7b5674688cc4e88394de1dcb6cec526a6ed12a5c Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Mon, 9 Jun 2025 16:43:45 +0100 Subject: [PATCH 4/8] Reduce code duplication --- .../epsilon/eol/dap/EpsilonDebugAdapter.java | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java index d2133025d2..abac399bbd 100644 --- a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java +++ b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java @@ -139,8 +139,8 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { if (topModule == null && ast.getModule() instanceof IEolModule) { /* * The very first module we will run is assumed to be the "main" module of our - * program. When this is considered terminated, the whole program will be considered - * terminated. + * program. When this is considered "done", the debugging session will be considered + * completed. */ topModule = (IEolModule) ast.getModule(); } @@ -169,42 +169,27 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { @Override public void finishedExecuting(ModuleElement ast, Object result, IEolContext context) { - if (runningRoots.remove(ast)) { - if (ast.getModule() instanceof IEolModule) { - final IEolModule eolModule = (IEolModule) ast.getModule(); - eolModule.getContext().getOutputStream().flush(); - eolModule.getContext().getErrorStream().flush(); - removeThreadFor(eolModule); - - if (topModule != null && ast.getModule() == topModule) { - ExecutionController execController = topModule.getContext().getExecutorFactory().getExecutionController(); - if (execController instanceof IEolDebugger && ((IEolDebugger) execController).isDoneAfterModuleElement(ast)) { - sendTerminated(); - sendExited(0); - topModule = null; - } - } - } - } + finishedExecutingWithException(ast, null, context); } @Override public void finishedExecutingWithException(ModuleElement ast, EolRuntimeException exception, IEolContext context) { - if (ast.getParent() == null && ast instanceof IEolModule) { - final IEolModule eolModule = (IEolModule) ast; - + if (runningRoots.remove(ast) && ast.getModule() instanceof IEolModule) { + final IEolModule eolModule = (IEolModule) ast.getModule(); eolModule.getContext().getOutputStream().flush(); eolModule.getContext().getErrorStream().flush(); removeThreadFor(eolModule); if (topModule != null && ast == topModule) { - // Report the exception that was propagated to the top module - topModule.getContext().getErrorStream().println(exception.toString()); + if (exception != null) { + // Report the exception that was propagated to the top module + topModule.getContext().getErrorStream().println(exception.toString()); + } ExecutionController execController = topModule.getContext().getExecutorFactory().getExecutionController(); if (execController instanceof IEolDebugger && ((IEolDebugger) execController).isDoneAfterModuleElement(ast)) { sendTerminated(); - sendExited(1); + sendExited(exception == null ? 0 : 1); topModule = null; } } From f872fbb9dbcba96fce0b83e486c4398fa37086b6 Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Mon, 9 Jun 2025 16:47:22 +0100 Subject: [PATCH 5/8] Fix for EML+ECL regression after changes --- .../src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java index abac399bbd..cc431b8fe8 100644 --- a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java +++ b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java @@ -143,6 +143,9 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { * completed. */ topModule = (IEolModule) ast.getModule(); + while (topModule.getParentModule() != null) { + topModule = topModule.getParentModule(); + } } /* From 7fc8736be8418f7c2084a8bba2a7885c8db32305 Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Mon, 9 Jun 2025 18:57:47 +0100 Subject: [PATCH 6/8] Tweak EVL Eclipse UI to allow for stopping during an EVL fix --- ...bug Adapter on 09-validate from Ant.launch | 19 ++++ .../build.xml | 13 +++ .../epsilon/09-validate.evl | 19 ++++ .../epsilon/models/invalidPerson.model | 4 + .../epsilon/eol/dap/EpsilonDebugAdapter.java | 44 ++++++-- .../epsilon/evl/dt/views/ValidationView.java | 26 +++-- .../epsilon/evl/debug/EvlDebugger.java | 4 +- .../epsilon/11-validation.evl | 9 +- .../dap/test/AbstractExecutionQueueTest.java | 2 +- .../eol/dap/test/evl/EvlFixDebugTest.java | 103 ++++++++++++++++-- 10 files changed, 211 insertions(+), 32 deletions(-) create mode 100644 examples/org.eclipse.epsilon.examples.eol.dap/Run Debug Adapter on 09-validate from Ant.launch create mode 100644 examples/org.eclipse.epsilon.examples.eol.dap/epsilon/09-validate.evl create mode 100644 examples/org.eclipse.epsilon.examples.eol.dap/epsilon/models/invalidPerson.model diff --git a/examples/org.eclipse.epsilon.examples.eol.dap/Run Debug Adapter on 09-validate from Ant.launch b/examples/org.eclipse.epsilon.examples.eol.dap/Run Debug Adapter on 09-validate from Ant.launch new file mode 100644 index 0000000000..2beba376da --- /dev/null +++ b/examples/org.eclipse.epsilon.examples.eol.dap/Run Debug Adapter on 09-validate from Ant.launch @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/examples/org.eclipse.epsilon.examples.eol.dap/build.xml b/examples/org.eclipse.epsilon.examples.eol.dap/build.xml index ffc7b82381..6d1994c5ac 100644 --- a/examples/org.eclipse.epsilon.examples.eol.dap/build.xml +++ b/examples/org.eclipse.epsilon.examples.eol.dap/build.xml @@ -38,6 +38,19 @@ + + + + + + + + + + diff --git a/examples/org.eclipse.epsilon.examples.eol.dap/epsilon/09-validate.evl b/examples/org.eclipse.epsilon.examples.eol.dap/epsilon/09-validate.evl new file mode 100644 index 0000000000..1daa736322 --- /dev/null +++ b/examples/org.eclipse.epsilon.examples.eol.dap/epsilon/09-validate.evl @@ -0,0 +1,19 @@ +context Person { + constraint HasFirstName { + check: self.firstName.isDefined() + message: 'Missing first name' + } + constraint HasLastName { + guard: self.firstName.isDefined() + check: self.lastName.isDefined() + message: 'Missing last name for ' + self.firstName + fix { + guard: true + title: 'Add placeholder last name' + do { + var newLastName = 'Unknown'; + self.lastName = newLastName; + } + } + } +} \ No newline at end of file diff --git a/examples/org.eclipse.epsilon.examples.eol.dap/epsilon/models/invalidPerson.model b/examples/org.eclipse.epsilon.examples.eol.dap/epsilon/models/invalidPerson.model new file mode 100644 index 0000000000..8fc2c6fdaf --- /dev/null +++ b/examples/org.eclipse.epsilon.examples.eol.dap/epsilon/models/invalidPerson.model @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java index cc431b8fe8..60353ee000 100644 --- a/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java +++ b/plugins/org.eclipse.epsilon.eol.dap/src/org/eclipse/epsilon/eol/dap/EpsilonDebugAdapter.java @@ -136,13 +136,14 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { if (ast.getParent() == null || runningRoots.isEmpty()) { runningRoots.add(ast); - if (topModule == null && ast.getModule() instanceof IEolModule) { + IEolModule module = getModule(ast); + if (topModule == null && module != null) { /* * The very first module we will run is assumed to be the "main" module of our * program. When this is considered "done", the debugging session will be considered * completed. */ - topModule = (IEolModule) ast.getModule(); + topModule = module; while (topModule.getParentModule() != null) { topModule = topModule.getParentModule(); } @@ -164,12 +165,21 @@ public void aboutToExecute(ModuleElement ast, IEolContext context) { } if (ast.getModule() instanceof IEolModule) { - ThreadState threadState = attachTo((IEolModule) ast.getModule()); + ThreadState threadState = attachTo(module); sendThreadEvent(threadState.getThreadId(), ThreadEventArgumentsReason.STARTED); } } } + protected IEolModule getModule(ModuleElement ast) { + if (ast instanceof IEolModule) { + return (IEolModule) ast; + } else if (ast.getModule() instanceof IEolModule) { + return (IEolModule) ast.getModule(); + } + return null; + } + @Override public void finishedExecuting(ModuleElement ast, Object result, IEolContext context) { finishedExecutingWithException(ast, null, context); @@ -177,11 +187,11 @@ public void finishedExecuting(ModuleElement ast, Object result, IEolContext cont @Override public void finishedExecutingWithException(ModuleElement ast, EolRuntimeException exception, IEolContext context) { - if (runningRoots.remove(ast) && ast.getModule() instanceof IEolModule) { - final IEolModule eolModule = (IEolModule) ast.getModule(); - eolModule.getContext().getOutputStream().flush(); - eolModule.getContext().getErrorStream().flush(); - removeThreadFor(eolModule); + IEolModule module = getModule(ast); + if (runningRoots.remove(ast) && module != null) { + module.getContext().getOutputStream().flush(); + module.getContext().getErrorStream().flush(); + removeThreadFor(module); if (topModule != null && ast == topModule) { if (exception != null) { @@ -592,6 +602,8 @@ public CompletableFuture initialize(InitializeRequestArguments arg Capabilities caps = new Capabilities(); caps.setSupportsTerminateRequest(true); caps.setSupportsConditionalBreakpoints(true); + caps.setSupportTerminateDebuggee(true); + caps.setSupportSuspendDebuggee(true); return caps; }); } @@ -806,9 +818,21 @@ public CompletableFuture terminate(TerminateArguments args) { @Override public CompletableFuture disconnect(DisconnectArguments args) { return CompletableFuture.runAsync(() -> { - this.isTerminated = true; + if (args.getTerminateDebuggee() == Boolean.TRUE) { + this.isTerminated = true; + resumeAllThreads(); + sendTerminated(); + sendExited(2); + } else if (args.getSuspendDebuggee() != Boolean.TRUE) { + lineBreakpointsByPath.clear(); + synchronized (threads) { + for (ThreadState thread : threads.values()) { + thread.lineBreakpointsByURI.clear(); + } + } + resumeAllThreads(); + } this.client = null; - resumeAllThreads(); }); } diff --git a/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java b/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java index 029bca8c3e..a9577df760 100644 --- a/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java +++ b/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java @@ -197,16 +197,24 @@ public PerformFixAction(UnsatisfiedConstraint unsatisfiedConstraint, FixInstance @Override public void run() { - //PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { - try { - fixInstance.perform(); - unsatisfiedConstraint.setFixed(true); - setDone(!existUnsatisfiedConstraintsToFix()); - viewer.refresh(); - } catch (Exception e) { - module.getContext().getErrorStream().println(e.toString()); + // Need to run fix from a non-UI background job (in case it is debugged) + new Job("Run fix") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + fixInstance.perform(); + unsatisfiedConstraint.setFixed(true); + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { + setDone(!existUnsatisfiedConstraintsToFix()); + viewer.refresh(); + }); + } catch (Exception e) { + module.getContext().getErrorStream().println(e.toString()); + return Status.error(e.getMessage()); + } + return Status.OK_STATUS; } - //}); + }.schedule(); } } diff --git a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java index 4c1b1ea62b..d954cf6c22 100644 --- a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java +++ b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java @@ -28,8 +28,8 @@ protected boolean isStructuralBlock(ModuleElement ast) { public boolean isDoneAfterModuleElement(ModuleElement ast) { if (super.isDoneAfterModuleElement(ast)) { for (UnsatisfiedConstraint unsatisfied : getModule().getContext().getUnsatisfiedConstraints()) { - if (!unsatisfied.getFixes().isEmpty()) { - // There is at least one unsatisfied constraint with fixes: leave it running + if (!unsatisfied.isFixed() && !unsatisfied.getFixes().isEmpty()) { + // There is at least one unfixed unsatisfied constraint with fixes: leave it running return false; } } diff --git a/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl b/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl index 54fd6d12ac..4daf4e1f99 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl +++ b/tests/org.eclipse.epsilon.eol.dap.test/epsilon/11-validation.evl @@ -10,9 +10,14 @@ context Person { return l.isDefined() and l.trim().length() > 0; } fix { - title: 'Add "NoLastName" as placeholder' + guard: true // just to have a place for a breakpoint + title { + var newLastName = 'NoLastName'; + return 'Add "' + newLastName + '" as placeholder'; + } do { - self.lastName = 'NoLastName'; + var fixedLastName = 'NoLastName'; + self.lastName = fixedLastName; } } } diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java index ce738543e8..512a0b8c74 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/AbstractExecutionQueueTest.java @@ -33,7 +33,7 @@ protected void shutdown() throws Exception { adapter.terminate(new TerminateArguments()); // Ensures the program has finished running, and that the script thread has died - assertProgramCompletedSuccessfully(); + assertProgramFailed(); runModuleResult.get(); } diff --git a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java index acad7eb8f3..1207f74baa 100644 --- a/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java +++ b/tests/org.eclipse.epsilon.eol.dap.test/src/org/eclipse/epsilon/eol/dap/test/evl/EvlFixDebugTest.java @@ -54,11 +54,11 @@ protected void setupModule() throws Exception { @Test public void canStopInsideFixTitleExpression() throws Exception { SetBreakpointsResponse breakpoints = adapter.setBreakpoints( - createBreakpoints(createBreakpoint(13))).get(); + createBreakpoints(createBreakpoint(16))).get(); assertTrue("The breakpoint on the file should be recognised", breakpoints.getBreakpoints()[0].isVerified()); - assertEquals("The breakpoint should be verified on the fix expression", - (Integer) 13, breakpoints.getBreakpoints()[0].getLine()); + assertEquals("The breakpoint should be verified on the fix block", + (Integer) 16, breakpoints.getBreakpoints()[0].getLine()); attach(); // Wait for the EVL script to run @@ -75,14 +75,14 @@ public void canStopInsideFixTitleExpression() throws Exception { assertStoppedBecauseOf(StoppedEventArgumentsReason.BREAKPOINT); assertFalse("Computing the title should not be done yet", futureTitle.isDone()); StackTraceResponse stackTrace = getStackTrace(); - assertEquals("Shold be stopped at the fix title expression line", - 13, stackTrace.getStackFrames()[0].getLine()); + assertEquals("Shold be stopped at the second line of the fix block", + 16, stackTrace.getStackFrames()[0].getLine()); - ScopesResponse scopes = getScopes(stackTrace.getStackFrames()[1]); + ScopesResponse scopes = getScopes(stackTrace.getStackFrames()[0]); VariablesResponse variables = getVariables(scopes.getScopes()[0]); Map varsByName = getVariablesByName(variables); - Variable personVariable = varsByName.get("self"); - assertNotNull("The second stack frame should have a 'self' variable", personVariable); + Variable personVariable = varsByName.get("newLastName"); + assertNotNull("The top stack frame should have a 'newLastName' variable", personVariable); adapter.continue_(new ContinueArguments()).get(); assertNotNull(futureTitle.get()); @@ -93,4 +93,91 @@ public void canStopInsideFixTitleExpression() throws Exception { // Terminated programs exit with a non-zero status assertProgramFailed(); } + + @Test + public void canStopInsideFixGuardExpression() throws Exception { + SetBreakpointsResponse breakpoints = adapter.setBreakpoints( + createBreakpoints(createBreakpoint(13))).get(); + assertTrue("The breakpoint on the file should be recognised", + breakpoints.getBreakpoints()[0].isVerified()); + assertEquals("The breakpoint should be verified on the guard expression", + (Integer) 13, breakpoints.getBreakpoints()[0].getLine()); + attach(); + + // Fix guards are immediately evaluated + assertStoppedBecauseOf(StoppedEventArgumentsReason.BREAKPOINT); + StackTraceResponse stackTrace = getStackTrace(); + assertEquals("Shold be stopped at the guard expression line", + 13, stackTrace.getStackFrames()[0].getLine()); + + // Let execution continue past the guard + adapter.continue_(new ContinueArguments()).get(); + + // Wait for the EVL script to run + runModuleResult.get(); + + // Try to get the title of the first fix + Future futureTitle = executor.submit(() -> { + Collection allUnsatisfied = + ((EvlModule) module).getContext().getUnsatisfiedConstraints(); + UnsatisfiedConstraint unsatisfied = allUnsatisfied.iterator().next(); + return unsatisfied.getFixes().get(0).getTitle(); + }); + + assertFalse("Computing the title should not be done yet", futureTitle.isDone()); + assertNotNull(futureTitle.get()); + + // We need to explicitly terminate as there is a lingering fix that could be applied + adapter.terminate(new TerminateArguments()).get(); + + // Terminated programs exit with a non-zero status + assertProgramFailed(); + } + + @Test + public void canStopInsideFixDoBlock() throws Exception { + SetBreakpointsResponse breakpoints = adapter.setBreakpoints( + createBreakpoints(createBreakpoint(20))).get(); + assertTrue("The breakpoint on the file should be recognised", + breakpoints.getBreakpoints()[0].isVerified()); + assertEquals("The breakpoint should be verified on the guard expression", + (Integer) 20, breakpoints.getBreakpoints()[0].getLine()); + attach(); + + // Wait for the EVL script to run + runModuleResult.get(); + + // Try to perform the first fix + Future futureFix = executor.submit(() -> { + Collection allUnsatisfied = + ((EvlModule) module).getContext().getUnsatisfiedConstraints(); + UnsatisfiedConstraint unsatisfied = allUnsatisfied.iterator().next(); + unsatisfied.getFixes().get(0).perform(); + return "done"; + }); + + // Check we are stopped in the middle of the do block + assertStoppedBecauseOf(StoppedEventArgumentsReason.BREAKPOINT); + StackTraceResponse stackTrace = getStackTrace(); + assertEquals("Shold be stopped at the second line of the do block", + 20, stackTrace.getStackFrames()[0].getLine()); + assertFalse("Performing the fix should not be done yet", futureFix.isDone()); + + ScopesResponse scopes = getScopes(stackTrace.getStackFrames()[0]); + VariablesResponse variables = getVariables(scopes.getScopes()[0]); + Map varsByName = getVariablesByName(variables); + Variable personVariable = varsByName.get("fixedLastName"); + assertNotNull("The top stack frame should have a 'fixedLastName' variable", personVariable); + + // Allow the fix to be performed + adapter.continue_(new ContinueArguments()).get(); + assertNotNull(futureFix.get()); + + // We need to explicitly terminate as fixes were identified + adapter.terminate(new TerminateArguments()).get(); + + // Terminated programs exit with a non-zero status + assertProgramFailed(); + } + } From 6c52bc374f8bd01da0838d26147cca2bccf842cb Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Mon, 9 Jun 2025 20:36:39 +0100 Subject: [PATCH 7/8] ValidationView: change quickfix pick to ElementListSelectionDialog to support title breakpoints --- .../epsilon/evl/dt/views/ValidationView.java | 137 ++++++++++++------ 1 file changed, 94 insertions(+), 43 deletions(-) diff --git a/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java b/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java index a9577df760..dea0ae634e 100644 --- a/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java +++ b/plugins/org.eclipse.epsilon.evl.dt/src/org/eclipse/epsilon/evl/dt/views/ValidationView.java @@ -12,6 +12,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; + import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; @@ -30,21 +34,21 @@ import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.MenuManager; -import org.eclipse.jface.action.Separator; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.ViewerComparator; +import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.IActionBars; -import org.eclipse.ui.IWorkbenchActionConstants; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.dialogs.ElementListSelectionDialog; import org.eclipse.ui.part.ViewPart; public class ValidationView extends ViewPart { @@ -167,54 +171,101 @@ private void fillLocalPullDown(IMenuManager manager) { } private void fillContextMenu(IMenuManager manager) { - UnsatisfiedConstraint unsatisfiedConstraint = (UnsatisfiedConstraint)((StructuredSelection) viewer.getSelection()).getFirstElement(); - if (unsatisfiedConstraint == null) return; - - for (FixInstance fixInstance : unsatisfiedConstraint.getFixes()) { - manager.add(new PerformFixAction(unsatisfiedConstraint, fixInstance)); + StructuredSelection selection = (StructuredSelection) viewer.getSelection(); + UnsatisfiedConstraint unsatisfiedConstraint = (UnsatisfiedConstraint) selection.getFirstElement(); + + if (unsatisfiedConstraint != null && !unsatisfiedConstraint.getFixes().isEmpty()) { + manager.add(new QuickFixAction(unsatisfiedConstraint)); } - - // Other plug-ins can contribute there actions here - manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS)); } - - class PerformFixAction extends Action { - UnsatisfiedConstraint unsatisfiedConstraint = null; - FixInstance fixInstance = null; - - public PerformFixAction(UnsatisfiedConstraint unsatisfiedConstraint, FixInstance fixInstance) { - this.unsatisfiedConstraint = unsatisfiedConstraint; - this.fixInstance = fixInstance; - this.setImageDescriptor(EvlPlugin.getDefault().getImageDescriptor("icons/fix.gif")); - try { - this.setText(fixInstance.getTitle()); + + protected class QuickFixAction extends Action { + protected class ComputeFixTitlesJob extends Job { + protected ComputeFixTitlesJob(String name) { + super(name); } - catch (EolRuntimeException e) { - module.getContext().getErrorStream().println(e.toString()); - this.setText("An exception occured while evaluating the title of the fix"); + + @Override + protected IStatus run(IProgressMonitor monitor) { + final Map fixesByTitle = new TreeMap<>(); + for (FixInstance fixInstance : unsatisfiedConstraint.getFixes()) { + try { + fixesByTitle.put(fixInstance.getTitle(), fixInstance); + } catch (EolRuntimeException e) { + return Status.error(e.getMessage(), e); + } + } + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { + showQuickFixDialog(unsatisfiedConstraint, fixesByTitle); + }); + return Status.OK_STATUS; + } + + @SuppressWarnings("unchecked") + protected void showQuickFixDialog(UnsatisfiedConstraint unsatisfiedConstraint, final Map fixesByTitle) { + ElementListSelectionDialog dialog = new ElementListSelectionDialog( + getViewSite().getShell(), new MapEntryLabelProvider()); + + dialog.setElements(fixesByTitle.entrySet().toArray()); + dialog.setTitle("Select a quick fix"); + dialog.setMultipleSelection(false); + if (dialog.open() == Window.OK) { + Entry selected = (Map.Entry) dialog.getFirstResult(); + if (selected != null) { + // Need to run fix from a non-UI background job (in case it is debugged) + new RunFixJob("Run fix", selected).schedule(); + } + } } } - + + protected class RunFixJob extends Job { + private final Entry selected; + + protected RunFixJob(String name, Entry selected) { + super(name); + this.selected = selected; + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + selected.getValue().perform(); + unsatisfiedConstraint.setFixed(true); + PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { + setDone(!existUnsatisfiedConstraintsToFix()); + viewer.refresh(); + }); + } catch (Exception e) { + module.getContext().getErrorStream().println(e.toString()); + return Status.error(e.getMessage()); + } + return Status.OK_STATUS; + } + } + + protected class MapEntryLabelProvider extends LabelProvider { + @SuppressWarnings("unchecked") + @Override + public String getText(Object element) { + if (element instanceof Map.Entry) { + return ((Map.Entry) element).getKey(); + } + return super.getText(element); + } + } + + private final UnsatisfiedConstraint unsatisfiedConstraint; + + public QuickFixAction(UnsatisfiedConstraint unsatisfiedConstraint) { + super("Quick Fix..."); + this.unsatisfiedConstraint = unsatisfiedConstraint; + } + @Override public void run() { - // Need to run fix from a non-UI background job (in case it is debugged) - new Job("Run fix") { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - fixInstance.perform(); - unsatisfiedConstraint.setFixed(true); - PlatformUI.getWorkbench().getDisplay().asyncExec(() -> { - setDone(!existUnsatisfiedConstraintsToFix()); - viewer.refresh(); - }); - } catch (Exception e) { - module.getContext().getErrorStream().println(e.toString()); - return Status.error(e.getMessage()); - } - return Status.OK_STATUS; - } - }.schedule(); + // Need to compute fix titles from a background job (could have a breakpoint) + new ComputeFixTitlesJob("Compute fix titles").schedule(); } } From 7664bbe304b18c38ba8b1056729d44bfd1c422bb Mon Sep 17 00:00:00 2001 From: Antonio Garcia-Dominguez Date: Wed, 11 Jun 2025 16:24:29 +0100 Subject: [PATCH 8/8] EvlTask: add 'fix' field --- .../build.xml | 4 ++-- .../eclipse/epsilon/evl/debug/EvlDebugger.java | 5 +++++ .../epsilon/evl/execute/CommandLineFixer.java | 12 +----------- .../epsilon/workflow/tasks/EvlTask.java | 18 ++++++++++++++---- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/examples/org.eclipse.epsilon.examples.eol.dap/build.xml b/examples/org.eclipse.epsilon.examples.eol.dap/build.xml index 6d1994c5ac..d6b7c67dfb 100644 --- a/examples/org.eclipse.epsilon.examples.eol.dap/build.xml +++ b/examples/org.eclipse.epsilon.examples.eol.dap/build.xml @@ -42,9 +42,9 @@ + read="true" store="true" /> - + diff --git a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java index d954cf6c22..d868f4d916 100644 --- a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java +++ b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/debug/EvlDebugger.java @@ -27,6 +27,11 @@ protected boolean isStructuralBlock(ModuleElement ast) { @Override public boolean isDoneAfterModuleElement(ModuleElement ast) { if (super.isDoneAfterModuleElement(ast)) { + if (getModule().getUnsatisfiedConstraintFixer() == null) { + // There is no fixer to apply the fixes + return true; + } + for (UnsatisfiedConstraint unsatisfied : getModule().getContext().getUnsatisfiedConstraints()) { if (!unsatisfied.isFixed() && !unsatisfied.getFixes().isEmpty()) { // There is at least one unfixed unsatisfied constraint with fixes: leave it running diff --git a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/execute/CommandLineFixer.java b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/execute/CommandLineFixer.java index dd4ab3d783..ffce7a33cd 100644 --- a/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/execute/CommandLineFixer.java +++ b/plugins/org.eclipse.epsilon.evl.engine/src/org/eclipse/epsilon/evl/execute/CommandLineFixer.java @@ -17,8 +17,6 @@ public class CommandLineFixer implements IEvlFixer { - protected boolean fix = false; - @Override public void fix(IEvlModule module) throws EolRuntimeException { IEvlContext context = module.getContext(); @@ -26,7 +24,7 @@ public void fix(IEvlModule module) throws EolRuntimeException { for (UnsatisfiedConstraint unsatisfiedConstraint : context.getUnsatisfiedConstraints()) { context.getOutputStream().println(unsatisfiedConstraint.getMessage()); - boolean fixIt = fix && (unsatisfiedConstraint.getFixes().size() > 0) && userInput.confirm("Fix error?", true); + boolean fixIt = unsatisfiedConstraint.getFixes().size() > 0 && userInput.confirm("Fix error?", true); if (fixIt) { FixInstance fixInstance = (FixInstance) userInput.choose(unsatisfiedConstraint.getMessage(), unsatisfiedConstraint.getFixes(), null); @@ -37,12 +35,4 @@ public void fix(IEvlModule module) throws EolRuntimeException { } } - public boolean isFix() { - return fix; - } - - public void setFix(boolean fix) { - this.fix = fix; - } - } diff --git a/plugins/org.eclipse.epsilon.workflow/ant/org/eclipse/epsilon/workflow/tasks/EvlTask.java b/plugins/org.eclipse.epsilon.workflow/ant/org/eclipse/epsilon/workflow/tasks/EvlTask.java index 794fdbb51f..ef051ee116 100644 --- a/plugins/org.eclipse.epsilon.workflow/ant/org/eclipse/epsilon/workflow/tasks/EvlTask.java +++ b/plugins/org.eclipse.epsilon.workflow/ant/org/eclipse/epsilon/workflow/tasks/EvlTask.java @@ -21,6 +21,7 @@ public class EvlTask extends ExportableModuleTask { protected String exportConstraintTrace; + protected boolean fix = false; public String getExportConstraintTrace() { return exportConstraintTrace; @@ -31,7 +32,15 @@ public void setExportConstraintTrace(String exportConstraintTrace) { ((IEvlModule) module).getContext().setOptimizeConstraintTrace(false); } } - + + public boolean isFix() { + return fix; + } + + public void setFix(boolean fix) { + this.fix = fix; + } + @Override protected IEvlModule createDefaultModule() { return new EvlModuleParallelAnnotation(); @@ -40,9 +49,10 @@ protected IEvlModule createDefaultModule() { @Override protected void initialize() throws Exception { IEvlModule evlModule = (IEvlModule) module; - CommandLineFixer clf = new CommandLineFixer(); - clf.setFix(false); - evlModule.setUnsatisfiedConstraintFixer(clf); + if (isFix()) { + CommandLineFixer clf = new CommandLineFixer(); + evlModule.setUnsatisfiedConstraintFixer(clf); + } } @Override