diff --git a/experiments/unit-tests/2/.gitignore b/experiments/unit-tests/2/.gitignore new file mode 100644 index 0000000..9c5877f --- /dev/null +++ b/experiments/unit-tests/2/.gitignore @@ -0,0 +1,4 @@ +/mods/ +/target-test/ +/src/module-info.java +/test/module-info.java diff --git a/experiments/unit-tests/2/Makefile b/experiments/unit-tests/2/Makefile new file mode 100644 index 0000000..22433e5 --- /dev/null +++ b/experiments/unit-tests/2/Makefile @@ -0,0 +1,28 @@ +jarfile := junit-platform-console-standalone-1.6.2.jar + +.PHONY: test + +test: mods/$(jarfile) target-test/one.jar + javahms -p mods/$(jarfile):target-test/one.jar \ + -m org.junit.platform.console.standalone/org.junit.platform.console.ConsoleLauncher \ + -o one + +target-test/one.jar: mods/$(jarfile) + cp module-info-src.java src/module-info.java + modulec src + + rm -rf target-test/classes + mkdir -p target-test + cp -r target/classes target-test/classes + rm src/module-info.java + cp module-info-test.java test/module-info.java + modulec -p mods/$(jarfile) -o target-test test + +mods/$(jarfile): mods + cp ../junit-jupiter/target/$(jarfile) $@ + +mods: + mkdir mods + +clean: + rm -rf mods src/module-info.java test/module-info.java target target-test diff --git a/experiments/unit-tests/2/README b/experiments/unit-tests/2/README new file mode 100644 index 0000000..e495754 --- /dev/null +++ b/experiments/unit-tests/2/README @@ -0,0 +1,9 @@ +This experiment tries to run unit test with JUnit 5 using the "standard" or +"naive" approach to whitebox unit testing with hybrid modules: + + 1. The modular JAR contains both the module and the unit tests, with a + module-info.class containing the union (module-info-test.java). + + 2. The console launcher non-modular JAR has been changed into a modular JAR. + + 3. (2) must require (1) in order to observe that module. diff --git a/experiments/unit-tests/2/module-info-src.java b/experiments/unit-tests/2/module-info-src.java new file mode 100644 index 0000000..65af2b0 --- /dev/null +++ b/experiments/unit-tests/2/module-info-src.java @@ -0,0 +1,3 @@ +module one { + exports one.exported; +} diff --git a/experiments/unit-tests/2/module-info-test.java b/experiments/unit-tests/2/module-info-test.java new file mode 100644 index 0000000..0c5baf4 --- /dev/null +++ b/experiments/unit-tests/2/module-info-test.java @@ -0,0 +1,9 @@ +// JUnit 5 needs reflective access to package-private test classes and methods, +// which is typical of JUnit 5, so open the test module. +open module one { + // Module requirements: + exports one.exported; + + // Additional unit test requirements: + requires org.junit.platform.console.standalone; +} diff --git a/experiments/unit-tests/2/src/one/exported/PackagePrivate.java b/experiments/unit-tests/2/src/one/exported/PackagePrivate.java new file mode 100644 index 0000000..6ad38e4 --- /dev/null +++ b/experiments/unit-tests/2/src/one/exported/PackagePrivate.java @@ -0,0 +1,7 @@ +package one.exported; + +class PackagePrivate { + public static final int PUBLIC = 10; + static final int PACKAGE_PRIVATE = 11; + private static final int PRIVATE = 12; +} diff --git a/experiments/unit-tests/2/src/one/exported/Public.java b/experiments/unit-tests/2/src/one/exported/Public.java new file mode 100644 index 0000000..c481702 --- /dev/null +++ b/experiments/unit-tests/2/src/one/exported/Public.java @@ -0,0 +1,7 @@ +package one.exported; + +public class Public { + public static final int PUBLIC = 1; + static final int PACKAGE_PRIVATE = 2; + private static final int PRIVATE = 3; +} diff --git a/experiments/unit-tests/2/src/one/internal/PackagePrivate.java b/experiments/unit-tests/2/src/one/internal/PackagePrivate.java new file mode 100644 index 0000000..225b01b --- /dev/null +++ b/experiments/unit-tests/2/src/one/internal/PackagePrivate.java @@ -0,0 +1,7 @@ +package one.internal; + +class PackagePrivate { + public static final int PUBLIC = 10; + static final int PACKAGE_PRIVATE = 11; + private static final int PRIVATE = 12; +} diff --git a/experiments/unit-tests/2/src/one/internal/Public.java b/experiments/unit-tests/2/src/one/internal/Public.java new file mode 100644 index 0000000..77bcd65 --- /dev/null +++ b/experiments/unit-tests/2/src/one/internal/Public.java @@ -0,0 +1,7 @@ +package one.internal; + +public class Public { + public static final int PUBLIC = 21; + static final int PACKAGE_PRIVATE = 22; + private static final int PRIVATE = 23; +} diff --git a/experiments/unit-tests/2/test/one/exported/PackagePrivateTest.java b/experiments/unit-tests/2/test/one/exported/PackagePrivateTest.java new file mode 100644 index 0000000..4747ada --- /dev/null +++ b/experiments/unit-tests/2/test/one/exported/PackagePrivateTest.java @@ -0,0 +1,13 @@ +package one.exported; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class PackagePrivateTest { + @Test + void verify() { + assertEquals(11, PackagePrivate.PACKAGE_PRIVATE); + } +} diff --git a/experiments/unit-tests/2/test/one/exported/PublicTest.java b/experiments/unit-tests/2/test/one/exported/PublicTest.java new file mode 100644 index 0000000..d8bc883 --- /dev/null +++ b/experiments/unit-tests/2/test/one/exported/PublicTest.java @@ -0,0 +1,5 @@ +package one.exported; + +public class PublicTest { + +} diff --git a/experiments/unit-tests/2/test/one/internal/PublicTest.java b/experiments/unit-tests/2/test/one/internal/PublicTest.java new file mode 100644 index 0000000..0684e69 --- /dev/null +++ b/experiments/unit-tests/2/test/one/internal/PublicTest.java @@ -0,0 +1,13 @@ +package one.internal; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class PublicTest { + @Test + void willPass() { + assertEquals(11, PackagePrivate.PACKAGE_PRIVATE); + } +} diff --git a/experiments/unit-tests/3/Makefile b/experiments/unit-tests/3/Makefile new file mode 100644 index 0000000..8e3749c --- /dev/null +++ b/experiments/unit-tests/3/Makefile @@ -0,0 +1,37 @@ +LIBS := mods/junit-jupiter-api-5.6.2.jar +MODULE_PATH_TEST_WO_DRIVER := target-test/one.jar:mods/junit-jupiter-api-5.6.2.jar:mods/junit-platform-commons-1.6.2.jar:mods/apiguardian-api-1.1.0.jar:mods/opentest4j-1.2.0.jar:mods/junit-platform-launcher-1.6.2.jar:mods/junit-platform-engine-1.6.2.jar +MODULE_PATH_TEST := mods/junit-jupiter-api-5.6.2.jar:mods/junit-platform-commons-1.6.2.jar:mods/apiguardian-api-1.1.0.jar:mods/opentest4j-1.2.0.jar:mods/junit-platform-launcher-1.6.2.jar:mods/junit-platform-engine-1.6.2.jar:lib/junit-jupiter-engine-5.6.2.jar + +unit-tests: compile-test + javahms -p $(MODULE_PATH_TEST) -p target-test/one.jar -m \ + one/no.ion.jhms.junit.jupiter.driver.Main + +compile-src: + rm -rf target/classes + mkdir -p target + cp module-info-src.java src/module-info.java + modulec src + +compile-test: compile-src lib/junit-jupiter-engine-5.6.2.jar + rm -rf target-test/classes + mkdir -p target-test + cp -r target/classes target-test/classes + + rm src/module-info.java + rm -f test/module-info.java + ln -s ../module-info-test.java test/module-info.java + modulec -p $(MODULE_PATH_TEST) -o target-test test + +modularize: + org.junit.jupiter.api@5.6.2 org.junit.platform.commons@ + + +lib/junit-jupiter-engine-5.6.2.jar: mods/junit-jupiter-engine-5.6.2.jar lib + cp $< $@ + modularize-jar -u --module-version 5.6.2 -I junit-jupiter-engine/module-info.java -f lib/junit-jupiter-engine-5.6.2.jar -p mods/apiguardian-api-1.1.0.jar -p mods/junit-jupiter-api-5.6.2.jar -p mods/junit-platform-commons-1.6.2.jar -p mods/junit-platform-engine-1.6.2.jar -p mods/opentest4j-1.2.0.jar + +lib: + mkdir $@ + +clean: + rm -rf lib src/module-info.java test/module-info.java target target-test diff --git a/experiments/unit-tests/3/junit-jupiter-engine/module-info.java b/experiments/unit-tests/3/junit-jupiter-engine/module-info.java new file mode 100644 index 0000000..20dc09f --- /dev/null +++ b/experiments/unit-tests/3/junit-jupiter-engine/module-info.java @@ -0,0 +1,45 @@ +module org.junit.jupiter.engine { + requires org.apiguardian.api; + requires org.junit.jupiter.api; + requires org.junit.platform.commons; + requires org.junit.platform.engine; + requires org.opentest4j; + + // Need access to JupiterTestEngine + exports org.junit.jupiter.engine; + + uses org.junit.jupiter.api.extension.Extension; + + provides org.junit.platform.engine.TestEngine + with org.junit.jupiter.engine.JupiterTestEngine; + + opens org.junit.jupiter.engine.extension to org.junit.platform.commons; +} + +// Original: +/* + * Copyright 2015-2020 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ +// ... +// module org.junit.jupiter.engine { +// requires org.apiguardian.api; +// requires org.junit.jupiter.api; +// requires org.junit.platform.commons; +// requires org.junit.platform.engine; +// requires org.opentest4j; + +// // exports org.junit.jupiter.engine; // Constants... + +// uses org.junit.jupiter.api.extension.Extension; + +// provides org.junit.platform.engine.TestEngine +// with org.junit.jupiter.engine.JupiterTestEngine; + +// opens org.junit.jupiter.engine.extension to org.junit.platform.commons; +// } diff --git a/experiments/unit-tests/3/lib/junit-jupiter-engine-5.6.2.jar b/experiments/unit-tests/3/lib/junit-jupiter-engine-5.6.2.jar new file mode 100644 index 0000000..5bfb6a8 Binary files /dev/null and b/experiments/unit-tests/3/lib/junit-jupiter-engine-5.6.2.jar differ diff --git a/experiments/unit-tests/3/mods-sources/junit-jupiter-api-5.6.2-sources.jar b/experiments/unit-tests/3/mods-sources/junit-jupiter-api-5.6.2-sources.jar new file mode 100644 index 0000000..08ab5e6 Binary files /dev/null and b/experiments/unit-tests/3/mods-sources/junit-jupiter-api-5.6.2-sources.jar differ diff --git a/experiments/unit-tests/3/mods-sources/junit-jupiter-engine-5.6.2-sources.jar b/experiments/unit-tests/3/mods-sources/junit-jupiter-engine-5.6.2-sources.jar new file mode 100644 index 0000000..b02a36e Binary files /dev/null and b/experiments/unit-tests/3/mods-sources/junit-jupiter-engine-5.6.2-sources.jar differ diff --git a/experiments/unit-tests/3/mods-sources/junit-platform-launcher-1.6.2-sources.jar b/experiments/unit-tests/3/mods-sources/junit-platform-launcher-1.6.2-sources.jar new file mode 100644 index 0000000..0a60f22 Binary files /dev/null and b/experiments/unit-tests/3/mods-sources/junit-platform-launcher-1.6.2-sources.jar differ diff --git a/experiments/unit-tests/3/mods/apiguardian-api-1.1.0.jar b/experiments/unit-tests/3/mods/apiguardian-api-1.1.0.jar new file mode 100644 index 0000000..e6fcead Binary files /dev/null and b/experiments/unit-tests/3/mods/apiguardian-api-1.1.0.jar differ diff --git a/experiments/unit-tests/3/mods/junit-jupiter-api-5.6.2.jar b/experiments/unit-tests/3/mods/junit-jupiter-api-5.6.2.jar new file mode 100644 index 0000000..58cfb3d Binary files /dev/null and b/experiments/unit-tests/3/mods/junit-jupiter-api-5.6.2.jar differ diff --git a/experiments/unit-tests/3/mods/junit-jupiter-engine-5.6.2.jar b/experiments/unit-tests/3/mods/junit-jupiter-engine-5.6.2.jar new file mode 100644 index 0000000..024a98f Binary files /dev/null and b/experiments/unit-tests/3/mods/junit-jupiter-engine-5.6.2.jar differ diff --git a/experiments/unit-tests/3/mods/junit-platform-commons-1.6.2.jar b/experiments/unit-tests/3/mods/junit-platform-commons-1.6.2.jar new file mode 100644 index 0000000..ebe59cb Binary files /dev/null and b/experiments/unit-tests/3/mods/junit-platform-commons-1.6.2.jar differ diff --git a/experiments/unit-tests/3/mods/junit-platform-engine-1.6.2.jar b/experiments/unit-tests/3/mods/junit-platform-engine-1.6.2.jar new file mode 100644 index 0000000..0efe30c Binary files /dev/null and b/experiments/unit-tests/3/mods/junit-platform-engine-1.6.2.jar differ diff --git a/experiments/unit-tests/3/mods/junit-platform-launcher-1.6.2.jar b/experiments/unit-tests/3/mods/junit-platform-launcher-1.6.2.jar new file mode 100644 index 0000000..c6e705b Binary files /dev/null and b/experiments/unit-tests/3/mods/junit-platform-launcher-1.6.2.jar differ diff --git a/experiments/unit-tests/3/mods/opentest4j-1.2.0.jar b/experiments/unit-tests/3/mods/opentest4j-1.2.0.jar new file mode 100644 index 0000000..d500636 Binary files /dev/null and b/experiments/unit-tests/3/mods/opentest4j-1.2.0.jar differ diff --git a/experiments/unit-tests/3/module-info-src.java b/experiments/unit-tests/3/module-info-src.java new file mode 100644 index 0000000..65af2b0 --- /dev/null +++ b/experiments/unit-tests/3/module-info-src.java @@ -0,0 +1,3 @@ +module one { + exports one.exported; +} diff --git a/experiments/unit-tests/3/module-info-test.java b/experiments/unit-tests/3/module-info-test.java new file mode 100644 index 0000000..03d68ba --- /dev/null +++ b/experiments/unit-tests/3/module-info-test.java @@ -0,0 +1,27 @@ +// JUnit 5 needs reflective access to package-private test classes and methods, +// which is typical of JUnit 5, so open the test module. +open module one { + // Module requirements: + exports one.exported; + + // even internal packages must be exported in the standard unit tests. + exports one.internal; + + // Additional unit test requirements: + requires org.junit.jupiter.api; + // Transitive requires: + // requires transitive org.apiguardian.api; + // requires transitive org.junit.platform.commons; + // requires transitive org.opentest4j; + + // Additional unit test driver requirements: + requires org.junit.platform.launcher; + // Transitive requires: + // requires transitive java.logging; + // requires transitive org.apiguardian.api; + // requires transitive org.junit.platform.commons; + // requires transitive org.junit.platform.engine; + + // Pulls in JupiterTestEngine + requires org.junit.jupiter.engine; +} diff --git a/experiments/unit-tests/3/src/one/exported/PackagePrivate.java b/experiments/unit-tests/3/src/one/exported/PackagePrivate.java new file mode 100644 index 0000000..6ad38e4 --- /dev/null +++ b/experiments/unit-tests/3/src/one/exported/PackagePrivate.java @@ -0,0 +1,7 @@ +package one.exported; + +class PackagePrivate { + public static final int PUBLIC = 10; + static final int PACKAGE_PRIVATE = 11; + private static final int PRIVATE = 12; +} diff --git a/experiments/unit-tests/3/src/one/exported/Public.java b/experiments/unit-tests/3/src/one/exported/Public.java new file mode 100644 index 0000000..c481702 --- /dev/null +++ b/experiments/unit-tests/3/src/one/exported/Public.java @@ -0,0 +1,7 @@ +package one.exported; + +public class Public { + public static final int PUBLIC = 1; + static final int PACKAGE_PRIVATE = 2; + private static final int PRIVATE = 3; +} diff --git a/experiments/unit-tests/3/src/one/internal/PackagePrivate.java b/experiments/unit-tests/3/src/one/internal/PackagePrivate.java new file mode 100644 index 0000000..225b01b --- /dev/null +++ b/experiments/unit-tests/3/src/one/internal/PackagePrivate.java @@ -0,0 +1,7 @@ +package one.internal; + +class PackagePrivate { + public static final int PUBLIC = 10; + static final int PACKAGE_PRIVATE = 11; + private static final int PRIVATE = 12; +} diff --git a/experiments/unit-tests/3/src/one/internal/Public.java b/experiments/unit-tests/3/src/one/internal/Public.java new file mode 100644 index 0000000..77bcd65 --- /dev/null +++ b/experiments/unit-tests/3/src/one/internal/Public.java @@ -0,0 +1,7 @@ +package one.internal; + +public class Public { + public static final int PUBLIC = 21; + static final int PACKAGE_PRIVATE = 22; + private static final int PRIVATE = 23; +} diff --git a/experiments/unit-tests/3/target-test/one.jar b/experiments/unit-tests/3/target-test/one.jar new file mode 100644 index 0000000..bbab0d3 Binary files /dev/null and b/experiments/unit-tests/3/target-test/one.jar differ diff --git a/experiments/unit-tests/3/test/module-info.java b/experiments/unit-tests/3/test/module-info.java new file mode 120000 index 0000000..431f004 --- /dev/null +++ b/experiments/unit-tests/3/test/module-info.java @@ -0,0 +1 @@ +../module-info-test.java \ No newline at end of file diff --git a/experiments/unit-tests/3/test/no/ion/jhms/junit/jupiter/driver/Main.java b/experiments/unit-tests/3/test/no/ion/jhms/junit/jupiter/driver/Main.java new file mode 100644 index 0000000..2222ceb --- /dev/null +++ b/experiments/unit-tests/3/test/no/ion/jhms/junit/jupiter/driver/Main.java @@ -0,0 +1,64 @@ +package no.ion.jhms.junit.jupiter.driver; + +import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.platform.engine.discovery.ClassSelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.LauncherConfig; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; +import org.junit.platform.launcher.listeners.TestExecutionSummary; + +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class Main { + public static void main(String... args) { + + var config = LauncherConfig.builder(); + config.addTestEngines(new JupiterTestEngine()); + Launcher factory = LauncherFactory.create(config.build()); + + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder + .request() + //.selectors(DiscoverySelectors.selectClass(one.exported.PublicTest.class)) + .selectors(ofClasses("one.exported.PublicTest", "one.exported.PackagePrivateTest", "one.internal.PublicTest")) + .build(); + + // Jupiter use context class loader if set, or otherwise the app/system class loader. + // Instead, it should use the hybrid module class loader. Hope threads are not crossed... + Thread.currentThread().setContextClassLoader(Main.class.getClassLoader()); + + var listener = new SummaryGeneratingListener(); + factory.registerTestExecutionListeners(listener); + factory.execute(request); + + TestExecutionSummary summary = listener.getSummary(); + summary.printTo(new PrintWriter(System.out)); + summary.getFailures().forEach(failure -> { + System.out.println(failure.getTestIdentifier()); + failure.getException().printStackTrace(); + }); + } + + private static List ofClasses(String... classNames) { + return Arrays.stream(classNames).map(Main::ofClass).collect(Collectors.toList()); + } + + private static ClassSelector ofClass(String className) { + return DiscoverySelectors.selectClass(findClass(className)); + } + + private static Class findClass(String binaryClassName) { + ClassLoader classLoader = Main.class.getClassLoader(); + try { + return classLoader.loadClass(binaryClassName); + } catch (ClassNotFoundException e) { + throw new NoClassDefFoundError(binaryClassName); + } + } +} diff --git a/experiments/unit-tests/3/test/one/exported/PackagePrivateTest.java b/experiments/unit-tests/3/test/one/exported/PackagePrivateTest.java new file mode 100644 index 0000000..20c25dc --- /dev/null +++ b/experiments/unit-tests/3/test/one/exported/PackagePrivateTest.java @@ -0,0 +1,13 @@ +package one.exported; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class PackagePrivateTest { + @Test + void willFail() { + assertEquals(-1, PackagePrivate.PACKAGE_PRIVATE); + } +} diff --git a/experiments/unit-tests/3/test/one/exported/PublicTest.java b/experiments/unit-tests/3/test/one/exported/PublicTest.java new file mode 100644 index 0000000..50fbb8d --- /dev/null +++ b/experiments/unit-tests/3/test/one/exported/PublicTest.java @@ -0,0 +1,12 @@ +package one.exported; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PublicTest { + @Test + void willPass() { + assertEquals(1, Public.PUBLIC); + } +} diff --git a/experiments/unit-tests/3/test/one/internal/PublicTest.java b/experiments/unit-tests/3/test/one/internal/PublicTest.java new file mode 100644 index 0000000..2c952e5 --- /dev/null +++ b/experiments/unit-tests/3/test/one/internal/PublicTest.java @@ -0,0 +1,12 @@ +package one.internal; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PublicTest { + @Test + void willPass() { + assertEquals(11, PackagePrivate.PACKAGE_PRIVATE); + } +} diff --git a/jar/modularize-jar b/jar/modularize-jar index e416b2a..a075e73 100755 --- a/jar/modularize-jar +++ b/jar/modularize-jar @@ -30,7 +30,8 @@ Options: applications bundled into a modular or executable jar. -I, --module-info FILE The module-info.java file [required] - -p, --module-path MPATH The module path when compiling module-info.java. + -p, --module-path MPATH Add to module path when compiling + module-info.java. -V, --module-version VERSION The module version -u, --update Update an existing jar archive [required] @@ -81,6 +82,7 @@ function Main { local module_info="" local -a javac_xopts=() local -a jar_xopts=() + local module_path="" while (( $# > 0 )) do @@ -110,7 +112,12 @@ function Main { shift 2 ;; -p|--module-path) - javac_xopts+=("$1" "$2") + if (( ${#module_path} == 0 )) + then + module_path="$2" + else + module_path+=:"$2" + fi shift 2 ;; -V|--module-version) @@ -133,6 +140,8 @@ function Main { test -n "$jarfile" || Fail "--file is required, see --help" test -n "$module_info" || Fail "--module-info is required, see --help" + (( ${#module_path} == 0 )) || javac_xopts+=(--module-path "$module_path") + if ! TMP_DIRECTORY=$(mktemp -d) then Fail "Failed to create temporary directory with 'mktemp -d'" diff --git a/javahms/javahms b/javahms/javahms index 88e5e2d..1f8d3f5 100755 --- a/javahms/javahms +++ b/javahms/javahms @@ -30,7 +30,7 @@ Options: GRAPH below for more details. --module-path,-p PATH A : separated list of paths, each path is a path to a hybrid modular JAR - file or a directory containing such files. + file, or a directory containing such files. May be repeated. To pass java command-line arguments (JAVA_OPTIONS...) to the java invocation when launching a JHMS application, the administrator would pick a token (TOK) @@ -76,10 +76,15 @@ function Main { local -a module_graph=() local -a module_path=() local -a module=() + local dry_run=false while (( $# > 0 )) do case "$1" in + --dry-run|-n) + dry_run=true + shift + ;; --help|-h) Usage ;; --java-options|-J) shift @@ -114,7 +119,12 @@ function Main { shift 2 || true ;; --module-path|-p) - module_path=("$1" "$2") + if (( ${#module_path[@]} == 0 )) + then + module_path=("$1" "$2") + else + module_path=("$1" "${module_path[1]}:$2") + fi shift 2 || true ;; --module|-m) @@ -150,8 +160,14 @@ function Main { Fail "There is no no.ion.jhms JAR file: '$jar_path'" fi - exec java "${java_options[@]}" -jar "$jar_path" \ - "${module_path[@]}" "${module_graph[@]}" "${module[@]}" "$@" + if $dry_run + then + echo java "${java_options[@]}" -jar "$jar_path" \ + "${module_path[@]}" "${module_graph[@]}" "${module[@]}" "$@" + else + exec java "${java_options[@]}" -jar "$jar_path" \ + "${module_path[@]}" "${module_graph[@]}" "${module[@]}" "$@" + fi } Main "$@" diff --git a/no.ion.jhms/src/main/java/no/ion/jhms/HybridModule.java b/no.ion.jhms/src/main/java/no/ion/jhms/HybridModule.java index 2c3e303..5800072 100644 --- a/no.ion.jhms/src/main/java/no/ion/jhms/HybridModule.java +++ b/no.ion.jhms/src/main/java/no/ion/jhms/HybridModule.java @@ -67,10 +67,10 @@ static class Builder { private final HybridModuleJar jar; private final Set packages = new HashSet<>(); private final Set requiresNames = new HashSet<>(); - private final List platformReads = new ArrayList<>(); - private final List platformReadClosure = new ArrayList<>(); - private final List hybridReads = new ArrayList<>(); - private final List hybridReadClosure = new ArrayList<>(); + private final Map platformReads = new HashMap<>(); + private final Map platformReadClosure = new HashMap<>(); + private final Map hybridReads = new HashMap<>(); + private final Map hybridReadClosure = new HashMap<>(); private final Map> exports = new HashMap<>(); private final HashMap transitiveByRequires = new HashMap<>(); @@ -88,13 +88,30 @@ void addHybridModuleRequires(HybridModule hybridModule, boolean transitive) { throw new ResolutionException("Hybrid module " + jar.hybridModuleId() + " requires " + hybridModule.id().name() + " twice"); } - hybridReads.addAll(hybridModule.hybridReadClosure()); - platformReads.addAll(hybridModule.platformReadClosure()); + hybridModule.hybridReadClosure.forEach(hm -> { + HybridModule previousHybridModule = hybridReads.put(hm.id.name(), hm); + + if (previousHybridModule != null && !previousHybridModule.id.version().equals(hm.id.version())) { + throw new ResolutionException(jar.hybridModuleId() + " requires hybrid module " + hm.id.name() + + " at two different versions: " + previousHybridModule.id.version() + " and " + hm.id.version()); + } + }); + + hybridModule.platformReadClosure.forEach(pm -> platformReads.put(pm.name(), pm)); transitiveByRequires.put(hybridModule.id().name(), transitive); if (transitive) { - hybridReadClosure.addAll(hybridModule.hybridReadClosure()); - platformReadClosure.addAll(hybridModule.platformReadClosure()); + hybridModule.hybridReadClosure().forEach(hm -> { + HybridModule previousHybridModule = hybridReadClosure.put(hm.id.name(), hm); + + if (previousHybridModule != null && !previousHybridModule.id.equals(hm.id)) { + throw new ResolutionException(jar.hybridModuleId() + " requires hybrid module " + hm.id.name() + + " (transitively) at two different versions: " + previousHybridModule.id.version() + + " and " + hm.id.version()); + } + }); + + hybridModule.platformReadClosure().forEach(pm -> platformReadClosure.put(pm.name(), pm)); } } @@ -104,11 +121,11 @@ void addPlatformModuleRequires(PlatformModule platformModule, boolean transitive throw new ResolutionException("Hybrid module " + jar.hybridModuleId() + " requires " + platformModule.name() + " twice"); } - platformReads.add(platformModule); + platformReads.put(platformModule.name(), platformModule); transitiveByRequires.put(platformModule.name(), transitive); if (transitive) { - platformReadClosure.add(platformModule); + platformReadClosure.put(platformModule.name(), platformModule); } } @@ -117,24 +134,27 @@ void addExports(String packageName, Set friends) { } HybridModule build() { + // 'module' adds 'this' to hybridRead (and hybridReadClosure), and we need to loop over hybridRead below + ArrayList hybridReads = new ArrayList<>(this.hybridReads.values()); + HybridModule module = new HybridModule( jar, packages, - platformReads, - platformReadClosure, + new ArrayList<>(platformReads.values()), + new ArrayList<>(platformReadClosure.values()), hybridReads, - hybridReadClosure, + new ArrayList<>(hybridReadClosure.values()), exports, transitiveByRequires); // The hybrid module has a reference to the class loader, and vice versa, which complicates construction. TreeMap platformModuleByPackage = new TreeMap<>(); - for (var platformModule : platformReads) { + for (var platformModule : platformReads.values()) { for (var packageName : platformModule.packagesVisibleTo(module)) { PlatformModule previousOwner = platformModuleByPackage.put(packageName, platformModule); - if (previousOwner != null) { + if (previousOwner != null && !previousOwner.name().equals(platformModule.name())) { throw new InvalidHybridModuleException("Package " + packageName + " visible to hybrid module " + module.id() + " is exported from two different readable modules (" + previousOwner.name() + " and " + platformModule.name() + ")"); @@ -153,7 +173,7 @@ HybridModule build() { } HybridModule previousOwner = hybridModuleByPackage.put(packageName, hybridModule); - if (previousOwner != null) { + if (previousOwner != null && !previousOwner.id().equals(hybridModule.id())) { throw new InvalidHybridModuleException("Package " + packageName + " visible to hybrid module " + module.id() + " is exported from two different readable modules (" + previousOwner.id() + " and " + hybridModule.id() + ")"); diff --git a/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleClassLoader.java b/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleClassLoader.java index f0dd9a1..e080869 100644 --- a/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleClassLoader.java +++ b/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleClassLoader.java @@ -157,12 +157,40 @@ private Class loadClassUnlocked(String name) throws ClassNotFoundException { return c; } + // While testing with the JUnit 5 console launcher it was found that + // java.lang.reflect.AnnotatedElement.isAnnotationPresent() requires jdk.internal.reflect.ConstructorAccessorImpl + // in java.base to be loaded by the invoking class loader. But its package is not exported. + // + // In JPMS all (types in) packages of all modules are visible, and all types can be loaded successfully. + // It is only if such types are accessed, e.g. a constructor being invoked, that JPMS enforces its accessibility + // restrictions. + // + // In JHMS a non-exported package is not even visible, which would cause a ClassNotFoundException to be thrown. + // + // OSGi also noted irregularities w.r.t. class loading of system classes. From its Core 7 specification: + // "Certain Java virtual machines, also Oracle's VMs, appear to make the erroneous assumption that the + // delegation to the parent class loader always occurs." OSGi specifies a org.osgi.framework.bootdelegation + // system property to force delegation to the "system class loader" (AFAIK the parent class loader of the + // OSGi framework). OSGi always delegates for java.* packages. + // + // Note: The canonical way to use reflection on a class C in a hybrid module M is to use M's class loader. + // + // In JHMS delegation is always to parent first (system class loader, not application class loader that + // includes the class path). An alternative is to have a system property of packages to delegate for, + // like org.osgi.framework.bootdelegation. + + try { + return getParent().loadClass(name); + } catch (ClassNotFoundException e) { + // nothing + } + // If the class is in a readable platform module package String packageName = getPackageName(name); - PlatformModule platformModule = platformModulesByPackage.get(packageName); + /*PlatformModule platformModule = platformModulesByPackage.get(packageName); if (platformModule != null) { return getParent().loadClass(name); - } + }*/ // If the class is in a readable hybrid module package HybridModule hybridModule = hybridModulesByPackage.get(packageName); diff --git a/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleContainer.java b/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleContainer.java index 232ef6d..d65fb99 100644 --- a/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleContainer.java +++ b/no.ion.jhms/src/main/java/no/ion/jhms/HybridModuleContainer.java @@ -264,7 +264,12 @@ private HybridModule resolveNewHybridModule(HybridModuleId id) { } else { HybridModuleVersion version = HybridModuleVersion.fromRaw(requires.rawCompiledVersion()); HybridModuleId requiredHybridModuleId = new HybridModuleId(requires.name(), version); - HybridModule requiredHybridModule = resolveHybridModule(requiredHybridModuleId); + final HybridModule requiredHybridModule; + try { + requiredHybridModule = resolveHybridModule(requiredHybridModuleId); + } catch (FindException e) { + throw new FindException(e.getMessage() + ": Required by " + id); + } builder.addHybridModuleRequires(requiredHybridModule, transitive); } } diff --git a/no.ion.jhms/src/test/java/no/ion/jhms/HybridModuleContainerTest.java b/no.ion.jhms/src/test/java/no/ion/jhms/HybridModuleContainerTest.java index fb12ee3..e168cef 100644 --- a/no.ion.jhms/src/test/java/no/ion/jhms/HybridModuleContainerTest.java +++ b/no.ion.jhms/src/test/java/no/ion/jhms/HybridModuleContainerTest.java @@ -176,8 +176,8 @@ public void testGraph3() { HybridModuleContainer.GraphParams graphParams = new HybridModuleContainer.GraphParams(); graphParams.includeSelfReads(); assertEquals("find.hybrid.module.one@1.2.3 reads find.hybrid.module.one@1.2.3 [no.ion.jhms.test.FindHybridModule,no.ion.jhms.test.FindHybridModule.one.exported]\n" + - "find.hybrid.module.two@1.2.3 reads find.hybrid.module.one@1.2.3 [no.ion.jhms.test.FindHybridModule.one.exported]\n" + - "find.hybrid.module.two@1.2.3 reads find.hybrid.module.two@1.2.3 [no.ion.jhms.test.FindHybridModule,no.ion.jhms.test.FindHybridModule.two.exported]\n", + "find.hybrid.module.two@1.2.3 reads find.hybrid.module.one@1.2.3 [no.ion.jhms.test.FindHybridModule.one.exported]\n" + + "find.hybrid.module.two@1.2.3 reads find.hybrid.module.two@1.2.3 [no.ion.jhms.test.FindHybridModule,no.ion.jhms.test.FindHybridModule.two.exported]\n", container.moduleGraph2(graphParams)); } }