8370136: Support async execution of jpackage tests

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2025-10-21 00:07:12 +00:00
parent a1302e5fbc
commit c781a2ff31
7 changed files with 301 additions and 149 deletions

View File

@ -374,8 +374,14 @@ final class ConfigFilesStasher {
private static Path setupDirectory(JPackageCommand cmd, String argName) {
if (!cmd.hasArgument(argName)) {
// Use absolute path as jpackage can be executed in another directory
cmd.setArgumentValue(argName, TKit.createTempDirectory("stash-script-resource-dir").toAbsolutePath());
// Use absolute path as jpackage can be executed in another directory.
// Some tests expect a specific last argument, don't interfere with them
// and insert the argument at the beginning of the command line.
List<String> args = new ArrayList<>();
args.add(argName);
args.add(TKit.createTempDirectory("stash-script-resource-dir").toAbsolutePath().toString());
args.addAll(cmd.getAllArguments());
cmd.clearArguments().addArguments(args);
}
return Path.of(cmd.getArgumentValue(argName));

View File

@ -39,7 +39,6 @@ import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
@ -691,7 +690,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public static void useToolProviderByDefault(ToolProvider jpackageToolProvider) {
defaultToolProvider = Optional.of(jpackageToolProvider);
defaultToolProvider.set(Optional.of(jpackageToolProvider));
}
public static void useToolProviderByDefault() {
@ -699,7 +698,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public static void useExecutableByDefault() {
defaultToolProvider = Optional.empty();
defaultToolProvider.set(Optional.empty());
}
public JPackageCommand useToolProvider(boolean v) {
@ -808,7 +807,9 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
}
public boolean isWithToolProvider() {
return Optional.ofNullable(withToolProvider).orElseGet(defaultToolProvider::isPresent);
return Optional.ofNullable(withToolProvider).orElseGet(() -> {
return defaultToolProvider.get().isPresent();
});
}
public JPackageCommand executePrerequisiteActions() {
@ -824,7 +825,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
.addArguments(args);
if (isWithToolProvider()) {
exec.setToolProvider(defaultToolProvider.orElseGet(JavaTool.JPACKAGE::asToolProvider));
exec.setToolProvider(defaultToolProvider.get().orElseGet(JavaTool.JPACKAGE::asToolProvider));
} else {
exec.setExecutable(JavaTool.JPACKAGE);
if (TKit.isWindows()) {
@ -1256,10 +1257,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private void assertFileInAppImage(Path filename, Path expectedPath) {
if (expectedPath != null) {
if (expectedPath.isAbsolute()) {
throw new IllegalArgumentException();
}
if (!expectedPath.getFileName().equals(filename.getFileName())) {
if (expectedPath.isAbsolute() || !expectedPath.getFileName().equals(filename.getFileName())) {
throw new IllegalArgumentException();
}
}
@ -1345,7 +1343,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
addArguments("--runtime-image", DEFAULT_RUNTIME_IMAGE);
}
if (!hasArgument("--verbose") && TKit.VERBOSE_JPACKAGE && !ignoreDefaultVerbose) {
if (!hasArgument("--verbose") && TKit.verboseJPackage() && !ignoreDefaultVerbose) {
addArgument("--verbose");
}
@ -1369,11 +1367,7 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
final var typesSet = Stream.of(types).collect(Collectors.toSet());
if (!hasArgument("--type")) {
if (!isImagePackageType()) {
if (TKit.isLinux() && typesSet.equals(PackageType.LINUX)) {
return;
}
if (TKit.isWindows() && typesSet.equals(PackageType.WINDOWS)) {
if ((TKit.isLinux() && typesSet.equals(PackageType.LINUX)) || (TKit.isWindows() && typesSet.equals(PackageType.WINDOWS))) {
return;
}
@ -1523,29 +1517,21 @@ public class JPackageCommand extends CommandArguments<JPackageCommand> {
private Set<ReadOnlyPathAssert> readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values());
private Set<AppLayoutAssert> appLayoutAsserts = Set.of(AppLayoutAssert.values());
private List<Consumer<Iterator<String>>> outputValidators = new ArrayList<>();
private static Optional<ToolProvider> defaultToolProvider = Optional.empty();
private static final Map<String, PackageType> PACKAGE_TYPES = Functional.identity(
() -> {
Map<String, PackageType> reply = new HashMap<>();
for (PackageType type : PackageType.values()) {
reply.put(type.getType(), type);
}
return reply;
}).get();
public static final Path DEFAULT_RUNTIME_IMAGE = Functional.identity(() -> {
// Set the property to the path of run-time image to speed up
// building app images and platform bundles by avoiding running jlink
// The value of the property will be automativcally appended to
// jpackage command line if the command line doesn't have
// `--runtime-image` parameter set.
String val = TKit.getConfigProperty("runtime-image");
if (val != null) {
return Path.of(val);
private static InheritableThreadLocal<Optional<ToolProvider>> defaultToolProvider = new InheritableThreadLocal<>() {
@Override
protected Optional<ToolProvider> initialValue() {
return Optional.empty();
}
return null;
}).get();
};
private static final Map<String, PackageType> PACKAGE_TYPES = Stream.of(PackageType.values()).collect(toMap(PackageType::getType, x -> x));
// Set the property to the path of run-time image to speed up
// building app images and platform bundles by avoiding running jlink.
// The value of the property will be automatically appended to
// jpackage command line if the command line doesn't have
// `--runtime-image` parameter set.
public static final Path DEFAULT_RUNTIME_IMAGE = Optional.ofNullable(TKit.getConfigProperty("runtime-image")).map(Path::of).orElse(null);
// [HH:mm:ss.SSS]
private static final Pattern TIMESTAMP_REGEXP = Pattern.compile(

View File

@ -41,11 +41,11 @@ import java.util.stream.Stream;
public final class Main {
public static void main(String args[]) throws Throwable {
public static void main(String... args) throws Throwable {
main(TestBuilder.build(), args);
}
public static void main(TestBuilder.Builder builder, String args[]) throws Throwable {
public static void main(TestBuilder.Builder builder, String... args) throws Throwable {
boolean listTests = false;
List<TestInstance> tests = new ArrayList<>();
try (TestBuilder testBuilder = builder.testConsumer(tests::add).create()) {

View File

@ -29,7 +29,6 @@ import static jdk.jpackage.internal.util.function.ThrowingBiFunction.toBiFunctio
import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier;
import java.io.Closeable;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
@ -39,6 +38,7 @@ 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;
@ -110,7 +110,7 @@ public final class TKit {
}).get();
static void withExtraLogStream(ThrowingRunnable action) {
if (extraLogStream != null) {
if (state().extraLogStream != null) {
ThrowingRunnable.toRunnable(action).run();
} else {
try (PrintStream logStream = openLogStream()) {
@ -120,12 +120,44 @@ public final class TKit {
}
static void withExtraLogStream(ThrowingRunnable action, PrintStream logStream) {
var oldExtraLogStream = extraLogStream;
withNewState(action, stateBuilder -> {
stateBuilder.extraLogStream(logStream);
});
}
public static void withMainLogStream(ThrowingRunnable action, PrintStream logStream) {
withNewState(action, stateBuilder -> {
stateBuilder.mainLogStream(logStream);
});
}
public static void withStackTraceStream(ThrowingRunnable 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 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 {
extraLogStream = logStream;
state(newState);
ThrowingRunnable.toRunnable(action).run();
} finally {
extraLogStream = oldExtraLogStream;
state(oldState);
}
}
@ -142,26 +174,25 @@ public final class TKit {
static void runTests(List<TestInstance> tests, Set<RunTestMode> modes) {
Objects.requireNonNull(tests);
Objects.requireNonNull(modes);
if (currentTest != null) {
throw new IllegalStateException(
"Unexpected nested or concurrent Test.run() call");
if (currentTest() != null) {
throw new IllegalStateException("Unexpected nested Test.run() call");
}
withExtraLogStream(() -> {
tests.stream().forEach(test -> {
currentTest = test;
try {
if (modes.contains(RunTestMode.FAIL_FAST)) {
ThrowingRunnable.toRunnable(test::run).run();
} else {
ignoreExceptions(test).run();
withNewState(() -> {
try {
if (modes.contains(RunTestMode.FAIL_FAST)) {
test.run();
} else {
ignoreExceptions(test).run();
}
} finally {
Optional.ofNullable(state().extraLogStream).ifPresent(PrintStream::flush);
}
} finally {
currentTest = null;
if (extraLogStream != null) {
extraLogStream.flush();
}
}
}, stateBuilder -> {
stateBuilder.currentTest(test);
});
});
});
}
@ -218,18 +249,18 @@ public final class TKit {
}
public static Path workDir() {
return currentTest.workDir();
return currentTest().workDir();
}
static String getCurrentDefaultAppName() {
// Construct app name from swapping and joining test base name
// and test function name.
// Say the test name is `FooTest.testBasic`. Then app name would be `BasicFooTest`.
String appNamePrefix = currentTest.functionName();
String appNamePrefix = currentTest().functionName();
if (appNamePrefix != null && appNamePrefix.startsWith("test")) {
appNamePrefix = appNamePrefix.substring("test".length());
}
return Stream.of(appNamePrefix, currentTest.baseName()).filter(
return Stream.of(appNamePrefix, currentTest().baseName()).filter(
v -> v != null && !v.isEmpty()).collect(Collectors.joining());
}
@ -257,9 +288,10 @@ public final class TKit {
static void log(String v) {
v = addTimestamp(v);
System.out.println(v);
if (extraLogStream != null) {
extraLogStream.println(v);
var state = state();
state.mainLogStream.println(v);
if (state.extraLogStream != null) {
state.extraLogStream.println(v);
}
}
@ -309,13 +341,13 @@ public final class TKit {
}
public static void trace(String v) {
if (TRACE) {
if (state().trace) {
log("TRACE: " + v);
}
}
private static void traceAssert(String v) {
if (TRACE_ASSERTS) {
if (state().traceAsserts) {
log("TRACE: " + v);
}
}
@ -576,10 +608,14 @@ public final class TKit {
public static RuntimeException throwSkippedException(RuntimeException ex) {
trace("Skip the test: " + ex.getMessage());
currentTest.notifySkipped(ex);
currentTest().notifySkipped(ex);
throw ex;
}
public static boolean isSkippedException(Throwable t) {
return JtregSkippedExceptionClass.INSTANCE.isInstance(t);
}
public static Path createRelativePathCopy(final Path file) {
Path fileCopy = ThrowingSupplier.toSupplier(() -> {
Path localPath = createTempFile(file.getFileName());
@ -654,10 +690,9 @@ public final class TKit {
}
static void printStackTrace(Throwable throwable) {
if (extraLogStream != null) {
throwable.printStackTrace(extraLogStream);
}
throwable.printStackTrace();
var state = state();
Optional.ofNullable(state.extraLogStream).ifPresent(throwable::printStackTrace);
throwable.printStackTrace(state.stackTraceStream);
}
private static String concatMessages(String msg, String msg2) {
@ -668,7 +703,7 @@ public final class TKit {
}
public static void assertEquals(long expected, long actual, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (expected != actual) {
error(concatMessages(String.format(
"Expected [%d]. Actual [%d]", expected, actual),
@ -679,7 +714,7 @@ public final class TKit {
}
public static void assertNotEquals(long expected, long actual, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (expected == actual) {
error(concatMessages(String.format("Unexpected [%d] value", actual),
msg));
@ -690,7 +725,7 @@ public final class TKit {
}
public static void assertEquals(boolean expected, boolean actual, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (expected != actual) {
error(concatMessages(String.format(
"Expected [%s]. Actual [%s]", expected, actual),
@ -701,7 +736,7 @@ public final class TKit {
}
public static void assertNotEquals(boolean expected, boolean actual, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (expected == actual) {
error(concatMessages(String.format("Unexpected [%s] value", actual),
msg));
@ -713,7 +748,7 @@ public final class TKit {
public static void assertEquals(Object expected, Object actual, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if ((actual != null && !actual.equals(expected))
|| (expected != null && !expected.equals(actual))) {
error(concatMessages(String.format(
@ -725,7 +760,7 @@ public final class TKit {
}
public static void assertNotEquals(Object expected, Object actual, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if ((actual != null && !actual.equals(expected))
|| (expected != null && !expected.equals(actual))) {
@ -738,7 +773,7 @@ public final class TKit {
}
public static void assertNull(Object value, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (value != null) {
error(concatMessages(String.format("Unexpected not null value [%s]",
value), msg));
@ -748,7 +783,7 @@ public final class TKit {
}
public static void assertNotNull(Object value, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (value == null) {
error(concatMessages("Unexpected null value", msg));
}
@ -765,7 +800,7 @@ public final class TKit {
}
public static void assertTrue(boolean actual, String msg, Runnable onFail) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (!actual) {
if (onFail != null) {
onFail.run();
@ -777,7 +812,7 @@ public final class TKit {
}
public static void assertFalse(boolean actual, String msg, Runnable onFail) {
currentTest.notifyAssert();
currentTest().notifyAssert();
if (actual) {
if (onFail != null) {
onFail.run();
@ -883,7 +918,7 @@ public final class TKit {
}
public static void assertUnexpected(String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
error(concatMessages("Unexpected", msg));
}
@ -909,7 +944,7 @@ public final class TKit {
}
public void match(Set<Path> expected) {
currentTest.notifyAssert();
currentTest().notifyAssert();
var comm = Comm.compare(content, expected);
if (!comm.unique1().isEmpty() && !comm.unique2().isEmpty()) {
@ -936,7 +971,7 @@ public final class TKit {
}
public void contains(Set<Path> expected) {
currentTest.notifyAssert();
currentTest().notifyAssert();
var comm = Comm.compare(content, expected);
if (!comm.unique2().isEmpty()) {
@ -981,7 +1016,7 @@ public final class TKit {
public static void assertStringListEquals(List<String> expected,
List<String> actual, String msg) {
currentTest.notifyAssert();
currentTest().notifyAssert();
traceAssert(concatMessages("assertStringListEquals()", msg));
@ -1207,12 +1242,13 @@ public final class TKit {
}
private static PrintStream openLogStream() {
if (LOG_FILE == null) {
return null;
}
return ThrowingSupplier.toSupplier(() -> new PrintStream(
new FileOutputStream(LOG_FILE.toFile(), true))).get();
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) {
@ -1256,15 +1292,6 @@ public final class TKit {
}
}
private static TestInstance currentTest;
private static PrintStream extraLogStream;
private static final boolean TRACE;
private static final boolean TRACE_ASSERTS;
static final boolean VERBOSE_JPACKAGE;
static final boolean VERBOSE_TEST_SETUP;
static String getConfigProperty(String propertyName) {
return System.getProperty(getConfigPropertyName(propertyName));
}
@ -1292,38 +1319,19 @@ public final class TKit {
return tokens.stream().collect(Collectors.toSet());
}
static final Path LOG_FILE = Functional.identity(() -> {
String val = getConfigProperty("logfile");
if (val == null) {
return null;
}
return Path.of(val);
}).get();
static {
Set<String> logOptions = tokenizeConfigProperty("suppress-logging");
if (logOptions == null) {
TRACE = true;
TRACE_ASSERTS = true;
VERBOSE_JPACKAGE = true;
VERBOSE_TEST_SETUP = true;
} else if (logOptions.contains("all")) {
TRACE = false;
TRACE_ASSERTS = false;
VERBOSE_JPACKAGE = false;
VERBOSE_TEST_SETUP = false;
} else {
Predicate<Set<String>> isNonOf = options -> {
return Collections.disjoint(logOptions, options);
};
TRACE = isNonOf.test(Set.of("trace", "t"));
TRACE_ASSERTS = isNonOf.test(Set.of("assert", "a"));
VERBOSE_JPACKAGE = isNonOf.test(Set.of("jpackage", "jp"));
VERBOSE_TEST_SETUP = isNonOf.test(Set.of("init", "i"));
}
private static TestInstance currentTest() {
return state().currentTest;
}
static boolean verboseJPackage() {
return state().verboseJPackage;
}
static boolean verboseTestSetup() {
return state().verboseTestSetup;
}
private static final class JtregSkippedExceptionClass extends ClassLoader {
@SuppressWarnings("unchecked")
JtregSkippedExceptionClass() {
@ -1349,4 +1357,159 @@ public final class TKit {
static final Class<RuntimeException> INSTANCE = new JtregSkippedExceptionClass().clazz;
}
public static final class State {
private State(
Optional<Path> logFile,
TestInstance currentTest,
PrintStream mainLogStream,
PrintStream stackTraceStream,
PrintStream extraLogStream,
boolean trace,
boolean traceAsserts,
boolean verboseJPackage,
boolean verboseTestSetup) {
Objects.requireNonNull(logFile);
Objects.requireNonNull(mainLogStream);
Objects.requireNonNull(stackTraceStream);
this.logFile = logFile;
this.currentTest = currentTest;
this.mainLogStream = mainLogStream;
this.stackTraceStream = stackTraceStream;
this.extraLogStream = extraLogStream;
this.trace = trace;
this.traceAsserts = traceAsserts;
this.verboseJPackage = verboseJPackage;
this.verboseTestSetup = verboseTestSetup;
}
Builder buildCopy() {
return build().initFrom(this);
}
static Builder build() {
return new Builder();
}
static final class Builder {
Builder initDefaults() {
logFile = Optional.ofNullable(getConfigProperty("logfile")).map(Path::of);
currentTest = null;
mainLogStream = System.out;
stackTraceStream = System.err;
extraLogStream = null;
var logOptions = tokenizeConfigProperty("suppress-logging");
if (logOptions == null) {
trace = true;
traceAsserts = true;
verboseJPackage = true;
verboseTestSetup = true;
} else if (logOptions.contains("all")) {
trace = false;
traceAsserts = false;
verboseJPackage = false;
verboseTestSetup = false;
} else {
Predicate<Set<String>> isNonOf = options -> {
return Collections.disjoint(logOptions, options);
};
trace = isNonOf.test(Set.of("trace", "t"));
traceAsserts = isNonOf.test(Set.of("assert", "a"));
verboseJPackage = isNonOf.test(Set.of("jpackage", "jp"));
verboseTestSetup = isNonOf.test(Set.of("init", "i"));
}
return this;
}
Builder initFrom(State state) {
logFile = state.logFile;
currentTest = state.currentTest;
mainLogStream = state.mainLogStream;
stackTraceStream = state.stackTraceStream;
extraLogStream = state.extraLogStream;
trace = state.trace;
traceAsserts = state.traceAsserts;
verboseJPackage = state.verboseJPackage;
verboseTestSetup = state.verboseTestSetup;
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;
return this;
}
Builder stackTraceStream(PrintStream v) {
stackTraceStream = v;
return this;
}
Builder extraLogStream(PrintStream v) {
extraLogStream = v;
return this;
}
State create() {
return new State(logFile, currentTest, mainLogStream, stackTraceStream, extraLogStream, trace, traceAsserts, verboseJPackage, verboseTestSetup);
}
private Optional<Path> logFile;
private TestInstance currentTest;
private PrintStream mainLogStream;
private PrintStream stackTraceStream;
private PrintStream extraLogStream;
private boolean trace;
private boolean traceAsserts;
private boolean verboseJPackage;
private boolean verboseTestSetup;
}
private final Optional<Path> logFile;
private final TestInstance currentTest;
private final PrintStream mainLogStream;
private final PrintStream stackTraceStream;
private final PrintStream extraLogStream;
private final boolean trace;
private final boolean traceAsserts;
private final boolean verboseJPackage;
private final boolean verboseTestSetup;
}
private static final InheritableThreadLocal<State> STATE = new InheritableThreadLocal<>() {
@Override
protected State initialValue() {
return State.build().initDefaults().create();
}
};
}

View File

@ -369,7 +369,7 @@ final class TestBuilder implements AutoCloseable {
}
static void trace(String msg) {
if (TKit.VERBOSE_TEST_SETUP) {
if (TKit.verboseTestSetup()) {
TKit.log(msg);
}
}

View File

@ -409,7 +409,7 @@ final class TestMethodSupplier {
}
private static void trace(String msg) {
if (TKit.VERBOSE_TEST_SETUP) {
if (TKit.verboseTestSetup()) {
TKit.log(msg);
}
}

View File

@ -21,16 +21,15 @@
* questions.
*/
import static jdk.jpackage.test.WindowsHelper.killAppLauncherProcess;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import jdk.jpackage.test.JPackageCommand;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.CfgFile;
import jdk.jpackage.test.HelloApp;
import static jdk.jpackage.test.WindowsHelper.killAppLauncherProcess;
import jdk.jpackage.test.JPackageCommand;
/* @test
* @bug 8340311
@ -93,18 +92,16 @@ public class WinNoRestartTest {
// Save updated main launcher .cfg file
cfgFile.save(cmd.appLauncherCfgPath(null));
try ( // Launch the app in a separate thread
ExecutorService exec = Executors.newSingleThreadExecutor()) {
exec.execute(() -> {
HelloApp.executeLauncher(cmd);
});
// Launch the app in a separate thread
new Thread(() -> {
HelloApp.executeLauncher(cmd);
}).start();
// Wait a bit to let the app start
Thread.sleep(Duration.ofSeconds(10));
// Wait a bit to let the app start
Thread.sleep(Duration.ofSeconds(10));
// Find the main app launcher process and kill it
killAppLauncherProcess(cmd, null, expectedNoRestarted ? 1 : 2);
}
// Find the main app launcher process and kill it
killAppLauncherProcess(cmd, null, expectedNoRestarted ? 1 : 2);
}
}