1 /* 2 * Copyright (c) 2016, 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 jdk.testlibrary.tasks; 25 26 import java.io.BufferedReader; 27 import java.io.ByteArrayOutputStream; 28 import java.io.File; 29 import java.io.IOException; 30 import java.io.InputStream; 31 import java.io.InputStreamReader; 32 import java.io.PrintStream; 33 import java.io.PrintWriter; 34 import java.io.StringWriter; 35 import java.util.ArrayList; 36 import java.util.stream.Collectors; 37 import java.util.EnumMap; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 42 import jdk.testlibrary.JDKToolFinder; 43 44 /** 45 * A utility base class to simplify the implementation of tasks. 46 * Provides support for running the task in a process and for 47 * capturing output written by the task to stdout, stderr and 48 * other writers where applicable. 49 * @param <T> the implementing subclass 50 */ 51 abstract class AbstractTask<T extends AbstractTask<T>> implements Task { 52 protected final Mode mode; 53 private final Map<OutputKind, String> redirects = new EnumMap<>(OutputKind.class); 54 private String inputRedirect; 55 private final Map<String, String> envVars = new HashMap<>(); 56 private Expect expect = Expect.SUCCESS; 57 int expectedExitCode = 0; 58 59 /** 60 * Create a task that will execute in the specified mode. 61 * @param mode the mode 62 */ 63 protected AbstractTask(Mode mode) { 64 this.mode = mode; 65 } 66 67 /** 68 * Sets the expected outcome of the task and calls {@code run()}. 69 * @param expect the expected outcome 70 * @return the result of calling {@code run()} 71 */ 72 public Result run(Expect expect) { 73 expect(expect, Integer.MIN_VALUE); 74 return run(); 75 } 76 77 /** 78 * Sets the expected outcome of the task and calls {@code run()}. 79 * @param expect the expected outcome 80 * @param exitCode the expected exit code if the expected outcome 81 * is {@code FAIL} 82 * @return the result of calling {@code run()} 83 */ 84 public Result run(Expect expect, int exitCode) { 85 expect(expect, exitCode); 86 return run(); 87 } 88 89 /** 90 * Sets the expected outcome and expected exit code of the task. 91 * The exit code will not be checked if the outcome is 92 * {@code Expect.SUCCESS} or if the exit code is set to 93 * {@code Integer.MIN_VALUE}. 94 * @param expect the expected outcome 95 * @param exitCode the expected exit code 96 */ 97 protected void expect(Expect expect, int exitCode) { 98 this.expect = expect; 99 this.expectedExitCode = exitCode; 100 } 101 102 /** 103 * Checks the exit code contained in a {@code Result} against the 104 * expected outcome and exit value 105 * @param result the result object 106 * @return the result object 107 * @throws TaskError if the exit code stored in the result object 108 * does not match the expected outcome and exit code. 109 */ 110 protected Result checkExit(Result result) throws TaskError { 111 switch (expect) { 112 case SUCCESS: 113 if (result.exitCode != 0) { 114 result.writeAll(); 115 throw new TaskError("Task " + name() + " failed: rc=" + result.exitCode); 116 } 117 break; 118 119 case FAIL: 120 if (result.exitCode == 0) { 121 result.writeAll(); 122 throw new TaskError("Task " + name() + " succeeded unexpectedly"); 123 } 124 125 if (expectedExitCode != Integer.MIN_VALUE 126 && result.exitCode != expectedExitCode) { 127 result.writeAll(); 128 throw new TaskError("Task " + name() + "failed with unexpected exit code " 129 + result.exitCode + ", expected " + expectedExitCode); 130 } 131 break; 132 } 133 return result; 134 } 135 136 /** 137 * Sets an environment variable to be used by this task. 138 * @param name the name of the environment variable 139 * @param value the value for the environment variable 140 * @return this task object 141 * @throws IllegalStateException if the task mode is not {@code EXEC} 142 */ 143 public T envVar(String name, String value) { 144 if (mode != Mode.EXEC) 145 throw new IllegalStateException(); 146 envVars.put(name, value); 147 return (T) this; 148 } 149 150 /** 151 * Redirects output from an output stream to a file. 152 * @param outputKind the name of the stream to be redirected. 153 * @param path the file 154 * @return this task object 155 * @throws IllegalStateException if the task mode is not {@code EXEC} 156 */ 157 public T redirect(OutputKind outputKind, String path) { 158 if (mode != Mode.EXEC) 159 throw new IllegalStateException(); 160 redirects.put(outputKind, path); 161 return (T) this; 162 } 163 164 /** 165 * Redirects input to the process input stream from a file. 166 * @param path the file 167 * @return this task object 168 */ 169 public T redirectInput(String path) { 170 inputRedirect = path; 171 return (T)this; 172 } 173 174 /** 175 * Calls a Java tool launcher with some arguments. 176 * @param tool the name of the tool to run 177 * @param arguments the arguments 178 * @return a Result object indicating the outcome of the task 179 * and the content of any output written to stdout or stderr. 180 * @throws TaskError if the outcome of the task is not as expected. 181 */ 182 protected Task.Result run(Tool tool, List<String> arguments) { 183 List<String> args = new ArrayList<>(); 184 args.add(JDKToolFinder.getJDKTool(tool.name).toString()); 185 args.addAll(arguments); 186 ProcessBuilder pb = getProcessBuilder(); 187 pb.command(args); 188 try { 189 System.out.println("Running " + pb.command().stream().map(s -> "\"" + s + "\"").collect(Collectors.joining(","))); 190 return runProcess(this, pb.start()); 191 } catch (IOException | InterruptedException e) { 192 throw new Error(e); 193 } 194 } 195 196 /** 197 * Returns a {@code ProcessBuilder} initialized with any 198 * redirects and environment variables that have been set. 199 * @return a {@code ProcessBuilder} 200 */ 201 protected ProcessBuilder getProcessBuilder() { 202 if (mode != Mode.EXEC) 203 throw new IllegalStateException(); 204 ProcessBuilder pb = new ProcessBuilder(); 205 if (redirects.get(OutputKind.STDOUT) != null) 206 pb.redirectOutput(new File(redirects.get(OutputKind.STDOUT))); 207 if (redirects.get(OutputKind.STDERR) != null) 208 pb.redirectError(new File(redirects.get(OutputKind.STDERR))); 209 if (inputRedirect != null) 210 pb.redirectInput(new File(inputRedirect)); 211 pb.environment().putAll(envVars); 212 return pb; 213 } 214 215 /** 216 * Collects the output from a process and saves it in a {@code Result}. 217 * @param t the task initiating the process 218 * @param p the process 219 * @return a Result object containing the output from the process and its 220 * exit value. 221 * @throws InterruptedException if the thread is interrupted 222 */ 223 protected Result runProcess(Task t, Process p) throws InterruptedException { 224 if (mode != Mode.EXEC) 225 throw new IllegalStateException(); 226 ProcessOutput sysOut = new ProcessOutput(p.getInputStream()).start(); 227 ProcessOutput sysErr = new ProcessOutput(p.getErrorStream()).start(); 228 sysOut.waitUntilDone(); 229 sysErr.waitUntilDone(); 230 int rc = p.waitFor(); 231 Map<OutputKind, String> outputMap = new EnumMap<>(OutputKind.class); 232 outputMap.put(OutputKind.STDOUT, sysOut.getOutput()); 233 outputMap.put(OutputKind.STDERR, sysErr.getOutput()); 234 return checkExit(new Result(t, rc, outputMap)); 235 } 236 237 /** 238 * Thread-friendly class to read the output from a process until the stream 239 * is exhausted. 240 */ 241 static class ProcessOutput implements Runnable { 242 ProcessOutput(InputStream from) { 243 in = new BufferedReader(new InputStreamReader(from)); 244 out = new StringBuilder(); 245 } 246 247 ProcessOutput start() { 248 new Thread(this).start(); 249 return this; 250 } 251 252 @Override 253 public void run() { 254 try { 255 String line; 256 while ((line = in.readLine()) != null) { 257 out.append(line).append(lineSeparator); 258 } 259 } catch (IOException e) { 260 } 261 synchronized (this) { 262 done = true; 263 notifyAll(); 264 } 265 } 266 267 synchronized void waitUntilDone() throws InterruptedException { 268 boolean interrupted = false; 269 270 // poll interrupted flag, while waiting for copy to complete 271 while (!(interrupted = Thread.interrupted()) && !done) 272 wait(1000); 273 274 if (interrupted) 275 throw new InterruptedException(); 276 } 277 278 String getOutput() { 279 return out.toString(); 280 } 281 282 private final BufferedReader in; 283 private final StringBuilder out; 284 private boolean done; 285 } 286 287 /** 288 * Utility class to simplify the handling of temporarily setting a 289 * new stream for System.out or System.err. 290 */ 291 static class StreamOutput { 292 // Functional interface to set a stream. 293 // Expected use: System::setOut, System::setErr 294 interface Initializer { 295 void set(PrintStream s); 296 } 297 298 private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 299 private final PrintStream ps = new PrintStream(baos); 300 private final PrintStream prev; 301 private final Initializer init; 302 303 StreamOutput(PrintStream s, Initializer init) { 304 prev = s; 305 init.set(ps); 306 this.init = init; 307 } 308 309 /** 310 * Closes the stream and returns the contents that were written to it. 311 * @return the contents that were written to it. 312 */ 313 String close() { 314 init.set(prev); 315 ps.close(); 316 return baos.toString(); 317 } 318 } 319 320 /** 321 * Utility class to simplify the handling of creating an in-memory PrintWriter. 322 */ 323 static class WriterOutput { 324 private final StringWriter sw = new StringWriter(); 325 final PrintWriter pw = new PrintWriter(sw); 326 327 /** 328 * Closes the stream and returns the contents that were written to it. 329 * @return the contents that were written to it. 330 */ 331 String close() { 332 pw.close(); 333 return sw.toString(); 334 } 335 } 336 337 protected enum Tool { 338 JAVA("java"), JAR("jar"); 339 public final String name; 340 private Tool(String name) { 341 this.name = name; 342 } 343 } 344 }