/*******************************************************************************
* Copyright (c) 2016 Ericsson and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.cdt.tests.dsf.gdb.tests.nonstop;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.cdt.debug.core.ICDTLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.ImmediateRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.Query;
import org.eclipse.cdt.dsf.datamodel.DMContexts;
import org.eclipse.cdt.dsf.datamodel.IDMContext;
import org.eclipse.cdt.dsf.datamodel.IDMEvent;
import org.eclipse.cdt.dsf.debug.service.IMultiRunControl;
import org.eclipse.cdt.dsf.debug.service.IRunControl.IContainerDMContext;
import org.eclipse.cdt.dsf.debug.service.IStack.IFrameDMContext;
import org.eclipse.cdt.dsf.gdb.IGDBLaunchConfigurationConstants;
import org.eclipse.cdt.dsf.gdb.internal.service.IGDBFocusSynchronizer;
import org.eclipse.cdt.dsf.gdb.internal.service.IGDBFocusSynchronizer.IGDBFocusChangedEvent;
import org.eclipse.cdt.dsf.gdb.service.command.IGDBControl;
import org.eclipse.cdt.dsf.mi.service.IMIExecutionDMContext;
import org.eclipse.cdt.dsf.mi.service.IMIProcesses;
import org.eclipse.cdt.dsf.mi.service.command.commands.CLICommand;
import org.eclipse.cdt.dsf.mi.service.command.events.MIStoppedEvent;
import org.eclipse.cdt.dsf.mi.service.command.output.CLIThreadInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MIConsoleStreamOutput;
import org.eclipse.cdt.dsf.mi.service.command.output.MIConst;
import org.eclipse.cdt.dsf.mi.service.command.output.MIInfo;
import org.eclipse.cdt.dsf.mi.service.command.output.MINotifyAsyncOutput;
import org.eclipse.cdt.dsf.mi.service.command.output.MIOOBRecord;
import org.eclipse.cdt.dsf.mi.service.command.output.MIOutput;
import org.eclipse.cdt.dsf.mi.service.command.output.MIResult;
import org.eclipse.cdt.dsf.mi.service.command.output.MITuple;
import org.eclipse.cdt.dsf.mi.service.command.output.MIValue;
import org.eclipse.cdt.dsf.service.DsfServiceEventHandler;
import org.eclipse.cdt.dsf.service.DsfServicesTracker;
import org.eclipse.cdt.dsf.service.DsfSession;
import org.eclipse.cdt.tests.dsf.gdb.framework.BaseParametrizedTestCase;
import org.eclipse.cdt.tests.dsf.gdb.framework.ServiceEventWaitor;
import org.eclipse.cdt.tests.dsf.gdb.framework.SyncUtil;
import org.eclipse.cdt.tests.dsf.gdb.launching.TestsPlugin;
import org.eclipse.cdt.tests.dsf.gdb.tests.ITestConstants;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.BeforeClass;
import org.junit.Test;
public class ThreadStackFrameSyncTest extends BaseParametrizedTestCase {
final static private int DEFAULT_TIMEOUT = 1000;
private DsfServicesTracker fServicesTracker;
private IMultiRunControl fMultiRunControl;
private IGDBControl fCommandControl;
private IGDBFocusSynchronizer fGdbSync;
private DsfSession fSession;
private List<IDMEvent<? extends IDMContext>> fEventsReceived = new ArrayList<IDMEvent<? extends IDMContext>>();
// Breakpoint tags in MultiThread.cc
public static final String[] LINE_TAGS = new String[] {
"LINE_MAIN_BEFORE_THREAD_START", // Just before StartThread
"LINE_MAIN_AFTER_THREAD_START", // Just after StartThread
"LINE_MAIN_ALL_THREADS_STARTED", // Where all threads are guaranteed to be started.
};
/*
* Name of the executable
*/
private static final String EXEC_NAME = "MultiThread.exe";
private static final String SOURCE_NAME = "MultiThread.cc";
@BeforeClass
public static void beforeClass() {
Assume.assumeTrue(supportsNonStop());
}
@Override
public void doBeforeTest() throws Exception {
assumeGdbVersionAtLeast(ITestConstants.SUFFIX_GDB_7_12);
super.doBeforeTest();
resolveLineTagLocations(SOURCE_NAME, LINE_TAGS);
fSession = getGDBLaunch().getSession();
Assert.assertNotNull(fSession);
Runnable runnable = new Runnable() {
@Override
public void run() {
fServicesTracker = new DsfServicesTracker(TestsPlugin.getBundleContext(), fSession.getId());
Assert.assertTrue(fServicesTracker != null);
fCommandControl = fServicesTracker.getService(IGDBControl.class);
Assert.assertTrue(fCommandControl != null);
fMultiRunControl = fServicesTracker.getService(IMultiRunControl.class);
Assert.assertTrue(fMultiRunControl != null);
fGdbSync = fServicesTracker.getService(IGDBFocusSynchronizer.class);
Assert.assertTrue(fGdbSync != null);
IMIProcesses procService = fServicesTracker.getService(IMIProcesses.class);
Assert.assertTrue(procService != null);
// Register to receive DSF events
fSession.addServiceEventListener(ThreadStackFrameSyncTest.this, null);
}
};
fSession.getExecutor().submit(runnable).get();
}
@Override
protected void setLaunchAttributes() {
super.setLaunchAttributes();
setLaunchAttribute(ICDTLaunchConfigurationConstants.ATTR_PROGRAM_NAME,
EXEC_PATH + EXEC_NAME);
// Multi run control only makes sense for non-stop mode
setLaunchAttribute(IGDBLaunchConfigurationConstants.ATTR_DEBUGGER_NON_STOP, true);
}
@Override
public void doAfterTest() throws Exception {
super.doAfterTest();
if (fSession != null) {
fSession.getExecutor().submit(() -> fSession.removeServiceEventListener(ThreadStackFrameSyncTest.this))
.get();
}
fEventsReceived.clear();
if (fServicesTracker!=null) fServicesTracker.dispose();
}
//////////////////////////////////////////////////////////////////////////////////////
// Start of tests
//////////////////////////////////////////////////////////////////////////////////////
/**
* This test verifies that changing the active thread, in GDB, in CLI,
* triggers a GDB notification that a new thread has been selected.
*/
@Test
public void testChangingCurrentThreadCLINotification() throws Throwable {
ServiceEventWaitor<MIStoppedEvent> eventWaitor =
new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class);
// add a breakpoint in main
SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false);
// add a breakpoint in thread code
SyncUtil.addBreakpoint("36", false);
// Run program
SyncUtil.resumeAll();
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
// *** at this point all 5 threads should be stopped
// Try some thread switching - SwitchThreadAndCaptureThreadSwitchedEvent will
// capture the "=thread-selected" event and return the newly selected thread
// for us to compare to what we ordered
for (int i = 0; i < 2; i++) {
assertEquals("2",switchThreadAndCaptureThreadSwitchedEvent("2"));
assertEquals("3",switchThreadAndCaptureThreadSwitchedEvent("3"));
assertEquals("4",switchThreadAndCaptureThreadSwitchedEvent("4"));
assertEquals("5",switchThreadAndCaptureThreadSwitchedEvent("5"));
assertEquals("1",switchThreadAndCaptureThreadSwitchedEvent("1"));
}
}
/**
* This test verifies that changing the active frame, in GDB, in CLI,
* triggers a GDB notification that a new frame has been selected.
*/
@Test
public void testChangingCurrentFrameCLINotification() throws Throwable {
ServiceEventWaitor<MIStoppedEvent> eventWaitor =
new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class);
// add a breakpoint in main
SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false);
// add a breakpoint in thread code
SyncUtil.addBreakpoint("36", false);
// Run program
SyncUtil.resumeAll();
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
// *** at this point all 5 threads should be stopped
// switch to a thread that has some stack frames
assertEquals("2",switchThreadAndCaptureThreadSwitchedEvent("2"));
// Try some stack frame switching - SwitchFrameAndCaptureThreadSwitchedEvent will
// capture the "=thread-selected" event and return the newly selected stack frame,
// for us to compare to what we ordered
for (int i = 0; i < 5; i++) {
assertEquals("1",switchFrameAndCaptureStackFrameSwitchedEvent("1"));
assertEquals("0",switchFrameAndCaptureStackFrameSwitchedEvent("0"));
}
}
/**
* This test verifies that the GDB Synchronizer service is able to set
* the current GDB thread
*/
@Test
public void testGdbSyncServiceCanSwitchGDBThread() throws Throwable {
ServiceEventWaitor<MIStoppedEvent> eventWaitor =
new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class);
// add a breakpoint in main
SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false);
// add a breakpoint in thread code
SyncUtil.addBreakpoint("36", false);
// Run program
SyncUtil.resumeAll();
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
// *** at this point all 5 threads should be stopped
// have the sync service set GDB current tid to thread 5
fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(5)}, new ImmediateRequestMonitor());
assertEquals("5", getCurrentThread());
fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(4)}, new ImmediateRequestMonitor());
assertEquals("4", getCurrentThread());
fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(3)}, new ImmediateRequestMonitor());
assertEquals("3", getCurrentThread());
fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(2)}, new ImmediateRequestMonitor());
assertEquals("2", getCurrentThread());
fGdbSync.setFocus(new IDMContext[] {getContextForThreadId(1)}, new ImmediateRequestMonitor());
assertEquals("1", getCurrentThread());
}
/**
* This test verifies that the GDB Synchronizer service is able to set
* the current GDB stack frame
*/
@Test
public void testGdbSyncServiceCanSwitchGDBStackFrame() throws Throwable {
ServiceEventWaitor<MIStoppedEvent> eventWaitor =
new ServiceEventWaitor<MIStoppedEvent>(fMultiRunControl.getSession(), MIStoppedEvent.class);
// add a breakpoint in main
SyncUtil.addBreakpoint(SOURCE_NAME + ":" + getLineForTag("LINE_MAIN_ALL_THREADS_STARTED"), false);
// add a breakpoint in thread code
SyncUtil.addBreakpoint("36", false);
// Run program
SyncUtil.resumeAll();
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for first thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000)); // Wait for second thread to stop
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
eventWaitor.waitForEvent(TestsPlugin.massageTimeout(2000));
// *** at this point all 5 threads should be stopped
final IFrameDMContext frame1 = SyncUtil.getStackFrame(1, 1);
final IFrameDMContext frame0 = SyncUtil.getStackFrame(1, 0);
// do a few of times
for (int i = 0; i < 50; i++) {
// have the sync service switch stack frame to 1
fSession.getExecutor().execute(new Runnable() {
@Override
public void run() {
fGdbSync.setFocus(new IDMContext[] {frame1}, new ImmediateRequestMonitor());
}
});
Thread.sleep(100);
assertEquals("1", getCurrentStackFrameLevel());
// have the sync service switch stack frame to 0
fSession.getExecutor().execute(new Runnable() {
@Override
public void run() {
fGdbSync.setFocus(new IDMContext[] {frame0}, new ImmediateRequestMonitor());
}
});
Thread.sleep(100);
assertEquals("0", getCurrentStackFrameLevel());
}
}
//////////////////////////////////////////////////////////////////////////////////////
// End of tests
//////////////////////////////////////////////////////////////////////////////////////
// SyncUtil.getExecutionContext() takes the index of the
// array of all threads, so it will return a thread off by one.
// We compensate for this in this method
private IMIExecutionDMContext getContextForThreadId(int tid) throws InterruptedException, ExecutionException, TimeoutException {
return SyncUtil.getExecutionContext(tid -1);
}
/**
* This is a wrapper around selectGdbThread(), that waits and captures the
* expected "=thread-selected" event, and returns the thread id from it.
* @throws Throwable
*/
private String switchThreadAndCaptureThreadSwitchedEvent(String tid) throws Throwable {
Thread.sleep(100);
fEventsReceived.clear();
selectGdbThread(tid);
IDMContext ctx = waitForEvent(IGDBFocusChangedEvent.class).getDMContext();
if (ctx instanceof IMIExecutionDMContext) {
IMIExecutionDMContext execDmc = (IMIExecutionDMContext) ctx;
return execDmc.getThreadId();
}
else if (ctx instanceof IFrameDMContext) {
IMIExecutionDMContext execDmc = DMContexts.getAncestorOfType(ctx, IMIExecutionDMContext.class);
return execDmc.getThreadId();
}
return "unknown";
}
/**
* Waits and captures the expected "=thread-selected" event, and returns the frame id from it.
* @throws Throwable
*/
private String switchFrameAndCaptureStackFrameSwitchedEvent(String frameLevel) throws Throwable {
IFrameDMContext newFrame = null;
Thread.sleep(100);
fEventsReceived.clear();
selectGdbStackFrame(frameLevel);
Object[] elems = fGdbSync.getFocus();
for (Object elem : elems) {
if (elem instanceof IFrameDMContext) {
newFrame = (IFrameDMContext)elem;
break;
}
}
return newFrame != null ? Integer.toString(newFrame.getLevel()) : null;
}
/**
* Changes the current thread, using the CLI command "thread <tid>"
* @param tid: the thread id of the thread to switch-to. If empty,
* the command will simply report the current thread.
* @return the tid of the (possibly newly) currently selected gdb thread
* @throws Exception
*/
private String sendCLIThread(String tid) throws Exception {
IContainerDMContext containerDmc = SyncUtil.getContainerContext();
Query<CLIThreadInfo> query = new Query<CLIThreadInfo>() {
@Override
protected void execute(DataRequestMonitor<CLIThreadInfo> rm) {
fCommandControl.queueCommand(new CLICommand<CLIThreadInfo>(containerDmc,"thread " + tid) {
@Override
public CLIThreadInfo getResult(MIOutput output) {
return new CLIThreadInfo(output);
}
}, rm);
}
};
fCommandControl.getExecutor().execute(query);
CLIThreadInfo info = query.get();
return info.getCurrentThread();
}
private String getCurrentThread() throws Exception {
return sendCLIThread("");
}
/**
* Changes the current stack frame, using the CLI command "frame <level>". Then parses
* the output to extract the current frame.
* @param level the frame level wanted. If empty, the command will report the current level
* @return newly set level.
* @throws Exception
*/
private String sendCLIFrame(String level) throws Exception {
IContainerDMContext containerDmc = SyncUtil.getContainerContext();
Query<MIInfo> query = new Query<MIInfo>() {
@Override
protected void execute(DataRequestMonitor<MIInfo> rm) {
fCommandControl.queueCommand(new CLICommand<MIInfo>(containerDmc,"frame " + level) {
@Override
public CLIThreadInfo getResult(MIOutput output) {
return new CLIThreadInfo(output);
}
}, rm);
}
};
fCommandControl.getExecutor().execute(query);
String frameLevel = null;
for (MIOOBRecord oobr : query.get().getMIOutput().getMIOOBRecords()) {
// if frame changed, we'll get this printout:
if (oobr instanceof MINotifyAsyncOutput) {
// example of output:
// =thread-selected,id="2",frame={level="1",addr="0x00007ffff7bc4184",func="start_thread",args=[],from="/lib/x86_64-linux-gnu/libpthread.so.0"}
MINotifyAsyncOutput out = (MINotifyAsyncOutput) oobr;
String miEvent = out.getAsyncClass();
if ("thread-selected".equals(miEvent)) { //$NON-NLS-1$
// parse =thread-selected to extract current stack frame
MIResult[] results = out.getMIResults();
for (int i = 0; i < results.length; i++) {
String var = results[i].getVariable();
MIValue val = results[i].getMIValue();
if (var.equals("frame") && val instanceof MITuple) { //$NON-NLS-1$
// dig deeper to get the frame level
MIResult[] res = ((MITuple)val).getMIResults();
for (int j = 0; j < res.length; j++) {
var = res[j].getVariable();
val = res[j].getMIValue();
if (var.equals("level")) { //$NON-NLS-1$
if (val instanceof MIConst) {
frameLevel = ((MIConst) val).getString();
}
}
}
}
}
}
}
// if frame command was not given a parameter or the parameter is already
// the current frame, we'll get this version of the printout:
else if (oobr instanceof MIConsoleStreamOutput) {
// example of output (here frame = 0):
// ~"#0 main (argc=1 ...
String printout = ((MIConsoleStreamOutput) oobr).getCString();
int index1 = printout.indexOf('#');
int index2 = printout.indexOf(' ');
if (index1 != -1 && index2 != -1) {
frameLevel = printout.substring(index1 + 1, index2);
break;
}
}
}
return frameLevel;
}
private String getCurrentStackFrameLevel() throws Throwable {
return sendCLIFrame("");
}
@DsfServiceEventHandler
public void eventDispatched(IDMEvent<? extends IDMContext> e) {
synchronized(this) {
fEventsReceived.add(e);
notifyAll();
}
}
private void selectGdbThread(String tid) throws Throwable {
queueConsoleCommand(String.format("thread %s", tid));
}
private void selectGdbStackFrame(String frameLevel) throws Throwable {
queueConsoleCommand(String.format("frame %s", frameLevel));
}
private void queueConsoleCommand(String command) throws Throwable {
queueConsoleCommand(command, TestsPlugin.massageTimeout(DEFAULT_TIMEOUT), TimeUnit.MILLISECONDS);
}
private void queueConsoleCommand(final String command, int timeout, TimeUnit unit) throws Throwable {
Query<MIInfo> query = new Query<MIInfo>() {
@Override
protected void execute(DataRequestMonitor<MIInfo> rm) {
fCommandControl.queueCommand(
fCommandControl.getCommandFactory().createMIInterpreterExecConsole(
fCommandControl.getContext(),
command),
rm);
}
};
fSession.getExecutor().execute(query);
query.get(timeout, unit);
}
private <V extends IDMEvent<? extends IDMContext>> V waitForEvent(Class<V> eventType) throws Exception {
return waitForEvent(eventType, TestsPlugin.massageTimeout(DEFAULT_TIMEOUT));
}
@SuppressWarnings("unchecked")
private <V extends IDMEvent<? extends IDMContext>> V waitForEvent(Class<V> eventType, int timeout) throws Exception {
IDMEvent<?> event = getEvent(eventType);
if (event == null) {
synchronized(this) {
try {
wait(timeout);
}
catch (InterruptedException ex) {
}
}
event = getEvent(eventType);
if (event == null) {
throw new Exception(String.format("Timed out waiting for '%s' to occur.", eventType.getName()));
}
}
return (V)event;
}
@SuppressWarnings("unchecked")
private synchronized <V extends IDMEvent<? extends IDMContext>> V getEvent(Class<V> eventType) {
for (IDMEvent<?> e : fEventsReceived) {
if (eventType.isAssignableFrom(e.getClass())) {
fEventsReceived.remove(e);
return (V)e;
}
}
return null;
}
}