559826c292
Reviewed-by: vromero
343 lines
12 KiB
Java
343 lines
12 KiB
Java
/*
|
|
* Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
package toolbox;
|
|
|
|
import java.io.BufferedReader;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.io.PrintStream;
|
|
import java.io.PrintWriter;
|
|
import java.io.StringWriter;
|
|
import java.nio.file.Path;
|
|
import java.util.EnumMap;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.function.BiConsumer;
|
|
import static toolbox.ToolBox.lineSeparator;
|
|
|
|
/**
|
|
* A utility base class to simplify the implementation of tasks.
|
|
* Provides support for running the task in a process and for
|
|
* capturing output written by the task to stdout, stderr and
|
|
* other writers where applicable.
|
|
* @param <T> the implementing subclass
|
|
*/
|
|
abstract class AbstractTask<T extends AbstractTask<T>> implements Task {
|
|
protected final ToolBox toolBox;
|
|
protected final Mode mode;
|
|
private final Map<OutputKind, String> redirects = new EnumMap<>(OutputKind.class);
|
|
private final Map<String, String> envVars = new HashMap<>();
|
|
private Expect expect = Expect.SUCCESS;
|
|
//validator for exit codes, first parameter is the exit code
|
|
//the second the test name:
|
|
private BiConsumer<Integer, String> exitCodeValidator = null;
|
|
|
|
/**
|
|
* Create a task that will execute in the specified mode.
|
|
* @param mode the mode
|
|
*/
|
|
protected AbstractTask(ToolBox tb, Mode mode) {
|
|
toolBox = tb;
|
|
this.mode = mode;
|
|
}
|
|
|
|
/**
|
|
* Sets the expected outcome of the task and calls {@code run()}.
|
|
* @param expect the expected outcome
|
|
* @return the result of calling {@code run()}
|
|
*/
|
|
public Result run(Expect expect) {
|
|
expect(expect, (_, _) -> {});
|
|
return run();
|
|
}
|
|
|
|
/**
|
|
* Sets the expected outcome of the task and calls {@code run()}.
|
|
* @param expect the expected outcome
|
|
* @param exitCode the expected exit code if the expected outcome
|
|
* is {@code FAIL}
|
|
* @return the result of calling {@code run()}
|
|
*/
|
|
public Result run(Expect expect, int exitCode) {
|
|
expect(expect, exitCode);
|
|
return run();
|
|
}
|
|
|
|
/**
|
|
* Sets the expected outcome of the task and calls {@code run()}.
|
|
* @param expect the expected outcome
|
|
* @param exitCodeValidator an exit code validator. The first parameter will
|
|
* be the actual exit code, the second test name,
|
|
* should throw TaskError if the exit code is not
|
|
* as expected. Only used if the expected outcome
|
|
* is {@code FAIL}
|
|
* @return the result of calling {@code run()}
|
|
*/
|
|
public Result run(Expect expect,
|
|
BiConsumer<Integer, String> exitCodeValidator) {
|
|
expect(expect, exitCodeValidator);
|
|
return run();
|
|
}
|
|
|
|
/**
|
|
* Sets the expected outcome and expected exit code of the task.
|
|
* The exit code will not be checked if the outcome is
|
|
* {@code Expect.SUCCESS} or if the exit code is set to
|
|
* {@code Integer.MIN_VALUE}.
|
|
* @param expect the expected outcome
|
|
* @param expectedExitCode the expected exit code
|
|
*/
|
|
protected void expect(Expect expect, int expectedExitCode) {
|
|
expect(expect, (exitCode, testName) -> {
|
|
if (expectedExitCode != Integer.MIN_VALUE &&
|
|
exitCode != expectedExitCode) {
|
|
throw new TaskError("Task " + testName + "failed with unexpected exit code "
|
|
+ exitCode + ", expected " + expectedExitCode);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets the expected outcome and expected exit code of the task.
|
|
* The exit code will not be checked if the outcome is
|
|
* {@code Expect.SUCCESS} or if the exit code is set to
|
|
* {@code Integer.MIN_VALUE}.
|
|
* @param expect the expected outcome
|
|
* @param exitCodeValidator an exit code validator. The first parameter will
|
|
* be the actual exit code, the second test name,
|
|
* should throw TaskError if the exit code is not
|
|
* as expected. Only used if the expected outcome
|
|
* is {@code FAIL}
|
|
*/
|
|
protected void expect(Expect expect,
|
|
BiConsumer<Integer, String> exitCodeValidator) {
|
|
this.expect = expect;
|
|
this.exitCodeValidator = exitCodeValidator;
|
|
}
|
|
|
|
/**
|
|
* Checks the exit code contained in a {@code Result} against the
|
|
* expected outcome and exit value
|
|
* @param result the result object
|
|
* @return the result object
|
|
* @throws TaskError if the exit code stored in the result object
|
|
* does not match the expected outcome and exit code.
|
|
*/
|
|
protected Result checkExit(Result result) throws TaskError {
|
|
switch (expect) {
|
|
case SUCCESS:
|
|
if (result.exitCode != 0) {
|
|
result.writeAll();
|
|
throw new TaskError("Task " + name() + " failed: rc=" + result.exitCode);
|
|
}
|
|
break;
|
|
|
|
case FAIL:
|
|
if (result.exitCode == 0) {
|
|
result.writeAll();
|
|
throw new TaskError("Task " + name() + " succeeded unexpectedly");
|
|
}
|
|
|
|
try {
|
|
exitCodeValidator.accept(result.exitCode, name());
|
|
} catch (Throwable t) {
|
|
result.writeAll();
|
|
throw t;
|
|
}
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Sets an environment variable to be used by this task.
|
|
* @param name the name of the environment variable
|
|
* @param value the value for the environment variable
|
|
* @return this task object
|
|
* @throws IllegalStateException if the task mode is not {@code EXEC}
|
|
*/
|
|
public T envVar(String name, String value) {
|
|
if (mode != Mode.EXEC)
|
|
throw new IllegalStateException();
|
|
envVars.put(name, value);
|
|
return (T) this;
|
|
}
|
|
|
|
/**
|
|
* Redirects output from an output stream to a file.
|
|
* @param outputKind the name of the stream to be redirected.
|
|
* @param path the file
|
|
* @return this task object
|
|
* @throws IllegalStateException if the task mode is not {@code EXEC}
|
|
*/
|
|
public T redirect(OutputKind outputKind, String path) {
|
|
if (mode != Mode.EXEC)
|
|
throw new IllegalStateException();
|
|
redirects.put(outputKind, path);
|
|
return (T) this;
|
|
}
|
|
|
|
/**
|
|
* Returns a {@code ProcessBuilder} initialized with any
|
|
* redirects and environment variables that have been set.
|
|
* @return a {@code ProcessBuilder}
|
|
*/
|
|
protected ProcessBuilder getProcessBuilder() {
|
|
if (mode != Mode.EXEC)
|
|
throw new IllegalStateException();
|
|
ProcessBuilder pb = new ProcessBuilder();
|
|
if (redirects.get(OutputKind.STDOUT) != null)
|
|
pb.redirectOutput(Path.of(redirects.get(OutputKind.STDOUT)).toFile());
|
|
if (redirects.get(OutputKind.STDERR) != null)
|
|
pb.redirectError(Path.of(redirects.get(OutputKind.STDERR)).toFile());
|
|
pb.environment().putAll(envVars);
|
|
return pb;
|
|
}
|
|
|
|
/**
|
|
* Collects the output from a process and saves it in a {@code Result}.
|
|
* @param tb the {@code ToolBox} containing the task {@code t}
|
|
* @param t the task initiating the process
|
|
* @param p the process
|
|
* @return a Result object containing the output from the process and its
|
|
* exit value.
|
|
* @throws InterruptedException if the thread is interrupted
|
|
*/
|
|
protected Result runProcess(ToolBox tb, Task t, Process p) throws InterruptedException {
|
|
if (mode != Mode.EXEC)
|
|
throw new IllegalStateException();
|
|
ProcessOutput sysOut = new ProcessOutput(p.getInputStream()).start();
|
|
ProcessOutput sysErr = new ProcessOutput(p.getErrorStream()).start();
|
|
sysOut.waitUntilDone();
|
|
sysErr.waitUntilDone();
|
|
int rc = p.waitFor();
|
|
Map<OutputKind, String> outputMap = new EnumMap<>(OutputKind.class);
|
|
outputMap.put(OutputKind.STDOUT, sysOut.getOutput());
|
|
outputMap.put(OutputKind.STDERR, sysErr.getOutput());
|
|
return checkExit(new Result(toolBox, t, rc, outputMap));
|
|
}
|
|
|
|
/**
|
|
* Thread-friendly class to read the output from a process until the stream
|
|
* is exhausted.
|
|
*/
|
|
static class ProcessOutput implements Runnable {
|
|
ProcessOutput(InputStream from) {
|
|
in = new BufferedReader(new InputStreamReader(from));
|
|
out = new StringBuilder();
|
|
}
|
|
|
|
ProcessOutput start() {
|
|
new Thread(this).start();
|
|
return this;
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
String line;
|
|
while ((line = in.readLine()) != null) {
|
|
out.append(line).append(lineSeparator);
|
|
}
|
|
} catch (IOException e) {
|
|
}
|
|
synchronized (this) {
|
|
done = true;
|
|
notifyAll();
|
|
}
|
|
}
|
|
|
|
synchronized void waitUntilDone() throws InterruptedException {
|
|
boolean interrupted = false;
|
|
|
|
// poll interrupted flag, while waiting for copy to complete
|
|
while (!(interrupted = Thread.interrupted()) && !done)
|
|
wait(1000);
|
|
|
|
if (interrupted)
|
|
throw new InterruptedException();
|
|
}
|
|
|
|
String getOutput() {
|
|
return out.toString();
|
|
}
|
|
|
|
private final BufferedReader in;
|
|
private final StringBuilder out;
|
|
private boolean done;
|
|
}
|
|
|
|
/**
|
|
* Utility class to simplify the handling of temporarily setting a
|
|
* new stream for System.out or System.err.
|
|
*/
|
|
static class StreamOutput {
|
|
// Functional interface to set a stream.
|
|
// Expected use: System::setOut, System::setErr
|
|
interface Initializer {
|
|
void set(PrintStream s);
|
|
}
|
|
|
|
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
private final PrintStream ps = new PrintStream(baos);
|
|
private final PrintStream prev;
|
|
private final Initializer init;
|
|
|
|
StreamOutput(PrintStream s, Initializer init) {
|
|
prev = s;
|
|
init.set(ps);
|
|
this.init = init;
|
|
}
|
|
|
|
/**
|
|
* Closes the stream and returns the contents that were written to it.
|
|
* @return the contents that were written to it.
|
|
*/
|
|
String close() {
|
|
init.set(prev);
|
|
ps.close();
|
|
return baos.toString();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility class to simplify the handling of creating an in-memory PrintWriter.
|
|
*/
|
|
static class WriterOutput {
|
|
private final StringWriter sw = new StringWriter();
|
|
final PrintWriter pw = new PrintWriter(sw);
|
|
|
|
/**
|
|
* Closes the stream and returns the contents that were written to it.
|
|
* @return the contents that were written to it.
|
|
*/
|
|
String close() {
|
|
pw.close();
|
|
return sw.toString();
|
|
}
|
|
}
|
|
}
|