1 /*
   2  * Copyright (c) 2016 Red Hat Inc.
   3  *
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * This code is free software; you can redistribute it and/or modify it
   7  * under the terms of the GNU General Public License version 2 only, as
   8  * published by the Free Software Foundation.
   9  *
  10  * This code is distributed in the hope that it will be useful, but WITHOUT
  11  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  12  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  13  * version 2 for more details (a copy is included in the LICENSE file that
  14  * accompanied this code).
  15  *
  16  * You should have received a copy of the GNU General Public License version
  17  * 2 along with this work; if not, write to the Free Software Foundation,
  18  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  19  *
  20  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  21  * or visit www.oracle.com if you need additional information or have any
  22  * questions.
  23  */
  24 
  25 /**
  26  *  @test
  27  *  @bug 8153711
  28  *  @summary JDWP: Memory Leak (global references not deleted after invokeMethod).
  29  *
  30  *  @author Severin Gehwolf <sgehwolf@redhat.com>
  31  *
  32  *  @requires vm.gc != "Z"
  33  *  @library ..
  34  *  @run build TestScaffold VMConnection TargetListener TargetAdapter
  35  *  @run compile -g OomDebugTest.java
  36  *  @run main OomDebugTest OomDebugTestTarget test1
  37  *  @run main OomDebugTest OomDebugTestTarget test2
  38  *  @run main OomDebugTest OomDebugTestTarget test3
  39  *  @run main OomDebugTest OomDebugTestTarget test4
  40  *  @run main OomDebugTest OomDebugTestTarget test5
  41  */
  42 import java.io.File;
  43 import java.io.FileInputStream;
  44 import java.io.FileNotFoundException;
  45 import java.io.FileOutputStream;
  46 import java.io.IOException;
  47 import java.util.ArrayList;
  48 import java.util.Arrays;
  49 import java.util.Collections;
  50 import java.util.HashSet;
  51 import java.util.List;
  52 import java.util.Properties;
  53 import java.util.Set;
  54 
  55 import com.sun.jdi.ArrayReference;
  56 import com.sun.jdi.ArrayType;
  57 import com.sun.jdi.ClassType;
  58 import com.sun.jdi.Field;
  59 import com.sun.jdi.InvocationException;
  60 import com.sun.jdi.Method;
  61 import com.sun.jdi.ObjectReference;
  62 import com.sun.jdi.ReferenceType;
  63 import com.sun.jdi.StackFrame;
  64 import com.sun.jdi.VMOutOfMemoryException;
  65 import com.sun.jdi.Value;
  66 import com.sun.jdi.event.BreakpointEvent;
  67 import com.sun.jdi.event.ExceptionEvent;
  68 
  69 /***************** Target program **********************/
  70 
  71 class OomDebugTestTarget {
  72 
  73     OomDebugTestTarget() {
  74         System.out.println("DEBUG: invoked constructor");
  75     }
  76     static class FooCls {
  77         @SuppressWarnings("unused")
  78         private byte[] bytes = new byte[3000000];
  79     };
  80 
  81     FooCls fooCls = new FooCls();
  82     byte[] byteArray = new byte[0];
  83 
  84     void testMethod(FooCls foo) {
  85         System.out.println("DEBUG: invoked 'void testMethod(FooCls)', foo == " + foo);
  86     }
  87 
  88     void testPrimitive(byte[] foo) {
  89         System.out.println("DEBUG: invoked 'void testPrimitive(byte[])', foo == " + foo);
  90     }
  91 
  92     byte[] testPrimitiveArrRetval() {
  93         System.out.println("DEBUG: invoked 'byte[] testPrimitiveArrRetval()'");
  94         return new byte[3000000];
  95     }
  96 
  97     FooCls testFooClsRetval() {
  98         System.out.println("DEBUG: invoked 'FooCls testFooClsRetval()'");
  99         return new FooCls();
 100     }
 101 
 102     public void entry() {}
 103 
 104     public static void main(String[] args){
 105         System.out.println("DEBUG: OomDebugTestTarget.main");
 106         new OomDebugTestTarget().entry();
 107     }
 108 }
 109 
 110 /***************** Test program ************************/
 111 
 112 public class OomDebugTest extends TestScaffold {
 113 
 114     private static final String[] ALL_TESTS = new String[] {
 115             "test1", "test2", "test3", "test4", "test5"
 116     };
 117     private static final Set<String> ALL_TESTS_SET = new HashSet<String>();
 118     static {
 119         ALL_TESTS_SET.addAll(Arrays.asList(ALL_TESTS));
 120     }
 121     private static final String TEST_CLASSES = System.getProperty("test.classes", ".");
 122     private static final File RESULT_FILE = new File(TEST_CLASSES, "results.properties");
 123     private static final String LAST_TEST = ALL_TESTS[ALL_TESTS.length - 1];
 124     private ReferenceType targetClass;
 125     private ObjectReference thisObject;
 126     private int failedTests;
 127     private final String testMethod;
 128 
 129     public OomDebugTest(String[] args) {
 130         super(args);
 131         if (args.length != 2) {
 132             throw new RuntimeException("Test failed unexpectedly.");
 133         }
 134         this.testMethod = args[1];
 135     }
 136 
 137     @Override
 138     protected void runTests() throws Exception {
 139         try {
 140             addListener(new TargetAdapter() {
 141 
 142                 @Override
 143                 public void exceptionThrown(ExceptionEvent event) {
 144                     String name = event.exception().referenceType().name();
 145                     System.err.println("DEBUG: Exception thrown in debuggee was: " + name);
 146                 }
 147             });
 148             /*
 149              * Get to the top of entry()
 150              * to determine targetClass and mainThread
 151              */
 152             BreakpointEvent bpe = startTo("OomDebugTestTarget", "entry", "()V");
 153             targetClass = bpe.location().declaringType();
 154 
 155             mainThread = bpe.thread();
 156 
 157             StackFrame frame = mainThread.frame(0);
 158             thisObject = frame.thisObject();
 159             java.lang.reflect.Method m = findTestMethod();
 160             m.invoke(this);
 161         } catch (NoSuchMethodException e) {
 162             e.printStackTrace();
 163             failure();
 164         } catch (SecurityException e) {
 165             e.printStackTrace();
 166             failure();
 167         }
 168         /*
 169          * resume the target, listening for events
 170          */
 171         listenUntilVMDisconnect();
 172     }
 173 
 174     private java.lang.reflect.Method findTestMethod()
 175             throws NoSuchMethodException, SecurityException {
 176         return OomDebugTest.class.getDeclaredMethod(testMethod);
 177     }
 178 
 179     private void failure() {
 180         failedTests++;
 181     }
 182 
 183     /*
 184      * Test case: Object reference as method parameter.
 185      */
 186     @SuppressWarnings("unused") // called via reflection
 187     private void test1() throws Exception {
 188         System.out.println("DEBUG: ------------> Running test1");
 189         try {
 190             Field field = targetClass.fieldByName("fooCls");
 191             ClassType clsType = (ClassType)field.type();
 192             Method constructor = getConstructorForClass(clsType);
 193             for (int i = 0; i < 15; i++) {
 194                 @SuppressWarnings({ "rawtypes", "unchecked" })
 195                 ObjectReference objRef = clsType.newInstance(mainThread,
 196                                                              constructor,
 197                                                              new ArrayList(0),
 198                                                              ObjectReference.INVOKE_NONVIRTUAL);
 199                 if (objRef.isCollected()) {
 200                     System.out.println("DEBUG: Object got GC'ed before we can use it. NO-OP.");
 201                     continue;
 202                 }
 203                 invoke("testMethod", "(LOomDebugTestTarget$FooCls;)V", objRef);
 204             }
 205         } catch (InvocationException e) {
 206             handleFailure(e);
 207         }
 208     }
 209 
 210     /*
 211      * Test case: Array reference as method parameter.
 212      */
 213     @SuppressWarnings("unused") // called via reflection
 214     private void test2() throws Exception {
 215         System.out.println("DEBUG: ------------> Running test2");
 216         try {
 217             Field field = targetClass.fieldByName("byteArray");
 218             ArrayType arrType = (ArrayType)field.type();
 219 
 220             for (int i = 0; i < 15; i++) {
 221                 ArrayReference byteArrayVal = arrType.newInstance(3000000);
 222                 if (byteArrayVal.isCollected()) {
 223                     System.out.println("DEBUG: Object got GC'ed before we can use it. NO-OP.");
 224                     continue;
 225                 }
 226                 invoke("testPrimitive", "([B)V", byteArrayVal);
 227             }
 228         } catch (VMOutOfMemoryException e) {
 229             defaultHandleOOMFailure(e);
 230         }
 231     }
 232 
 233     /*
 234      * Test case: Array reference as return value.
 235      */
 236     @SuppressWarnings("unused") // called via reflection
 237     private void test3() throws Exception {
 238         System.out.println("DEBUG: ------------> Running test3");
 239         try {
 240             for (int i = 0; i < 15; i++) {
 241                 invoke("testPrimitiveArrRetval",
 242                        "()[B",
 243                        Collections.EMPTY_LIST,
 244                        vm().mirrorOfVoid());
 245             }
 246         } catch (InvocationException e) {
 247             handleFailure(e);
 248         }
 249     }
 250 
 251     /*
 252      * Test case: Object reference as return value.
 253      */
 254     @SuppressWarnings("unused") // called via reflection
 255     private void test4() throws Exception {
 256         System.out.println("DEBUG: ------------> Running test4");
 257         try {
 258             for (int i = 0; i < 15; i++) {
 259                 invoke("testFooClsRetval",
 260                        "()LOomDebugTestTarget$FooCls;",
 261                        Collections.EMPTY_LIST,
 262                        vm().mirrorOfVoid());
 263             }
 264         } catch (InvocationException e) {
 265             handleFailure(e);
 266         }
 267     }
 268 
 269     /*
 270      * Test case: Constructor
 271      */
 272     @SuppressWarnings({ "unused", "unchecked", "rawtypes" }) // called via reflection
 273     private void test5() throws Exception {
 274         System.out.println("DEBUG: ------------> Running test5");
 275         try {
 276             ClassType type = (ClassType)thisObject.type();
 277             for (int i = 0; i < 15; i++) {
 278                 type.newInstance(mainThread,
 279                                  findMethod(targetClass, "<init>", "()V"),
 280                                  new ArrayList(0),
 281                                  ObjectReference.INVOKE_NONVIRTUAL);
 282             }
 283         } catch (InvocationException e) {
 284             handleFailure(e);
 285         }
 286     }
 287 
 288     private Method getConstructorForClass(ClassType clsType) {
 289         List<Method> methods = clsType.methodsByName("<init>");
 290         if (methods.size() != 1) {
 291             throw new RuntimeException("FAIL. Expected only one, the default, constructor");
 292         }
 293         return methods.get(0);
 294     }
 295 
 296     private void handleFailure(InvocationException e) {
 297         // There is no good way to see the OOME diagnostic message in the target since the
 298         // TestScaffold might throw an exception while trying to print the stack trace. I.e
 299         // it might get a a VMDisconnectedException before the stack trace printing finishes.
 300         System.err.println("FAILURE: InvocationException thrown. Trying to determine cause...");
 301         defaultHandleOOMFailure(e);
 302     }
 303 
 304     private void defaultHandleOOMFailure(Exception e) {
 305         e.printStackTrace();
 306         failure();
 307     }
 308 
 309     @SuppressWarnings({ "rawtypes", "unchecked" })
 310     void invoke(String methodName, String methodSig, Value value)
 311             throws Exception {
 312         List args = new ArrayList(1);
 313         args.add(value);
 314         invoke(methodName, methodSig, args, value);
 315     }
 316 
 317     void invoke(String methodName,
 318                 String methodSig,
 319                 @SuppressWarnings("rawtypes") List args,
 320                 Value value) throws Exception {
 321         Method method = findMethod(targetClass, methodName, methodSig);
 322         if ( method == null) {
 323             failure("FAILED: Can't find method: "
 324                     + methodName  + " for class = " + targetClass);
 325             return;
 326         }
 327         invoke(method, args, value);
 328     }
 329 
 330     @SuppressWarnings({ "rawtypes", "unchecked" })
 331     void invoke(Method method, List args, Value value) throws Exception {
 332         thisObject.invokeMethod(mainThread, method, args, 0);
 333         System.out.println("DEBUG: Done invoking method via debugger.");
 334     }
 335 
 336     Value fieldValue(String fieldName) {
 337         Field field = targetClass.fieldByName(fieldName);
 338         return thisObject.getValue(field);
 339     }
 340 
 341     // Determine the pass/fail status on some heuristic and don't fail the
 342     // test if < 3 of the total number of tests (currently 5) fail. This also
 343     // has the nice side effect that all tests are first attempted and only
 344     // all tests ran an overall pass/fail status is determined.
 345     private static void determineOverallTestStatus(OomDebugTest oomTest)
 346                                    throws IOException, FileNotFoundException {
 347         Properties resultProps = new Properties();
 348         if (!RESULT_FILE.exists()) {
 349             RESULT_FILE.createNewFile();
 350         }
 351         FileInputStream fin = null;
 352         try {
 353             fin = new FileInputStream(RESULT_FILE);
 354             resultProps.load(fin);
 355             resultProps.put(oomTest.testMethod,
 356                             Integer.toString(oomTest.failedTests));
 357         } finally {
 358             if (fin != null) {
 359                 fin.close();
 360             }
 361         }
 362         System.out.println("DEBUG: Finished running test '"
 363                            + oomTest.testMethod + "'.");
 364         if (LAST_TEST.equals(oomTest.testMethod)) {
 365             System.out.println("DEBUG: Determining overall test status.");
 366             Set<String> actualTestsRun = new HashSet<String>();
 367             int totalTests = ALL_TESTS.length;
 368             int failedTests = 0;
 369             for (Object key: resultProps.keySet()) {
 370                 actualTestsRun.add((String)key);
 371                 Object propVal = resultProps.get(key);
 372                 int value = Integer.parseInt((String)propVal);
 373                 failedTests += value;
 374             }
 375             if (!ALL_TESTS_SET.equals(actualTestsRun)) {
 376                 String errorMsg = "Test failed! Expected to run tests '"
 377                         + ALL_TESTS_SET + "', but only these were run '"
 378                         + actualTestsRun + "'";
 379                 throw new RuntimeException(errorMsg);
 380             }
 381             if (failedTests >= 3) {
 382                 String errorMsg = "Test failed. Expected < 3 sub-tests to fail "
 383                                   + "for a pass. Got " + failedTests
 384                                   + " failed tests out of " + totalTests + ".";
 385                 throw new RuntimeException(errorMsg);
 386             }
 387             RESULT_FILE.delete();
 388             System.out.println("All " + totalTests + " tests passed.");
 389         } else {
 390             System.out.println("DEBUG: More tests to run. Coninuing.");
 391             FileOutputStream fout = null;
 392             try {
 393                 fout = new FileOutputStream(RESULT_FILE);
 394                 resultProps.store(fout, "Storing results after test "
 395                                          + oomTest.testMethod);
 396             } finally {
 397                 if (fout != null) {
 398                     fout.close();
 399                 }
 400             }
 401         }
 402     }
 403 
 404     public static void main(String[] args) throws Exception {
 405         System.setProperty("test.vm.opts", "-Xmx40m"); // Set debuggee VM option
 406         OomDebugTest oomTest = new OomDebugTest(args);
 407         try {
 408             oomTest.startTests();
 409         } catch (Throwable e) {
 410             System.out.println("DEBUG: Got exception for test run. " + e);
 411             e.printStackTrace();
 412             oomTest.failure();
 413         }
 414         determineOverallTestStatus(oomTest);
 415     }
 416 
 417 }