Files
openidea/java/jvm-inspect/tests/SelfTest.java
T
ntbang0901 a3d1e57f3e feat(debugger): memory view for a paused JVM (#2648)
Adds a Memory tab to the Debug tool window for JVM sessions: loaded classes with instance counts and shallow sizes (with deltas between refreshes), read over the HotSpot attach listener (`GC.class_histogram`), falling back to counting reachable objects through the debugger on JDKs that cannot answer while suspended. While paused, a class's instances, an instance's fields and its referrers are listed through a JDWP link (the Kotlin adapter's path, and a jdtls bundle `java/jvm-inspect` for java-debug). Debug launches started by the IDE get `-XX:+StartAttachListener` unless the user's VM options decide otherwise (not on Windows).

New crates: ide-jdwp, ide-jvm-attach, ide-jvm-inspect. The bundled jar rebuilds byte-for-byte from build.py against the pinned JDT LS plugins.

Co-authored-by: ntbang0901 <121003585+ntbang0901@users.noreply.github.com>
2026-11-11 17:10:21 +08:00

165 lines
7.3 KiB
Java

package ai.openidea.jvminspect;
import ai.openidea.jvminspect.VmSelector.Target;
import ai.openidea.jvminspect.VmSelector.VmInfo;
import java.util.List;
/** The JDI-free logic of the bundle; run with {@code build.py <jdtls> --test}. */
public final class SelfTest {
public static void main(String[] args) throws Exception {
selector_matches_launch_pid();
selector_matches_attach_port_not_threads();
selector_filters_same_port_by_host();
selector_reports_vm_not_found();
connected_vms_missing_field_is_unsupported();
request_timeout_raised_and_restored();
handles_cleared_on_new_generation();
handles_cleared_when_full();
string_preview_rule();
}
/** Stands in for java-debug's provider: its manager has no {@code connectedVMs}. */
public static final class FakeProvider {
public Object getVirtualMachineManager() {
return new Object();
}
}
static void selector_matches_launch_pid() {
List<VmInfo> vms = List.of(vm(10L, "127.0.0.1", 50001), vm(11L, "127.0.0.1", 50002));
check(VmSelector.select(vms, launch(11)).orElseThrow() == vms.get(1), "pid 11 selects the second VM");
pass("selector_matches_launch_pid");
}
static void selector_matches_attach_port_not_threads() {
// G1: two JVMs running the same program have identical thread lists; only the port tells them apart.
List<VmInfo> vms = List.of(vm(null, "127.0.0.1", 59246), vm(null, "127.0.0.1", 59247));
check(VmSelector.select(vms, attach("127.0.0.1", 59247)).orElseThrow() == vms.get(1), "port 59247 selects the second VM");
pass("selector_matches_attach_port_not_threads");
}
static void selector_filters_same_port_by_host() {
List<VmInfo> vms = List.of(vm(null, "127.0.0.1", 5005), vm(null, "10.0.0.5", 5005));
check(VmSelector.select(vms, attach("localhost", 5005)).orElseThrow() == vms.get(0), "localhost selects 127.0.0.1");
pass("selector_filters_same_port_by_host");
}
static void selector_reports_vm_not_found() {
List<VmInfo> distinct = List.of(vm(10L, "127.0.0.1", 5005), vm(null, "127.0.0.1", 5006));
check(VmSelector.select(distinct, launch(12)).isEmpty(), "unknown pid");
check(VmSelector.select(distinct, attach("127.0.0.1", 5007)).isEmpty(), "unknown port");
check(VmSelector.select(distinct, new Target(null, null, null, 1)).isEmpty(), "no pid and no port");
List<VmInfo> twins = List.of(vm(null, "127.0.0.1", 5005), vm(null, "127.0.0.1", 5005));
check(VmSelector.select(twins, attach("localhost", 5005)).isEmpty(), "same port and host is ambiguous");
pass("selector_reports_vm_not_found");
}
static void connected_vms_missing_field_is_unsupported() {
try {
ConnectedVms.read(name -> FakeProvider.class);
throw new AssertionError("a manager without connectedVMs must be unsupported");
} catch (ConnectedVms.Unsupported e) {
check(e.getMessage().contains("connectedVMs"), "detail names the field: " + e.getMessage());
}
try {
ConnectedVms.read(name -> null);
throw new AssertionError("a missing java-debug must be unsupported");
} catch (ConnectedVms.Unsupported e) {
check(e.getMessage().contains(ConnectedVms.PROVIDER), "detail names the class: " + e.getMessage());
}
pass("connected_vms_missing_field_is_unsupported");
}
static void request_timeout_raised_and_restored() {
int[] value = {3000};
RequestTimeout.Knob knob = new RequestTimeout.Knob() {
@Override
public int get() {
return value[0];
}
@Override
public void set(int ms) {
value[0] = ms;
}
};
try (RequestTimeout.Raised raised = RequestTimeout.raise(knob, 60_000)) {
check(value[0] == 60_000, "raised to 60000, was " + value[0]);
}
check(value[0] == 3000, "restored to 3000, was " + value[0]);
try (RequestTimeout.Raised raised = RequestTimeout.raise(knob, 60_000)) {
value[0] = 5000; // java-debug applied a changed setting meanwhile
}
check(value[0] == 5000, "a value changed meanwhile is kept, was " + value[0]);
value[0] = 90_000;
try (RequestTimeout.Raised raised = RequestTimeout.raise(knob, 60_000)) {
check(value[0] == 90_000, "a larger timeout is not lowered, was " + value[0]);
}
check(value[0] == 90_000, "a larger timeout stays, was " + value[0]);
pass("request_timeout_raised_and_restored");
}
static void handles_cleared_on_new_generation() throws Handles.Stale {
Handles<String> handles = new Handles<>();
handles.begin(1);
handles.put(7, "seven");
check("seven".equals(handles.get(7)), "id 7 is known in generation 1");
handles.begin(2);
expectStale(handles, 7, "id 7 from generation 1 in generation 2");
pass("handles_cleared_on_new_generation");
}
static void handles_cleared_when_full() throws Handles.Stale {
Handles<String> handles = new Handles<>();
handles.begin(1);
for (long id = 0; id <= Handles.MAX; id++) handles.put(id, "o");
check("o".equals(handles.get(0)), "a command's own objects stay known while it runs");
handles.begin(1);
expectStale(handles, 0, "a table over the limit is cleared at the next command");
pass("handles_cleared_when_full");
}
static void string_preview_rule() {
Values.StringPreview shortText = Values.stringPreview(2000, 1, () -> "text");
check(shortText.length() == 1000 && "text".equals(shortText.text()), "2000 UTF-16 bytes: 1000 chars, read: " + shortText);
Values.StringPreview longText = Values.stringPreview(2002, 1, () -> {
throw new AssertionError("the text of a long string must not be read");
});
check(longText.length() == 1001 && longText.text() == null, "2002 UTF-16 bytes: 1001 chars, not read: " + longText);
Values.StringPreview latin1 = Values.stringPreview(1000, 0, () -> "latin");
check(latin1.length() == 1000 && "latin".equals(latin1.text()), "Latin-1: one byte per char: " + latin1);
Values.StringPreview jdk8 = Values.stringPreview(5, null, () -> "chars");
check(jdk8.length() == 5 && "chars".equals(jdk8.text()), "no coder (JDK 8): one char per element: " + jdk8);
pass("string_preview_rule");
}
private static VmInfo vm(Long pid, String host, Integer port) {
return new VmInfo(null, pid, host, port, null);
}
private static Target launch(long pid) {
return new Target(pid, null, null, 1);
}
private static Target attach(String host, int port) {
return new Target(null, host, port, 1);
}
private static void expectStale(Handles<String> handles, long id, String what) {
try {
handles.get(id);
throw new AssertionError(what + ": expected stale");
} catch (Handles.Stale expected) {
// the id belongs to an earlier table
}
}
private static void check(boolean ok, String what) {
if (!ok) throw new AssertionError(what);
}
private static void pass(String name) {
System.out.println("PASS " + name);
}
}