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 }