Skip to content

Commit fc72a2f

Browse files
authored
test: add DebugScopesAndVariablesTest (#1393)
1 parent ba1c63a commit fc72a2f

2 files changed

Lines changed: 268 additions & 0 deletions

File tree

org.eclipse.lsp4e.test/META-INF/MANIFEST.MF

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Require-Bundle: org.eclipse.core.runtime,
2121
org.eclipse.lsp4e.tests.mock,
2222
org.eclipse.lsp4e.debug,
2323
org.eclipse.lsp4j,
24+
org.eclipse.lsp4j.debug,
2425
org.eclipse.jdt.annotation,
2526
org.eclipse.ui.tests.harness,
2627
org.eclipse.ui.monitoring,
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Vegard IT GmbH and others.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Sebastian Thomschke (Vegard IT GmbH) - initial implementation.
11+
*******************************************************************************/
12+
package org.eclipse.lsp4e.test.debug;
13+
14+
import static org.junit.Assert.*;
15+
16+
import java.io.InputStream;
17+
import java.io.OutputStream;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import java.util.concurrent.CompletableFuture;
21+
import java.util.concurrent.ExecutorService;
22+
import java.util.function.UnaryOperator;
23+
24+
import org.eclipse.core.runtime.NullProgressMonitor;
25+
import org.eclipse.debug.core.DebugPlugin;
26+
import org.eclipse.debug.core.ILaunch;
27+
import org.eclipse.debug.core.ILaunchConfigurationType;
28+
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
29+
import org.eclipse.debug.core.ILaunchManager;
30+
import org.eclipse.debug.core.Launch;
31+
import org.eclipse.debug.core.model.IStackFrame;
32+
import org.eclipse.debug.core.model.IVariable;
33+
import org.eclipse.lsp4e.debug.debugmodel.DSPDebugTarget;
34+
import org.eclipse.lsp4e.debug.debugmodel.DSPStackFrame;
35+
import org.eclipse.lsp4e.debug.debugmodel.TransportStreams;
36+
import org.eclipse.lsp4e.test.utils.AbstractTestWithProject;
37+
import org.eclipse.lsp4e.test.utils.TestUtils;
38+
import org.eclipse.lsp4j.debug.Capabilities;
39+
import org.eclipse.lsp4j.debug.EvaluateResponse;
40+
import org.eclipse.lsp4j.debug.InitializeRequestArguments;
41+
import org.eclipse.lsp4j.debug.Scope;
42+
import org.eclipse.lsp4j.debug.ScopesArguments;
43+
import org.eclipse.lsp4j.debug.ScopesResponse;
44+
import org.eclipse.lsp4j.debug.StackFrame;
45+
import org.eclipse.lsp4j.debug.StackTraceArguments;
46+
import org.eclipse.lsp4j.debug.StackTraceResponse;
47+
import org.eclipse.lsp4j.debug.StoppedEventArguments;
48+
import org.eclipse.lsp4j.debug.Thread;
49+
import org.eclipse.lsp4j.debug.ThreadsResponse;
50+
import org.eclipse.lsp4j.debug.Variable;
51+
import org.eclipse.lsp4j.debug.VariablesArguments;
52+
import org.eclipse.lsp4j.debug.VariablesResponse;
53+
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
54+
import org.eclipse.lsp4j.debug.services.IDebugProtocolServer;
55+
import org.eclipse.lsp4j.jsonrpc.Launcher;
56+
import org.eclipse.lsp4j.jsonrpc.MessageConsumer;
57+
import org.eclipse.lsp4j.jsonrpc.RemoteEndpoint;
58+
import org.junit.Test;
59+
60+
/**
61+
* End-to-end style test around DSPStackFrame.getVariables() to verify that
62+
* scopes and variables are retrieved when the adapter reports a stop.
63+
*
64+
* Scenario:
65+
* <ol>
66+
* <li>create a mock DAP server that supports
67+
* initialize/launch/threads/stackTrace/scopes/variables
68+
* <li>wire it into a DSPDebugTarget via a Launcher stub (no real IO)
69+
* <li>server sends initialized + stopped; client refreshes threads and frames
70+
* <li>assert frame.getVariables() returns scopes; assert expanding returns
71+
* variables
72+
* </ol>
73+
*/
74+
public class DebugScopesAndVariablesTest extends AbstractTestWithProject {
75+
76+
/**
77+
* Minimal in-memory mock of a DAP server sufficient for this test
78+
*/
79+
private static final class MockDebugServer implements IDebugProtocolServer {
80+
// Fixed ids for this test
81+
private static final int THREAD_ID = 1;
82+
83+
private static final int FRAME_ID = 101;
84+
private static final int LOCALS_REF = 201;
85+
// Wired by TestDebugTarget#createLauncher
86+
IDebugProtocolClient client;
87+
88+
// Unused in this test but required by interface since LSP4E may call evaluate
89+
@Override
90+
public CompletableFuture<EvaluateResponse> evaluate(org.eclipse.lsp4j.debug.EvaluateArguments args) {
91+
var r = new EvaluateResponse();
92+
r.setResult("n/a");
93+
r.setVariablesReference(0);
94+
return CompletableFuture.completedFuture(r);
95+
}
96+
97+
@Override
98+
public CompletableFuture<Capabilities> initialize(InitializeRequestArguments args) {
99+
var caps = new Capabilities();
100+
// Keep configurationDone optional for simplicity
101+
caps.setSupportsConfigurationDoneRequest(false);
102+
// Notify client that we are initialized as LSP4E waits for this signal.
103+
if (client != null) {
104+
client.initialized();
105+
}
106+
return CompletableFuture.completedFuture(caps);
107+
}
108+
109+
@Override
110+
public CompletableFuture<Void> launch(Map<String, Object> args) {
111+
// Immediately report a stopped event so client populates threads/frames.
112+
if (client != null) {
113+
var stopped = new StoppedEventArguments();
114+
stopped.setReason("breakpoint");
115+
stopped.setThreadId(THREAD_ID);
116+
client.stopped(stopped);
117+
}
118+
return CompletableFuture.completedFuture(null);
119+
}
120+
121+
@Override
122+
public CompletableFuture<ScopesResponse> scopes(ScopesArguments args) {
123+
var scope = new Scope();
124+
scope.setName("locals");
125+
scope.setVariablesReference(LOCALS_REF);
126+
var resp = new ScopesResponse();
127+
resp.setScopes(new Scope[] { scope });
128+
return CompletableFuture.completedFuture(resp);
129+
}
130+
131+
@Override
132+
public CompletableFuture<StackTraceResponse> stackTrace(StackTraceArguments args) {
133+
var sf = new StackFrame();
134+
sf.setId(FRAME_ID);
135+
sf.setName("func");
136+
sf.setLine(1);
137+
var resp = new StackTraceResponse();
138+
resp.setTotalFrames(1);
139+
resp.setStackFrames(new StackFrame[] { sf });
140+
return CompletableFuture.completedFuture(resp);
141+
}
142+
143+
@Override
144+
public CompletableFuture<ThreadsResponse> threads() {
145+
var r = new ThreadsResponse();
146+
var t = new Thread();
147+
t.setId(THREAD_ID);
148+
t.setName("Main");
149+
r.setThreads(new Thread[] { t });
150+
return CompletableFuture.completedFuture(r);
151+
}
152+
153+
@Override
154+
public CompletableFuture<VariablesResponse> variables(VariablesArguments args) {
155+
var v = new Variable();
156+
v.setName("x");
157+
v.setValue("42");
158+
v.setVariablesReference(0);
159+
var resp = new VariablesResponse();
160+
resp.setVariables(new Variable[] { v });
161+
return CompletableFuture.completedFuture(resp);
162+
}
163+
}
164+
165+
/**
166+
* DSPDebugTarget variant that injects a mock server without real JSON-RPC IO
167+
*/
168+
private static final class TestDebugTarget extends DSPDebugTarget {
169+
private final IDebugProtocolServer server;
170+
171+
TestDebugTarget(ILaunch launch, Map<String, Object> dspParameters, IDebugProtocolServer server) {
172+
super(launch, () -> new TransportStreams.DefaultTransportStreams(InputStream.nullInputStream(),
173+
OutputStream.nullOutputStream()), dspParameters);
174+
this.server = server;
175+
}
176+
177+
@Override
178+
protected Launcher<? extends IDebugProtocolServer> createLauncher(UnaryOperator<MessageConsumer> wrapper,
179+
InputStream in, OutputStream out, ExecutorService threadPool) {
180+
// Give the server a handle to the client so it can send notifications.
181+
if (server instanceof MockDebugServer m) {
182+
m.client = this;
183+
}
184+
return new Launcher<>() {
185+
@Override
186+
public RemoteEndpoint getRemoteEndpoint() {
187+
return null;
188+
}
189+
190+
@Override
191+
public IDebugProtocolServer getRemoteProxy() {
192+
return server;
193+
}
194+
195+
@Override
196+
public CompletableFuture<Void> startListening() {
197+
return CompletableFuture.completedFuture(null);
198+
}
199+
200+
};
201+
}
202+
}
203+
204+
private static final String LAUNCH_TYPE_ID = "org.eclipse.lsp4e.debug.launchType";
205+
206+
private static ILaunch newLaunch(String mode) throws Exception {
207+
ILaunchConfigurationType type = DebugPlugin.getDefault().getLaunchManager()
208+
.getLaunchConfigurationType(LAUNCH_TYPE_ID);
209+
ILaunchConfigurationWorkingCopy wc = type.newInstance(null,
210+
"ScopesAndVariablesTest-" + System.currentTimeMillis());
211+
return new Launch(wc, mode, null);
212+
}
213+
214+
@Test
215+
public void testScopesAndVariablesAreReturned() throws Exception {
216+
ILaunch launch = newLaunch(ILaunchManager.RUN_MODE);
217+
218+
var params = new HashMap<String, Object>();
219+
params.put("type", "mock");
220+
params.put("request", "launch");
221+
params.put("program", "dummy");
222+
223+
var server = new MockDebugServer();
224+
var target = new TestDebugTarget(launch, params, server);
225+
226+
target.initialize(new NullProgressMonitor());
227+
228+
// Wait until server has sent 'stopped' and client marked itself suspended
229+
TestUtils.waitForAndAssertCondition(5000, target::isSuspended);
230+
231+
var threads = target.getThreads();
232+
assertTrue("No threads reported by debug target", threads.length > 0);
233+
assertEquals("Expected exactly one thread", 1, threads.length);
234+
assertEquals("Thread name mismatch", "Main", threads[0].getName());
235+
assertEquals("Thread id mismatch", Integer.valueOf(1), threads[0].getId());
236+
237+
var frames = threads[0].getStackFrames();
238+
assertTrue("No stack frames available on stopped thread", frames.length > 0);
239+
assertEquals("Expected exactly one frame", 1, frames.length);
240+
241+
IStackFrame frame = frames[0];
242+
assertEquals("Frame name mismatch", "func", frame.getName());
243+
assertEquals("Frame line mismatch", 1, frame.getLineNumber());
244+
assertEquals("Frame id mismatch", 101, ((DSPStackFrame) frame).getFrameId().intValue());
245+
IVariable[] scopes = frame.getVariables();
246+
assertTrue("Expected at least one scope", scopes.length > 0);
247+
assertEquals("Expected exactly one scope", 1, scopes.length);
248+
// Expect exactly one scope named "locals"
249+
assertArrayEquals(new String[] { "locals" }, new String[] { scopes[0].getName() });
250+
assertTrue("Scope should advertise child variables", scopes[0].getValue().hasVariables());
251+
252+
// Expand the scope to fetch actual variables via 'variables' request
253+
var value = scopes[0].getValue();
254+
var vars = value.getVariables();
255+
assertNotNull("Scope value should not be null", value);
256+
assertTrue("Expected at least one variable under 'locals'", vars != null && vars.length > 0);
257+
assertEquals("Expected exactly one local variable", 1, vars.length);
258+
assertArrayEquals(new String[] { "x" }, new String[] { vars[0].getName() });
259+
assertEquals("Variable value mismatch", "42", vars[0].getValue().getValueString());
260+
assertFalse("Leaf variable should not have children", vars[0].getValue().hasVariables());
261+
262+
// Capabilities returned by mock initialize
263+
assertNotNull("Capabilities should be available after initialize", target.getCapabilities());
264+
assertFalse("supportsConfigurationDoneRequest should be false",
265+
target.getCapabilities().getSupportsConfigurationDoneRequest());
266+
}
267+
}

0 commit comments

Comments
 (0)