GP-5620 - Fixed a bug that introduced duplicate data when renaming

functions with a filter
This commit is contained in:
dragonmacher 2025-04-25 15:34:48 -04:00
parent 71e7f65d3f
commit 8d2c94e28d
4 changed files with 171 additions and 34 deletions

View file

@ -4,9 +4,9 @@
* 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.
@ -18,25 +18,32 @@ package ghidra.app.plugin.core.functionwindow;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.table.*;
import org.junit.*;
import docking.*;
import docking.ActionContext;
import docking.DefaultActionContext;
import docking.action.DockingActionIf;
import docking.tool.ToolConstants;
import docking.widgets.combobox.GComboBox;
import docking.widgets.dialogs.SettingsDialog;
import docking.widgets.table.GTable;
import docking.widgets.table.*;
import docking.widgets.table.threaded.ThreadedTableModel;
import ghidra.app.cmd.label.RenameLabelCmd;
import ghidra.app.plugin.core.clear.ClearCmd;
import ghidra.app.plugin.core.clear.ClearOptions;
import ghidra.app.services.ProgramManager;
import ghidra.framework.cmd.CompoundCmd;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressFactory;
import ghidra.program.model.listing.*;
import ghidra.program.model.symbol.*;
import ghidra.test.*;
public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTest {
@ -45,7 +52,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
private Program program;
private FunctionWindowPlugin plugin;
private GTable functionTable;
private ComponentProvider provider;
private FunctionWindowProvider provider;
@Before
public void setUp() throws Exception {
@ -57,7 +64,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
plugin.showFunctions();
waitForSwing();
provider = tool.getComponentProvider("Functions Window");
provider = (FunctionWindowProvider) tool.getComponentProvider("Functions Window");
functionTable = (GTable) findComponentByName(provider.getComponent(), "Functions Table");
}
@ -76,7 +83,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
ProgramManager pm = tool.getService(ProgramManager.class);
pm.closeProgram(program, true);
waitForSwing();
waitForNotBusy(functionTable);
waitForTable();
}
@Test
@ -91,12 +98,12 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
cmd.add(new ClearCmd(f.getBody(), new ClearOptions()));
}
applyCmd(program, cmd);
waitForNotBusy(functionTable);
waitForTable();
assertEquals(0, functionTable.getRowCount());
undo(program);
waitForNotBusy(functionTable);
waitForTable();
assertEquals(numData, functionTable.getRowCount());
}
@ -104,7 +111,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
@Test
public void testProgramClose() throws Exception {
closeProgram();
waitForNotBusy(functionTable);
waitForTable();
assertEquals(functionTable.getRowCount(), 0);
}
@ -165,6 +172,117 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
assertThat(copyText, containsString(signatureText));
}
@Test
public void testChange_WithFitler() throws Exception {
//
// This tests a regression with changed items. Normally a changed item is handled by a
// remove of the existing row object, with a re-add of that object. This allows us to avoid
// duplicates and to sort the item. We had a bug that prevented the item from being
// removed.
//
// the bug was only present when sorted on the name column, since the sort was no longer
// correct when the name had changed
sort("Name");
int startRowCount = functionTable.getRowCount();
// verify the function we will rename is in the table
assertFunctionInTable("FUN_010058b8");
// apply a filter that will hide an item we will rename
filter("entry");
assertEquals(1, functionTable.getRowCount());
assertFunctionInTable("entry");
// rename a function not showing, using a name that will pass the filter
// FUN_010058b8 -> entry2
renameFunction(addr("010058b8"), "entry2");
// verify the new item appears
assertEquals(2, functionTable.getRowCount());
assertFunctionInTable("entry2");
// remove the filter
filter("");
// verify the old item is gone and the new item is still there
assertFunctionInTable("entry2");
assertFunctionNotInTable("FUN_010058b8");
assertEquals("Table row count should not have changed for a function rename", startRowCount,
functionTable.getRowCount());
}
private void sort(String columnName) {
int column = getColumn(columnName);
TableSortState descendingSortState = TableSortState.createDefaultSortState(column, false);
FunctionTableModel model = (FunctionTableModel) functionTable.getModel();
runSwing(() -> model.setTableSortState(descendingSortState));
waitForTable();
}
private int getColumn(String columnName) {
int n = functionTable.getColumnCount();
for (int i = 0; i < n; i++) {
String name = functionTable.getColumnName(i);
if (name.equals(columnName)) {
return i;
}
}
fail("Could not find column '%s'".formatted(columnName));
return 0;
}
private void assertFunctionNotInTable(String expectedName) {
FunctionTableModel model = (FunctionTableModel) functionTable.getModel();
List<FunctionRowObject> data = model.getModelData();
for (FunctionRowObject rowObject : data) {
Function f = rowObject.getFunction();
String name = f.getName();
if (name.equals(expectedName)) {
fail("The table should not have a function by name '%s'".formatted(expectedName));
}
}
}
private void assertFunctionInTable(String expectedName) {
FunctionTableModel model = (FunctionTableModel) functionTable.getModel();
List<FunctionRowObject> data = model.getModelData();
for (FunctionRowObject rowObject : data) {
Function f = rowObject.getFunction();
String name = f.getName();
if (name.equals(expectedName)) {
return;
}
}
fail("The table should have a function by name '%s'".formatted(expectedName));
}
private void renameFunction(Address entry, String newName) {
FunctionManager fm = program.getFunctionManager();
Function f = fm.getFunctionAt(entry);
Symbol symbol = f.getSymbol();
Namespace namespace = f.getParentNamespace();
RenameLabelCmd cmd =
new RenameLabelCmd(symbol, newName, namespace, SourceType.USER_DEFINED);
applyCmd(program, cmd);
waitForTable();
}
private Address addr(String s) {
AddressFactory af = program.getAddressFactory();
return af.getAddress(s);
}
private void filter(String text) {
GTableFilterPanel<?> filterPanel = functionTable.getTableFilterPanel();
runSwing(() -> filterPanel.setFilterText(text));
waitForTable();
}
private String getCopyText(int row, int column) {
Object value = runSwing(() -> functionTable.getValueAt(row, column));
assertNotNull(value);
@ -227,8 +345,8 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
runSwing(() -> table.editingStopped(new ChangeEvent(table)));
}
private void waitForNotBusy(GTable table) {
waitForTableModel((ThreadedTableModel<?, ?>) table.getModel());
private void waitForTable() {
waitForTableModel((ThreadedTableModel<?, ?>) functionTable.getModel());
}
}