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 }