GP-857: Added interpreter-based tests for activation, breakpoints.

This commit is contained in:
Dan 2021-04-20 16:05:01 -04:00
parent 4d65ccd052
commit a61c2e1400
55 changed files with 1212 additions and 357 deletions

View file

@ -15,21 +15,25 @@
*/ */
package agent.dbgeng.model; package agent.dbgeng.model;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.*;
import java.util.List; import java.util.List;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetStackFrame;
import ghidra.dbg.test.*; import ghidra.dbg.test.*;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.*; import ghidra.program.model.address.*;
public abstract class AbstractModelForDbgengBreakpointsTest public abstract class AbstractModelForDbgengBreakpointsTest
extends AbstractDebuggerModelBreakpointsTest implements ProvidesTargetViaLaunchSpecimen { extends AbstractDebuggerModelBreakpointsTest implements ProvidesTargetViaLaunchSpecimen {
private static final PathPattern BREAK_PATTERN =
new PathPattern(PathUtils.parse("Sessions[0].Processes[].Debug.Breakpoints[]"));
private static final int BREAK_ID_POS = 1;
@Override @Override
public AbstractDebuggerModelTest getTest() { public AbstractDebuggerModelTest getTest() {
return this; return this;
@ -80,4 +84,88 @@ public abstract class AbstractModelForDbgengBreakpointsTest
throw new AssertionError(); throw new AssertionError();
} }
} }
@Override
protected void placeBreakpointViaInterpreter(AddressRange range, TargetBreakpointKind kind,
TargetInterpreter interpreter) throws Throwable {
Address min = range.getMinAddress();
if (range.getLength() == 4) {
switch (kind) {
case READ:
waitOn(interpreter.execute("ba r4 " + min));
break;
case WRITE:
waitOn(interpreter.execute("ba w4 " + min));
break;
default:
fail();
}
}
else if (range.getLength() == 1) {
switch (kind) {
case SW_EXECUTE:
waitOn(interpreter.execute("bp " + min));
break;
case HW_EXECUTE:
waitOn(interpreter.execute("ba e1 " + min));
break;
default:
fail();
}
}
else {
fail();
}
}
@Override
protected void disableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
String bpId = BREAK_PATTERN.matchIndices(t.getPath()).get(BREAK_ID_POS);
waitOn(interpreter.execute("bd " + bpId));
}
@Override
protected void enableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
String bpId = BREAK_PATTERN.matchIndices(t.getPath()).get(BREAK_ID_POS);
waitOn(interpreter.execute("be " + bpId));
}
@Override
protected void deleteViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
String bpId = BREAK_PATTERN.matchIndices(d.getPath()).get(BREAK_ID_POS);
waitOn(interpreter.execute("bc " + bpId));
}
@Override
protected void assertLocCoversViaInterpreter(AddressRange range, TargetBreakpointKind kind,
TargetBreakpointLocation loc, TargetInterpreter interpreter) throws Throwable {
String bpId = BREAK_PATTERN.matchIndices(loc.getPath()).get(BREAK_ID_POS);
String line = waitOn(interpreter.executeCapture("bl " + bpId)).trim();
assertFalse(line.contains("\n"));
// NB. WinDbg numbers breakpoints in base 10, by default
assertTrue(line.startsWith(bpId));
// TODO: Do I care to parse the details? The ID is confirmed, and details via the object...
}
@Override
protected void assertEnabledViaInterpreter(TargetTogglable t, boolean enabled,
TargetInterpreter interpreter) throws Throwable {
String bpId = BREAK_PATTERN.matchIndices(t.getPath()).get(BREAK_ID_POS);
String line = waitOn(interpreter.executeCapture("bl " + bpId)).trim();
assertFalse(line.contains("\n"));
assertTrue(line.startsWith(bpId));
String e = line.split("\\s+")[1];
assertEquals(enabled ? "e" : "d", e);
}
@Override
protected void assertDeletedViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
String bpId = BREAK_PATTERN.matchIndices(d.getPath()).get(BREAK_ID_POS);
String line = waitOn(interpreter.executeCapture("bl " + bpId)).trim();
assertEquals("", line);
}
} }

View file

