From 38f138bc22ae705e8e09f75fe6bac4bb470dc29b Mon Sep 17 00:00:00 2001 From: Alexey Semenyuk Date: Thu, 17 Apr 2025 23:37:45 +0000 Subject: [PATCH] 8354985: Add unit tests for Executor class from jpackage test lib Reviewed-by: almatvee --- .../jdk/jpackage/test/ExecutorTest.java | 370 ++++++++++++++++++ .../helpers/jdk/jpackage/test/Executor.java | 30 +- 2 files changed, 387 insertions(+), 13 deletions(-) create mode 100644 test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ExecutorTest.java diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ExecutorTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ExecutorTest.java new file mode 100644 index 00000000000..8175f49a2b2 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ExecutorTest.java @@ -0,0 +1,370 @@ +/* + * Copyright (c) 2025, 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 static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.Consumer; +import java.util.spi.ToolProvider; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class ExecutorTest extends JUnitAdapter { + + private record Command(List stdout, List stderr) { + Command { + stdout.forEach(Objects::requireNonNull); + stderr.forEach(Objects::requireNonNull); + } + + List asExecutable() { + final List commandline = new ArrayList<>(); + if (TKit.isWindows()) { + commandline.addAll(List.of("cmd", "/C")); + } else { + commandline.addAll(List.of("sh", "-c")); + } + commandline.add(Stream.concat(createEchoCommands(stdout), + createEchoCommands(stderr).map(v -> v + ">&2")).collect(joining(" && "))); + return commandline; + } + + private static Stream createEchoCommands(List lines) { + return lines.stream().map(line -> { + if (TKit.isWindows()) { + return "(echo " + line + ")"; + } else { + return "echo " + line; + } + }); + } + + ToolProvider asToolProvider() { + return new ToolProvider() { + + @Override + public int run(PrintWriter out, PrintWriter err, String... args) { + stdout.forEach(out::println); + stderr.forEach(err::println); + return 0; + } + + @Override + public String name() { + return "test"; + } + }; + } + } + + private enum OutputData { + EMPTY(List.of()), + ONE_LINE(List.of("Jupiter")), + MANY(List.of("Uranus", "Saturn", "Earth")); + + OutputData(List data) { + data.forEach(Objects::requireNonNull); + this.data = data; + } + + final List data; + } + + private record CommandSpec(OutputData stdout, OutputData stderr) { + CommandSpec { + Objects.requireNonNull(stdout); + Objects.requireNonNull(stderr); + } + + Command command() { + return new Command(stdout.data.stream().map(line -> { + return "stdout." + line; + }).toList(), stderr.data.stream().map(line -> { + return "stderr." + line; + }).toList()); + } + } + + public enum OutputControl { + DUMP(Executor::dumpOutput), + SAVE_ALL(Executor::saveOutput), + SAVE_FIRST_LINE(Executor::saveFirstLineOfOutput), + DISCARD_STDOUT(NOP), + DISCARD_STDERR(NOP), + ; + + OutputControl(Consumer configureExector) { + this.configureExector = Objects.requireNonNull(configureExector); + } + + Executor applyTo(Executor exec) { + configureExector.accept(exec); + return exec; + } + + static List> variants() { + final List> variants = new ArrayList<>(); + for (final var withDump : BOOLEAN_VALUES) { + variants.addAll(Stream.of( + Set.of(), + Set.of(SAVE_ALL), + Set.of(SAVE_FIRST_LINE), + Set.of(DISCARD_STDOUT), + Set.of(DISCARD_STDERR), + Set.of(SAVE_ALL, DISCARD_STDOUT), + Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT), + Set.of(SAVE_ALL, DISCARD_STDERR), + Set.of(SAVE_FIRST_LINE, DISCARD_STDERR), + Set.of(SAVE_ALL, DISCARD_STDOUT, DISCARD_STDERR), + Set.of(SAVE_FIRST_LINE, DISCARD_STDOUT, DISCARD_STDERR) + ).map(v -> { + if (withDump) { + return Stream.concat(Stream.of(DUMP), v.stream()).collect(toSet()); + } else { + return v; + } + }).toList()); + } + return variants.stream().map(options -> { + return options.stream().filter(o -> { + return o.configureExector != NOP; + }).collect(toSet()); + }).distinct().toList(); + } + + private final Consumer configureExector; + + static final Set SAVE = Set.of(SAVE_ALL, SAVE_FIRST_LINE); + } + + public record OutputTestSpec(boolean toolProvider, Set outputControl, CommandSpec commandSpec) { + public OutputTestSpec { + outputControl.forEach(Objects::requireNonNull); + if (outputControl.containsAll(OutputControl.SAVE)) { + throw new IllegalArgumentException(); + } + Objects.requireNonNull(commandSpec); + } + + @Override + public String toString() { + final List tokens = new ArrayList<>(); + + if (toolProvider) { + tokens.add("tool-provider"); + } + + tokens.add("output=" + format(outputControl)); + tokens.add("command=" + commandSpec); + + return String.join(",", tokens.toArray(String[]::new)); + } + + void test() { + final var command = commandSpec.command(); + final var commandWithDiscardedStreams = discardStreams(command); + + final Executor.Result[] result = new Executor.Result[1]; + final var outputCapture = OutputCapture.captureOutput(() -> { + result[0] = createExecutor(command).executeWithoutExitCodeCheck(); + }); + + assertEquals(0, result[0].getExitCode()); + + assertEquals(expectedCapturedSystemOut(commandWithDiscardedStreams), outputCapture.outLines()); + assertEquals(expectedCapturedSystemErr(commandWithDiscardedStreams), outputCapture.errLines()); + + if (!saveOutput()) { + assertNull(result[0].getOutput()); + } else { + assertNotNull(result[0].getOutput()); + final var allExpectedOutput = expectedCommandOutput(command); + assertEquals(allExpectedOutput.isEmpty(), result[0].getOutput().isEmpty()); + if (!allExpectedOutput.isEmpty()) { + if (outputControl.contains(OutputControl.SAVE_ALL)) { + assertEquals(allExpectedOutput, result[0].getOutput()); + } else if (outputControl.contains(OutputControl.SAVE_FIRST_LINE)) { + assertEquals(1, result[0].getOutput().size()); + assertEquals(allExpectedOutput.getFirst(), result[0].getFirstLineOfOutput()); + } else { + throw new UnsupportedOperationException(); + } + } + } + } + + private boolean dumpOutput() { + return outputControl.contains(OutputControl.DUMP); + } + + private boolean saveOutput() { + return !Collections.disjoint(outputControl, OutputControl.SAVE); + } + + private boolean discardStdout() { + return outputControl.contains(OutputControl.DISCARD_STDOUT); + } + + private boolean discardStderr() { + return outputControl.contains(OutputControl.DISCARD_STDERR); + } + + private static String format(Set outputControl) { + return outputControl.stream().map(OutputControl::name).sorted().collect(joining("+")); + } + + private List expectedCapturedSystemOut(Command command) { + if (!dumpOutput() || (!toolProvider && !saveOutput())) { + return List.of(); + } else if(saveOutput()) { + return Stream.concat(command.stdout().stream(), command.stderr().stream()).toList(); + } else { + return command.stdout(); + } + } + + private List expectedCapturedSystemErr(Command command) { + if (!dumpOutput() || (!toolProvider && !saveOutput())) { + return List.of(); + } else if(saveOutput()) { + return List.of(); + } else { + return command.stderr(); + } + } + + private Command discardStreams(Command command) { + return new Command(discardStdout() ? List.of() : command.stdout(), discardStderr() ? List.of() : command.stderr()); + } + + private record OutputCapture(byte[] out, byte[] err, Charset outCharset, Charset errCharset) { + OutputCapture { + Objects.requireNonNull(out); + Objects.requireNonNull(err); + Objects.requireNonNull(outCharset); + Objects.requireNonNull(errCharset); + } + + List outLines() { + return toLines(out, outCharset); + } + + List errLines() { + return toLines(err, errCharset); + } + + private static List toLines(byte[] buf, Charset charset) { + try (var reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf), charset))) { + return reader.lines().filter(line -> { + return !line.contains("TRACE"); + }).toList(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + static OutputCapture captureOutput(Runnable runnable) { + final var captureOut = new ByteArrayOutputStream(); + final var captureErr = new ByteArrayOutputStream(); + + final var out = System.out; + final var err = System.err; + try { + final var outCharset = System.out.charset(); + final var errCharset = System.err.charset(); + System.setOut(new PrintStream(captureOut, true, outCharset)); + System.setErr(new PrintStream(captureErr, true, errCharset)); + runnable.run(); + return new OutputCapture(captureOut.toByteArray(), captureErr.toByteArray(), outCharset, errCharset); + } finally { + try { + System.setOut(out); + } finally { + System.setErr(err); + } + } + } + } + + private List expectedCommandOutput(Command command) { + command = discardStreams(command); + return Stream.of(command.stdout(), command.stderr()).flatMap(List::stream).toList(); + } + + private Executor createExecutor(Command command) { + final Executor exec; + if (toolProvider) { + exec = Executor.of(command.asToolProvider()); + } else { + exec = Executor.of(command.asExecutable()); + } + + outputControl.forEach(control -> control.applyTo(exec)); + + return exec; + } + } + + @ParameterizedTest + @MethodSource + public void testSavedOutput(OutputTestSpec spec) { + spec.test(); + } + + public static List testSavedOutput() { + List testCases = new ArrayList<>(); + for (final var toolProvider : BOOLEAN_VALUES) { + for (final var outputControl : OutputControl.variants()) { + for (final var stdoutContent : List.of(OutputData.values())) { + for (final var stderrContent : List.of(OutputData.values())) { + final var commandSpec = new CommandSpec(stdoutContent, stderrContent); + testCases.add(new OutputTestSpec(toolProvider, outputControl, commandSpec)); + } + } + } + } + return testCases; + } + + private static final List BOOLEAN_VALUES = List.of(Boolean.TRUE, Boolean.FALSE); + private static final Consumer NOP = exec -> {}; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java index 66cd422203b..053674960c4 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/Executor.java @@ -29,9 +29,9 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.io.StringReader; +import java.io.Writer; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -50,8 +50,16 @@ import jdk.jpackage.internal.util.function.ThrowingSupplier; public final class Executor extends CommandArguments { public static Executor of(String... cmdline) { - return new Executor().setExecutable(cmdline[0]).addArguments( - Arrays.copyOfRange(cmdline, 1, cmdline.length)); + return of(List.of(cmdline)); + } + + public static Executor of(List cmdline) { + cmdline.forEach(Objects::requireNonNull); + return new Executor().setExecutable(cmdline.getFirst()).addArguments(cmdline.subList(1, cmdline.size())); + } + + public static Executor of(ToolProvider toolProvider, String... args) { + return new Executor().setToolProvider(toolProvider).addArguments(List.of(args)); } public Executor() { @@ -414,8 +422,8 @@ public final class Executor extends CommandArguments { || saveOutputType.contains(SaveOutputType.FULL)) { outputLines = outReader.lines().collect(Collectors.toList()); } else { - outputLines = Arrays.asList( - outReader.lines().findFirst().orElse(null)); + outputLines = Optional.ofNullable(outReader.readLine()).map(List::of).orElseGet(List::of); + outReader.transferTo(Writer.nullWriter()); } } finally { if (saveOutputType.contains(SaveOutputType.DUMP) && outputLines != null) { @@ -475,15 +483,11 @@ public final class Executor extends CommandArguments { final var exitCode = runToolProvider(ps, ps); ps.flush(); final List output; + final var bufAsString = buf.toString(); try (BufferedReader bufReader = new BufferedReader(new StringReader( - buf.toString()))) { + bufAsString))) { if (saveOutputType.contains(SaveOutputType.FIRST_LINE)) { - String firstLine = bufReader.lines().findFirst().orElse(null); - if (firstLine != null) { - output = List.of(firstLine); - } else { - output = null; - } + output = bufReader.lines().findFirst().map(List::of).orElseGet(List::of); } else if (saveOutputType.contains(SaveOutputType.FULL)) { output = bufReader.lines().collect(Collectors.toUnmodifiableList()); } else { @@ -495,7 +499,7 @@ public final class Executor extends CommandArguments { if (saveOutputType.contains(SaveOutputType.FULL)) { lines = output.stream(); } else { - lines = bufReader.lines(); + lines = new BufferedReader(new StringReader(bufAsString)).lines(); } lines.forEach(System.out::println); }