Skip to content

Commit

Permalink
Improve integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Naton1 committed Aug 13, 2022
1 parent 2cdd3fb commit b739e67
Show file tree
Hide file tree
Showing 23 changed files with 941 additions and 106 deletions.
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ Notes:

<br/>

Notes:

* You can call methods in the remote classes - the loaded classes are used for the compiler classpath

![Execute Code Example](assets/execute-code.gif)
</details>

Expand Down Expand Up @@ -78,9 +82,11 @@ Notes:

Notes:

* You can modify the method or replace the entire method body
* You can insert code to the beginning of a method or replace the entire method body
* The compiler currently uses the loaded classes as the classpath, so if a parameter type or return type isn't loaded,
it may fail
it may fail (you can replace the type with Object to make it compile if you don't need that type)
* To access fields in the class, add a field with the same name and type in the class to compile - the reference will
automatically be replaced

![Modify Method Example](assets/modify-method.gif)
</details>
Expand All @@ -95,8 +101,7 @@ Notes:
* Ctrl+S to re-recompile and patch class (or Right-Click -> Save Changes)
* Often, decompiled code is not valid Java code, so it may not compile in some cases - the modify method feature is the
workaround for this
* The compiler currently uses the loaded classes as the classpath, so if a parameter type or return type isn't loaded,
it may fail
* The compiler currently uses the loaded classes as the classpath, so if some required type isn't loaded, it may fail

![Modify Class Example](assets/modify-class.gif)
</details>
Expand All @@ -118,11 +123,11 @@ Notes:
## Getting Started

There are three ways to run the application. Execute a provided platform-specific installer, a provided JAR file, or
build and run it yourself. This application is intended to run on Java 11+ and can attach to JVMs running Java 7+.
build and run it yourself. This application is intended to run on **Java 11+** and can attach to JVMs running Java 7+.

### Run Installer:

The platform-specific installers currently support Windows, Linux, and Mac with Intel 64-bit arch.
The platform-specific installers currently support Windows, Linux, and Mac with Intel 64-bit arch

1) Download a platform-specific installer (look at file extension)
from [the latest release](https://github.com/naton1/jvm-explorer/releases/latest)
Expand All @@ -133,8 +138,9 @@ The platform-specific installers currently support Windows, Linux, and Mac with

A platform-specific JAR is provided for each OS/arch combinations

1) Download a jvm-explorer.jar file for your OS/arch from [the latest release](https://github.com/naton1/jvm-explorer/releases/latest) - the file name includes the OS/arch
2) Run with at least Java 11
1) Download a jvm-explorer.jar file for your OS/arch (look at file name)
from [the latest release](https://github.com/naton1/jvm-explorer/releases/latest)
2) Run the JAR (Java 11+)

### Build And Run:

Expand All @@ -148,15 +154,15 @@ This approach will work on all platforms and architectures.

`cd jvm-explorer`

3) Run with Gradle
3) Run with Gradle (Java 11+)

`./gradlew run`

## Troubleshooting

Two logs files `application.log` and `agent.log` are created at `[User Home]/jvm-explorer/logs`

* Must run the JAR with a Java version of at least Java 11
* Must run the application with a Java version of at least Java 11
* Must attach to a JVM running a Java version of at least Java 7
* Must attach to a JVM running the same architecture - a 32-bit JVM must attach to a 32-bit JVM
* May have to attach to a JVM that the same user started
Expand Down
6 changes: 1 addition & 5 deletions explorer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {
}

group = "com.github.naton1"
version = "0.9.0"
version = "1.0.0"

repositories {
mavenCentral()
Expand Down Expand Up @@ -174,10 +174,6 @@ tasks {
linux {
linuxShortcut = true
}
mac {
// mac doesn't let the version start with 0, TODO remove when version is at least 1
appVersion = "1.0.0"
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.github.naton1.jvmexplorer.integration.e2e;

import com.github.naton1.jvmexplorer.integration.helper.TestJvm;
import com.github.naton1.jvmexplorer.integration.programs.SleepForeverProgram;
import javafx.scene.control.TextInputControl;
import javafx.scene.control.TreeView;
import javafx.scene.input.Clipboard;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;

@Slf4j
class ClassFieldsTest extends EndToEndTest {

@Test
void testNestedFieldsShow() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
appHelper.selectJvm(testJvm);
appHelper.selectMainClass(testJvm);
appHelper.openTab(2);
appHelper.waitUntilDisassemble(); // not directly related, but means fields are loaded too
final TreeView<?> fieldTree = appHelper.getFieldTree();
// Verify there is a nested field
fieldTree.getRoot()
.getChildren()
.stream()
.filter(c -> c.getChildren().size() > 0) // verify it has nested fields
.filter(c -> c.getValue().toString().contains("private")) // make sure it's reading a private
// field
.filter(c -> c.getValue().toString().contains("List"))
.findFirst()
.orElseThrow();
}
}

@Test
void testCopyField() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
appHelper.selectJvm(testJvm);
appHelper.selectMainClass(testJvm);
appHelper.openTab(2);
appHelper.waitUntilDisassemble(); // not directly related, but means fields are loaded too
final TreeView<?> fieldTree = appHelper.getFieldTree();
// Verify there is a nested field
fxRobot.interact(() -> fieldTree.getSelectionModel().selectFirst());
fxRobot.selectContextMenu(fieldTree, "Copy");

fxRobot.interact(() -> {
final String text = fieldTree.getSelectionModel().getSelectedItem().getValue().toString();
final String clipboard = Clipboard.getSystemClipboard().getString();

Assertions.assertTrue(text.matches(".* = " + Pattern.quote(clipboard)));
});
}
}