@ -15,23 +15,29 @@
*/ */
package agent.dbgeng.model; package agent.dbgeng.model;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.*;
import static org.junit.Assert.assertTrue;
import java.util.*; import java.util.*;
import ghidra.dbg.target.*; import org.junit.Ignore;
import ghidra.dbg.test.AbstractDebuggerModelFocusTest;
public abstract class AbstractModelForDbgengFrameFocusTest import ghidra.dbg.target.*;
extends AbstractDebuggerModelFocusTest { import ghidra.dbg.test.AbstractDebuggerModelActivationTest;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForDbgengFrameActivationTest
extends AbstractDebuggerModelActivationTest {
private static final PathPattern STACK_PATTERN =
new PathPattern(PathUtils.parse("Sessions[0].Processes[].Threads[].Stack.Frames[]"));
protected DebuggerTestSpecimen getSpecimen() { protected DebuggerTestSpecimen getSpecimen() {
return WindowsSpecimen.STACK; return WindowsSpecimen.STACK;
} }
@Override @Override
protected Set<TargetObject> getFocusableThings() throws Throwable { protected Set<TargetObject> getActivatableThings() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen(); DebuggerTestSpecimen specimen = getSpecimen();
TargetLauncher launcher = findLauncher(); // root launcher should generate new inferiors TargetLauncher launcher = findLauncher(); // root launcher should generate new inferiors
waitOn(launcher.launch(specimen.getLauncherArgs())); waitOn(launcher.launch(specimen.getLauncherArgs()));
@ -44,6 +50,8 @@ public abstract class AbstractModelForDbgengFrameFocusTest
trapAt("expStack!break_here", process); trapAt("expStack!break_here", process);
waitSettled(m.getModel(), 200);
return retry(() -> { return retry(() -> {
Map<List<String>, TargetStackFrame> frames = Map<List<String>, TargetStackFrame> frames =
m.findAll(TargetStackFrame.class, seedPath(), true); m.findAll(TargetStackFrame.class, seedPath(), true);
@ -51,4 +59,21 @@ public abstract class AbstractModelForDbgengFrameFocusTest
return Set.copyOf(frames.values()); return Set.copyOf(frames.values());
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
} }
// TODO: Should probably assert default focus/activation here
@Override
@Ignore("dbgeng.dll has no event for frame activation")
public void testActivateEachViaInterpreter() throws Throwable {
}
@Override
protected void assertActiveViaInterpreter(TargetObject expected, TargetInterpreter interpreter)
throws Throwable {
String line = waitOn(interpreter.executeCapture(".frame")).trim();
assertFalse(line.contains("\n"));
int frameId = Integer.parseInt(line.split("\\s+")[0], 16);
int expId = Integer.parseInt(STACK_PATTERN.matchIndices(expected.getPath()).get(2), 16);
assertEquals(expId, frameId);
}
} }

View file

@ -18,13 +18,20 @@ package agent.dbgeng.model;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import generic.Unique;
import ghidra.dbg.target.*; import ghidra.dbg.target.*;
import ghidra.dbg.test.AbstractDebuggerModelFocusTest; import ghidra.dbg.test.AbstractDebuggerModelActivationTest;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForDbgengProcessFocusTest public abstract class AbstractModelForDbgengProcessActivationTest
extends AbstractDebuggerModelFocusTest { extends AbstractDebuggerModelActivationTest {
private static final PathPattern PROCESS_PATTERN =
new PathPattern(PathUtils.parse("Sessions[0].Processes[]"));
protected int getCount() { protected int getCount() {
return 3; return 3;
@ -35,13 +42,16 @@ public abstract class AbstractModelForDbgengProcessFocusTest
} }
@Override @Override
protected Set<TargetObject> getFocusableThings() throws Throwable { protected Set<TargetObject> getActivatableThings() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen(); DebuggerTestSpecimen specimen = getSpecimen();
TargetLauncher launcher = findLauncher(); TargetLauncher launcher = findLauncher();
int count = getCount(); int count = getCount();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
waitOn(launcher.launch(specimen.getLauncherArgs())); waitOn(launcher.launch(specimen.getLauncherArgs()));
} }
waitSettled(m.getModel(), 200);
return retry(() -> { return retry(() -> {
Map<List<String>, TargetProcess> found = Map<List<String>, TargetProcess> found =
m.findAll(TargetProcess.class, PathUtils.parse("Sessions[0]"), true); m.findAll(TargetProcess.class, PathUtils.parse("Sessions[0]"), true);
@ -49,4 +59,22 @@ public abstract class AbstractModelForDbgengProcessFocusTest
return Set.copyOf(found.values()); return Set.copyOf(found.values());
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
} }
@Override
protected void activateViaInterpreter(TargetObject obj, TargetInterpreter interpreter)
throws Throwable {
String id = Unique.assertOne(PROCESS_PATTERN.matchIndices(obj.getPath()));
waitOn(interpreter.execute("|" + id + "s"));
}
@Override
protected void assertActiveViaInterpreter(TargetObject expected, TargetInterpreter interpreter)
throws Throwable {
String output = waitOn(interpreter.executeCapture("|"));
String line = Unique.assertOne(Stream.of(output.split("\n"))
.filter(l -> l.trim().startsWith("."))
.collect(Collectors.toList())).trim();
String procId = line.split("\\s+")[1];
assertEquals(expected.getPath(), PROCESS_PATTERN.applyIndices(procId).getSingletonPath());
}
} }

View file

@ -20,13 +20,13 @@ import static ghidra.lifecycle.Unfinished.TODO;
import java.util.Set; import java.util.Set;
import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetObject;
import ghidra.dbg.test.AbstractDebuggerModelFocusTest; import ghidra.dbg.test.AbstractDebuggerModelActivationTest;
public abstract class AbstractModelForDbgengSessionFocusTest public abstract class AbstractModelForDbgengSessionActivationTest
extends AbstractDebuggerModelFocusTest { extends AbstractDebuggerModelActivationTest {
@Override @Override
protected Set<TargetObject> getFocusableThings() throws Throwable { protected Set<TargetObject> getActivatableThings() throws Throwable {
TODO("Don't know how to make multiple sessions"); TODO("Don't know how to make multiple sessions");
return null; return null;
} }

View file

@ -0,0 +1,82 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.dbgeng.model;
import static org.junit.Assert.assertEquals;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import generic.Unique;
import ghidra.dbg.target.*;
import ghidra.dbg.test.AbstractDebuggerModelActivationTest;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForDbgengThreadActivationTest
extends AbstractDebuggerModelActivationTest {
private static final PathPattern THREAD_PATTERN =
new PathPattern(PathUtils.parse("Sessions[0].Processes[].Threads[]"));
protected DebuggerTestSpecimen getSpecimen() {
return WindowsSpecimen.PRINT;
}
protected int getCount() {
return 3;
}
@Override
protected Set<TargetObject> getActivatableThings() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen();
TargetLauncher launcher = findLauncher();
int count = getCount();
for (int i = 0; i < count; i++) {
waitOn(launcher.launch(specimen.getLauncherArgs()));
}
waitSettled(m.getModel(), 200);
return retry(() -> {
Map<List<String>, TargetThread> found =
m.findAll(TargetThread.class, PathUtils.parse("Sessions[0]"), true);
assertEquals(count, found.size());
return Set.copyOf(found.values());
}, List.of(AssertionError.class));
}
@Override
protected void activateViaInterpreter(TargetObject obj, TargetInterpreter interpreter)
throws Throwable {
String threadId = THREAD_PATTERN.matchIndices(obj.getPath()).get(1);
// TODO: This test is imperfect, since processes are activated as well
waitOn(interpreter.execute("~" + threadId + "s"));
}
@Override
protected void assertActiveViaInterpreter(TargetObject expected, TargetInterpreter interpreter)
throws Throwable {
String output = waitOn(interpreter.executeCapture("~"));
String line = Unique.assertOne(Stream.of(output.split("\n"))
.filter(l -> l.trim().startsWith("."))
.collect(Collectors.toList())).trim();
int threadId = Integer.parseInt(line.split("\\s+")[1]); // dbgeng TIDs are base 10
int expId = Integer.parseInt(THREAD_PATTERN.matchIndices(expected.getPath()).get(1));
assertEquals(expId, threadId);
}
}

View file

@ -1,52 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.dbgeng.model;
import static org.junit.Assert.assertEquals;
import java.util.*;
import ghidra.dbg.target.*;
import ghidra.dbg.test.AbstractDebuggerModelFocusTest;
import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForDbgengThreadFocusTest
extends AbstractDebuggerModelFocusTest {
protected int getCount() {
return 3;
}
protected DebuggerTestSpecimen getSpecimen() {
return WindowsSpecimen.PRINT;
}
@Override
protected Set<TargetObject> getFocusableThings() throws Throwable {
DebuggerTestSpecimen specimen = getSpecimen();
TargetLauncher launcher = findLauncher();
int count = getCount();
for (int i = 0; i < count; i++) {
waitOn(launcher.launch(specimen.getLauncherArgs()));
}
return retry(() -> {
Map<List<String>, TargetThread> found =
m.findAll(TargetThread.class, PathUtils.parse("Sessions[0]"), true);
assertEquals(count, found.size());
return Set.copyOf(found.values());
}, List.of(AssertionError.class));
}
}

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgeng.model.gadp; package agent.dbgeng.model.gadp;
import agent.dbgeng.model.AbstractModelForDbgengFrameFocusTest; import agent.dbgeng.model.AbstractModelForDbgengFrameActivationTest;
public class GadpModelForDbgengFrameFocusTest extends AbstractModelForDbgengFrameFocusTest { public class GadpModelForDbgengFrameFocusTest extends AbstractModelForDbgengFrameActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new GadpDbgengModelHost(); return new GadpDbgengModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgeng.model.gadp; package agent.dbgeng.model.gadp;
import agent.dbgeng.model.AbstractModelForDbgengProcessFocusTest; import agent.dbgeng.model.AbstractModelForDbgengProcessActivationTest;
public class GadpModelForDbgengProcessFocusTest extends AbstractModelForDbgengProcessFocusTest { public class GadpModelForDbgengProcessFocusTest extends AbstractModelForDbgengProcessActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new GadpDbgengModelHost(); return new GadpDbgengModelHost();

View file

@ -17,10 +17,10 @@ package agent.dbgeng.model.gadp;
import org.junit.Ignore; import org.junit.Ignore;
import agent.dbgeng.model.AbstractModelForDbgengSessionFocusTest; import agent.dbgeng.model.AbstractModelForDbgengSessionActivationTest;
@Ignore("Don't know how to make multiple sessions") @Ignore("Don't know how to make multiple sessions")
public class GadpModelForDbgengSessionFocusTest extends AbstractModelForDbgengSessionFocusTest { public class GadpModelForDbgengSessionFocusTest extends AbstractModelForDbgengSessionActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new GadpDbgengModelHost(); return new GadpDbgengModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgeng.model.gadp; package agent.dbgeng.model.gadp;
import agent.dbgeng.model.AbstractModelForDbgengThreadFocusTest; import agent.dbgeng.model.AbstractModelForDbgengThreadActivationTest;
public class GadpModelForDbgengThreadFocusTest extends AbstractModelForDbgengThreadFocusTest { public class GadpModelForDbgengThreadFocusTest extends AbstractModelForDbgengThreadActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new GadpDbgengModelHost(); return new GadpDbgengModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgeng.model.invm; package agent.dbgeng.model.invm;
import agent.dbgeng.model.AbstractModelForDbgengFrameFocusTest; import agent.dbgeng.model.AbstractModelForDbgengFrameActivationTest;
public class InVmModelForDbgengFrameFocusTest extends AbstractModelForDbgengFrameFocusTest { public class InVmModelForDbgengFrameFocusTest extends AbstractModelForDbgengFrameActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgengModelHost(); return new InVmDbgengModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgeng.model.invm; package agent.dbgeng.model.invm;
import agent.dbgeng.model.AbstractModelForDbgengProcessFocusTest; import agent.dbgeng.model.AbstractModelForDbgengProcessActivationTest;
public class InVmModelForDbgengProcessFocusTest extends AbstractModelForDbgengProcessFocusTest { public class InVmModelForDbgengProcessFocusTest extends AbstractModelForDbgengProcessActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgengModelHost(); return new InVmDbgengModelHost();

View file

@ -17,10 +17,10 @@ package agent.dbgeng.model.invm;
import org.junit.Ignore; import org.junit.Ignore;
import agent.dbgeng.model.AbstractModelForDbgengSessionFocusTest; import agent.dbgeng.model.AbstractModelForDbgengSessionActivationTest;
@Ignore("Don't know how to make multiple sessions") @Ignore("Don't know how to make multiple sessions")
public class InVmModelForDbgengSessionFocusTest extends AbstractModelForDbgengSessionFocusTest { public class InVmModelForDbgengSessionFocusTest extends AbstractModelForDbgengSessionActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgengModelHost(); return new InVmDbgengModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgeng.model.invm; package agent.dbgeng.model.invm;
import agent.dbgeng.model.AbstractModelForDbgengThreadFocusTest; import agent.dbgeng.model.AbstractModelForDbgengThreadActivationTest;
public class InVmModelForDbgengThreadFocusTest extends AbstractModelForDbgengThreadFocusTest { public class InVmModelForDbgengThreadFocusTest extends AbstractModelForDbgengThreadActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgengModelHost(); return new InVmDbgengModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgmodel.model.invm; package agent.dbgmodel.model.invm;
import agent.dbgeng.model.AbstractModelForDbgengFrameFocusTest; import agent.dbgeng.model.AbstractModelForDbgengFrameActivationTest;
public class InVmModelForDbgmodelFrameFocusTest extends AbstractModelForDbgengFrameFocusTest { public class InVmModelForDbgmodelFrameFocusTest extends AbstractModelForDbgengFrameActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgmodelModelHost(); return new InVmDbgmodelModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgmodel.model.invm; package agent.dbgmodel.model.invm;
import agent.dbgeng.model.AbstractModelForDbgengProcessFocusTest; import agent.dbgeng.model.AbstractModelForDbgengProcessActivationTest;
public class InVmModelForDbgmodelProcessFocusTest extends AbstractModelForDbgengProcessFocusTest { public class InVmModelForDbgmodelProcessFocusTest extends AbstractModelForDbgengProcessActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgmodelModelHost(); return new InVmDbgmodelModelHost();

View file

@ -17,10 +17,10 @@ package agent.dbgmodel.model.invm;
import org.junit.Ignore; import org.junit.Ignore;
import agent.dbgeng.model.AbstractModelForDbgengSessionFocusTest; import agent.dbgeng.model.AbstractModelForDbgengSessionActivationTest;
@Ignore("Don't know how to make multiple sessions") @Ignore("Don't know how to make multiple sessions")
public class InVmModelForDbgmodelSessionFocusTest extends AbstractModelForDbgengSessionFocusTest { public class InVmModelForDbgmodelSessionFocusTest extends AbstractModelForDbgengSessionActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgmodelModelHost(); return new InVmDbgmodelModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.dbgmodel.model.invm; package agent.dbgmodel.model.invm;
import agent.dbgeng.model.AbstractModelForDbgengThreadFocusTest; import agent.dbgeng.model.AbstractModelForDbgengThreadActivationTest;
public class InVmModelForDbgmodelThreadFocusTest extends AbstractModelForDbgengThreadFocusTest { public class InVmModelForDbgmodelThreadFocusTest extends AbstractModelForDbgengThreadActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmDbgmodelModelHost(); return new InVmDbgmodelModelHost();

View file

@ -152,9 +152,10 @@ public interface GdbInferior extends GdbMemoryOperations {
* will apply to this inferior. Commands issued from this handle are always executed with this * will apply to this inferior. Commands issued from this handle are always executed with this
* inferior in focus, so it is rare to invoke his method directly. * inferior in focus, so it is rare to invoke his method directly.
* *
* @param internal true to prevent announcement of the change
* @return a future that completes when GDB has executed the command * @return a future that completes when GDB has executed the command
*/ */
CompletableFuture<Void> setActive(); CompletableFuture<Void> setActive(boolean internal);
/** /**
* Specify a binary image for execution and debug symbols * Specify a binary image for execution and debug symbols

View file

@ -47,9 +47,10 @@ public interface GdbStackFrame extends GdbStackFrameOperations {
/** /**
* Make this frame the current frame * Make this frame the current frame
* *
* @param internal true to prevent announcement of the change
* @return a future that completes when the frame is the current frame * @return a future that completes when the frame is the current frame
*/ */
CompletableFuture<Void> setActive(); CompletableFuture<Void> setActive(boolean internal);
/** /**
* Get the thread for this frame * Get the thread for this frame

View file

@ -68,9 +68,10 @@ public interface GdbThread
/** /**
* Make this thread the current thread * Make this thread the current thread
* *
* @param internal true to prevent announcement of the change
* @return a future that completes when the thread is the current thread * @return a future that completes when the thread is the current thread
*/ */
CompletableFuture<Void> setActive(); CompletableFuture<Void> setActive(boolean internal);
/** /**
* Set the value of an internal GDB variable * Set the value of an internal GDB variable

View file

@ -67,7 +67,7 @@ public interface GdbCommand<T> {
* <p> * <p>
* Complete {@code} pending with a result to short-circuit the execution of this command. * Complete {@code} pending with a result to short-circuit the execution of this command.
* *
* @param pending the pending command result * @param pending the pend@Override ing command result
*/ */
void preCheck(GdbPendingCommand<? super T> pending); void preCheck(GdbPendingCommand<? super T> pending);
@ -92,6 +92,13 @@ public interface GdbCommand<T> {
*/ */
public Integer impliesCurrentFrameId(); public Integer impliesCurrentFrameId();
/**
* Check if focus announcements from this command should be suppressed
*
* @return true to suppress announcements
*/
public boolean isFocusInternallyDriven();
/** /**
* Handle an event that occurred during the execution of this command * Handle an event that occurred during the execution of this command
* *

View file

@ -208,7 +208,7 @@ public class GdbInferiorImpl implements GdbInferior {
* longer be current for the actual command execution. NB: The select command will cancel * longer be current for the actual command execution. NB: The select command will cancel
* itself if this inferior is already current. * itself if this inferior is already current.
*/ */
return setActive().thenCombine(manager.execute(cmd), (s, e) -> e); return setActive(true).thenCombine(manager.execute(cmd), (s, e) -> e);
} }
@Override @Override
@ -347,8 +347,8 @@ public class GdbInferiorImpl implements GdbInferior {
} }
@Override @Override
public CompletableFuture<Void> setActive() { public CompletableFuture<Void> setActive(boolean internal) {
return manager.setActiveInferior(this); return manager.setActiveInferior(this, internal);
} }
@Override @Override

View file

@ -961,7 +961,7 @@ public class GdbManagerImpl implements GdbManager {
event(() -> listenersEvent.fire.inferiorSelected(cur, evt.getCause()), event(() -> listenersEvent.fire.inferiorSelected(cur, evt.getCause()),
"groupRemoved-sel"); "groupRemoved-sel");
// Also cause GDB to generate thread selection events, if applicable // Also cause GDB to generate thread selection events, if applicable
setActiveInferior(cur); setActiveInferior(cur, false);
} }
} }
@ -1527,10 +1527,11 @@ public class GdbManagerImpl implements GdbManager {
* This issues a command to GDB to change its focus. It is not just a manager concept. * This issues a command to GDB to change its focus. It is not just a manager concept.
* *
* @param inferior the inferior to select * @param inferior the inferior to select
* @param internal true to prevent announcement of the change
* @return a future that completes when GDB has executed the command * @return a future that completes when GDB has executed the command
*/ */
CompletableFuture<Void> setActiveInferior(GdbInferior inferior) { CompletableFuture<Void> setActiveInferior(GdbInferior inferior, boolean internal) {
return execute(new GdbInferiorSelectCommand(this, inferior.getId())); return execute(new GdbInferiorSelectCommand(this, inferior.getId(), internal));
} }
@Override @Override

View file

@ -104,8 +104,9 @@ public class GdbStackFrameImpl implements GdbStackFrame {
} }
@Override @Override
public CompletableFuture<Void> setActive() { public CompletableFuture<Void> setActive(boolean internal) {
return manager.execute(new GdbSetActiveThreadCommand(manager, thread.getId(), level)); return manager
.execute(new GdbSetActiveThreadCommand(manager, thread.getId(), level, internal));
} }
@Override @Override

View file

@ -128,7 +128,7 @@ public class GdbThreadImpl implements GdbThread {
protected <T> CompletableFuture<T> execute(AbstractGdbCommand<T> cmd) { protected <T> CompletableFuture<T> execute(AbstractGdbCommand<T> cmd) {
switch (cmd.getInterpreter()) { switch (cmd.getInterpreter()) {
case CLI: case CLI:
return setActive().thenCombine(manager.execute(cmd), (__, v) -> v); return setActive(true).thenCombine(manager.execute(cmd), (__, v) -> v);
case MI2: case MI2:
return manager.execute(cmd); return manager.execute(cmd);
default: default:
@ -137,9 +137,9 @@ public class GdbThreadImpl implements GdbThread {
} }
@Override @Override
public CompletableFuture<Void> setActive() { public CompletableFuture<Void> setActive(boolean internal) {
// Bypass the select-me-first logic // Bypass the select-me-first logic
return manager.execute(new GdbSetActiveThreadCommand(manager, id, null)); return manager.execute(new GdbSetActiveThreadCommand(manager, id, null, internal));
} }
@Override @Override

View file

@ -106,4 +106,9 @@ public abstract class AbstractGdbCommand<T> implements GdbCommand<T> {
public Integer impliesCurrentFrameId() { public Integer impliesCurrentFrameId() {
return null; return null;
} }
@Override
public boolean isFocusInternallyDriven() {
return true;
}
} }

View file

@ -78,4 +78,9 @@ public class GdbConsoleExecCommand extends AbstractGdbCommandWithThreadAndFrameI
public Output getOutputTo() { public Output getOutputTo() {
return to; return to;
} }
@Override
public boolean isFocusInternallyDriven() {
return to == Output.CAPTURE;
}
} }

View file

@ -20,10 +20,12 @@ import agent.gdb.manager.impl.*;
public class GdbInferiorSelectCommand extends AbstractGdbCommand<Void> { public class GdbInferiorSelectCommand extends AbstractGdbCommand<Void> {
private final int id; private final int id;
private final boolean internal;
public GdbInferiorSelectCommand(GdbManagerImpl manager, int id) { public GdbInferiorSelectCommand(GdbManagerImpl manager, int id, boolean internal) {
super(manager); super(manager);
this.id = id; this.id = id;
this.internal = internal;
} }
@Override @Override
@ -58,4 +60,9 @@ public class GdbInferiorSelectCommand extends AbstractGdbCommand<Void> {
pending.checkCompletion(GdbCommandDoneEvent.class); pending.checkCompletion(GdbCommandDoneEvent.class);
return null; return null;
} }
@Override
public boolean isFocusInternallyDriven() {
return internal;
}
} }

View file

@ -20,6 +20,8 @@ import agent.gdb.manager.impl.*;
import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList; import agent.gdb.manager.parsing.GdbMiParser.GdbMiFieldList;
public class GdbSetActiveThreadCommand extends AbstractGdbCommandWithThreadAndFrameId<Void> { public class GdbSetActiveThreadCommand extends AbstractGdbCommandWithThreadAndFrameId<Void> {
private final boolean internal;
/** /**
* Select the given thread and frame level * Select the given thread and frame level
* *
@ -29,9 +31,12 @@ public class GdbSetActiveThreadCommand extends AbstractGdbCommandWithThreadAndFr
* @param manager the manager to execute the command * @param manager the manager to execute the command
* @param threadId the desired thread Id * @param threadId the desired thread Id
* @param frameId the desired frame level * @param frameId the desired frame level
* @param internal true to prevent announcement of the change
*/ */
public GdbSetActiveThreadCommand(GdbManagerImpl manager, int threadId, Integer frameId) { public GdbSetActiveThreadCommand(GdbManagerImpl manager, int threadId, Integer frameId,
boolean internal) {
super(manager, threadId, frameId); super(manager, threadId, frameId);
this.internal = internal;
} }
@Override @Override
@ -73,4 +78,9 @@ public class GdbSetActiveThreadCommand extends AbstractGdbCommandWithThreadAndFr
manager.doThreadSelected(thread, frame, done.getCause()); manager.doThreadSelected(thread, frame, done.getCause());
return null; return null;
} }
@Override
public boolean isFocusInternallyDriven() {
return internal;
}
} }

View file

@ -375,7 +375,7 @@ public class GdbModelTargetInferior
@Override @Override
@Internal @Internal
public CompletableFuture<Void> setActive() { public CompletableFuture<Void> setActive() {
return impl.gateFuture(inferior.setActive()); return impl.gateFuture(inferior.setActive(false));
} }
@TargetAttributeType(name = EXIT_CODE_ATTRIBUTE_NAME) @TargetAttributeType(name = EXIT_CODE_ATTRIBUTE_NAME)

View file

@ -21,8 +21,6 @@ import java.util.concurrent.CompletableFuture;
import agent.gdb.manager.*; import agent.gdb.manager.*;
import agent.gdb.manager.impl.*; import agent.gdb.manager.impl.*;
import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand;
import agent.gdb.manager.impl.cmd.GdbConsoleExecCommand.Output;
import agent.gdb.manager.impl.cmd.GdbStateChangeRecord; import agent.gdb.manager.impl.cmd.GdbStateChangeRecord;
import agent.gdb.manager.reason.GdbReason; import agent.gdb.manager.reason.GdbReason;
import ghidra.async.AsyncUtils; import ghidra.async.AsyncUtils;
@ -150,6 +148,13 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot
// Otherwise, we'll presumably get the =thread-selected event // Otherwise, we'll presumably get the =thread-selected event
} }
/**
* TODO: This check should be done in the manager? This "internal" concept is either a manager
* concept or a model concept. Right now, it breaches the interface.
*
* @param cause the cause to examine
* @return true if internal
*/
protected boolean isFocusInternallyDriven(GdbCause cause) { protected boolean isFocusInternallyDriven(GdbCause cause) {
if (cause == null || cause == GdbCause.Causes.UNCLAIMED) { if (cause == null || cause == GdbCause.Causes.UNCLAIMED) {
return false; return false;
@ -160,13 +165,7 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot
if (cause instanceof GdbPendingCommand<?>) { if (cause instanceof GdbPendingCommand<?>) {
GdbPendingCommand<?> pcmd = (GdbPendingCommand<?>) cause; GdbPendingCommand<?> pcmd = (GdbPendingCommand<?>) cause;
GdbCommand<?> cmd = pcmd.getCommand(); GdbCommand<?> cmd = pcmd.getCommand();
if (cmd instanceof GdbConsoleExecCommand) { return cmd.isFocusInternallyDriven();
GdbConsoleExecCommand exec = (GdbConsoleExecCommand) cmd;
if (exec.getOutputTo() == Output.CAPTURE) {
return true;
}
return false;
}
} }
return true; return true;
} }
@ -323,7 +322,7 @@ public class GdbModelTargetSession extends DefaultTargetModelRoot
if (impl.gdb.getKnownThreads().get(thread.getId()) != thread) { if (impl.gdb.getKnownThreads().get(thread.getId()) != thread) {
return; return;
} }
thread.setActive().exceptionally(ex -> { thread.setActive(true).exceptionally(ex -> {
impl.reportError(this, "Could not restore event thread", ex); impl.reportError(this, "Could not restore event thread", ex);
return null; return null;
}); });

View file

@ -115,7 +115,7 @@ public class GdbModelTargetStackFrame extends DefaultTargetObject<TargetObject,
@Override @Override
@Internal @Internal
public CompletableFuture<Void> setActive() { public CompletableFuture<Void> setActive() {
return impl.gateFuture(frame.setActive()); return impl.gateFuture(frame.setActive(false));
} }
@TargetAttributeType(name = FUNC_ATTRIBUTE_NAME) @TargetAttributeType(name = FUNC_ATTRIBUTE_NAME)

View file

@ -226,7 +226,7 @@ public class GdbModelTargetThread
@Override @Override
@Internal @Internal
public CompletableFuture<Void> setActive() { public CompletableFuture<Void> setActive() {
return impl.gateFuture(thread.setActive()); return impl.gateFuture(thread.setActive(false));
} }
public GdbModelTargetBreakpointLocation breakpointHit(GdbBreakpointHitReason reason) { public GdbModelTargetBreakpointLocation breakpointHit(GdbBreakpointHitReason reason) {

View file

@ -412,7 +412,7 @@ public abstract class AbstractGdbManagerTest extends AbstractGhidraHeadlessInteg
GdbThread thread = waitOn(mgr.currentInferior().run()); GdbThread thread = waitOn(mgr.currentInferior().run());
waitOn(mgr.waitForState(GdbState.STOPPED)); waitOn(mgr.waitForState(GdbState.STOPPED));
//waitOn(mgr.waitForPrompt()); //waitOn(mgr.waitForPrompt());
waitOn(thread.setActive()); waitOn(thread.setActive(false));
} }
} }

View file

@ -15,21 +15,27 @@
*/ */
package agent.gdb.model; package agent.gdb.model;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.*;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import generic.Unique;
import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet; import ghidra.dbg.target.TargetBreakpointSpecContainer.TargetBreakpointKindSet;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.target.TargetStackFrame;
import ghidra.dbg.test.*; import ghidra.dbg.test.*;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.*; import ghidra.program.model.address.*;
public abstract class AbstractModelForGdbBreakpointsTest public abstract class AbstractModelForGdbBreakpointsTest
extends AbstractDebuggerModelBreakpointsTest implements ProvidesTargetViaLaunchSpecimen { extends AbstractDebuggerModelBreakpointsTest implements ProvidesTargetViaLaunchSpecimen {
private static final PathPattern BREAK_PATTERN =
new PathPattern(PathUtils.parse("Breakpoints[]"));
@Override @Override
public AbstractDebuggerModelTest getTest() { public AbstractDebuggerModelTest getTest() {
return this; return this;
@ -75,4 +81,97 @@ public abstract class AbstractModelForGdbBreakpointsTest
throw new AssertionError(); throw new AssertionError();
} }
} }
@Override
protected void placeBreakpointViaInterpreter(AddressRange range, TargetBreakpointKind kind,
TargetInterpreter interpreter) throws Throwable {
Address min = range.getMinAddress();
if (range.getLength() == 4) {
switch (kind) {
case READ:
waitOn(interpreter.execute("rwatch -l *((int*) 0x" + min + ")"));
break;
case WRITE:
waitOn(interpreter.execute("watch -l *((int*) 0x" + min + ")"));
break;
default:
fail();
}
}
else if (range.getLength() == 1) {
switch (kind) {
case SW_EXECUTE:
waitOn(interpreter.execute("break *0x" + min));
break;
case HW_EXECUTE:
waitOn(interpreter.execute("hbreak *0x" + min));
break;
default:
fail();
}
}
else {
fail();
}
}
@Override
protected void disableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
assert t instanceof TargetBreakpointSpec; // TODO: or Location
String index = Unique.assertOne(BREAK_PATTERN.matchIndices(t.getPath()));
waitOn(interpreter.execute("disable " + index));
}
@Override
protected void enableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
assert t instanceof TargetBreakpointSpec; // TODO: or Location
String index = Unique.assertOne(BREAK_PATTERN.matchIndices(t.getPath()));
waitOn(interpreter.execute("enable " + index));
}
@Override
protected void deleteViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
assert d instanceof TargetBreakpointSpec; // TODO: or Location
String index = Unique.assertOne(BREAK_PATTERN.matchIndices(d.getPath()));
waitOn(interpreter.execute("delete " + index));
}
@Override
protected void assertLocCoversViaInterpreter(AddressRange range, TargetBreakpointKind kind,
TargetBreakpointLocation loc, TargetInterpreter interpreter) throws Throwable {
String index =
Unique.assertOne(BREAK_PATTERN.matchIndices(loc.getSpecification().getPath()));
String output = waitOn(interpreter.executeCapture("info break " + index));
String line = Unique.assertOne(Stream.of(output.split("\n"))
.filter(l -> !l.trim().startsWith("Num"))
.collect(Collectors.toList())).trim();
assertTrue(line.startsWith(index));
// TODO: Do I care to parse the details? The ID is confirmed, and details via the object...
}
@Override
protected void assertEnabledViaInterpreter(TargetTogglable t, boolean enabled,
TargetInterpreter interpreter) throws Throwable {
assert t instanceof TargetBreakpointSpec; // TODO: or Location
String index = Unique.assertOne(BREAK_PATTERN.matchIndices(t.getPath()));
String output = waitOn(interpreter.executeCapture("info break " + index));
String line = Unique.assertOne(Stream.of(output.split("\n"))
.filter(l -> !l.trim().startsWith("Num"))
.collect(Collectors.toList())).trim();
assertTrue(line.startsWith(index));
String enb = line.split("keep")[1].trim().split("\\s+")[0];
assertEquals(enabled ? "y" : "n", enb);
}
@Override
protected void assertDeletedViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
assert d instanceof TargetBreakpointSpec; // TODO: or Location
String index = Unique.assertOne(BREAK_PATTERN.matchIndices(d.getPath()));
String output = waitOn(interpreter.executeCapture("info break " + index));
assertTrue(output.contains("No breakpoint"));
}
} }

View file

@ -15,23 +15,31 @@
*/ */
package agent.gdb.model; package agent.gdb.model;
import static org.junit.Assert.*;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import generic.Unique;
import ghidra.dbg.target.*; import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind; import ghidra.dbg.target.TargetBreakpointSpec.TargetBreakpointKind;
import ghidra.dbg.test.AbstractDebuggerModelFocusTest; import ghidra.dbg.test.AbstractDebuggerModelActivationTest;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForGdbFrameFocusTest extends AbstractDebuggerModelFocusTest { public abstract class AbstractModelForGdbFrameActivationTest
extends AbstractDebuggerModelActivationTest {
private static final PathPattern STACK_PATTERN =
new PathPattern(PathUtils.parse("Inferiors[1].Threads[1].Stack[]"));
DebuggerTestSpecimen getSpecimen() { DebuggerTestSpecimen getSpecimen() {
return GdbLinuxSpecimen.STACK; return GdbLinuxSpecimen.STACK;
} }
@Override @Override
protected Set<TargetObject> getFocusableThings() throws Throwable { protected Set<TargetObject> getActivatableThings() throws Throwable {
CompletableFuture<?> frame0 = CompletableFuture<?> frame0 =
m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1].Stack[0]")); m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1].Stack[0]"));
CompletableFuture<?> frame1 = CompletableFuture<?> frame1 =
@ -48,6 +56,8 @@ public abstract class AbstractModelForGdbFrameFocusTest extends AbstractDebugger
(TargetResumable) waitOn(m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1]"))); (TargetResumable) waitOn(m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1]")));
waitOn(inf.resume()); waitOn(inf.resume());
waitSettled(m.getModel(), 200);
return Set.of( return Set.of(
(TargetObject) waitOn(frame0), (TargetObject) waitOn(frame0),
(TargetObject) waitOn(frame1), (TargetObject) waitOn(frame1),
@ -55,7 +65,24 @@ public abstract class AbstractModelForGdbFrameFocusTest extends AbstractDebugger
} }
@Override @Override
protected List<String> getExpectedDefaultFocus() { protected List<String> getExpectedDefaultActivePath() {
return PathUtils.parse("Inferiors[1].Threads[1].Stack[0]"); return PathUtils.parse("Inferiors[1].Threads[1].Stack[0]");
} }
@Override
protected void activateViaInterpreter(TargetObject obj, TargetInterpreter interpreter)
throws Throwable {
String index = Unique.assertOne(STACK_PATTERN.matchIndices(obj.getPath()));
waitOn(interpreter.execute("frame " + index));
}
@Override
protected void assertActiveViaInterpreter(TargetObject expected, TargetInterpreter interpreter)
throws Throwable {
String line = waitOn(interpreter.executeCapture("frame")).trim();
assertFalse(line.contains("\n"));
assertTrue(line.startsWith("#"));
String frameLevel = line.substring(1).split("\\s+")[0];
assertEquals(expected.getPath(), STACK_PATTERN.applyIndices(frameLevel).getSingletonPath());
}
} }

