--- /dev/null 2019-11-13 18:31:07.000000000 -0500 +++ new/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java 2019-11-13 18:31:04.491265700 -0500 @@ -0,0 +1,364 @@ +/* + * Copyright (c) 2019, 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 jdk.jpackage.test; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.StringReader; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Pattern; +import java.util.spi.ToolProvider; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import jdk.jpackage.test.Functional.ThrowingSupplier; + +public final class Executor extends CommandArguments { + + public Executor() { + saveOutputType = new HashSet<>(Set.of(SaveOutputType.NONE)); + } + + public Executor setExecutable(String v) { + return setExecutable(Path.of(v)); + } + + public Executor setExecutable(Path v) { + executable = Objects.requireNonNull(v); + toolProvider = null; + return this; + } + + public Executor setToolProvider(ToolProvider v) { + toolProvider = Objects.requireNonNull(v); + executable = null; + return this; + } + + public Executor setToolProvider(JavaTool v) { + return setToolProvider(v.asToolProvider()); + } + + public Executor setDirectory(Path v) { + directory = v; + return this; + } + + public Executor setExecutable(JavaTool v) { + return setExecutable(v.getPath()); + } + + /** + * Configures this instance to save full output that command will produce. + * This function is mutual exclusive with + * saveFirstLineOfOutput() function. + * + * @return this + */ + public Executor saveOutput() { + saveOutputType.remove(SaveOutputType.FIRST_LINE); + saveOutputType.add(SaveOutputType.FULL); + return this; + } + + /** + * Configures how to save output that command will produce. If + * v is true, the function call is equivalent to + * saveOutput() call. If v is false, + * the function will result in not preserving command output. + * + * @return this + */ + public Executor saveOutput(boolean v) { + if (v) { + saveOutput(); + } else { + saveOutputType.remove(SaveOutputType.FIRST_LINE); + saveOutputType.remove(SaveOutputType.FULL); + } + return this; + } + + /** + * Configures this instance to save only the first line out output that + * command will produce. This function is mutual exclusive with + * saveOutput() function. + * + * @return this + */ + public Executor saveFirstLineOfOutput() { + saveOutputType.add(SaveOutputType.FIRST_LINE); + saveOutputType.remove(SaveOutputType.FULL); + return this; + } + + /** + * Configures this instance to dump all output that command will produce to + * System.out and System.err. Can be used together with saveOutput() and + * saveFirstLineOfOutput() to save command output and also copy it in the + * default output streams. + * + * @return this + */ + public Executor dumpOutput() { + return dumpOutput(true); + } + + public Executor dumpOutput(boolean v) { + if (v) { + saveOutputType.add(SaveOutputType.DUMP); + } else { + saveOutputType.remove(SaveOutputType.DUMP); + } + return this; + } + + public class Result { + + Result(int exitCode) { + this.exitCode = exitCode; + } + + public String getFirstLineOfOutput() { + return output.get(0); + } + + public List getOutput() { + return output; + } + + public String getPrintableCommandLine() { + return Executor.this.getPrintableCommandLine(); + } + + public Result assertExitCodeIs(int expectedExitCode) { + TKit.assertEquals(expectedExitCode, exitCode, String.format( + "Check command %s exited with %d code", + getPrintableCommandLine(), expectedExitCode)); + return this; + } + + public Result assertExitCodeIsZero() { + return assertExitCodeIs(0); + } + + final int exitCode; + private List output; + } + + public Result execute() { + if (toolProvider != null && directory != null) { + throw new IllegalArgumentException( + "Can't change directory when using tool provider"); + } + + return ThrowingSupplier.toSupplier(() -> { + if (toolProvider != null) { + return runToolProvider(); + } + + if (executable != null) { + return runExecutable(); + } + + throw new IllegalStateException("No command to execute"); + }).get(); + } + + public String executeAndGetFirstLineOfOutput() { + return saveFirstLineOfOutput().execute().assertExitCodeIsZero().getFirstLineOfOutput(); + } + + public List executeAndGetOutput() { + return saveOutput().execute().assertExitCodeIsZero().getOutput(); + } + + private boolean withSavedOutput() { + return saveOutputType.contains(SaveOutputType.FULL) || saveOutputType.contains( + SaveOutputType.FIRST_LINE); + } + + private Path executablePath() { + if (directory == null || executable.isAbsolute()) { + return executable; + } + + // If relative path to executable is used it seems to be broken when + // ProcessBuilder changes the directory. On Windows it changes the + // directory first and on Linux it looks up for executable before + // changing the directory. So to stay of safe side, use absolute path + // to executable. + return executable.toAbsolutePath(); + } + + private Result runExecutable() throws IOException, InterruptedException { + List command = new ArrayList<>(); + command.add(executablePath().toString()); + command.addAll(args); + ProcessBuilder builder = new ProcessBuilder(command); + StringBuilder sb = new StringBuilder(getPrintableCommandLine()); + if (withSavedOutput()) { + builder.redirectErrorStream(true); + sb.append("; save output"); + } else if (saveOutputType.contains(SaveOutputType.DUMP)) { + builder.inheritIO(); + sb.append("; inherit I/O"); + } else { + builder.redirectError(ProcessBuilder.Redirect.DISCARD); + builder.redirectOutput(ProcessBuilder.Redirect.DISCARD); + sb.append("; discard I/O"); + } + if (directory != null) { + builder.directory(directory.toFile()); + sb.append(String.format("; in directory [%s]", directory)); + } + + TKit.trace("Execute " + sb.toString() + "..."); + Process process = builder.start(); + + List outputLines = null; + if (withSavedOutput()) { + try (BufferedReader outReader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + if (saveOutputType.contains(SaveOutputType.DUMP) + || saveOutputType.contains(SaveOutputType.FULL)) { + outputLines = outReader.lines().collect(Collectors.toList()); + } else { + outputLines = Arrays.asList( + outReader.lines().findFirst().orElse(null)); + } + } finally { + if (saveOutputType.contains(SaveOutputType.DUMP) && outputLines != null) { + outputLines.stream().forEach(System.out::println); + if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { + // Pick the first line of saved output if there is one + for (String line: outputLines) { + outputLines = List.of(line); + break; + } + } + } + } + } + + Result reply = new Result(process.waitFor()); + TKit.trace("Done. Exit code: " + reply.exitCode); + + if (outputLines != null) { + reply.output = Collections.unmodifiableList(outputLines); + } + return reply; + } + + private Result runToolProvider(PrintStream out, PrintStream err) { + TKit.trace("Execute " + getPrintableCommandLine() + "..."); + Result reply = new Result(toolProvider.run(out, err, args.toArray( + String[]::new))); + TKit.trace("Done. Exit code: " + reply.exitCode); + return reply; + } + + + private Result runToolProvider() throws IOException { + if (!withSavedOutput()) { + if (saveOutputType.contains(SaveOutputType.DUMP)) { + return runToolProvider(System.out, System.err); + } + + PrintStream nullPrintStream = new PrintStream(new OutputStream() { + @Override + public void write(int b) { + // Nop + } + }); + return runToolProvider(nullPrintStream, nullPrintStream); + } + + try (ByteArrayOutputStream buf = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(buf)) { + Result reply = runToolProvider(ps, ps); + ps.flush(); + try (BufferedReader bufReader = new BufferedReader(new StringReader( + buf.toString()))) { + if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { + String firstLine = bufReader.lines().findFirst().orElse(null); + if (firstLine != null) { + reply.output = List.of(firstLine); + } + } else if (saveOutputType.contains(SaveOutputType.FULL)) { + reply.output = bufReader.lines().collect( + Collectors.toUnmodifiableList()); + } + + if (saveOutputType.contains(SaveOutputType.DUMP)) { + Stream lines; + if (saveOutputType.contains(SaveOutputType.FULL)) { + lines = reply.output.stream(); + } else { + lines = bufReader.lines(); + } + lines.forEach(System.out::println); + } + } + return reply; + } + } + + public String getPrintableCommandLine() { + final String exec; + String format = "[%s](%d)"; + if (toolProvider == null && executable == null) { + exec = ""; + } else if (toolProvider != null) { + format = "tool provider " + format; + exec = toolProvider.name(); + } else { + exec = executablePath().toString(); + } + + return String.format(format, printCommandLine(exec, args), + args.size() + 1); + } + + private static String printCommandLine(String executable, List args) { + // Want command line printed in a way it can be easily copy/pasted + // to be executed manally + Pattern regex = Pattern.compile("\\s"); + return Stream.concat(Stream.of(executable), args.stream()).map( + v -> (v.isEmpty() || regex.matcher(v).find()) ? "\"" + v + "\"" : v).collect( + Collectors.joining(" ")); + } + + private ToolProvider toolProvider; + private Path executable; + private Set saveOutputType; + private Path directory; + + private static enum SaveOutputType { + NONE, FULL, FIRST_LINE, DUMP + }; +}