| /* | |
| * Copyright (c) 2013, 2024, 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(); | |
| } | |
| } | |
| } |