View file

@ -15,19 +15,28 @@
*/ */
package agent.gdb.model; package agent.gdb.model;
import static org.junit.Assert.assertEquals;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import generic.Unique;
import ghidra.dbg.target.TargetInterpreter; import ghidra.dbg.target.TargetInterpreter;
import ghidra.dbg.target.TargetObject; import ghidra.dbg.target.TargetObject;
import ghidra.dbg.test.AbstractDebuggerModelFocusTest; import ghidra.dbg.test.AbstractDebuggerModelActivationTest;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForGdbInferiorFocusTest extends AbstractDebuggerModelFocusTest { public abstract class AbstractModelForGdbInferiorActivationTest
extends AbstractDebuggerModelActivationTest {
private static final PathPattern INF_PATTERN = new PathPattern(PathUtils.parse("Inferiors[]"));;
@Override @Override
protected Set<TargetObject> getFocusableThings() throws Throwable { protected Set<TargetObject> getActivatableThings() throws Throwable {
CompletableFuture<?> inf1 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1]")); CompletableFuture<?> inf1 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1]"));
CompletableFuture<?> inf2 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[2]")); CompletableFuture<?> inf2 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[2]"));
CompletableFuture<?> inf3 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[3]")); CompletableFuture<?> inf3 = m.getAddedWaiter().wait(PathUtils.parse("Inferiors[3]"));
@ -37,6 +46,8 @@ public abstract class AbstractModelForGdbInferiorFocusTest extends AbstractDebug
waitOn(interpreter.execute("add-inferior")); waitOn(interpreter.execute("add-inferior"));
waitOn(interpreter.execute("add-inferior")); waitOn(interpreter.execute("add-inferior"));
waitSettled(m.getModel(), 200);
return Set.of( return Set.of(
(TargetObject) waitOn(inf1), (TargetObject) waitOn(inf1),
(TargetObject) waitOn(inf2), (TargetObject) waitOn(inf2),
@ -44,7 +55,25 @@ public abstract class AbstractModelForGdbInferiorFocusTest extends AbstractDebug
} }
@Override @Override
protected List<String> getExpectedDefaultFocus() { protected List<String> getExpectedDefaultActivePath() {
return PathUtils.parse("Inferiors[1]"); return PathUtils.parse("Inferiors[1]");
} }
@Override
protected void activateViaInterpreter(TargetObject obj, TargetInterpreter interpreter)
throws Throwable {
String index = Unique.assertOne(INF_PATTERN.matchIndices(obj.getPath()));
waitOn(interpreter.execute("inferior " + index));
}
@Override
protected void assertActiveViaInterpreter(TargetObject expected, TargetInterpreter interpreter)
throws Throwable {
String output = waitOn(interpreter.executeCapture("info inferiors"));
String line = Unique.assertOne(Stream.of(output.split("\n"))
.filter(l -> l.trim().startsWith("*"))
.collect(Collectors.toList())).trim();
String inferiorId = line.split("\\s+")[1];
assertEquals(expected.getPath(), INF_PATTERN.applyIndices(inferiorId).getSingletonPath());
}
} }

