mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 09:49:23 +02:00
GP-5620 - Fixed a bug that introduced duplicate data when renaming
functions with a filter
This commit is contained in:
parent
71e7f65d3f
commit
8d2c94e28d
4 changed files with 171 additions and 34 deletions
|
@ -4,9 +4,9 @@
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* 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.hamcrest.CoreMatchers.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.event.ChangeEvent;
|
import javax.swing.event.ChangeEvent;
|
||||||
import javax.swing.table.*;
|
import javax.swing.table.*;
|
||||||
|
|
||||||
import org.junit.*;
|
import org.junit.*;
|
||||||
|
|
||||||
import docking.*;
|
import docking.ActionContext;
|
||||||
|
import docking.DefaultActionContext;
|
||||||
import docking.action.DockingActionIf;
|
import docking.action.DockingActionIf;
|
||||||
import docking.tool.ToolConstants;
|
import docking.tool.ToolConstants;
|
||||||
import docking.widgets.combobox.GComboBox;
|
import docking.widgets.combobox.GComboBox;
|
||||||
import docking.widgets.dialogs.SettingsDialog;
|
import docking.widgets.dialogs.SettingsDialog;
|
||||||
import docking.widgets.table.GTable;
|
import docking.widgets.table.*;
|
||||||
import docking.widgets.table.threaded.ThreadedTableModel;
|
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.ClearCmd;
|
||||||
import ghidra.app.plugin.core.clear.ClearOptions;
|
import ghidra.app.plugin.core.clear.ClearOptions;
|
||||||
import ghidra.app.services.ProgramManager;
|
import ghidra.app.services.ProgramManager;
|
||||||
import ghidra.framework.cmd.CompoundCmd;
|
import ghidra.framework.cmd.CompoundCmd;
|
||||||
import ghidra.framework.plugintool.PluginTool;
|
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.listing.*;
|
||||||
|
import ghidra.program.model.symbol.*;
|
||||||
import ghidra.test.*;
|
import ghidra.test.*;
|
||||||
|
|
||||||
public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTest {
|
public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTest {
|
||||||
|
@ -45,7 +52,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
private Program program;
|
private Program program;
|
||||||
private FunctionWindowPlugin plugin;
|
private FunctionWindowPlugin plugin;
|
||||||
private GTable functionTable;
|
private GTable functionTable;
|
||||||
private ComponentProvider provider;
|
private FunctionWindowProvider provider;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
@ -57,7 +64,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
|
|
||||||
plugin.showFunctions();
|
plugin.showFunctions();
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
provider = tool.getComponentProvider("Functions Window");
|
provider = (FunctionWindowProvider) tool.getComponentProvider("Functions Window");
|
||||||
functionTable = (GTable) findComponentByName(provider.getComponent(), "Functions Table");
|
functionTable = (GTable) findComponentByName(provider.getComponent(), "Functions Table");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +83,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
ProgramManager pm = tool.getService(ProgramManager.class);
|
ProgramManager pm = tool.getService(ProgramManager.class);
|
||||||
pm.closeProgram(program, true);
|
pm.closeProgram(program, true);
|
||||||
waitForSwing();
|
waitForSwing();
|
||||||
waitForNotBusy(functionTable);
|
waitForTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -91,12 +98,12 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
cmd.add(new ClearCmd(f.getBody(), new ClearOptions()));
|
cmd.add(new ClearCmd(f.getBody(), new ClearOptions()));
|
||||||
}
|
}
|
||||||
applyCmd(program, cmd);
|
applyCmd(program, cmd);
|
||||||
waitForNotBusy(functionTable);
|
waitForTable();
|
||||||
|
|
||||||
assertEquals(0, functionTable.getRowCount());
|
assertEquals(0, functionTable.getRowCount());
|
||||||
|
|
||||||
undo(program);
|
undo(program);
|
||||||
waitForNotBusy(functionTable);
|
waitForTable();
|
||||||
|
|
||||||
assertEquals(numData, functionTable.getRowCount());
|
assertEquals(numData, functionTable.getRowCount());
|
||||||
}
|
}
|
||||||
|
@ -104,7 +111,7 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
@Test
|
@Test
|
||||||
public void testProgramClose() throws Exception {
|
public void testProgramClose() throws Exception {
|
||||||
closeProgram();
|
closeProgram();
|
||||||
waitForNotBusy(functionTable);
|
waitForTable();
|
||||||
assertEquals(functionTable.getRowCount(), 0);
|
assertEquals(functionTable.getRowCount(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,6 +172,117 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
assertThat(copyText, containsString(signatureText));
|
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) {
|
private String getCopyText(int row, int column) {
|
||||||
Object value = runSwing(() -> functionTable.getValueAt(row, column));
|
Object value = runSwing(() -> functionTable.getValueAt(row, column));
|
||||||
assertNotNull(value);
|
assertNotNull(value);
|
||||||
|
@ -227,8 +345,8 @@ public class FunctionWindowPluginTest extends AbstractGhidraHeadedIntegrationTes
|
||||||
runSwing(() -> table.editingStopped(new ChangeEvent(table)));
|
runSwing(() -> table.editingStopped(new ChangeEvent(table)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void waitForNotBusy(GTable table) {
|
private void waitForTable() {
|
||||||
waitForTableModel((ThreadedTableModel<?, ?>) table.getModel());
|
waitForTableModel((ThreadedTableModel<?, ?>) functionTable.getModel());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,22 +4,23 @@
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package docking.widgets.table;
|
package docking.widgets.table.threaded;
|
||||||
|
|
||||||
import static docking.widgets.table.AddRemoveListItem.Type.*;
|
import static docking.widgets.table.AddRemoveListItem.Type.*;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import docking.widgets.table.threaded.*;
|
import docking.widgets.table.AddRemoveListItem;
|
||||||
|
import docking.widgets.table.TableSortingContext;
|
||||||
import ghidra.util.exception.CancelledException;
|
import ghidra.util.exception.CancelledException;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
|
@ -76,6 +77,10 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
public void process(List<AddRemoveListItem<T>> addRemoveList, TableData<T> tableData,
|
public void process(List<AddRemoveListItem<T>> addRemoveList, TableData<T> tableData,
|
||||||
TaskMonitor monitor) throws CancelledException {
|
TaskMonitor monitor) throws CancelledException {
|
||||||
|
|
||||||
|
if (addRemoveList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Set<AddRemoveListItem<T>> items = coalesceAddRemoveItems(addRemoveList);
|
Set<AddRemoveListItem<T>> items = coalesceAddRemoveItems(addRemoveList);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -85,11 +90,7 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
// existing item still has the data used to sort it. If the sort data has changed, then
|
// existing item still has the data used to sort it. If the sort data has changed, then
|
||||||
// even this step will not allow the TableData to find the item in a search.
|
// even this step will not allow the TableData to find the item in a search.
|
||||||
//
|
//
|
||||||
Map<T, T> hashed = new HashMap<>();
|
Map<T, T> hashed = hashAllTableData(tableData, monitor);
|
||||||
for (T t : tableData) {
|
|
||||||
hashed.put(t, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<T> failedToRemove = new HashSet<>();
|
Set<T> failedToRemove = new HashSet<>();
|
||||||
|
|
||||||
int n = items.size();
|
int n = items.size();
|
||||||
|
@ -103,14 +104,13 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
if (item.isChange()) {
|
if (item.isChange()) {
|
||||||
T toRemove = hashed.remove(value);
|
T toRemove = hashed.remove(value);
|
||||||
remove(tableData, toRemove, failedToRemove);
|
remove(tableData, toRemove, failedToRemove);
|
||||||
monitor.incrementProgress(1);
|
|
||||||
}
|
}
|
||||||
else if (item.isRemove()) {
|
else if (item.isRemove()) {
|
||||||
T toRemove = hashed.remove(value);
|
T toRemove = hashed.remove(value);
|
||||||
remove(tableData, toRemove, failedToRemove);
|
remove(tableData, toRemove, failedToRemove);
|
||||||
it.remove();
|
it.remove();
|
||||||
}
|
}
|
||||||
monitor.checkCancelled();
|
monitor.increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!failedToRemove.isEmpty()) {
|
if (!failedToRemove.isEmpty()) {
|
||||||
|
@ -119,7 +119,7 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
monitor.setMessage("Removing " + message);
|
monitor.setMessage("Removing " + message);
|
||||||
|
|
||||||
tableData.process((data, sortContext) -> {
|
tableData.process((data, sortContext) -> {
|
||||||
return expungeLostItems(failedToRemove, data, sortContext);
|
return expungeLostItems(failedToRemove, data, sortContext, monitor);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,8 +137,7 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
tableData.insert(value);
|
tableData.insert(value);
|
||||||
hashed.put(value, value);
|
hashed.put(value, value);
|
||||||
}
|
}
|
||||||
monitor.checkCancelled();
|
monitor.increment();
|
||||||
monitor.incrementProgress(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.setMessage("Done adding/removing");
|
monitor.setMessage("Done adding/removing");
|
||||||
|
@ -165,6 +164,17 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
return new HashSet<>(map.values());
|
return new HashSet<>(map.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<T, T> hashAllTableData(TableData<T> tableData, TaskMonitor monitor)
|
||||||
|
throws CancelledException {
|
||||||
|
TableData<T> allData = tableData.getRootData();
|
||||||
|
Map<T, T> hashed = new HashMap<>();
|
||||||
|
for (T t : allData) {
|
||||||
|
monitor.checkCancelled();
|
||||||
|
hashed.put(t, t);
|
||||||
|
}
|
||||||
|
return hashed;
|
||||||
|
}
|
||||||
|
|
||||||
private void handleAdd(AddRemoveListItem<T> item, Map<T, AddRemoveListItem<T>> map) {
|
private void handleAdd(AddRemoveListItem<T> item, Map<T, AddRemoveListItem<T>> map) {
|
||||||
|
|
||||||
T rowObject = item.getValue();
|
T rowObject = item.getValue();
|
||||||
|
@ -241,7 +251,7 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
* against the entire list of table data, locating the item to be removed.
|
* against the entire list of table data, locating the item to be removed.
|
||||||
*/
|
*/
|
||||||
private List<T> expungeLostItems(Set<T> toRemove, List<T> data,
|
private List<T> expungeLostItems(Set<T> toRemove, List<T> data,
|
||||||
TableSortingContext<T> sortContext) {
|
TableSortingContext<T> sortContext, TaskMonitor monitor) {
|
||||||
|
|
||||||
if (sortContext.isUnsorted()) {
|
if (sortContext.isUnsorted()) {
|
||||||
// this can happen if the data is unsorted and we were asked to remove an item that
|
// this can happen if the data is unsorted and we were asked to remove an item that
|
||||||
|
@ -249,10 +259,14 @@ public class CoalescingAddRemoveStrategy<T> implements TableAddRemoveStrategy<T>
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy to a new list those items that are not marked for removal. This saves the
|
// Copy to a new list those items that are not marked for removal. This saves a list move
|
||||||
// list move its items every time a remove takes place
|
// time a remove takes place
|
||||||
List<T> newList = new ArrayList<>(data.size() - toRemove.size());
|
List<T> newList = new ArrayList<>(data.size() - toRemove.size());
|
||||||
for (int i = 0; i < data.size(); i++) {
|
for (int i = 0; i < data.size(); i++) {
|
||||||
|
if (monitor.isCancelled()) {
|
||||||
|
return newList;
|
||||||
|
}
|
||||||
|
|
||||||
T rowObject = data.get(i);
|
T rowObject = data.get(i);
|
||||||
if (!toRemove.contains(rowObject)) {
|
if (!toRemove.contains(rowObject)) {
|
||||||
newList.add(rowObject);
|
newList.add(rowObject);
|
|
@ -170,7 +170,7 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We used to have code that pass proxy objects to this class to remove items. That code
|
// We used to have code that passed proxy objects to this class to remove items. That code
|
||||||
// has been updated to no longer pass proxy objects. Leaving this code here for a while
|
// has been updated to no longer pass proxy objects. Leaving this code here for a while
|
||||||
// just in case we find another client doing the same thing.
|
// just in case we find another client doing the same thing.
|
||||||
// return data.remove(t);
|
// return data.remove(t);
|
||||||
|
@ -189,6 +189,10 @@ public class TableData<ROW_OBJECT> implements Iterable<ROW_OBJECT> {
|
||||||
public void process(
|
public void process(
|
||||||
BiFunction<List<ROW_OBJECT>, TableSortingContext<ROW_OBJECT>, List<ROW_OBJECT>> function) {
|
BiFunction<List<ROW_OBJECT>, TableSortingContext<ROW_OBJECT>, List<ROW_OBJECT>> function) {
|
||||||
|
|
||||||
|
if (!isSorted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (source != null) {
|
if (source != null) {
|
||||||
source.process(function);
|
source.process(function);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,16 @@
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package docking.widgets.table;
|
package docking.widgets.table.threaded;
|
||||||
|
|
||||||
import static docking.widgets.table.AddRemoveListItem.Type.*;
|
import static docking.widgets.table.AddRemoveListItem.Type.*;
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
@ -23,7 +23,8 @@ import java.util.*;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import docking.widgets.table.threaded.TestTableData;
|
import docking.widgets.table.*;
|
||||||
|
import docking.widgets.table.threaded.CoalescingAddRemoveStrategy;
|
||||||
import ghidra.util.task.TaskMonitor;
|
import ghidra.util.task.TaskMonitor;
|
||||||
|
|
||||||
public class CoalescingAddRemoveStrategyTest {
|
public class CoalescingAddRemoveStrategyTest {
|
Loading…
Add table
Add a link
Reference in a new issue