@Test
@Timeout(30)
void testModifyField() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
appHelper.selectJvm(testJvm);
appHelper.selectMainClass(testJvm);
appHelper.openTab(2);
final TreeView<?> fieldTree = appHelper.getFieldTree();
fxRobot.interact(() -> fieldTree.getSelectionModel().selectFirst());
final AtomicBoolean success = new AtomicBoolean(false);
final Thread thread = new Thread(() -> {
// Dialog is blocking
fxRobot.waitForStageExists("Update Field");
final TextInputControl textField = fxRobot.targetWindow("Update Field")
.lookup(".text-field")
.match(t -> t instanceof TextInputControl
&& ((TextInputControl) t).getText().equals("15"))
.queryTextInputControl();
fxRobot.interact(() -> {
textField.setText("1000");
fxRobot.targetWindow("Update Field").lookup("OK").queryButton().fire();
});
fxRobot.waitUntil(() -> fieldTree.getSelectionModel()
.getSelectedItem()
.getValue()
.toString()
.contains("1000"), 5000);
success.set(true);
});
thread.setUncaughtExceptionHandler((t, e) -> log.warn("Async thread failed", e));
thread.start();
fxRobot.selectContextMenu(fieldTree, "Edit Value");
thread.join();
Assertions.assertTrue(success.get());
}
}

}
Original file line number Diff line number Diff line change
@@ -1,45 +1,33 @@
package com.github.naton1.jvmexplorer.integration.e2e;

import com.github.naton1.jvmexplorer.integration.helper.FxRobotPlus;
import com.github.naton1.jvmexplorer.integration.helper.TestJvm;
import com.github.naton1.jvmexplorer.integration.programs.SleepForeverProgram;
import com.github.naton1.jvmexplorer.protocol.helper.ClassNameHelper;
import javafx.scene.control.ListView;
import javafx.scene.control.TabPane;
import javafx.scene.control.TreeView;
import lombok.extern.slf4j.Slf4j;
import org.fxmisc.richtext.CodeArea;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.testfx.api.FxRobot;