View file

@ -0,0 +1,92 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.model;
import static org.junit.Assert.assertEquals;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import generic.Unique;
import ghidra.dbg.target.*;
import ghidra.dbg.test.AbstractDebuggerModelActivationTest;
import ghidra.dbg.util.PathPattern;
import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForGdbThreadActivationTest
extends AbstractDebuggerModelActivationTest {
private static final PathPattern THREAD_PATTERN =
new PathPattern(PathUtils.parse("Inferiors[].Threads[]"));
protected DebuggerTestSpecimen getSpecimen() {
return GdbLinuxSpecimen.ECHO_HW;
}
@Override
protected Set<TargetObject> getActivatableThings() throws Throwable {
/**
* TODO: GDB should really use the 1.1 and 2.1 numbering instead of the GId, but I don't
* know a good way via GDB/MI to obtain the thread's per-inferior Id.
*
* NB: A lot of the test takes advantage of the iid and tid being the same. Don't try to
* apply the pattern matching used here in other contexts.
*/
CompletableFuture<?> inf1 =
m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1]"));
CompletableFuture<?> inf2 =
m.getAddedWaiter().wait(PathUtils.parse("Inferiors[2].Threads[2]"));
DebuggerTestSpecimen specimen = getSpecimen();
TargetLauncher launcher = findLauncher(); // root launcher should generate new inferiors
waitOn(launcher.launch(specimen.getLauncherArgs()));
waitOn(launcher.launch(specimen.getLauncherArgs()));
waitSettled(m.getModel(), 200);
return Set.of(
(TargetObject) waitOn(inf1),
(TargetObject) waitOn(inf2));
}
@Override
protected List<String> getExpectedDefaultActivePath() {
return PathUtils.parse("Inferiors[2].Threads[2].Stack[0]");
}
@Override
protected void activateViaInterpreter(TargetObject obj, TargetInterpreter interpreter)
throws Throwable {
String index = Unique.assertOne(Set.copyOf(THREAD_PATTERN.matchIndices(obj.getPath())));
// TODO: This test is imperfect, since inferiors are activated as well
waitOn(interpreter.execute("thread " + index + ".1"));
}
@Override
protected void assertActiveViaInterpreter(TargetObject expected, TargetInterpreter interpreter)
throws Throwable {
String output = waitOn(interpreter.executeCapture("info threads -gid"));
String line = Unique.assertOne(Stream.of(output.split("\n"))
.filter(l -> l.trim().startsWith("*"))
.collect(Collectors.toList()));
String threadGid = line.split("\\s+")[2];
assertEquals(expected.getPath(),
THREAD_PATTERN.applyIndices(threadGid, threadGid).getSingletonPath());
}
}

