GP-1982 - Function Graph

This commit is contained in:
dragonmacher 2022-07-01 13:09:55 -04:00 committed by ghidragon
parent f808431251
commit 16e32317f0
18 changed files with 2906 additions and 47 deletions

View file

@ -24,6 +24,7 @@ import org.jdom.Element;
import docking.ComponentPlaceholder;
import docking.DockingWindowManager;
import docking.options.editor.GhidraColorChooser;
import docking.theme.GColor;
import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex;
import ghidra.app.plugin.core.functiongraph.mvc.FunctionGraphVertexAttributes;
import ghidra.framework.options.SaveState;
@ -111,8 +112,7 @@ class IndependentColorProvider implements FGColorProvider {
List<Element> colorElements = xmlElement.getChildren("COLOR");
for (Element element : colorElements) {
String rgbString = element.getAttributeValue("RGB");
int rgb = Integer.parseInt(rgbString);
recentColorCache.addColor(new Color(rgb, true));
recentColorCache.addColor(Color.decode(rgbString));
}
}
}
@ -138,7 +138,9 @@ class IndependentColorProvider implements FGColorProvider {
//==================================================================================================
private class RecentColorCache extends LinkedHashMap<Color, Color> implements Iterable<Color> {
private static final int MAX_SIZE = 10;
private Color mostRecentColor = Color.blue;
// not sure what the default color should be here
private Color mostRecentColor = new GColor("color.bg");
RecentColorCache() {
super(16, 0.75f, true);

View file

@ -37,7 +37,6 @@ import ghidra.graph.viewer.*;
import ghidra.graph.viewer.layout.LayoutListener.ChangeType;
import ghidra.graph.viewer.layout.LayoutProvider;
import ghidra.graph.viewer.layout.VisualGraphLayout;
import ghidra.graph.viewer.options.VisualGraphOptions;
import ghidra.graph.viewer.renderer.VisualGraphEdgeLabelRenderer;
import ghidra.program.model.listing.Function;
import ghidra.program.util.ProgramLocation;
@ -219,14 +218,14 @@ public class FGComponent extends GraphComponent<FGVertex, FGEdge, FunctionGraph>
// note: this label renderer is the stamp for the label; we use another edge label
// renderer inside of the VisualGraphRenderer
VisualGraphEdgeLabelRenderer edgeLabelRenderer =
new VisualGraphEdgeLabelRenderer(Color.BLACK);
edgeLabelRenderer.setNonPickedForegroundColor(Color.LIGHT_GRAY);
new VisualGraphEdgeLabelRenderer(new GColor("color.fg.label.picked"));
edgeLabelRenderer.setNonPickedForegroundColor(new GColor("color.fg.label.non-picked"));
edgeLabelRenderer.setRotateEdgeLabels(false);
renderContext.setEdgeLabelRenderer(edgeLabelRenderer);
viewer.setGraphOptions(vgOptions);
Color bgColor = vgOptions.getGraphBackgroundColor();
if (bgColor.equals(VisualGraphOptions.DEFAULT_GRAPH_BACKGROUND_COLOR)) {
if (vgOptions.isDefaultBackgroundColor(bgColor)) {
// Give user notice when seeing the graph for a non-function (such as an undefined
// function), as this is typical for Ghidra UI widgets.

View file

@ -26,6 +26,7 @@ import edu.uci.ics.jung.visualization.picking.PickedInfo;
import ghidra.app.plugin.core.functiongraph.graph.FGVertexType;
import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex;
import ghidra.program.util.ProgramSelection;
import ghidra.util.ColorUtils;
public class FGVertexPickableBackgroundPaintTransformer implements Function<FGVertex, Paint> {
@ -37,8 +38,7 @@ public class FGVertexPickableBackgroundPaintTransformer implements Function<FGVe
private final Color pickedExitColor;
private static Color mix(Color c1, Color c2) {
return new Color((c1.getRed() + c2.getRed()) / 2, (c1.getGreen() + c2.getGreen()) / 2,
(c1.getBlue() + c2.getBlue()) / 2);
return ColorUtils.blend(c1, c2, .5f);
}
public FGVertexPickableBackgroundPaintTransformer(PickedInfo<FGVertex> info, Color pickedColor,

View file

@ -20,6 +20,7 @@ import java.awt.event.MouseEvent;
import javax.swing.JComponent;
import docking.theme.GThemeDefaults.Colors;
import ghidra.app.plugin.core.functiongraph.graph.FGEdge;
import ghidra.app.plugin.core.functiongraph.graph.FGVertexType;
import ghidra.app.plugin.core.functiongraph.mvc.FGController;
@ -39,7 +40,7 @@ import ghidra.program.util.ProgramSelection;
*/
public interface FGVertex extends VisualVertex {
static final Color TOOLTIP_BACKGROUND_COLOR = new Color(255, 255, 230);
static final Color TOOLTIP_BACKGROUND_COLOR = Colors.TOOLTIP_BACKGROUND;
public FGVertex cloneVertex(FGController newController);

View file

@ -21,11 +21,13 @@ import java.util.Set;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.border.Border;
import docking.ActionContext;
import docking.GenericHeader;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import docking.theme.GColor;
import ghidra.app.plugin.core.functiongraph.FunctionGraphPlugin;
import ghidra.app.plugin.core.functiongraph.graph.FGEdge;
import ghidra.app.plugin.core.functiongraph.mvc.FGController;
@ -137,10 +139,11 @@ public class GroupedFunctionGraphComponentPanel extends AbstractGraphComponentPa
add(genericHeader, BorderLayout.NORTH);
add(contentPanel, BorderLayout.CENTER);
BevelBorder beveledBorder =
(BevelBorder) BorderFactory.createBevelBorder(BevelBorder.RAISED,
new Color(225, 225, 225), new Color(155, 155, 155), new Color(96, 96, 96),
new Color(0, 0, 0));
Border beveledBorder =
BorderFactory.createBevelBorder(BevelBorder.RAISED,
new GColor("color.border.bevel.highlight"),
new GColor("color.border.bevel.shadow"));
setBorder(beveledBorder);
createActions();

View file

@ -25,7 +25,6 @@ import ghidra.app.plugin.core.functiongraph.graph.FGEdge;
import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph;
import ghidra.app.plugin.core.functiongraph.mvc.FGController;
import ghidra.app.plugin.core.functiongraph.mvc.FunctionGraphOptions;
import ghidra.util.Msg;
import ghidra.util.xml.XmlUtilities;
/**
@ -93,26 +92,11 @@ class GroupedVertexInfo extends VertexInfo {
return FunctionGraphOptions.DEFAULT_GROUP_BACKGROUND_COLOR;
}
StringTokenizer tokenizer = new StringTokenizer(colorString, ",");
int tokenCount = tokenizer.countTokens();
if (tokenCount != 4) {
return FunctionGraphOptions.DEFAULT_GROUP_BACKGROUND_COLOR;
}
String redString = tokenizer.nextToken();
String greenString = tokenizer.nextToken();
String blueString = tokenizer.nextToken();
String alphaString = tokenizer.nextToken();
try {
int red = Integer.parseInt(redString);
int green = Integer.parseInt(greenString);
int blue = Integer.parseInt(blueString);
int alpha = Integer.parseInt(alphaString);
return new Color(red, green, blue, alpha);
return Color.decode(colorString);
}
catch (NumberFormatException e) {
Msg.error(this, "Unexpected exception parsing number", e);
// must be an old format
return FunctionGraphOptions.DEFAULT_GROUP_BACKGROUND_COLOR;
}
}
@ -121,8 +105,9 @@ class GroupedVertexInfo extends VertexInfo {
if (color == null) {
return encodeColor(FunctionGraphOptions.DEFAULT_GROUP_BACKGROUND_COLOR);
}
return color.getRed() + "," + color.getGreen() + "," + color.getBlue() + "," +
color.getAlpha();
int rgb = color.getRGB();
return Integer.toString(rgb);
}
@Override

View file

@ -26,11 +26,13 @@ import java.util.List;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.border.Border;
import docking.ActionContext;
import docking.GenericHeader;
import docking.action.DockingAction;
import docking.action.ToolBarData;
import docking.theme.GColor;
import docking.theme.GThemeDefaults.Colors;
import docking.widgets.fieldpanel.FieldPanel;
import docking.widgets.fieldpanel.Layout;
@ -134,10 +136,10 @@ public class ListingGraphComponentPanel extends AbstractGraphComponentPanel {
add(listingPanel, BorderLayout.CENTER);
BevelBorder beveledBorder =
(BevelBorder) BorderFactory.createBevelBorder(BevelBorder.RAISED,
new Color(225, 225, 225), new Color(155, 155, 155), new Color(96, 96, 96),
new Color(0, 0, 0));
Border beveledBorder =
BorderFactory.createBevelBorder(BevelBorder.RAISED,
new GColor("color.border.bevel.highlight"),
new GColor("color.border.bevel.shadow"));
setBorder(beveledBorder);
addKeyListener(new FieldPanelKeyListener());
@ -542,7 +544,7 @@ public class ListingGraphComponentPanel extends AbstractGraphComponentPanel {
previewListingPanel.getFieldPanel()
.setBackgroundColorModel(
new HighlightingColorModel(address, getColorForEdge(edge)));
new HighlightingColorModel(address, getToolTipColorForEdge(edge)));
}
private void initializeToolTipComponent(Address goToAddress) {
@ -556,9 +558,13 @@ public class ListingGraphComponentPanel extends AbstractGraphComponentPanel {
previewListingPanel.getFieldPanel().setCursorOn(false);
}
private Color getColorForEdge(FGEdge edge) {
private Color getToolTipColorForEdge(FGEdge edge) {
FunctionGraphOptions options = controller.getFunctionGraphOptions();
Color c = options.getColor(edge.getFlowType());
return withAlpha(c, 125);
}
private Color withAlpha(Color c, int alpha) {
return new Color(c.getRed(), c.getGreen(), c.getBlue(), 125);
}

View file

@ -25,6 +25,7 @@ import docking.ActionContext;
import docking.action.*;
import docking.menu.MultiActionDockingAction;
import docking.menu.MultipleActionDockingToolbarButton;
import docking.theme.GColor;
import ghidra.app.plugin.core.functiongraph.FGColorProvider;
import ghidra.app.plugin.core.functiongraph.FunctionGraphPlugin;
import ghidra.app.plugin.core.functiongraph.mvc.FGController;
@ -47,7 +48,7 @@ public class SetVertexMostRecentColorAction extends MultiActionDockingAction {
this.controller = controller;
this.vertex = vertex;
setDescription("Set this block's background color");
colorIcon = new ColorIcon3D(new Color(189, 221, 252), 12, 12) {
colorIcon = new ColorIcon3D(new GColor("color.bg.functiongraph.paint.icon"), 12, 12) {
@Override
public Color getColor() {
return controller.getMostRecentColor();

View file

@ -20,6 +20,7 @@ import java.util.*;
import java.util.Map.Entry;
import docking.theme.GColor;
import docking.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.plugin.core.functiongraph.FunctionGraphPlugin;
import ghidra.app.plugin.core.functiongraph.graph.layout.FGLayoutOptions;
import ghidra.framework.options.Options;
@ -264,7 +265,7 @@ public class FunctionGraphOptions extends VisualGraphOptions {
return getConditionalJumpEdgeColor();
}
return Color.BLACK;
return Palette.BLACK;
}
public Color getHighlightColor(FlowType flowType) {
@ -278,7 +279,7 @@ public class FunctionGraphOptions extends VisualGraphOptions {
return getConditionalJumpEdgeHighlightColor();
}
return Color.BLACK;
return Palette.BLACK;
}
public boolean optionChangeRequiresRelayout(String optionName) {

View file

@ -0,0 +1,980 @@
/* ###
* 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.app.plugin.core.functiongraph;
import static ghidra.graph.viewer.GraphViewerUtils.*;
import static org.junit.Assert.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.geom.Point2D;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.*;
import docking.ActionContext;
import docking.ComponentProvider;
import docking.action.DockingAction;
import docking.dnd.GClipboard;
import docking.theme.GThemeDefaults.Colors.Palette;
import edu.uci.ics.jung.algorithms.layout.Layout;
import edu.uci.ics.jung.visualization.VisualizationModel;
import edu.uci.ics.jung.visualization.VisualizationViewer;
import edu.uci.ics.jung.visualization.util.Caching;
import generic.test.TestUtils;
import ghidra.app.cmd.label.AddLabelCmd;
import ghidra.app.events.ProgramSelectionPluginEvent;
import ghidra.app.nav.LocationMemento;
import ghidra.app.nav.Navigatable;
import ghidra.app.plugin.core.colorizer.ColorizingPlugin;
import ghidra.app.plugin.core.colorizer.ColorizingService;
import ghidra.app.plugin.core.functiongraph.graph.*;
import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex;
import ghidra.app.plugin.core.functiongraph.mvc.*;
import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin;
import ghidra.app.plugin.core.navigation.NextPrevAddressPlugin;
import ghidra.app.services.*;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.block.*;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionManager;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.util.task.TaskMonitor;
public class FunctionGraphPlugin1Test extends AbstractFunctionGraphTest {
public FunctionGraphPlugin1Test() {
super();
}
@Override
@Before
public void setUp() throws Exception {
super.setUp();
tool.addPlugin(GoToAddressLabelPlugin.class.getName());
tool.addPlugin(NextPrevAddressPlugin.class.getName());
waitForGraphToLoad();
}
// public void testCodeBlockSpeed() throws CancelledException {
//// ProgramDB cppProgram = env.getProgram("msvidctl.dll_MFCAnalysis");
// ProgramDB cppProgram = env.getProgram("winword.exe");
//
// FunctionManager functionManager = cppProgram.getFunctionManager();
//// Function function = functionManager.getFunctionAt(getAddress("5a1f903e"));
// Function function = functionManager.getFunctionAt(getAddress("3036d629"));
//
// long startTime = System.nanoTime();
// BasicBlockModel blockModel = new BasicBlockModel(cppProgram, false);
// CodeBlockIterator iterator =
// blockModel.getCodeBlocksContaining(function.getBody(), TaskMonitorAdapter.DUMMY_MONITOR);
// while (iterator.hasNext()) {
// iterator.next();
// }
// long endTime = System.nanoTime();
// double totalTime = (endTime - startTime) / 1000000000d;
// System.err.println("total time: " + totalTime);
// }
@Test
public void testLocationChanged() {
// get the graph contents
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramLocation location = getLocationForAddressString(startAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
// locate vertex with cursor
FGVertex focusedVertex = getFocusVertex(functionGraph);
assertNotNull("We did not start with a focused vertex", focusedVertex);
// change the location
String newLocationString = "01004192";
goToAddress(newLocationString);
// locate vertex with cursor
graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramLocation newLocation = getLocationForAddressString(newLocationString);
assertTrue(graphData.containsLocation(newLocation));
functionGraph = graphData.getFunctionGraph();
FGVertex newFocusedVertex = functionGraph.getFocusedVertex();
assertTrue(newFocusedVertex.containsProgramLocation(newLocation));
// make sure the two vertices are not the same
assertTrue("Changing locations in the code browser did not move the cursor location to " +
"a new graph vertex", !focusedVertex.equals(newFocusedVertex));
}
@Test
public void testProgramSelectionAcrossVerticesFromCodeBrowser() {
FGData graphData = getFunctionGraphData();// make sure the graph gets loaded
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramSelection ps = makeMultiVertexSelectionInCodeBrowser();
// this address is in a different vertex than the start address
ProgramLocation location = getLocationForAddressString("0x01004192");
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
FGVertex startVertex = functionGraph.getVertexForAddress(location.getAddress());
// locate vertex with cursor
assertNotNull("We did not start with a focused vertex", startVertex);
assertTrue(startVertex.containsProgramLocation(location));
// make a selection starting at the current location
ProgramSelection firstSelection = startVertex.getProgramSelection();
assertTrue("A selection too big for one vertex has fit into a start vertex",
!ps.equals(firstSelection));
Address address = getAddress("0x01004196");
FGVertex secondVertex = functionGraph.getVertexForAddress(address);
ProgramSelection secondSelection = secondVertex.getProgramSelection();
assertTrue(!secondSelection.isEmpty());
assertTrue(ps.getMinAddress().equals(firstSelection.getMinAddress()));
assertTrue(ps.getMaxAddress().equals(secondSelection.getMaxAddress()));
}
@Test
public void testGraphWithCloseAndReopenProgram_ForSCR_7813() {
//
// This test is meant to ensure that graph contents are properly disposed and that no
// exceptions happen while switching programs.
//
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramManager pm = tool.getService(ProgramManager.class);
pm.closeProgram(program, true);
// should have empty data after closing the program
graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Graph data should be empty after closing the program", !graphData.hasResults());
pm.openProgram(program.getDomainFile());
program.flushEvents();
// we should have some sort of non-null data--either real or empty
graphData = getFunctionGraphData();
assertNotNull(graphData);
goToAddress(startAddressString);
// verify we can still graph data
graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
}
// note: unreliable--garbage collection works differently across platforms (sad face)
// public void testClearCacheAndMemoryLeak() {
// //
// // Test that when we clear the cache of a graph the vertices of that graph will be
// // garbage collected
// //
// WeakSet<FunctionGraphVertex> weakSet =
// WeakDataStructureFactory.createSingleThreadAccessWeakSet();
//
// FunctionGraphData graphData = getFunctionGraphData();
// assertNotNull(graphData);
// assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
//
// FunctionGraph graph = graphData.getFunctionGraph();
// FunctionGraphVertex rootVertex = graph.getRootVertex();
// assertNotNull(rootVertex);
//
// weakSet.add(rootVertex);
// assertTrue(weakSet.iterator().hasNext());
//
// // move to a new function so that we the FG will not be holding a reference to the
// // originally graphed function
// String address = "01002239";
// goToAddress(address);
//
// FunctionGraphData newGraphData = getFunctionGraphData();
// assertNotNull(newGraphData);
// assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults());
//
// assertTrue(
// "Function Graph did not graph a new function as expected at address: " + address,
// !graphData.equals(newGraphData));
//
// triggerGraphDisposal(graphData);
//
// rootVertex = null;
// graph = null;
// graphData = null;
//
// // let (force) the Garbage Collector run
// System.gc();
// sleep(100);
// System.gc();
// sleep(100);
// System.gc();
// sleep(100);
//
// boolean isNotCollected = weakSet.iterator().hasNext();
// assertFalse(isNotCollected);
// }
//
// private void triggerGraphDisposal(FunctionGraphData dataToDispose) {
// Object controller = getInstanceField("controller", graphProvider);
// LRUMap<?, ?> cache = (LRUMap<?, ?>) getInstanceField("graphCache", controller);
// cache.clear();
// dataToDispose.dispose();
// }
@Test
public void testSaveVertexPositions() {
//
// Test that we can move a node, load a new graph, reload the original graph and have
// the moved node return to the moved position
//
FGData originalGraphData = getFunctionGraphData();
assertNotNull(originalGraphData);
assertTrue("Unexpectedly received an empty FunctionGraphData",
originalGraphData.hasResults());
FunctionGraph graph = originalGraphData.getFunctionGraph();
final FGVertex rootVertex = graph.getRootVertex();
assertNotNull(rootVertex);
Point2D originalPoint = rootVertex.getLocation();
double dx = originalPoint.getX() + 100;
double dy = originalPoint.getY() + 100;
Point2D newPoint = new Point2D.Double(dx, dy);
final Layout<FGVertex, FGEdge> primaryLayout = getPrimaryLayout();
final Point2D finalNewPoint = newPoint;
runSwing(() -> primaryLayout.setLocation(rootVertex, finalNewPoint));
// we have to wait for the paint to take place, as the rendering will change the vertex
// locations
FGPrimaryViewer primaryGraphViewer = getPrimaryGraphViewer();
primaryGraphViewer.repaint();
waitForSwing();
// now that we have changed the data, load a new graph and then come back to the start
// graph so that we can see that the settings have been re-applied
String address = "01002239";
goToAddress(address);
FGData newGraphData = getFunctionGraphData();
assertNotNull(newGraphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults());
assertTrue(!newGraphData.equals(originalGraphData));
// ...now go back and check the position
goToAddress(startAddressString);
newGraphData = getFunctionGraphData();
assertNotNull(newGraphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults());
graph = newGraphData.getFunctionGraph();
FGVertex newRootVertex = graph.getRootVertex();
assertNotNull(newRootVertex);
waitForSwing();
// Note: we can't test for exact equality based upon the location we set, as the values
// are updated based upon other factors, like screen location and size. We want to make
// sure that the value is not the default.
Point2D reloadedPoint = newRootVertex.getLocation();
assertTrue("Vertex location not restored after regraphing a function",
!originalPoint.equals(reloadedPoint));
}
@Test
public void testRelayout() throws Exception {
doTestRelayout(false);
}
@Test
public void testReload() throws Exception {
doTestRelayout(true);
}
@Test
public void testLabelChangeAtVertexEntryUpdatesTitle() {
int txID = -1;
try {
txID = program.startTransaction("Test: " + testName.getMethodName());
doTestLabelChangeAtVertexEntryUpdatesTitle();
}
finally {
program.endTransaction(txID, false);
}
}
@Test
public void testChangeFormat() throws Exception {
//
// Test that we can change the view's format. As part of this test verify:
// -That each graph gets the view changes
// -Test reset format
// -That view changes are persisted
//
FGController primaryController = getFunctionGraphController();
waitForBusyRunManager(primaryController);
FGData functionGraphData = getFunctionGraphData();
FunctionGraph functionGraph = functionGraphData.getFunctionGraph();
// Be sure to pick a vertex that will get bigger when a new field is added. The
// root vertex is already so wide (due to the function signature), that adding a small
// field does not change its width.
FGVertex vertex = functionGraph.getVertexForAddress(getAddress("1004178"));
Rectangle originalBounds = vertex.getBounds();
// Also, be sure that we are not on the function signature field, as that does not have
// the 'Bytes' field.
goTo("1004179");
addBytesFormatFieldFactory();
//
// Verify the vertex size has change (due to the format getting larger)
//
FGPrimaryViewer viewer = getPrimaryGraphViewer();
viewer.repaint();
waitForSwing();
Rectangle updatedBounds = vertex.getBounds();
assertTrue("bounds not updated - was: " + originalBounds + "; is now: " + updatedBounds,
originalBounds.width < updatedBounds.width);
performResetFormatAction();
viewer.repaint();
waitForSwing();
Rectangle newNewBounds = vertex.getBounds();
assertTrue(updatedBounds.width > newNewBounds.width);
}
@Test
public void testCopyKeyBinding() throws Exception {
//
// Make a program selection and test that executing the copy keybinding will copy the
// selection (across vertices).
//
//
// Initialize the clipboard with known data
//
Clipboard systemClipboard = GClipboard.getSystemClipboard();
systemClipboard.setContents(DUMMY_TRANSFERABLE, null);
waitForSwing();
//
// Verify our initial state
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramLocation location = getLocationForAddressString(startAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
// locate vertex with cursor
FGVertex focusedVertex = getFocusVertex(functionGraph);
assertNotNull("We did not start with a focused vertex", focusedVertex);
//
// Create a selection that we will copy from the executing the action
//
AddressSetView addresses = focusedVertex.getAddresses();
Address address = addresses.getMinAddress();
ProgramSelection selection =
new ProgramSelection(program.getAddressFactory(), address, address.add(8));
tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program));
//
// Validate and execute the action
//
DockingAction copyAction = getCopyAction();
FGController controller = getFunctionGraphController();
ComponentProvider provider = controller.getProvider();
assertTrue(copyAction.isEnabledForContext(provider.getActionContext(null)));
performAction(copyAction, provider, false);
waitForTasks();
Transferable contents = systemClipboard.getContents(systemClipboard);
assertNotNull(contents);
assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
}
@Test
public void testCopyAction() {
//
// Put the cursor in a vertex on a field with text and make sure that the copy action
// is enabled.
//
//
// Initialize the clipboard with known data
//
Clipboard systemClipboard = GClipboard.getSystemClipboard();
systemClipboard.setContents(DUMMY_TRANSFERABLE, null);
waitForSwing();
//
// Verify our initial state
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramLocation location = getLocationForAddressString(startAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
// locate vertex with cursor
FGVertex focusedVertex = getFocusVertex(functionGraph);
assertNotNull("We did not start with a focused vertex", focusedVertex);
//
// Put the cursor on a copyable thing
//
codeBrowser.goToField(getAddress("0x01004196"), "Mnemonic", 0, 0, 2, true);
waitForSwing();
// sanity check
DockingAction copyAction = getCopyAction();
assertClipboardServiceAddress(copyAction, "0x01004196");
//
// Validate and execute the action
//
FGController controller = getFunctionGraphController();
ComponentProvider provider = controller.getProvider();
ActionContext actionContext = provider.getActionContext(null);
boolean isEnabled = copyAction.isEnabledForContext(actionContext);
debugAction(copyAction, actionContext);
assertTrue(isEnabled);
performAction(copyAction, actionContext, true);
Transferable contents = systemClipboard.getContents(systemClipboard);
assertNotNull(contents);
assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE));
}
@Test
public void testSatelliteViewIsResizedToFit() {
//
// Test that the satellite view zoomed to fit completely in the window if the window
// is resized or the graph is made bigger via dragging a vertex.
//
showSatellite();// make sure it is on
FGController controller = getFunctionGraphController();
waitForBusyRunManager(controller);
FGView view = controller.getView();
VisualizationViewer<FGVertex, FGEdge> satelliteViewer = view.getSatelliteViewer();
Double originalGraphScale = getGraphScale(satelliteViewer);
//
// window size change test
//
final Window window = windowForComponent(graphProvider.getComponent());
final Dimension originalSize = window.getSize();
final Dimension newSize = new Dimension(originalSize.width >> 1, originalSize.height >> 1);
runSwing(() -> window.setSize(newSize));
waitForSwing();
waitForBusyGraph();
Double newGraphScale = getGraphScale(satelliteViewer);
Assert.assertNotEquals("The graph's scale did not change after resizing the window",
originalGraphScale, newGraphScale);
runSwing(() -> window.setSize(originalSize));
waitForSwing();
waitForBusyGraph();
newGraphScale = getGraphScale(satelliteViewer);
assertEquals(originalGraphScale, newGraphScale);
//
// graph size change test
//
FGData functionGraphData = getFunctionGraphData();
FunctionGraph functionGraph = functionGraphData.getFunctionGraph();
VisualizationViewer<FGVertex, FGEdge> primaryGraphViewer = view.getPrimaryGraphViewer();
VisualizationModel<FGVertex, FGEdge> model = primaryGraphViewer.getModel();
final Layout<FGVertex, FGEdge> graphLayout = model.getGraphLayout();
final FGVertex vertex = functionGraph.getRootVertex();
final Point2D startPoint = graphLayout.apply(vertex);
final Point2D newPoint = new Point2D.Double(startPoint.getX() + 2000, startPoint.getY());
runSwing(() -> {
Caching cachingLayout = (Caching) graphLayout;
cachingLayout.clear();
graphLayout.setLocation(vertex, newPoint);
});
waitForSwing();
Double scaleAfterDragging = getGraphScale(satelliteViewer);
Assert.assertNotEquals(newGraphScale, scaleAfterDragging);
// put the vertex back and make sure the scale is reverted
runSwing(() -> {
Caching cachingLayout = (Caching) graphLayout;
cachingLayout.clear();
graphLayout.setLocation(vertex, startPoint);
});
waitForSwing();
scaleAfterDragging = getGraphScale(satelliteViewer);
assertEquals(newGraphScale, scaleAfterDragging);
}
public void TODO_testPersistence() {
//
// This wants to test that graph perspective info is saved between Ghidra sessions. In
// other words, is my graph location and zoom level the same between Ghidra runs. We
// also want to test that colors and vertex locations are persisted between sessions.
//
//
// Note: This is probably too hard to worry about. To test this, we have to setup a
// Ghidra environment with varied values. Then, close down Ghidra, relaunch Ghidra,
// and test the changed values for sameness
}
public void TODO_testEdgeHover() {
// hover should show information about the connected nodes in a tooltip?
// should hover highlight the same edge in the satellite?
//
// Note: these are GUI intensive tests--low reward/benefit ratios
//
}
@Test
public void testGraphNodesCreated() throws Exception {
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
FunctionGraph functionGraph = graphData.getFunctionGraph();
Collection<FGVertex> vertices = functionGraph.getVertices();
BlockModelService blockService = tool.getService(BlockModelService.class);
CodeBlockModel blockModel = blockService.getActiveBlockModel(program);
FunctionManager functionManager = program.getFunctionManager();
Function function = functionManager.getFunctionContaining(getAddress(startAddressString));
CodeBlockIterator iterator =
blockModel.getCodeBlocksContaining(function.getBody(), TaskMonitor.DUMMY);
// we should have one vertex for each code block
Set<Address> vertexAddresses = new HashSet<>();
for (FGVertex vertex : vertices) {
AddressSetView addresses = vertex.getAddresses();
vertexAddresses.add(addresses.getMinAddress());
}
for (; iterator.hasNext();) {
CodeBlock codeBlock = iterator.next();
assertTrue(vertexAddresses.contains(codeBlock.getMinAddress()));
}
}
@Test
public void testClearColorAction() throws Exception {
tool.addPlugin(ColorizingPlugin.class.getName());
FGVertex focusedVertex = getFocusedVertex();
ColorizingService colorizingService = tool.getService(ColorizingService.class);
Color appliedBackgroundColor =
colorizingService.getBackgroundColor(focusedVertex.getVertexAddress());
Color testColor = Palette.RED;
assertTrue("Unexpected start color--must change the test!",
!testColor.equals(appliedBackgroundColor));
chooseColor(focusedVertex, testColor);
Color newVertexBackgroundColor = focusedVertex.getUserDefinedColor();
assertEquals("Background color not set", testColor, newVertexBackgroundColor);
DockingAction clearColorAction = getClearColorAction(focusedVertex);
performAction(clearColorAction, graphProvider, true);
Color userDefinedColor = focusedVertex.getUserDefinedColor();
assertNull(userDefinedColor);
Color serviceBackgroundColor =
colorizingService.getBackgroundColor(focusedVertex.getVertexAddress());
assertNull("Clear action did not clear the service's applied color",
serviceBackgroundColor);
}
// test that navigating a vertex updates the code browser's location
@Test
public void testNavigationFromVertexToCodeBrowser() {
//
// This test covers navigation, which relies on the provider being focused to work
//
setProviderAlwaysFocused();
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramLocation location = getLocationForAddressString(startAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
// locate vertex with cursor
FGVertex focusedVertex = getFocusVertex(functionGraph);
assertNotNull("We did not start with a focused vertex", focusedVertex);
Collection<FGVertex> vertices = functionGraph.getVertices();
FGVertex otherVertex = null;
for (FGVertex vertex : vertices) {
if (vertex != focusedVertex) {
otherVertex = vertex;
break;
}
}
assertNotNull(otherVertex);
Address address = otherVertex.getAddresses().getMinAddress();
final ProgramLocation newVertexLocation = new ProgramLocation(program, address);
final FGController controller =
(FGController) TestUtils.getInstanceField("controller", graphProvider);
runSwing(() -> controller.display(program, newVertexLocation));
// we must 'fake out' the listing to generate a location event from within the listing
pressRightArrowKey(otherVertex);
waitForSwing();
ProgramLocation codeBrowserLocation = runSwing(() -> codeBrowser.getCurrentLocation());
ProgramLocation actualVertexLocation = otherVertex.getProgramLocation();
assertEquals(newVertexLocation.getAddress(), actualVertexLocation.getAddress());
assertEquals(actualVertexLocation.getAddress(), codeBrowserLocation.getAddress());
}
@Test
public void testFullyZoomedOutOption() throws Exception {
//
// Test the default option that fits the entire graph into the window. Then toggle the
// option and test that a new graph starts fully zoomed-in.
//
hideSatellite();// for readability
setGraphWindowSize(300, 300);// make window small for easier testing
setZoomOutOption(true);
assertZoomedOut();
setZoomOutOption(false);
assertZoomedIn();
}
@Test
public void testNavigationHistory_VertexChangesOption() throws Exception {
setNavigationHistoryOption(NavigationHistoryChoices.VERTEX_CHANGES);
FGData graphData = getFunctionGraphData();
FunctionGraph graph = graphData.getFunctionGraph();
Collection<FGVertex> vertices = graph.getVertices();
FGVertex start = getFocusedVertex();
Iterator<FGVertex> it = vertices.iterator();
FGVertex v1 = it.next();
pickVertex(v1);
FGVertex v2 = it.next();
pickVertex(v2);
FGVertex v3 = it.next();
pickVertex(v3);
assertInHistory(start, v1, v2);
}
@Test
public void testNavigationHistory_NavigationEventsOption() throws Exception {
setNavigationHistoryOption(NavigationHistoryChoices.NAVIGATION_EVENTS);
clearHistory();
FGVertex v1 = vertex("01004178");
pickVertex(v1);
FGVertex v2 = vertex("01004192");
pickVertex(v2);
FGVertex v3 = vertex("010041a4");
pickVertex(v3);
// in this navigation mode, merely selecting nodes does *not* put previous nodes in history
assertNotInHistory(v1, v2);
//
// Perform a navigation action (e.g., goTo()) and verify the old function is in the history
//
Address ghidra = getAddress("0x01002cf5");
goTo(ghidra);
assertInHistory(v3.getVertexAddress());
Address foo = getAddress("0x01002339");
goTo(foo);
assertInHistory(v3.getVertexAddress(), ghidra);
}
//==================================================================================================
// Private Methods
//==================================================================================================
private void assertNotInHistory(FGVertex... vertices) {
List<Address> vertexAddresses =
Arrays.stream(vertices)
.map(v -> v.getVertexAddress())
.collect(Collectors.toList());
assertNotInHistory(vertexAddresses);
}
private void assertNotInHistory(List<Address> addresses) {
List<LocationMemento> locations = getNavigationHistory();
List<Address> actualAddresses =
locations.stream()
.map(memento -> memento.getProgramLocation().getAddress())
.collect(Collectors.toList());
for (Address a : addresses) {
assertFalse("Vertex address should not be in the history list: " + a + ".\nHistory: " +
actualAddresses + "\nNavigated vertices: " + Arrays.asList(addresses),
actualAddresses.contains(a));
}
}
private void clearHistory() {
GoToService goTo = tool.getService(GoToService.class);
Navigatable navigatable = goTo.getDefaultNavigatable();
NavigationHistoryService service = tool.getService(NavigationHistoryService.class);
service.clear(navigatable);
}
private List<LocationMemento> getNavigationHistory() {
GoToService goTo = tool.getService(GoToService.class);
Navigatable navigatable = goTo.getDefaultNavigatable();
NavigationHistoryService service = tool.getService(NavigationHistoryService.class);
List<LocationMemento> locations = service.getPreviousLocations(navigatable);
return locations;
}
private void assertInHistory(FGVertex... vertices) {
List<Address> vertexAddresses =
Arrays.stream(vertices)
.map(v -> v.getVertexAddress())
.collect(Collectors.toList());
assertInHistory(vertexAddresses);
}
private void assertInHistory(Address... addresses) {
assertInHistory(Arrays.asList(addresses));
}
private void assertInHistory(List<Address> expectedAddresses) {
List<LocationMemento> actualLocations = getNavigationHistory();
assertTrue(
"Vertex address should be in the history list: " + expectedAddresses + ".\nHistory: " +
actualLocations + "\nNavigated vertices: " + expectedAddresses,
expectedAddresses.size() <= actualLocations.size());
List<Address> actualAddresses =
actualLocations.stream()
.map(memento -> memento.getProgramLocation().getAddress())
.collect(Collectors.toList());
for (Address a : expectedAddresses) {
assertTrue("Vertex address should be in the history list: " + a + ".\nActual: " +
actualAddresses + "\nExpected: " + expectedAddresses,
actualAddresses.contains(a));
}
}
private void setNavigationHistoryOption(NavigationHistoryChoices choice) throws Exception {
FGController controller = getFunctionGraphController();
FunctionGraphOptions options = controller.getFunctionGraphOptions();
runSwing(() -> setInstanceField("navigationHistoryChoice", options, choice));
waitForSwing();
}
private void doTestLabelChangeAtVertexEntryUpdatesTitle() {
// get the graph contents
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
// locate vertex with cursor
Address vertexAddressWithDefaultLabel = getAddress("01004178");
FunctionGraph graph = graphData.getFunctionGraph();
FGVertex vertex = graph.getVertexForAddress(vertexAddressWithDefaultLabel);
String originalTitle = vertex.getTitle();
// add a label in the listing
String labelName = testName.getMethodName();
AddLabelCmd addCmd =
new AddLabelCmd(vertexAddressWithDefaultLabel, labelName, SourceType.USER_DEFINED);
addCmd.applyTo(program);
program.flushEvents();
waitForSwing();
// make sure the label appears in the vertex
String updatedTitle = vertex.getTitle();
Assert.assertNotEquals(originalTitle, updatedTitle);
assertTrue(updatedTitle.indexOf(testName.getMethodName()) != -1);
}
private void doTestRelayout(boolean fullReload) throws Exception {
//
// This test covers navigation, which relies on the provider being focused to work
//
setProviderAlwaysFocused();
//
// Test that we can move a node, call relayout and that the moved node will not be
// at the moved position.
//
FGData originalGraphData = getFunctionGraphData();
assertNotNull(originalGraphData);
assertTrue("Unexpectedly received an empty FunctionGraphData",
originalGraphData.hasResults());
//
// Unusual Code: The initial load values may not be exactly the same as the values
// set during a relayout. To make sure that we are comparing apples-to-apples,
// we want to record the initial values after performing a relayout. Then,
// we change a node's position, relayout again, and check the final values
// with that after the first relayout.
//
if (fullReload) {
performReload();
}
else {
performRelayout();
}
originalGraphData = getFunctionGraphData();
FunctionGraph graph = originalGraphData.getFunctionGraph();
final FGVertex rootVertex = graph.getRootVertex();
assertNotNull(rootVertex);
Point2D originalPoint = rootVertex.getLocation();
double dx = originalPoint.getX() + 100;
double dy = originalPoint.getY() + 100;
Point2D newPoint = new Point2D.Double(dx, dy);
final Layout<FGVertex, FGEdge> primaryLayout = getPrimaryLayout();
final Point2D finalNewPoint = newPoint;
runSwing(() -> primaryLayout.setLocation(rootVertex, finalNewPoint));
assertEquals("Vertex location not correctly set", newPoint, rootVertex.getLocation());
// we have to wait for the paint to take place, as the rendering will change the vertex
// locations
FGPrimaryViewer primaryGraphViewer = getPrimaryGraphViewer();
primaryGraphViewer.repaint();
waitForSwing();
// move the location a bit, for later testing the sync between the listing and the graph
goToAddress("1004196");
// relayout
if (fullReload) {
performReload();
}
else {
performRelayout();
}
FGData newGraphData = getFunctionGraphData();
assertNotNull(newGraphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults());
FunctionGraph newGraph = newGraphData.getFunctionGraph();
FGVertex newRootVertex = newGraph.getRootVertex();
assertNotNull(newRootVertex);
waitForSwing();
// Note: we can't test for exact equality based upon the location we set, as the values
// are updated based upon other factors, like screen location and size. We want to make
// sure that the value is not the default.
Point2D reloadedPoint = newRootVertex.getLocation();
assertTrue(
"Vertex location not restored to default after performing a relayout " +
"original point: " + originalPoint + " - reloaded point: " + reloadedPoint,
pointsAreSimilar(originalPoint, reloadedPoint));
//
// Make sure the CodeBrowser's location matches ours after the relayout (the location should
// get broadcast to the CodeBrowser)
//
// Note: there is a timing failure that happens for this check; the event broadcast
// only happens if the FG provider has focus; in parallel batch mode focus is
// unreliable
if (!BATCH_MODE) {
assertTrue(graphAddressMatchesCodeBrowser(newGraph));
}
}
private boolean graphAddressMatchesCodeBrowser(FunctionGraph graph) {
FGVertex focusedVertex = runSwing(() -> graph.getFocusedVertex());
ProgramLocation graphLocation = focusedVertex.getProgramLocation();
ProgramLocation codeBrowserLocation = runSwing(() -> codeBrowser.getCurrentLocation());
return graphLocation.getAddress().equals(codeBrowserLocation.getAddress());
}
}

View file

@ -0,0 +1,841 @@
/* ###
* 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.app.plugin.core.functiongraph;
import static ghidra.graph.viewer.GraphViewerUtils.*;
import static org.junit.Assert.*;
import java.awt.Color;
import java.awt.Point;
import java.util.*;
import javax.swing.JComponent;
import org.junit.*;
import docking.action.DockingActionIf;
import docking.theme.GThemeDefaults.Colors.Palette;
import edu.uci.ics.jung.graph.Graph;
import generic.test.TestUtils;
import ghidra.app.cmd.label.AddLabelCmd;
import ghidra.app.cmd.label.DeleteLabelCmd;
import ghidra.app.cmd.refs.AddMemRefCmd;
import ghidra.app.cmd.refs.RemoveReferenceCmd;
import ghidra.app.plugin.core.colorizer.ColorizingPlugin;
import ghidra.app.plugin.core.colorizer.ColorizingService;
import ghidra.app.plugin.core.functiongraph.graph.*;
import ghidra.app.plugin.core.functiongraph.graph.vertex.*;
import ghidra.app.plugin.core.functiongraph.mvc.*;
import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin;
import ghidra.app.plugin.core.navigation.NextPrevAddressPlugin;
import ghidra.app.util.viewer.listingpanel.ListingPanel;
import ghidra.graph.viewer.GraphPerspectiveInfo;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.symbol.*;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.util.Msg;
public class FunctionGraphPlugin2Test extends AbstractFunctionGraphTest {
@Override
@Before
public void setUp() throws Exception {
super.setUp();
tool.addPlugin(GoToAddressLabelPlugin.class.getName());
tool.addPlugin(NextPrevAddressPlugin.class.getName());
waitForGraphToLoad();
}
@Test
public void testSelectionFromCodeBrowser() {
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramLocation location = getLocationForAddressString(startAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
// locate vertex with cursor
FGVertex focusedVertex = getFocusVertex(functionGraph);
assertNotNull("We did not start with a focused vertex", focusedVertex);
assertTrue(focusedVertex.containsProgramLocation(location));
// make a selection starting at the current location
ProgramSelection ps = makeSingleVertexSelectionInCodeBrowser();
ProgramSelection vertexSelection = focusedVertex.getProgramSelection();
assertTrue(!ps.isEmpty());
assertEquals("A selection from the code browser that is completely contained in the " +
"tested vertex is not in the vertex", ps, vertexSelection);
}
@Test
public void testRedockSatellite() {
showSatellite();// make sure it is on
undockSatellite();
redockSatellite();
assertNoUndockedProvider();
assertSatelliteVisible(true);
}
@Test
public void testUndockSatellite() {
showSatellite();// make sure it is on
assertNoUndockedProvider();
undockSatellite();
assertSatelliteVisible(true);
assertUndockedProviderShowing();
}
@Test
public void testShowSatelliteButtonWhenDocked() {
showSatellite();// make sure it is on
toggleSatalliteVisible(false);
assertSatelliteVisible(false);
pressShowSatelliteButton();
assertSatelliteVisible(true);
assertNoUndockedProvider();
}
@Test
public void testShowSatelliteButtonWhenUnDocked() {
showSatellite();// make sure it is on
undockSatellite();
assertUndockedProviderShowing();
closeUndockedProvider();
assertUndockedProviderNotShowing();
pressShowSatelliteButton();
assertUndockedProviderShowing();
assertSatelliteVisible(true);
}
@Test
public void testUndockWhileInvisible() {
toggleSatalliteVisible(false);
assertSatelliteVisible(false);
undockSatellite();
assertUndockedProviderShowing();
redockSatellite();
// note: redocking the satellite will make it visible again
assertSatelliteVisible(true);
}
@Test
public void testSnapshotWithUndockedSatellite() {
undockSatellite();
FGController newController = cloneGraph();
assertUndockedProviderShowing(newController.getProvider());
isSatelliteVisible(newController);
}
@SuppressWarnings("unchecked")
// list cast
@Test
public void testSnapshot() {
List<FGProvider> disconnectedProviders =
(List<FGProvider>) getInstanceField("disconnectedProviders", graphPlugin);
assertTrue(disconnectedProviders.isEmpty());
FGController primaryController = getFunctionGraphController();
waitForBusyRunManager(primaryController);
ProgramLocation location = graphProvider.getLocation();
GraphPerspectiveInfo<FGVertex, FGEdge> primaryPerspective =
primaryController.getGraphPerspective(location);
DockingActionIf snapshotAction =
getAction(tool, graphPlugin.getName(), "Function Graph Clone");
performAction(snapshotAction, true);
assertEquals(1, disconnectedProviders.size());
FGProvider providerClone = disconnectedProviders.get(0);
FGController controllerClone = (FGController) getInstanceField("controller", providerClone);
waitForBusyRunManager(controllerClone);
ProgramLocation cloneLocation = providerClone.getLocation();
GraphPerspectiveInfo<FGVertex, FGEdge> clonePerspective =
controllerClone.getGraphPerspective(cloneLocation);
double primaryPerspectiveZoom = primaryPerspective.getZoom();
double clonePerspectiveZoom = clonePerspective.getZoom();
assertEquals(primaryPerspectiveZoom, clonePerspectiveZoom, .001);
Point primaryPoint = primaryPerspective.getLayoutTranslateCoordinates();
Point clonePoint = clonePerspective.getLayoutTranslateCoordinates();
assertPointsAreAboutEqual("Cloned graph view info does not match the source graph",
primaryPoint, clonePoint);
}
@Test
public void testZoom() {
setZoom(0.5d);
waitForAnimation();
FGPrimaryViewer primaryGraphViewer = getPrimaryGraphViewer();
Double originalGraphScale = getGraphScale(primaryGraphViewer);
Msg.debug(this, "original scale: " + originalGraphScale);
// zoom at code level
Msg.debug(this, "zooming in...");
zoomInCompletely();
Double zoomedInGraphScale = getGraphScale(primaryGraphViewer);
Msg.debug(this, "new scale: " + zoomedInGraphScale);
Assert.assertNotEquals(originalGraphScale, zoomedInGraphScale);
}
@Test
public void testSplitAndMergeNodesOnStaleGraph_ForReference() {
int txID = -1;
try {
txID = program.startTransaction("Test: " + testName.getMethodName());
doTestSplitAndMergeNodesOnStaleGraph_ForReference();
}
finally {
program.endTransaction(txID, false);
}
}
@Test
public void testSplitAndMergeNodesOnStaleGraph_ForSymbol() {
int txID = -1;
try {
txID = program.startTransaction("Test: " + testName.getMethodName());
doTestSplitAndMergeNodesOnStaleGraph_ForSymbol();
}
finally {
program.endTransaction(txID, false);
}
}
@Test
public void testSetVertexColor() {
FGVertex focusedVertex = getFocusedVertex();
JComponent panel = focusedVertex.getComponent();
ListingPanel listingPanel =
(ListingPanel) TestUtils.getInstanceField("listingPanel", panel);
Color startBackgrond = listingPanel.getTextBackgroundColor();
Color testColor = Palette.RED;
assertTrue("Unexpected start color--must change the test!",
!testColor.equals(startBackgrond));
chooseColor(focusedVertex, testColor);
Color newBackground = listingPanel.getTextBackgroundColor();
assertTrue(!startBackgrond.equals(newBackground));
}
@Test
public void testSharedColorExperience() throws Exception {
//
// Tests the new way of coloring vertices, by way of the ColorizerService, which will
// set the color in both the vertex and the listing (really just in the listing, but
// the vertex displays this color.
//
// install ColorizerPlugin
tool.addPlugin(ColorizingPlugin.class.getName());
FGVertex vertex = getFocusedVertex();
ColorizingService colorizingService = tool.getService(ColorizingService.class);
Color appliedBackgroundColor =
colorizingService.getBackgroundColor(vertex.getVertexAddress());
Color testColor = Palette.RED;
assertTrue("Unexpected start color--must change the test!",
!testColor.equals(appliedBackgroundColor));
chooseColor(vertex, testColor);
// make sure the service is also cognizant of the color change
appliedBackgroundColor = colorizingService.getBackgroundColor(vertex.getVertexAddress());
assertEquals(testColor, appliedBackgroundColor);
Color vBg = vertex.getBackgroundColor();
assertEquals(appliedBackgroundColor, vBg);
//
// Reload and make sure the color is re-applied to the vertex (this was broken)
//
Address vertexAddress = vertex.getVertexAddress();
performReload();
FGVertex reloadedVertex = vertex(vertexAddress);
assertNotSame(vertex, reloadedVertex);
vBg = reloadedVertex.getBackgroundColor();
assertEquals(appliedBackgroundColor, vBg);
}
@Test
public void testSetMostRecentColorAction() throws Exception {
//
// Test that the 'set most recent color' action will set the color of the vertex *and*
// in the Listing.
//
// install ColorizerPlugin
tool.addPlugin(ColorizingPlugin.class.getName());
FGVertex focusedVertex = getFocusedVertex();
ColorizingService colorizingService = tool.getService(ColorizingService.class);
Color startBackgroundColor =
colorizingService.getBackgroundColor(focusedVertex.getVertexAddress());
FGController controller = getFunctionGraphController();
Color mostRecentColor = controller.getMostRecentColor();
Assert.assertNotEquals(
"Test environment not setup correctly--should have default backgrond " +
"colors applied",
startBackgroundColor, mostRecentColor);
SetVertexMostRecentColorAction setRecentColorAction =
getSetMostRecentColorAction(focusedVertex);
performAction(setRecentColorAction, graphProvider, true);
Color newVertexBackgroundColor = focusedVertex.getBackgroundColor();
assertEquals("'Set Most Recent Color' action did not apply that color to the vertex",
mostRecentColor, newVertexBackgroundColor);
Color newBackgroundColor =
colorizingService.getBackgroundColor(focusedVertex.getVertexAddress());
assertEquals("'Set Most Recent Color' action did not apply that color to the color service",
mostRecentColor, newBackgroundColor);
}
// TODO: see SCR 9208 - we don't currently support this, although we could
public void dont_testNavigatingBackwardsRestoresPerspectiveInfo_ZoomOutOn() throws Exception {
//
// Test, that with the default 'zoomed-out' option *on* for new graphs, we will restore the
// user's previous graph perspective data when they navigate back to a function that
// were examining (instead of zooming back out to the full view).
//
hideSatellite();// for readability
setGraphWindowSize(300, 300);// make window small for easier testing
setZoomOutOption(true);
// zoom in to a vertex (pick something beside the entry point)
goTo("1004196");
zoomInCompletely();
// move that vertex off center
moveView(10);
FGVertex v = getFocusedVertex();
Point originalLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v);
// 'go to' a new function to change the graph
goTo("01002239");
// trigger a back navigation
navigateBack();
// make sure that the zoom is restored (note: the point information reflects both
// location and zoom)
Point restoredLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v);
assertEquals(originalLocation, restoredLocation);
}
// not sure how to trigger a selection from a node via the GUI...if we use the API method,
// as it does now, the CodeBrowser does not get the callback, because that's how the system works.
// If it is worth testing, and we can do it from the mouse, then have at it
public void dont_testSelectionFromGraphToCodeBrowser() {
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
ProgramLocation location = getLocationForAddressString(startAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
// locate vertex with cursor
FGVertex focusedVertex = getFocusVertex(functionGraph);
assertNotNull("We did not start with a focused vertex", focusedVertex);
AddressSetView addresses = focusedVertex.getAddresses();
Address address = addresses.getMinAddress();
focusedVertex.setProgramSelection(new ProgramSelection(address, address));
// make sure the code browser now contains a matching selection
ProgramSelection firstSelection = focusedVertex.getProgramSelection();
ListingPanel listingPanel = codeBrowser.getListingPanel();
ProgramSelection codeBrowserSelection = listingPanel.getProgramSelection();
assertTrue(!firstSelection.isEmpty());
assertEquals("Selecting text in the vertex did not select text in the code browser",
firstSelection, codeBrowserSelection);
Collection<FGVertex> vertices = functionGraph.getVertices();
FGVertex otherVertex = null;
for (FGVertex vertex : vertices) {
if (vertex != focusedVertex) {
otherVertex = vertex;
break;
}
}
assertNotNull(otherVertex);
Address otherAddress = otherVertex.getAddresses().getMinAddress();
otherVertex.setProgramSelection(new ProgramSelection(otherAddress, otherAddress));
ProgramSelection secondSelection = otherVertex.getProgramSelection();
assertTrue(!secondSelection.isEmpty());
codeBrowserSelection = listingPanel.getProgramSelection();
// the new code browser selection should have both of our vertex selections
assertTrue(codeBrowserSelection.getMinAddress().equals(firstSelection.getMinAddress()));
assertTrue(codeBrowserSelection.getMaxAddress().equals(secondSelection.getMaxAddress()));
}
// TODO: see SCR 9208 - we don't currently support this, although we could
public void dontTestNavigatingBackwardsRestoresPerspectiveInfo_ZoomOutOff() throws Exception {
//
// Test, that with the default 'zoomed-out' option *off* for new graphs, we will restore the
// user's previous graph perspective data when they navigate back to a function that
// were examining (instead of zooming back out to the full view).
//
hideSatellite();// for readability
setGraphWindowSize(300, 300);// make window small for easier testing
setZoomOutOption(false);
// zoom in to a vertex (pick something beside the entry point)
goTo("1004196");
zoomInCompletely();
// move that vertex off center
moveView(10);
FGVertex v = getFocusedVertex();
Point originalLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v);
// 'go to' a new function to change the graph
goTo("01002239");
// trigger a back navigation
navigateBack();
// make sure that the zoom is restored (note: the point information reflects both
// location and zoom)
Point restoredLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v);
assertEquals(originalLocation, restoredLocation);
}
//
// TODO: I have changed the groups such that you can group a single node, which lets you
// replace the view of the node with user-defined text.
//
// This tests verifies that a group will not be created if there is only one vertex
// found upon restoring settings. If we want to put that code back, then this test
// is again valid.
//
@Override
public void dontTestRestoringWhenCodeBlocksHaveChanged_DoesntRegroup() {
int transactionID = -1;
try {
transactionID = program.startTransaction(testName.getMethodName());
doTestRestoringWhenCodeBlocksHaveChanged_DoesntRegroup();
}
finally {
program.endTransaction(transactionID, false);
}
}
public void doTestSplitAndMergeNodesOnStaleGraph_ForReference() {
//
// Test that we can split a node into two for reference operations:
// 1) Adding a reference splits a node
// 2) Removing a reference merges a node
// 2) The above actions do not take place with automatic updates on
//
// Edges cases:
// 1) Adding a reference to a location already containing a symbol does not split the node
// 2) Removing a reference with other references at the vertex entry point does not merge nodes
//
// Find a good test function.
goToAddress("01002cf5");
// Find a node to manipulate
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
String vertexAddressString = "01002d2b";
Address vertexAddress = getAddress(vertexAddressString);
ProgramLocation location = getLocationForAddressString(vertexAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
Graph<FGVertex, FGEdge> graph = functionGraph;
FGVertex initialVertex = functionGraph.getVertexForAddress(vertexAddress);
// Find a location within that node to which we will add a label
String referenceFromAddress = "01002d3d";
String referenceToAddress = "01002d47";
Address fromAddress = getAddress(referenceFromAddress);
Address toAddress = getAddress(referenceToAddress);
//
// Start with case 1 -- adding a reference
//
// Verify the graph is not stale
FGController controller = getFunctionGraphController();
FGView view = controller.getView();
assertTrue(!view.isGraphViewStale());
// Add a reference
AddMemRefCmd addCmd =
new AddMemRefCmd(fromAddress, toAddress, RefType.DATA, SourceType.USER_DEFINED, -1);
addCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the edited node has been split--the old vertex is gone; two new vertices exist
assertTrue(!graph.containsVertex(initialVertex));
FGVertex newParentVertex = functionGraph.getVertexForAddress(fromAddress);
assertNotNull(newParentVertex);
FGVertex newChildVertex = functionGraph.getVertexForAddress(toAddress);
assertNotNull(newChildVertex);
Collection<FGEdge> parentOutEdges = graph.getOutEdges(newParentVertex);
assertTrue(parentOutEdges.size() == 1);
assertEquals(newChildVertex, parentOutEdges.iterator().next().getEnd());
// Verify the graph is stale
assertTrue(view.isGraphViewStale());
// Now remove the reference
ReferenceManager referenceManager = program.getReferenceManager();
Reference reference = referenceManager.getReference(fromAddress, toAddress, -1);
assertNotNull(reference);
RemoveReferenceCmd deleteCmd = new RemoveReferenceCmd(reference);
deleteCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the new nodes have been merged
assertTrue(!graph.containsVertex(newParentVertex));
assertTrue(!graph.containsVertex(newChildVertex));
FGVertex newOldVertex = functionGraph.getVertexForAddress(vertexAddress);
assertNotNull(newParentVertex);
FGVertex newOldChildVertex = functionGraph.getVertexForAddress(toAddress);
assertNotNull(newChildVertex);
assertEquals(newOldVertex, newOldChildVertex);// this should be the same after the merge
//
// Edge Cases
//
// Relayout to put the graph in a known good state
performRelayout();
// Find a node to manipulate (one that already has a reference)
referenceFromAddress = "01002d29";
referenceToAddress = "01002d52";
fromAddress = getAddress(referenceFromAddress);
toAddress = getAddress(referenceToAddress);
FGVertex fromVertex = functionGraph.getVertexForAddress(vertexAddress);
assertTrue(graph.containsVertex(fromVertex));
// Add a reference to the entry point of the node
addCmd =
new AddMemRefCmd(fromAddress, toAddress, RefType.DATA, SourceType.USER_DEFINED, -1);
addCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the node was not split
assertTrue(graph.containsVertex(fromVertex));
// Now remove the reference we added
reference = referenceManager.getReference(fromAddress, toAddress, -1);
assertNotNull(reference);
deleteCmd = new RemoveReferenceCmd(reference);
deleteCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the node was not merged
assertTrue(graph.containsVertex(fromVertex));
}
protected void doTestRestoringWhenCodeBlocksHaveChanged_DoesntRegroup() {
//
// Tests the behavior of how group vertices are restored when one or more of the vertices
// inside of the grouped vertex is no longer available when the graph attempts to restore
// the group vertex user settings (i.e., when restarting Ghidra, the previously grouped
// vertices should reappear).
//
// In this test, we will be mutating a group of 2 nodes such
// that one of the nodes has been split into two. This leaves only one vertex to
// be found by the regrouping algorithm. Furthermore, the regrouping will not take place
// if at least two vertices cannot be found.
//
//
// Pick a function and group some nodes.
//
FGData graphData = graphFunction("01002cf5");
FunctionGraph functionGraph = graphData.getFunctionGraph();
Set<FGVertex> ungroupedVertices =
selectVertices(functionGraph, "01002d11" /* LAB_01002d11 */, "01002cf5" /* ghidra */);
group(ungroupedVertices);
// 5 edges expected:
// -01002cf5: 2 out
// -01002cf5: 2 in, 1 out
int expectedGroupedEdgeCount = 5;
GroupedFunctionGraphVertex groupedVertex = validateNewGroupedVertexFromVertices(
functionGraph, ungroupedVertices, expectedGroupedEdgeCount);
AddressSetView addresses = groupedVertex.getAddresses();
Address minAddress = addresses.getMinAddress();
//
// Ideally, we would like to save, close and re-open the program so that we can get
// a round-trip saving and reloading. However, in the test environment, we cannot save
// our programs. So, we will instead just navigate away from the current function, clear
// the cache (to make sure that we read the settings again), and then verify that the
// data saved in the program has been used to re-group.
//
graphFunction("0100415a");
clearCache();
//
// Add a label to trigger a code block change
//
createLabel("01002d18");// in the middle of the LAB_01002d11 code block
//
// Relaunch the graph, which will use the above persisted group settings...
//
graphData = graphFunction("01002cf5");
waitForAnimation();// the re-grouping may be using animation, which runs after the graph is loaded
functionGraph = graphData.getFunctionGraph();
FGVertex expectedGroupVertex = functionGraph.getVertexForAddress(minAddress);
assertFalse(expectedGroupVertex instanceof GroupedFunctionGraphVertex);
}
protected void doTestSplitAndMergeNodesOnStaleGraph_ForSymbol() {
//
// Test that we can split a node into two for symbol operations:
// 1) Adding a symbol splits a node
// 2) Removing a symbol merges a node
// 2) The above actions do not take place with automatic updates on
//
// Edges cases:
// 1) Adding a symbol to a location already containing a symbol does not split the node
// 2) Removing a symbol with other symbols at the vertex entry point does not merge nodes
//
// Find a good test function.
goToAddress("01002cf5");
// Find a node to manipulate
FGData graphData = getFunctionGraphData();
assertNotNull(graphData);
assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
String vertexAddressString = "01002d2b";
Address vertexAddress = getAddress(vertexAddressString);
ProgramLocation location = getLocationForAddressString(vertexAddressString);
assertTrue(graphData.containsLocation(location));
FunctionGraph functionGraph = graphData.getFunctionGraph();
Graph<FGVertex, FGEdge> graph = functionGraph;
FGVertex initialVertex = functionGraph.getVertexForAddress(vertexAddress);
// Find a location within that node to which we will add a label
String labelAddressString = "01002d47";
Address labelAddress = getAddress(labelAddressString);
//
// Start with case 1 -- adding a symbol
//
// Verify the graph is not stale
FGController controller = getFunctionGraphController();
FGView view = controller.getView();
assertTrue(!view.isGraphViewStale());
// Add a label
String labelName = testName.getMethodName();
AddLabelCmd addCmd = new AddLabelCmd(labelAddress, labelName, SourceType.USER_DEFINED);
addCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the edited node has been split--the old vertex is gone; two new vertices exist
assertTrue(!graph.containsVertex(initialVertex));
FGVertex newParentVertex = functionGraph.getVertexForAddress(vertexAddress);
assertNotNull(newParentVertex);
FGVertex newChildVertex = functionGraph.getVertexForAddress(labelAddress);
assertNotNull(newChildVertex);
Collection<FGEdge> parentOutEdges = graph.getOutEdges(newParentVertex);
assertTrue(parentOutEdges.size() == 1);
assertEquals(newChildVertex, parentOutEdges.iterator().next().getEnd());
// Verify the graph is stale
assertTrue(view.isGraphViewStale());
// Now remove the label
DeleteLabelCmd deleteCmd = new DeleteLabelCmd(labelAddress, labelName);
deleteCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the new nodes have been merged
assertTrue(!graph.containsVertex(newParentVertex));
assertTrue(!graph.containsVertex(newChildVertex));
FGVertex newOldVertex = functionGraph.getVertexForAddress(vertexAddress);
assertNotNull(newParentVertex);
FGVertex newOldChildVertex = functionGraph.getVertexForAddress(labelAddress);
assertNotNull(newChildVertex);
assertEquals(newOldVertex, newOldChildVertex);// this should be the same after the merge
// Verify the graph is still stale
assertTrue(view.isGraphViewStale());
//
// Edge Cases
//
// Relayout to put the graph in a known good state
performRelayout();
// Find a node to manipulate (one that already has a label)
FGVertex vertexWithLabel = functionGraph.getVertexForAddress(vertexAddress);
// Add a label to the entry point of the node
labelName = testName.getMethodName() + "2";
addCmd = new AddLabelCmd(vertexAddress, labelName, SourceType.USER_DEFINED);
addCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the node was not split
assertTrue(graph.containsVertex(vertexWithLabel));
// Verify the graph is stale
assertTrue(view.isGraphViewStale());
// Now remove the label we added
deleteCmd = new DeleteLabelCmd(vertexAddress, labelName);
deleteCmd.applyTo(program);
program.flushEvents();
waitForSwing();
waitForAnimation();
// Verify the node was not merged
assertTrue(graph.containsVertex(vertexWithLabel));
}
// note: unreliable--garbage collection works differently across platforms (sad face)
// public void testClearCacheAndMemoryLeak() {
// //
// // Test that when we clear the cache of a graph the vertices of that graph will be
// // garbage collected
// //
// WeakSet<FunctionGraphVertex> weakSet =
// WeakDataStructureFactory.createSingleThreadAccessWeakSet();
//
// FunctionGraphData graphData = getFunctionGraphData();
// assertNotNull(graphData);
// assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults());
//
// FunctionGraph graph = graphData.getFunctionGraph();
// FunctionGraphVertex rootVertex = graph.getRootVertex();
// assertNotNull(rootVertex);
//
// weakSet.add(rootVertex);
// assertTrue(weakSet.iterator().hasNext());
//
// // move to a new function so that we the FG will not be holding a reference to the
// // originally graphed function
// String address = "01002239";
// goToAddress(address);
//
// FunctionGraphData newGraphData = getFunctionGraphData();
// assertNotNull(newGraphData);
// assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults());
//
// assertTrue(
// "Function Graph did not graph a new function as expected at address: " + address,
// !graphData.equals(newGraphData));
//
// triggerGraphDisposal(graphData);
//
// rootVertex = null;
// graph = null;
// graphData = null;
//
// // let (force) the Garbage Collector run
// System.gc();
// sleep(100);
// System.gc();
// sleep(100);
// System.gc();
// sleep(100);
//
// boolean isNotCollected = weakSet.iterator().hasNext();
// assertFalse(isNotCollected);
// }
//
// private void triggerGraphDisposal(FunctionGraphData dataToDispose) {
// Object controller = getInstanceField("controller", graphProvider);
// LRUMap<?, ?> cache = (LRUMap<?, ?>) getInstanceField("graphCache", controller);
// cache.clear();
// dataToDispose.dispose();
// }
}