1 /*
2 * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 /**
25 * @test
26 * @summary Test value class constructor debugging
27 * @library ..
28 * @enablePreview
29 *
30 * @comment No other references
31 * @run main CtorDebuggingTest
32 *
33 * @comment All references exist
34 * @run main CtorDebuggingTest 1 2 3
35 *
36 * @comment No reference at step 2
37 * @run main CtorDebuggingTest 1 3
38 */
39
40 import java.io.IOException;
41 import java.nio.file.Files;
42 import java.nio.file.Paths;
43 import java.util.Arrays;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Objects;
48 import java.util.regex.Pattern;
49 import java.util.regex.Matcher;
50
51 import com.sun.jdi.ClassType;
52 import com.sun.jdi.Field;
53 import com.sun.jdi.IncompatibleThreadStateException;
54 import com.sun.jdi.IntegerValue;
55 import com.sun.jdi.Location;
56 import com.sun.jdi.ObjectReference;
57 import com.sun.jdi.ReferenceType;
58 import com.sun.jdi.StackFrame;
59 import com.sun.jdi.ThreadReference;
60 import com.sun.jdi.event.BreakpointEvent;
61 import com.sun.jdi.event.ClassPrepareEvent;
62 import com.sun.jdi.event.Event;
63 import com.sun.jdi.event.EventSet;
64 import com.sun.jdi.event.VMDisconnectEvent;
65 import com.sun.jdi.request.BreakpointRequest;
66 import com.sun.jdi.request.EventRequest;
67 import com.sun.jdi.request.EventRequestManager;
68
69 /*
70 * The test reproduces scenarios when ObjectReference for value object is fetched during the object construction
71 * and the object content is changed later. When "this" ObjectReference for value object is requested,
72 * JDI returns existing reference if there are other references to the equal value object or create a new one otherwise.
73 * The test debugs "new Value(3,6)" statement by setting breakpoints in Value class constructor at locations
74 * when the object being constructed is (0,0), (3,0) and (3,6).
75 *
76 * Test scenarios are defined by test arguments; TestScaffold passes them creating debuggee process (TargetApp class).
77 * Debugsee initializes static fields with value objects (0,0), (3,0), (3,6) depending on the passed arguments.
78 * Debugger gets ObjectReferences for the fields before testing.
79 *
80 * Tested scenarios:
81 * - no existing references;
82 * - all 3 references exists;
83 * - there are references for 1 and 3 breakpoints.
84 *
85 */
86 public class CtorDebuggingTest extends TestScaffold {
87
88 static value class Value {
89 int x;
90 int y;
91 Value(int x, int y) {
92 this.x = x; // @1 breakpoint
93 this.y = y; // @2 breakpoint
94 System.out.println("."); // @3 breakpoint
95 }
96 }
97
98 static class TargetApp {
99 public static Value v1;
100 public static Value v2;
101 public static Value v3;
102
103 public static void main(String[] args) throws Exception {
104 // ensure the class is loaded
105 Class.forName(Value.class.getName());
106 List<String> argList = Arrays.asList(args);
107 if (argList.contains("1")) {
108 v1 = new Value(0, 0);
109 }
110 if (argList.contains("2")) {
111 v2 = new Value(3, 0);
112 }
113 if (argList.contains("3")) {
114 v3 = new Value(3, 6);
115 }
116 System.out.println(">>main"); // @prepared breakpoint
117 Value v = new Value(3, 6);
118 System.out.println("<<main"); // @done breakpoint
119 }
120 }
121
122
123 public static void main(String[] args) throws Exception {
124 new CtorDebuggingTest(args).startTests();
125 }
126
127 Field xField;
128 Field yField;
129
130 CtorDebuggingTest(String args[]) {
131 super(args);
132 }
133
134 ObjectReference getStaticFieldObject(ReferenceType cls, String fieldName) throws Exception {
135 Field field = cls.fieldByName(fieldName);
136 ObjectReference result = (ObjectReference)cls.getValue(field);
137 System.out.println(fieldName + " static field: " + valueString(result));
138 return result;
139 }
140
141 ObjectReference getThisObject(BreakpointEvent bkptEvent) {
142 try {
143 return bkptEvent.thread().frame(0).thisObject();
144 } catch (IncompatibleThreadStateException ex) {
145 throw new RuntimeException("Cannot get 'this' object", ex);
146 }
147 }
148
149 String valueString(ObjectReference obj) {
150 if (obj == null) {
151 return "null";
152 }
153 IntegerValue ix = (IntegerValue)obj.getValue(xField);
154 IntegerValue iy = (IntegerValue)obj.getValue(yField);
155 return obj + " (" + "x: " + ix + ", y: " + iy + ")";
156 }
157
158 void assertEquals(Object obj1, Object obj2) {
159 if (!Objects.equals(obj1, obj2)) {
160 throw new RuntimeException("Must be equal: " + obj1 + " and " + obj2);
161 }
162 // Sanity check that equal objects has equal hashCode.
163 if (obj1 != null) {
164 if (obj1.hashCode() != obj2.hashCode()) {
165 throw new RuntimeException("Equal objects have different hashCode: "
166 + obj1.hashCode() + " and " + obj2.hashCode());
167 }
168 }
169 }
170
171 void assertNotEquals(Object obj1, Object obj2) {
172 if (Objects.equals(obj1, obj2)) {
173 throw new RuntimeException("Must be different: " + obj1 + " and " + obj2);
174 }
175 }
176
177 // Sanity testing for value object detection logic.
178 void verifyClassIsValueClass(ClassType theClass, boolean expected) {
179 // VM constants
180 final int IDENTITY = 0x0020;
181 final int INTERFACE = 0x00000200;
182 final int ABSTRACT = 0x00000400;
183
184 int modifiers = theClass.modifiers();
185 boolean isIdentity = (modifiers & IDENTITY) != 0;
186 boolean isInterface = (modifiers & INTERFACE) != 0;
187 boolean isAbstract = (modifiers & ABSTRACT) != 0;
188 boolean isValueClass = !isIdentity;
189 System.out.println("Class " + theClass + " is value class: " + (isValueClass ? "YES" : "NO"));
190 if (isValueClass != expected) {
191 throw new RuntimeException("IsValueClass verification failed: "
192 + " " + isValueClass + ", expected: " + expected
193 + " (isIdentity: " + isIdentity
194 + " (isInterface: " + isInterface
195 + " (isAbstract: " + isAbstract + ")");
196 }
197 }
198
199 // Parses the specified source file for "@{id} breakpoint" tags and returns <id, line_number> map.
200 // Example:
201 // System.out.println("BP is here"); // @my_breakpoint breakpoint
202 public static Map<String, Integer> parseBreakpoints(String filePath) {
203 return parseTags("breakpoint", filePath);
204 }
205
206 public static Map<String, Integer> parseTags(String tag, String filePath) {
207 final String regexp = "\\@(.*?) " + tag;
208 Pattern pattern = Pattern.compile(regexp);
209 int lineNum = 1;
210 Map<String, Integer> result = new HashMap<>();
211 try {
212 for (String line: Files.readAllLines(Paths.get(filePath))) {
213 Matcher matcher = pattern.matcher(line);
214 if (matcher.find()) {
215 result.put(matcher.group(1), lineNum);
216 }
217 lineNum++;
218 }
219 } catch (IOException ex) {
220 throw new RuntimeException("failed to parse " + filePath, ex);
221 }
222 return result;
223 }
224
225 public static String getTestSourcePath(String fileName) {
226 return Paths.get(System.getProperty("test.src")).resolve(fileName).toString();
227 }
228
229 public static String getThisTestFile() {
230 return System.getProperty("test.file");
231 }
232
233 // TestScaffold is not very good in handling multiple breakpoints.
234 // This helper class is a listener which resumes debuggee after breakpoints.
235 class MultiBreakpointHandler extends TargetAdapter {
236 boolean needToResume = false;
237 // the map stores "this" in all breakpoints
238 Map<BreakpointRequest, ObjectReference> thisObjects = new HashMap<>();
239
240 @Override
241 public void eventSetComplete(EventSet set) {
242 if (needToResume) {
243 set.resume();
244 needToResume = false;
245 }
246 }
247
248 BreakpointRequest addBreakpoint(Location loc, ObjectReference filterObject) {
249 final BreakpointRequest request = eventRequestManager().createBreakpointRequest(loc);
250 if (filterObject != null) {
251 request.addInstanceFilter(filterObject);
252 }
253 request.enable();
254
255 TargetAdapter adapter = new TargetAdapter() {
256 @Override
257 public void breakpointReached(BreakpointEvent event) {
258 if (request.equals(event.request())) {
259 ObjectReference thisObject = getThisObject(event);
260 System.out.println("BreakpointEvent: " + event
261 + " (instanceFilter: " + valueString(filterObject) + ")"
262 + ", this = " + valueString(thisObject));
263 thisObjects.put((BreakpointRequest)event.request(), thisObject);
264 needToResume = true;
265 removeThisListener();
266 }
267 }
268 };
269
270 addListener(adapter);
271
272 System.out.println("Breakpoint added: " + loc
273 + " (instanceFilter: " + valueString(filterObject) + ")");
274
275 return request;
276 }
277
278 // Resumes the debuggee and goes through all breackpoints until the location specified is reached.
279 void resumeTo(Location loc) {
280 final BreakpointRequest request = eventRequestManager().createBreakpointRequest(loc);
281 request.enable();
282
283 class EventNotification {
284 boolean completed = false;
285 boolean disconnected = false;
286 }
287 final EventNotification en = new EventNotification();
288
289 TargetAdapter adapter = new TargetAdapter() {
290 public void eventReceived(Event event) {
291 if (request.equals(event.request())) {
292 synchronized (en) {
293 en.completed = true;
294 en.notifyAll();
295 }
296 removeThisListener();
297 } else if (event instanceof VMDisconnectEvent) {
298 synchronized (en) {
299 en.disconnected = true;
300 en.notifyAll();
301 }
302 removeThisListener();
303 }
304 }
305 };
306
307 addListener(adapter);
308 // this must be the last listener (as it resumes the debuggee)
309 addListener(this);
310
311 try {
312 synchronized (en) {
313 vm().resume();
314 while (!en.completed && !en.disconnected) {
315 en.wait();
316 }
317 }
318 } catch (InterruptedException e) {
319 }
320
321 removeListener(this);
322
323 if (en.disconnected) {
324 throw new RuntimeException("VM Disconnected before requested event occurred");
325 }
326 }
327
328 // check if the breakpoint was hit.
329 boolean breakpointHit(BreakpointRequest bkpt) {
330 return thisObjects.containsKey(bkpt);
331 }
332
333 ObjectReference thisAtBreakpoint(BreakpointRequest bkpt) {
334 return thisObjects.get(bkpt);
335 }
336 }
337
338 @Override
339 protected void runTests() throws Exception {
340 BreakpointEvent bpe = startToMain(TargetApp.class.getName());
341 ClassType targetClass = (ClassType)bpe.location().declaringType();
342
343 Map<String, Integer> breakpoints = parseBreakpoints(getThisTestFile());
344 System.out.println("breakpoints:");
345 for (var entry : breakpoints.entrySet()) {
346 System.out.println(" tag " + entry.getKey() + ", line " + entry.getValue());
347 }
348
349 Location locPrepared = findLocation(targetClass, breakpoints.get("prepared"));
350 Location locDone = findLocation(targetClass, breakpoints.get("done"));
351
352 resumeTo(locPrepared);
353 System.out.println("PREPARED");
354
355 ClassType valueClass = (ClassType)findReferenceType(Value.class.getName());
356 System.out.println(Value.class.getName() + ": " + valueClass);
357 xField = valueClass.fieldByName("x");
358 yField = valueClass.fieldByName("y");
359
360 verifyClassIsValueClass(valueClass, true);
361 verifyClassIsValueClass(targetClass, false);
362
363 // Get references for pre-created objects created by debuggee.
364 ObjectReference v1 = getStaticFieldObject(targetClass, "v1");
365 ObjectReference v2 = getStaticFieldObject(targetClass, "v2");
366 ObjectReference v3 = getStaticFieldObject(targetClass, "v3");
367
368 MultiBreakpointHandler breakpointHandler = new MultiBreakpointHandler();
369
370 // Breakpoints for location when "this" is (0,0).
371 Location loc1 = findLocation(valueClass, breakpoints.get("1"));
372 BreakpointRequest bkpt1 = breakpointHandler.addBreakpoint(loc1, null);
373 BreakpointRequest bkpt1_v1 = v1 == null ? null : breakpointHandler.addBreakpoint(loc1, v1);
374 BreakpointRequest bkpt1_v2 = v2 == null ? null : breakpointHandler.addBreakpoint(loc1, v2);
375 BreakpointRequest bkpt1_v3 = v3 == null ? null : breakpointHandler.addBreakpoint(loc1, v3);
376
377 // Breakpoints for location when "this" is (3,0).
378 Location loc2 = findLocation(valueClass, breakpoints.get("2"));
379 BreakpointRequest bkpt2 = breakpointHandler.addBreakpoint(loc2, null);
380 BreakpointRequest bkpt2_v1 = v1 == null ? null : breakpointHandler.addBreakpoint(loc2, v1);
381 BreakpointRequest bkpt2_v2 = v2 == null ? null : breakpointHandler.addBreakpoint(loc2, v2);
382 BreakpointRequest bkpt2_v3 = v3 == null ? null : breakpointHandler.addBreakpoint(loc2, v3);
383
384 // Breakpoints for location when "this" is (3,6).
385 Location loc3 = findLocation(valueClass, breakpoints.get("3"));
386 BreakpointRequest bkpt3 = breakpointHandler.addBreakpoint(loc3, null);
387 BreakpointRequest bkpt3_v1 = v1 == null ? null : breakpointHandler.addBreakpoint(loc3, v1);
388 BreakpointRequest bkpt3_v2 = v2 == null ? null : breakpointHandler.addBreakpoint(loc3, v2);
389 BreakpointRequest bkpt3_v3 = v3 == null ? null : breakpointHandler.addBreakpoint(loc3, v3);
390
391 // Go through all breakpoints.
392 breakpointHandler.resumeTo(locDone);
393
394 System.out.println("DONE");
395
396 // Analyze gathered data depending on the testcase.
397 if (v1 == null && v2 == null && v3 == null) {
398 // No other references.
399 // ObjectID is generated at the 1st breakpoint (reference to heap object being constructed),
400 // and later we get the same oop (although it's content is changing).
401 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1), breakpointHandler.thisAtBreakpoint(bkpt2));
402 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2), breakpointHandler.thisAtBreakpoint(bkpt3));
403 // There is no breakpoints with instance filter.
404 } else if (v1 != null && v2 != null && v3 != null) {
405 // Existing references to value objects with the same content as the object being constructed.
406 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1), v1);
407 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v1), v1);
408 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v2), null);
409 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v3), null);
410 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2), v2);
411 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2_v1), null);
412 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2_v2), v2);
413 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt2_v3), null);
414 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3), v3);
415 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v1), null);
416 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v2), null);
417 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v3), v3);
418 } else if (v1 != null && v2 == null && v3 != null) {
419 // At 2nd breakpoint new ObjectID is generated.
420 ObjectReference thisAt2 = breakpointHandler.thisAtBreakpoint(bkpt2);
421 assertNotEquals(thisAt2, null);
422 assertNotEquals(thisAt2, v1);
423 // Now thisAt2 has the same content as v3.
424 assertEquals(thisAt2, v3);
425 // At breakpoint 1 this == v1.
426 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1), v1);
427 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v1), v1);
428 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt1_v3), null);
429 // At breakpoint 3 this == v3.
430 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3), v3);
431 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v1), null);
432 assertEquals(breakpointHandler.thisAtBreakpoint(bkpt3_v3), v3);
433 } else {
434 throw new RuntimeException("Unknown test case");
435 }
436
437 resumeToVMDisconnect();
438 }
439 }