View file

@ -1,54 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package agent.gdb.model;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import ghidra.dbg.target.TargetLauncher;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.test.AbstractDebuggerModelFocusTest;
import ghidra.dbg.util.PathUtils;
public abstract class AbstractModelForGdbThreadFocusTest extends AbstractDebuggerModelFocusTest {
protected DebuggerTestSpecimen getSpecimen() {
return GdbLinuxSpecimen.ECHO_HW;
}
@Override
protected Set<TargetObject> getFocusableThings() throws Throwable {
CompletableFuture<?> inf1 =
m.getAddedWaiter().wait(PathUtils.parse("Inferiors[1].Threads[1]"));
CompletableFuture<?> inf2 =
m.getAddedWaiter().wait(PathUtils.parse("Inferiors[2].Threads[2]"));
DebuggerTestSpecimen specimen = getSpecimen();
TargetLauncher launcher = findLauncher(); // root launcher should generate new inferiors
waitOn(launcher.launch(specimen.getLauncherArgs()));
waitOn(launcher.launch(specimen.getLauncherArgs()));
return Set.of(
(TargetObject) waitOn(inf1),
(TargetObject) waitOn(inf2));
}
@Override
protected List<String> getExpectedDefaultFocus() {
return PathUtils.parse("Inferiors[2].Threads[2].Stack[0]");
}
}

View file

