1 /*
   2  * Copyright (c) 2018, 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 package lib.jdb;
  25 
  26 import jdk.test.lib.Utils;
  27 import jdk.test.lib.process.OutputAnalyzer;
  28 import jdk.test.lib.process.ProcessTools;
  29 
  30 import java.io.IOException;
  31 import java.nio.file.Files;
  32 import java.nio.file.Paths;
  33 import java.util.Arrays;
  34 import java.util.LinkedList;
  35 import java.util.List;
  36 import java.util.concurrent.TimeUnit;
  37 import java.util.concurrent.TimeoutException;
  38 import java.util.regex.Matcher;
  39 import java.util.regex.Pattern;
  40 import java.util.stream.Collectors;
  41 
  42 public abstract class JdbTest {
  43 
  44     public static class LaunchOptions {
  45         public final String debuggeeClass;
  46         public final List<String> debuggeeOptions = new LinkedList<>();
  47         public String sourceFilename;
  48         public final List<String> debuggerOptions = new LinkedList<>();
  49 
  50 
  51         public LaunchOptions(String debuggeeClass) {
  52             this.debuggeeClass = debuggeeClass;
  53         }
  54         public LaunchOptions addDebuggeeOption(String option) {
  55             debuggeeOptions.add(option);
  56             return this;
  57         }
  58         public LaunchOptions addDebuggeeOptions(String[] options) {
  59             debuggeeOptions.addAll(Arrays.asList(options));
  60             return this;
  61         }
  62         public LaunchOptions setSourceFilename(String name) {
  63             sourceFilename = name;
  64             return this;
  65         }
  66 
  67         public LaunchOptions addDebuggerOptions(String[] options) {
  68             debuggerOptions.addAll(Arrays.asList(options));
  69             return this;
  70         }
  71 
  72         public LaunchOptions addDebuggerOption(String option) {
  73             debuggerOptions.add(option);
  74             return this;
  75         }
  76 
  77     }
  78 
  79     public JdbTest(LaunchOptions launchOptions) {
  80         this.launchOptions = launchOptions;
  81     }
  82     public JdbTest(String debuggeeClass) {
  83         this(new LaunchOptions(debuggeeClass));
  84     }
  85 
  86     // sourceFilename is used by setBreakpoints and redefineClass
  87     public JdbTest(String debuggeeClass, String sourceFilename) {
  88         this(new LaunchOptions(debuggeeClass).setSourceFilename(sourceFilename));
  89     }
  90 
  91     protected Jdb jdb;
  92     protected Process debuggee;
  93     private final List<String> debuggeeOutput = new LinkedList<>();
  94     private final LaunchOptions launchOptions;
  95 
  96     // returns the whole jdb output as a string
  97     public String getJdbOutput() {
  98         return jdb == null ? "" : jdb.getJdbOutput();
  99     }
 100 
 101     // returns the whole debuggee output as a string
 102     public String getDebuggeeOutput() {
 103         return debuggeeOutput.stream().collect(Collectors.joining(lineSeparator));
 104     }
 105 
 106     public void run() {
 107         try {
 108             setup();
 109             runCases();
 110         } catch (Throwable e) {
 111             jdb.log("=======================================");
 112             jdb.log("Exception thrown during test execution: " + e.getMessage());
 113             jdb.log("=======================================");
 114             throw e;
 115         } finally {
 116             shutdown();
 117         }
 118     }
 119 
 120     protected void setup() {
 121         /* run debuggee as:
 122             java -agentlib:jdwp=transport=dt_socket,address=0,server=n,suspend=y <debuggeeClass>
 123         it reports something like : Listening for transport dt_socket at address: 60810
 124         after that connect jdb by:
 125             jdb -connect com.sun.jdi.SocketAttach:port=60810
 126         */
 127         // launch debuggee
 128         List<String> debuggeeArgs = new LinkedList<>();
 129         // specify address=0 to automatically select free port
 130         debuggeeArgs.add("-agentlib:jdwp=transport=dt_socket,address=0,server=y,suspend=y");
 131         debuggeeArgs.addAll(launchOptions.debuggeeOptions);
 132         debuggeeArgs.add(launchOptions.debuggeeClass);
 133         ProcessBuilder pbDebuggee = ProcessTools.createJavaProcessBuilder(true, debuggeeArgs.toArray(new String[0]));
 134 
 135         // debuggeeListen[0] - transport, debuggeeListen[1] - address
 136         String[] debuggeeListen = new String[2];
 137         Pattern listenRegexp = Pattern.compile("Listening for transport \\b(.+)\\b at address: \\b(\\d+)\\b");
 138         try {
 139             debuggee = ProcessTools.startProcess("debuggee", pbDebuggee,
 140                     s -> debuggeeOutput.add(s),  // output consumer
 141                     s -> {  // warm-up predicate
 142                         Matcher m = listenRegexp.matcher(s);
 143                         if (!m.matches()) {
 144                             return false;
 145                         }
 146                         debuggeeListen[0] = m.group(1);
 147                         debuggeeListen[1] = m.group(2);
 148                         return true;
 149                     },
 150                     30, TimeUnit.SECONDS);
 151         } catch (IOException | InterruptedException | TimeoutException ex) {
 152             throw new RuntimeException("failed to launch debuggee", ex);
 153         }
 154 
 155         // launch jdb
 156         try {
 157             List<String> jdbOptions = new LinkedList<>();
 158             jdbOptions.add("-connect");
 159             jdbOptions.add("com.sun.jdi.SocketAttach:port=" + debuggeeListen[1]);
 160             jdbOptions.addAll(launchOptions.debuggerOptions);
 161 
 162             jdb = new Jdb(jdbOptions);
 163         } catch (Throwable ex) {
 164             // terminate debuggee if something went wrong
 165             debuggee.destroy();
 166             throw ex;
 167         }
 168         // wait while jdb is initialized
 169         jdb.waitForPrompt(1, false);
 170     }
 171 
 172     protected abstract void runCases();
 173 
 174     protected void shutdown() {
 175         if (jdb != null) {
 176             jdb.shutdown();
 177         }
 178         // shutdown debuggee
 179         if (debuggee != null && debuggee.isAlive()) {
 180             try {
 181                 debuggee.waitFor(Utils.adjustTimeout(10), TimeUnit.SECONDS);
 182             } catch (InterruptedException e) {
 183                 // ignore
 184             } finally {
 185                 if (debuggee.isAlive()) {
 186                     debuggee.destroy();
 187                 }
 188             }
 189         }
 190     }
 191 
 192     protected static final String lineSeparator = System.getProperty("line.separator");
 193 
 194 
 195     // Parses the specified source file for "@{id} breakpoint" tags and returns
 196     // list of the line numbers containing the tag.
 197     // Example:
 198     //   System.out.println("BP is here");  // @1 breakpoint
 199     public static List<Integer> parseBreakpoints(String filePath, int id) {
 200         final String pattern = "@" + id + " breakpoint";
 201         int lineNum = 1;
 202         List<Integer> result = new LinkedList<>();
 203         try {
 204             for (String line: Files.readAllLines(Paths.get(filePath))) {
 205                 if (line.contains(pattern)) {
 206                     result.add(lineNum);
 207                 }
 208                 lineNum++;
 209             }
 210         } catch (IOException ex) {
 211             throw new RuntimeException("failed to parse " + filePath, ex);
 212         }
 213         return result;
 214     }
 215 
 216     // sets breakpoints to the lines parsed by {@code parseBreakpoints}
 217     // returns number of the breakpoints set.
 218     public static int setBreakpoints(Jdb jdb, String debuggeeClass, String sourcePath, int id) {
 219         List<Integer> bps = parseBreakpoints(sourcePath, id);
 220         for (int bp : bps) {
 221             String reply = jdb.command(JdbCommand.stopAt(debuggeeClass, bp)).stream()
 222                     .collect(Collectors.joining("\n"));
 223             if (reply.contains("Unable to set")) {
 224                 throw new RuntimeException("jdb failed to set breakpoint at " + debuggeeClass + ":" + bp);
 225             }
 226 
 227         }
 228         return bps.size();
 229     }
 230 
 231     // sets breakpoints to the lines parsed by {@code parseBreakpoints}
 232     // from the file from test source directory.
 233     // returns number of the breakpoints set.
 234     protected int setBreakpointsFromTestSource(String debuggeeFileName, int id) {
 235         return setBreakpoints(jdb, launchOptions.debuggeeClass,
 236                               getTestSourcePath(debuggeeFileName), id);
 237     }
 238 
 239     // sets breakpoints in the class {@code launchOptions.debuggeeClass}
 240     // to the lines parsed by {@code parseBreakpoints}
 241     // from the file from test source directory specified by {@code launchOptions.sourceFilename}.
 242     // returns number of the breakpoints set.
 243     protected int setBreakpoints(int id) {
 244         verifySourceFilename();
 245         return setBreakpointsFromTestSource(launchOptions.sourceFilename, id);
 246     }
 247 
 248     // transforms class with the specified id (see {@code ClassTransformer})
 249     // and executes "redefine" jdb command for {@code launchOptions.debuggeeClass}.
 250     // returns reply for the command.
 251     protected List<String> redefineClass(int id, String... compilerOptions) {
 252         verifySourceFilename();
 253         String transformedClassFile = ClassTransformer.fromTestSource(launchOptions.sourceFilename)
 254                 .transform(id, launchOptions.debuggeeClass, compilerOptions);
 255         return jdb.command(JdbCommand.redefine(launchOptions.debuggeeClass, transformedClassFile));
 256     }
 257 
 258     // gets full test source path for the given test filename
 259     public static String getTestSourcePath(String fileName) {
 260         return Paths.get(System.getProperty("test.src")).resolve(fileName).toString();
 261     }
 262 
 263     // verifies that sourceFilename is specified in ctor
 264     private void verifySourceFilename() {
 265         if (launchOptions.sourceFilename == null) {
 266             throw new RuntimeException("launchOptions.sourceFilename must be specified.");
 267         }
 268     }
 269 
 270     protected OutputAnalyzer execCommand(JdbCommand cmd) {
 271         List<String> reply = jdb.command(cmd);
 272         return new OutputAnalyzer(reply.stream().collect(Collectors.joining(lineSeparator)));
 273     }
 274 
 275     // helpers for "eval" jdb command.
 276     // executes "eval <expr>" and verifies output contains the specified text
 277     protected void evalShouldContain(String expr, String expectedString) {
 278         execCommand(JdbCommand.eval(expr))
 279                 .shouldContain(expectedString);
 280     }
 281     // executes "eval <expr>" and verifies output does not contain the specified text
 282     protected void evalShouldNotContain(String expr, String unexpectedString) {
 283         execCommand(JdbCommand.eval(expr))
 284                 .shouldNotContain(unexpectedString);
 285     }
 286 }