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