@ -15,9 +15,9 @@
*/ */
package agent.gdb.model.invm; package agent.gdb.model.invm;
import agent.gdb.model.AbstractModelForGdbFrameFocusTest; import agent.gdb.model.AbstractModelForGdbFrameActivationTest;
public class InVmModelForGdbFrameFocusTest extends AbstractModelForGdbFrameFocusTest { public class InVmModelForGdbFrameFocusTest extends AbstractModelForGdbFrameActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmGdbModelHost(); return new InVmGdbModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.gdb.model.invm; package agent.gdb.model.invm;
import agent.gdb.model.AbstractModelForGdbInferiorFocusTest; import agent.gdb.model.AbstractModelForGdbInferiorActivationTest;
public class InVmModelForGdbInferiorFocusTest extends AbstractModelForGdbInferiorFocusTest { public class InVmModelForGdbInferiorFocusTest extends AbstractModelForGdbInferiorActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmGdbModelHost(); return new InVmGdbModelHost();

View file

@ -15,9 +15,9 @@
*/ */
package agent.gdb.model.invm; package agent.gdb.model.invm;
import agent.gdb.model.AbstractModelForGdbThreadFocusTest; import agent.gdb.model.AbstractModelForGdbThreadActivationTest;
public class InVmModelForGdbThreadFocusTest extends AbstractModelForGdbThreadFocusTest { public class InVmModelForGdbThreadFocusTest extends AbstractModelForGdbThreadActivationTest {
@Override @Override
public ModelHost modelHost() throws Throwable { public ModelHost modelHost() throws Throwable {
return new InVmGdbModelHost(); return new InVmGdbModelHost();

View file

@ -742,9 +742,8 @@ public enum DebugModelConventions {
this.name = name; this.name = name;
this.obj = obj; this.obj = obj;
obj.addListener(this); obj.addListener(this);
obj.fetchAttribute(name).thenAccept(t -> { set((T) obj.getCachedAttribute(name), null);
set((T) t, null); obj.fetchAttribute(name).exceptionally(ex -> {
}).exceptionally(ex -> {
Msg.error(this, "Could not get initial value of " + name + " for " + obj, ex); Msg.error(this, "Could not get initial value of " + name + " for " + obj, ex);
return null; return null;
}); });

View file

@ -22,6 +22,7 @@ import ghidra.dbg.DebuggerTargetObjectIface;
/** /**
* An object made active * An object made active
* *
* <p>
* "Active" here describes which object in a given class the target should operate on * "Active" here describes which object in a given class the target should operate on
*/ */
@DebuggerTargetObjectIface("ActiveScope") @DebuggerTargetObjectIface("ActiveScope")

View file

@ -194,4 +194,40 @@ public class PathPattern implements PathPredicates {
} }
return new PathPattern(result); return new PathPattern(result);
} }
/**
* If the given path matches, extract indices where matched by wildcards
*
* <p>
* This is essentially the inverse of {@link #applyIndices(List)}, but can only be asked of one
* pattern. The keys are returned from left to right, in the order matched by the pattern. Only
* those keys matched by a wildcard are included in the result. Indices are extracted with the
* brackets {@code []} removed.
*
* @param path the path to match
* @return the list of matched indices or {@code null} if not matched
*/
public List<String> matchIndices(List<String> path) {
int length = pattern.size();
if (length != path.size()) {
return null;
}
List<String> result = new ArrayList<>();
for (int i = 0; i < length; i++) {
String pat = pattern.get(i);
String key = path.get(i);
if (!keyMatches(pat, key)) {
return null;
}
if (isWildcard(pat)) {
if (PathUtils.isIndex(pat)) {
result.add(PathUtils.parseIndex(key));
}
else {
result.add(key);
}
}
}
return result;
}
} }

View file

@ -0,0 +1,198 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.Test;
import generic.Unique;
import ghidra.dbg.target.*;
import ghidra.dbg.util.PathUtils;
/**
* Test model object activation and focus
*
* <p>
* Activation and focus are related but separate concepts. Focus is a little looser, and is allowed
* by the model to exactly match the client's notion of focus, usually indicating the object of the
* user's interest. Activation, however, commands the model to make the given object the "current"
* object. This implies any commands issued to the CLI will affect the active object. The model
* reflects the active object back to the client via focus. This allows the model and client to
* synchronize their "active" objects, while reducing the likelihood of event feedback loops.
* Furthermore, not every object can be activated. For example, activating a register will likely
* result in the containing thread or frame becoming active instead. Or, activating a thread may
* result in its innermost frame becoming active as well.
*/
public abstract class AbstractDebuggerModelActivationTest extends AbstractDebuggerModelTest {
/**
* Use the interpreter to activate the given object
*
* @param obj the object to activate
* @param interpreter the interpreter to use
* @throws Throwable if anything goes wrong
*/
protected void activateViaInterpreter(TargetObject obj, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Use the interpreter to verify the given object is active/current
*
* <p>
* Note, it may be necessary to run and capture several commands, depending on what's being
* verified and what sort of commands the interpreter makes available. For example, to verify a
* frame is active, the test should check that the containing thread and process are active,
* too.
*
* @param expected the expected active or current object
* @param interpreter the interpreter to use
* @throws Throwable if anything goes wrong
*/
protected void assertActiveViaInterpreter(TargetObject expected,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Get (possibly generate) things for this focus test to try out
*
* @throws Throwable if anything goes wrong
*/
protected abstract Set<TargetObject> getActivatableThings() throws Throwable;
/**
* Governs whether assertions permit the actual object to be a successor of the expected object
*
* @return true to permit successors, false to require exact
*/
protected boolean permitSuccessor() {
return true;
}
protected void assertSuccessorOrExact(TargetObject expected, TargetObject actual) {
assertNotNull(actual);
if (permitSuccessor()) {
assertTrue("Expected successor of '" + expected.getJoinedPath(".") +
"' got '" + actual.getJoinedPath(".") + "'",
PathUtils.isAncestor(expected.getPath(), actual.getPath()));
}
else {
assertSame(expected, actual);
}
}
/**
* If the default focus is one of the activatable things (after generation), assert its path
*
* @return the path of the expected default focus, or {@code null} for no assertion
*/
protected List<String> getExpectedDefaultActivePath() {
return null;
}
@Test
public void testDefaultFocusIsAsExpected() throws Throwable {
List<String> expectedDefaultFocus = getExpectedDefaultActivePath();
assumeNotNull(expectedDefaultFocus);
m.build();
TargetFocusScope focusScope = findFocusScope();
Set<TargetObject> activatable = getActivatableThings();
// The default must be one of the activatable objects
TargetObject obj = Unique.assertOne(activatable.stream()
.filter(f -> PathUtils.isAncestor(f.getPath(), expectedDefaultFocus))
.collect(Collectors.toList()));
retryVoid(() -> {
assertEquals(expectedDefaultFocus, focusScope.getFocus().getPath());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
}
@Test
public void testActivateEachOnce() throws Throwable {
m.build();
TargetFocusScope focusScope = findFocusScope();
TargetActiveScope activeScope = findActiveScope();
Set<TargetObject> activatable = getActivatableThings();
for (TargetObject obj : activatable) {
waitOn(activeScope.requestActivation(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
}
}
@Test
public void testActivateEachTwice() throws Throwable {
m.build();
TargetFocusScope focusScope = findFocusScope();
TargetActiveScope activeScope = findActiveScope();
Set<TargetObject> activatable = getActivatableThings();
for (TargetObject obj : activatable) {
waitOn(activeScope.requestActivation(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
waitOn(activeScope.requestActivation(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertActiveViaInterpreter(obj, interpreter);
}
}
}
@Test
public void testActivateEachViaInterpreter() throws Throwable {
assumeTrue(m.hasInterpreter());
m.build();
TargetFocusScope focusScope = findFocusScope();
TargetInterpreter interpreter = findInterpreter();
Set<TargetObject> activatable = getActivatableThings();
for (TargetObject obj : activatable) {
activateViaInterpreter(obj, interpreter);
retryVoid(() -> {
assertSuccessorOrExact(obj, focusScope.getFocus());
}, List.of(AssertionError.class));
assertActiveViaInterpreter(obj, interpreter);
}
}
}

View file

@ -162,10 +162,97 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
return null; return null;
} }
protected void runTestPlaceBreakpoint(TargetBreakpointKind kind) throws Throwable { /**
assumeTrue(getExpectedSupportedKinds().contains(kind)); * Verify that the given breakpoint location covers the required range and kind, using the
m.build(); * interpreter
*
* @param range the requested range of the breakpoint
* @param kind the requested kind of the breakpoint
* @param loc the location object
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void assertLocCoversViaInterpreter(AddressRange range,
TargetBreakpointKind kind, TargetBreakpointLocation loc,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Verify that the given spec and/or location is in the given state, using the interpreter
*
* @param t the spec or location
* @param enabled the expected state: true for enabled, false for disabled
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void assertEnabledViaInterpreter(TargetTogglable t, boolean enabled,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Verify that the given spec and/or location no longer exists, using the interpreter
*
* @param d the spec or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void assertDeletedViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Place the given breakpoint using the interpreter
*
* @param range the requested range
* @param kind the requested kind
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void placeBreakpointViaInterpreter(AddressRange range, TargetBreakpointKind kind,
TargetInterpreter interpreter) throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Disable the given spec and/or location using the interpreter
*
* @param t the spec and/or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void disableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Enable the given spec and/or location using the interpreter
*
* @param t the spec and/or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void enableViaInterpreter(TargetTogglable t, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
/**
* Delete the given spec and/or location using the interpreter
*
* @param d the spec and/or location
* @param interpreter the interpreter
* @throws Throwable if anything goes wrong
*/
protected void deleteViaInterpreter(TargetDeletable d, TargetInterpreter interpreter)
throws Throwable {
fail("Unless hasInterpreter is false, the test must implement this method");
}
protected void addMonitor() {
var monitor = new DebuggerModelListener() { var monitor = new DebuggerModelListener() {
DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this); DebuggerCallbackReorderer reorderer = new DebuggerCallbackReorderer(this);
@ -217,25 +304,56 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
} }
}; };
m.getModel().addModelListener(monitor.reorderer, true); m.getModel().addModelListener(monitor.reorderer, true);
}
protected void runTestPlaceBreakpoint(TargetBreakpointKind kind) throws Throwable {
assumeTrue(getExpectedSupportedKinds().contains(kind));
m.build();
addMonitor();
TargetObject target = obtainTarget(); TargetObject target = obtainTarget();
TargetBreakpointSpecContainer container = findBreakpointSpecContainer(target.getPath()); TargetBreakpointSpecContainer container = findBreakpointSpecContainer(target.getPath());
AddressRange range = getSuitableRangeForBreakpoint(target, kind); AddressRange range = getSuitableRangeForBreakpoint(target, kind);
waitOn(container.placeBreakpoint(range, Set.of(kind))); waitOn(container.placeBreakpoint(range, Set.of(kind)));
retryVoid(() -> { TargetBreakpointLocation loc = retry(() -> {
Collection<? extends TargetBreakpointLocation> found = Collection<? extends TargetBreakpointLocation> found =
m.findAll(TargetBreakpointLocation.class, target.getPath(), true).values(); m.findAll(TargetBreakpointLocation.class, target.getPath(), true).values();
assertAtLeastOneLocCovers(found, range, kind); return assertAtLeastOneLocCovers(found, range, kind);
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertLocCoversViaInterpreter(range, kind, loc, interpreter);
}
}
protected void runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind kind)
throws Throwable {
assumeTrue(getExpectedSupportedKinds().contains(kind));
assumeTrue(m.hasInterpreter());
m.build();
addMonitor();
TargetObject target = obtainTarget();
TargetInterpreter interpreter = findInterpreter();
AddressRange range = getSuitableRangeForBreakpoint(target, kind);
placeBreakpointViaInterpreter(range, kind, interpreter);
TargetBreakpointLocation loc = retry(() -> {
Collection<? extends TargetBreakpointLocation> found =
m.findAll(TargetBreakpointLocation.class, target.getPath(), true).values();
return assertAtLeastOneLocCovers(found, range, kind);
}, List.of(AssertionError.class));
assertLocCoversViaInterpreter(range, kind, loc, interpreter);
} }
@Test @Test
public void testPlaceSoftwareBreakpoint() throws Throwable { public void testPlaceSoftwareExecuteBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.SW_EXECUTE); runTestPlaceBreakpoint(TargetBreakpointKind.SW_EXECUTE);
} }
@Test @Test
public void testPlaceHardwareBreakpoint() throws Throwable { public void testPlaceHardwareExecuteBreakpoint() throws Throwable {
runTestPlaceBreakpoint(TargetBreakpointKind.HW_EXECUTE); runTestPlaceBreakpoint(TargetBreakpointKind.HW_EXECUTE);
} }
@ -249,6 +367,26 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
runTestPlaceBreakpoint(TargetBreakpointKind.WRITE); runTestPlaceBreakpoint(TargetBreakpointKind.WRITE);
} }
@Test
public void testPlaceSoftwareExecuteBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.SW_EXECUTE);
}
@Test
public void testPlaceHardwareExecuteBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.HW_EXECUTE);
}
@Test
public void testPlaceReadBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.READ);
}
@Test
public void testPlaceWriteBreakpointViaInterpreter() throws Throwable {
runTestPlaceBreakpointViaInterpreter(TargetBreakpointKind.WRITE);
}
protected Set<TargetBreakpointLocation> createLocations() throws Throwable { protected Set<TargetBreakpointLocation> createLocations() throws Throwable {
// TODO: Test with multiple targets? // TODO: Test with multiple targets?
TargetObject target = obtainTarget(); TargetObject target = obtainTarget();
@ -278,6 +416,10 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> { retryVoid(() -> {
assertFalse(t.isEnabled()); assertFalse(t.isEnabled());
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, false, interpreter);
}
} }
// Repeat it for fun. Should have no effect // Repeat it for fun. Should have no effect
for (TargetTogglable t : order) { for (TargetTogglable t : order) {
@ -285,6 +427,10 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> { retryVoid(() -> {
assertFalse(t.isEnabled()); assertFalse(t.isEnabled());
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, false, interpreter);
}
} }
// Enable each // Enable each
@ -293,6 +439,10 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> { retryVoid(() -> {
assertTrue(t.isEnabled()); assertTrue(t.isEnabled());
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, true, interpreter);
}
} }
// Repeat it for fun. Should have no effect // Repeat it for fun. Should have no effect
for (TargetTogglable t : order) { for (TargetTogglable t : order) {
@ -300,6 +450,49 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
retryVoid(() -> { retryVoid(() -> {
assertTrue(t.isEnabled()); assertTrue(t.isEnabled());
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertEnabledViaInterpreter(t, true, interpreter);
}
}
}
protected void runToggleTestViaInterpreter(Set<TargetTogglable> set,
TargetInterpreter interpreter) throws Throwable {
List<TargetTogglable> order = new ArrayList<>(set);
Collections.shuffle(order);
// Disable each
for (TargetTogglable t : order) {
disableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, false, interpreter);
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
disableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertFalse(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, false, interpreter);
}
// Enable each
for (TargetTogglable t : order) {
enableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, true, interpreter);
}
// Repeat it for fun. Should have no effect
for (TargetTogglable t : order) {
enableViaInterpreter(t, interpreter);
retryVoid(() -> {
assertTrue(t.isEnabled());
}, List.of(AssertionError.class));
assertEnabledViaInterpreter(t, true, interpreter);
} }
} }
@ -313,6 +506,19 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
@Test
public void testToggleBreakpointsViaInterpreter() throws Throwable {
assumeTrue(m.hasInterpreter());
m.build();
TargetInterpreter interpreter = findInterpreter();
Set<TargetBreakpointLocation> locs = createLocations();
runToggleTestViaInterpreter(locs.stream()
.map(l -> l.getSpecification().as(TargetTogglable.class))
.collect(Collectors.toSet()),
interpreter);
}
@Test @Test
public void testToggleBreakpointLocations() throws Throwable { public void testToggleBreakpointLocations() throws Throwable {
assumeTrue(isSupportsTogglableLocations()); assumeTrue(isSupportsTogglableLocations());
@ -323,15 +529,46 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
locs.stream().map(l -> l.as(TargetTogglable.class)).collect(Collectors.toSet())); locs.stream().map(l -> l.as(TargetTogglable.class)).collect(Collectors.toSet()));
} }
@Test
public void testToggleBreakpointLocationsViaInterpreter() throws Throwable {
assumeTrue(isSupportsTogglableLocations());
assumeTrue(m.hasInterpreter());
m.build();
TargetInterpreter interpreter = findInterpreter();
Set<TargetBreakpointLocation> locs = createLocations();
runToggleTestViaInterpreter(
locs.stream().map(l -> l.as(TargetTogglable.class)).collect(Collectors.toSet()),
interpreter);
}
protected void runDeleteTest(Set<TargetDeletable> set) throws Throwable { protected void runDeleteTest(Set<TargetDeletable> set) throws Throwable {
List<TargetDeletable> order = new ArrayList<>(set); List<TargetDeletable> order = new ArrayList<>(set);
Collections.shuffle(order); Collections.shuffle(order);
// Disable each // Delete each
for (TargetDeletable d : order) { for (TargetDeletable d : order) {
waitOn(d.delete()); waitOn(d.delete());
retryVoid(() -> { retryVoid(() -> {
assertFalse(d.isValid()); assertFalse(d.isValid());
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
if (m.hasInterpreter()) {
TargetInterpreter interpreter = findInterpreter();
assertDeletedViaInterpreter(d, interpreter);
}
}
}
protected void runDeleteTestViaInterpreter(Set<TargetDeletable> set,
TargetInterpreter interpreter) throws Throwable {
List<TargetDeletable> order = new ArrayList<>(set);
Collections.shuffle(order);
// Delete each
for (TargetDeletable d : order) {
deleteViaInterpreter(d, interpreter);
retryVoid(() -> {
assertFalse(d.isValid());
}, List.of(AssertionError.class));
assertDeletedViaInterpreter(d, interpreter);
} }
} }
@ -345,6 +582,19 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
@Test
public void testDeleteBreakpointsViaInterpreter() throws Throwable {
assumeTrue(m.hasInterpreter());
m.build();
TargetInterpreter interpreter = findInterpreter();
Set<TargetBreakpointLocation> locs = createLocations();
runDeleteTestViaInterpreter(locs.stream()
.map(l -> l.getSpecification().as(TargetDeletable.class))
.collect(Collectors.toSet()),
interpreter);
}
@Test @Test
public void testDeleteBreakpointLocations() throws Throwable { public void testDeleteBreakpointLocations() throws Throwable {
assumeTrue(isSupportsDeletableLocations()); assumeTrue(isSupportsDeletableLocations());
@ -354,4 +604,17 @@ public abstract class AbstractDebuggerModelBreakpointsTest extends AbstractDebug
runDeleteTest( runDeleteTest(
locs.stream().map(l -> l.as(TargetDeletable.class)).collect(Collectors.toSet())); locs.stream().map(l -> l.as(TargetDeletable.class)).collect(Collectors.toSet()));
} }
@Test
public void testDeleteBreakpointLocationsViaInterpreter() throws Throwable {
assumeTrue(isSupportsDeletableLocations());
assumeTrue(m.hasInterpreter());
m.build();
TargetInterpreter interpreter = findInterpreter();
Set<TargetBreakpointLocation> locs = createLocations();
runDeleteTestViaInterpreter(
locs.stream().map(l -> l.as(TargetDeletable.class)).collect(Collectors.toSet()),
interpreter);
}
} }

View file

@ -1,120 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.dbg.test;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import ghidra.dbg.target.TargetFocusScope;
import ghidra.dbg.target.TargetObject;
import ghidra.dbg.util.PathUtils;
/**
* TODO: Since activation and focus are separate concepts, we need to fix the terminology here and
* ensure we're testing the right things.
*/
public abstract class AbstractDebuggerModelFocusTest extends AbstractDebuggerModelTest {
/**
* Get (possibly generate) things for this focus test to try out
*
* @throws Throwable if anything goes wrong
*/
protected abstract Set<TargetObject> getFocusableThings() throws Throwable;
/**
* Governs whether assertions permit the actual object to be a successor of the expected object
*
* @return true to permit successors, false to require exact
*/
protected boolean permitSuccessor() {
return true;
}
protected void assertSuccessorOrExact(TargetObject expected, TargetObject actual) {
assertNotNull(actual);
if (permitSuccessor()) {
assertTrue("Expected successor of '" + expected.getJoinedPath(".") +
"' got '" + actual.getJoinedPath(".") + "'",
PathUtils.isAncestor(expected.getPath(), actual.getPath()));
}
else {
assertSame(expected, actual);
}
}
/**
* If the default focus is one of the focusable things (after generation), assert its path
*
* @return the path of the expected default focus, or {@code null} for no assertion
*/
protected List<String> getExpectedDefaultFocus() {
return null;
}
@Test
public void testDefaultFocusIsAsExpected() throws Throwable {
List<String> expectedDefaultFocus = getExpectedDefaultFocus();
assumeNotNull(expectedDefaultFocus);
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
// The default must be one of the focusable objects
assertTrue(focusable.stream()
.anyMatch(f -> PathUtils.isAncestor(f.getPath(), expectedDefaultFocus)));
retryVoid(() -> {
assertEquals(expectedDefaultFocus, scope.getFocus().getPath());
}, List.of(AssertionError.class));
}
@Test
public void testFocusEachOnce() throws Throwable {
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
for (TargetObject obj : focusable) {
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
}
}
@Test
public void testFocusEachTwice() throws Throwable {
m.build();
TargetFocusScope scope = findFocusScope();
Set<TargetObject> focusable = getFocusableThings();
for (TargetObject obj : focusable) {
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
waitOn(scope.requestFocus(obj));
retryVoid(() -> {
assertSuccessorOrExact(obj, scope.getFocus());
}, List.of(AssertionError.class));
}
}
}

View file

@ -15,7 +15,6 @@
*/ */
package ghidra.dbg.test; package ghidra.dbg.test;
import static ghidra.lifecycle.Unfinished.TODO;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static org.junit.Assume.assumeNotNull; import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue; import static org.junit.Assume.assumeTrue;
@ -24,7 +23,6 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import ghidra.async.AsyncReference; import ghidra.async.AsyncReference;
@ -182,21 +180,6 @@ public abstract class AbstractDebuggerModelInterpreterTest extends AbstractDebug
runTestExecute(interpreter, cmd); runTestExecute(interpreter, cmd);
} }
@Test
@Ignore
public void testFocusIsSynced() throws Throwable {
TODO();
}
@Test
@Ignore
public void testBreakpointsAreSynced() throws Throwable {
TODO();
// TODO: Place different kinds
// TODO: Enable/disable
// TODO: Delete (spec vs. loc?)
}
protected TargetProcess runTestLaunchViaInterpreterShowsInProcessContainer( protected TargetProcess runTestLaunchViaInterpreterShowsInProcessContainer(
TargetInterpreter interpreter) throws Throwable { TargetInterpreter interpreter) throws Throwable {
DebuggerTestSpecimen specimen = getLaunchSpecimen(); DebuggerTestSpecimen specimen = getLaunchSpecimen();

View file

@ -56,6 +56,10 @@ public abstract class AbstractDebuggerModelTest extends AbstractGhidraHeadlessIn
return List.of(); return List.of();
} }
protected TargetActiveScope findActiveScope() throws Throwable {
return m.find(TargetActiveScope.class, seedPath());
}
protected TargetObject findAttachableContainer() throws Throwable { protected TargetObject findAttachableContainer() throws Throwable {
return m.findContainer(TargetAttachable.class, seedPath()); return m.findContainer(TargetAttachable.class, seedPath());
} }

