mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 17:59:46 +02:00
1220 lines
38 KiB
Java
1220 lines
38 KiB
Java
/* ###
|
|
* 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.test;
|
|
|
|
import java.awt.Dialog;
|
|
import java.awt.Window;
|
|
import java.io.*;
|
|
import java.util.*;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import java.util.stream.Collectors;
|
|
|
|
import javax.swing.JFrame;
|
|
|
|
import org.jdom.Element;
|
|
|
|
import docking.ComponentProvider;
|
|
import docking.DialogComponentProvider;
|
|
import docking.test.AbstractDockingTest;
|
|
import docking.tool.ToolConstants;
|
|
import generic.jar.ResourceFile;
|
|
import generic.test.*;
|
|
import ghidra.app.events.CloseProgramPluginEvent;
|
|
import ghidra.app.events.OpenProgramPluginEvent;
|
|
import ghidra.app.plugin.core.analysis.AutoAnalysisManager;
|
|
import ghidra.app.plugin.core.progmgr.ProgramManagerPlugin;
|
|
import ghidra.app.plugin.core.script.GhidraScriptMgrPlugin;
|
|
import ghidra.app.script.JavaScriptProvider;
|
|
import ghidra.app.services.ProgramManager;
|
|
import ghidra.base.project.GhidraProject;
|
|
import ghidra.framework.Application;
|
|
import ghidra.framework.ToolUtils;
|
|
import ghidra.framework.main.*;
|
|
import ghidra.framework.model.*;
|
|
import ghidra.framework.options.Options;
|
|
import ghidra.framework.plugintool.Plugin;
|
|
import ghidra.framework.plugintool.PluginTool;
|
|
import ghidra.framework.plugintool.util.PluginException;
|
|
import ghidra.framework.project.DefaultProjectManager;
|
|
import ghidra.program.database.ProgramDB;
|
|
import ghidra.program.model.data.FileDataTypeManager;
|
|
import ghidra.program.model.lang.*;
|
|
import ghidra.program.model.listing.Program;
|
|
import ghidra.program.util.DefaultLanguageService;
|
|
import ghidra.program.util.ProgramUtilities;
|
|
import ghidra.util.*;
|
|
import ghidra.util.datastruct.WeakSet;
|
|
import ghidra.util.exception.*;
|
|
import ghidra.util.task.*;
|
|
import utilities.util.FileUtilities;
|
|
|
|
public class TestEnv {
|
|
|
|
private static final int FIVE_MINUTES = 5;
|
|
|
|
private static int toolID = 2;
|
|
|
|
/**
|
|
* Used to perform emergency cleanup. Tests are expected to call {@link #dispose()} in
|
|
* their <tt>tearDown</tt> method. This is here to catch the case where the is some fatal
|
|
* error that prevents that from taking place.
|
|
*/
|
|
private static Set<TestEnv> instances = new HashSet<>();
|
|
|
|
private FrontEndTool frontEndTool;
|
|
private PluginTool tool;
|
|
|
|
private static TestProgramManager programManager = new TestProgramManager();
|
|
|
|
private GhidraProject gp;
|
|
|
|
/**
|
|
* A list of tools that have been created by instances of this class that will be
|
|
* disposed of at cleanup time.
|
|
*/
|
|
private List<PluginTool> extraTools = new ArrayList<>();
|
|
|
|
/**
|
|
* Constructor for Ghidra
|
|
* A new test project is established.
|
|
* If it already exists it will first be deleted.
|
|
*
|
|
* @throws IOException if there is an issue creating a test project
|
|
*/
|
|
public TestEnv() throws IOException {
|
|
this(FIVE_MINUTES, AbstractGhidraHeadlessIntegrationTest.PROJECT_NAME);
|
|
}
|
|
|
|
/**
|
|
* Constructor for Ghidra
|
|
* A new test project is established using the specified projectName.
|
|
* If it already exists it will first be deleted.
|
|
* If the test environment is not disposed within 1 minute the tests iwll be aborted
|
|
*
|
|
* @param projectName the name of the project
|
|
* @throws IOException if there is an issue creating a test project
|
|
*/
|
|
public TestEnv(String projectName) throws IOException {
|
|
this(FIVE_MINUTES, projectName);
|
|
}
|
|
|
|
/**
|
|
* Constructor for Ghidra
|
|
* A new test project is established using the specified projectName.
|
|
* If it already exists it will first be deleted.
|
|
* @param abortTimeout number of minutes within which this test environment must be
|
|
* disposed. If not disposed in a timely manner, System.exit will be invoked.
|
|
* @param projectName the name of the project
|
|
* @throws IOException if there is an issue creating a test project
|
|
*/
|
|
public TestEnv(long abortTimeout, String projectName) throws IOException {
|
|
|
|
if (!Application.isInitialized()) {
|
|
throw new AssertException("The TestEnv requires the system to be " +
|
|
"initialized before usage. JUnit test should be an instance of " +
|
|
"GhidraHeadedIntegrationTest or GhidraHeadlessIntegrationTest");
|
|
}
|
|
|
|
cleanupOldInstances();
|
|
|
|
this.gp = createGhidraTestProject(projectName);
|
|
|
|
instances.add(this);
|
|
}
|
|
|
|
/**
|
|
* This constructor allows clients to manage their own projects. Also, this constructor
|
|
* will not enforce having only a single env instance running, which allows for multi-project
|
|
* testing. All other constructors will enforce that a single instance of TestEnv can
|
|
* be running at one time, closing any previously opened instances before finishing
|
|
* construction.
|
|
*
|
|
* <P>Note: this constructor is meant for subclasses.
|
|
*
|
|
* @param project the initialized project
|
|
*/
|
|
protected TestEnv(GhidraProject project) {
|
|
if (!Application.isInitialized()) {
|
|
throw new AssertException("The TestEnv requires the system to be " +
|
|
"initialized before usage. JUnit test should be an instance of " +
|
|
"GhidraHeadedIntegrationTest or GhidraHeadlessIntegrationTest");
|
|
}
|
|
|
|
this.gp = Objects.requireNonNull(project);
|
|
}
|
|
|
|
private void cleanupOldInstances() {
|
|
if (instances.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
Msg.error(this,
|
|
"\n\tFound non-disposed() TestEnv instances. Please examine this test!\n\n");
|
|
Set<TestEnv> copy = new HashSet<>(instances);
|
|
copy.forEach(env -> env.dispose());
|
|
}
|
|
|
|
/**
|
|
* Get the tool associated with this test environment.
|
|
* @return the default test tool for this environment
|
|
*/
|
|
public PluginTool getTool() {
|
|
return lazyTool();
|
|
}
|
|
|
|
/**
|
|
* Closes the TestEnv's default tool. This method is asynchronous, so you
|
|
* must wait for the Swing thread to perform the work yourself.
|
|
* Watch out for modal dialogs.
|
|
*/
|
|
public void closeTool() {
|
|
if (tool == null) {
|
|
Msg.info(this, "Test Env tool does not exist; cannot close");
|
|
return;
|
|
}
|
|
|
|
closeAllProgramsFor(tool);
|
|
|
|
// don't want to prompt for saving
|
|
AbstractGenericTest.runSwing(() -> {
|
|
tool.setConfigChanged(false);
|
|
|
|
});
|
|
AbstractGenericTest.runSwing(() -> tool.close(), false);
|
|
AbstractGenericTest.waitForSwing();
|
|
|
|
tool = null;
|
|
}
|
|
|
|
private void closeAllProgramsFor(PluginTool theTool) {
|
|
List<Program> programs = getOpenProgamsFor(theTool);
|
|
programs.forEach(p -> close(p));
|
|
}
|
|
|
|
private List<Program> getOpenProgamsFor(PluginTool theTool) {
|
|
//@formatter:off
|
|
List<Program> toolPrograms = programManager.getOpenPrograms()
|
|
.stream()
|
|
.filter(p -> p.getConsumerList().contains(theTool))
|
|
.collect(Collectors.toList())
|
|
;
|
|
//@formatter:on
|
|
|
|
return toolPrograms;
|
|
}
|
|
|
|
/**
|
|
* Closes the given tool. This method is asynchronous, so you must wait for the Swing thread
|
|
* to perform the work yourself. Watch out for modal dialogs.
|
|
* @param toolToClose The tool to close.
|
|
*/
|
|
public void closeTool(final PluginTool toolToClose) {
|
|
closeTool(toolToClose, true);
|
|
}
|
|
|
|
public void closeTool(PluginTool toolToClose, boolean ignoreChanges) {
|
|
if (toolToClose == tool) {
|
|
tool = null;
|
|
}
|
|
|
|
extraTools.remove(toolToClose);
|
|
AbstractGenericTest.executeOnSwingWithoutBlocking(() -> {
|
|
if (ignoreChanges) {
|
|
toolToClose.setConfigChanged(false);
|
|
}
|
|
toolToClose.close();
|
|
});
|
|
}
|
|
|
|
protected void disposeFrontEndTool() {
|
|
if (frontEndTool == null) {
|
|
return;
|
|
}
|
|
|
|
AbstractGenericTest.runSwing(() -> frontEndTool.close());
|
|
frontEndTool = null;
|
|
removeFrontEndFromSystem();
|
|
}
|
|
|
|
private void dipsoseTestTools() {
|
|
AbstractGenericTest.runSwing(() -> {
|
|
disposeSingleTool(tool);
|
|
|
|
Iterator<PluginTool> it = extraTools.iterator();
|
|
while (it.hasNext()) {
|
|
PluginTool pt = it.next();
|
|
disposeSingleTool(pt);
|
|
}
|
|
extraTools.clear();
|
|
});
|
|
}
|
|
|
|
private void disposeSingleTool(final PluginTool pluginTool) {
|
|
if (pluginTool == null) {
|
|
return; // can happen if the default tool is not initialized
|
|
}
|
|
|
|
String toolName = pluginTool.getName();
|
|
try {
|
|
pluginTool.setConfigChanged(false); // don't want to prompt for saving
|
|
pluginTool.close();
|
|
cleanupAutoAnalysisManagers(pluginTool);
|
|
}
|
|
catch (Throwable t) {
|
|
Msg.error(TestEnv.class, "Unexpected exception closing tool: " + toolName, t);
|
|
}
|
|
}
|
|
|
|
public void saveRestoreToolState() {
|
|
AbstractGenericTest.runSwing(() -> {
|
|
Element element = lazyTool().saveDataStateToXml(true);
|
|
lazyTool().restoreDataStateFromXml(element);
|
|
});
|
|
}
|
|
|
|
public <T extends Plugin> T getPlugin(Class<T> c) {
|
|
return AbstractGhidraHeadlessIntegrationTest.getPlugin(lazyTool(), c);
|
|
}
|
|
|
|
/**
|
|
* Adds and returns the plugin to this env's tool for the given class.
|
|
*
|
|
* <P>If you have not created a tool using this env, then the default
|
|
* tool from {@link #lazyTool()} is used. If you have launched a tool, then that tool
|
|
* is used. In the following example, the given plugin is added to the default tool:
|
|
* <pre>
|
|
* TestEnv env = new TestEnv();
|
|
* env.launchDefaultTool();
|
|
* FooPlugin foo = env.addPlugin(FooPlugin.class);
|
|
* </pre>
|
|
*
|
|
*
|
|
* @param c the plugin class
|
|
* @return the plugin instance
|
|
* @throws PluginException if there is an exception adding the given tool
|
|
*/
|
|
public <T extends Plugin> T addPlugin(Class<T> c) throws PluginException {
|
|
PluginTool defaultTool = lazyTool();
|
|
defaultTool.addPlugin(c.getName());
|
|
return AbstractGhidraHeadlessIntegrationTest.getPlugin(defaultTool, c);
|
|
}
|
|
|
|
/**
|
|
* Shows any previously created tool, creating a simple empty tool if not tool has yet
|
|
* been created.
|
|
*
|
|
* <P>This method is considered sub-standard and users should prefer instead
|
|
* {@link #launchDefaultTool()} or {@link #launchDefaultTool(Program)}.
|
|
*
|
|
* @return the newly shown tool
|
|
*/
|
|
public PluginTool showTool() {
|
|
return AbstractGhidraHeadedIntegrationTest.showTool(lazyTool());
|
|
}
|
|
|
|
/**
|
|
* Shows any previously created tool, creating a simple empty tool if not tool has yet
|
|
* been created. The given program will be opened in the tool.
|
|
*
|
|
* <P>This method is considered sub-standard and users should prefer instead
|
|
* {@link #launchDefaultTool()} or {@link #launchDefaultTool(Program)}.
|
|
*
|
|
* @return the newly shown tool
|
|
*/
|
|
public PluginTool showTool(Program p) {
|
|
|
|
open(p); // this call lazyTool()
|
|
PluginTool t = AbstractGhidraHeadedIntegrationTest.showTool(lazyTool());
|
|
removeAllConsumersExceptTool(p, t);
|
|
return t;
|
|
}
|
|
|
|
private void removeAllConsumersExceptTool(Program p, PluginTool t) {
|
|
p.getConsumerList().forEach(c -> {
|
|
if (c != t) {
|
|
p.release(c);
|
|
}
|
|
});
|
|
}
|
|
|
|
@Deprecated // use DockingTestCase.waitForWindow(String title) instead
|
|
public Window waitForWindow(String title, int timeoutMS) {
|
|
return AbstractDockingTest.waitForWindow(title, timeoutMS);
|
|
}
|
|
|
|
/**
|
|
* Waits for the first window of the given class. This method is the same as
|
|
* {@link #waitForDialogComponent(Window, Class, int)} with the exception that the parent
|
|
* window is assumed to be this instance's tool frame.
|
|
*
|
|
* @param ghidraClass The class of the dialog the user desires
|
|
* @param maxTimeMS The max amount of time in milliseconds to wait for the requested dialog
|
|
* to appear.
|
|
* @return The first occurrence of a dialog that extends the given <tt>ghirdraClass</tt>
|
|
* @deprecated use instead {@link AbstractDockingTest#waitForDialogComponent(Class)}
|
|
*/
|
|
@Deprecated
|
|
public <T extends DialogComponentProvider> T waitForDialogComponent(Class<T> ghidraClass,
|
|
int maxTimeMS) {
|
|
JFrame frame = lazyTool().getToolFrame();
|
|
return AbstractDockingTest.waitForDialogComponent(frame, ghidraClass, maxTimeMS);
|
|
}
|
|
|
|
private static GhidraProject createGhidraTestProject(String projectName) throws IOException {
|
|
|
|
// delete this content before creating the project, as the project may try to use
|
|
// these files when initializing
|
|
deleteOldTestTools();
|
|
deleteSavedFrontEndTool();
|
|
|
|
String projectDirectoryName = AbstractGTest.getTestDirectoryPath();
|
|
return GhidraProject.createProject(projectDirectoryName, projectName, true);
|
|
}
|
|
|
|
private static void deleteOldTestTools() {
|
|
// this fixes tool loading from previous tests that have classes not in current classpath
|
|
String toolDirPath = ToolUtils.getApplicationToolDirPath();
|
|
FileUtilities.deleteDir(new File(toolDirPath));
|
|
}
|
|
|
|
private static void deleteSavedFrontEndTool() {
|
|
String frontEndFilename =
|
|
(String) TestUtils.getInstanceField("FRONT_END_FILE_NAME", FrontEndTool.class);
|
|
File frontEndFile = new File(Application.getUserSettingsDirectory(), frontEndFilename);
|
|
if (frontEndFile.exists()) {
|
|
frontEndFile.delete();
|
|
}
|
|
}
|
|
|
|
private void initializeSimpleTool() {
|
|
|
|
if (tool != null) {
|
|
throw new AssertException("Tool already exists--you are doing something wrong!");
|
|
}
|
|
|
|
AbstractGenericTest.runSwing(() -> {
|
|
|
|
Project project = gp.getProject();
|
|
tool = new TestTool(project);
|
|
|
|
try {
|
|
tool.addPlugin(ProgramManagerPlugin.class.getName());
|
|
}
|
|
catch (PluginException e) {
|
|
e.printStackTrace();
|
|
}
|
|
}, true);
|
|
|
|
getFrontEndTool(); // initialize the Front End
|
|
}
|
|
|
|
private PluginTool lazyTool() {
|
|
if (tool != null) {
|
|
return tool;
|
|
}
|
|
|
|
initializeSimpleTool();
|
|
return tool;
|
|
}
|
|
|
|
public FrontEndTool getFrontEndTool() {
|
|
if (frontEndTool != null) {
|
|
return frontEndTool;
|
|
}
|
|
|
|
AbstractGenericTest.runSwing(() -> {
|
|
frontEndTool = new TestFrontEndTool(gp.getProjectManager());
|
|
frontEndTool.setActiveProject(getProject());
|
|
|
|
// turn off auto-saving so tests don't affect other tests
|
|
setAutoSaveEnabled(frontEndTool, false);
|
|
frontEndTool.setConfigChanged(false);
|
|
});
|
|
|
|
return frontEndTool;
|
|
}
|
|
|
|
public ComponentProvider getFrontEndProvider() {
|
|
ComponentProvider provider =
|
|
(ComponentProvider) TestUtils.invokeInstanceMethod("getProvider", getFrontEndTool());
|
|
return provider;
|
|
}
|
|
|
|
private void removeFrontEndFromSystem() {
|
|
TestUtils.setInstanceField("tool", AppInfo.class, null);
|
|
}
|
|
|
|
public FrontEndTool showFrontEndTool() {
|
|
getFrontEndTool();
|
|
AbstractGhidraHeadedIntegrationTest.showTool(frontEndTool);
|
|
return frontEndTool;
|
|
}
|
|
|
|
/**
|
|
* This method differs from {@link #launchDefaultTool()} in that this method does not set the
|
|
* <tt>tool</tt> variable in of this <tt>TestEnv</tt> instance.
|
|
*/
|
|
public PluginTool createDefaultTool() {
|
|
PluginTool newTool = launchDefaultToolByName(AbstractGenericTest.DEFAULT_TEST_TOOL_NAME);
|
|
return newTool;
|
|
}
|
|
|
|
/**
|
|
* Launches the default tool of the test system ("CodeBrowser").
|
|
* This method will load the tool from resources and <b>not from the
|
|
* user's Ghidra settings</b>.
|
|
* <p>
|
|
* <b>Note:</b> Calling this method also changes the tool that this
|
|
* instance of the TestEnv is using, which is the reason for the existence
|
|
* of this method.
|
|
* @return the tool that is launched
|
|
*/
|
|
public PluginTool launchDefaultTool() {
|
|
|
|
if (tool != null) {
|
|
Msg.error(this, "Tool already exists--you are doing something wrong!");
|
|
}
|
|
|
|
tool = launchDefaultToolByName(AbstractGenericTest.DEFAULT_TEST_TOOL_NAME);
|
|
if (tool == null) {
|
|
throw new NullPointerException(
|
|
"Unable to launch the default tool: " + AbstractGenericTest.DEFAULT_TEST_TOOL_NAME);
|
|
}
|
|
|
|
return tool;
|
|
}
|
|
|
|
protected PluginTool launchDefaultToolByName(final String toolName) {
|
|
AtomicReference<PluginTool> ref = new AtomicReference<>();
|
|
AbstractGenericTest.runSwing(() -> {
|
|
|
|
ToolTemplate toolTemplate =
|
|
ToolUtils.readToolTemplate("defaultTools/" + toolName + ".tool");
|
|
if (toolTemplate == null) {
|
|
Msg.debug(this, "Unable to find tool: " + toolName);
|
|
return;
|
|
}
|
|
|
|
boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
|
|
AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool
|
|
FrontEndTool frontEndToolInstance = getFrontEndTool();
|
|
|
|
Project project = frontEndToolInstance.getProject();
|
|
ToolManager toolManager = project.getToolManager();
|
|
Workspace workspace = toolManager.getActiveWorkspace();
|
|
ref.set((PluginTool) workspace.runTool(toolTemplate));
|
|
|
|
AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
|
|
});
|
|
return ref.get();
|
|
}
|
|
|
|
public ScriptTaskListener runScript(File script) throws PluginException {
|
|
|
|
JavaScriptProvider scriptProvider = new JavaScriptProvider();
|
|
PrintWriter writer = new PrintWriter(System.out);
|
|
ResourceFile resourceFile = new ResourceFile(script);
|
|
Boolean result = (Boolean) AbstractGenericTest.invokeInstanceMethod("compile",
|
|
scriptProvider, new Class<?>[] { ResourceFile.class, PrintWriter.class },
|
|
new Object[] { resourceFile, writer });
|
|
|
|
if (!result) {
|
|
writer.flush();
|
|
throw new RuntimeException("Failed to compile script " + script.getAbsolutePath());
|
|
}
|
|
|
|
GhidraScriptMgrPlugin sm = getPlugin(GhidraScriptMgrPlugin.class);
|
|
if (sm == null) {
|
|
lazyTool().addPlugin(GhidraScriptMgrPlugin.class.getName());
|
|
sm = getPlugin(GhidraScriptMgrPlugin.class);
|
|
}
|
|
|
|
String scriptName = script.getName();
|
|
ScriptTaskListener listener = new ScriptTaskListener(scriptName);
|
|
sm.runScript(scriptName, listener);
|
|
return listener;
|
|
}
|
|
|
|
/**
|
|
* Returns GhidraProject associated with this environment
|
|
*/
|
|
public GhidraProject getGhidraProject() {
|
|
return gp;
|
|
}
|
|
|
|
/**
|
|
* A convenience method to close and then reopen the default project created by this TestEnv
|
|
* instance. This will not delete the project between opening and closing and will restore
|
|
* the project to its previous state.
|
|
*/
|
|
public void closeAndReopenProject() throws IOException {
|
|
gp.setDeleteOnClose(false);
|
|
Project project = gp.getProject();
|
|
ProjectLocator projectLocator = project.getProjectLocator();
|
|
gp.close();
|
|
|
|
extraTools.clear();
|
|
|
|
gp = GhidraProject.openProject(
|
|
projectLocator.getProjectDir().getParentFile().getAbsolutePath(),
|
|
projectLocator.getName(), true /* restore the project */);
|
|
gp.setDeleteOnClose(true);
|
|
initializeSimpleTool();
|
|
}
|
|
|
|
public ProjectManager getProjectManager() {
|
|
return gp.getProjectManager();
|
|
}
|
|
|
|
/**
|
|
* Returns Project associated with this environment
|
|
*/
|
|
public Project getProject() {
|
|
return gp.getProject();
|
|
}
|
|
|
|
public PluginTool restartTool() {
|
|
closeTool();
|
|
|
|
AbstractGenericTest.waitForSwing();
|
|
|
|
tool = null;
|
|
initializeSimpleTool();
|
|
|
|
return tool;
|
|
}
|
|
|
|
/**
|
|
* Launches another default tool, not overwriting this env's current tool.
|
|
* @return the new tool
|
|
*/
|
|
public PluginTool launchAnotherDefaultTool() {
|
|
PluginTool newTool = createDefaultTool();
|
|
newTool.setToolName(newTool.getToolName() + toolID++);
|
|
extraTools.add(newTool);
|
|
return newTool;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns an array of tools spawned by the Ghidra environment.
|
|
* NOTE: This array will not contain any of the TestTools!
|
|
* @return an array of tools spawned by the Ghidra environment
|
|
*/
|
|
public Tool[] getGhidraCreatedTools() {
|
|
return gp.getProject().getToolManager().getRunningTools();
|
|
}
|
|
|
|
public ToolConnection connectTools(PluginTool producer, PluginTool consumer) {
|
|
ToolConnection tc = gp.getProject().getToolManager().getConnection(producer, consumer);
|
|
String[] events = tc.getEvents();
|
|
for (String element : events) {
|
|
tc.connect(element);
|
|
}
|
|
return tc;
|
|
}
|
|
|
|
public void disconnectTools(PluginTool producer, PluginTool consumer) {
|
|
|
|
if (producer == null || consumer == null) {
|
|
return; // can happen if the default tool was never initialized
|
|
}
|
|
|
|
ToolConnection tc = gp.getProject().getToolManager().getConnection(producer, consumer);
|
|
String[] events = tc.getEvents();
|
|
for (String element : events) {
|
|
tc.disconnect(element);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies the specified program zip file to the JUnit test project's root folder. <b>This
|
|
* means that the program will appear in the FrontEndTool as part of the project.</b> That is
|
|
* the only reason to use this method vice openProgram().
|
|
*
|
|
* @param programName the name of the program zip file without the ".gzf" extension.
|
|
*/
|
|
public DomainFile restoreProgram(String programName) throws FileNotFoundException {
|
|
DomainFile df = programManager.addProgramToProject(getProject(), programName);
|
|
return df;
|
|
}
|
|
|
|
public static ResourceFile findProvidedDataTypeArchive(String relativePathName) {
|
|
relativePathName = relativePathName.replace('\\', '/');
|
|
String suffix = FileDataTypeManager.SUFFIX;
|
|
if (!relativePathName.endsWith(suffix)) {
|
|
relativePathName = relativePathName + suffix;
|
|
}
|
|
for (ResourceFile file : Application.findFilesByExtensionInApplication(suffix)) {
|
|
String path = file.getAbsolutePath().replace('\\', '/');
|
|
if (path.endsWith(relativePathName)) {
|
|
return file;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Creates a project data type archive in the indicated test project folder from the ".gdt"
|
|
* file indicated by the relative pathname.
|
|
*
|
|
* @param relativePathName This should be a pathname relative to the "test_resources/testdata"
|
|
* director or relative to the "typeinfo" directory. The name should
|
|
* include the ".gdt" suffix.
|
|
* @param domainFolder the folder in the test project where the archive should be created.
|
|
* @param monitor monitor for canceling this restore.
|
|
* @return the domain file that was created in the project
|
|
*/
|
|
public DomainFile restoreDataTypeArchive(String relativePathName, DomainFolder domainFolder)
|
|
throws InvalidNameException, IOException, VersionException {
|
|
|
|
File gdtFile;
|
|
try {
|
|
gdtFile = AbstractGenericTest.getTestDataFile(relativePathName);
|
|
}
|
|
catch (FileNotFoundException e) {
|
|
gdtFile = findProvidedDataTypeArchive(relativePathName).getFile(true);
|
|
}
|
|
|
|
if (gdtFile == null || !gdtFile.exists()) {
|
|
throw new RuntimeException("Data type archive not found: " + relativePathName);
|
|
}
|
|
|
|
String baseName = relativePathName;
|
|
String suffix = FileDataTypeManager.SUFFIX;
|
|
if (relativePathName.toLowerCase().endsWith(suffix)) {
|
|
baseName = baseName.substring(0, baseName.length() - suffix.length());
|
|
}
|
|
|
|
int lastIndex = baseName.lastIndexOf('/');
|
|
if (lastIndex >= 0) {
|
|
baseName = baseName.substring(lastIndex + 1);
|
|
}
|
|
|
|
lastIndex = baseName.lastIndexOf('\\');
|
|
if (lastIndex >= 0) {
|
|
baseName = baseName.substring(lastIndex + 1);
|
|
}
|
|
|
|
String name = baseName;
|
|
DomainObject domainObject = null;
|
|
DomainFile domainFile = null;
|
|
try {
|
|
TaskMonitor monitor = TaskMonitor.DUMMY;
|
|
domainFile = domainFolder.createFile(name, gdtFile, monitor);
|
|
domainObject = domainFile.getDomainObject(this, true, false, monitor);
|
|
if (domainObject.canSave()) {
|
|
domainObject.save("Saving " + gdtFile.getName() + " GDT to project", monitor);
|
|
}
|
|
}
|
|
catch (CancelledException e) {
|
|
// can't happen; dummy monitor
|
|
}
|
|
finally {
|
|
if (domainObject != null) {
|
|
domainObject.release(this);
|
|
}
|
|
}
|
|
return domainFile;
|
|
}
|
|
|
|
/**
|
|
* Save a program to the cached program store. A SaveAs will be performed on the
|
|
* program to its cached storage location.
|
|
* @param progName program name
|
|
* @param program program object
|
|
* @param replace if true any existing cached database with the same name will be replaced
|
|
* @param monitor task monitor
|
|
* @throws DuplicateNameException if already cached
|
|
*/
|
|
public void saveToCache(String progName, ProgramDB program, boolean replace,
|
|
TaskMonitor monitor) throws IOException, DuplicateNameException, CancelledException {
|
|
|
|
programManager.saveToCache(progName, program, replace, monitor);
|
|
}
|
|
|
|
/**
|
|
* Determine if specified program already exists with the program cache
|
|
* @param programName the name
|
|
* @return true if specified program already exists with the program cache
|
|
*/
|
|
public boolean isProgramCached(String programName) {
|
|
return programManager.isProgramCached(programName);
|
|
}
|
|
|
|
/**
|
|
* Remove specified program from cache
|
|
* @param programName the name
|
|
*/
|
|
public void removeFromProgramCache(String programName) {
|
|
programManager.removeFromProgramCache(programName);
|
|
}
|
|
|
|
public ProgramDB loadAnalyzedNotepad() {
|
|
return getProgram("pe/w2krtm/NOTEPAD.EXE.analyzed.dont.edit");
|
|
}
|
|
|
|
/**
|
|
* Open a read-only test program from the test data directory.
|
|
* This program must be released prior to disposing this test environment.
|
|
* NOTE: Some tests rely on this method returning null when file does
|
|
* not yet exist within the resource area (e.g., test binaries for P-Code Tests)
|
|
*
|
|
* @param programName name of program database within the test data directory.
|
|
* @return program or null if program file not found
|
|
*/
|
|
|
|
public ProgramDB getProgram(String programName) {
|
|
ProgramDB p = programManager.getProgram(programName);
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* Launches the default tool of the test system ("CodeBrowser") using the
|
|
* given program. This method will load the tool from resources and <b>not from the
|
|
* user's Ghidra settings</b>.
|
|
* <p>
|
|
* <b>Note:</b> Calling this method also changes the tool that this
|
|
* instance of the TestEnv is using, which is the reason for the existence
|
|
* of this method.
|
|
*
|
|
* @param program The program to load into the default tool; may be null
|
|
* @return the tool that is launched
|
|
*/
|
|
public PluginTool launchDefaultTool(Program program) {
|
|
|
|
if (tool != null) {
|
|
throw new AssertException("Tool already exists--you are doing something wrong!");
|
|
}
|
|
|
|
AbstractGenericTest.runSwing(() -> {
|
|
tool = launchDefaultTool();
|
|
ProgramManager pm = tool.getService(ProgramManager.class);
|
|
pm.openProgram(program.getDomainFile());
|
|
});
|
|
|
|
if (tool == null) {
|
|
throw new NullPointerException(
|
|
"Unable to launch the default tool: " + ToolServices.DEFAULT_TOOLNAME);
|
|
}
|
|
|
|
AbstractGenericTest.waitForSwing();
|
|
|
|
removeAllConsumersExceptTool(program, tool);
|
|
|
|
if (program != null) {
|
|
programManager.add(program);
|
|
}
|
|
|
|
return tool;
|
|
}
|
|
|
|
/**
|
|
* Launches a tool of the given name using the given domain file.
|
|
* <p>
|
|
* Note: the tool returned will have auto save disabled by default.
|
|
*
|
|
* @return the tool that is launched
|
|
*/
|
|
public PluginTool launchTool(String toolName) {
|
|
return launchTool(toolName, null);
|
|
}
|
|
|
|
/**
|
|
* Launches a tool of the given name using the given domain file.
|
|
* <p>
|
|
* Note: the tool returned will have auto save disabled by default.
|
|
*
|
|
* @param toolName the name of the tool to launch
|
|
* @param domainFile The domain file used to launch the tool; may be null
|
|
* @return the tool that is launched
|
|
*/
|
|
public PluginTool launchTool(final String toolName, final DomainFile domainFile) {
|
|
AtomicReference<PluginTool> ref = new AtomicReference<>();
|
|
|
|
AbstractGenericTest.runSwing(() -> {
|
|
boolean wasErrorGUIEnabled = AbstractDockingTest.isUseErrorGUI();
|
|
AbstractDockingTest.setErrorGUIEnabled(false); // disable the error GUI while launching the tool
|
|
FrontEndTool frontEndToolInstance = getFrontEndTool();
|
|
|
|
Project project = frontEndToolInstance.getProject();
|
|
ToolServices toolServices = project.getToolServices();
|
|
PluginTool newTool = (PluginTool) toolServices.launchTool(toolName, null);
|
|
if (newTool == null) {
|
|
// couldn't find the tool in the workspace...check the test area
|
|
newTool = launchDefaultToolByName(toolName);
|
|
}
|
|
|
|
ref.set(newTool);
|
|
|
|
AbstractDockingTest.setErrorGUIEnabled(wasErrorGUIEnabled);
|
|
newTool.acceptDomainFiles(new DomainFile[] { domainFile });
|
|
});
|
|
|
|
PluginTool launchedTool = ref.get();
|
|
if (launchedTool == null) {
|
|
throw new NullPointerException("Unable to launch the tool: " + toolName);
|
|
}
|
|
|
|
// this will make sure that our tool is closed during disposal
|
|
extraTools.add(launchedTool);
|
|
return launchedTool;
|
|
}
|
|
|
|
/**
|
|
* Sets the auto-save feature for all tool instances running under the {@link FrontEndTool}
|
|
* created by this TestEnv instance. Auto-save is off by default when testing.
|
|
*
|
|
* @param enabled true enables auto-save
|
|
*/
|
|
public void setAutoSaveEnabled(boolean enabled) {
|
|
FrontEndTool frontEndToolInstance = getFrontEndTool();
|
|
setAutoSaveEnabled(frontEndToolInstance, enabled);
|
|
}
|
|
|
|
protected void setAutoSaveEnabled(final FrontEndTool frontEndToolInstance,
|
|
final boolean enabled) {
|
|
AbstractGenericTest.runSwing(() -> {
|
|
Options options = frontEndToolInstance.getOptions(ToolConstants.TOOL_OPTIONS);
|
|
options.setBoolean(FrontEndTool.AUTOMATICALLY_SAVE_TOOLS, enabled);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Import a program as binary.
|
|
* @param programName resource name that is the name of the program
|
|
* @param language language
|
|
* @param compilerSpec compiler spec
|
|
* @return program
|
|
* @throws IOException
|
|
* @throws LanguageNotFoundException
|
|
* @throws VersionException
|
|
* @throws InvalidNameException
|
|
* @throws DuplicateNameException
|
|
* @throws CancelledException
|
|
*/
|
|
public Program loadResourceProgramAsBinary(String programName, Language language,
|
|
CompilerSpec compilerSpec) throws LanguageNotFoundException, IOException,
|
|
CancelledException, DuplicateNameException, InvalidNameException, VersionException {
|
|
File file = AbstractGenericTest.getTestDataFile(programName);
|
|
if (file == null || !file.exists()) {
|
|
throw new FileNotFoundException("Can not find test program: " + programName);
|
|
}
|
|
return gp.importProgram(file, language, compilerSpec);
|
|
}
|
|
|
|
/**
|
|
* Import a program as binary.
|
|
* @param programName resource name that is the name of the program
|
|
* @param processor processor
|
|
* @return program
|
|
* @throws IOException
|
|
* @throws LanguageNotFoundException
|
|
* @throws VersionException
|
|
* @throws InvalidNameException
|
|
* @throws DuplicateNameException
|
|
* @throws CancelledException
|
|
*/
|
|
public Program loadResourceProgramAsBinary(String programName, Processor processor)
|
|
throws CancelledException, DuplicateNameException, InvalidNameException,
|
|
VersionException, IOException {
|
|
Language language =
|
|
DefaultLanguageService.getLanguageService().getDefaultLanguage(processor);
|
|
CompilerSpec compilerSpec = language.getDefaultCompilerSpec();
|
|
return loadResourceProgramAsBinary(programName, language, compilerSpec);
|
|
}
|
|
|
|
/**
|
|
* Release a program which was obtained from this test environment.
|
|
* @param program the program
|
|
*/
|
|
public void release(Program program) {
|
|
programManager.release(program);
|
|
}
|
|
|
|
/**
|
|
* Special code to make sure that the tool used by an AutoAnalysis manager instance is
|
|
* removed. This prevents the accidental re-use of the wrong tool between test runs.
|
|
* (Why? Well, the AA manager has a static map of tools and it sometimes picks tools to
|
|
* use based upon which GUI window is active, which when the current window is not active,
|
|
* can cause the wrong tool to be used for a test, which means that a disposed tool can
|
|
* be used, which prevents AA from running).
|
|
* <p>
|
|
* This code is our use of inside knowledge to cleanup testing artifacts.
|
|
* <p>
|
|
* Note: if each test fired a 'program closed' event, then this wouldn't be an issue, but
|
|
* they don't. Further, doing that here has ramifications with threading and timely
|
|
* closing of the test environment. So, rather than fire an event here, we will just
|
|
* do our magic.
|
|
*/
|
|
private void cleanupAutoAnalysisManagers(PluginTool t) {
|
|
|
|
@SuppressWarnings("unchecked")
|
|
Map<Program, AutoAnalysisManager> map =
|
|
(Map<Program, AutoAnalysisManager>) TestUtils.getInstanceField("managerMap",
|
|
AutoAnalysisManager.class);
|
|
Collection<AutoAnalysisManager> managers = map.values();
|
|
for (AutoAnalysisManager manager : managers) {
|
|
@SuppressWarnings("unchecked")
|
|
Map<Program, WeakSet<PluginTool>> toolMap =
|
|
(Map<Program, WeakSet<PluginTool>>) TestUtils.getInstanceField("toolMap", manager);
|
|
|
|
Collection<WeakSet<PluginTool>> values = toolMap.values();
|
|
for (WeakSet<PluginTool> toolSet : values) {
|
|
Iterator<PluginTool> iterator = toolSet.iterator();
|
|
while (iterator.hasNext()) {
|
|
PluginTool aaTool = iterator.next();
|
|
manager.removeTool(aaTool);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Opens the given program in the test tool.
|
|
*
|
|
* @param program the program to open
|
|
*/
|
|
public void open(Program program) {
|
|
lazyTool().firePluginEvent(new OpenProgramPluginEvent("Test", program));
|
|
programManager.add(program);
|
|
}
|
|
|
|
/**
|
|
* Closes the given program, ignoring all changes, for each tool known to this TestEnv.
|
|
*
|
|
* @param p the program to close
|
|
*/
|
|
public void close(Program p) {
|
|
|
|
release(p);
|
|
|
|
boolean ignoreChanges = true;
|
|
lazyTool().firePluginEvent(new CloseProgramPluginEvent("Test", p, ignoreChanges));
|
|
|
|
extraTools.forEach(
|
|
t -> t.firePluginEvent(new CloseProgramPluginEvent("Test", p, ignoreChanges)));
|
|
}
|
|
|
|
// we are a framework method, so we know it is OK to call the deprecated Swing wait methods
|
|
public void dispose() {
|
|
|
|
instances.remove(this);
|
|
|
|
AbstractDockingTest.disposeErrorGUI();
|
|
|
|
printOpenModalDialogs();
|
|
|
|
try {
|
|
disconnectConnectedTools();
|
|
}
|
|
catch (Throwable t) {
|
|
Msg.error(TestEnv.class, "Problem disconnecting tools", t);
|
|
}
|
|
|
|
disposeAllTasks();
|
|
|
|
markAllProgramsAsUnchanged();
|
|
|
|
disposeTestTools();
|
|
|
|
privateWaitForSwingRunnables();
|
|
programManager.disposeOpenPrograms();
|
|
|
|
if (gp.getProject() == null) {
|
|
throw new IllegalStateException("The TestEnv's GhidraProject has already been closed!");
|
|
}
|
|
|
|
Project project = gp.getProject();
|
|
String projectName = project.getName();
|
|
try {
|
|
AbstractGenericTest.runSwing(() -> gp.close());
|
|
}
|
|
catch (Throwable t) {
|
|
Msg.error(TestEnv.class, "Problem disposing the test project", t);
|
|
}
|
|
|
|
privateWaitForSwingRunnables();
|
|
|
|
disposeFrontEndTool();
|
|
|
|
AbstractDockingTest.closeAllWindows(true);
|
|
|
|
disposeAllSwingUpdateManagers();
|
|
|
|
deleteTestProject(projectName);
|
|
}
|
|
|
|
private void deleteTestProject(String projectName) {
|
|
boolean deletedProject = AbstractGhidraHeadlessIntegrationTest.deleteProject(
|
|
AbstractGTest.getTestDirectoryPath(), projectName);
|
|
|
|
if (!deletedProject) {
|
|
Msg.error(TestEnv.class, "dispose() - Open programs after disposing project: ");
|
|
Iterator<Program> iterator = ProgramUtilities.getSystemPrograms();
|
|
while (iterator.hasNext()) {
|
|
Program program = iterator.next();
|
|
if (program.isClosed()) {
|
|
continue;
|
|
}
|
|
System.err.println("->" + projectName + " " + program.getName());
|
|
printProgramConsumers(program);
|
|
}
|
|
|
|
// signal a potential issue by printing out a throwable--we don't throw the exception
|
|
// so that the tests may limp along if this is not a serious issue--throwing the
|
|
// exception my prevent other cleanup from taking place.
|
|
Msg.error(TestEnv.class, "Unable to delete project: " + projectName +
|
|
" in directory: " + AbstractGTest.getTestDirectoryPath(), new RuntimeException());
|
|
}
|
|
}
|
|
|
|
private void disposeAllTasks() {
|
|
|
|
// Note: background tool tasks are disposed by the tool
|
|
|
|
@SuppressWarnings("unchecked")
|
|
Map<Task, TaskMonitor> tasks =
|
|
(Map<Task, TaskMonitor>) TestUtils.getInstanceField("runningTasks",
|
|
TaskUtilities.class);
|
|
for (TaskMonitor tm : tasks.values()) {
|
|
tm.cancel();
|
|
}
|
|
|
|
// wait just a bit for the tasks to finish; we don't really care at this point, since
|
|
// we are disposing
|
|
AbstractGTest.waitForConditionWithoutFailing(() -> !TaskUtilities.isExecutingTasks());
|
|
privateWaitForSwingRunnables();
|
|
}
|
|
|
|
private void printOpenModalDialogs() {
|
|
boolean hasModal = false;
|
|
Set<Window> windows = AbstractGenericTest.getAllWindows();
|
|
for (Window window : windows) {
|
|
if (window instanceof Dialog) {
|
|
if (((Dialog) window).isModal() && window.isShowing()) {
|
|
hasModal = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!hasModal) {
|
|
return;
|
|
}
|
|
|
|
String windowInfo = AbstractDockingTest.getOpenWindowsAsString();
|
|
if (!windowInfo.isEmpty()) {
|
|
Msg.error(TestEnv.class, "Open modal dialogs - all windows: " + windowInfo);
|
|
}
|
|
|
|
}
|
|
|
|
private void disposeTestTools() {
|
|
AbstractGenericTest.runSwing(() -> {
|
|
try {
|
|
dipsoseTestTools();
|
|
}
|
|
catch (Throwable t) {
|
|
Msg.error(TestEnv.class, "Problem disposing the test tool", t);
|
|
}
|
|
}, false);
|
|
|
|
privateWaitForSwingRunnables();
|
|
}
|
|
|
|
// the deprecation is OK--we are a framework method and we know we can use it
|
|
@SuppressWarnings("deprecation")
|
|
private void privateWaitForSwingRunnables() {
|
|
AbstractGenericTest.privateWaitForPostedSwingRunnables_SwingSafe();
|
|
}
|
|
|
|
private void disposeAllSwingUpdateManagers() {
|
|
//
|
|
// Cleanup all statically tracked SwingUpdateManagers. If we do not do this, then as
|
|
// tools are launched, the number of tracked managers increases, as not all clients of
|
|
// the managers will dispose the managers.
|
|
//
|
|
@SuppressWarnings("unchecked")
|
|
WeakSet<SwingUpdateManager> s =
|
|
(WeakSet<SwingUpdateManager>) TestUtils.getInstanceField("instances",
|
|
SwingUpdateManager.class);
|
|
|
|
/* Debug for undisposed SwingUpdateManagers
|
|
Msg.out("complete update manager list: ");
|
|
List<SwingUpdateManager> list = new ArrayList<>(s.values());
|
|
Collections.sort(list, (v1, v2) -> {
|
|
|
|
return v1.toString().compareTo(v2.toString());
|
|
});
|
|
Msg.out(StringUtils.join(list, ",\n"));
|
|
*/
|
|
|
|
AbstractGenericTest.runSwing(() -> s.clear());
|
|
}
|
|
|
|
private void markAllProgramsAsUnchanged() {
|
|
programManager.markAllProgramsAsUnchanged();
|
|
}
|
|
|
|
private void disconnectConnectedTools() {
|
|
|
|
if (extraTools.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
PluginTool[] tools = new PluginTool[extraTools.size()];
|
|
extraTools.toArray(tools);
|
|
for (PluginTool otherTool : tools) {
|
|
disconnectTools(tool, otherTool);
|
|
disconnectTools(otherTool, tool);
|
|
}
|
|
|
|
for (int i = 0; i < tools.length; i++) {
|
|
PluginTool tool1 = tools[i];
|
|
for (int j = 0; j < tools.length; j++) {
|
|
if (i == j) {
|
|
continue;
|
|
}
|
|
PluginTool tool2 = tools[j];
|
|
disconnectTools(tool1, tool2);
|
|
disconnectTools(tool2, tool1);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void printProgramConsumers(Program program) {
|
|
List<?> consumerList = (List<?>) AbstractGenericTest.getInstanceField("consumers", program);
|
|
System.err.println("\tConsumers for: " + program.getName());
|
|
for (Object name : consumerList) {
|
|
System.err.println("\t->" + name);
|
|
}
|
|
}
|
|
|
|
public void resetDefaultTools() {
|
|
ToolChest tc = gp.getProject().getLocalToolChest();
|
|
// reset default tools in tool chest
|
|
if (tc.getToolCount() > 0) {
|
|
ToolTemplate[] templates = tc.getToolTemplates();
|
|
for (ToolTemplate element : templates) {
|
|
tc.remove(element.getName());
|
|
}
|
|
}
|
|
|
|
DefaultProjectManager pm = gp.getProjectManager();
|
|
pm.addDefaultTools(tc);
|
|
}
|
|
}
|