diff --git a/build.gradle.kts b/build.gradle.kts index ac2b1c43ec54..917c24b1805a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,7 +43,7 @@ val vintageProjects by extra(listOf( dependencyProject(projects.junitVintageEngine) )) -val mavenizedProjects by extra(platformProjects + jupiterProjects + vintageProjects) +val mavenizedProjects by extra(listOf(dependencyProject(projects.junitStart)) + platformProjects + jupiterProjects + vintageProjects) val modularProjects by extra(mavenizedProjects - setOf(dependencyProject(projects.junitPlatformConsoleStandalone))) dependencies { diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClasspathFilters.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClasspathFilters.java index 1887ed7a5211..4598cce8a483 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClasspathFilters.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ClasspathFilters.java @@ -19,11 +19,21 @@ class ClasspathFilters { static final String CLASS_FILE_SUFFIX = ".class"; + static final String SOURCE_FILE_SUFFIX = ".java"; + + // System property defined since Java 12: https://bugs.java/bugdatabase/JDK-8210877 + private static final boolean SOURCE_MODE = System.getProperty("jdk.launcher.sourcefile") != null; + private static final String PACKAGE_INFO_FILE_NAME = "package-info" + CLASS_FILE_SUFFIX; private static final String MODULE_INFO_FILE_NAME = "module-info" + CLASS_FILE_SUFFIX; + static boolean isClassOrSourceFileName(String name) { + return name.endsWith(CLASS_FILE_SUFFIX) || (SOURCE_MODE && name.endsWith(SOURCE_FILE_SUFFIX)); + } + static Predicate classFiles() { - return file -> isNotPackageInfo(file) && isNotModuleInfo(file) && isClassFile(file); + return file -> isNotPackageInfo(file) && isNotModuleInfo(file) + && (isClassFile(file) || (SOURCE_MODE && isSourceFile(file))); } static Predicate resourceFiles() { @@ -42,6 +52,10 @@ private static boolean isClassFile(Path file) { return file.getFileName().toString().endsWith(CLASS_FILE_SUFFIX); } + private static boolean isSourceFile(Path file) { + return file.getFileName().toString().endsWith(SOURCE_FILE_SUFFIX); + } + private ClasspathFilters() { } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/DefaultClasspathScanner.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/DefaultClasspathScanner.java index e7a66df2a44d..1720d895055f 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/DefaultClasspathScanner.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/DefaultClasspathScanner.java @@ -11,7 +11,6 @@ package org.junit.platform.commons.util; import static java.util.stream.Collectors.joining; -import static org.junit.platform.commons.util.ClasspathFilters.CLASS_FILE_SUFFIX; import static org.junit.platform.commons.util.StringUtils.isNotBlank; import java.io.IOException; @@ -32,6 +31,7 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.function.Try; import org.junit.platform.commons.io.Resource; @@ -182,10 +182,10 @@ private static void walkFilesForUri(URI baseUri, Predicate filter, BiConsu } } - private void processClassFileSafely(Path baseDir, String basePackageName, ClassFilter classFilter, Path classFile, + private void processClassFileSafely(Path baseDir, String basePackageName, ClassFilter classFilter, Path file, Consumer> classConsumer) { try { - String fullyQualifiedClassName = determineFullyQualifiedClassName(baseDir, basePackageName, classFile); + String fullyQualifiedClassName = determineFullyQualifiedClassName(baseDir, basePackageName, file); if (classFilter.match(fullyQualifiedClassName)) { try { // @formatter:off @@ -196,12 +196,12 @@ private void processClassFileSafely(Path baseDir, String basePackageName, ClassF // @formatter:on } catch (InternalError internalError) { - handleInternalError(classFile, fullyQualifiedClassName, internalError); + handleInternalError(file, fullyQualifiedClassName, internalError); } } } catch (Throwable throwable) { - handleThrowable(classFile, throwable); + handleThrowable(file, throwable); } } @@ -221,12 +221,12 @@ private void processResourceFileSafely(Path baseDir, String basePackageName, Res } } - private String determineFullyQualifiedClassName(Path baseDir, String basePackageName, Path classFile) { + private String determineFullyQualifiedClassName(Path baseDir, String basePackageName, Path file) { // @formatter:off return Stream.of( basePackageName, - determineSubpackageName(baseDir, classFile), - determineSimpleClassName(classFile) + determineSubpackageName(baseDir, file), + determineSimpleClassName(file) ) .filter(value -> !value.isEmpty()) // Handle default package appropriately. .collect(joining(PACKAGE_SEPARATOR_STRING)); @@ -253,21 +253,29 @@ private String determineFullyQualifiedResourceName(Path baseDir, String basePack // @formatter:on } - private String determineSimpleClassName(Path classFile) { - String fileName = classFile.getFileName().toString(); - return fileName.substring(0, fileName.length() - CLASS_FILE_SUFFIX.length()); + private String determineSimpleClassName(Path file) { + String fileName = file.getFileName().toString(); + return determineSimpleClassName(fileName); + } + + static String determineSimpleClassName(String fileName) { + int lastDot = fileName.lastIndexOf('.'); + if (lastDot < 0) { + throw new JUnitException("Expected file name with file extension, but got: " + fileName); + } + return fileName.substring(0, lastDot); } private String determineSimpleResourceName(Path resourceFile) { return resourceFile.getFileName().toString(); } - private String determineSubpackageName(Path baseDir, Path classFile) { - Path relativePath = baseDir.relativize(classFile.getParent()); + private String determineSubpackageName(Path baseDir, Path file) { + Path relativePath = baseDir.relativize(file.getParent()); String pathSeparator = baseDir.getFileSystem().getSeparator(); String subpackageName = relativePath.toString().replace(pathSeparator, PACKAGE_SEPARATOR_STRING); if (subpackageName.endsWith(pathSeparator)) { - // Workaround for JDK bug: https://bugs.openjdk.java.net/browse/JDK-8153248 + // TODO: Remove workaround for JDK bug: https://bugs.openjdk.org/browse/JDK-8153248 subpackageName = subpackageName.substring(0, subpackageName.length() - pathSeparator.length()); } return subpackageName; diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java index 350f443b3ad1..a236b89fc90e 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ExceptionUtils.java @@ -41,6 +41,8 @@ @API(status = INTERNAL, since = "1.0") public final class ExceptionUtils { + private static final String JUNIT_START_PACKAGE_PREFIX = "org.junit.start."; + private static final String JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX = "org.junit.platform.launcher."; private static final Predicate STACK_TRACE_ELEMENT_FILTER = ClassNamePatternFilterUtils // @@ -139,6 +141,9 @@ public static void pruneStackTrace(Throwable throwable, List classNames) prunedStackTrace.addAll(stackTrace.subList(i, stackTrace.size())); break; } + else if (className.startsWith(JUNIT_START_PACKAGE_PREFIX)) { + prunedStackTrace.clear(); + } else if (className.startsWith(JUNIT_PLATFORM_LAUNCHER_PACKAGE_PREFIX)) { prunedStackTrace.clear(); } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java index 9c0ae00120c4..f4e5c9eaedbf 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ModuleUtils.java @@ -253,9 +253,11 @@ List> scan(ModuleReference reference) { try (ModuleReader reader = reference.open()) { try (Stream names = reader.list()) { // @formatter:off - return names.filter(name -> name.endsWith(".class")) - .map(this::className) + return names.filter(ClasspathFilters::isClassOrSourceFileName) + .map(DefaultClasspathScanner::determineSimpleClassName) + .map(name -> name.replace('/', '.')) .filter(name -> !"module-info".equals(name)) + .filter(name -> !name.endsWith("package-info")) .filter(classFilter::match) .> map(this::loadClassUnchecked) .filter(classFilter::match) @@ -268,15 +270,6 @@ List> scan(ModuleReference reference) { } } - /** - * Convert resource name to binary class name. - */ - private String className(String resourceName) { - resourceName = resourceName.substring(0, resourceName.length() - 6); // 6 = ".class".length() - resourceName = resourceName.replace('/', '.'); - return resourceName; - } - /** * Load class by its binary name. * diff --git a/junit-platform-console/junit-platform-console.gradle.kts b/junit-platform-console/junit-platform-console.gradle.kts index 803d76a1da1c..dd9568faf82f 100644 --- a/junit-platform-console/junit-platform-console.gradle.kts +++ b/junit-platform-console/junit-platform-console.gradle.kts @@ -27,6 +27,7 @@ dependencies { tasks { compileJava { options.compilerArgs.addAll(listOf( + "-Xlint:-module", // due to qualified exports "--add-modules", "info.picocli", "--add-reads", "${javaModuleName}=info.picocli" )) diff --git a/junit-platform-console/src/main/java/module-info.java b/junit-platform-console/src/main/java/module-info.java index 336c201f8252..7ca71d3518dd 100644 --- a/junit-platform-console/src/main/java/module-info.java +++ b/junit-platform-console/src/main/java/module-info.java @@ -24,5 +24,7 @@ requires org.junit.platform.launcher; requires org.junit.platform.reporting; + exports org.junit.platform.console.output to org.junit.start; + provides java.util.spi.ToolProvider with org.junit.platform.console.ConsoleLauncherToolProvider; } diff --git a/junit-start/junit-start.gradle.kts b/junit-start/junit-start.gradle.kts new file mode 100644 index 000000000000..a86df06f0f85 --- /dev/null +++ b/junit-start/junit-start.gradle.kts @@ -0,0 +1,21 @@ +plugins { + id("junitbuild.java-library-conventions") +} + +description = "JUnit Start Module" + +dependencies { + api(platform(projects.junitBom)) + api(projects.junitJupiter) + + compileOnlyApi(libs.apiguardian) + compileOnlyApi(libs.jspecify) + compileOnlyApi(projects.junitJupiterEngine) + + implementation(projects.junitPlatformLauncher) + implementation(projects.junitPlatformConsole) +} + +backwardCompatibilityChecks { + enabled = false // already checked by individual projects +} diff --git a/junit-start/src/main/java/module-info.java b/junit-start/src/main/java/module-info.java new file mode 100644 index 000000000000..9917e8569067 --- /dev/null +++ b/junit-start/src/main/java/module-info.java @@ -0,0 +1,37 @@ +/* + * Copyright 2015-2025 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 + */ + +/** + * Defines the API of the JUnit Start module for writing and running tests. + *

+ * Usage example: + *

{@code
+ * import module org.junit.start;
+ *
+ *  void main() {
+ *    JUnit.run();
+ *  }
+ *
+ *  @Test
+ *  void addition() {
+ *    Assertions.assertEquals(2, 1 + 1, "Addition error detected!");
+ *  }
+ * }
+ */ +module org.junit.start { + requires static transitive org.apiguardian.api; + requires static transitive org.jspecify; + + requires transitive org.junit.jupiter; + requires org.junit.platform.launcher; + requires org.junit.platform.console; + + exports org.junit.start; +} diff --git a/junit-start/src/main/java/org/junit/start/JUnit.java b/junit-start/src/main/java/org/junit/start/JUnit.java new file mode 100644 index 000000000000..c01ba51ff860 --- /dev/null +++ b/junit-start/src/main/java/org/junit/start/JUnit.java @@ -0,0 +1,80 @@ +/* + * Copyright 2015-2025 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 + */ + +package org.junit.start; + +import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectModule; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import java.io.PrintWriter; +import java.nio.charset.Charset; + +import org.apiguardian.api.API; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.console.output.ColorPalette; +import org.junit.platform.console.output.Theme; +import org.junit.platform.console.output.TreePrintingListener; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; + +@API(status = EXPERIMENTAL, since = "6.0") +public final class JUnit { + /** + * Run all tests defined in the caller class. + */ + public static void run() { + var walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); + run(selectClass(walker.getCallerClass())); + } + + /** + * Run all tests defined in the given test class. + * @param testClass the class to discover and execute tests in + */ + public static void run(Class testClass) { + run(selectClass(testClass)); + } + + /** + * Run all tests defined in the given module. + * @param testModule the module to discover and execute tests in + */ + public static void run(Module testModule) { + run(selectModule(testModule)); + } + + private static void run(DiscoverySelector selector) { + var listener = new SummaryGeneratingListener(); + var charset = Charset.defaultCharset(); + var writer = new PrintWriter(System.out, true, charset); + var printer = new TreePrintingListener(writer, ColorPalette.DEFAULT, Theme.valueOf(charset)); + var request = request().selectors(selector).forExecution() // + .listeners(listener, printer) // + .build(); + var launcher = LauncherFactory.create(); + launcher.execute(request); + var summary = listener.getSummary(); + + if (summary.getTotalFailureCount() == 0) + return; + + summary.printFailuresTo(new PrintWriter(System.err, true, charset)); + throw new JUnitException("JUnit run finished with %d failure%s".formatted( // + summary.getTotalFailureCount(), // + summary.getTotalFailureCount() == 1 ? "" : "s")); + } + + private JUnit() { + } +} diff --git a/junit-start/src/main/java/org/junit/start/package-info.java b/junit-start/src/main/java/org/junit/start/package-info.java new file mode 100644 index 000000000000..f6b2a0ce7dda --- /dev/null +++ b/junit-start/src/main/java/org/junit/start/package-info.java @@ -0,0 +1,8 @@ +/** + * Contains JUnit Start API for writing and running tests. + */ + +@NullMarked +package org.junit.start; + +import org.jspecify.annotations.NullMarked; diff --git a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts index 57115677d812..46681ff3d45c 100644 --- a/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts +++ b/platform-tooling-support-tests/platform-tooling-support-tests.gradle.kts @@ -20,6 +20,7 @@ javaLibrary { spotless { java { target(files(project.java.sourceSets.map { it.allJava }), "projects/**/*.java") + targetExclude("projects/junit-start/**/*.java") // due to compact source files and module imports } kotlin { target("projects/**/*.kt") diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-console.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-console.expected.txt index 1309f73a2f35..870d9bd85bd8 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-console.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-console.expected.txt @@ -7,9 +7,9 @@ requires org.junit.platform.engine requires org.junit.platform.launcher requires org.junit.platform.reporting provides java.util.spi.ToolProvider with org.junit.platform.console.ConsoleLauncherToolProvider +qualified exports org.junit.platform.console.output to org.junit.start contains org.junit.platform.console contains org.junit.platform.console.command contains org.junit.platform.console.options -contains org.junit.platform.console.output contains org.junit.platform.console.shadow.picocli main-class org.junit.platform.console.ConsoleLauncher diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-start.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-start.expected.txt new file mode 100644 index 000000000000..0c4ee8aa39b8 --- /dev/null +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-start.expected.txt @@ -0,0 +1,8 @@ +org.junit.start@${version} jar:file:.+/junit-start-\d.+\.jar..module-info\.class +exports org.junit.start +requires java.base mandated +requires org.apiguardian.api static transitive +requires org.jspecify static transitive +requires org.junit.jupiter transitive +requires org.junit.platform.console +requires org.junit.platform.launcher diff --git a/platform-tooling-support-tests/projects/junit-start/compact/JUnitRun.java b/platform-tooling-support-tests/projects/junit-start/compact/JUnitRun.java new file mode 100644 index 000000000000..8d38641fb08f --- /dev/null +++ b/platform-tooling-support-tests/projects/junit-start/compact/JUnitRun.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2025 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 + */ + +import module org.junit.start; + +void main() { + JUnit.run(); +} + +@Test +void addition() { + Assertions.assertEquals(2, 1 + 1, "Addition error detected!"); +} diff --git a/platform-tooling-support-tests/projects/junit-start/compact/JUnitRunClass.java b/platform-tooling-support-tests/projects/junit-start/compact/JUnitRunClass.java new file mode 100644 index 000000000000..86e76c0d6523 --- /dev/null +++ b/platform-tooling-support-tests/projects/junit-start/compact/JUnitRunClass.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015-2025 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 + */ + +import module org.junit.start; + +void main() { + JUnit.run(getClass()); +} + +@Test +void substraction() { + Assertions.assertEquals(2, 3 - 1, "Subtraction error detected!"); +} diff --git a/platform-tooling-support-tests/projects/junit-start/modular/module-info.java b/platform-tooling-support-tests/projects/junit-start/modular/module-info.java new file mode 100644 index 000000000000..9f29b2f31b7f --- /dev/null +++ b/platform-tooling-support-tests/projects/junit-start/modular/module-info.java @@ -0,0 +1,13 @@ +/* + * Copyright 2015-2025 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 + */ + +open module m { + requires org.junit.start; +} diff --git a/platform-tooling-support-tests/projects/junit-start/modular/p/JUnitRunModule.java b/platform-tooling-support-tests/projects/junit-start/modular/p/JUnitRunModule.java new file mode 100644 index 000000000000..bde03f682da9 --- /dev/null +++ b/platform-tooling-support-tests/projects/junit-start/modular/p/JUnitRunModule.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2025 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 + */ + +package p; + +import module org.junit.start; + +class JUnitRunModule { + void main() { + JUnit.run(getClass().getModule()); + } +} diff --git a/platform-tooling-support-tests/projects/junit-start/modular/p/MultiplicationTests.java b/platform-tooling-support-tests/projects/junit-start/modular/p/MultiplicationTests.java new file mode 100644 index 000000000000..af41e24566b3 --- /dev/null +++ b/platform-tooling-support-tests/projects/junit-start/modular/p/MultiplicationTests.java @@ -0,0 +1,21 @@ +/* + * Copyright 2015-2025 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 + */ + +package p; + +import module org.junit.jupiter.api; + +class MultiplicationTests { + + @Test + void multiplication() { + Assertions.assertEquals(4, 2 * 2, "Multiplication error detected!"); + } +} diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java index d755f73cd5e8..1d0dc9dea053 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/HelperTests.java @@ -36,6 +36,7 @@ void loadModuleDirectoryNames() { "junit-jupiter-engine", // "junit-jupiter-migrationsupport", // "junit-jupiter-params", // + "junit-start", // "junit-platform-commons", // "junit-platform-console", // "junit-platform-engine", // diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/JUnitStartTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/JUnitStartTests.java new file mode 100644 index 000000000000..42a58f21776a --- /dev/null +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/JUnitStartTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015-2025 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 + */ + +package platform.tooling.support.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static platform.tooling.support.tests.Projects.copyToWorkspace; + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; +import org.junit.jupiter.api.io.TempDir; +import org.junit.platform.tests.process.OutputFiles; + +import platform.tooling.support.Helper; +import platform.tooling.support.MavenRepo; +import platform.tooling.support.ProcessStarters; +import platform.tooling.support.ThirdPartyJars; + +/** + * @since 6.1 + */ +class JUnitStartTests { + + @TempDir + static Path workspace; + + @BeforeAll + static void prepareLocalLibraryDirectoryWithJUnitModules() throws Exception { + copyToWorkspace(Projects.JUNIT_start, workspace); + var lib = workspace.resolve("lib"); + try { + Files.createDirectories(lib); + try (var directoryStream = Files.newDirectoryStream(lib, "*.jar")) { + for (Path jarFile : directoryStream) { + Files.delete(jarFile); + } + } + for (var module : Helper.loadModuleDirectoryNames()) { + if (module.startsWith("junit-platform") || module.startsWith("junit-jupiter") + || module.equals("junit-start")) { + if (module.equals("junit-jupiter-migrationsupport")) + continue; + if (module.startsWith("junit-platform-suite")) + continue; + if (module.equals("junit-platform-testkit")) + continue; + var jar = MavenRepo.jar(module); + Files.copy(jar, lib.resolve(module + ".jar")); + } + } + ThirdPartyJars.copy(lib, "org.apiguardian", "apiguardian-api"); + ThirdPartyJars.copy(lib, "org.jspecify", "jspecify"); + ThirdPartyJars.copy(lib, "org.opentest4j", "opentest4j"); + ThirdPartyJars.copy(lib, "org.opentest4j.reporting", "open-test-reporting-tooling-spi"); + } + catch (Exception e) { + throw new AssertionError("Preparing local library folder failed", e); + } + } + + @Test + @EnabledOnJre(JRE.JAVA_25) + void junitRun(@FilePrefix("junit-run") OutputFiles outputFiles) throws Exception { + var result = ProcessStarters.java() // + .workingDir(workspace) // + .addArguments("--module-path", "lib") // relative to workspace + .addArguments("--add-modules", "org.junit.start") // configure root module + .addArguments("compact/JUnitRun.java") // leverage Java's source mode + .redirectOutput(outputFiles) // + .startAndWait(); + + assertEquals(0, result.exitCode()); + assertTrue(result.stdOut().contains("addition()"), result.stdOut()); + } + + @Test + @EnabledOnJre(JRE.JAVA_25) + void junitRunClass(@FilePrefix("junit-run-class") OutputFiles outputFiles) throws Exception { + var result = ProcessStarters.java() // + .workingDir(workspace) // + .addArguments("--module-path", "lib") // relative to workspace + .addArguments("--add-modules", "org.junit.start") // configure root module + .addArguments("compact/JUnitRunClass.java") // leverage Java's source mode + .redirectOutput(outputFiles) // + .startAndWait(); + + assertEquals(0, result.exitCode()); + assertTrue(result.stdOut().contains("substraction()"), result.stdOut()); + } + + @Test + @EnabledOnJre(JRE.JAVA_25) + void junitRunModule(@FilePrefix("junit-run-Module") OutputFiles outputFiles) throws Exception { + var result = ProcessStarters.java() // + .workingDir(workspace) // + .putEnvironment("NO_COLOR", "1") // --disable-ansi-colors + .addArguments("--module-path", "lib") // relative to workspace + .addArguments("modular/p/JUnitRunModule.java") // leverage Java's source mode + .redirectOutput(outputFiles) // + .startAndWait(); + + assertEquals(0, result.exitCode()); + assertTrue(result.stdOut().contains("multiplication()"), result.stdOut()); + } + +} diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/Projects.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/Projects.java index be645466ee34..b62b9ca99cf4 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/Projects.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/Projects.java @@ -21,6 +21,7 @@ public class Projects { public static final String GRADLE_KOTLIN_EXTENSIONS = "gradle-kotlin-extensions"; public static final String GRADLE_MISSING_ENGINE = "gradle-missing-engine"; public static final String JAR_DESCRIBE_MODULE = "jar-describe-module"; + public static final String JUNIT_start = "junit-start"; public static final String JUPITER_STARTER = "jupiter-starter"; public static final String KOTLIN_COROUTINES = "kotlin-coroutines"; public static final String MAVEN_SUREFIRE_COMPATIBILITY = "maven-surefire-compatibility"; diff --git a/settings.gradle.kts b/settings.gradle.kts index 53dc8d7c4f78..22cff4977cde 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -79,6 +79,7 @@ include("junit-jupiter-api") include("junit-jupiter-engine") include("junit-jupiter-migrationsupport") include("junit-jupiter-params") +include("junit-start") include("junit-platform-commons") include("junit-platform-console") include("junit-platform-console-standalone")