@Slf4j
class DecompileClassTest extends EndToEndTest {

@Test
void testProcessAppears_classesLoad_classDecompiles(FxRobot fxRobot) throws Exception {
final FxRobotPlus fxRobotPlus = new FxRobotPlus(fxRobot);
void testProcessAppears_classesLoad_classDecompiles() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
final ListView<?> listView = fxRobotPlus.lookup("#processes").queryListView();
fxRobotPlus.waitUntil(() -> fxRobotPlus.select(listView, testJvm.getProcess().pid() + ""), 5000);
final TreeView<?> treeView = fxRobotPlus.lookup("#classes").queryAs(TreeView.class);
appHelper.selectJvm(testJvm);
appHelper.waitForClassesToLoad();
final String simpleName = ClassNameHelper.getSimpleName(testJvm.getMainClassName());
fxRobotPlus.waitUntil(() -> fxRobotPlus.select(treeView, simpleName), 5000);
appHelper.selectClass(simpleName);

// Verify decompile
final CodeArea classFile = fxRobotPlus.lookup("#classFile").queryAs(CodeArea.class);
fxRobotPlus.waitUntil(() -> classFile.getText().contains("class " + simpleName), 5000);
appHelper.waitUntilJavaCodeContains("class " + simpleName);

// Verify disassemble
final TabPane tabPane = fxRobotPlus.lookup("#currentClassTabPane").queryAs(TabPane.class);
tabPane.getSelectionModel().select(1); // select bytecode
final CodeArea bytecode = fxRobotPlus.lookup("#bytecode").queryAs(CodeArea.class);
fxRobotPlus.waitUntil(() -> classFile.getText().contains("class " + simpleName), 5000);
appHelper.openTab(1);
appHelper.waitUntilByteCodeContains("class " + simpleName);

// Verify fields
tabPane.getSelectionModel().select(2); // select fields
final TreeView<?> classFields = fxRobotPlus.lookup("#classFields").queryAs(TreeView.class);
Assertions.assertTrue(classFields.getRoot().getChildren().size() > 0);
appHelper.openTab(2);
Assertions.assertTrue(appHelper.getFieldTree().getRoot().getChildren().size() > 0);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.github.naton1.jvmexplorer.integration.e2e;

import com.github.naton1.jvmexplorer.JvmExplorer;
import com.github.naton1.jvmexplorer.integration.helper.AppHelper;
import com.github.naton1.jvmexplorer.integration.helper.FxRobotPlus;
import lombok.experimental.Delegate;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.testfx.api.FxRobot;
import org.testfx.api.FxToolkit;
import org.testfx.framework.junit5.ApplicationExtension;
import org.testfx.util.DebugUtils;
Expand All @@ -23,6 +27,9 @@ abstract class EndToEndTest {
private static final AtomicInteger testNumber = new AtomicInteger();
private static final Path SCREENSHOT_DIR = Paths.get("integration-test-screenshots");

protected FxRobotPlus fxRobot;
protected AppHelper appHelper;

static {
SCREENSHOT_DIR.toFile().mkdirs();
try {
Expand All @@ -36,12 +43,14 @@ abstract class EndToEndTest {
}

@BeforeEach
void setup() throws Exception {
void setup(FxRobot fxRobot) throws Exception {
log.debug("Setting up test");
// Closing the stage kills the executor service, and highlighting throws an unhandled exception causing the
// test to fail
WaitForAsyncUtils.autoCheckException = false;
FxToolkit.setupApplication(JvmExplorer.class);
this.fxRobot = new FxRobotPlus(fxRobot);
this.appHelper = new AppHelper(this.fxRobot);
}

@AfterEach
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.github.naton1.jvmexplorer.integration.e2e;

import com.github.naton1.jvmexplorer.integration.helper.TestJvm;
import com.github.naton1.jvmexplorer.integration.programs.SleepForeverProgram;
import com.github.naton1.jvmexplorer.protocol.helper.ClassNameHelper;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.io.File;

@Slf4j
class ImportExportTest extends EndToEndTest {

@Test
void testGlobalExportThenPatch() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
appHelper.selectJvm(testJvm);
appHelper.waitForClassesToLoad();

final String simpleName = ClassNameHelper.getSimpleName(testJvm.getMainClassName());
appHelper.searchClasses(simpleName);

// Make sure it works with both classloader settings

appHelper.disableClassLoaders();
doJarExportThenImport("Export Classes", "Replace Classes");

appHelper.enableClassLoaders();
doJarExportThenImport("Export Classes", "Replace Classes");
}
}

@Test
void testExportClassThenPatch() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
appHelper.selectJvm(testJvm);
appHelper.waitForClassesToLoad();
final String simpleName = ClassNameHelper.getSimpleName(testJvm.getMainClassName());
appHelper.selectClass(simpleName);

final File tempClass = appHelper.createTempClass();
appHelper.setTestFile(tempClass);

appHelper.selectClassAction("Export Class");

fxRobot.waitUntil(() -> tempClass.length() > 0, 5000);

appHelper.setTestFile(tempClass);
appHelper.selectClassAction("Replace Class");

appHelper.waitForAlert("Replaced Class", "Successfully replaced class");
}
}

@Test
void testExportPackageThenPatch() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
appHelper.selectJvm(testJvm);
appHelper.waitForClassesToLoad();

final String packageName = ClassNameHelper.getPackageName(testJvm.getMainClassName());
final String simplePackageName = ClassNameHelper.getSimpleName(packageName);
appHelper.selectClass(simplePackageName);

doJarExportThenImport("Export Package", "Replace Package");
}
}

@Test
void testExportClassLoaderThenPatch() throws Exception {
try (final TestJvm testJvm = TestJvm.of(SleepForeverProgram.class)) {
appHelper.selectJvm(testJvm);
appHelper.waitForClassesToLoad();

appHelper.enableClassLoaders();

appHelper.selectClass("AppClassLoader");

doJarExportThenImport("Export Class Loader", "Replace Class Loader");
}
}

private void doJarExportThenImport(String exportAction, String importAction) {
final File tempJar = appHelper.createTempJar();
appHelper.setTestFile(tempJar);

appHelper.selectClassAction(exportAction);

fxRobot.waitUntil(() -> tempJar.length() > 0, 5000);
fxRobot.waitForStageExists("Export Finished");
final long exportedClasses = appHelper.streamClasses(tempJar).count();
Assertions.assertTrue(exportedClasses > 0);

log.info("Exported {} classes", exportedClasses);

appHelper.setTestFile(tempJar);
appHelper.selectClassAction(importAction);

fxRobot.waitForStageExists("Patching Finished");
appHelper.waitForAlert("Patching Finished", "Patch succeeded");
log.info("Patched {} classes", exportedClasses);
}

}
Loading

0 comments on commit b739e67

Please sign in to comment.