View file

@ -124,6 +124,11 @@ public abstract class AbstractModelHost implements ModelHost, DebuggerModelTestU
return true; return true;
} }
@Override
public boolean hasInterpreter() {
return true;
}
@Override @Override
public boolean hasInterruptibleProcesses() { public boolean hasInterruptibleProcesses() {
return true; return true;

View file

@ -47,7 +47,7 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
public final DebuggerObjectModel model; public final DebuggerObjectModel model;
public Thread thread = null; public Thread thread = null;
public Set<TargetObject> valid = new HashSet<>(); public Map<TargetObject, TargetObject> valid = new HashMap<>();
// Knobs // Knobs
// TODO: Make these methods instead? // TODO: Make these methods instead?
@ -85,12 +85,8 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
thread = Thread.currentThread(); thread = Thread.currentThread();
} }
if (requireSameThread) { if (requireSameThread) {
if (thread != Thread.currentThread()) { assertEquals("Completion came from an unexpected thread. Probably forgot gateFuture()",
throw new AssertionError("Completion came from an unexpected thread expected:" + thread, Thread.currentThread());
thread + " but got " + Thread.currentThread());
}
assertEquals("Completion came from an unexpected thread", thread,
Thread.currentThread());
} }
} }
@ -113,14 +109,14 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
public void validateObjectValid(String callback, TargetObject obj) { public void validateObjectValid(String callback, TargetObject obj) {
if (requireValid) { if (requireValid) {
assertTrue("Object " + obj.getJoinedPath(".") + " invalid during callback " + callback, assertTrue("Object " + obj.getJoinedPath(".") + " invalid during callback " + callback,
valid.contains(obj)); valid.containsKey(obj));
} }
} }
public void validateObjectInvalid(String callback, TargetObject obj) { public void validateObjectInvalid(String callback, TargetObject obj) {
if (requireValid) { if (requireValid) {
assertFalse("Object " + obj.getJoinedPath(".") + " valid during callback " + callback + assertFalse("Object " + obj.getJoinedPath(".") + " valid during callback " + callback +
", but should have been invalid", valid.contains(obj)); ", but should have been invalid", valid.containsKey(obj));
} }
} }
@ -260,8 +256,16 @@ public class CallbackValidator implements DebuggerModelListener, AutoCloseable {
if (log) { if (log) {
Msg.info(this, "created(object=" + object + ")"); Msg.info(this, "created(object=" + object + ")");
} }
valid.add(object); TargetObject exists = valid.put(object, object);
off.catching(() -> { off.catching(() -> {
if (exists != null) {
if (exists == object) {
fail("created twice (same object): " + object.getJoinedPath("."));
}
else {
fail("replaced before invalidation. old= " + exists + ", new=" + object);
}
}
validateCallbackThread("created"); validateCallbackThread("created");
validateObject("created", object); validateObject("created", object);
}); });

View file

@ -23,15 +23,19 @@ import java.util.Map.Entry;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import ghidra.async.AsyncReference; import ghidra.async.*;
import ghidra.async.AsyncTestUtils; import ghidra.dbg.*;
import ghidra.dbg.DebugModelConventions;
import ghidra.dbg.DebugModelConventions.AsyncAccess; import ghidra.dbg.DebugModelConventions.AsyncAccess;
import ghidra.dbg.error.DebuggerMemoryAccessException;
import ghidra.dbg.target.*; import ghidra.dbg.target.*;
import ghidra.dbg.target.TargetConsole.Channel;
import ghidra.dbg.target.TargetEventScope.TargetEventType;
import ghidra.dbg.target.TargetSteppable.TargetStepKind; import ghidra.dbg.target.TargetSteppable.TargetStepKind;
import ghidra.dbg.test.AbstractDebuggerModelTest; import ghidra.dbg.test.AbstractDebuggerModelTest;
import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen; import ghidra.dbg.test.AbstractDebuggerModelTest.DebuggerTestSpecimen;
import ghidra.dbg.util.PathUtils; import ghidra.dbg.util.PathUtils;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressRange;
import ghidra.util.NumericUtilities; import ghidra.util.NumericUtilities;
public interface DebuggerModelTestUtils extends AsyncTestUtils { public interface DebuggerModelTestUtils extends AsyncTestUtils {
@ -204,4 +208,78 @@ public interface DebuggerModelTestUtils extends AsyncTestUtils {
return process; return process;
}, List.of(AssertionError.class)); }, List.of(AssertionError.class));
} }
default void waitSettled(DebuggerObjectModel model, int ms) throws Throwable {
AsyncDebouncer<Void> debouncer = new AsyncDebouncer<>(AsyncTimer.DEFAULT_TIMER, ms);
var listener = new DebuggerModelListener() {
@Override
public void attributesChanged(TargetObject object, Collection<String> removed,
Map<String, ?> added) {
debouncer.contact(null);
}
@Override
public void breakpointHit(TargetObject container, TargetObject trapped,
TargetStackFrame frame, TargetBreakpointSpec spec,
TargetBreakpointLocation breakpoint) {
debouncer.contact(null);
}
@Override
public void consoleOutput(TargetObject console, Channel channel, byte[] data) {
debouncer.contact(null);
}
@Override
public void created(TargetObject object) {
debouncer.contact(null);
}
@Override
public void elementsChanged(TargetObject object, Collection<String> removed,
Map<String, ? extends TargetObject> added) {
debouncer.contact(null);
}
@Override
public void event(TargetObject object, TargetThread eventThread, TargetEventType type,
String description, List<Object> parameters) {
debouncer.contact(null);
}
@Override
public void invalidateCacheRequested(TargetObject object) {
debouncer.contact(null);
}
@Override
public void invalidated(TargetObject object, TargetObject branch, String reason) {
debouncer.contact(null);
}
@Override
public void memoryReadError(TargetObject memory, AddressRange range,
DebuggerMemoryAccessException e) {
debouncer.contact(null);
}
@Override
public void memoryUpdated(TargetObject memory, Address address, byte[] data) {
debouncer.contact(null);
}
@Override
public void registersUpdated(TargetObject bank, Map<String, byte[]> updates) {
debouncer.contact(null);
}
@Override
public void rootAdded(TargetObject root) {
debouncer.contact(null);
}
};
model.addModelListener(listener);
debouncer.contact(null);
waitOnNoValidate(debouncer.settled());
}
} }

View file

@ -39,6 +39,8 @@ public interface TestDebuggerModelProvider {
boolean hasDetachableProcesses(); boolean hasDetachableProcesses();
boolean hasInterpreter();
boolean hasInterruptibleProcesses(); boolean hasInterruptibleProcesses();
boolean hasKillableProcesses(); boolean hasKillableProcesses();