8375061: Multiple jpackage tool providers may share the same logging config

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2026-01-13 22:38:12 +00:00
parent 4d0ad0a4a3
commit 9ed0ecbcc1
13 changed files with 532 additions and 308 deletions

View File

@ -24,6 +24,7 @@
*/
package jdk.jpackage.internal;
import java.io.PrintWriter;
import java.util.Optional;
import java.util.function.Supplier;
@ -46,6 +47,18 @@ public final class Globals {
return objectFactory(ObjectFactory.build(objectFactory).executorFactory(v).create());
}
Log.Logger logger() {
return logger;
}
public void loggerOutputStreams(PrintWriter out, PrintWriter err) {
logger.setPrintWriter(out, err);
}
public void loggerVerbose() {
logger.setVerbose();
}
public static int main(Supplier<Integer> mainBody) {
if (INSTANCE.isBound()) {
return mainBody.get();
@ -65,6 +78,7 @@ public final class Globals {
}
private ObjectFactory objectFactory = ObjectFactory.DEFAULT;
private final Log.Logger logger = new Log.Logger();
private static final ScopedValue<Globals> INSTANCE = ScopedValue.newInstance();
private static final Globals DEFAULT = new Globals();

View File

@ -61,16 +61,6 @@ public class Log {
this.err = err;
}
public void flush() {
if (out != null) {
out.flush();
}
if (err != null) {
err.flush();
}
}
public void info(String msg) {
if (out != null) {
out.println(msg);
@ -111,46 +101,27 @@ public class Log {
}
}
private static final InheritableThreadLocal<Logger> instance =
new InheritableThreadLocal<Logger>() {
@Override protected Logger initialValue() {
return new Logger();
}
};
public static void setPrintWriter (PrintWriter out, PrintWriter err) {
instance.get().setPrintWriter(out, err);
}
public static void flush() {
instance.get().flush();
}
public static void info(String msg) {
instance.get().info(msg);
Globals.instance().logger().info(msg);
}
public static void fatalError(String msg) {
instance.get().fatalError(msg);
Globals.instance().logger().fatalError(msg);
}
public static void error(String msg) {
instance.get().error(msg);
}
public static void setVerbose() {
instance.get().setVerbose();
Globals.instance().logger().error(msg);
}
public static boolean isVerbose() {
return instance.get().isVerbose();
return Globals.instance().logger().isVerbose();
}
public static void verbose(String msg) {
instance.get().verbose(msg);
Globals.instance().logger().verbose(msg);
}
public static void verbose(Throwable t) {
instance.get().verbose(t);
Globals.instance().logger().verbose(t);
}
}

View File

@ -79,8 +79,8 @@ public final class Main {
@Override
public int run(PrintStream out, PrintStream err, String... args) {
PrintWriter outWriter = new PrintWriter(out, true);
PrintWriter errWriter = new PrintWriter(err, true);
PrintWriter outWriter = toPrintWriter(out);
PrintWriter errWriter = toPrintWriter(err);
try {
try {
return run(outWriter, errWriter, args);
@ -98,8 +98,8 @@ public final class Main {
}
public static void main(String... args) {
var out = new PrintWriter(System.out, true);
var err = new PrintWriter(System.err, true);
var out = toPrintWriter(System.out);
var err = toPrintWriter(System.err);
System.exit(run(out, err, args));
}
@ -127,7 +127,7 @@ public final class Main {
Objects.requireNonNull(out);
Objects.requireNonNull(err);
Log.setPrintWriter(out, err);
Globals.instance().loggerOutputStreams(out, err);
final var runner = new Runner(t -> {
new ErrorReporter(_ -> {
@ -179,7 +179,7 @@ public final class Main {
}
if (VERBOSE.containsIn(options)) {
Log.setVerbose();
Globals.instance().loggerVerbose();
}
final var optionsProcessor = new OptionsProcessor(parsedOptionsBuilder, bundlingEnv);
@ -310,6 +310,10 @@ public final class Main {
return System.getProperty("java.version");
}
private static PrintWriter toPrintWriter(PrintStream ps) {
return new PrintWriter(ps, true, ps.charset());
}
private enum DefaultBundlingEnvironmentLoader implements Supplier<CliBundlingEnvironment> {
INSTANCE;

View File

@ -0,0 +1,183 @@
/*
* Copyright (c) 2026, 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 org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.spi.ToolProvider;
import jdk.jpackage.internal.util.function.ExceptionBox;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
class JPackageCommandTest extends JUnitAdapter.TestSrcInitializer {
@ParameterizedTest
@MethodSource
void testUseToolProvider(UseToolProviderTestSpec spec) {
// Run the test with the new state to avoid UnsupportedOperationException
// that will be thrown if it attempts to alter global variables in the default R/O state.
TKit.withNewState(spec::test);
}
private static List<UseToolProviderTestSpec> testUseToolProvider() {
var testCases = new ArrayList<UseToolProviderTestSpec>();
for (var globalToolProvider : ExecutableSetterType.values()) {
for (var instanceToolProvider : ExecutableSetterType.values()) {
testCases.add(new UseToolProviderTestSpec(globalToolProvider, instanceToolProvider));
}
}
return testCases;
}
record UseToolProviderTestSpec(ExecutableSetterType globalType, ExecutableSetterType instanceType) {
UseToolProviderTestSpec {
Objects.requireNonNull(globalType);
Objects.requireNonNull(instanceType);
}
@Override
public String toString() {
return String.format("%s, global=%s", instanceType, globalType);
}
void test() {
final Optional<ToolProvider> global;
switch (globalType) {
case SET_CUSTOM_TOOL_PROVIDER -> {
global = Optional.of(createNewToolProvider("jpackage-mock-global"));
JPackageCommand.useToolProviderByDefault(global.get());
}
case SET_DEFAULT_TOOL_PROVIDER -> {
global = Optional.of(JavaTool.JPACKAGE.asToolProvider());
JPackageCommand.useToolProviderByDefault();
}
case SET_PROCESS -> {
global = Optional.empty();
JPackageCommand.useExecutableByDefault();
}
case SET_NONE -> {
global = Optional.empty();
}
default -> {
throw ExceptionBox.reachedUnreachable();
}
}
var cmd = new JPackageCommand();
final Optional<ToolProvider> instance;
switch (instanceType) {
case SET_CUSTOM_TOOL_PROVIDER -> {
instance = Optional.of(createNewToolProvider("jpackage-mock"));
cmd.useToolProvider(instance.get());
}
case SET_DEFAULT_TOOL_PROVIDER -> {
instance = Optional.of(JavaTool.JPACKAGE.asToolProvider());
cmd.useToolProvider(true);
}
case SET_PROCESS -> {
instance = Optional.empty();
cmd.useToolProvider(false);
}
case SET_NONE -> {
instance = Optional.empty();
}
default -> {
throw ExceptionBox.reachedUnreachable();
}
}
var actual = cmd.createExecutor().getToolProvider();
switch (instanceType) {
case SET_CUSTOM_TOOL_PROVIDER -> {
assertSame(actual.get(), instance.get());
assertTrue(cmd.isWithToolProvider());
}
case SET_DEFAULT_TOOL_PROVIDER -> {
global.ifPresentOrElse(expected -> {
assertEquals(expected.name(), actual.orElseThrow().name());
}, () -> {
assertEquals(instance.get().name(), actual.get().name());
});
assertTrue(cmd.isWithToolProvider());
}
case SET_PROCESS -> {
assertFalse(actual.isPresent());
assertFalse(cmd.isWithToolProvider());
}
case SET_NONE -> {
switch (globalType) {
case SET_CUSTOM_TOOL_PROVIDER -> {
assertSame(global.get(), actual.get());
assertTrue(cmd.isWithToolProvider());
}
case SET_DEFAULT_TOOL_PROVIDER -> {
assertEquals(global.get().name(), actual.orElseThrow().name());
assertTrue(cmd.isWithToolProvider());
}
case SET_PROCESS, SET_NONE -> {
assertFalse(actual.isPresent());
assertFalse(cmd.isWithToolProvider());
}
}
}
}
}
private static ToolProvider createNewToolProvider(String name) {
return new ToolProvider() {
@Override
public int run(PrintWriter out, PrintWriter err, String... args) {
throw new UnsupportedOperationException();
}
@Override
public String name() {
return name;
}
};
}
}
enum ExecutableSetterType {
SET_DEFAULT_TOOL_PROVIDER,
SET_CUSTOM_TOOL_PROVIDER,
SET_PROCESS,
SET_NONE,
;
}
}

View File

@ -63,6 +63,7 @@ public final class Executor extends CommandArguments<Executor> {
}
public Executor() {
commandOutputControl.dumpStdout(TKit.state().out()).dumpStderr(TKit.state().err());
}
public Executor setExecutable(String v) {
@ -85,6 +86,10 @@ public final class Executor extends CommandArguments<Executor> {
return setToolProvider(v.asToolProvider());
}
public Optional<ToolProvider> getToolProvider() {
return Optional.ofNullable(toolProvider);
}
public Optional<Path> getExecutable() {
return Optional.ofNullable(executable);
}

View File

@ -52,8 +52,6 @@ import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
@ -63,6 +61,7 @@ import java.util.spi.ToolProvider;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingConsumer;
import jdk.jpackage.internal.util.function.ThrowingFunction;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
@ -77,6 +76,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
@SuppressWarnings("this-escape")
public JPackageCommand() {
toolProviderSource = new ToolProviderSource();
prerequisiteActions = new Actions();
verifyActions = new Actions();
excludeStandardAsserts(StandardAssert.MAIN_LAUNCHER_DESCRIPTION);
@ -84,7 +84,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private JPackageCommand(JPackageCommand cmd, boolean immutable) {
args.addAll(cmd.args);
withToolProvider = cmd.withToolProvider;
toolProviderSource = cmd.toolProviderSource.copy();
saveConsoleOutput = cmd.saveConsoleOutput;
discardStdout = cmd.discardStdout;
discardStderr = cmd.discardStderr;
@ -770,7 +770,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public static void useToolProviderByDefault(ToolProvider jpackageToolProvider) {
defaultToolProvider.set(Optional.of(jpackageToolProvider));
TKit.state().setProperty(DefaultToolProviderKey.VALUE, Objects.requireNonNull(jpackageToolProvider));
}
public static void useToolProviderByDefault() {
@ -778,45 +778,22 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public static void useExecutableByDefault() {
defaultToolProvider.set(Optional.empty());
}
/**
* In a separate thread calls {@link #useToolProviderByDefault(ToolProvider)}
* with the specified {@code jpackageToolProvider} and then calls
* {@code workload.run()}. Joins the thread.
* <p>
* The idea is to run the {@code workload} in the context of the specified
* jpackage {@code ToolProvider} without altering the global variable holding
* the default jpackage {@code ToolProvider}. The global variable is
* thread-local; setting its value in a new thread doesn't alter its copy in the
* calling thread.
*
* @param jpackageToolProvider jpackage {@code ToolProvider}
* @param workload the workload to run
*/
public static void withToolProvider(Runnable workload, ToolProvider jpackageToolProvider) {
Objects.requireNonNull(workload);
Objects.requireNonNull(jpackageToolProvider);
CompletableFuture.runAsync(() -> {
var oldValue = defaultToolProvider.get();
useToolProviderByDefault(jpackageToolProvider);
try {
workload.run();
} finally {
defaultToolProvider.set(oldValue);
}
// Run the future in a new native thread. Don't run it in a virtual/pooled thread.
// Pooled and/or virtual threads are problematic when used with inheritable thread-local variables.
// TKit class depends on such a variable, which results in intermittent test failures
// if the default executor runs this future.
}, Executors.newThreadPerTaskExecutor(Thread.ofPlatform().factory())).join();
TKit.state().setProperty(DefaultToolProviderKey.VALUE, null);
}
public JPackageCommand useToolProvider(boolean v) {
verifyMutable();
withToolProvider = v;
if (v) {
toolProviderSource.useDefaultToolProvider();
} else {
toolProviderSource.useProcess();
}
return this;
}
public JPackageCommand useToolProvider(ToolProvider v) {
verifyMutable();
toolProviderSource.useToolProvider(v);
return this;
}
@ -928,9 +905,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public boolean isWithToolProvider() {
return Optional.ofNullable(withToolProvider).orElseGet(() -> {
return defaultToolProvider.get().isPresent();
});
return toolProviderSource.toolProvider().isPresent();
}
public JPackageCommand executePrerequisiteActions() {
@ -938,21 +913,19 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
return this;
}
private Executor createExecutor() {
Executor createExecutor() {
Executor exec = new Executor()
.saveOutput(saveConsoleOutput).dumpOutput(!suppressOutput)
.discardStdout(discardStdout).discardStderr(discardStderr)
.setDirectory(executeInDirectory)
.addArguments(args);
if (isWithToolProvider()) {
exec.setToolProvider(defaultToolProvider.get().orElseGet(JavaTool.JPACKAGE::asToolProvider));
} else {
exec.setExecutable(JavaTool.JPACKAGE);
if (TKit.isWindows()) {
exec.setWindowsTmpDir(System.getProperty("java.io.tmpdir"));
}
}
toolProviderSource.toolProvider().ifPresentOrElse(exec::setToolProvider, () -> {
exec.setExecutable(JavaTool.JPACKAGE);
if (TKit.isWindows()) {
exec.setWindowsTmpDir(System.getProperty("java.io.tmpdir"));
}
});
return exec;
}
@ -1731,7 +1704,70 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private final List<Action> actions;
}
private Boolean withToolProvider;
private static final class ToolProviderSource {
ToolProviderSource copy() {
return new ToolProviderSource(this);
}
void useDefaultToolProvider() {
customToolProvider = null;
mode = Mode.USE_TOOL_PROVIDER;
}
void useToolProvider(ToolProvider tp) {
customToolProvider = Objects.requireNonNull(tp);
mode = Mode.USE_TOOL_PROVIDER;
}
void useProcess() {
customToolProvider = null;
mode = Mode.USE_PROCESS;
}
Optional<ToolProvider> toolProvider() {
switch (mode) {
case USE_PROCESS -> {
return Optional.empty();
}
case USE_TOOL_PROVIDER -> {
if (customToolProvider != null) {
return Optional.of(customToolProvider);
} else {
return TKit.state().findProperty(DefaultToolProviderKey.VALUE).map(ToolProvider.class::cast).or(() -> {
return Optional.of(JavaTool.JPACKAGE.asToolProvider());
});
}
}
case INHERIT_DEFAULTS -> {
return TKit.state().findProperty(DefaultToolProviderKey.VALUE).map(ToolProvider.class::cast);
}
default -> {
throw ExceptionBox.reachedUnreachable();
}
}
}
ToolProviderSource() {
mode = Mode.INHERIT_DEFAULTS;
}
private ToolProviderSource(ToolProviderSource other) {
this.customToolProvider = other.customToolProvider;
this.mode = other.mode;
}
private enum Mode {
INHERIT_DEFAULTS,
USE_PROCESS,
USE_TOOL_PROVIDER
}
private ToolProvider customToolProvider;
private Mode mode;
}
private final ToolProviderSource toolProviderSource;
private boolean saveConsoleOutput;
private boolean discardStdout;
private boolean discardStderr;
@ -1747,12 +1783,10 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private Set<ReadOnlyPathAssert> readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values());
private Set<StandardAssert> standardAsserts = Set.of(StandardAssert.values());
private List<Consumer<Iterator<String>>> outputValidators = new ArrayList<>();
private static InheritableThreadLocal<Optional<ToolProvider>> defaultToolProvider = new InheritableThreadLocal<>() {
@Override
protected Optional<ToolProvider> initialValue() {
return Optional.empty();
}
};
private enum DefaultToolProviderKey {
VALUE
}
private static final Map<String, PackageType> PACKAGE_TYPES = Stream.of(PackageType.values()).collect(toMap(PackageType::getType, x -> x));

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -27,17 +27,24 @@ import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Collectors.toMap;
import static jdk.jpackage.test.TestBuilder.CMDLINE_ARG_PREFIX;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.function.ExceptionBox;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
public final class Main {
@ -47,10 +54,52 @@ public final class Main {
}
public static void main(TestBuilder.Builder builder, String... args) throws Exception {
Objects.requireNonNull(builder);
var argList = List.of(args);
var ignoreLogfile = argList.contains(CMDLINE_ARG_PREFIX + "ignore-logfile");
List<String> filteredArgs;
if (ignoreLogfile) {
filteredArgs = argList.stream().filter(Predicate.isEqual(CMDLINE_ARG_PREFIX + "ignore-logfile").negate()).toList();
} else {
filteredArgs = argList;
}
ThrowingRunnable<Exception> workload = () -> {
run(builder, filteredArgs);
};
try {
Optional.ofNullable(TKit.getConfigProperty("logfile")).filter(_ -> {
return !ignoreLogfile;
}).map(Path::of).ifPresentOrElse(logfile -> {
try (var out = new PrintStream(
Files.newOutputStream(logfile, StandardOpenOption.CREATE, StandardOpenOption.APPEND),
true,
System.out.charset())) {
TKit.withOutput(workload, out, out);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}, () -> {
ThrowingRunnable.toRunnable(workload).run();
});
} catch (Exception ex) {
throw ExceptionBox.unbox(ex);
}
}
private static void run(TestBuilder.Builder builder, List<String> args) throws Exception {
boolean listTests = false;
List<TestInstance> tests = new ArrayList<>();
try (TestBuilder testBuilder = builder.testConsumer(tests::add).create()) {
Deque<String> argsAsList = new ArrayDeque<>(List.of(args));
Deque<String> argsAsList = new ArrayDeque<>(args);
while (!argsAsList.isEmpty()) {
var arg = argsAsList.pop();
TestBuilder.trace(String.format("Parsing [%s]...", arg));
@ -115,7 +164,7 @@ public final class Main {
return;
}
TKit.withExtraLogStream(() -> runTests(orderedTests));
runTests(orderedTests);
}
private static void runTests(List<TestInstance> tests) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2026, 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
@ -38,7 +38,6 @@ import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
@ -52,6 +51,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
@ -109,56 +109,45 @@ public final class TKit {
throw throwUnknownPlatformError();
}).get();
static void withExtraLogStream(ThrowingRunnable<? extends Exception> action) {
if (state().extraLogStream != null) {
ThrowingRunnable.toRunnable(action).run();
} else {
try (PrintStream logStream = openLogStream()) {
withExtraLogStream(action, logStream);
public static void withOutput(ThrowingRunnable<? extends Exception> action, PrintStream out, PrintStream err) {
Objects.requireNonNull(action);
Objects.requireNonNull(out);
Objects.requireNonNull(err);
try {
withState(action, stateBuilder -> {
stateBuilder.out(out).err(err);
});
} finally {
try {
out.flush();
} finally {
err.flush();
}
}
}
static void withExtraLogStream(ThrowingRunnable<? extends Exception> action, PrintStream logStream) {
withNewState(action, stateBuilder -> {
stateBuilder.extraLogStream(logStream);
});
}
public static void withMainLogStream(ThrowingRunnable<? extends Exception> action, PrintStream logStream) {
withNewState(action, stateBuilder -> {
stateBuilder.mainLogStream(logStream);
});
}
public static void withStackTraceStream(ThrowingRunnable<? extends Exception> action, PrintStream logStream) {
withNewState(action, stateBuilder -> {
stateBuilder.stackTraceStream(logStream);
});
}
public static State state() {
return STATE.get();
}
public static void state(State v) {
STATE.set(Objects.requireNonNull(v));
}
private static void withNewState(ThrowingRunnable<? extends Exception> action, Consumer<State.Builder> stateBuilderMutator) {
public static void withState(ThrowingRunnable<? extends Exception> action, Consumer<State.Builder> stateBuilderMutator) {
Objects.requireNonNull(action);
Objects.requireNonNull(stateBuilderMutator);
var oldState = state();
var builder = oldState.buildCopy();
stateBuilderMutator.accept(builder);
var newState = builder.create();
try {
state(newState);
ThrowingRunnable.toRunnable(action).run();
} finally {
state(oldState);
}
var stateBuilder = state().buildCopy();
stateBuilderMutator.accept(stateBuilder);
withState(action, stateBuilder.create());
}
public static void withNewState(ThrowingRunnable<? extends Exception> action) {
withState(action, _ -> {});
}
public static void withState(ThrowingRunnable<? extends Exception> action, State state) {
Objects.requireNonNull(action);
Objects.requireNonNull(state);
ScopedValue.where(STATE, state).run(ThrowingRunnable.toRunnable(action));
}
public static State state() {
return STATE.orElse(DEFAULT_STATE);
}
enum RunTestMode {
@ -178,33 +167,19 @@ public final class TKit {
throw new IllegalStateException("Unexpected nested Test.run() call");
}
withExtraLogStream(() -> {
tests.stream().forEach(test -> {
withNewState(() -> {
try {
if (modes.contains(RunTestMode.FAIL_FAST)) {
test.run();
} else {
ignoreExceptions(test).run();
}
} finally {
Optional.ofNullable(state().extraLogStream).ifPresent(PrintStream::flush);
}
}, stateBuilder -> {
stateBuilder.currentTest(test);
});
tests.stream().forEach(test -> {
withState(() -> {
if (modes.contains(RunTestMode.FAIL_FAST)) {
test.run();
} else {
ignoreExceptions(test).run();
}
}, stateBuilder -> {
stateBuilder.currentTest(test);
});
});
}
static <T> T runAdhocTest(ThrowingSupplier<T, ? extends Exception> action) {
final List<T> box = new ArrayList<>();
runAdhocTest(() -> {
box.add(action.get());
});
return box.getFirst();
}
static void runAdhocTest(ThrowingRunnable<? extends Exception> action) {
Objects.requireNonNull(action);
@ -281,10 +256,7 @@ public final class TKit {
static void log(String v) {
v = addTimestamp(v);
var state = state();
state.mainLogStream.println(v);
if (state.extraLogStream != null) {
state.extraLogStream.println(v);
}
state.out.println(v);
}
static Path removeRootFromAbsolutePath(Path v) {
@ -692,8 +664,7 @@ public final class TKit {
static void printStackTrace(Throwable throwable) {
var state = state();
Optional.ofNullable(state.extraLogStream).ifPresent(throwable::printStackTrace);
throwable.printStackTrace(state.stackTraceStream);
throwable.printStackTrace(state.err);
}
private static String concatMessages(String msg, String msg2) {
@ -1255,16 +1226,6 @@ public final class TKit {
return new TextStreamVerifier(what);
}
private static PrintStream openLogStream() {
return state().logFile.map(logfile -> {
try {
return Files.newOutputStream(logfile, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}).map(PrintStream::new).orElse(null);
}
public record PathSnapshot(List<String> contentHashes) {
public PathSnapshot {
contentHashes.forEach(Objects::requireNonNull);
@ -1376,25 +1337,23 @@ public final class TKit {
public static final class State {
private State(
Optional<Path> logFile,
TestInstance currentTest,
PrintStream mainLogStream,
PrintStream stackTraceStream,
PrintStream extraLogStream,
PrintStream out,
PrintStream err,
Map<Object, Object> properties,
boolean trace,
boolean traceAsserts,
boolean verboseJPackage,
boolean verboseTestSetup) {
Objects.requireNonNull(logFile);
Objects.requireNonNull(mainLogStream);
Objects.requireNonNull(stackTraceStream);
Objects.requireNonNull(out);
Objects.requireNonNull(err);
Objects.requireNonNull(properties);
this.logFile = logFile;
this.currentTest = currentTest;
this.mainLogStream = mainLogStream;
this.stackTraceStream = stackTraceStream;
this.extraLogStream = extraLogStream;
this.out = out;
this.err = err;
this.properties = Collections.synchronizedMap(properties);
this.trace = trace;
this.traceAsserts = traceAsserts;
@ -1403,11 +1362,30 @@ public final class TKit {
this.verboseTestSetup = verboseTestSetup;
}
Builder buildCopy() {
return build().initFrom(this);
}
PrintStream out() {
return out;
}
PrintStream err() {
return err;
}
Optional<Object> findProperty(Object key) {
return Optional.ofNullable(properties.get(Objects.requireNonNull(key)));
}
void setProperty(Object key, Object value) {
if (value == null) {
properties.remove(Objects.requireNonNull(key));
} else {
properties.put(Objects.requireNonNull(key), value);
}
}
static Builder build() {
return new Builder();
}
@ -1416,11 +1394,9 @@ public final class TKit {
static final class Builder {
Builder initDefaults() {
logFile = Optional.ofNullable(getConfigProperty("logfile")).map(Path::of);
currentTest = null;
mainLogStream = System.out;
stackTraceStream = System.err;
extraLogStream = null;
out = System.out;
err = System.err;
var logOptions = tokenizeConfigProperty("suppress-logging");
if (logOptions == null) {
@ -1444,15 +1420,17 @@ public final class TKit {
verboseTestSetup = isNonOf.test(Set.of("init", "i"));
}
mutable = true;
return this;
}
Builder initFrom(State state) {
logFile = state.logFile;
currentTest = state.currentTest;
mainLogStream = state.mainLogStream;
stackTraceStream = state.stackTraceStream;
extraLogStream = state.extraLogStream;
out = state.out;
err = state.err;
properties.clear();
properties.putAll(state.properties);
trace = state.trace;
traceAsserts = state.traceAsserts;
@ -1463,54 +1441,67 @@ public final class TKit {
return this;
}
Builder logFile(Optional<Path> v) {
logFile = v;
return this;
}
Builder currentTest(TestInstance v) {
currentTest = v;
return this;
}
Builder mainLogStream(PrintStream v) {
mainLogStream = v;
Builder out(PrintStream v) {
out = v;
return this;
}
Builder stackTraceStream(PrintStream v) {
stackTraceStream = v;
Builder err(PrintStream v) {
err = v;
return this;
}
Builder extraLogStream(PrintStream v) {
extraLogStream = v;
Builder property(Object key, Object value) {
if (value == null) {
properties.remove(Objects.requireNonNull(key));
} else {
properties.put(Objects.requireNonNull(key), value);
}
return this;
}
Builder mutable(boolean v) {
mutable = v;
return this;
}
State create() {
return new State(logFile, currentTest, mainLogStream, stackTraceStream, extraLogStream, trace, traceAsserts, verboseJPackage, verboseTestSetup);
return new State(
currentTest,
out,
err,
mutable ? new HashMap<>(properties) : Map.copyOf(properties),
trace,
traceAsserts,
verboseJPackage,
verboseTestSetup);
}
private Optional<Path> logFile;
private TestInstance currentTest;
private PrintStream mainLogStream;
private PrintStream stackTraceStream;
private PrintStream extraLogStream;
private PrintStream out;
private PrintStream err;
private Map<Object, Object> properties = new HashMap<>();
private boolean trace;
private boolean traceAsserts;
private boolean verboseJPackage;
private boolean verboseTestSetup;
private boolean mutable = true;
}
private final Optional<Path> logFile;
private final TestInstance currentTest;
private final PrintStream mainLogStream;
private final PrintStream stackTraceStream;
private final PrintStream extraLogStream;
private final PrintStream out;
private final PrintStream err;
private final Map<Object, Object> properties;
private final boolean trace;
private final boolean traceAsserts;
@ -1520,10 +1511,6 @@ public final class TKit {
}
private static final InheritableThreadLocal<State> STATE = new InheritableThreadLocal<>() {
@Override
protected State initialValue() {
return State.build().initDefaults().create();
}
};
private static final ScopedValue<State> STATE = ScopedValue.newInstance();
private static final State DEFAULT_STATE = State.build().initDefaults().mutable(false).create();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -200,13 +200,14 @@ public class OptionsValidationFailTest {
Stream.of("--jpt-run=ErrorTest")
).flatMap(x -> x).toArray(String[]::new)).map(dynamicTest -> {
return DynamicTest.dynamicTest(dynamicTest.getDisplayName(), () -> {
JPackageCommand.withToolProvider(() -> {
TKit.withNewState(() -> {
JPackageCommand.useToolProviderByDefault(jpackageToolProviderMock);
try {
dynamicTest.getExecutable().execute();
} catch (Throwable t) {
throw ExceptionBox.toUnchecked(ExceptionBox.unbox(t));
}
}, jpackageToolProviderMock);
});
});
});
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -29,11 +29,11 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import jdk.jpackage.internal.util.TeeOutputStream;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
@ -70,12 +70,16 @@ public class JUnitAdapter {
static List<String> captureJPackageTestLog(ThrowingRunnable<? extends Exception> runnable) {
final var buf = new ByteArrayOutputStream();
try (PrintStream ps = new PrintStream(buf, true, StandardCharsets.UTF_8)) {
TKit.withExtraLogStream(runnable, ps);
}
var ps = new PrintStream(buf, false, TKit.state().out().charset());
final var out = new PrintStream(new TeeOutputStream(List.of(TKit.state().out(), ps)), true, ps.charset());
TKit.withOutput(runnable, out, TKit.state().err());
ps.flush();
try (final var in = new ByteArrayInputStream(buf.toByteArray());
final var reader = new InputStreamReader(in, StandardCharsets.UTF_8);
final var reader = new InputStreamReader(in, ps.charset());
final var bufReader = new BufferedReader(reader)) {
return bufReader.lines().map(line -> {
// Skip timestamp

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -23,7 +23,6 @@
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collection;
@ -34,15 +33,12 @@ import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.spi.ToolProvider;
import java.util.stream.IntStream;
import jdk.jpackage.internal.util.function.ThrowingRunnable;
import jdk.jpackage.internal.util.Slot;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.HelloApp;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.JavaTool;
import jdk.jpackage.test.JavaAppDesc;
import jdk.jpackage.test.Main;
import jdk.jpackage.test.PackageTest;
@ -66,10 +62,10 @@ public class AsyncTest {
// Create test jar only once.
// Besides of saving time, this avoids asynchronous invocations of java tool provider that randomly fail.
APP_JAR.set(HelloApp.createBundle(JavaAppDesc.parse("Hello!"), TKit.workDir()));
var appJar = HelloApp.createBundle(JavaAppDesc.parse("Hello!"), TKit.workDir());
//
// Run test cases from InternalAsyncTest class asynchronously.
// Run test cases from AsyncInnerTest class asynchronously.
// Spawn a thread for every test case.
// Input data for test cases will be cooked asynchronously but in a safe way because every test case has an isolated work directory.
// Multiple jpackage tool provider instances will be invoked asynchronously.
@ -79,14 +75,10 @@ public class AsyncTest {
var testFuncNames = List.of("testAppImage", "testNativeBundle");
var runArg = String.format("--jpt-run=%s", AsyncInnerTest.class.getName());
var futures = executor.invokeAll(IntStream.range(0, JOB_COUNT).mapToObj(Integer::toString).<Workload>mapMulti((idx, consumer) -> {
for (var testFuncName : testFuncNames) {
var id = String.format("%s(%s)", testFuncName, idx);
consumer.accept(new Workload(() -> {
Main.main(runArg, String.format("--jpt-include=%s", id));
}, id));
consumer.accept(new Workload(id, appJar));
}
}).toList());
@ -99,10 +91,8 @@ public class AsyncTest {
for (var future : futures) {
var result = future.get();
TKit.trace(String.format("[%s] STDOUT BEGIN\n%s", result.id(), result.stdoutBuffer()));
TKit.trace(String.format("[%s] STDOUT END", result.id()));
TKit.trace(String.format("[%s] STDERR BEGIN\n%s", result.id(), result.stderrBuffer()));
TKit.trace(String.format("[%s] STDERR END", result.id()));
TKit.trace(String.format("[%s] OUTPUT BEGIN\n%s", result.testCaseId(), result.testOutput()));
TKit.trace(String.format("[%s] OUTPUT END", result.testCaseId()));
result.exception().filter(Predicate.not(TKit::isSkippedException)).ifPresent(fatalError::set);
}
@ -142,80 +132,56 @@ public class AsyncTest {
}
private record Result(String stdoutBuffer, String stderrBuffer, String id, Optional<Exception> exception) {
private record Result(String testOutput, String testCaseId, Optional<Exception> exception) {
Result {
Objects.requireNonNull(stdoutBuffer);
Objects.requireNonNull(stderrBuffer);
Objects.requireNonNull(id);
Objects.requireNonNull(testOutput);
Objects.requireNonNull(testCaseId);
Objects.requireNonNull(exception);
}
}
private record Workload(
ByteArrayOutputStream stdoutBuffer,
ByteArrayOutputStream stderrBuffer,
ThrowingRunnable<? extends Exception> runnable,
String id) implements Callable<Result> {
String testCaseId,
ByteArrayOutputStream outputSink,
Path appJar) implements Callable<Result> {
Workload {
Objects.requireNonNull(stdoutBuffer);
Objects.requireNonNull(stderrBuffer);
Objects.requireNonNull(runnable);
Objects.requireNonNull(id);
Objects.requireNonNull(testCaseId);
Objects.requireNonNull(outputSink);
Objects.requireNonNull(appJar);
}
Workload(ThrowingRunnable<? extends Exception> runnable, String id) {
this(new ByteArrayOutputStream(), new ByteArrayOutputStream(), runnable, id);
Workload(String testCaseId, Path appJar) {
this(testCaseId, new ByteArrayOutputStream(), appJar);
}
private String stdoutBufferAsString() {
return new String(stdoutBuffer.toByteArray(), StandardCharsets.UTF_8);
}
private String stderrBufferAsString() {
return new String(stderrBuffer.toByteArray(), StandardCharsets.UTF_8);
private String testOutput() {
return new String(outputSink.toByteArray(), StandardCharsets.UTF_8);
}
@Override
public Result call() {
// Reset the current test inherited in the state from the parent thread.
TKit.state(DEFAULT_STATE);
var defaultToolProvider = JavaTool.JPACKAGE.asToolProvider();
JPackageCommand.useToolProviderByDefault(new ToolProvider() {
@Override
public int run(PrintWriter out, PrintWriter err, String... args) {
try (var bufOut = new PrintWriter(stdoutBuffer, true, StandardCharsets.UTF_8);
var bufErr = new PrintWriter(stderrBuffer, true, StandardCharsets.UTF_8)) {
return defaultToolProvider.run(bufOut, bufErr, args);
}
}
@Override
public String name() {
return defaultToolProvider.name();
}
});
var runArg = String.format("--jpt-run=%s", AsyncInnerTest.class.getName());
Optional<Exception> err = Optional.empty();
try (var bufOut = new PrintStream(stdoutBuffer, true, StandardCharsets.UTF_8);
var bufErr = new PrintStream(stderrBuffer, true, StandardCharsets.UTF_8)) {
TKit.withStackTraceStream(() -> {
TKit.withMainLogStream(runnable, bufOut);
}, bufErr);
try {
try (var out = new PrintStream(outputSink, false, System.out.charset())) {
ScopedValue.where(APP_JAR, appJar).run(() -> {
TKit.withOutput(() -> {
JPackageCommand.useToolProviderByDefault();
Main.main("--jpt-ignore-logfile", runArg, String.format("--jpt-include=%s", testCaseId));
}, out, out);
});
}
} catch (Exception ex) {
err = Optional.of(ex);
}
return new Result(stdoutBufferAsString(), stderrBufferAsString(), id, err);
return new Result(testOutput(), testCaseId, err);
}
}
private static final int JOB_COUNT = 30;
private static final TKit.State DEFAULT_STATE = TKit.state();
private static final InheritableThreadLocal<Path> APP_JAR = new InheritableThreadLocal<>();
private static final ScopedValue<Path> APP_JAR = ScopedValue.newInstance();
}

View File

@ -56,11 +56,14 @@ public class Win8301247Test {
cmd.addArguments("--java-options", "-Djpackage.test.noexit=true");
cmd.executeAndAssertImageCreated();
var state = TKit.state();
var f = new CompletableFuture<Process>();
// Launch the app in a separate thread
new Thread(() -> {
HelloApp.assertMainLauncher(cmd).get().processListener(f::complete).execute();
TKit.withState(() -> {
HelloApp.assertMainLauncher(cmd).get().processListener(f::complete).execute();
}, state);
}).start();
var mainLauncherProcess = f.get();

View File

@ -95,11 +95,14 @@ public class WinNoRestartTest {
// Save updated main launcher .cfg file
cfgFile.save(cmd.appLauncherCfgPath(null));
var state = TKit.state();
var f = new CompletableFuture<Process>();
// Launch the app in a separate thread
new Thread(() -> {
HelloApp.assertMainLauncher(cmd).get().processListener(f::complete).execute();
TKit.withState(() -> {
HelloApp.assertMainLauncher(cmd).get().processListener(f::complete).execute();
}, state);
}).start();
var mainLauncherProcess = f.get();