diff --git a/common/graalvm-reachability-metadata/build.gradle.kts b/common/graalvm-reachability-metadata/build.gradle.kts index a55de5b36..994d6dca6 100644 --- a/common/graalvm-reachability-metadata/build.gradle.kts +++ b/common/graalvm-reachability-metadata/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(libs.openjson) testImplementation(platform(libs.test.junit.bom)) testImplementation(libs.test.junit.jupiter.core) + testRuntimeOnly(libs.test.junit.platform.launcher) } tasks.withType().configureEach { diff --git a/common/junit-platform-native/build.gradle b/common/junit-platform-native/build.gradle index 3c47d325c..6231a8486 100644 --- a/common/junit-platform-native/build.gradle +++ b/common/junit-platform-native/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation libs.test.junit.platform.launcher implementation libs.test.junit.jupiter.core testImplementation libs.test.junit.vintage + testRuntimeOnly libs.test.junit.platform.launcher } apply from: "gradle/native-image-testing.gradle" diff --git a/common/junit-platform-native/gradle/native-image-testing.gradle b/common/junit-platform-native/gradle/native-image-testing.gradle index 912f88b7c..814daef49 100644 --- a/common/junit-platform-native/gradle/native-image-testing.gradle +++ b/common/junit-platform-native/gradle/native-image-testing.gradle @@ -38,6 +38,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ + def agentOutput = layout.buildDirectory.dir("agent") ext { @@ -90,6 +91,17 @@ abstract class NativeTestArgumentProvider implements CommandLineArgumentProvider @Input abstract Property getDiscovery() + @InputDirectory + abstract DirectoryProperty getExcludeConfigDir() + + private List addExcludeConfig() { + String[] jarPatternPair = getExcludeConfigDir().get().file("exclude-config").getAsFile().readLines().get(0).split(",") + var jar = jarPatternPair[0] + var pattern = jarPatternPair[1] + + return ["--exclude-config", jar, pattern] + } + @Override Iterable asArguments() { def args = [ @@ -99,6 +111,9 @@ abstract class NativeTestArgumentProvider implements CommandLineArgumentProvider "-o", "native-image-tests", "-Djunit.platform.listeners.uid.tracking.output.dir=${testIdsDir.get().asFile.absolutePath}" ] + + args.addAll(addExcludeConfig()) + if (agentOutputDir.isPresent()) { def outputDir = agentOutputDir.get().asFile if (!outputDir.exists()) { @@ -134,6 +149,7 @@ tasks.register("nativeTestCompile", Exec) { def argsProvider = objects.newInstance(NativeTestArgumentProvider) argsProvider.classpath.from(test.classpath) argsProvider.testIdsDir.set(testIdsDir) + argsProvider.excludeConfigDir.set(layout.buildDirectory.dir("resources").get().dir("main").dir("extra-build-args")) argsProvider.agentOutputDir.set(agentOutput) argsProvider.discovery.set(providers.systemProperty("testDiscovery").map(v -> Boolean.valueOf(v)).orElse(false)) argumentProviders.add(argsProvider) @@ -143,4 +159,5 @@ tasks.register("nativeTest", Exec) { dependsOn nativeTestCompile workingDir = "${buildDir}" executable = "${buildDir}/native-image-tests" + args = ["-Djunit.platform.listeners.uid.tracking.output.dir=${testIdsDir.get().asFile.absolutePath}"] } diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/JUnitPlatformFeature.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/JUnitPlatformFeature.java index e9fc8c155..5f22314ff 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/JUnitPlatformFeature.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/JUnitPlatformFeature.java @@ -45,6 +45,8 @@ import org.graalvm.nativeimage.ImageSingletons; import org.graalvm.nativeimage.hosted.Feature; import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; +import org.graalvm.nativeimage.hosted.RuntimeReflection; + import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.discovery.UniqueIdSelector; @@ -57,15 +59,19 @@ import org.junit.platform.launcher.core.LauncherFactory; import org.junit.platform.launcher.listeners.UniqueIdTrackingListener; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -74,10 +80,24 @@ public final class JUnitPlatformFeature implements Feature { public final boolean debug = System.getProperty("debug") != null; - private static final NativeImageConfigurationImpl nativeImageConfigImpl = new NativeImageConfigurationImpl(); private final ServiceLoader extensionConfigProviders = ServiceLoader.load(PluginConfigProvider.class); + public static void debug(String format, Object... args) { + if (debug()) { + System.out.printf("[Debug] " + format + "%n", args); + } + } + + @Override + public void afterRegistration(AfterRegistrationAccess access) { + extensionConfigProviders.forEach(p -> p.initialize(access.getApplicationClassLoader(), nativeImageConfigImpl)); + } + + private static boolean debug() { + return ImageSingletons.lookup(JUnitPlatformFeature.class).debug; + } + @Override public void duringSetup(DuringSetupAccess access) { forEachProvider(p -> p.onLoad(nativeImageConfigImpl)); @@ -85,55 +105,60 @@ public void duringSetup(DuringSetupAccess access) { @Override public void beforeAnalysis(BeforeAnalysisAccess access) { - RuntimeClassInitialization.initializeAtBuildTime(NativeImageJUnitLauncher.class); + /* Before GraalVM version 22 we couldn't have classes initialized at run-time + * that are also used at build-time but not added to the image heap */ + if (Runtime.version().feature() <= 21) { + initializeClassesForJDK21OrEarlier(); + } - List classpathRoots = access.getApplicationClassPath(); - List selectors = getSelectors(classpathRoots); + List selectors = getSelectors(); + registerTestClassesForReflection(selectors); - Launcher launcher = LauncherFactory.create(); - TestPlan testplan = discoverTestsAndRegisterTestClassesForReflection(launcher, selectors); - ImageSingletons.add(NativeImageJUnitLauncher.class, new NativeImageJUnitLauncher(launcher, testplan)); + /* support for JUnit Vintage */ + registerClassesForHamcrestSupport(access); } - private List getSelectors(List classpathRoots) { + private List getSelectors() { try { - Path outputDir = Paths.get(System.getProperty(UniqueIdTrackingListener.OUTPUT_DIR_PROPERTY_NAME)); - String prefix = System.getProperty(UniqueIdTrackingListener.OUTPUT_FILE_PREFIX_PROPERTY_NAME, + String uniqueIdDirectoryProperty = System.getProperty(UniqueIdTrackingListener.OUTPUT_DIR_PROPERTY_NAME); + if (uniqueIdDirectoryProperty == null) { + throw new IllegalStateException("Cannot determine test-ids directory because junit.platform.listeners.uid.tracking.output.dir property is null"); + } + + String uniqueIdFilePrefix = System.getProperty(UniqueIdTrackingListener.OUTPUT_FILE_PREFIX_PROPERTY_NAME, UniqueIdTrackingListener.DEFAULT_OUTPUT_FILE_PREFIX); - List selectors = readAllFiles(outputDir, prefix) + if (uniqueIdFilePrefix == null) { + throw new IllegalStateException("Cannot determine unique test-ids prefix because junit.platform.listeners.uid.tracking.output.file.prefix property is null"); + } + + Path uniqueIdDirectory = Path.of(uniqueIdDirectoryProperty); + List selectors = readAllFiles(uniqueIdDirectory, uniqueIdFilePrefix) .map(DiscoverySelectors::selectUniqueId) .collect(Collectors.toList()); if (!selectors.isEmpty()) { System.out.printf( "[junit-platform-native] Running in 'test listener' mode using files matching pattern [%s*] " + "found in folder [%s] and its subfolders.%n", - prefix, outputDir.toAbsolutePath()); + uniqueIdFilePrefix, uniqueIdDirectory.toAbsolutePath()); return selectors; } } catch (Exception ex) { debug("Failed to read UIDs from UniqueIdTrackingListener output files: " + ex.getMessage()); } - System.out.println("[junit-platform-native] Running in 'test discovery' mode. Note that this is a fallback mode."); - if (debug) { - classpathRoots.forEach(entry -> debug("Selecting classpath root: " + entry)); - } - return DiscoverySelectors.selectClasspathRoots(new HashSet<>(classpathRoots)); + throw new RuntimeException("Cannot compute test selectors from test ids."); } /** - * Use the JUnit Platform Launcher to discover tests and register classes - * for reflection. + * Use the JUnit Platform Launcher to register classes for reflection. */ - private TestPlan discoverTestsAndRegisterTestClassesForReflection(Launcher launcher, - List selectors) { - + private void registerTestClassesForReflection(List selectors) { LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() .selectors(selectors) .build(); + Launcher launcher = LauncherFactory.create(); TestPlan testPlan = launcher.discover(request); - testPlan.getRoots().stream() .flatMap(rootIdentifier -> testPlan.getDescendants(rootIdentifier).stream()) .map(TestIdentifier::getSource) @@ -143,14 +168,44 @@ private TestPlan discoverTestsAndRegisterTestClassesForReflection(Launcher launc .map(ClassSource.class::cast) .map(ClassSource::getJavaClass) .forEach(this::registerTestClassForReflection); + } + + private final Set> registeredClasses = new HashSet<>(); + + private boolean shouldRegisterClass(Class clazz) { + /* avoid registering java internal classes */ + if (ModuleLayer.boot().modules().contains(clazz.getModule())) { + return false; + } + + /* avoid loops (possible case: class B is inner class of A, and B extends A) */ + if (registeredClasses.contains(clazz)) { + return false; + } + registeredClasses.add(clazz); - return testPlan; + return true; } private void registerTestClassForReflection(Class clazz) { + if (!shouldRegisterClass(clazz)) { + return; + } + debug("Registering test class for reflection: %s", clazz.getName()); nativeImageConfigImpl.registerAllClassMembersForReflection(clazz); forEachProvider(p -> p.onTestClassRegistered(clazz, nativeImageConfigImpl)); + + Class[] declaredClasses = clazz.getDeclaredClasses(); + for (Class declaredClass : declaredClasses) { + registerTestClassForReflection(declaredClass); + } + + Class[] interfaces = clazz.getInterfaces(); + for (Class inter : interfaces) { + registerTestClassForReflection(inter); + } + Class superClass = clazz.getSuperclass(); if (superClass != null && superClass != Object.class) { registerTestClassForReflection(superClass); @@ -163,16 +218,6 @@ private void forEachProvider(Consumer consumer) { } } - public static void debug(String format, Object... args) { - if (debug()) { - System.out.printf("[Debug] " + format + "%n", args); - } - } - - public static boolean debug() { - return ImageSingletons.lookup(JUnitPlatformFeature.class).debug; - } - private Stream readAllFiles(Path dir, String prefix) throws IOException { return findFiles(dir, prefix).map(outputFile -> { try { @@ -192,4 +237,38 @@ private static Stream findFiles(Path dir, String prefix) throws IOExceptio && path.getFileName().toString().startsWith(prefix))); } + private static void registerClassesForHamcrestSupport(BeforeAnalysisAccess access) { + ClassLoader applicationLoader = access.getApplicationClassLoader(); + Class typeSafeMatcher = findClassOrNull(applicationLoader, "org.hamcrest.TypeSafeMatcher"); + Class typeSafeDiagnosingMatcher = findClassOrNull(applicationLoader, "org.hamcrest.TypeSafeDiagnosingMatcher"); + if (typeSafeMatcher != null || typeSafeDiagnosingMatcher != null) { + BiConsumer> registerMatcherForReflection = (a, c) -> RuntimeReflection.register(c.getDeclaredMethods()); + if (typeSafeMatcher != null) { + access.registerSubtypeReachabilityHandler(registerMatcherForReflection, typeSafeMatcher); + } + if (typeSafeDiagnosingMatcher != null) { + access.registerSubtypeReachabilityHandler(registerMatcherForReflection, typeSafeDiagnosingMatcher); + } + } + } + + private static Class findClassOrNull(ClassLoader loader, String className) { + try { + return loader.loadClass(className); + } catch (ClassNotFoundException e) { + return null; + } + } + + private static void initializeClassesForJDK21OrEarlier() { + try (InputStream is = JUnitPlatformFeature.class.getResourceAsStream("/initialize-at-buildtime")) { + if (is != null) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + br.lines().forEach(RuntimeClassInitialization::initializeAtBuildTime); + } + } + } catch (IOException e) { + throw new RuntimeException("Failed to process build time initializations for JDK 21 or earlier"); + } + } } diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageConfigurationImpl.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageConfigurationImpl.java index acda95462..6ffb424e0 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageConfigurationImpl.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageConfigurationImpl.java @@ -42,7 +42,6 @@ package org.graalvm.junit.platform; import org.graalvm.junit.platform.config.core.NativeImageConfiguration; -import org.graalvm.nativeimage.hosted.RuntimeClassInitialization; import org.graalvm.nativeimage.hosted.RuntimeReflection; import java.lang.reflect.Executable; @@ -64,10 +63,4 @@ public void registerForReflection(Executable... methods) { public void registerForReflection(Field... fields) { RuntimeReflection.register(fields); } - - - @Override - public void initializeAtBuildTime(Class... classes) { - RuntimeClassInitialization.initializeAtBuildTime(classes); - } } diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageJUnitLauncher.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageJUnitLauncher.java index c386fa1c5..5829d6f42 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageJUnitLauncher.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/NativeImageJUnitLauncher.java @@ -42,38 +42,35 @@ package org.graalvm.junit.platform; import org.graalvm.nativeimage.ImageInfo; -import org.graalvm.nativeimage.ImageSingletons; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.discovery.UniqueIdSelector; import org.junit.platform.launcher.Launcher; -import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.TestPlan; +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 org.junit.platform.launcher.listeners.UniqueIdTrackingListener; import org.junit.platform.reporting.legacy.xml.LegacyXmlReportGeneratingListener; +import java.io.File; +import java.io.IOException; import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class NativeImageJUnitLauncher { static final String DEFAULT_OUTPUT_FOLDER = Paths.get("test-results-native").resolve("test").toString(); - final Launcher launcher; - final TestPlan testPlan; - - public NativeImageJUnitLauncher(Launcher launcher, TestPlan testPlan) { - this.launcher = launcher; - this.testPlan = testPlan; - } - - public void registerTestExecutionListeners(TestExecutionListener testExecutionListener) { - launcher.registerTestExecutionListeners(testExecutionListener); - } - - public void execute() { - launcher.execute(testPlan); - } - static String stringPad(String input) { return String.format("%1$-20s", input); } @@ -84,6 +81,7 @@ public static void main(String... args) { System.exit(1); } + /* scan runtime arguments */ String xmlOutput = DEFAULT_OUTPUT_FOLDER; boolean silent = false; @@ -113,9 +111,14 @@ public static void main(String... args) { } } - PrintWriter out = new PrintWriter(System.out); - NativeImageJUnitLauncher launcher = ImageSingletons.lookup(NativeImageJUnitLauncher.class); + if (xmlOutput == null) { + throw new RuntimeException("xml-output-dir argument passed incorrectly to the launcher class."); + } + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = getTestPlan(launcher); + + PrintWriter out = new PrintWriter(System.out); if (!silent) { out.println("JUnit Platform on Native Image - report"); out.println("----------------------------------------\n"); @@ -126,7 +129,7 @@ public static void main(String... args) { SummaryGeneratingListener summaryListener = new SummaryGeneratingListener(); launcher.registerTestExecutionListeners(summaryListener); launcher.registerTestExecutionListeners(new LegacyXmlReportGeneratingListener(Paths.get(xmlOutput), out)); - launcher.execute(); + launcher.execute(testPlan); TestExecutionSummary summary = summaryListener.getSummary(); if (!silent) { @@ -137,4 +140,135 @@ public static void main(String... args) { long failedCount = summary.getTotalFailureCount(); System.exit(failedCount > 0 ? 1 : 0); } + + private static TestPlan getTestPlan(Launcher launcher) { + List selectors = getSelectors(); + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors(selectors) + .build(); + + return launcher.discover(request); + } + + private static List getSelectors() { + try { + String systemPropertyBasedLocation = System.getProperty(UniqueIdTrackingListener.OUTPUT_DIR_PROPERTY_NAME); + Path uniqueIdDirectory = systemPropertyBasedLocation != null ? Path.of(systemPropertyBasedLocation) : getTestIDsFromDefaultLocations(); + + String uniqueIdFilePrefix = System.getProperty(UniqueIdTrackingListener.OUTPUT_FILE_PREFIX_PROPERTY_NAME, + UniqueIdTrackingListener.DEFAULT_OUTPUT_FILE_PREFIX); + if (uniqueIdFilePrefix == null) { + throw new RuntimeException("Test-ids unique id file prefix not provided to the NativeImageJUnitLauncher."); + } + + List selectors = readAllFiles(uniqueIdDirectory, uniqueIdFilePrefix) + .map(DiscoverySelectors::selectUniqueId) + .collect(Collectors.toList()); + if (!selectors.isEmpty()) { + System.out.printf( + "[junit-platform-native] Running in 'test listener' mode using files matching pattern [%s*] " + + "found in folder [%s] and its subfolders.%n", + uniqueIdFilePrefix, uniqueIdDirectory.toAbsolutePath()); + return selectors; + } + } catch (Exception ex) { + throw new RuntimeException("Failed to read UIDs from UniqueIdTrackingListener output files: " + ex.getMessage()); + } + + throw new RuntimeException("Cannot compute test selectors from test ids."); + } + + private static Stream readAllFiles(Path dir, String prefix) throws IOException { + return findFiles(dir, prefix).flatMap(outputFile -> { + try { + return Files.readAllLines(outputFile).stream(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + } + + private static Stream findFiles(Path dir, String prefix) throws IOException { + if (!Files.exists(dir)) { + return Stream.empty(); + } + return Files.find(dir, Integer.MAX_VALUE, + (path, basicFileAttributes) -> (basicFileAttributes.isRegularFile() + && path.getFileName().toString().startsWith(prefix))); + } + + private static Path getTestIDsFromDefaultLocations() { + System.out.println("[junit-platform-native] WARNING: -djunit.platform.listeners.uid.tracking.output.dir not specified, " + + "trying to find test-ids on default Gradle/Maven locations. " + + "As this is a fallback mode, it could take a while. " + + "This should only happen if you are running tests executable manually and you didn't pass uid output directory with -djunit.platform.listeners.uid.tracking.output.dir=."); + Path defaultGradleTestIDsLocation = getGradleTestIdsDefaultLocation(); + Path defaultMavenTestIDsLocation = getMavenTestIDsDefaultLocation(); + + if (testIdsDirectoryExists(defaultGradleTestIDsLocation) && testIdsDirectoryExists(defaultMavenTestIDsLocation)) { + throw new RuntimeException("[junit-platform-native] test-ids found in both " + defaultGradleTestIDsLocation + " and " + defaultMavenTestIDsLocation + + ". Please specify the test-ids location by passing the '--test-ids ' argument to your tests executable."); + } + + if (testIdsDirectoryExists(defaultGradleTestIDsLocation)) { + System.out.println("[junit-platform-native] WARNING: Using test-ids from default Gradle project location:" + defaultGradleTestIDsLocation); + return defaultGradleTestIDsLocation; + } + + if (testIdsDirectoryExists(defaultMavenTestIDsLocation)) { + System.out.println("[junit-platform-native] WARNING: Using test-ids from default Maven project location:" + defaultMavenTestIDsLocation); + return defaultMavenTestIDsLocation; + } + + throw new RuntimeException("[junit-platform-native] test-ids not provided to the NativeImageJUnitLauncher and cannot be found on default locations."); + } + + private static Path getGradleTestIdsDefaultLocation() { + File gradleBuildDirectory = new File(getBuildDirectory(File.separator + "build" + File.separator)); + return searchForDirectory(gradleBuildDirectory, "testlist"); + } + + private static Path getMavenTestIDsDefaultLocation() { + File mavenTargetDirectory = new File(getBuildDirectory(File.separator + "target" + File.separator)); + return searchForDirectory(mavenTargetDirectory, "test-ids"); + } + + private static String getBuildDirectory(String buildDir) { + String executableLocation = Path.of(".").toAbsolutePath().toString(); + int index = executableLocation.indexOf(buildDir); + if (index < 0) { + return buildDir.substring(1); + } + + return executableLocation.substring(0, index + buildDir.length()); + } + + private static Path searchForDirectory(File root, String target) { + if (root == null || !root.isDirectory()) { + return null; + } + + if (root.getName().equals(target)) { + return Path.of(root.getAbsolutePath()); + } + + File[] content = root.listFiles(); + if (content == null) { + return null; + } + + for (File file : content) { + Path result = searchForDirectory(file, target); + if (result != null) { + return result; + } + } + + return null; + } + + private static boolean testIdsDirectoryExists(Path directory) { + return directory != null && Files.exists(directory); + } + } diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/NativeImageConfiguration.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/NativeImageConfiguration.java index 42e1f736c..96b9ec089 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/NativeImageConfiguration.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/NativeImageConfiguration.java @@ -57,7 +57,7 @@ public interface NativeImageConfiguration { default void registerAllClassMembersForReflection(String... classNames) { registerAllClassMembersForReflection(Utils.toClasses(classNames)); - }; + } default void registerAllClassMembersForReflection(Class... classes) { for (Class clazz : classes) { @@ -68,10 +68,4 @@ default void registerAllClassMembersForReflection(Class... classes) { registerForReflection(clazz.getDeclaredFields()); } } - - default void initializeAtBuildTime(String... classNames) { - initializeAtBuildTime(Utils.toClasses(classNames)); - }; - - void initializeAtBuildTime(Class... classes); } diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/PluginConfigProvider.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/PluginConfigProvider.java index d55c59e81..fe183e49a 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/PluginConfigProvider.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/core/PluginConfigProvider.java @@ -41,14 +41,38 @@ package org.graalvm.junit.platform.config.core; -public interface PluginConfigProvider { +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; - void onLoad(NativeImageConfiguration config); +public abstract class PluginConfigProvider { - void onTestClassRegistered(Class testClass, NativeImageConfiguration registry); + protected ClassLoader applicationClassLoader; + protected NativeImageConfiguration nativeImageConfigImpl; - default int getMajorJDKVersion() { - return Runtime.version().feature(); + + public abstract void onLoad(NativeImageConfiguration config); + + public abstract void onTestClassRegistered(Class testClass, NativeImageConfiguration registry); + + public final void initialize(ClassLoader classLoader, NativeImageConfiguration nic) { + applicationClassLoader = classLoader; + nativeImageConfigImpl = nic; } + @SuppressWarnings("unchecked") + protected final T getAnnotationElementValue(Class annotatedClass, String annotationName, String annotationElementName) { + try { + Class annotation = (Class) applicationClassLoader.loadClass(annotationName); + Method classProvider = annotation.getDeclaredMethod(annotationElementName); + + Annotation classAnnotation = annotatedClass.getAnnotation(annotation); + if (classAnnotation != null) { + return (T) classProvider.invoke(classAnnotation); + } + } catch (ReflectiveOperationException e) { + // intentionally ignored + } + + return null; + } } diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/jupiter/JupiterConfigProvider.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/jupiter/JupiterConfigProvider.java index f8b3f7959..f666eba56 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/jupiter/JupiterConfigProvider.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/jupiter/JupiterConfigProvider.java @@ -55,59 +55,19 @@ import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.jupiter.params.provider.FieldSource; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; -import java.util.Optional; import static org.graalvm.junit.platform.JUnitPlatformFeature.debug; -public class JupiterConfigProvider implements PluginConfigProvider { +public class JupiterConfigProvider extends PluginConfigProvider { @Override public void onLoad(NativeImageConfiguration config) { - config.initializeAtBuildTime( - "org.junit.jupiter.api.condition.OS", - "org.junit.jupiter.engine.config.EnumConfigurationParameterConverter", - "org.junit.jupiter.engine.descriptor.ClassTestDescriptor", - "org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor", - "org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor", - "org.junit.jupiter.engine.descriptor.JupiterTestDescriptor", - "org.junit.jupiter.engine.descriptor.JupiterTestDescriptor$1", - "org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor", - "org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor", - "org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor", - "org.junit.jupiter.engine.execution.ConditionEvaluator", - "org.junit.jupiter.engine.execution.ExecutableInvoker", - "org.junit.jupiter.params.provider.EnumSource$Mode", - // new in Junit 5.10 - "org.junit.platform.launcher.core.LauncherConfig", - "org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter" - ); - - if (getMajorJDKVersion() >= 21) { - /* new with simulated class initialization */ - config.initializeAtBuildTime( - "org.junit.jupiter.api.DisplayNameGenerator$Standard", - "org.junit.jupiter.api.extension.ConditionEvaluationResult", - "org.junit.jupiter.api.TestInstance$Lifecycle", - "org.junit.jupiter.engine.config.CachingJupiterConfiguration", - "org.junit.jupiter.engine.config.DefaultJupiterConfiguration", - "org.junit.jupiter.engine.descriptor.DynamicDescendantFilter", - "org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor", - "org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor", - "org.junit.jupiter.engine.execution.InterceptingExecutableInvoker", - "org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall", - "org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall$VoidMethodInterceptorCall", - "org.junit.jupiter.engine.execution.InvocationInterceptorChain", - "org.junit.jupiter.engine.JupiterTestEngine", - "org.junit.jupiter.params.provider.EnumSource$Mode$Validator" - ); - } - - + /* Provide support for Timeout annotation */ config.registerAllClassMembersForReflection( "org.junit.jupiter.engine.extension.TimeoutExtension$ExecutorResource", "org.junit.jupiter.engine.extension.TimeoutInvocationFactory$SingleThreadExecutorResource" @@ -116,15 +76,17 @@ public void onLoad(NativeImageConfiguration config) { @Override public void onTestClassRegistered(Class testClass, NativeImageConfiguration registry) { + /* Provide support for various annotations */ + AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, TestMethodOrder.class, TestMethodOrder::value); AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, ArgumentsSource.class, ArgumentsSource::value); AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, ExtendWith.class, ExtendWith::value); - AnnotationUtils.forEachAnnotatedMethod(testClass, EnumSource.class, (m, annotation) -> handleEnumSource(m, annotation, registry)); - handleTestOrderer(testClass, registry); AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, DisplayNameGeneration.class, DisplayNameGeneration::value); AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, IndicativeSentencesGeneration.class, IndicativeSentencesGeneration::generator); AnnotationUtils.forEachAnnotatedMethodParameter(testClass, ConvertWith.class, annotation -> registry.registerAllClassMembersForReflection(annotation.value())); AnnotationUtils.forEachAnnotatedMethodParameter(testClass, AggregateWith.class, annotation -> registry.registerAllClassMembersForReflection(annotation.value())); + AnnotationUtils.forEachAnnotatedMethod(testClass, EnumSource.class, (m, annotation) -> handleEnumSource(m, annotation, registry)); AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, MethodSource.class, JupiterConfigProvider::handleMethodSource); + AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, FieldSource.class, JupiterConfigProvider::handleFieldSource); AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, EnabledIf.class, JupiterConfigProvider::handleEnabledIf); AnnotationUtils.registerClassesFromAnnotationForReflection(testClass, registry, DisabledIf.class, JupiterConfigProvider::handleDisabledIf); } @@ -133,6 +95,10 @@ private static Class[] handleMethodSource(MethodSource annotation) { return handleMethodReference(annotation.value()); } + private static Class[] handleFieldSource(FieldSource annotation) { + return handleMethodReference(annotation.value()); + } + private static Class[] handleEnabledIf(EnabledIf annotation) { return handleMethodReference(annotation.value()); } @@ -181,14 +147,4 @@ public static void handleEnumSource(Method method, EnumSource source, NativeImag debug("Method doesn't have at least 1 parameter - skipping enum registration. Method: %s", method); } } - - private static void handleTestOrderer(Class testClass, NativeImageConfiguration registry) { - Optional annotation = AnnotationSupport.findAnnotation(testClass, TestMethodOrder.class); - if (annotation.isPresent()) { - TestMethodOrder testMethodOrder = annotation.get(); - Class clazz = testMethodOrder.value(); - registry.initializeAtBuildTime(clazz); - } - } - } diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/platform/PlatformConfigProvider.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/platform/PlatformConfigProvider.java index 4b94023b1..9125c5f48 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/platform/PlatformConfigProvider.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/platform/PlatformConfigProvider.java @@ -43,66 +43,14 @@ import org.graalvm.junit.platform.config.core.NativeImageConfiguration; import org.graalvm.junit.platform.config.core.PluginConfigProvider; +import org.graalvm.nativeimage.hosted.RuntimeSerialization; +import org.junit.platform.launcher.TestIdentifier; -public class PlatformConfigProvider implements PluginConfigProvider { +public class PlatformConfigProvider extends PluginConfigProvider { @Override public void onLoad(NativeImageConfiguration config) { - config.initializeAtBuildTime( - "org.junit.platform.launcher.TestIdentifier", - "org.junit.platform.launcher.core.InternalTestPlan", - "org.junit.platform.commons.util.StringUtils", - "org.junit.platform.launcher.core.TestExecutionListenerRegistry", - "org.junit.platform.commons.logging.LoggerFactory$DelegatingLogger", - "org.junit.platform.launcher.core.EngineDiscoveryOrchestrator", - "org.junit.platform.launcher.core.LauncherConfigurationParameters", - "org.junit.platform.commons.logging.LoggerFactory", - "org.junit.platform.engine.UniqueIdFormat", - "org.junit.platform.commons.util.ReflectionUtils", - // https://github.com/graalvm/native-build-tools/issues/300 - "org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener", - // https://github.com/graalvm/native-build-tools/issues/602 - "org.junit.platform.commons.util.LruCache" - ); - - if (getMajorJDKVersion() >= 21) { - /* new with --strict-image-heap */ - config.initializeAtBuildTime( - "org.junit.platform.engine.support.descriptor.ClassSource", - "org.junit.platform.engine.support.descriptor.MethodSource", - "org.junit.platform.engine.support.hierarchical.Node$ExecutionMode", - "org.junit.platform.engine.TestDescriptor$Type", - "org.junit.platform.engine.UniqueId", - "org.junit.platform.engine.UniqueId$Segment", - "org.junit.platform.launcher.core.DefaultLauncher", - "org.junit.platform.launcher.core.DefaultLauncherConfig", - "org.junit.platform.launcher.core.EngineExecutionOrchestrator", - "org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$1", - "org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$2", - "org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$3", - "org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$4", - "org.junit.platform.launcher.core.LauncherDiscoveryResult", - "org.junit.platform.launcher.core.LauncherListenerRegistry", - "org.junit.platform.launcher.core.ListenerRegistry", - "org.junit.platform.launcher.core.SessionPerRequestLauncher", - "org.junit.platform.launcher.LauncherSessionListener$1", - "org.junit.platform.launcher.listeners.UniqueIdTrackingListener", - "org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api.DocumentWriter$1", - "org.junit.platform.suite.engine.SuiteEngineDescriptor", - "org.junit.platform.suite.engine.SuiteLauncher", - "org.junit.platform.suite.engine.SuiteTestDescriptor", - "org.junit.platform.suite.engine.SuiteTestEngine" - ); - } - - try { - /* Verify if the core JUnit Platform test class is available on the classpath */ - Class.forName("org.junit.platform.commons.annotation.Testable"); - } catch (ClassNotFoundException e) { - throw new RuntimeException("Missing some JUnit Platform classes for runtime reflection configuration. \n" + - "Check if JUnit Platform is on your classpath or if that version is supported. \n" + - "Original error: " + e); - } + RuntimeSerialization.register(TestIdentifier.class.getDeclaredClasses()); } @Override diff --git a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/vintage/VintageConfigProvider.java b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/vintage/VintageConfigProvider.java index af653cbbe..f4399457f 100644 --- a/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/vintage/VintageConfigProvider.java +++ b/common/junit-platform-native/src/main/java/org/graalvm/junit/platform/config/vintage/VintageConfigProvider.java @@ -43,37 +43,31 @@ import org.graalvm.junit.platform.config.core.NativeImageConfiguration; import org.graalvm.junit.platform.config.core.PluginConfigProvider; +import org.graalvm.nativeimage.hosted.RuntimeReflection; +import org.graalvm.nativeimage.hosted.RuntimeSerialization; -public class VintageConfigProvider implements PluginConfigProvider { +public class VintageConfigProvider extends PluginConfigProvider { @Override public void onLoad(NativeImageConfiguration config) { - config.initializeAtBuildTime( - "org.junit.vintage.engine.descriptor.RunnerTestDescriptor", - "org.junit.vintage.engine.support.UniqueIdReader", - "org.junit.vintage.engine.support.UniqueIdStringifier", - "org.junit.runner.Description", - "org.junit.runners.BlockJUnit4ClassRunner", - "org.junit.runners.JUnit4", - /* Workaround until we can register serializable classes from a native-image feature */ - "org.junit.runner.Result" - ); - - if (getMajorJDKVersion() >= 21) { - /* new with simulated class initialization */ - config.initializeAtBuildTime( - "java.lang.annotation.Annotation", - "org.junit.runners.model.FrameworkMethod", - "org.junit.runners.model.TestClass", - "org.junit.runners.ParentRunner$1", - "org.junit.Test", - "org.junit.vintage.engine.descriptor.VintageEngineDescriptor", - "org.junit.vintage.engine.VintageTestEngine" - ); + try { + RuntimeSerialization.register(Class.forName("org.junit.runner.Result").getDeclaredClasses()); + RuntimeReflection.register(Class.forName("org.junit.runner.Description").getDeclaredFields()); + } catch (ClassNotFoundException e) { + System.out.println("Cannot register declared classes of org.junit.runner.Result for serialization or fields of org.junit.runner.Description for reflection. Vintage JUnit not available."); } } @Override public void onTestClassRegistered(Class testClass, NativeImageConfiguration registry) { + registerAnnotationClassesForReflection(testClass, "org.junit.runner.RunWith", "value"); + registerAnnotationClassesForReflection(testClass, "org.junit.runners.Parameterized.UseParametersRunnerFactory", "value"); + } + + private void registerAnnotationClassesForReflection(Class testClass, String annotationName, String annotationElementName) { + Class annotationArgument = getAnnotationElementValue(testClass, annotationName, annotationElementName); + if (annotationArgument != null) { + nativeImageConfigImpl.registerAllClassMembersForReflection(annotationArgument); + } } } diff --git a/common/junit-platform-native/src/main/resources/extra-build-args/exclude-config b/common/junit-platform-native/src/main/resources/extra-build-args/exclude-config new file mode 100644 index 000000000..ceae5d2e3 --- /dev/null +++ b/common/junit-platform-native/src/main/resources/extra-build-args/exclude-config @@ -0,0 +1 @@ +.*.jar,META-INF\/native-image\/org.junit.*.properties.* diff --git a/common/junit-platform-native/src/main/resources/initialize-at-buildtime b/common/junit-platform-native/src/main/resources/initialize-at-buildtime new file mode 100644 index 000000000..052ceb5f8 --- /dev/null +++ b/common/junit-platform-native/src/main/resources/initialize-at-buildtime @@ -0,0 +1,118 @@ +org.junit.internal.runners.rules.RuleMemberValidator +org.junit.jupiter.api.condition.OS +org.junit.jupiter.api.DisplayNameGenerator$IndicativeSentences +org.junit.jupiter.api.DisplayNameGenerator$Standard +org.junit.jupiter.api.extension.ConditionEvaluationResult +org.junit.jupiter.api.MethodOrderer$DisplayName +org.junit.jupiter.api.MethodOrderer$MethodName +org.junit.jupiter.api.MethodOrderer$Random +org.junit.jupiter.api.RandomOrdererUtils +org.junit.jupiter.api.TestInstance$Lifecycle +org.junit.jupiter.engine.config.CachingJupiterConfiguration +org.junit.jupiter.engine.config.DefaultJupiterConfiguration +org.junit.jupiter.engine.config.EnumConfigurationParameterConverter +org.junit.jupiter.engine.config.InstantiatingConfigurationParameterConverter +org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor +org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$ClassInfo +org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$LifecycleMethods +org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor +org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor +org.junit.jupiter.engine.descriptor.ClassTestDescriptor +org.junit.jupiter.engine.descriptor.DisplayNameUtils +org.junit.jupiter.engine.descriptor.DynamicDescendantFilter +org.junit.jupiter.engine.descriptor.DynamicDescendantFilter$Mode +org.junit.jupiter.engine.descriptor.ExclusiveResourceCollector$1 +org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor +org.junit.jupiter.engine.descriptor.JupiterTestDescriptor +org.junit.jupiter.engine.descriptor.JupiterTestDescriptor$1 +org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor +org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor$MethodInfo +org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor +org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor +org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor +org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor +org.junit.jupiter.engine.discovery.AbstractOrderingVisitor +org.junit.jupiter.engine.discovery.AbstractOrderingVisitor$DescriptorWrapperOrderer +org.junit.jupiter.engine.discovery.ClassOrderingVisitor +org.junit.jupiter.engine.discovery.ClassSelectorResolver +org.junit.jupiter.engine.discovery.ClassSelectorResolver$DummyClassTemplateInvocationContext +org.junit.jupiter.engine.discovery.DiscoverySelectorResolver +org.junit.jupiter.engine.discovery.MethodFinder +org.junit.jupiter.engine.discovery.MethodSelectorResolver +org.junit.jupiter.engine.discovery.MethodSelectorResolver$MethodType +org.junit.jupiter.engine.discovery.MethodSelectorResolver$MethodType$1 +org.junit.jupiter.engine.discovery.MethodSelectorResolver$MethodType$2 +org.junit.jupiter.engine.discovery.MethodSelectorResolver$MethodType$3 +org.junit.jupiter.engine.discovery.predicates.IsTestClassWithTests +org.junit.jupiter.engine.discovery.predicates.IsTestFactoryMethod +org.junit.jupiter.engine.execution.ConditionEvaluator +org.junit.jupiter.engine.execution.InterceptingExecutableInvoker +org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall +org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall$VoidMethodInterceptorCall +org.junit.jupiter.engine.execution.InvocationInterceptorChain +org.junit.jupiter.engine.JupiterTestEngine +org.junit.jupiter.params.provider.EnumSource$Mode +org.junit.jupiter.params.provider.EnumSource$Mode$Validator +org.junit.platform.commons.logging.LoggerFactory +org.junit.platform.commons.logging.LoggerFactory$DelegatingLogger +org.junit.platform.commons.support.scanning.DefaultClasspathScanner +org.junit.platform.commons.util.ClasspathScanner +org.junit.platform.commons.util.LruCache +org.junit.platform.commons.util.ReflectionUtils +org.junit.platform.commons.util.StringUtils +org.junit.platform.engine.SelectorResolutionResult +org.junit.platform.engine.support.descriptor.ClassSource +org.junit.platform.engine.support.descriptor.MethodSource +org.junit.platform.engine.support.discovery.SelectorResolver$Resolution +org.junit.platform.engine.support.hierarchical.Node$ExecutionMode +org.junit.platform.engine.support.store.NamespacedHierarchicalStore +org.junit.platform.engine.support.store.NamespacedHierarchicalStore$EvaluatedValue +org.junit.platform.engine.TestDescriptor$Type +org.junit.platform.engine.UniqueId +org.junit.platform.engine.UniqueId$Segment +org.junit.platform.engine.UniqueIdFormat +org.junit.platform.launcher.core.DefaultLauncher +org.junit.platform.launcher.core.DefaultLauncherConfig +org.junit.platform.launcher.core.DiscoveryIssueCollector +org.junit.platform.launcher.core.DiscoveryIssueNotifier +org.junit.platform.launcher.core.EngineDiscoveryOrchestrator +org.junit.platform.launcher.core.EngineExecutionOrchestrator +org.junit.platform.launcher.core.EngineFilterer +org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider +org.junit.platform.launcher.core.InternalTestPlan +org.junit.platform.launcher.core.LauncherConfig +org.junit.platform.launcher.core.LauncherConfigurationParameters +org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$1 +org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$2 +org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$3 +org.junit.platform.launcher.core.LauncherConfigurationParameters$ParameterProvider$4 +org.junit.platform.launcher.core.LauncherDiscoveryResult +org.junit.platform.launcher.core.LauncherDiscoveryResult$EngineResultInfo +org.junit.platform.launcher.core.LauncherListenerRegistry +org.junit.platform.launcher.core.ListenerRegistry +org.junit.platform.launcher.core.SessionPerRequestLauncher +org.junit.platform.launcher.EngineDiscoveryResult +org.junit.platform.launcher.LauncherSessionListener$1 +org.junit.platform.launcher.listeners.discovery.LauncherDiscoveryListeners$LauncherDiscoveryListenerType +org.junit.platform.launcher.listeners.UniqueIdTrackingListener +org.junit.platform.launcher.TestIdentifier +org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener +org.junit.platform.reporting.shadow.org.opentest4j.reporting.events.api.DocumentWriter$1 +org.junit.platform.suite.engine.SuiteEngineDescriptor +org.junit.platform.suite.engine.SuiteLauncher +org.junit.platform.suite.engine.SuiteTestDescriptor +org.junit.platform.suite.engine.SuiteTestDescriptor$LifecycleMethods +org.junit.platform.suite.engine.SuiteTestEngine +org.junit.runner.Description +org.junit.runner.Result +org.junit.runners.BlockJUnit4ClassRunner +org.junit.runners.JUnit4 +org.junit.validator.PublicClassValidator +org.junit.vintage.engine.descriptor.RunnerTestDescriptor +org.junit.vintage.engine.descriptor.VintageEngineDescriptor +org.junit.vintage.engine.discovery.DefensiveAllDefaultPossibilitiesBuilder +org.junit.vintage.engine.discovery.VintageDiscoverer +org.junit.vintage.engine.JUnit4VersionCheck +org.junit.vintage.engine.support.UniqueIdReader +org.junit.vintage.engine.support.UniqueIdStringifier +org.junit.vintage.engine.VintageTestEngine diff --git a/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/AbstractParentClassTests.java b/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/AbstractParentClassTests.java index 6a90af460..73620a497 100644 --- a/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/AbstractParentClassTests.java +++ b/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/AbstractParentClassTests.java @@ -43,6 +43,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -52,6 +53,32 @@ public class AbstractParentClassTests { + public static class OuterClass { + @Test + protected void test() { + Assertions.assertTrue(true, "Just a dummy test that should be executed in outer class"); + } + + /* Since at org.graalvm.junit.platform.JUnitPlatformFeature#registerTestClassForReflection we register all + * declared classes and superclass of the test class, and we do so recursively, we want to avoid infinite loop. + * This inheritance shows that we won't call registration of these classes indefinitely (call registration of + * all declared classes of AbstractParentClassTests, then recursively call superclass of InfiniteLoopTest and + * repeat the process indefinitely) */ + private class InfiniteLoopTest extends OuterClass { + @Test + protected void test() { + Assertions.assertTrue(true, "Just a dummy test that should be executed in inner class"); + } + } + + /* Since enum here is declared class of AbstractParentClassTests, we want to avoid registrations of + * enum's internal superclasses and sub-classes at org.graalvm.junit.platform.JUnitPlatformFeature#registerTestClassForReflection */ + private enum EnumTest { + SOME_VALUE, + OTHER_VALUE + } + } + public abstract static class MathPowerTests { protected static BiFunction powFunction; diff --git a/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/MethodSourceTests.java b/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/MethodSourceTests.java index 7083a3d01..626bfb617 100644 --- a/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/MethodSourceTests.java +++ b/common/junit-platform-native/src/test/java/org/graalvm/junit/jupiter/MethodSourceTests.java @@ -60,6 +60,7 @@ public abstract static class ArgumentTestBase { @BeforeAll public static void setup() { + expectedArgs.clear(); actualArgs.clear(); } diff --git a/common/utils/src/main/java/org/graalvm/buildtools/utils/JUnitUtils.java b/common/utils/src/main/java/org/graalvm/buildtools/utils/JUnitUtils.java new file mode 100644 index 000000000..e8dd7cf70 --- /dev/null +++ b/common/utils/src/main/java/org/graalvm/buildtools/utils/JUnitUtils.java @@ -0,0 +1,17 @@ +package org.graalvm.buildtools.utils; + +import java.util.ArrayList; +import java.util.List; + +public final class JUnitUtils { + + public static List excludeJUnitClassInitializationFiles() { + List args = new ArrayList<>(); + args.add("--exclude-config"); + args.add(".*.jar"); + args.add("META-INF\\/native-image\\/org.junit.*.properties.*"); + + return args; + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 101943a79..80e42d905 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,8 +11,8 @@ mavenEmbedder = "3.9.9" mavenResolver = "1.9.22" graalvm = "23.0.2" openjson = "1.0.13" -junitPlatform = "1.10.0" -junitJupiter = "5.10.0" +junitPlatform = "1.11.0" +junitJupiter = "5.11.0" slf4j = "1.7.9" groovy = "3.0.11" jetty = "11.0.11" diff --git a/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JUnitFunctionalTests.groovy b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JUnitFunctionalTests.groovy new file mode 100644 index 000000000..c630abc4f --- /dev/null +++ b/native-gradle-plugin/src/functionalTest/groovy/org/graalvm/buildtools/gradle/JUnitFunctionalTests.groovy @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.buildtools.gradle + +import org.graalvm.buildtools.gradle.fixtures.AbstractFunctionalTest + +class JUnitFunctionalTests extends AbstractFunctionalTest { + def "test if JUint support works with various annotations, reflection and resources"() { + + given: + withSample("junit-tests") + + when: + run 'nativeTest' + + then: + tasks { + succeeded ':testClasses', ':nativeTestCompile', ':nativeTest' + } + outputDoesNotContain "[junit-platform-native] WARNING: Trying to find test-ids on default locations" + outputContains "Running in 'test listener' mode using files matching pattern [junit-platform-unique-ids*] found in folder [" + outputContains """ +[ 10 containers found ] +[ 0 containers skipped ] +[ 10 containers started ] +[ 0 containers aborted ] +[ 10 containers successful ] +[ 0 containers failed ] +[ 24 tests found ] +[ 1 tests skipped ] +[ 23 tests started ] +[ 0 tests aborted ] +[ 23 tests successful ] +[ 0 tests failed ] +""".trim() + } +} diff --git a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java index dfa3b521a..e0647eab9 100644 --- a/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java +++ b/native-gradle-plugin/src/main/java/org/graalvm/buildtools/gradle/NativeImagePlugin.java @@ -64,6 +64,7 @@ import org.graalvm.buildtools.gradle.tasks.NativeRunTask; import org.graalvm.buildtools.gradle.tasks.actions.CleanupAgentFilesAction; import org.graalvm.buildtools.gradle.tasks.actions.MergeAgentFilesAction; +import org.graalvm.buildtools.utils.JUnitUtils; import org.graalvm.buildtools.utils.SharedConstants; import org.graalvm.reachability.DirectoryConfiguration; import org.gradle.api.Action; @@ -650,7 +651,6 @@ public void registerTestBinary(Project project, // Following ensures that required feature jar is on classpath for every project injectTestPluginDependencies(project, graalExtension.getTestSupport()); - TaskProvider testImageBuilder = tasks.named(deriveTaskName(name, "native", "Compile"), BuildNativeImageTask.class, task -> { task.setOnlyIf(t -> graalExtension.getTestSupport().get() && testListDirectory.getAsFile().get().exists()); task.getTestListDirectory().set(testListDirectory); @@ -662,6 +662,7 @@ public void registerTestBinary(Project project, // Later this will be replaced by a dedicated task not requiring execution of tests testList.from(testListDirectory).builtBy(testTask); testOptions.getClasspath().from(testList); + testOptions.getRuntimeArgs().add("-D" + JUNIT_PLATFORM_LISTENERS_UID_TRACKING_OUTPUT_DIR + "=" + testResultsDir.dir(testTask.getName() + "/testlist").get().getAsFile().getAbsolutePath()); }); if (isPrimaryTest) { tasks.register(DEPRECATED_NATIVE_TEST_BUILD_TASK, t -> { @@ -764,14 +765,20 @@ private static NativeImageOptions createTestOptions(GraalVMExtension graalExtens testExtension.getMainClass().set("org.graalvm.junit.platform.NativeImageJUnitLauncher"); testExtension.getMainClass().finalizeValue(); testExtension.getImageName().convention(mainExtension.getImageName().map(name -> name + SharedConstants.NATIVE_TESTS_SUFFIX)); + ListProperty runtimeArgs = testExtension.getRuntimeArgs(); runtimeArgs.add("--xml-output-dir"); runtimeArgs.add(project.getLayout().getBuildDirectory().dir("test-results/" + binaryName + "-native").map(d -> d.getAsFile().getAbsolutePath())); + testExtension.buildArgs("--features=org.graalvm.junit.platform.JUnitPlatformFeature"); ConfigurableFileCollection classpath = testExtension.getClasspath(); classpath.from(configs.getImageClasspathConfiguration()); classpath.from(sourceSet.getOutput().getClassesDirs()); classpath.from(sourceSet.getOutput().getResourcesDir()); + + /* in version 5.12.0 JUnit added initialize-at-build-time properties files which we need to exclude */ + testExtension.getBuildArgs().addAll(JUnitUtils.excludeJUnitClassInitializationFiles()); + return testExtension; } diff --git a/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JUnitFunctionalTests.groovy b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JUnitFunctionalTests.groovy new file mode 100644 index 000000000..5dd7c57d2 --- /dev/null +++ b/native-maven-plugin/src/functionalTest/groovy/org/graalvm/buildtools/maven/JUnitFunctionalTests.groovy @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.graalvm.buildtools.maven + +class JUnitFunctionalTests extends AbstractGraalVMMavenFunctionalTest { + def "test if JUint support works with various annotations, reflection and resources"() { + withSample("junit-tests") + + when: + mvn '-DquickBuild', '-Pnative', 'test' + + then: + buildSucceeded + outputDoesNotContain "[junit-platform-native] WARNING: Trying to find test-ids on default locations" + outputContains "[junit-platform-native] Running in 'test listener' mode" + outputContains """ +[ 10 containers found ] +[ 0 containers skipped ] +[ 10 containers started ] +[ 0 containers aborted ] +[ 10 containers successful ] +[ 0 containers failed ] +[ 24 tests found ] +[ 1 tests skipped ] +[ 23 tests started ] +[ 0 tests aborted ] +[ 23 tests successful ] +[ 0 tests failed ] +""".trim() + } +} diff --git a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java index a59009a57..f6006b71e 100644 --- a/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java +++ b/native-maven-plugin/src/main/java/org/graalvm/buildtools/maven/NativeTestMojo.java @@ -60,6 +60,8 @@ import org.eclipse.aether.resolution.DependencyRequest; import org.eclipse.aether.resolution.DependencyResolutionException; import org.eclipse.aether.resolution.DependencyResult; +import org.graalvm.buildtools.utils.FileUtils; +import org.graalvm.buildtools.utils.JUnitUtils; import org.graalvm.buildtools.utils.NativeImageConfigurationUtils; import java.io.IOException; @@ -74,6 +76,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -157,6 +160,9 @@ public void execute() throws MojoExecutionException { configureEnvironment(); buildArgs.add("--features=org.graalvm.junit.platform.JUnitPlatformFeature"); + /* in version 5.12.0 JUnit added initialize-at-build-time properties files which we need to exclude */ + buildArgs.addAll(JUnitUtils.excludeJUnitClassInitializationFiles()); + if (systemProperties == null) { systemProperties = new HashMap<>(); } @@ -218,6 +224,7 @@ private void runNativeTests(Path executable) throws MojoExecutionException { if (!xmlLocation.toFile().exists() && !xmlLocation.toFile().mkdirs()) { throw new MojoExecutionException("Failed creating xml output directory"); } + try { ProcessBuilder processBuilder = new ProcessBuilder(executable.toAbsolutePath().toString()); processBuilder.inheritIO(); @@ -227,6 +234,7 @@ private void runNativeTests(Path executable) throws MojoExecutionException { command.add("--xml-output-dir"); command.add(xmlLocation.toString()); systemProperties.forEach((key, value) -> command.add("-D" + key + "=" + value)); + processBuilder.command().addAll(command); processBuilder.environment().putAll(environment); diff --git a/samples/junit-tests/build.gradle b/samples/junit-tests/build.gradle new file mode 100644 index 000000000..0e5021ca1 --- /dev/null +++ b/samples/junit-tests/build.gradle @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +plugins { + id 'application' + id 'org.graalvm.buildtools.native' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' + testImplementation "org.junit.vintage:junit-vintage-engine:5.11.0" +} + +application { + // Define the main class for the application. + mainClass = 'tests.App' +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} + diff --git a/samples/junit-tests/gradle.properties b/samples/junit-tests/gradle.properties new file mode 100644 index 000000000..543c078cd --- /dev/null +++ b/samples/junit-tests/gradle.properties @@ -0,0 +1 @@ +native.gradle.plugin.version = 0.10.7-SNAPSHOT diff --git a/samples/junit-tests/pom.xml b/samples/junit-tests/pom.xml new file mode 100644 index 000000000..9cdc96369 --- /dev/null +++ b/samples/junit-tests/pom.xml @@ -0,0 +1,139 @@ + + + + + 4.0.0 + + org.graalvm.buildtools.examples + maven + 1.0.0-SNAPSHOT + + + 1.8 + UTF-8 + 5.11.0 + 0.10.7-SNAPSHOT + 0.10.7-SNAPSHOT + example-app + + + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + org.junit.vintage + junit-vintage-engine + ${junit.jupiter.version} + test + + + + + + native + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + org.graalvm.buildtools + native-maven-plugin + ${native.maven.plugin.version} + true + + + + test-native + + test + + test + + + build-native + + compile-no-fork + + package + + + + false + ${imageName} + false + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + 1.8 + + + + + + diff --git a/samples/junit-tests/settings.gradle b/samples/junit-tests/settings.gradle new file mode 100644 index 000000000..11735ca92 --- /dev/null +++ b/samples/junit-tests/settings.gradle @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +pluginManagement { + plugins { + id 'org.graalvm.buildtools.native' version getProperty('native.gradle.plugin.version') + } +} + +rootProject.name = 'junit-tests' diff --git a/samples/junit-tests/src/test/java/tests/ComplexTest.java b/samples/junit-tests/src/test/java/tests/ComplexTest.java new file mode 100644 index 000000000..7b6c7db78 --- /dev/null +++ b/samples/junit-tests/src/test/java/tests/ComplexTest.java @@ -0,0 +1,75 @@ +package tests; + +import org.junit.jupiter.api.Test; +import tests.common.Fruits; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + + +public class ComplexTest { + + private static final String RESOURCE = "/resource.txt"; + + @Test + public void callMethodFromOtherClass() { + String fruit = Fruits.getSomeFruit(); + assertNotNull(fruit); + assertTrue(fruit.contains("berry")); + } + + @Test + public void accessMethodReflectively() { + try { + String methodName = "get" + "Blackberry"; + Method method = Fruits.class.getDeclaredMethod(methodName, (Class[]) null); + method.setAccessible(true); + + Fruits f = new Fruits(); + String retval = (String) method.invoke(f); + assertTrue(retval.equalsIgnoreCase("blackberry")); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + + @Test + public void accessFiledReflectively() { + try { + Field fruitsField = Fruits.class.getDeclaredField("fruits"); + fruitsField.setAccessible(true); + + Fruits f = new Fruits(); + List fruits = (List) fruitsField.get(f); + + assertEquals(3, fruits.size()); + assertEquals(3, fruits.stream().filter(fruit -> fruit.contains("berry")).collect(Collectors.toList()).size()); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Test + public void resourceTest() { + try(InputStream is = ComplexTest.class.getResourceAsStream(RESOURCE)) { + assertNotNull(is); + InputStreamReader isr = new InputStreamReader(is); + BufferedReader br = new BufferedReader(isr); + + assertTrue(br.readLine().equalsIgnoreCase("Hello from resource!")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/samples/junit-tests/src/test/java/tests/JUnitAnnotationsTests.java b/samples/junit-tests/src/test/java/tests/JUnitAnnotationsTests.java new file mode 100644 index 000000000..d1ee42731 --- /dev/null +++ b/samples/junit-tests/src/test/java/tests/JUnitAnnotationsTests.java @@ -0,0 +1,113 @@ +/* + * This Java source file was generated by the Gradle 'init' task. + */ +package tests; + +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.FieldSource; +import tests.common.TestInterface; + + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + + +public class JUnitAnnotationsTests implements TestInterface { + + /* test Disabled tests */ + @Test + @Disabled + void skipThisTest() { + throw new RuntimeException("This test should not be executed!"); + } + + /* test MethodSource with Interface */ + @ParameterizedTest + @MethodSource("names") + void test(String name) { + assertEquals(5, name.length()); + assertTrue(name.startsWith("S")); + } + + /* test FieldSource with other class */ + @ParameterizedTest + @FieldSource("tests.common.Fruits#fruits") + void testWithExternalFieldSource(String fruit) { + assertTrue(fruit.contains("berry")); + } + + + /* test FieldSource with field from this class */ + static final List listOfFruits = Arrays.asList("apple", "ananas"); + + @ParameterizedTest + @FieldSource("listOfFruits") + void singleFieldSource(String fruit) { + assertTrue(fruit.startsWith("a")); + } + + + /* test RepeatedTest */ + private static int numberOfRepetitions = -1; + + @BeforeAll + static void initializeNumberOfRepetitions() { + /* if we don't execute this first, repeated tests will fail */ + numberOfRepetitions = 1; + } + + @AfterAll + static void checkIfThisComesLast() { + /* if this comes last, 3 repeated tests should have increased this value to 4 */ + assertEquals(4, numberOfRepetitions); + } + + @RepeatedTest(3) + void repeatedTest(RepetitionInfo repetitionInfo) { + assertEquals(repetitionInfo.getCurrentRepetition(), numberOfRepetitions); + numberOfRepetitions++; + } + + /* test BeforeEach and AfterEach annotations */ + private static int beforeEachTestValue = -1; + private static int afterEachTestValue = -1; + + @BeforeEach + void setBeforeEach() { + beforeEachTestValue = 0; + } + + @AfterEach + void setAfterEach() { + afterEachTestValue = -1; + } + + @Test + void beforeAndAfterEachTest1() { + assertEquals(0, beforeEachTestValue); + assertEquals(-1, afterEachTestValue); + beforeEachTestValue = (int) (Math.random() * 10 + 1); + afterEachTestValue = (int) (Math.random() * 10 + 1); + } + + @Test + void beforeAndAfterEachTest2() { + assertEquals(0, beforeEachTestValue); + assertEquals(-1, afterEachTestValue); + beforeEachTestValue = (int) (Math.random() * 10 + 1); + afterEachTestValue = (int) (Math.random() * 10 + 1); + } + + @Test + void beforeAndAfterEachTest3() { + assertEquals(0, beforeEachTestValue); + assertEquals(-1, afterEachTestValue); + beforeEachTestValue = (int) (Math.random() * 10 + 1); + afterEachTestValue = (int) (Math.random() * 10 + 1); + } + +} diff --git a/samples/junit-tests/src/test/java/tests/OrderTests.java b/samples/junit-tests/src/test/java/tests/OrderTests.java new file mode 100644 index 000000000..d09936ced --- /dev/null +++ b/samples/junit-tests/src/test/java/tests/OrderTests.java @@ -0,0 +1,35 @@ +package tests; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class OrderTests { + /* tests execution order should be dictated by the @Order annotation */ + protected static String testOrderChecker = "First test"; + + @Test + @Order(2) + void secondTest() { + assertTrue(testOrderChecker.equalsIgnoreCase("Second test")); + testOrderChecker = null; + } + + @Test + @Order(3) + void thirdTest() { + assertNull(testOrderChecker); + } + + @Test + @Order(1) + void firstTest() { + assertTrue(testOrderChecker.equalsIgnoreCase("First test")); + testOrderChecker = "Second test"; + } +} diff --git a/samples/junit-tests/src/test/java/tests/VintageTests.java b/samples/junit-tests/src/test/java/tests/VintageTests.java new file mode 100644 index 000000000..01c411401 --- /dev/null +++ b/samples/junit-tests/src/test/java/tests/VintageTests.java @@ -0,0 +1,45 @@ +package tests; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.core.Every; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; + +public class VintageTests { + + /* passes but with some exceptions ("as warning") */ + + @Test + public void testEvery() { + List numbers = Arrays.asList(1, 1, 1, 1); + MatcherAssert.assertThat(numbers, Every.everyItem(is(1))); + } + + @SuppressWarnings("deprecation") + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testExpectedException() { + expectedException.expect(ArithmeticException.class); + throw new ArithmeticException(); + } + + @Test + public void testExpectedExceptionCause() { + expectedException.expectCause(instanceOf(ArithmeticException.class)); + try { + throw new ArithmeticException(); + } catch (ArithmeticException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/samples/junit-tests/src/test/java/tests/common/Fruits.java b/samples/junit-tests/src/test/java/tests/common/Fruits.java new file mode 100644 index 000000000..c427250fc --- /dev/null +++ b/samples/junit-tests/src/test/java/tests/common/Fruits.java @@ -0,0 +1,19 @@ +package tests.common; + +import java.util.Arrays; +import java.util.List; + +public class Fruits { + + private static final List fruits = Arrays.asList("blackberry", "raspberry", "strawberry"); + + public static String getSomeFruit() { + int index = Math.random() > 0.5 ? 1 : 0; + return fruits.get(index); + } + + private String getBlackberry() { + return fruits.get(0); + } + +} diff --git a/samples/junit-tests/src/test/java/tests/common/TestInterface.java b/samples/junit-tests/src/test/java/tests/common/TestInterface.java new file mode 100644 index 000000000..ab8e1bc2d --- /dev/null +++ b/samples/junit-tests/src/test/java/tests/common/TestInterface.java @@ -0,0 +1,10 @@ +package tests.common; + +import java.util.List; + +public interface TestInterface { + + static List names() { + return List.of("Sarah", "Susan"); + } +} diff --git a/samples/junit-tests/src/test/resources/resource.txt b/samples/junit-tests/src/test/resources/resource.txt new file mode 100644 index 000000000..a07388701 --- /dev/null +++ b/samples/junit-tests/src/test/resources/resource.txt @@ -0,0 +1 @@ +Hello from resource!