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 }