8343876: Enhancements to jpackage test lib

Reviewed-by: almatvee
This commit is contained in:
Alexey Semenyuk 2024-11-17 23:46:49 +00:00
parent aa10ec7c96
commit 41a627b789
13 changed files with 1466 additions and 341 deletions

View File

@ -0,0 +1,408 @@
/*
* Copyright (c) 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 jdk.jpackage.test;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import static java.util.stream.Collectors.toMap;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import static jdk.internal.util.OperatingSystem.LINUX;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import static jdk.jpackage.test.Functional.ThrowingSupplier.toSupplier;
/*
* @test
* @summary Test jpackage test library's annotation processor
* @library /test/jdk/tools/jpackage/helpers
* @build jdk.jpackage.test.*
* @run main/othervm/timeout=360 -Xmx512m jdk.jpackage.test.AnnotationsTest
*/
public class AnnotationsTest {
public static void main(String... args) {
runTests(BasicTest.class, ParameterizedInstanceTest.class);
for (var os : OperatingSystem.values()) {
try {
TestBuilderConfig.setOperatingSystem(os);
TKit.log("Current operating system: " + os);
runTests(IfOSTest.class);
} finally {
TestBuilderConfig.setDefaults();
}
}
}
public static class BasicTest extends TestExecutionRecorder {
@Test
public void testNoArg() {
recordTestCase();
}
@Test
@Parameter("TRUE")
public int testNoArg(boolean v) {
recordTestCase(v);
return 0;
}
@Test
@Parameter({})
@Parameter("a")
@Parameter({"b", "c"})
public void testVarArg(Path ... paths) {
recordTestCase((Object[]) paths);
}
@Test
@Parameter({"12", "foo"})
@Parameter({"-89", "bar", "more"})
@Parameter({"-89", "bar", "more", "moore"})
public void testVarArg2(int a, String b, String ... other) {
recordTestCase(a, b, other);
}
@Test
@ParameterSupplier("dateSupplier")
@ParameterSupplier("jdk.jpackage.test.AnnotationsTest.dateSupplier")
public void testDates(LocalDate v) {
recordTestCase(v);
}
public static Set<String> getExpectedTestDescs() {
return Set.of(
"().testNoArg()",
"().testNoArg(true)",
"().testVarArg()",
"().testVarArg(a)",
"().testVarArg(b, c)",
"().testVarArg2(-89, bar, [more, moore](length=2))",
"().testVarArg2(-89, bar, [more](length=1))",
"().testVarArg2(12, foo, [](length=0))",
"().testDates(2018-05-05)",
"().testDates(2018-07-11)",
"().testDates(2034-05-05)",
"().testDates(2056-07-11)"
);
}
public static Collection<Object[]> dateSupplier() {
return List.of(new Object[][] {
{ LocalDate.parse("2018-05-05") },
{ LocalDate.parse("2018-07-11") },
});
}
}
public static class ParameterizedInstanceTest extends TestExecutionRecorder {
public ParameterizedInstanceTest(String... args) {
super((Object[]) args);
}
public ParameterizedInstanceTest(int o) {
super(o);
}
public ParameterizedInstanceTest(int a, Boolean[] b, String c, String ... other) {
super(a, b, c, other);
}
@Test
public void testNoArgs() {
recordTestCase();
}
@Test
@ParameterSupplier("jdk.jpackage.test.AnnotationsTest.dateSupplier")
public void testDates(LocalDate v) {
recordTestCase(v);
}
@Test
@Parameter("a")
public static void staticTest(String arg) {
staticRecorder.recordTestCase(arg);
}
@Parameters
public static Collection<Object[]> input() {
return List.of(new Object[][] {
{},
{55, new Boolean[]{false, true, false}, "foo", "bar"},
{78},
});
}
@Parameters
public static Collection<Object[]> input2() {
return List.of(new Object[][] {
{51, new boolean[]{true, true, true}, "foo"},
{33},
{55, null, null },
{55, null, null, "1" },
});
}
public static Set<String> getExpectedTestDescs() {
return Set.of(
"().testNoArgs()",
"(33).testNoArgs()",
"(78).testNoArgs()",
"(55, [false, true, false](length=3), foo, [bar](length=1)).testNoArgs()",
"(51, [true, true, true](length=3), foo, [](length=0)).testNoArgs()",
"().testDates(2034-05-05)",
"().testDates(2056-07-11)",
"(33).testDates(2034-05-05)",
"(33).testDates(2056-07-11)",
"(51, [true, true, true](length=3), foo, [](length=0)).testDates(2034-05-05)",
"(51, [true, true, true](length=3), foo, [](length=0)).testDates(2056-07-11)",
"(55, [false, true, false](length=3), foo, [bar](length=1)).testDates(2034-05-05)",
"(55, [false, true, false](length=3), foo, [bar](length=1)).testDates(2056-07-11)",
"(78).testDates(2034-05-05)",
"(78).testDates(2056-07-11)",
"(55, null, null, [1](length=1)).testDates(2034-05-05)",
"(55, null, null, [1](length=1)).testDates(2056-07-11)",
"(55, null, null, [1](length=1)).testNoArgs()",
"(55, null, null, [](length=0)).testDates(2034-05-05)",
"(55, null, null, [](length=0)).testDates(2056-07-11)",
"(55, null, null, [](length=0)).testNoArgs()",
"().staticTest(a)"
);
}
private final static TestExecutionRecorder staticRecorder = new TestExecutionRecorder(ParameterizedInstanceTest.class);
}
public static class IfOSTest extends TestExecutionRecorder {
public IfOSTest(int a, String b) {
super(a, b);
}
@Test(ifOS = OperatingSystem.LINUX)
public void testNoArgs() {
recordTestCase();
}
@Test(ifNotOS = OperatingSystem.LINUX)
public void testNoArgs2() {
recordTestCase();
}
@Test
@Parameter(value = "foo", ifOS = OperatingSystem.LINUX)
@Parameter(value = {"foo", "bar"}, ifOS = { OperatingSystem.LINUX, OperatingSystem.MACOS })
@Parameter(value = {}, ifNotOS = { OperatingSystem.WINDOWS })
public void testVarArgs(String ... args) {
recordTestCase((Object[]) args);
}
@Test
@ParameterSupplier(value = "jdk.jpackage.test.AnnotationsTest.dateSupplier", ifOS = OperatingSystem.WINDOWS)
public void testDates(LocalDate v) {
recordTestCase(v);
}
@Parameters(ifOS = OperatingSystem.LINUX)
public static Collection<Object[]> input() {
return Set.of(new Object[][] {
{7, null},
});
}
@Parameters(ifNotOS = {OperatingSystem.LINUX, OperatingSystem.MACOS})
public static Collection<Object[]> input2() {
return Set.of(new Object[][] {
{10, "hello"},
});
}
@Parameters(ifNotOS = OperatingSystem.LINUX)
public static Collection<Object[]> input3() {
return Set.of(new Object[][] {
{15, "bye"},
});
}
public static Set<String> getExpectedTestDescs() {
switch (TestBuilderConfig.getDefault().getOperatingSystem()) {
case LINUX -> {
return Set.of(
"(7, null).testNoArgs()",
"(7, null).testVarArgs()",
"(7, null).testVarArgs(foo)",
"(7, null).testVarArgs(foo, bar)"
);
}
case MACOS -> {
return Set.of(
"(15, bye).testNoArgs2()",
"(15, bye).testVarArgs()",
"(15, bye).testVarArgs(foo, bar)"
);
}
case WINDOWS -> {
return Set.of(
"(15, bye).testDates(2034-05-05)",
"(15, bye).testDates(2056-07-11)",
"(15, bye).testNoArgs2()",
"(10, hello).testDates(2034-05-05)",
"(10, hello).testDates(2056-07-11)",
"(10, hello).testNoArgs2()"
);
}
case AIX -> {
return Set.of(
);
}
}
throw new UnsupportedOperationException();
}
}
public static Collection<Object[]> dateSupplier() {
return List.of(new Object[][] {
{ LocalDate.parse("2034-05-05") },
{ LocalDate.parse("2056-07-11") },
});
}
private static void runTests(Class<? extends TestExecutionRecorder>... tests) {
ACTUAL_TEST_DESCS.get().clear();
var expectedTestDescs = Stream.of(tests)
.map(AnnotationsTest::getExpectedTestDescs)
.flatMap(x -> x)
// Collect in the map to check for collisions for free
.collect(toMap(x -> x, x -> ""))
.keySet();
var args = Stream.of(tests).map(test -> {
return String.format("--jpt-run=%s", test.getName());
}).toArray(String[]::new);
try {
Main.main(args);
assertRecordedTestDescs(expectedTestDescs);
} catch (Throwable t) {
t.printStackTrace(System.err);
System.exit(1);
}
}
private static Stream<String> getExpectedTestDescs(Class<?> type) {
return toSupplier(() -> {
var method = type.getMethod("getExpectedTestDescs");
var testDescPefix = type.getName();
return ((Set<String>)method.invoke(null)).stream().map(desc -> {
return testDescPefix + desc;
});
}).get();
}
private static void assertRecordedTestDescs(Set<String> expectedTestDescs) {
var comm = Comm.compare(expectedTestDescs, ACTUAL_TEST_DESCS.get());
if (!comm.unique1().isEmpty()) {
System.err.println("Missing test case signatures:");
comm.unique1().stream().sorted().sequential().forEachOrdered(System.err::println);
System.err.println("<>");
}
if (!comm.unique2().isEmpty()) {
System.err.println("Unexpected test case signatures:");
comm.unique2().stream().sorted().sequential().forEachOrdered(System.err::println);
System.err.println("<>");
}
if (!comm.unique2().isEmpty() || !comm.unique1().isEmpty()) {
// Don't use TKit asserts as this call is outside the test execution
throw new AssertionError("Test case signatures mismatched");
}
}
private static class TestExecutionRecorder {
protected TestExecutionRecorder(Object ... args) {
this.testClass = getClass();
this.testDescBuilder = TestInstance.TestDesc.createBuilder().ctorArgs(args);
}
TestExecutionRecorder(Class<?> testClass) {
this.testClass = testClass;
this.testDescBuilder = TestInstance.TestDesc.createBuilder().ctorArgs();
}
protected void recordTestCase(Object ... args) {
testDescBuilder.methodArgs(args).method(getCurrentTestCase());
var testCaseDescs = ACTUAL_TEST_DESCS.get();
var testCaseDesc = testDescBuilder.get().testFullName();
TKit.assertTrue(!testCaseDescs.contains(testCaseDesc), String.format(
"Check this test case is executed for the first time",
testCaseDesc));
TKit.assertTrue(!executed, "Check this test case instance is not reused");
executed = true;
testCaseDescs.add(testCaseDesc);
}
private Method getCurrentTestCase() {
return StackWalker.getInstance(RETAIN_CLASS_REFERENCE).walk(frames -> {
return frames.map(frame -> {
var methodType = frame.getMethodType();
var methodName = frame.getMethodName();
var methodReturn = methodType.returnType();
var methodParameters = methodType.parameterArray();
return Stream.of(testClass.getDeclaredMethods()).filter(method -> {
return method.getName().equals(methodName)
&& method.getReturnType().equals(methodReturn)
&& Arrays.equals(method.getParameterTypes(), methodParameters)
&& method.isAnnotationPresent(Test.class);
}).findFirst();
}).dropWhile(Optional::isEmpty).map(Optional::get).findFirst();
}).get();
}
private boolean executed;
private final TestInstance.TestDesc.Builder testDescBuilder;
private final Class<?> testClass;
}
private static final ThreadLocal<Set<String>> ACTUAL_TEST_DESCS = new ThreadLocal<>() {
@Override
protected Set<String> initialValue() {
return new HashSet<>();
}
};
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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
@ -27,6 +27,7 @@ import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jdk.internal.util.OperatingSystem;
public class Annotations {
@ -43,6 +44,14 @@ public class Annotations {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
OperatingSystem[] ifOS() default {
OperatingSystem.LINUX,
OperatingSystem.WINDOWS,
OperatingSystem.MACOS
};
OperatingSystem[] ifNotOS() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@ -51,6 +60,14 @@ public class Annotations {
public @interface Parameter {
String[] value();
OperatingSystem[] ifOS() default {
OperatingSystem.LINUX,
OperatingSystem.WINDOWS,
OperatingSystem.MACOS
};
OperatingSystem[] ifNotOS() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@ -60,8 +77,39 @@ public class Annotations {
Parameter[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ParameterSupplierGroup.class)
public @interface ParameterSupplier {
String value();
OperatingSystem[] ifOS() default {
OperatingSystem.LINUX,
OperatingSystem.WINDOWS,
OperatingSystem.MACOS
};
OperatingSystem[] ifNotOS() default {};
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ParameterSupplierGroup {
ParameterSupplier[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Parameters {
OperatingSystem[] ifOS() default {
OperatingSystem.LINUX,
OperatingSystem.WINDOWS,
OperatingSystem.MACOS
};
OperatingSystem[] ifNotOS() default {};
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 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 jdk.jpackage.test;
import java.util.HashSet;
import java.util.Set;
record Comm<T>(Set<T> common, Set<T> unique1, Set<T> unique2) {
static <T> Comm<T> compare(Set<T> a, Set<T> b) {
Set<T> common = new HashSet<>(a);
common.retainAll(b);
Set<T> unique1 = new HashSet<>(a);
unique1.removeAll(common);
Set<T> unique2 = new HashSet<>(b);
unique2.removeAll(common);
return new Comm(common, unique1, unique2);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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
@ -40,6 +40,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.internal.ApplicationLayout;
import jdk.jpackage.internal.IOUtils;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.PackageTest.PackageHandlers;
@ -399,6 +400,15 @@ public final class LinuxHelper {
private static void verifyDesktopFile(JPackageCommand cmd, Path desktopFile)
throws IOException {
TKit.trace(String.format("Check [%s] file BEGIN", desktopFile));
var launcherName = Stream.of(List.of(cmd.name()), cmd.addLauncherNames()).flatMap(List::stream).filter(name -> {
return getDesktopFile(cmd, name).equals(desktopFile);
}).findAny();
if (!cmd.hasArgument("--app-image")) {
TKit.assertTrue(launcherName.isPresent(),
"Check the desktop file corresponds to one of app launchers");
}
List<String> lines = Files.readAllLines(desktopFile);
TKit.assertEquals("[Desktop Entry]", lines.get(0), "Check file header");
@ -428,7 +438,7 @@ public final class LinuxHelper {
"Check value of [%s] key", key));
}
// Verify value of `Exec` property in .desktop files are escaped if required
// Verify the value of `Exec` key is escaped if required
String launcherPath = data.get("Exec");
if (Pattern.compile("\\s").matcher(launcherPath).find()) {
TKit.assertTrue(launcherPath.startsWith("\"")
@ -437,10 +447,25 @@ public final class LinuxHelper {
launcherPath = launcherPath.substring(1, launcherPath.length() - 1);
}
Stream.of(launcherPath, data.get("Icon"))
.map(Path::of)
.map(cmd::pathToUnpackedPackageFile)
.forEach(TKit::assertFileExists);
if (launcherName.isPresent()) {
TKit.assertEquals(launcherPath, cmd.pathToPackageFile(
cmd.appLauncherPath(launcherName.get())).toString(),
String.format(
"Check the value of [Exec] key references [%s] app launcher",
launcherName.get()));
}
for (var e : List.<Map.Entry<Map.Entry<String, Optional<String>>, Function<ApplicationLayout, Path>>>of(
Map.entry(Map.entry("Exec", Optional.of(launcherPath)), ApplicationLayout::launchersDirectory),
Map.entry(Map.entry("Icon", Optional.empty()), ApplicationLayout::destktopIntegrationDirectory))) {
var path = e.getKey().getValue().or(() -> Optional.of(data.get(
e.getKey().getKey()))).map(Path::of).get();
TKit.assertFileExists(cmd.pathToUnpackedPackageFile(path));
Path expectedDir = cmd.pathToPackageFile(e.getValue().apply(cmd.appLayout()));
TKit.assertTrue(path.getParent().equals(expectedDir), String.format(
"Check the value of [%s] key references a file in [%s] folder",
e.getKey().getKey(), expectedDir));
}
TKit.trace(String.format("Check [%s] file END", desktopFile));
}
@ -725,10 +750,10 @@ public final class LinuxHelper {
private static Map<PackageType, String> archs;
private final static Pattern XDG_CMD_ICON_SIZE_PATTERN = Pattern.compile("\\s--size\\s+(\\d+)\\b");
private static final Pattern XDG_CMD_ICON_SIZE_PATTERN = Pattern.compile("\\s--size\\s+(\\d+)\\b");
// Values grabbed from https://linux.die.net/man/1/xdg-icon-resource
private final static Set<Integer> XDG_CMD_VALID_ICON_SIZES = Set.of(16, 22, 32, 48, 64, 128);
private static final Set<Integer> XDG_CMD_VALID_ICON_SIZES = Set.of(16, 22, 32, 48, 64, 128);
private final static Method getServiceUnitFileName = initGetServiceUnitFileName();
private static final Method getServiceUnitFileName = initGetServiceUnitFileName();
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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
@ -23,11 +23,17 @@
package jdk.jpackage.test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.toCollection;
import java.util.stream.Stream;
import static jdk.jpackage.test.TestBuilder.CMDLINE_ARG_PREFIX;
@ -36,7 +42,9 @@ public final class Main {
boolean listTests = false;
List<TestInstance> tests = new ArrayList<>();
try (TestBuilder testBuilder = new TestBuilder(tests::add)) {
for (var arg : args) {
Deque<String> argsAsList = new ArrayDeque<>(List.of(args));
while (!argsAsList.isEmpty()) {
var arg = argsAsList.pop();
TestBuilder.trace(String.format("Parsing [%s]...", arg));
if ((CMDLINE_ARG_PREFIX + "list").equals(arg)) {
@ -44,6 +52,29 @@ public final class Main {
continue;
}
if (arg.startsWith("@")) {
// Command file
// @=args will read arguments from the "args" file, one argument per line
// @args will read arguments from the "args" file, splitting lines into arguments at whitespaces
arg = arg.substring(1);
var oneArgPerLine = arg.startsWith("=");
if (oneArgPerLine) {
arg = arg.substring(1);
}
var newArgsStream = Files.readAllLines(Path.of(arg)).stream();
if (!oneArgPerLine) {
newArgsStream.map(line -> {
return Stream.of(line.split("\\s+"));
}).flatMap(x -> x);
}
var newArgs = newArgsStream.collect(toCollection(ArrayDeque::new));
newArgs.addAll(argsAsList);
argsAsList = newArgs;
continue;
}
boolean success = false;
try {
testBuilder.processCmdLineArg(arg);
@ -62,12 +93,11 @@ public final class Main {
// Order tests by their full names to have stable test sequence.
List<TestInstance> orderedTests = tests.stream()
.sorted((a, b) -> a.fullName().compareTo(b.fullName()))
.collect(Collectors.toList());
.sorted(Comparator.comparing(TestInstance::fullName)).toList();
if (listTests) {
// Just list the tests
orderedTests.stream().forEach(test -> System.out.println(String.format(
orderedTests.forEach(test -> System.out.println(String.format(
"%s; workDir=[%s]", test.fullName(), test.workDir())));
return;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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
@ -22,34 +22,33 @@
*/
package jdk.jpackage.test;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.TestInstance.TestDesc;
class MethodCall implements ThrowingConsumer {
MethodCall(Object[] instanceCtorArgs, Method method) {
this.ctorArgs = Optional.ofNullable(instanceCtorArgs).orElse(
DEFAULT_CTOR_ARGS);
this.method = method;
this.methodArgs = new Object[0];
}
MethodCall(Object[] instanceCtorArgs, Method method, Object ... args) {
Objects.requireNonNull(instanceCtorArgs);
Objects.requireNonNull(method);
MethodCall(Object[] instanceCtorArgs, Method method, Object arg) {
this.ctorArgs = Optional.ofNullable(instanceCtorArgs).orElse(
DEFAULT_CTOR_ARGS);
this.ctorArgs = instanceCtorArgs;
this.method = method;
this.methodArgs = new Object[]{arg};
this.methodArgs = args;
}
TestDesc createDescription() {
@ -76,68 +75,35 @@ class MethodCall implements ThrowingConsumer {
return null;
}
Constructor ctor = findRequiredConstructor(method.getDeclaringClass(),
ctorArgs);
if (ctor.isVarArgs()) {
// Assume constructor doesn't have fixed, only variable parameters.
return ctor.newInstance(new Object[]{ctorArgs});
}
var ctor = findMatchingConstructor(method.getDeclaringClass(), ctorArgs);
return ctor.newInstance(ctorArgs);
return ctor.newInstance(mapArgs(ctor, ctorArgs));
}
static Object[] mapArgs(Executable executable, final Object ... args) {
return mapPrimitiveTypeArgs(executable, mapVarArgs(executable, args));
}
void checkRequiredConstructor() throws NoSuchMethodException {
if ((method.getModifiers() & Modifier.STATIC) == 0) {
findRequiredConstructor(method.getDeclaringClass(), ctorArgs);
findMatchingConstructor(method.getDeclaringClass(), ctorArgs);
}
}
private static Constructor findVarArgConstructor(Class type) {
return Stream.of(type.getConstructors()).filter(
Constructor::isVarArgs).findFirst().orElse(null);
}
private Constructor findRequiredConstructor(Class type, Object... ctorArgs)
private static Constructor findMatchingConstructor(Class type, Object... ctorArgs)
throws NoSuchMethodException {
Supplier<NoSuchMethodException> notFoundException = () -> {
return new NoSuchMethodException(String.format(
var ctors = filterMatchingExecutablesForParameterValues(Stream.of(
type.getConstructors()), ctorArgs).toList();
if (ctors.size() != 1) {
// No public constructors that can handle the given arguments.
throw new NoSuchMethodException(String.format(
"No public contructor in %s for %s arguments", type,
Arrays.deepToString(ctorArgs)));
};
if (Stream.of(ctorArgs).allMatch(Objects::nonNull)) {
// No `null` in constructor args, take easy path
try {
return type.getConstructor(Stream.of(ctorArgs).map(
Object::getClass).collect(Collectors.toList()).toArray(
Class[]::new));
} catch (NoSuchMethodException ex) {
// Failed to find ctor that can take the given arguments.
Constructor varArgCtor = findVarArgConstructor(type);
if (varArgCtor != null) {
// There is one with variable number of arguments. Use it.
return varArgCtor;
}
throw notFoundException.get();
}
}
List<Constructor> ctors = Stream.of(type.getConstructors())
.filter(ctor -> ctor.getParameterCount() == ctorArgs.length)
.collect(Collectors.toList());
if (ctors.isEmpty()) {
// No public constructors that can handle the given arguments.
throw notFoundException.get();
}
if (ctors.size() == 1) {
return ctors.iterator().next();
}
// Revisit this tricky case when it will start bothering.
throw notFoundException.get();
return ctors.get(0);
}
@Override
@ -145,9 +111,159 @@ class MethodCall implements ThrowingConsumer {
method.invoke(thiz, methodArgs);
}
private static Object[] mapVarArgs(Executable executable, final Object ... args) {
if (executable.isVarArgs()) {
var paramTypes = executable.getParameterTypes();
Class varArgParamType = paramTypes[paramTypes.length - 1];
Object[] newArgs;
if (paramTypes.length - args.length == 1) {
// Empty var args
// "args" can be of type String[] if the "executable" is "foo(String ... str)"
newArgs = Arrays.copyOf(args, args.length + 1, Object[].class);
newArgs[newArgs.length - 1] = Array.newInstance(varArgParamType.componentType(), 0);
} else {
var varArgs = Arrays.copyOfRange(args, paramTypes.length - 1,
args.length, varArgParamType);
// "args" can be of type String[] if the "executable" is "foo(String ... str)"
newArgs = Arrays.copyOfRange(args, 0, paramTypes.length, Object[].class);
newArgs[newArgs.length - 1] = varArgs;
}
return newArgs;
}
return args;
}
private static Object[] mapPrimitiveTypeArgs(Executable executable, final Object ... args) {
var paramTypes = executable.getParameterTypes();
if (paramTypes.length != args.length) {
throw new IllegalArgumentException(
"The number of arguments must be equal to the number of parameters of the executable");
}
if (IntStream.range(0, args.length).allMatch(idx -> {
return Optional.ofNullable(args[idx]).map(Object::getClass).map(paramTypes[idx]::isAssignableFrom).orElse(true);
})) {
return args;
} else {
final var newArgs = Arrays.copyOf(args, args.length, Object[].class);
for (var idx = 0; idx != args.length; ++idx) {
final var paramType = paramTypes[idx];
final var argValue = args[idx];
newArgs[idx] = Optional.ofNullable(argValue).map(Object::getClass).map(argType -> {
if(argType.isArray() && !paramType.isAssignableFrom(argType)) {
var length = Array.getLength(argValue);
var newArray = Array.newInstance(paramType.getComponentType(), length);
for (var arrayIdx = 0; arrayIdx != length; ++arrayIdx) {
Array.set(newArray, arrayIdx, Array.get(argValue, arrayIdx));
}
return newArray;
} else {
return argValue;
}
}).orElse(argValue);
}
return newArgs;
}
}
private static <T extends Executable> Stream<T> filterMatchingExecutablesForParameterValues(
Stream<T> executables, Object... args) {
return filterMatchingExecutablesForParameterTypes(
executables,
Stream.of(args)
.map(arg -> arg != null ? arg.getClass() : null)
.toArray(Class[]::new));
}
private static <T extends Executable> Stream<T> filterMatchingExecutablesForParameterTypes(
Stream<T> executables, Class<?>... argTypes) {
return executables.filter(executable -> {
var parameterTypes = executable.getParameterTypes();
final int checkArgTypeCount;
if (parameterTypes.length <= argTypes.length) {
checkArgTypeCount = parameterTypes.length;
} else if (parameterTypes.length - argTypes.length == 1 && executable.isVarArgs()) {
// Empty optional arguments.
checkArgTypeCount = argTypes.length;
} else {
// Not enough mandatory arguments.
return false;
}
var unmatched = IntStream.range(0, checkArgTypeCount).dropWhile(idx -> {
return new ParameterTypeMatcher(parameterTypes[idx]).test(argTypes[idx]);
}).toArray();
if (argTypes.length == parameterTypes.length && unmatched.length == 0) {
// Number of argument types equals to the number of parameters
// of the executable and all types match.
return true;
}
if (executable.isVarArgs()) {
var varArgType = parameterTypes[parameterTypes.length - 1].componentType();
return IntStream.of(unmatched).allMatch(idx -> {
return new ParameterTypeMatcher(varArgType).test(argTypes[idx]);
});
}
return false;
});
}
private static final class ParameterTypeMatcher implements Predicate<Class<?>> {
ParameterTypeMatcher(Class<?> parameterType) {
Objects.requireNonNull(parameterType);
this.parameterType = NORM_TYPES.getOrDefault(parameterType, parameterType);
}
@Override
public boolean test(Class<?> paramaterValueType) {
if (paramaterValueType == null) {
return true;
}
paramaterValueType = NORM_TYPES.getOrDefault(paramaterValueType, paramaterValueType);
return parameterType.isAssignableFrom(paramaterValueType);
}
private final Class<?> parameterType;
}
private final Object[] methodArgs;
private final Method method;
private final Object[] ctorArgs;
final static Object[] DEFAULT_CTOR_ARGS = new Object[0];
private static final Map<Class<?>, Class<?>> NORM_TYPES;
static {
Map<Class<?>, Class<?>> primitives = Map.of(
boolean.class, Boolean.class,
byte.class, Byte.class,
short.class, Short.class,
int.class, Integer.class,
long.class, Long.class,
float.class, Float.class,
double.class, Double.class);
Map<Class<?>, Class<?>> primitiveArrays = Map.of(
boolean[].class, Boolean[].class,
byte[].class, Byte[].class,
short[].class, Short[].class,
int[].class, Integer[].class,
long[].class, Long[].class,
float[].class, Float[].class,
double[].class, Double[].class);
Map<Class<?>, Class<?>> combined = new HashMap<>(primitives);
combined.putAll(primitiveArrays);
NORM_TYPES = Collections.unmodifiableMap(combined);
}
}

View File

@ -781,18 +781,18 @@ public final class TKit {
currentTest.notifyAssert();
var comm = Comm.compare(content, expected);
if (!comm.unique1.isEmpty() && !comm.unique2.isEmpty()) {
if (!comm.unique1().isEmpty() && !comm.unique2().isEmpty()) {
error(String.format(
"assertDirectoryContentEquals(%s): Some expected %s. Unexpected %s. Missing %s",
baseDir, format(comm.common), format(comm.unique1), format(comm.unique2)));
} else if (!comm.unique1.isEmpty()) {
baseDir, format(comm.common()), format(comm.unique1()), format(comm.unique2())));
} else if (!comm.unique1().isEmpty()) {
error(String.format(
"assertDirectoryContentEquals(%s): Expected %s. Unexpected %s",
baseDir, format(comm.common), format(comm.unique1)));
} else if (!comm.unique2.isEmpty()) {
baseDir, format(comm.common()), format(comm.unique1())));
} else if (!comm.unique2().isEmpty()) {
error(String.format(
"assertDirectoryContentEquals(%s): Some expected %s. Missing %s",
baseDir, format(comm.common), format(comm.unique2)));
baseDir, format(comm.common()), format(comm.unique2())));
} else {
traceAssert(String.format(
"assertDirectoryContentEquals(%s): Expected %s",
@ -808,10 +808,10 @@ public final class TKit {
currentTest.notifyAssert();
var comm = Comm.compare(content, expected);
if (!comm.unique2.isEmpty()) {
if (!comm.unique2().isEmpty()) {
error(String.format(
"assertDirectoryContentContains(%s): Some expected %s. Missing %s",
baseDir, format(comm.common), format(comm.unique2)));
baseDir, format(comm.common()), format(comm.unique2())));
} else {
traceAssert(String.format(
"assertDirectoryContentContains(%s): Expected %s",
@ -838,21 +838,6 @@ public final class TKit {
this.content = contents;
}
private static record Comm(Set<Path> common, Set<Path> unique1, Set<Path> unique2) {
static Comm compare(Set<Path> a, Set<Path> b) {
Set<Path> common = new HashSet<>(a);
common.retainAll(b);
Set<Path> unique1 = new HashSet<>(a);
unique1.removeAll(common);
Set<Path> unique2 = new HashSet<>(b);
unique2.removeAll(common);
return new Comm(common, unique1, unique2);
}
}
private static String format(Set<Path> paths) {
return Arrays.toString(
paths.stream().sorted().map(Path::toString).toArray(

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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
@ -23,32 +23,28 @@
package jdk.jpackage.test;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.jpackage.test.Annotations.AfterEach;
import jdk.jpackage.test.Annotations.BeforeEach;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterGroup;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import static jdk.jpackage.test.Functional.ThrowingConsumer.toConsumer;
import jdk.jpackage.test.Functional.ThrowingFunction;
import jdk.jpackage.test.TestMethodSupplier.InvalidAnnotationException;
import static jdk.jpackage.test.TestMethodSupplier.MethodQuery.fromQualifiedMethodName;
final class TestBuilder implements AutoCloseable {
@ -58,6 +54,7 @@ final class TestBuilder implements AutoCloseable {
}
TestBuilder(Consumer<TestInstance> testConsumer) {
this.testMethodSupplier = TestBuilderConfig.getDefault().createTestMethodSupplier();
argProcessors = Map.of(
CMDLINE_ARG_PREFIX + "after-run",
arg -> getJavaMethodsFromArg(arg).map(
@ -70,7 +67,7 @@ final class TestBuilder implements AutoCloseable {
CMDLINE_ARG_PREFIX + "run",
arg -> addTestGroup(getJavaMethodsFromArg(arg).map(
ThrowingFunction.toFunction(
TestBuilder::toMethodCalls)).flatMap(s -> s).collect(
this::toMethodCalls)).flatMap(s -> s).collect(
Collectors.toList())),
CMDLINE_ARG_PREFIX + "exclude",
@ -219,23 +216,29 @@ final class TestBuilder implements AutoCloseable {
.filter(m -> m.getParameterCount() == 0)
.filter(m -> !m.isAnnotationPresent(Test.class))
.filter(m -> m.isAnnotationPresent(annotationType))
.sorted((a, b) -> a.getName().compareTo(b.getName()));
.sorted(Comparator.comparing(Method::getName));
}
private static Stream<String> cmdLineArgValueToMethodNames(String v) {
private Stream<String> cmdLineArgValueToMethodNames(String v) {
List<String> result = new ArrayList<>();
String defaultClassName = null;
for (String token : v.split(",")) {
Class testSet = probeClass(token);
if (testSet != null) {
if (testMethodSupplier.isTestClass(testSet)) {
toConsumer(testMethodSupplier::verifyTestClass).accept(testSet);
}
// Test set class specified. Pull in all public methods
// from the class with @Test annotation removing name duplicates.
// Overloads will be handled at the next phase of processing.
defaultClassName = token;
Stream.of(testSet.getMethods()).filter(
m -> m.isAnnotationPresent(Test.class)).map(
Method::getName).distinct().forEach(
name -> result.add(String.join(".", token, name)));
result.addAll(Stream.of(testSet.getMethods())
.filter(m -> m.isAnnotationPresent(Test.class))
.filter(testMethodSupplier::isEnabled)
.map(Method::getName).distinct()
.map(name -> String.join(".", token, name))
.toList());
continue;
}
@ -246,7 +249,7 @@ final class TestBuilder implements AutoCloseable {
qualifiedMethodName = token;
defaultClassName = token.substring(0, lastDotIdx);
} else if (defaultClassName == null) {
throw new ParseException("Default class name not found in");
throw new ParseException("Missing default class name in");
} else {
qualifiedMethodName = String.join(".", defaultClassName, token);
}
@ -255,155 +258,43 @@ final class TestBuilder implements AutoCloseable {
return result.stream();
}
private static boolean filterMethod(String expectedMethodName, Method method) {
if (!method.getName().equals(expectedMethodName)) {
return false;
}
switch (method.getParameterCount()) {
case 0:
return !isParametrized(method);
case 1:
return isParametrized(method);
}
return false;
}
private static boolean isParametrized(Method method) {
return method.isAnnotationPresent(ParameterGroup.class) || method.isAnnotationPresent(
Parameter.class);
}
private static List<Method> getJavaMethodFromString(
String qualifiedMethodName) {
private List<Method> getJavaMethodFromString(String qualifiedMethodName) {
int lastDotIdx = qualifiedMethodName.lastIndexOf('.');
if (lastDotIdx == -1) {
throw new ParseException("Class name not found in");
throw new ParseException("Missing class name in");
}
String className = qualifiedMethodName.substring(0, lastDotIdx);
String methodName = qualifiedMethodName.substring(lastDotIdx + 1);
Class methodClass;
try {
methodClass = Class.forName(className);
} catch (ClassNotFoundException ex) {
throw new ParseException(String.format("Class [%s] not found;",
className));
return testMethodSupplier.findNullaryLikeMethods(
fromQualifiedMethodName(qualifiedMethodName));
} catch (NoSuchMethodException ex) {
throw new ParseException(ex.getMessage() + ";", ex);
}
// Get the list of all public methods as need to deal with overloads.
List<Method> methods = Stream.of(methodClass.getMethods()).filter(
(m) -> filterMethod(methodName, m)).collect(Collectors.toList());
if (methods.isEmpty()) {
throw new ParseException(String.format(
"Method [%s] not found in [%s] class;",
methodName, className));
}
trace(String.format("%s -> %s", qualifiedMethodName, methods));
return methods;
}
private static Stream<Method> getJavaMethodsFromArg(String argValue) {
return cmdLineArgValueToMethodNames(argValue).map(
ThrowingFunction.toFunction(
TestBuilder::getJavaMethodFromString)).flatMap(
List::stream).sequential();
private Stream<Method> getJavaMethodsFromArg(String argValue) {
var methods = cmdLineArgValueToMethodNames(argValue)
.map(this::getJavaMethodFromString)
.flatMap(List::stream).toList();
trace(String.format("%s -> %s", argValue, methods));
return methods.stream();
}
private static Parameter[] getMethodParameters(Method method) {
if (method.isAnnotationPresent(ParameterGroup.class)) {
return ((ParameterGroup) method.getAnnotation(ParameterGroup.class)).value();
}
if (method.isAnnotationPresent(Parameter.class)) {
return new Parameter[]{(Parameter) method.getAnnotation(
Parameter.class)};
}
// Unexpected
return null;
}
private static Stream<Object[]> toCtorArgs(Method method) throws
IllegalAccessException, InvocationTargetException {
Class type = method.getDeclaringClass();
List<Method> paremetersProviders = Stream.of(type.getMethods())
.filter(m -> m.getParameterCount() == 0)
.filter(m -> (m.getModifiers() & Modifier.STATIC) != 0)
.filter(m -> m.isAnnotationPresent(Parameters.class))
.sorted()
.collect(Collectors.toList());
if (paremetersProviders.isEmpty()) {
// Single instance using the default constructor.
return Stream.ofNullable(MethodCall.DEFAULT_CTOR_ARGS);
}
// Pick the first method from the list.
Method paremetersProvider = paremetersProviders.iterator().next();
if (paremetersProviders.size() > 1) {
trace(String.format(
"Found %d public static methods without arguments with %s annotation. Will use %s",
paremetersProviders.size(), Parameters.class,
paremetersProvider));
paremetersProviders.stream().map(Method::toString).forEach(
TestBuilder::trace);
}
// Construct collection of arguments for test class instances.
return ((Collection) paremetersProvider.invoke(null)).stream();
}
private static Stream<MethodCall> toMethodCalls(Method method) throws
IllegalAccessException, InvocationTargetException {
return toCtorArgs(method).map(v -> toMethodCalls(v, method)).flatMap(
s -> s).peek(methodCall -> {
// Make sure required constructor is accessible if the one is needed.
// Need to probe all methods as some of them might be static
// and some class members.
// Only class members require ctors.
try {
methodCall.checkRequiredConstructor();
} catch (NoSuchMethodException ex) {
throw new ParseException(ex.getMessage() + ".");
}
});
}
private static Stream<MethodCall> toMethodCalls(Object[] ctorArgs, Method method) {
if (!isParametrized(method)) {
return Stream.of(new MethodCall(ctorArgs, method));
}
Parameter[] annotations = getMethodParameters(method);
if (annotations.length == 0) {
return Stream.of(new MethodCall(ctorArgs, method));
}
return Stream.of(annotations).map((a) -> {
Class paramType = method.getParameterTypes()[0];
final Object annotationValue;
if (!paramType.isArray()) {
annotationValue = fromString(a.value()[0], paramType);
} else {
Class paramComponentType = paramType.getComponentType();
annotationValue = Array.newInstance(paramComponentType, a.value().length);
var idx = new AtomicInteger(-1);
Stream.of(a.value()).map(v -> fromString(v, paramComponentType)).sequential().forEach(
v -> Array.set(annotationValue, idx.incrementAndGet(), v));
private Stream<MethodCall> toMethodCalls(Method method) throws
IllegalAccessException, InvocationTargetException, InvalidAnnotationException {
return testMethodSupplier.mapToMethodCalls(method).peek(methodCall -> {
// Make sure required constructor is accessible if the one is needed.
// Need to probe all methods as some of them might be static
// and some class members.
// Only class members require ctors.
try {
methodCall.checkRequiredConstructor();
} catch (NoSuchMethodException ex) {
throw new ParseException(ex.getMessage() + ".", ex);
}
return new MethodCall(ctorArgs, method, annotationValue);
});
}
private static Object fromString(String value, Class toType) {
if (toType.isEnum()) {
return Enum.valueOf(toType, value);
}
Function<String, Object> converter = conv.get(toType);
if (converter == null) {
throw new RuntimeException(String.format(
"Failed to find a conversion of [%s] string to %s type",
value, toType));
}
return converter.apply(value);
}
// Wraps Method.invike() into ThrowingRunnable.run()
private ThrowingConsumer wrap(Method method) {
return (test) -> {
@ -427,6 +318,10 @@ final class TestBuilder implements AutoCloseable {
super(msg);
}
ParseException(String msg, Exception ex) {
super(msg, ex);
}
void setContext(String badCmdLineArg) {
this.badCmdLineArg = badCmdLineArg;
}
@ -448,8 +343,9 @@ final class TestBuilder implements AutoCloseable {
}
}
private final TestMethodSupplier testMethodSupplier;
private final Map<String, ThrowingConsumer<String>> argProcessors;
private Consumer<TestInstance> testConsumer;
private final Consumer<TestInstance> testConsumer;
private List<MethodCall> testGroup;
private List<ThrowingConsumer> beforeActions;
private List<ThrowingConsumer> afterActions;
@ -458,14 +354,5 @@ final class TestBuilder implements AutoCloseable {
private String spaceSubstitute;
private boolean dryRun;
private final static Map<Class, Function<String, Object>> conv = Map.of(
boolean.class, Boolean::valueOf,
Boolean.class, Boolean::valueOf,
int.class, Integer::valueOf,
Integer.class, Integer::valueOf,
long.class, Long::valueOf,
Long.class, Long::valueOf,
String.class, String::valueOf);
final static String CMDLINE_ARG_PREFIX = "--jpt-";
static final String CMDLINE_ARG_PREFIX = "--jpt-";
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 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 jdk.jpackage.test;
import java.util.Objects;
import jdk.internal.util.OperatingSystem;
final class TestBuilderConfig {
TestBuilderConfig() {
}
TestMethodSupplier createTestMethodSupplier() {
return new TestMethodSupplier(os);
}
OperatingSystem getOperatingSystem() {
return os;
}
static TestBuilderConfig getDefault() {
return DEFAULT.get();
}
static void setOperatingSystem(OperatingSystem os) {
Objects.requireNonNull(os);
DEFAULT.get().os = os;
}
static void setDefaults() {
DEFAULT.set(new TestBuilderConfig());
}
private OperatingSystem os = OperatingSystem.current();
private static final ThreadLocal<TestBuilderConfig> DEFAULT = new ThreadLocal<>() {
@Override
protected TestBuilderConfig initialValue() {
return new TestBuilderConfig();
}
};
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 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
@ -32,8 +32,10 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -50,7 +52,7 @@ final class TestInstance implements ThrowingRunnable {
String testFullName() {
StringBuilder sb = new StringBuilder();
sb.append(clazz.getSimpleName());
sb.append(clazz.getName());
if (instanceArgs != null) {
sb.append('(').append(instanceArgs).append(')');
}
@ -78,12 +80,12 @@ final class TestInstance implements ThrowingRunnable {
}
Builder ctorArgs(Object... v) {
ctorArgs = ofNullable(v);
ctorArgs = Arrays.asList(v);
return this;
}
Builder methodArgs(Object... v) {
methodArgs = ofNullable(v);
methodArgs = Arrays.asList(v);
return this;
}
@ -107,22 +109,18 @@ final class TestInstance implements ThrowingRunnable {
}
return values.stream().map(v -> {
if (v != null && v.getClass().isArray()) {
return String.format("%s(length=%d)",
Arrays.deepToString((Object[]) v),
Array.getLength(v));
String asString;
if (v.getClass().getComponentType().isPrimitive()) {
asString = PRIMITIVE_ARRAY_FORMATTERS.get(v.getClass()).apply(v);
} else {
asString = Arrays.deepToString((Object[]) v);
}
return String.format("%s(length=%d)", asString, Array.getLength(v));
}
return String.format("%s", v);
}).collect(Collectors.joining(", "));
}
private static List<Object> ofNullable(Object... values) {
List<Object> result = new ArrayList();
for (var v: values) {
result.add(v);
}
return result;
}
private List<Object> ctorArgs;
private List<Object> methodArgs;
private Method method;
@ -331,7 +329,7 @@ final class TestInstance implements ThrowingRunnable {
private final boolean dryRun;
private final Path workDir;
private final static Set<Status> KEEP_WORK_DIR = Functional.identity(
private static final Set<Status> KEEP_WORK_DIR = Functional.identity(
() -> {
final String propertyName = "keep-work-dir";
Set<String> keepWorkDir = TKit.tokenizeConfigProperty(
@ -355,4 +353,15 @@ final class TestInstance implements ThrowingRunnable {
return Collections.unmodifiableSet(result);
}).get();
private static final Map<Class<?>, Function<Object, String>> PRIMITIVE_ARRAY_FORMATTERS = Map.of(
boolean[].class, v -> Arrays.toString((boolean[])v),
byte[].class, v -> Arrays.toString((byte[])v),
char[].class, v -> Arrays.toString((char[])v),
short[].class, v -> Arrays.toString((short[])v),
int[].class, v -> Arrays.toString((int[])v),
long[].class, v -> Arrays.toString((long[])v),
float[].class, v -> Arrays.toString((float[])v),
double[].class, v -> Arrays.toString((double[])v)
);
}

View File

@ -0,0 +1,510 @@
/*
* Copyright (c) 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 jdk.jpackage.test;
import java.lang.annotation.Annotation;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.ParameterGroup;
import jdk.jpackage.test.Annotations.ParameterSupplier;
import jdk.jpackage.test.Annotations.ParameterSupplierGroup;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import static jdk.jpackage.test.Functional.ThrowingFunction.toFunction;
import static jdk.jpackage.test.Functional.ThrowingSupplier.toSupplier;
import static jdk.jpackage.test.MethodCall.mapArgs;
final class TestMethodSupplier {
TestMethodSupplier(OperatingSystem os) {
Objects.requireNonNull(os);
this.os = os;
}
record MethodQuery(String className, String methodName) {
List<Method> lookup() throws ClassNotFoundException {
final Class methodClass = Class.forName(className);
// Get the list of all public methods as need to deal with overloads.
return Stream.of(methodClass.getMethods()).filter(method -> {
return method.getName().equals(methodName);
}).toList();
}
static MethodQuery fromQualifiedMethodName(String qualifiedMethodName) {
int lastDotIdx = qualifiedMethodName.lastIndexOf('.');
if (lastDotIdx == -1) {
throw new IllegalArgumentException("Class name not specified");
}
var className = qualifiedMethodName.substring(0, lastDotIdx);
var methodName = qualifiedMethodName.substring(lastDotIdx + 1);
return new MethodQuery(className, methodName);
}
}
List<Method> findNullaryLikeMethods(MethodQuery query) throws NoSuchMethodException {
List<Method> methods;
try {
methods = query.lookup();
} catch (ClassNotFoundException ex) {
throw new NoSuchMethodException(
String.format("Class [%s] not found", query.className()));
}
if (methods.isEmpty()) {
throw new NoSuchMethodException(String.format(
"Public method [%s] not found in [%s] class",
query.methodName(), query.className()));
}
methods = methods.stream().filter(method -> {
if (isParameterized(method) && isTest(method)) {
// Always accept test method with annotations producing arguments for its invocation.
return true;
} else {
return method.getParameterCount() == 0;
}
}).filter(this::isEnabled).toList();
if (methods.isEmpty()) {
throw new NoSuchMethodException(String.format(
"Suitable public method [%s] not found in [%s] class",
query.methodName(), query.className()));
}
return methods;
}
boolean isTestClass(Class<?> type) {
var typeStatus = processedTypes.get(type);
if (typeStatus == null) {
typeStatus = Verifier.isTestClass(type) ? TypeStatus.TEST_CLASS : TypeStatus.NOT_TEST_CLASS;
processedTypes.put(type, typeStatus);
}
return !TypeStatus.NOT_TEST_CLASS.equals(typeStatus);
}
void verifyTestClass(Class<?> type) throws InvalidAnnotationException {
var typeStatus = processedTypes.get(type);
if (typeStatus == null) {
// The "type" has not been verified yet.
try {
Verifier.verifyTestClass(type);
processedTypes.put(type, TypeStatus.VALID_TEST_CLASS);
return;
} catch (InvalidAnnotationException ex) {
processedTypes.put(type, TypeStatus.TEST_CLASS);
throw ex;
}
}
switch (typeStatus) {
case NOT_TEST_CLASS -> Verifier.throwNotTestClassException(type);
case TEST_CLASS -> Verifier.verifyTestClass(type);
case VALID_TEST_CLASS -> {}
}
}
boolean isEnabled(Method method) {
return Stream.of(Test.class, Parameters.class)
.filter(method::isAnnotationPresent)
.findFirst()
.map(method::getAnnotation)
.map(this::canRunOnTheOperatingSystem)
.orElse(true);
}
Stream<MethodCall> mapToMethodCalls(Method method) throws
IllegalAccessException, InvocationTargetException {
return toCtorArgs(method).map(v -> toMethodCalls(v, method)).flatMap(x -> x);
}
private Stream<Object[]> toCtorArgs(Method method) throws
IllegalAccessException, InvocationTargetException {
if ((method.getModifiers() & Modifier.STATIC) != 0) {
// Static method, no instance
return Stream.ofNullable(DEFAULT_CTOR_ARGS);
}
final var type = method.getDeclaringClass();
final var paremeterSuppliers = filterParameterSuppliers(type)
.filter(m -> m.isAnnotationPresent(Parameters.class))
.filter(this::isEnabled)
.sorted(Comparator.comparing(Method::getName)).toList();
if (paremeterSuppliers.isEmpty()) {
// Single instance using the default constructor.
return Stream.ofNullable(DEFAULT_CTOR_ARGS);
}
// Construct collection of arguments for test class instances.
return createArgs(paremeterSuppliers.toArray(Method[]::new));
}
private Stream<MethodCall> toMethodCalls(Object[] ctorArgs, Method method) {
if (!isParameterized(method)) {
return Stream.of(new MethodCall(ctorArgs, method));
}
var fromParameter = Stream.of(getMethodParameters(method)).map(a -> {
return createArgsForAnnotation(method, a);
}).flatMap(List::stream);
var fromParameterSupplier = Stream.of(getMethodParameterSuppliers(method)).map(a -> {
return toSupplier(() -> createArgsForAnnotation(method, a)).get();
}).flatMap(List::stream);
return Stream.concat(fromParameter, fromParameterSupplier).map(args -> {
return new MethodCall(ctorArgs, method, args);
});
}
private List<Object[]> createArgsForAnnotation(Executable exec, Parameter a) {
if (!canRunOnTheOperatingSystem(a)) {
return List.of();
}
final var annotationArgs = a.value();
final var execParameterTypes = exec.getParameterTypes();
if (execParameterTypes.length > annotationArgs.length) {
if (execParameterTypes.length - annotationArgs.length == 1 && exec.isVarArgs()) {
} else {
throw new RuntimeException(String.format(
"Not enough annotation values %s for [%s]",
List.of(annotationArgs), exec));
}
}
final Class<?>[] argTypes;
if (exec.isVarArgs()) {
List<Class<?>> argTypesBuilder = new ArrayList<>();
var lastExecParameterTypeIdx = execParameterTypes.length - 1;
argTypesBuilder.addAll(List.of(execParameterTypes).subList(0,
lastExecParameterTypeIdx));
argTypesBuilder.addAll(Collections.nCopies(
Integer.max(0, annotationArgs.length - lastExecParameterTypeIdx),
execParameterTypes[lastExecParameterTypeIdx].componentType()));
argTypes = argTypesBuilder.toArray(Class[]::new);
} else {
argTypes = execParameterTypes;
}
if (argTypes.length < annotationArgs.length) {
throw new RuntimeException(String.format(
"Too many annotation values %s for [%s]",
List.of(annotationArgs), exec));
}
var args = mapArgs(exec, IntStream.range(0, argTypes.length).mapToObj(idx -> {
return fromString(annotationArgs[idx], argTypes[idx]);
}).toArray(Object[]::new));
return List.<Object[]>of(args);
}
private List<Object[]> createArgsForAnnotation(Executable exec,
ParameterSupplier a) throws IllegalAccessException,
InvocationTargetException {
if (!canRunOnTheOperatingSystem(a)) {
return List.of();
}
final Class<?> execClass = exec.getDeclaringClass();
final var supplierFuncName = a.value();
final MethodQuery methodQuery;
if (!a.value().contains(".")) {
// No class name specified
methodQuery = new MethodQuery(execClass.getName(), a.value());
} else {
methodQuery = MethodQuery.fromQualifiedMethodName(supplierFuncName);
}
final Method supplierMethod;
try {
final var parameterSupplierCandidates = findNullaryLikeMethods(methodQuery);
final Function<String, Class> classForName = toFunction(Class::forName);
final var supplierMethodClass = classForName.apply(methodQuery.className());
if (parameterSupplierCandidates.isEmpty()) {
throw new RuntimeException(String.format(
"No parameter suppliers in [%s] class",
supplierMethodClass.getName()));
}
var allParameterSuppliers = filterParameterSuppliers(supplierMethodClass).toList();
supplierMethod = findNullaryLikeMethods(methodQuery)
.stream()
.filter(allParameterSuppliers::contains)
.findFirst().orElseThrow(() -> {
var msg = String.format(
"No suitable parameter supplier found for %s(%s) annotation",
a, supplierFuncName);
trace(String.format(
"%s. Parameter suppliers of %s class:", msg,
execClass.getName()));
IntStream.range(0, allParameterSuppliers.size()).mapToObj(idx -> {
return String.format(" [%d/%d] %s()", idx + 1,
allParameterSuppliers.size(),
allParameterSuppliers.get(idx).getName());
}).forEachOrdered(TestMethodSupplier::trace);
return new RuntimeException(msg);
});
} catch (NoSuchMethodException ex) {
throw new RuntimeException(String.format(
"Method not found for %s(%s) annotation", a, supplierFuncName));
}
return createArgs(supplierMethod).map(args -> {
return mapArgs(exec, args);
}).toList();
}
private boolean canRunOnTheOperatingSystem(Annotation a) {
switch (a) {
case Test t -> {
return canRunOnTheOperatingSystem(os, t.ifOS(), t.ifNotOS());
}
case Parameters t -> {
return canRunOnTheOperatingSystem(os, t.ifOS(), t.ifNotOS());
}
case Parameter t -> {
return canRunOnTheOperatingSystem(os, t.ifOS(), t.ifNotOS());
}
case ParameterSupplier t -> {
return canRunOnTheOperatingSystem(os, t.ifOS(), t.ifNotOS());
}
default -> {
return true;
}
}
}
private static boolean isParameterized(Method method) {
return Stream.of(
Parameter.class, ParameterGroup.class,
ParameterSupplier.class, ParameterSupplierGroup.class
).anyMatch(method::isAnnotationPresent);
}
private static boolean isTest(Method method) {
return method.isAnnotationPresent(Test.class);
}
private static boolean canRunOnTheOperatingSystem(OperatingSystem value,
OperatingSystem[] include, OperatingSystem[] exclude) {
Set<OperatingSystem> suppordOperatingSystems = new HashSet<>();
suppordOperatingSystems.addAll(List.of(include));
suppordOperatingSystems.removeAll(List.of(exclude));
return suppordOperatingSystems.contains(value);
}
private static Parameter[] getMethodParameters(Method method) {
if (method.isAnnotationPresent(ParameterGroup.class)) {
return ((ParameterGroup) method.getAnnotation(ParameterGroup.class)).value();
}
if (method.isAnnotationPresent(Parameter.class)) {
return new Parameter[]{(Parameter) method.getAnnotation(Parameter.class)};
}
return new Parameter[0];
}
private static ParameterSupplier[] getMethodParameterSuppliers(Method method) {
if (method.isAnnotationPresent(ParameterSupplierGroup.class)) {
return ((ParameterSupplierGroup) method.getAnnotation(ParameterSupplierGroup.class)).value();
}
if (method.isAnnotationPresent(ParameterSupplier.class)) {
return new ParameterSupplier[]{(ParameterSupplier) method.getAnnotation(
ParameterSupplier.class)};
}
return new ParameterSupplier[0];
}
private static Stream<Method> filterParameterSuppliers(Class<?> type) {
return Stream.of(type.getMethods())
.filter(m -> m.getParameterCount() == 0)
.filter(m -> (m.getModifiers() & Modifier.STATIC) != 0)
.sorted(Comparator.comparing(Method::getName));
}
private static Stream<Object[]> createArgs(Method ... parameterSuppliers) throws
IllegalAccessException, InvocationTargetException {
List<Object[]> args = new ArrayList<>();
for (var parameterSupplier : parameterSuppliers) {
args.addAll((Collection) parameterSupplier.invoke(null));
}
return args.stream();
}
private static Object fromString(String value, Class toType) {
if (toType.isEnum()) {
return Enum.valueOf(toType, value);
}
Function<String, Object> converter = FROM_STRING.get(toType);
if (converter == null) {
throw new RuntimeException(String.format(
"Failed to find a conversion of [%s] string to %s type",
value, toType.getName()));
}
return converter.apply(value);
}
private static void trace(String msg) {
if (TKit.VERBOSE_TEST_SETUP) {
TKit.log(msg);
}
}
static class InvalidAnnotationException extends Exception {
InvalidAnnotationException(String msg) {
super(msg);
}
}
private static class Verifier {
static boolean isTestClass(Class<?> type) {
for (var method : type.getDeclaredMethods()) {
if (isParameterized(method) || isTest(method)) {
return true;
}
}
return false;
}
static void verifyTestClass(Class<?> type) throws InvalidAnnotationException {
boolean withTestAnnotations = false;
for (var method : type.getDeclaredMethods()) {
if (!withTestAnnotations && (isParameterized(method) || isTest(method))) {
withTestAnnotations = true;
}
verifyAnnotationsCorrect(method);
}
if (!withTestAnnotations) {
throwNotTestClassException(type);
}
}
static void throwNotTestClassException(Class<?> type) throws InvalidAnnotationException {
throw new InvalidAnnotationException(String.format(
"Type [%s] is not a test class", type.getName()));
}
private static void verifyAnnotationsCorrect(Method method) throws
InvalidAnnotationException {
var parameterized = isParameterized(method);
if (parameterized && !isTest(method)) {
throw new InvalidAnnotationException(String.format(
"Missing %s annotation on [%s] method", Test.class.getName(), method));
}
var isPublic = Modifier.isPublic(method.getModifiers());
if (isTest(method) && !isPublic) {
throw new InvalidAnnotationException(String.format(
"Non-public method [%s] with %s annotation",
method, Test.class.getName()));
}
if (method.isAnnotationPresent(Parameters.class) && !isPublic) {
throw new InvalidAnnotationException(String.format(
"Non-public method [%s] with %s annotation",
method, Test.class.getName()));
}
}
}
private enum TypeStatus {
NOT_TEST_CLASS,
TEST_CLASS,
VALID_TEST_CLASS,
}
private final OperatingSystem os;
private final Map<Class<?>, TypeStatus> processedTypes = new HashMap<>();
private static final Object[] DEFAULT_CTOR_ARGS = new Object[0];
private static final Map<Class, Function<String, Object>> FROM_STRING;
static {
Map<Class, Function<String, Object>> primitives = Map.of(
boolean.class, Boolean::valueOf,
byte.class, Byte::valueOf,
short.class, Short::valueOf,
int.class, Integer::valueOf,
long.class, Long::valueOf,
float.class, Float::valueOf,
double.class, Double::valueOf);
Map<Class, Function<String, Object>> boxed = Map.of(
Boolean.class, Boolean::valueOf,
Byte.class, Byte::valueOf,
Short.class, Short::valueOf,
Integer.class, Integer::valueOf,
Long.class, Long::valueOf,
Float.class, Float::valueOf,
Double.class, Double::valueOf);
Map<Class, Function<String, Object>> other = Map.of(
String.class, String::valueOf,
Path.class, Path::of);
Map<Class, Function<String, Object>> combined = new HashMap<>(primitives);
combined.putAll(other);
combined.putAll(boxed);
FROM_STRING = Collections.unmodifiableMap(combined);
}
}

View File

@ -29,12 +29,12 @@ import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import static java.util.stream.Collectors.toSet;
import java.util.stream.Stream;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.internal.AppImageFile;
import jdk.jpackage.internal.ApplicationLayout;
import jdk.jpackage.internal.PackageFile;
import jdk.jpackage.test.Annotations;
import jdk.jpackage.test.Annotations.Parameters;
import jdk.jpackage.test.Annotations.Test;
import jdk.jpackage.test.Functional.ThrowingConsumer;
import jdk.jpackage.test.JPackageCommand;
@ -55,47 +55,51 @@ import jdk.jpackage.test.TKit;
*/
public final class InOutPathTest {
@Annotations.Parameters
@Parameters
public static Collection input() {
List<Object[]> data = new ArrayList<>();
for (var packageTypes : List.of(PackageType.IMAGE.toString(), ALL_NATIVE_PACKAGE_TYPES)) {
for (var packageTypeAlias : PackageTypeAlias.values()) {
data.addAll(List.of(new Object[][]{
{packageTypes, wrap(InOutPathTest::outputDirInInputDir, "--dest in --input")},
{packageTypes, wrap(InOutPathTest::outputDirSameAsInputDir, "--dest same as --input")},
{packageTypes, wrap(InOutPathTest::tempDirInInputDir, "--temp in --input")},
{packageTypes, wrap(cmd -> {
{packageTypeAlias, wrap(InOutPathTest::outputDirInInputDir, "--dest in --input")},
{packageTypeAlias, wrap(InOutPathTest::outputDirSameAsInputDir, "--dest same as --input")},
{packageTypeAlias, wrap(InOutPathTest::tempDirInInputDir, "--temp in --input")},
{packageTypeAlias, wrap(cmd -> {
outputDirInInputDir(cmd);
tempDirInInputDir(cmd);
}, "--dest and --temp in --input")},
}));
data.addAll(additionalContentInput(packageTypes, "--app-content"));
}
if (!TKit.isOSX()) {
data.addAll(List.of(new Object[][]{
{PackageType.IMAGE.toString(), wrap(cmd -> {
additionalContent(cmd, "--app-content", cmd.outputBundle());
}, "--app-content same as output bundle")},
}));
} else {
var contentsFolder = "Contents/MacOS";
data.addAll(List.of(new Object[][]{
{PackageType.IMAGE.toString(), wrap(cmd -> {
additionalContent(cmd, "--app-content", cmd.outputBundle().resolve(contentsFolder));
}, String.format("--app-content same as the \"%s\" folder in the output bundle", contentsFolder))},
}));
}
if (TKit.isOSX()) {
data.addAll(additionalContentInput(PackageType.MAC_DMG.toString(),
"--mac-dmg-content"));
data.addAll(additionalContentInput(packageTypeAlias, "--app-content"));
}
return data;
}
private static List<Object[]> additionalContentInput(String packageTypes, String argName) {
@Parameters(ifNotOS = OperatingSystem.MACOS)
public static Collection<Object[]> appContentInputOther() {
return List.of(new Object[][]{
{PackageTypeAlias.IMAGE, wrap(cmd -> {
additionalContent(cmd, "--app-content", cmd.outputBundle());
}, "--app-content same as output bundle")},
});
}
@Parameters(ifOS = OperatingSystem.MACOS)
public static Collection<Object[]> appContentInputOSX() {
var contentsFolder = "Contents/MacOS";
return List.of(new Object[][]{
{PackageTypeAlias.IMAGE, wrap(cmd -> {
additionalContent(cmd, "--app-content", cmd.outputBundle().resolve(contentsFolder));
}, String.format("--app-content same as the \"%s\" folder in the output bundle", contentsFolder))},
});
}
@Parameters(ifOS = OperatingSystem.MACOS)
public static Collection<Object[]> inputOSX() {
return List.of(additionalContentInput(PackageType.MAC_DMG, "--mac-dmg-content").toArray(Object[][]::new));
}
private static List<Object[]> additionalContentInput(Object packageTypes, String argName) {
List<Object[]> data = new ArrayList<>();
data.addAll(List.of(new Object[][]{
@ -127,13 +131,16 @@ public final class InOutPathTest {
return data;
}
public InOutPathTest(String packageTypes, Envelope configure) {
if (ALL_NATIVE_PACKAGE_TYPES.equals(packageTypes)) {
this.packageTypes = PackageType.NATIVE;
} else {
this.packageTypes = Stream.of(packageTypes.split(",")).map(
PackageType::valueOf).collect(toSet());
}
public InOutPathTest(PackageTypeAlias packageTypeAlias, Envelope configure) {
this(packageTypeAlias.packageTypes, configure);
}
public InOutPathTest(PackageType packageType, Envelope configure) {
this(Set.of(packageType), configure);
}
public InOutPathTest(Set<PackageType> packageTypes, Envelope configure) {
this.packageTypes = packageTypes;
this.configure = configure.value;
}
@ -271,6 +278,18 @@ public final class InOutPathTest {
}
}
private enum PackageTypeAlias {
IMAGE(Set.of(PackageType.IMAGE)),
NATIVE(PackageType.NATIVE),
;
PackageTypeAlias(Set<PackageType> packageTypes) {
this.packageTypes = packageTypes;
}
private final Set<PackageType> packageTypes;
}
private final Set<PackageType> packageTypes;
private final ThrowingConsumer<JPackageCommand> configure;
@ -279,6 +298,4 @@ public final class InOutPathTest {
// For other platforms it doesn't matter. Keep it the same across
// all platforms for simplicity.
private static final Path JAR_PATH = Path.of("Resources/duke.jar");
private static final String ALL_NATIVE_PACKAGE_TYPES = "NATIVE";
}

View File

@ -22,13 +22,12 @@
*/
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import jdk.internal.util.OperatingSystem;
import jdk.jpackage.test.TKit;
import jdk.jpackage.test.PackageTest;
import jdk.jpackage.test.PackageType;
import jdk.jpackage.test.Functional;
import jdk.jpackage.test.Annotations.Parameter;
import jdk.jpackage.test.Annotations.Test;
/**
* Test --install-dir parameter. Output of the test should be
@ -76,28 +75,18 @@ import jdk.jpackage.test.Annotations.Parameter;
*/
public class InstallDirTest {
public static void testCommon() {
final Map<PackageType, Path> INSTALL_DIRS = Functional.identity(() -> {
Map<PackageType, Path> reply = new HashMap<>();
reply.put(PackageType.WIN_MSI, Path.of("TestVendor\\InstallDirTest1234"));
reply.put(PackageType.WIN_EXE, reply.get(PackageType.WIN_MSI));
reply.put(PackageType.LINUX_DEB, Path.of("/opt/jpackage"));
reply.put(PackageType.LINUX_RPM, reply.get(PackageType.LINUX_DEB));
reply.put(PackageType.MAC_PKG, Path.of("/Applications/jpackage"));
reply.put(PackageType.MAC_DMG, reply.get(PackageType.MAC_PKG));
return reply;
}).get();
@Test
@Parameter(value = "TestVendor\\InstallDirTest1234", ifOS = OperatingSystem.WINDOWS)
@Parameter(value = "/opt/jpackage", ifOS = OperatingSystem.LINUX)
@Parameter(value = "/Applications/jpackage", ifOS = OperatingSystem.MACOS)
public static void testCommon(Path installDir) {
new PackageTest().configureHelloApp()
.addInitializer(cmd -> {
cmd.addArguments("--install-dir", INSTALL_DIRS.get(
cmd.packageType()));
cmd.addArguments("--install-dir", installDir);
}).run();
}
@Test(ifOS = OperatingSystem.LINUX)
@Parameter("/")
@Parameter(".")
@Parameter("foo")