diff --git a/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/IdentityWrapper.java b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/IdentityWrapper.java new file mode 100644 index 0000000000000..0a7a8cc8d4b24 --- /dev/null +++ b/src/jdk.jpackage/share/classes/jdk/jpackage/internal/util/IdentityWrapper.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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.internal.util; + +import java.util.Objects; + +/** + * Object wrapper implementing {@link Object#equals(Object)} such that it + * returns {@code true} only when the argument is another instance of this class + * wrapping the same object. + *

+ * The class guarantees that {@link Object#equals(Object)} and + * {@link Object#hashCode()} methods of the wrapped object will never be called + * inside of the class methods. + * + * @param the type of the wrapped value + */ +public final class IdentityWrapper { + + public IdentityWrapper(T value) { + this.value = Objects.requireNonNull(value); + } + + public T value() { + return value; + } + + @Override + public int hashCode() { + return System.identityHashCode(value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + var other = (IdentityWrapper) obj; + return value == other.value; + } + + @Override + public String toString() { + return String.format("Identity[%s]", value); + } + + private final T value; +} diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/JUnitUtilsTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/JUnitUtilsTest.java new file mode 100644 index 0000000000000..28b55f98fe203 --- /dev/null +++ b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/JUnitUtilsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class JUnitUtilsTest { + + @Test + public void test_assertArrayEquals() { + JUnitUtils.assertArrayEquals(new int[] {1, 2, 3}, new int[] {1, 2, 3}); + JUnitUtils.assertArrayEquals(new long[] {1, 2, 3}, new long[] {1, 2, 3}); + JUnitUtils.assertArrayEquals(new boolean[] {true, true}, new boolean[] {true, true}); + } + + @Test + public void test_assertArrayEquals_negative() { + assertThrows(AssertionError.class, () -> { + JUnitUtils.assertArrayEquals(new int[] {1, 2, 3}, new int[] {2, 3}); + }); + } + + @Test + public void test_exceptionAsPropertyMapWithMessageWithoutCause() { + + var ex = new Exception("foo"); + + var map = JUnitUtils.exceptionAsPropertyMap(ex); + + assertEquals(Map.of("getClass", Exception.class.getName(), "getMessage", "foo"), map); + } +} diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ObjectMapperTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ObjectMapperTest.java new file mode 100644 index 0000000000000..0310d276e218b --- /dev/null +++ b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/ObjectMapperTest.java @@ -0,0 +1,731 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.math.BigInteger; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; + +public class ObjectMapperTest { + + @Test + public void test_String() { + var om = ObjectMapper.blank().create(); + + var map = om.map("foo"); + + assertEquals("foo", map); + } + + @Test + public void test_int() { + var om = ObjectMapper.blank().create(); + + var map = om.map(100); + + assertEquals(100, map); + } + + @Test + public void test_null() { + var om = ObjectMapper.blank().create(); + + var map = om.map(null); + + assertNull(map); + } + + @Test + public void test_Object() { + var obj = new Object(); + assertSame(obj, ObjectMapper.blank().create().map(obj)); + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_Path() { + var obj = Path.of("foo/bar"); + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_UUID() { + var obj = UUID.randomUUID(); + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_BigInteger() { + var obj = BigInteger.TEN; + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_Enum() { + + var expected = Map.of( + "name", TestEnum.BAR.name(), + "ordinal", TestEnum.BAR.ordinal(), + "a", "A", + "b", 123, + "num", 100 + ); + + assertEquals(expected, ObjectMapper.standard().create().map(TestEnum.BAR)); + } + + @Test + public void test_array_int() { + + var obj = new int[] { 1, 4, 5 }; + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_array_String() { + + var obj = new String[] { "Hello", "Bye" }; + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_array_empty() { + + var obj = new Thread[0]; + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_array_nulls() { + + var obj = new Thread[10]; + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_array_Path() { + + var obj = new Path[] { Path.of("foo/bar"), null, Path.of("").toAbsolutePath() }; + + assertSame(obj, ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_array_Object() { + + var obj = new Object[] { Path.of("foo/bar"), null, 145, new Simple.Stub("Hello", 738), "foo" }; + + var expected = new Object[] { Path.of("foo/bar"), null, 145, Map.of("a", "Hello", "b", 738), "foo" }; + + assertArrayEquals(expected, (Object[])ObjectMapper.standard().create().map(obj)); + } + + @Test + public void test_functional() { + assertWrappedIdentity(new Function() { + + @Override + public Integer apply(Object o) { + throw new AssertionError(); + } + + }); + + assertWrappedIdentity(new BiFunction() { + + @Override + public Integer apply(Object a, String b) { + throw new AssertionError(); + } + + }); + + assertWrappedIdentity(new Consumer<>() { + + @Override + public void accept(Object o) { + throw new AssertionError(); + } + + }); + + assertWrappedIdentity(new BiConsumer<>() { + + @Override + public void accept(Object a, Object b) { + throw new AssertionError(); + } + + }); + + assertWrappedIdentity(new Predicate<>() { + + @Override + public boolean test(Object o) { + throw new AssertionError(); + } + + }); + + assertWrappedIdentity(new Supplier<>() { + + @Override + public Object get() { + throw new AssertionError(); + } + + }); + + assertWrappedIdentity(new Runnable() { + + @Override + public void run() { + throw new AssertionError(); + } + + }); + } + + @Test + public void testIdentityWrapper() { + var om = ObjectMapper.standard().create(); + + var a = new Object() {}; + var b = new Object() {}; + + var amap = om.map(a); + var amap2 = om.map(a); + + assertEquals(amap, amap2); + assertEquals(ObjectMapper.wrapIdentity(a), amap); + + var bmap = om.map(b); + + assertNotEquals(amap, bmap); + assertEquals(ObjectMapper.wrapIdentity(b), bmap); + } + + @Test + public void test_wrapIdentity() { + + assertThrowsExactly(NullPointerException.class, () -> ObjectMapper.wrapIdentity(null)); + + var iw = ObjectMapper.wrapIdentity(new Object()); + + assertSame(iw, ObjectMapper.wrapIdentity(iw)); + + var simpleStubA = new Simple.Stub("Hello", 77); + var simpleStubB = new Simple.Stub("Hello", 77); + + assertEquals(simpleStubA, simpleStubB); + assertNotEquals(ObjectMapper.wrapIdentity(simpleStubA), ObjectMapper.wrapIdentity(simpleStubB)); + assertEquals(ObjectMapper.wrapIdentity(simpleStubA), ObjectMapper.wrapIdentity(simpleStubA)); + } + + @Test + public void test_empty_List() { + var om = ObjectMapper.blank().create(); + + var map = om.map(List.of()); + + assertEquals(List.of(), map); + } + + @Test + public void test_List() { + var om = ObjectMapper.blank().create(); + + var map = om.map(List.of(100, "foo")); + + assertEquals(List.of(100, "foo"), map); + } + + @Test + public void test_empty_Map() { + var om = ObjectMapper.blank().create(); + + var map = om.map(Map.of()); + + assertEquals(Map.of(), map); + } + + @Test + public void test_Map() { + var om = ObjectMapper.blank().create(); + + var map = om.map(Map.of(100, "foo")); + + assertEquals(Map.of(100, "foo"), map); + } + + @Test + public void test_MapSimple() { + var om = ObjectMapper.standard().create(); + + var map = om.map(Map.of(123, "foo", 321, new Simple.Stub("Hello", 567))); + + assertEquals(Map.of(123, "foo", 321, Map.of("a", "Hello", "b", 567)), map); + } + + @Test + public void test_ListSimple() { + var om = ObjectMapper.standard().create(); + + var map = om.map(List.of(100, new Simple.Stub("Hello", 567), "bar", new Simple() {})); + + assertEquals(List.of(100, Map.of("a", "Hello", "b", 567), "bar", Map.of("a", "foo", "b", 123)), map); + } + + @Test + public void test_Simple() { + var om = ObjectMapper.standard().create(); + + var map = om.map(new Simple() {}); + + assertEquals(Map.of("a", "foo", "b", 123), map); + } + + @Test + public void test_Proxy() { + var om = ObjectMapper.standard().create(); + + var map = om.map(Proxy.newProxyInstance(Simple.class.getClassLoader(), new Class[] { Simple.class }, new InvocationHandler() { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + switch (method.getName()) { + case "a" -> { + return "Bye"; + } + case "b" -> { + return 335; + } + default -> { + throw new UnsupportedOperationException(); + } + } + } + + })); + + assertEquals(Map.of("a", "Bye", "b", 335), map); + } + + @Test + public void test_Simple_null_property() { + var om = ObjectMapper.standard().create(); + + var map = om.map(new Simple.Stub(null, 123)); + + assertEquals(Map.of("b", 123, "a", ObjectMapper.NULL), map); + } + + @Test + public void test_Optional_String() { + var om = ObjectMapper.standard().create(); + + var map = om.map(Optional.of("foo")); + + assertEquals(Map.of("get", "foo"), map); + } + + @Test + public void test_Optional_empty() { + var om = ObjectMapper.standard().create(); + + var map = om.map(Optional.empty()); + + assertEquals(Map.of("get", ObjectMapper.NULL), map); + } + + @Test + public void test_toMap() { + var om = ObjectMapper.standard().create(); + + assertNull(om.toMap(null)); + assertEquals(Map.of("value", "Hello"), om.toMap("Hello")); + assertEquals(Map.of("a", "foo", "b", 123), om.toMap(new Simple() {})); + } + + @Test + public void test_getter_throws() { + var om = ObjectMapper.blank() + .mutate(ObjectMapper.configureObject()) + .mutate(ObjectMapper.configureLeafClasses()) + .mutate(ObjectMapper.configureException()) + .create(); + + var expected = Map.of("get", om.toMap(new UnsupportedOperationException("Not for you!"))); + + var actual = om.toMap(new Supplier<>() { + @Override + public Object get() { + throw new UnsupportedOperationException("Not for you!"); + } + }); + + assertEquals(expected, actual); + } + + @Test + public void test_exception_with_message_with_cause() { + + var ex = new Exception("foo", new IllegalArgumentException("Cause", new RuntimeException("Ops!"))); + + var om = ObjectMapper.standard().create(); + + var map = om.toMap(ex); + + assertEquals(Map.of( + "getClass", Exception.class.getName(), + "getMessage", "foo", + "getCause", Map.of( + "getClass", IllegalArgumentException.class.getName(), + "getMessage", "Cause", + "getCause", Map.of( + "getClass", RuntimeException.class.getName(), + "getMessage", "Ops!" + ) + ) + ), map); + } + + @Test + public void test_exception_without_message_with_cause() { + + var ex = new RuntimeException(null, new UnknownError("Ops!")); + + var om = ObjectMapper.standard().create(); + + var map = om.toMap(ex); + + assertEquals(Map.of( + "getClass", RuntimeException.class.getName(), + "getCause", Map.of( + "getMessage", "Ops!", + "getCause", ObjectMapper.NULL + ) + ), map); + } + + @Test + public void test_exception_without_message_without_cause() { + + var ex = new UnsupportedOperationException(); + + var om = ObjectMapper.standard().create(); + + var map = om.toMap(ex); + + assertEquals(Map.of("getClass", UnsupportedOperationException.class.getName()), map); + } + + @Test + public void test_exception_CustomException() { + + var ex = new CustomException("Hello", Path.of(""), Optional.empty(), null); + + var om = ObjectMapper.standard().create(); + + var map = om.toMap(ex); + + assertEquals(Map.of( + "getClass", CustomException.class.getName(), + "getMessage", "Hello", + "op", Map.of("get", ObjectMapper.NULL), + "path2", Path.of("") + ), map); + } + + @Test + public void test_Builder_accessPackageMethods() { + + var obj = new TestType().foo("Hello").bar(81); + + var map = ObjectMapper.standard().create().toMap(obj); + + assertEquals(Map.of("foo", "Hello"), map); + + map = ObjectMapper.standard().accessPackageMethods(TestType.class.getPackage()).create().toMap(obj); + + assertEquals(Map.of("foo", "Hello", "bar", 81), map); + } + + @Test + public void test_Builder_methods_Simple() { + + var om = ObjectMapper.standard().exceptSomeMethods(Simple.class).add("a").apply().create(); + + assertEquals(Map.of("b", 123), om.toMap(new Simple() {})); + assertEquals(Map.of("b", 345), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("b", 123), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("b", 345 + 10), om.toMap(new Simple.DefaultExt("Hello", 345))); + + om = ObjectMapper.standard().exceptSomeMethods(Simple.class).add("b").apply().create(); + + assertEquals(Map.of("a", "foo"), om.toMap(new Simple() {})); + assertEquals(Map.of("a", "Hello"), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("a", "Hello"), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("a", "[Hello]"), om.toMap(new Simple.DefaultExt("Hello", 345))); + } + + @Test + public void test_Builder_methods_SimpleStub() { + + var om = ObjectMapper.standard().exceptSomeMethods(Simple.Stub.class).add("a").apply().create(); + + assertEquals(Map.of("a", "foo", "b", 123), om.toMap(new Simple() {})); + assertEquals(Map.of("b", 345), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("a", "Hello", "b", 123), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("a", "[Hello]", "b", 345 + 10), om.toMap(new Simple.DefaultExt("Hello", 345))); + + om = ObjectMapper.standard().exceptSomeMethods(Simple.Stub.class).add("b").apply().create(); + + assertEquals(Map.of("a", "foo", "b", 123), om.toMap(new Simple() {})); + assertEquals(Map.of("a", "Hello"), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("a", "Hello", "b", 123), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("a", "[Hello]", "b", 345 + 10), om.toMap(new Simple.DefaultExt("Hello", 345))); + } + + @Test + public void test_Builder_methods_SimpleDefault() { + + var om = ObjectMapper.standard().exceptSomeMethods(Simple.Default.class).add("a").apply().create(); + + assertEquals(Map.of("a", "foo", "b", 123), om.toMap(new Simple() {})); + assertEquals(Map.of("a", "Hello", "b", 345), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("b", 123), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("b", 345 + 10), om.toMap(new Simple.DefaultExt("Hello", 345))); + + om = ObjectMapper.standard().exceptSomeMethods(Simple.Default.class).add("b").apply().create(); + + assertEquals(Map.of("a", "foo"), om.toMap(new Simple() {})); + assertEquals(Map.of("a", "Hello"), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("a", "Hello"), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("a", "[Hello]"), om.toMap(new Simple.DefaultExt("Hello", 345))); + } + + @Test + public void test_Builder_methods_SimpleDefaultExt() { + + var om = ObjectMapper.standard().exceptSomeMethods(Simple.DefaultExt.class).add("a").apply().create(); + + assertEquals(Map.of("a", "foo", "b", 123), om.toMap(new Simple() {})); + assertEquals(Map.of("a", "Hello", "b", 345), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("a", "Hello", "b", 123), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("b", 345 + 10), om.toMap(new Simple.DefaultExt("Hello", 345))); + + om = ObjectMapper.standard().exceptSomeMethods(Simple.DefaultExt.class).add("b").apply().create(); + + assertEquals(Map.of("a", "foo", "b", 123), om.toMap(new Simple() {})); + assertEquals(Map.of("a", "Hello", "b", 345), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("a", "Hello", "b", 123), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("a", "[Hello]"), om.toMap(new Simple.DefaultExt("Hello", 345))); + } + + @Test + public void test_Builder_methods_SimpleStub_and_SimpleDefault() { + + var om = ObjectMapper.standard() + .exceptSomeMethods(Simple.Stub.class).add("a").apply() + .exceptSomeMethods(Simple.Default.class).add("a").apply() + .create(); + + assertEquals(Map.of("a", "foo", "b", 123), om.toMap(new Simple() {})); + assertEquals(Map.of("b", 345), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("b", 123), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("b", 345 + 10), om.toMap(new Simple.DefaultExt("Hello", 345))); + + om = ObjectMapper.standard() + .exceptSomeMethods(Simple.Stub.class).add("b").apply() + .exceptSomeMethods(Simple.Default.class).add("b").apply() + .create(); + + assertEquals(Map.of("a", "foo"), om.toMap(new Simple() {})); + assertEquals(Map.of("a", "Hello"), om.toMap(new Simple.Stub("Hello", 345))); + assertEquals(Map.of("a", "Hello"), om.toMap(new Simple.Default("Hello"))); + assertEquals(Map.of("a", "[Hello]"), om.toMap(new Simple.DefaultExt("Hello", 345))); + } + + @Test + public void test_Builder_methods_all_excluded() { + + var om = ObjectMapper.standard() + .exceptSomeMethods(Simple.class).add("a").apply() + .exceptSomeMethods(Simple.Stub.class).add("b").apply() + .create(); + + var obj = new Simple.Stub("Hello", 345); + + assertEquals(ObjectMapper.wrapIdentity(obj), om.map(obj)); + } + + interface Simple { + default String a() { + return "foo"; + } + + default int b() { + return 123; + } + + record Stub(String a, int b) implements Simple {} + + static class Default implements Simple { + Default(String a) { + this.a = a; + } + + @Override + public String a() { + return a; + } + + private final String a; + } + + static class DefaultExt extends Default { + DefaultExt(String a, int b) { + super(a); + this.b = b; + } + + @Override + public String a() { + return "[" + super.a() + "]"; + } + + @Override + public int b() { + return 10 + b; + } + + private final int b; + } + } + + final class TestType { + + public String foo() { + return foo; + } + + public TestType foo(String v) { + foo = v; + return this; + } + + int bar() { + return bar; + } + + TestType bar(int v) { + bar = v; + return this; + } + + private String foo; + private int bar; + } + + enum TestEnum implements Simple { + FOO, + BAR; + + public int num() { + return 100; + } + + public int num(int v) { + return v; + } + + @Override + public String a() { + return "A"; + } + } + + static final class CustomException extends Exception { + + CustomException(String message, Path path, Optional optional, Throwable cause) { + super(message, cause); + this.path = path; + this.optional = optional; + } + + Path path() { + return path; + } + + public Path path2() { + return path; + } + + public Optional op() { + return optional; + } + + private final Path path; + private final Optional optional; + + private static final long serialVersionUID = 1L; + + } + + private static void assertWrappedIdentity(ObjectMapper om, Object obj) { + var map = om.toMap(obj); + assertEquals(Map.of("value", ObjectMapper.wrapIdentity(obj)), map); + } + + private static void assertWrappedIdentity(Object obj) { + assertWrappedIdentity(ObjectMapper.standard().create(), obj); + } +} diff --git a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java index da94db30925da..4cf89fca3cc8a 100644 --- a/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java +++ b/test/jdk/tools/jpackage/helpers-test/jdk/jpackage/test/PackageTestTest.java @@ -341,7 +341,7 @@ public PackageType packageType() { } @Override - JPackageCommand assertAppLayout() { + JPackageCommand runStandardAsserts() { return this; } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java index 50222d89cebdc..66da89fc3f98a 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/AdditionalLauncher.java @@ -198,7 +198,7 @@ static void forEachAdditionalLauncher(JPackageCommand cmd, } } - static PropertyFile getAdditionalLauncherProperties( + public static PropertyFile getAdditionalLauncherProperties( JPackageCommand cmd, String launcherName) { PropertyFile shell[] = new PropertyFile[1]; forEachAdditionalLauncher(cmd, (name, propertiesFilePath) -> { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java index 7ab3b824aa44b..0701421e999f8 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ApplicationLayout.java @@ -98,12 +98,18 @@ public static ApplicationLayout platformAppImage() { throw new IllegalArgumentException("Unknown platform"); } - public static ApplicationLayout javaRuntime() { + public static ApplicationLayout platformJavaRuntime() { + Path runtime = Path.of(""); + Path runtimeHome = runtime; + if (TKit.isOSX()) { + runtimeHome = Path.of("Contents/Home"); + } + return new ApplicationLayout( null, null, - Path.of(""), - null, + runtime, + runtimeHome, null, null, null, diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigurationTarget.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigurationTarget.java new file mode 100644 index 0000000000000..ba3131a76807d --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ConfigurationTarget.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * Provides uniform way to configure {@code JPackageCommand} and + * {@code PackageTest} instances. + */ +public record ConfigurationTarget(Optional cmd, Optional test) { + + public ConfigurationTarget { + Objects.requireNonNull(cmd); + Objects.requireNonNull(test); + if (cmd.isEmpty() == test.isEmpty()) { + throw new IllegalArgumentException(); + } + } + + public ConfigurationTarget(JPackageCommand target) { + this(Optional.of(target), Optional.empty()); + } + + public ConfigurationTarget(PackageTest target) { + this(Optional.empty(), Optional.of(target)); + } + + public ConfigurationTarget apply(Consumer a, Consumer b) { + cmd.ifPresent(Objects.requireNonNull(a)); + test.ifPresent(Objects.requireNonNull(b)); + return this; + } + + public ConfigurationTarget addInitializer(Consumer initializer) { + cmd.ifPresent(Objects.requireNonNull(initializer)); + test.ifPresent(v -> { + v.addInitializer(initializer::accept); + }); + return this; + } + + public ConfigurationTarget add(AdditionalLauncher addLauncher) { + return apply(addLauncher::applyTo, addLauncher::applyTo); + } +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java index 69ea4ecfaa099..6c7b6a2525570 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/HelloApp.java @@ -125,6 +125,8 @@ private JarBuilder createJarBuilder() { if (appDesc.isWithMainClass()) { builder.setMainClass(appDesc.className()); } + // Use an old release number to make test app classes runnable on older runtimes. + builder.setRelease(11); return builder; } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java index 6945cd2b7223a..22e75a5791125 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JPackageCommand.java @@ -67,9 +67,11 @@ */ public class JPackageCommand extends CommandArguments { + @SuppressWarnings("this-escape") public JPackageCommand() { prerequisiteActions = new Actions(); verifyActions = new Actions(); + excludeStandardAsserts(StandardAssert.MAIN_LAUNCHER_DESCRIPTION); } private JPackageCommand(JPackageCommand cmd, boolean immutable) { @@ -85,7 +87,7 @@ private JPackageCommand(JPackageCommand cmd, boolean immutable) { dmgInstallDir = cmd.dmgInstallDir; prerequisiteActions = new Actions(cmd.prerequisiteActions); verifyActions = new Actions(cmd.verifyActions); - appLayoutAsserts = cmd.appLayoutAsserts; + standardAsserts = cmd.standardAsserts; readOnlyPathAsserts = cmd.readOnlyPathAsserts; outputValidators = cmd.outputValidators; executeInDirectory = cmd.executeInDirectory; @@ -459,7 +461,7 @@ public ApplicationLayout appLayout() { if (layout != null) { } else if (isRuntime()) { - layout = ApplicationLayout.javaRuntime(); + layout = ApplicationLayout.platformJavaRuntime(); } else { layout = ApplicationLayout.platformAppImage(); } @@ -933,7 +935,7 @@ public Executor.Result executeAndAssertImageCreated() { public JPackageCommand assertImageCreated() { verifyIsOfType(PackageType.IMAGE); - assertAppLayout(); + runStandardAsserts(); return this; } @@ -976,10 +978,10 @@ private static final class ReadOnlyPathsAssert { void updateAndAssert() { final var newSnapshots = createSnapshots(); for (final var a : asserts.keySet().stream().sorted().toList()) { - final var snapshopGroup = snapshots.get(a); - final var newSnapshopGroup = newSnapshots.get(a); - for (int i = 0; i < snapshopGroup.size(); i++) { - TKit.PathSnapshot.assertEquals(snapshopGroup.get(i), newSnapshopGroup.get(i), + final var snapshotGroup = snapshots.get(a); + final var newSnapshotGroup = newSnapshots.get(a); + for (int i = 0; i < snapshotGroup.size(); i++) { + snapshotGroup.get(i).assertEquals(newSnapshotGroup.get(i), String.format("Check jpackage didn't modify ${%s}=[%s]", a, asserts.get(a).get(i))); } } @@ -1094,7 +1096,7 @@ public JPackageCommand excludeReadOnlyPathAssert(ReadOnlyPathAssert... asserts) asSet::contains)).toArray(ReadOnlyPathAssert[]::new)); } - public static enum AppLayoutAssert { + public static enum StandardAssert { APP_IMAGE_FILE(JPackageCommand::assertAppImageFile), PACKAGE_FILE(JPackageCommand::assertPackageFile), NO_MAIN_LAUNCHER_IN_RUNTIME(cmd -> { @@ -1114,6 +1116,11 @@ public static enum AppLayoutAssert { LauncherVerifier.Action.VERIFY_MAC_ENTITLEMENTS); } }), + MAIN_LAUNCHER_DESCRIPTION(cmd -> { + if (!cmd.isRuntime()) { + new LauncherVerifier(cmd).verify(cmd, LauncherVerifier.Action.VERIFY_DESCRIPTION); + } + }), MAIN_JAR_FILE(cmd -> { Optional.ofNullable(cmd.getArgumentValue("--main-jar", () -> null)).ifPresent(mainJar -> { TKit.assertFileExists(cmd.appLayout().appDirectory().resolve(mainJar)); @@ -1138,7 +1145,7 @@ public static enum AppLayoutAssert { }), ; - AppLayoutAssert(Consumer action) { + StandardAssert(Consumer action) { this.action = action; } @@ -1156,21 +1163,21 @@ private static JPackageCommand convertFromRuntime(JPackageCommand cmd) { private final Consumer action; } - public JPackageCommand setAppLayoutAsserts(AppLayoutAssert ... asserts) { + public JPackageCommand setStandardAsserts(StandardAssert ... asserts) { verifyMutable(); - appLayoutAsserts = Set.of(asserts); + standardAsserts = Set.of(asserts); return this; } - public JPackageCommand excludeAppLayoutAsserts(AppLayoutAssert... asserts) { + public JPackageCommand excludeStandardAsserts(StandardAssert... asserts) { var asSet = Set.of(asserts); - return setAppLayoutAsserts(appLayoutAsserts.stream().filter(Predicate.not( - asSet::contains)).toArray(AppLayoutAssert[]::new)); + return setStandardAsserts(standardAsserts.stream().filter(Predicate.not( + asSet::contains)).toArray(StandardAssert[]::new)); } - JPackageCommand assertAppLayout() { - for (var appLayoutAssert : appLayoutAsserts.stream().sorted().toList()) { - appLayoutAssert.action.accept(this); + JPackageCommand runStandardAsserts() { + for (var standardAssert : standardAsserts.stream().sorted().toList()) { + standardAssert.action.accept(this); } return this; } @@ -1520,7 +1527,7 @@ public void run() { private Path winMsiLogFile; private Path unpackedPackageDirectory; private Set readOnlyPathAsserts = Set.of(ReadOnlyPathAssert.values()); - private Set appLayoutAsserts = Set.of(AppLayoutAssert.values()); + private Set standardAsserts = Set.of(StandardAssert.values()); private List>> outputValidators = new ArrayList<>(); private static InheritableThreadLocal> defaultToolProvider = new InheritableThreadLocal<>() { @Override diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java index d62575d2fefcf..c69c29af53a04 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/JarBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,6 +27,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Optional; /** @@ -48,6 +49,11 @@ public JarBuilder setMainClass(String v) { return this; } + public JarBuilder setRelease(int v) { + release = v; + return this; + } + public JarBuilder addSourceFile(Path v) { sourceFiles.add(v); return this; @@ -61,11 +67,15 @@ public JarBuilder setModuleVersion(String v) { public void create() { TKit.withTempDirectory("jar-workdir", workDir -> { if (!sourceFiles.isEmpty()) { - new Executor() + var exec = new Executor() .setToolProvider(JavaTool.JAVAC) - .addArguments("-d", workDir.toString()) - .addPathArguments(sourceFiles) - .execute(); + .addArguments("-d", workDir.toString()); + + Optional.ofNullable(release).ifPresent(r -> { + exec.addArguments("--release", r.toString()); + }); + + exec.addPathArguments(sourceFiles).execute(); } Files.createDirectories(outputJar.getParent()); @@ -92,4 +102,5 @@ public void create() { private Path outputJar; private String mainClass; private String moduleVersion; + private Integer release; } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java index 278cd569baca7..79652a9828e00 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LauncherIconVerifier.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.Optional; public final class LauncherIconVerifier { public LauncherIconVerifier() { @@ -37,19 +38,33 @@ public LauncherIconVerifier setLauncherName(String v) { public LauncherIconVerifier setExpectedIcon(Path v) { expectedIcon = v; + expectedDefault = false; return this; } public LauncherIconVerifier setExpectedDefaultIcon() { + expectedIcon = null; expectedDefault = true; return this; } + public LauncherIconVerifier setExpectedNoIcon() { + return setExpectedIcon(null); + } + public LauncherIconVerifier verifyFileInAppImageOnly(boolean v) { verifyFileInAppImageOnly = true; return this; } + public boolean expectDefaultIcon() { + return expectedDefault; + } + + public Optional expectIcon() { + return Optional.ofNullable(expectedIcon); + } + public void applyTo(JPackageCommand cmd) throws IOException { final String curLauncherName; final String label; @@ -70,7 +85,7 @@ public void applyTo(JPackageCommand cmd) throws IOException { WinExecutableIconVerifier.verifyLauncherIcon(cmd, launcherName, expectedIcon, expectedDefault); } } else if (expectedDefault) { - TKit.assertPathExists(iconPath, true); + TKit.assertFileExists(iconPath); } else if (expectedIcon == null) { TKit.assertPathExists(iconPath, false); } else { diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java index 3a27ae32f437a..9776ab5c4c838 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/LinuxHelper.java @@ -22,7 +22,11 @@ */ package jdk.jpackage.test; +import static jdk.jpackage.test.AdditionalLauncher.getAdditionalLauncherProperties; import static java.util.Collections.unmodifiableSortedSet; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; import java.io.IOException; import java.io.UncheckedIOException; @@ -45,7 +49,6 @@ import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.stream.Stream; import jdk.jpackage.internal.util.PathUtils; import jdk.jpackage.internal.util.function.ThrowingConsumer; @@ -164,8 +167,7 @@ public static List getPrerequisitePackages(JPackageCommand cmd) { switch (packageType) { case LINUX_DEB: return Stream.of(getDebBundleProperty(cmd.outputBundle(), - "Depends").split(",")).map(String::strip).collect( - Collectors.toList()); + "Depends").split(",")).map(String::strip).toList(); case LINUX_RPM: return Executor.of("rpm", "-qp", "-R") @@ -326,10 +328,9 @@ static void verifyPackageBundleEssential(JPackageCommand cmd) { if (cmd.isRuntime()) { Path runtimeDir = cmd.appRuntimeDirectory(); Set expectedCriticalRuntimePaths = CRITICAL_RUNTIME_FILES.stream().map( - runtimeDir::resolve).collect(Collectors.toSet()); + runtimeDir::resolve).collect(toSet()); Set actualCriticalRuntimePaths = getPackageFiles(cmd).filter( - expectedCriticalRuntimePaths::contains).collect( - Collectors.toSet()); + expectedCriticalRuntimePaths::contains).collect(toSet()); checkPrerequisites = expectedCriticalRuntimePaths.equals( actualCriticalRuntimePaths); } else { @@ -375,8 +376,7 @@ static void addBundleDesktopIntegrationVerifier(PackageTest test, boolean integr Function, String> verifier = (lines) -> { // Lookup for xdg commands return lines.stream().filter(line -> { - Set words = Stream.of(line.split("\\s+")).collect( - Collectors.toSet()); + Set words = Stream.of(line.split("\\s+")).collect(toSet()); return words.contains("xdg-desktop-menu") || words.contains( "xdg-mime") || words.contains("xdg-icon-resource"); }).findFirst().orElse(null); @@ -454,11 +454,29 @@ static void verifyDesktopFiles(JPackageCommand cmd, boolean installed) { } private static Collection getDesktopFiles(JPackageCommand cmd) { + var unpackedDir = cmd.appLayout().desktopIntegrationDirectory(); + + return relativePackageFilesInSubdirectory(cmd, ApplicationLayout::desktopIntegrationDirectory) + .filter(path -> { + return path.getNameCount() == 1; + }) + .filter(path -> { + return ".desktop".equals(PathUtils.getSuffix(path)); + }) + .map(unpackedDir::resolve) + .toList(); + } + + private static Stream relativePackageFilesInSubdirectory( + JPackageCommand cmd, Function subdirFunc) { + + var unpackedDir = subdirFunc.apply(cmd.appLayout()); var packageDir = cmd.pathToPackageFile(unpackedDir); + return getPackageFiles(cmd).filter(path -> { - return packageDir.equals(path.getParent()) && path.getFileName().toString().endsWith(".desktop"); - }).map(Path::getFileName).map(unpackedDir::resolve).toList(); + return path.startsWith(packageDir); + }).map(packageDir::relativize); } private static String launcherNameFromDesktopFile(JPackageCommand cmd, Optional predefinedAppImage, Path desktopFile) { @@ -496,7 +514,20 @@ private static void verifyDesktopFile(JPackageCommand cmd, Optional { + return Optional.ofNullable(cmd.getArgumentValue("--description")); + }).orElseGet(cmd::name); + } + + for (var e : List.of( + Map.entry("Type", "Application"), + Map.entry("Terminal", "false"), + Map.entry("Comment", launcherDescription) + )) { String key = e.getKey(); TKit.assertEquals(e.getValue(), data.find(key).orElseThrow(), String.format( "Check value of [%s] key", key)); @@ -768,10 +799,10 @@ private static enum Scriptlet { static final Pattern RPM_HEADER_PATTERN = Pattern.compile(String.format( "(%s) scriptlet \\(using /bin/sh\\):", Stream.of(values()).map( - v -> v.rpm).collect(Collectors.joining("|")))); + v -> v.rpm).collect(joining("|")))); static final Map RPM_MAP = Stream.of(values()).collect( - Collectors.toMap(v -> v.rpm, v -> v)); + toMap(v -> v.rpm, v -> v)); } public static String getDefaultPackageArch(PackageType type) { @@ -848,7 +879,7 @@ private static final class DesktopFile { } else { return Map.entry(components[0], components[1]); } - }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + }).collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); } catch (IOException ex) { throw new UncheckedIOException(ex); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ObjectMapper.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ObjectMapper.java new file mode 100644 index 0000000000000..f35e255951eeb --- /dev/null +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/ObjectMapper.java @@ -0,0 +1,780 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toSet; +import static jdk.jpackage.internal.util.function.ExceptionBox.rethrowUnchecked; +import static jdk.jpackage.internal.util.function.ThrowingConsumer.toConsumer; +import static jdk.jpackage.internal.util.function.ThrowingRunnable.toRunnable; +import static jdk.jpackage.internal.util.function.ThrowingSupplier.toSupplier; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigInteger; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +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.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntPredicate; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.stream.XMLStreamWriter; +import jdk.jpackage.internal.util.IdentityWrapper; + +public final class ObjectMapper { + + private ObjectMapper( + Predicate classFilter, + Predicate> methodFilter, + Predicate leafClassFilter, + Map> substitutes, + Map, BiConsumer>> mutators, + Set accessPackageMethods) { + + this.classFilter = Objects.requireNonNull(classFilter); + this.methodFilter = Objects.requireNonNull(methodFilter); + this.leafClassFilter = Objects.requireNonNull(leafClassFilter); + this.substitutes = Objects.requireNonNull(substitutes); + this.mutators = Objects.requireNonNull(mutators); + this.accessPackageMethods = accessPackageMethods; + } + + public static Builder blank() { + return new Builder().allowAllLeafClasses(false).exceptLeafClasses().add(Stream.of( + Object.class, + String.class, String[].class, + boolean.class, Boolean.class, boolean[].class, Boolean[].class, + byte.class, Byte.class, byte[].class, Byte[].class, + char.class, Character.class, char[].class, Character[].class, + short.class, Short.class, short[].class, Short[].class, + int.class, Integer.class, int[].class, Integer[].class, + long.class, Long.class, long[].class, Long[].class, + float.class, Float.class, float[].class, Float[].class, + double.class, Double.class, double[].class, Double[].class, + void.class, Void.class, Void[].class + ).map(Class::getName).toList()).apply(); + } + + public static Builder standard() { + return blank() + .mutate(configureObject()) + .mutate(configureLeafClasses()) + .mutate(configureOptional()) + .mutate(configureFunctionalTypes()) + .mutate(configureEnum()) + .mutate(configureException()); + } + + public static Consumer configureObject() { + // Exclude all method of Object class. + return builder -> { + builder.exceptMethods().add(OBJECT_METHODS).apply(); + }; + } + + public static Consumer configureLeafClasses() { + return builder -> { + builder.exceptLeafClasses().add(Stream.of( + IdentityWrapper.class, + Class.class, + Path.class, + Path.of("").getClass(), + UUID.class, + BigInteger.class + ).map(Class::getName).toList()).apply(); + }; + } + + public static Consumer configureOptional() { + return builder -> { + // Filter out all but "get()" methods of "Optional" class. + builder.exceptAllMethods(Optional.class).remove("get").apply(); + // Substitute "Optional.get()" with the function that will return "null" if the value is "null". + builder.subst(Optional.class, "get", opt -> { + if (opt.isPresent()) { + return opt.get(); + } else { + return null; + } + }); + }; + } + + public static Consumer configureFunctionalTypes() { + // Remove all getters from the standard functional types. + return builder -> { + builder.exceptAllMethods(Predicate.class).apply(); + builder.exceptAllMethods(Supplier.class).apply(); + }; + } + + public static Consumer configureEnum() { + return builder -> { + // Filter out "getDeclaringClass()" and "describeConstable()" methods of "Enum" class. + builder.exceptSomeMethods(Enum.class).add("getDeclaringClass", "describeConstable").apply(); + }; + } + + public static Consumer configureException() { + return builder -> { + // Include only "getMessage()" and "getCause()" methods of "Exception" class. + builder.exceptAllMethods(Exception.class).remove("getMessage", "getCause").apply(); + builder.mutator(Exception.class, (ex, map) -> { + var eit = map.entrySet().iterator(); + while (eit.hasNext()) { + var e = eit.next(); + if (e.getValue() == NULL) { + // Remove property with the "null" value. + eit.remove(); + } + } + map.put("getClass", ex.getClass().getName()); + }); + }; + } + + public static String lookupFullMethodName(Method m) { + return lookupFullMethodName(m.getDeclaringClass(), m.getName()); + } + + public static String lookupFullMethodName(Class c, String m) { + return Objects.requireNonNull(c).getName() + lookupMethodName(m); + } + + public static String lookupMethodName(Method m) { + return lookupMethodName(m.getName()); + } + + public static String lookupMethodName(String m) { + return "#" + Objects.requireNonNull(m); + } + + public static Object wrapIdentity(Object v) { + if (v instanceof IdentityWrapper wrapper) { + return wrapper; + } else { + return new IdentityWrapper(v); + } + } + + public static void store(Map map, XMLStreamWriter xml) { + XmlWriter.writePropertyMap(map, xml); + } + + @SuppressWarnings("unchecked") + public static Optional findNonNullProperty(Map map, String propertyName) { + Objects.requireNonNull(propertyName); + Objects.requireNonNull(map); + + return Optional.ofNullable(map.get(propertyName)).filter(Predicate.not(NULL::equals)).map(v -> { + return (T)v; + }); + } + + public Object map(Object obj) { + if (obj != null) { + return mapObject(obj).orElseGet(Map::of); + } else { + return null; + } + } + + @SuppressWarnings("unchecked") + public Map toMap(Object obj) { + if (obj == null) { + return null; + } else { + var mappedObj = map(obj); + if (mappedObj instanceof Map m) { + return (Map)m; + } else { + return Map.of("value", mappedObj); + } + } + } + + public Optional mapObject(Object obj) { + if (obj == null) { + return Optional.empty(); + } + + if (leafClassFilter.test(obj.getClass().getName())) { + return Optional.of(obj); + } + + if (!filter(obj.getClass())) { + return Optional.empty(); + } + + if (obj instanceof Iterable col) { + return Optional.of(mapIterable(col)); + } + + if (obj instanceof Map map) { + return Optional.of(mapMap(map)); + } + + if (obj.getClass().isArray()) { + return Optional.of(mapArray(obj)); + } + + var theMap = getMethods(obj).map(m -> { + final Object propertyValue; + final var subst = substitutes.get(m); + if (subst != null) { + propertyValue = applyGetter(obj, subst); + } else { + propertyValue = invoke(m, obj); + } + return Map.entry(m.getName(), mapObject(propertyValue).orElse(NULL)); + }).collect(toMutableMap(Map.Entry::getKey, Map.Entry::getValue)); + + mutators.entrySet().stream().filter(m -> { + return m.getKey().isInstance(obj); + }).findFirst().ifPresent(m -> { + m.getValue().accept(obj, theMap); + }); + + if (theMap.isEmpty()) { + return Optional.of(wrapIdentity(obj)); + } + + return Optional.of(theMap); + } + + private Object invoke(Method m, Object obj) { + try { + return m.invoke(obj); + } catch (IllegalAccessException ex) { + throw rethrowUnchecked(ex); + } catch (InvocationTargetException ex) { + return map(ex.getTargetException()); + } + } + + private Collection mapIterable(Iterable col) { + final List list = new ArrayList<>(); + for (var obj : col) { + list.add(mapObject(obj).orElse(NULL)); + } + return list; + } + + private Map mapMap(Map map) { + return map.entrySet().stream().collect(toMutableMap(e -> { + return mapObject(e.getKey()).orElse(NULL); + }, e -> { + return mapObject(e.getValue()).orElse(NULL); + })); + } + + private Object mapArray(Object arr) { + final var len = Array.getLength(arr); + + if (len == 0) { + return arr; + } + + Object[] buf = null; + + for (int i = 0; i != len; i++) { + var from = Array.get(arr, i); + if (from != null) { + var to = mapObject(from).orElseThrow(); + if (from != to || buf != null) { + if (buf == null) { + buf = (Object[])Array.newInstance(Object.class, len); + System.arraycopy(arr, 0, buf, 0, i); + } + buf[i] = to; + } + } + } + + return Optional.ofNullable((Object)buf).orElse(arr); + } + + @SuppressWarnings("unchecked") + private static Object applyGetter(Object obj, Function getter) { + return getter.apply((T)obj); + } + + private boolean filter(Class type) { + return classFilter.test(type.getName()); + } + + private boolean filter(Method m) { + return methodFilter.test(List.of(lookupMethodName(m), lookupFullMethodName(m))); + } + + private Stream getMethods(Object obj) { + return MethodGroups.create(obj.getClass(), accessPackageMethods).filter(this::filter).map(MethodGroup::callable); + } + + private static boolean defaultFilter(Method m) { + if (Modifier.isStatic(m.getModifiers()) || (m.getParameterCount() > 0) || void.class.equals(m.getReturnType())) { + return false; + } + return true; + } + + private static + Collector> toMutableMap(Function keyMapper, + Function valueMapper) { + return Collectors.toMap(keyMapper, valueMapper, (x , y) -> { + throw new UnsupportedOperationException( + String.format("Entries with the same key and different values [%s] and [%s]", x, y)); + }, HashMap::new); + } + + public static final class Builder { + + private Builder() { + allowAllClasses(); + allowAllLeafClasses(); + allowAllMethods(); + } + + public ObjectMapper create() { + return new ObjectMapper( + classFilter.createPredicate(), + methodFilter.createMultiPredicate(), + leafClassFilter.createPredicate(), + Map.copyOf(substitutes), + Map.copyOf(mutators), + accessPackageMethods); + } + + + public final class NamePredicateBuilder { + + NamePredicateBuilder(Filter sink) { + this.sink = Objects.requireNonNull(sink); + } + + public Builder apply() { + sink.addAll(items); + return Builder.this; + } + + public NamePredicateBuilder add(String... v) { + return add(List.of(v)); + } + + public NamePredicateBuilder add(Collection v) { + items.addAll(v); + return this; + } + + private final Filter sink; + private final Set items = new HashSet<>(); + } + + + public final class AllMethodPredicateBuilder { + + AllMethodPredicateBuilder(Class type) { + impl = new MethodPredicateBuilder(type, false); + } + + public AllMethodPredicateBuilder remove(String... v) { + return remove(List.of(v)); + } + + public AllMethodPredicateBuilder remove(Collection v) { + impl.add(v); + return this; + } + + public Builder apply() { + return impl.apply(); + } + + private final MethodPredicateBuilder impl; + } + + + public final class SomeMethodPredicateBuilder { + + SomeMethodPredicateBuilder(Class type) { + impl = new MethodPredicateBuilder(type, true); + } + + public SomeMethodPredicateBuilder add(String... v) { + return add(List.of(v)); + } + + public SomeMethodPredicateBuilder add(Collection v) { + impl.add(v); + return this; + } + + public Builder apply() { + return impl.apply(); + } + + private final MethodPredicateBuilder impl; + } + + + public Builder allowAllClasses(boolean v) { + classFilter.negate(v); + return this; + } + + public Builder allowAllClasses() { + return allowAllClasses(true); + } + + public Builder allowAllMethods(boolean v) { + methodFilter.negate(v); + return this; + } + + public Builder allowAllMethods() { + return allowAllMethods(true); + } + + public Builder allowAllLeafClasses(boolean v) { + leafClassFilter.negate(v); + return this; + } + + public Builder allowAllLeafClasses() { + return allowAllLeafClasses(true); + } + + public NamePredicateBuilder exceptClasses() { + return new NamePredicateBuilder(classFilter); + } + + public AllMethodPredicateBuilder exceptAllMethods(Class type) { + return new AllMethodPredicateBuilder(type); + } + + public SomeMethodPredicateBuilder exceptSomeMethods(Class type) { + return new SomeMethodPredicateBuilder(type); + } + + public NamePredicateBuilder exceptMethods() { + return new NamePredicateBuilder(methodFilter); + } + + public NamePredicateBuilder exceptLeafClasses() { + return new NamePredicateBuilder(leafClassFilter); + } + + public Builder subst(Method target, Function substitute) { + substitutes.put(Objects.requireNonNull(target), Objects.requireNonNull(substitute)); + return this; + } + + public Builder subst(Class targetClass, String targetMethodName, Function substitute) { + var method = toSupplier(() -> targetClass.getMethod(targetMethodName)).get(); + return subst(method, substitute); + } + + public Builder mutator(Class targetClass, BiConsumer> mutator) { + mutators.put(Objects.requireNonNull(targetClass), Objects.requireNonNull(mutator)); + return this; + } + + public Builder mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + + public Builder accessPackageMethods(Package... packages) { + Stream.of(packages).map(Package::getName).forEach(accessPackageMethods::add); + return this; + } + + + private final class MethodPredicateBuilder { + + MethodPredicateBuilder(Class type, boolean negate) { + this.type = Objects.requireNonNull(type); + buffer.negate(negate); + } + + void add(Collection v) { + buffer.addAll(v); + } + + Builder apply() { + var pred = buffer.createPredicate(); + + var items = MethodGroups.create(type, accessPackageMethods).groups().stream().map(MethodGroup::primary).filter(m -> { + return !OBJECT_METHODS.contains(ObjectMapper.lookupMethodName(m)); + }).filter(m -> { + return !pred.test(m.getName()); + }).map(ObjectMapper::lookupFullMethodName).toList(); + + return exceptMethods().add(items).apply(); + } + + private final Class type; + private final Filter buffer = new Filter(); + } + + + private static final class Filter { + Predicate> createMultiPredicate() { + if (items.isEmpty()) { + var match = negate; + return v -> match; + } else if (negate) { + return v -> { + return v.stream().noneMatch(Set.copyOf(items)::contains); + }; + } else { + return v -> { + return v.stream().anyMatch(Set.copyOf(items)::contains); + }; + } + } + + Predicate createPredicate() { + if (items.isEmpty()) { + var match = negate; + return v -> match; + } else if (negate) { + return Predicate.not(Set.copyOf(items)::contains); + } else { + return Set.copyOf(items)::contains; + } + } + + void addAll(Collection v) { + items.addAll(v); + } + + void negate(boolean v) { + negate = v; + } + + private boolean negate; + private final Set items = new HashSet<>(); + } + + + private final Filter classFilter = new Filter(); + private final Filter methodFilter = new Filter(); + private final Filter leafClassFilter = new Filter(); + private final Map> substitutes = new HashMap<>(); + private final Map, BiConsumer>> mutators = new HashMap<>(); + private final Set accessPackageMethods = new HashSet<>(); + } + + + private record MethodGroup(List methods) { + + MethodGroup { + Objects.requireNonNull(methods); + + if (methods.isEmpty()) { + throw new IllegalArgumentException(); + } + + methods.stream().map(Method::getName).reduce((a, b) -> { + if (!a.equals(b)) { + throw new IllegalArgumentException(); + } else { + return a; + } + }); + } + + Method callable() { + var primary = primary(); + if (!primary.getDeclaringClass().isInterface()) { + primary = methods.stream().filter(m -> { + return m.getDeclaringClass().isInterface(); + }).findFirst().orElse(primary); + } + return primary; + } + + Method primary() { + return methods.getFirst(); + } + + boolean match(Predicate predicate) { + Objects.requireNonNull(predicate); + return methods.stream().allMatch(predicate); + } + } + + + private record MethodGroups(Collection groups) { + + MethodGroups { + Objects.requireNonNull(groups); + } + + Stream filter(Predicate predicate) { + Objects.requireNonNull(predicate); + + return groups.stream().filter(g -> { + return g.match(predicate); + }); + } + + static MethodGroups create(Class type, Set accessPackageMethods) { + List> types = new ArrayList<>(); + + collectSuperclassAndInterfaces(type, types::add); + + final var methodGroups = types.stream() + .map(c -> { + if (accessPackageMethods.contains(c.getPackageName())) { + return PUBLIC_AND_PACKAGE_METHODS_GETTER.apply(c); + } else { + return PUBLIC_METHODS_GETTER.apply(c); + } + }) + .flatMap(x -> x) + .filter(ObjectMapper::defaultFilter) + .collect(groupingBy(Method::getName)); + + return new MethodGroups(methodGroups.values().stream().distinct().map(MethodGroup::new).toList()); + } + + private static void collectSuperclassAndInterfaces(Class type, Consumer> sink) { + Objects.requireNonNull(type); + Objects.requireNonNull(sink); + + for (; type != null; type = type.getSuperclass()) { + sink.accept(type); + for (var i : type.getInterfaces()) { + collectSuperclassAndInterfaces(i, sink); + } + } + } + } + + + private static final class XmlWriter { + static void write(Object obj, XMLStreamWriter xml) { + if (obj instanceof Map map) { + writePropertyMap(map, xml); + } else if (obj instanceof Collection col) { + writeCollection(col, xml); + } else if (obj.getClass().isArray()) { + writeArray(obj, xml); + } else { + toRunnable(() -> xml.writeCharacters(obj.toString())).run(); + } + } + + private static void writePropertyMap(Map map, XMLStreamWriter xml) { + map.entrySet().stream().sorted(Comparator.comparing(e -> e.getKey().toString())).forEach(toConsumer(e -> { + xml.writeStartElement("property"); + xml.writeAttribute("name", e.getKey().toString()); + write(e.getValue(), xml); + xml.writeEndElement(); + })); + } + + private static void writeCollection(Collection col, XMLStreamWriter xml) { + try { + xml.writeStartElement("collection"); + xml.writeAttribute("size", Integer.toString(col.size())); + for (var item : col) { + xml.writeStartElement("item"); + write(item, xml); + xml.writeEndElement(); + } + xml.writeEndElement(); + } catch (Exception ex) { + rethrowUnchecked(ex); + } + } + + private static void writeArray(Object arr, XMLStreamWriter xml) { + var len = Array.getLength(arr); + try { + xml.writeStartElement("array"); + xml.writeAttribute("size", Integer.toString(len)); + for (int i = 0; i != len; i++) { + xml.writeStartElement("item"); + write(Array.get(arr, i), xml); + xml.writeEndElement(); + } + xml.writeEndElement(); + } catch (Exception ex) { + rethrowUnchecked(ex); + } + } + } + + + private final Predicate classFilter; + private final Predicate> methodFilter; + private final Predicate leafClassFilter; + private final Map> substitutes; + private final Map, BiConsumer>> mutators; + private final Set accessPackageMethods; + + static final Object NULL = new Object() { + @Override + public String toString() { + return ""; + } + }; + + private static final Set OBJECT_METHODS = + Stream.of(Object.class.getMethods()).map(ObjectMapper::lookupMethodName).collect(toSet()); + + private static final Function, Stream> PUBLIC_METHODS_GETTER = type -> { + return Stream.of(type.getMethods()); + }; + + private static final Function, Stream> PUBLIC_AND_PACKAGE_METHODS_GETTER = type -> { + return Stream.of(type.getDeclaredMethods()).filter(m -> { + return Stream.of(Modifier::isPrivate, Modifier::isProtected).map(p -> { + return p.test(m.getModifiers()); + }).allMatch(v -> !v); + }).map(m -> { + m.setAccessible(true); + return m; + }); + }; +} diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java index 84453038cd2c8..3226811fe36e3 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/PackageTest.java @@ -39,7 +39,6 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -318,6 +317,11 @@ PackageTest addHelloAppFileAssociationsVerifier(FileAssociations fa) { return this; } + public PackageTest mutate(Consumer mutator) { + mutator.accept(this); + return this; + } + public PackageTest forTypes(Collection types, Runnable action) { final var oldTypes = Set.of(currentTypes.toArray(PackageType[]::new)); try { @@ -334,7 +338,11 @@ public PackageTest forTypes(PackageType type, Runnable action) { } public PackageTest forTypes(PackageType type, Consumer action) { - return forTypes(List.of(type), () -> action.accept(this)); + return forTypes(List.of(type), action); + } + + public PackageTest forTypes(Collection types, Consumer action) { + return forTypes(types, () -> action.accept(this)); } public PackageTest notForTypes(Collection types, Runnable action) { @@ -348,7 +356,11 @@ public PackageTest notForTypes(PackageType type, Runnable action) { } public PackageTest notForTypes(PackageType type, Consumer action) { - return notForTypes(List.of(type), () -> action.accept(this)); + return notForTypes(List.of(type), action); + } + + public PackageTest notForTypes(Collection types, Consumer action) { + return notForTypes(types, () -> action.accept(this)); } public PackageTest configureHelloApp() { @@ -780,7 +792,7 @@ private void verifyPackageInstalled(JPackageCommand cmd) { LauncherAsServiceVerifier.verify(cmd); } - cmd.assertAppLayout(); + cmd.runStandardAsserts(); installVerifiers.forEach(v -> v.accept(cmd)); } diff --git a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java index bdf9fb85672c1..a19b3697a8159 100644 --- a/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java +++ b/test/jdk/tools/jpackage/helpers/jdk/jpackage/test/TKit.java @@ -1260,8 +1260,8 @@ public PathSnapshot(Path path) { this(hashRecursive(path)); } - public static void assertEquals(PathSnapshot a, PathSnapshot b, String msg) { - assertStringListEquals(a.contentHashes(), b.contentHashes(), msg); + public void assertEquals(PathSnapshot other, String msg) { + assertStringListEquals(contentHashes(), other.contentHashes(), msg); } private static List hashRecursive(Path path) { diff --git a/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/IdentityWrapperTest.java b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/IdentityWrapperTest.java new file mode 100644 index 0000000000000..471a7cb55a966 --- /dev/null +++ b/test/jdk/tools/jpackage/junit/share/jdk.jpackage/jdk/jpackage/internal/util/IdentityWrapperTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package jdk.jpackage.internal.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + + +public class IdentityWrapperTest { + + @Test + public void test_null() { + assertThrows(NullPointerException.class, () -> identityOf(null)); + } + + @Test + public void test_equals() { + var obj = new TestRecord(10); + assertEquals(identityOf(obj), identityOf(obj)); + } + + @Test + public void test_not_equals() { + var identity = identityOf(new TestRecord(10)); + var identity2 = identityOf(new TestRecord(10)); + assertNotEquals(identity, identity2); + assertEquals(identity.value(), identity2.value()); + } + + @Test + public void test_Foo() { + var foo = new Foo(10); + assertFalse(foo.accessed()); + + foo.hashCode(); + assertTrue(foo.accessed()); + assertTrue(foo.hashCodeCalled()); + assertFalse(foo.equalsCalled()); + + foo = new Foo(1); + foo.equals(null); + assertTrue(foo.accessed()); + assertFalse(foo.hashCodeCalled()); + assertTrue(foo.equalsCalled()); + } + + @Test + public void test_wrappedValue_not_accessed() { + var identity = identityOf(new Foo(10)); + var identity2 = identityOf(new Foo(10)); + assertNotEquals(identity, identity2); + + assertFalse(identity.value().accessed()); + assertFalse(identity2.value().accessed()); + + assertEquals(identity.value(), identity2.value()); + assertEquals(identity2.value(), identity.value()); + + assertTrue(identity.value().accessed()); + assertTrue(identity2.value().accessed()); + } + + @Test + public void test_wrappedValue_not_accessed_in_set() { + var identitySet = Set.of(identityOf(new Foo(10)), identityOf(new Foo(10)), identityOf(new Foo(10))); + assertEquals(3, identitySet.size()); + + var valueSet = identitySet.stream().peek(identity -> { + assertFalse(identity.value().accessed()); + }).map(IdentityWrapper::value).collect(Collectors.toSet()); + + assertEquals(1, valueSet.size()); + } + + private static IdentityWrapper identityOf(T obj) { + return new IdentityWrapper<>(obj); + } + + private record TestRecord(int v) {} + + private final static class Foo { + + Foo(int v) { + this.v = v; + } + + @Override + public int hashCode() { + try { + return Objects.hash(v); + } finally { + hashCodeCalled = true; + } + } + + @Override + public boolean equals(Object obj) { + try { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Foo other = (Foo) obj; + return v == other.v; + } finally { + equalsCalled = true; + } + } + + boolean equalsCalled() { + return equalsCalled; + } + + boolean hashCodeCalled() { + return hashCodeCalled; + } + + boolean accessed() { + return equalsCalled() || hashCodeCalled(); + } + + private final int v; + private boolean equalsCalled; + private boolean hashCodeCalled; + } +} diff --git a/test/jdk/tools/jpackage/junit/tools/jdk/jpackage/test/JUnitUtils.java b/test/jdk/tools/jpackage/junit/tools/jdk/jpackage/test/JUnitUtils.java new file mode 100644 index 0000000000000..c91b178cb108b --- /dev/null +++ b/test/jdk/tools/jpackage/junit/tools/jdk/jpackage/test/JUnitUtils.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package jdk.jpackage.test; + +import java.util.Map; +import java.util.Objects; +import org.junit.jupiter.api.Assertions; + + +public final class JUnitUtils { + + /** + * Convenience adapter for {@link Assertions#assertArrayEquals(byte[], byte[])}, + * {@link Assertions#assertArrayEquals(int[], int[])}, + * {@link Assertions#assertArrayEquals(Object[], Object[])}, etc. methods. + * + * @param expected the expected array to test for equality + * @param actual the actual array to test for equality + */ + public static void assertArrayEquals(Object expected, Object actual) { + ARRAY_ASSERTERS.getOrDefault(expected.getClass().componentType(), OBJECT_ARRAY_ASSERTER).acceptUnchecked(expected, actual); + } + + /** + * Converts the given exception object to a property map. + *

+ * Values returned by public getters are added to the map. Names of getters are + * the keys in the returned map. The values are property map representations of + * the objects returned by the getters. Only {@link Throwable#getMessage()} and + * {@link Throwable#getCause()} getters are picked for the property map by + * default. If the exception class has additional getters, they will be added to + * the map. {@code null} is permitted. + * + * @param ex the exception to convert into a property map + * @return the property map view of the given exception object + */ + public static Map exceptionAsPropertyMap(Exception ex) { + return EXCEPTION_OM.toMap(ex); + } + + + public static final class ExceptionPattern { + + public ExceptionPattern() { + } + + public boolean match(Exception ex) { + Objects.requireNonNull(ex); + + if (expectedType != null && !expectedType.isInstance(ex)) { + return false; + } + + if (expectedMessage != null && !expectedMessage.equals(ex.getMessage())) { + return false; + } + + if (expectedCauseType != null && !expectedCauseType.isInstance(ex.getCause())) { + return false; + } + + return true; + } + + public ExceptionPattern hasMessage(String v) { + expectedMessage = v; + return this; + } + + public ExceptionPattern isInstanceOf(Class v) { + expectedType = v; + return this; + } + + public ExceptionPattern isCauseInstanceOf(Class v) { + expectedCauseType = v; + return this; + } + + public ExceptionPattern hasCause(boolean v) { + return isCauseInstanceOf(v ? Exception.class : null); + } + + public ExceptionPattern hasCause() { + return hasCause(true); + } + + private String expectedMessage; + private Class expectedType; + private Class expectedCauseType; + } + + + @FunctionalInterface + private interface ArrayEqualsAsserter { + void accept(T expected, T actual); + + @SuppressWarnings("unchecked") + default void acceptUnchecked(Object expected, Object actual) { + accept((T)expected, (T)actual); + } + } + + + private static final Map, ArrayEqualsAsserter> ARRAY_ASSERTERS = Map.of( + boolean.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals, + byte.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals, + char.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals, + double.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals, + float.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals, + int.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals, + long.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals, + short.class, (ArrayEqualsAsserter)Assertions::assertArrayEquals + ); + + private static final ArrayEqualsAsserter OBJECT_ARRAY_ASSERTER = Assertions::assertArrayEquals; + + private static final ObjectMapper EXCEPTION_OM = ObjectMapper.standard().create(); +} diff --git a/test/jdk/tools/jpackage/linux/ShortcutHintTest.java b/test/jdk/tools/jpackage/linux/ShortcutHintTest.java index 4d3b33bcd6b58..8d373cb2b86dc 100644 --- a/test/jdk/tools/jpackage/linux/ShortcutHintTest.java +++ b/test/jdk/tools/jpackage/linux/ShortcutHintTest.java @@ -164,7 +164,7 @@ public static void testDesktopFileFromResourceDir() throws IOException { "Exec=APPLICATION_LAUNCHER", "Terminal=false", "Type=Application", - "Comment=", + "Comment=APPLICATION_DESCRIPTION", "Icon=APPLICATION_ICON", "Categories=DEPLOY_BUNDLE_CATEGORY", expectedVersionString diff --git a/test/jdk/tools/jpackage/share/AddLShortcutTest.java b/test/jdk/tools/jpackage/share/AddLShortcutTest.java index 9c50c6ffc98ca..f000e79227e76 100644 --- a/test/jdk/tools/jpackage/share/AddLShortcutTest.java +++ b/test/jdk/tools/jpackage/share/AddLShortcutTest.java @@ -118,6 +118,12 @@ public void test() { HelloApp.createBundle(JavaAppDesc.parse(addLauncherApp + "*another.jar:Welcome"), cmd.inputDir()); }); + if (RunnablePackageTest.hasAction(RunnablePackageTest.Action.INSTALL)) { + // Ensure launchers are executable because the output bundle will be installed + // and launchers will be attempted to be executed through their shortcuts. + packageTest.addInitializer(JPackageCommand::ignoreFakeRuntime); + } + new FileAssociations(packageName).applyTo(packageTest); new AdditionalLauncher("Foo") diff --git a/test/jdk/tools/jpackage/share/AddLauncherTest.java b/test/jdk/tools/jpackage/share/AddLauncherTest.java index a7bfbf376edb8..21f475cbd7810 100644 --- a/test/jdk/tools/jpackage/share/AddLauncherTest.java +++ b/test/jdk/tools/jpackage/share/AddLauncherTest.java @@ -21,18 +21,22 @@ * questions. */ -import java.nio.file.Path; -import java.util.Map; import java.lang.invoke.MethodHandles; -import jdk.jpackage.test.PackageTest; -import jdk.jpackage.test.FileAssociations; +import java.nio.file.Path; +import java.util.function.Consumer; +import jdk.internal.util.OperatingSystem; import jdk.jpackage.test.AdditionalLauncher; +import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.CfgFile; +import jdk.jpackage.test.ConfigurationTarget; +import jdk.jpackage.test.FileAssociations; import jdk.jpackage.test.JPackageCommand; import jdk.jpackage.test.JavaAppDesc; +import jdk.jpackage.test.PackageTest; +import jdk.jpackage.test.PackageType; +import jdk.jpackage.test.RunnablePackageTest.Action; import jdk.jpackage.test.TKit; -import jdk.jpackage.test.Annotations.Test; -import jdk.jpackage.test.Annotations.Parameter; -import jdk.jpackage.test.CfgFile; /** * Test --add-launcher parameter. Output of the test should be @@ -233,6 +237,61 @@ public void testMainLauncherIsModular(boolean mainLauncherIsModular) { "Check app.classpath value in ModularAppLauncher cfg file"); } + /** + * Test --description option + */ + @Test(ifNotOS = OperatingSystem.MACOS) // Don't run on macOS as launcher description is ignored on this platform + @Parameter("true") + @Parameter("fase") + public void testDescription(boolean withPredefinedAppImage) { + + ConfigurationTarget target; + if (TKit.isWindows() || withPredefinedAppImage) { + target = new ConfigurationTarget(JPackageCommand.helloAppImage()); + } else { + target = new ConfigurationTarget(new PackageTest().configureHelloApp()); + } + + target.addInitializer(cmd -> { + cmd.setArgumentValue("--name", "Foo").setArgumentValue("--description", "Hello"); + cmd.setFakeRuntime(); + cmd.setStandardAsserts(JPackageCommand.StandardAssert.MAIN_LAUNCHER_DESCRIPTION); + }); + + target.add(new AdditionalLauncher("x")); + target.add(new AdditionalLauncher("bye").setProperty("description", "Bye")); + + target.test().ifPresent(test -> { + // Make all launchers have shortcuts and thus .desktop files. + // Launcher description is recorded in a desktop file and verified automatically. + test.mutate(addLinuxShortcuts()); + }); + + target.cmd().ifPresent(withPredefinedAppImage ? JPackageCommand::execute : JPackageCommand::executeAndAssertImageCreated); + target.test().ifPresent(test -> { + test.run(Action.CREATE_AND_UNPACK); + }); + + if (withPredefinedAppImage) { + new PackageTest().addInitializer(cmd -> { + cmd.setArgumentValue("--name", "Bar"); + // Should not have impact of launcher descriptions, but it does. + cmd.setArgumentValue("--description", "Installer"); + cmd.removeArgumentWithValue("--input").setArgumentValue("--app-image", target.cmd().orElseThrow().outputBundle()); + }).mutate(addLinuxShortcuts()).run(Action.CREATE_AND_UNPACK); + } + } + + private static Consumer addLinuxShortcuts() { + return test -> { + test.forTypes(PackageType.LINUX, () -> { + test.addInitializer(cmd -> { + cmd.addArgument("--linux-shortcut"); + }); + }); + }; + } + private static final Path GOLDEN_ICON = TKit.TEST_SRC_ROOT.resolve(Path.of( "resources", "icon" + TKit.ICON_SUFFIX)); } diff --git a/test/jdk/tools/jpackage/share/AppImagePackageTest.java b/test/jdk/tools/jpackage/share/AppImagePackageTest.java index 34a418c6f9ec9..aacb76b122b29 100644 --- a/test/jdk/tools/jpackage/share/AppImagePackageTest.java +++ b/test/jdk/tools/jpackage/share/AppImagePackageTest.java @@ -21,20 +21,21 @@ * questions. */ -import java.nio.file.Path; -import java.nio.file.Files; import java.io.IOException; -import java.util.List; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Predicate; import jdk.jpackage.internal.util.XmlUtils; -import jdk.jpackage.test.AppImageFile; import jdk.jpackage.test.Annotations.Parameter; +import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.AppImageFile; import jdk.jpackage.test.CannedFormattedString; -import jdk.jpackage.test.TKit; import jdk.jpackage.test.JPackageCommand; +import jdk.jpackage.test.JPackageCommand.StandardAssert; import jdk.jpackage.test.JPackageStringBundle; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.RunnablePackageTest.Action; -import jdk.jpackage.test.Annotations.Test; +import jdk.jpackage.test.TKit; /** * Test --app-image parameter. The output installer should provide the same @@ -55,56 +56,86 @@ */ public class AppImagePackageTest { + /** + * Create a native bundle from a valid predefined app image produced by jpackage. + */ @Test public static void test() { - Path appimageOutput = TKit.workDir().resolve("appimage"); - JPackageCommand appImageCmd = JPackageCommand.helloAppImage() - .setArgumentValue("--dest", appimageOutput); + var appImageCmd = JPackageCommand.helloAppImage() + .setArgumentValue("--dest", TKit.createTempDirectory("appimage")); new PackageTest() - .addRunOnceInitializer(() -> appImageCmd.execute()) + .addRunOnceInitializer(appImageCmd::execute) .addInitializer(cmd -> { cmd.addArguments("--app-image", appImageCmd.outputBundle()); cmd.removeArgumentWithValue("--input"); }).addBundleDesktopIntegrationVerifier(false).run(); } + /** + * Create a native bundle from a predefined app image not produced by jpackage + * but having a valid ".jpackage.xml" file. + * + * @param withIcon {@code true} if jpackage command line should have "--icon" + * option + */ @Test @Parameter("true") @Parameter("false") public static void testEmpty(boolean withIcon) throws IOException { - final String name = "EmptyAppImagePackageTest"; - final String imageName = name + (TKit.isOSX() ? ".app" : ""); - Path appImageDir = TKit.createTempDirectory("appimage").resolve(imageName); - Files.createDirectories(appImageDir.resolve("bin")); - Path libDir = Files.createDirectories(appImageDir.resolve("lib")); - TKit.createTextFile(libDir.resolve("README"), - List.of("This is some arbitrary text for the README file\n")); + var appImageCmd = JPackageCommand.helloAppImage() + .setFakeRuntime() + .setArgumentValue("--name", "EmptyAppImagePackageTest") + .setArgumentValue("--dest", TKit.createTempDirectory("appimage")); new PackageTest() + .addRunOnceInitializer(appImageCmd::execute) + .addRunOnceInitializer(() -> { + var layout = appImageCmd.appLayout(); + if (!TKit.isOSX()) { + // Delete the launcher if not on macOS. + // On macOS, deleting the launcher will render the app bundle invalid. + TKit.deleteIfExists(appImageCmd.appLauncherPath()); + } + // Delete the runtime. + TKit.deleteDirectoryRecursive(layout.runtimeDirectory()); + // Delete the "app" dir. + TKit.deleteDirectoryRecursive(layout.appDirectory()); + + new AppImageFile(appImageCmd.name(), "PhonyMainClass").save(appImageCmd.outputBundle()); + var appImageDir = appImageCmd.outputBundle(); + + TKit.trace(String.format("Files in [%s] app image:", appImageDir)); + try (var files = Files.walk(appImageDir)) { + files.sequential() + .filter(Predicate.isEqual(appImageDir).negate()) + .map(path -> String.format("[%s]", appImageDir.relativize(path))) + .forEachOrdered(TKit::trace); + TKit.trace("Done"); + } + }) .addInitializer(cmd -> { - cmd.addArguments("--app-image", appImageDir); + cmd.addArguments("--app-image", appImageCmd.outputBundle()); if (withIcon) { cmd.addArguments("--icon", iconPath("icon")); } cmd.removeArgumentWithValue("--input"); - new AppImageFile("EmptyAppImagePackageTest", "Hello").save(appImageDir); - // on mac, with --app-image and without --mac-package-identifier, - // will try to infer it from the image, so foreign image needs it. - if (TKit.isOSX()) { - cmd.addArguments("--mac-package-identifier", name); - } + cmd.excludeStandardAsserts( + StandardAssert.MAIN_JAR_FILE, + StandardAssert.MAIN_LAUNCHER_FILES, + StandardAssert.MAC_BUNDLE_STRUCTURE, + StandardAssert.RUNTIME_DIRECTORY); }) - // On macOS we always signing app image and signing will fail, since - // test produces invalid app bundle. - .setExpectedExitCode(TKit.isOSX() ? 1 : 0) - .run(Action.CREATE, Action.UNPACK); - // default: {CREATE, UNPACK, VERIFY}, but we can't verify foreign image + .run(Action.CREATE_AND_UNPACK); } + /** + * Bad predefined app image - not an output of jpackage. + * jpackage command using the bad predefined app image doesn't have "--name" option. + */ @Test public static void testBadAppImage() throws IOException { Path appImageDir = TKit.createTempDirectory("appimage"); @@ -114,6 +145,9 @@ public static void testBadAppImage() throws IOException { }).run(Action.CREATE); } + /** + * Bad predefined app image - not an output of jpackage. + */ @Test public static void testBadAppImage2() throws IOException { Path appImageDir = TKit.createTempDirectory("appimage"); @@ -121,8 +155,11 @@ public static void testBadAppImage2() throws IOException { configureBadAppImage(appImageDir).run(Action.CREATE); } + /** + * Bad predefined app image - valid app image missing ".jpackage.xml" file. + */ @Test - public static void testBadAppImage3() throws IOException { + public static void testBadAppImage3() { Path appImageDir = TKit.createTempDirectory("appimage"); JPackageCommand appImageCmd = JPackageCommand.helloAppImage(). @@ -134,8 +171,11 @@ public static void testBadAppImage3() throws IOException { }).run(Action.CREATE); } + /** + * Bad predefined app image - valid app image with invalid ".jpackage.xml" file. + */ @Test - public static void testBadAppImageFile() throws IOException { + public static void testBadAppImageFile() { final var appImageRoot = TKit.createTempDirectory("appimage"); final var appImageCmd = JPackageCommand.helloAppImage(). diff --git a/test/jdk/tools/jpackage/share/InOutPathTest.java b/test/jdk/tools/jpackage/share/InOutPathTest.java index f7c597d2ed346..d36731c2960e5 100644 --- a/test/jdk/tools/jpackage/share/InOutPathTest.java +++ b/test/jdk/tools/jpackage/share/InOutPathTest.java @@ -38,7 +38,7 @@ import jdk.jpackage.test.Annotations.Test; import jdk.jpackage.internal.util.function.ThrowingConsumer; import jdk.jpackage.test.JPackageCommand; -import jdk.jpackage.test.JPackageCommand.AppLayoutAssert; +import jdk.jpackage.test.JPackageCommand.StandardAssert; import jdk.jpackage.test.PackageTest; import jdk.jpackage.test.PackageType; import static jdk.jpackage.test.RunnablePackageTest.Action.CREATE_AND_UNPACK; @@ -177,7 +177,7 @@ private static void runTest(Set packageTypes, if (!isAppImageValid(cmd)) { // Standard asserts for .jpackage.xml fail in messed up app image. Disable them. // Other standard asserts for app image contents should pass. - cmd.excludeAppLayoutAsserts(AppLayoutAssert.APP_IMAGE_FILE); + cmd.excludeStandardAsserts(StandardAssert.APP_IMAGE_FILE); } }; diff --git a/test/jdk/tools/jpackage/share/LicenseTest.java b/test/jdk/tools/jpackage/share/LicenseTest.java index c9e3c8508aa61..1c6bfd51b62d6 100644 --- a/test/jdk/tools/jpackage/share/LicenseTest.java +++ b/test/jdk/tools/jpackage/share/LicenseTest.java @@ -208,7 +208,7 @@ private static Path linuxLicenseFile(JPackageCommand cmd) { private static void verifyLicenseFileInLinuxPackage(JPackageCommand cmd, Path expectedLicensePath) { TKit.assertTrue(LinuxHelper.getPackageFiles(cmd).filter(path -> path.equals( - expectedLicensePath)).findFirst().orElse(null) != null, + expectedLicensePath)).findFirst().isPresent(), String.format("Check license file [%s] is in %s package", expectedLicensePath, LinuxHelper.getPackageName(cmd))); } diff --git a/test/jdk/tools/jpackage/share/RuntimePackageTest.java b/test/jdk/tools/jpackage/share/RuntimePackageTest.java index f66f774b227ac..caa129713b48b 100644 --- a/test/jdk/tools/jpackage/share/RuntimePackageTest.java +++ b/test/jdk/tools/jpackage/share/RuntimePackageTest.java @@ -135,11 +135,7 @@ private static PackageTest init(ThrowingSupplier createRuntime) { }) .addInstallVerifier(cmd -> { var src = TKit.assertDirectoryContentRecursive(inputRuntimeDir(cmd)).items(); - Path dest = cmd.appRuntimeDirectory(); - if (TKit.isOSX()) { - dest = dest.resolve("Contents/Home"); - } - + var dest = cmd.appLayout().runtimeHomeDirectory(); TKit.assertDirectoryContentRecursive(dest).match(src); }) .forTypes(PackageType.LINUX_DEB, test -> {