diff --git a/.github/actions/run-gradle/action.yml b/.github/actions/run-gradle/action.yml index 70191fd53f43..15afa773760b 100644 --- a/.github/actions/run-gradle/action.yml +++ b/.github/actions/run-gradle/action.yml @@ -13,6 +13,7 @@ runs: with: distribution: temurin java-version: 21 + check-latest: true - uses: gradle/actions/setup-gradle@v3 - shell: bash env: diff --git a/.github/actions/setup-test-jdk/action.yml b/.github/actions/setup-test-jdk/action.yml index 84f84589a0dd..70a571e59a7f 100644 --- a/.github/actions/setup-test-jdk/action.yml +++ b/.github/actions/setup-test-jdk/action.yml @@ -4,7 +4,7 @@ inputs: distribution: required: true description: 'The JDK distribution to use' - default: 'temurin' + default: 'liberica' runs: using: "composite" steps: @@ -12,5 +12,6 @@ runs: with: distribution: ${{ inputs.distribution }} java-version: 8 + check-latest: true - shell: bash run: echo "JDK8=$JAVA_HOME" >> $GITHUB_ENV diff --git a/.github/workflows/cross-version.yml b/.github/workflows/cross-version.yml index aa3a9b4dec12..d3c5574fae3c 100644 --- a/.github/workflows/cross-version.yml +++ b/.github/workflows/cross-version.yml @@ -70,6 +70,7 @@ jobs: with: distribution: semeru java-version: ${{ matrix.jdk }} + check-latest: true - name: 'Prepare JDK${{ matrix.jdk }} env var' shell: bash run: echo "JDK${{ matrix.jdk }}=$JAVA_HOME" >> $GITHUB_ENV diff --git a/.github/workflows/gradle-dependency-submission.yml b/.github/workflows/gradle-dependency-submission.yml index 89d804260109..32e037d8f4e7 100644 --- a/.github/workflows/gradle-dependency-submission.yml +++ b/.github/workflows/gradle-dependency-submission.yml @@ -22,5 +22,6 @@ jobs: with: distribution: temurin java-version: 21 + check-latest: true - name: Generate and submit dependency graph uses: gradle/actions/dependency-submission@v3 diff --git a/README.md b/README.md index de2fdb444779..65e4d7c05c69 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This repository is the home of _JUnit 5_. ## Latest Releases - General Availability (GA): [JUnit 5.10.2](https://github.com/junit-team/junit5/releases/tag/r5.10.2) (February 4, 2024) -- Preview (Milestone/Release Candidate): [JUnit 5.11.0-M1](https://github.com/junit-team/junit5/releases/tag/r5.11.0-M1) (April 23, 2024) +- Preview (Milestone/Release Candidate): [JUnit 5.11.0-M2](https://github.com/junit-team/junit5/releases/tag/r5.11.0-M2) (May 17, 2024) ## Documentation diff --git a/documentation/documentation.gradle.kts b/documentation/documentation.gradle.kts index 26117d3fc741..bcafd6d427fa 100644 --- a/documentation/documentation.gradle.kts +++ b/documentation/documentation.gradle.kts @@ -119,7 +119,7 @@ val experimentalApisTableFile = generatedAsciiDocPath.map { it.file("experimenta val deprecatedApisTableFile = generatedAsciiDocPath.map { it.file("deprecated-apis-table.adoc") } val standaloneConsoleLauncherShadowedArtifactsFile = generatedAsciiDocPath.map { it.file("console-launcher-standalone-shadowed-artifacts.adoc") } -val jdkJavadocBaseUrl = "https://docs.oracle.com/en/java/javase/11/docs/api" +val jdkJavadocBaseUrl = "https://docs.oracle.com/en/java/javase/${JavaVersion.current().majorVersion}/docs/api" val elementListsDir = layout.buildDirectory.dir("elementLists") val externalModulesWithoutModularJavadoc = mapOf( "org.apiguardian.api" to "https://apiguardian-team.github.io/apiguardian/docs/$apiGuardianDocVersion/api/", diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index a4a6e0113c70..dfbe7932d479 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -22,9 +22,35 @@ endif::[] // Platform Engine :junit-platform-engine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/package-summary.html[junit-platform-engine] :junit-platform-engine-support-discovery: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/discovery/package-summary.html[org.junit.platform.engine.support.discovery] -:DiscoverySelectors_selectMethod: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectMethod-java.lang.String-[selectMethod(String) in DiscoverySelectors] +:ClasspathResourceSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClasspathResourceSelector.html[ClasspathResourceSelector] +:ClasspathRootSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClasspathRootSelector.html[ClasspathRootSelector] +:ClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClassSelector.html[ClassSelector] +:DirectorySelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DirectorySelector.html[DirectorySelector] +:DiscoverySelectors: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html[DiscoverySelectors] +:DiscoverySelectors_selectClasspathResource: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectClasspathResource(java.lang.String)[selectClasspathResource] +:DiscoverySelectors_selectClasspathRoots: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectClasspathRoots(java.util.Set)[selectClasspathRoots] +:DiscoverySelectors_selectClass: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectClass(java.lang.String)[selectClass] +:DiscoverySelectors_selectDirectory: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectDirectory(java.lang.String)[selectDirectory] +:DiscoverySelectors_selectFile: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectFile(java.lang.String)[selectFile] +:DiscoverySelectors_selectIteration: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectIteration(org.junit.platform.engine.DiscoverySelector,int\...)[selectIteration] +:DiscoverySelectors_selectMethod: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectMethod(java.lang.String)[selectMethod] +:DiscoverySelectors_selectModule: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectModule(java.lang.String)[selectModule] +:DiscoverySelectors_selectNestedClass: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectNestedClass(java.util.List,java.lang.Class)[selectNestedClass] +:DiscoverySelectors_selectNestedMethod: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectNestedMethod(java.util.List,java.lang.Class,java.lang.String)[selectNestedMethod] +:DiscoverySelectors_selectPackage: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectPackage(java.lang.String)[selectPackage] +:DiscoverySelectors_selectUniqueId: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUniqueId(java.lang.String)[selectUniqueId] +:DiscoverySelectors_selectUri: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/DiscoverySelectors.html#selectUri(java.lang.String)[selectUri] +:FileSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/FileSelector.html[FileSelector] :HierarchicalTestEngine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/hierarchical/HierarchicalTestEngine.html[HierarchicalTestEngine] +:IterationSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/IterationSelector.html[IterationSelector] +:MethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/MethodSelector.html[MethodSelector] +:ModuleSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ModuleSelector.html[ModuleSelector] +:NestedClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedClassSelector.html[NestedClassSelector] +:NestedMethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedMethodSelector.html[NestedMethodSelector] +:PackageSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/PackageSelector.html[PackageSelector] :ParallelExecutionConfigurationStrategy: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/hierarchical/ParallelExecutionConfigurationStrategy.html[ParallelExecutionConfigurationStrategy] +:UniqueIdSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/UniqueIdSelector.html[UniqueIdSelector] +:UriSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/UriSelector.html[UriSelector] :TestEngine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/TestEngine.html[TestEngine] // Platform Launcher API :junit-platform-launcher: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/package-summary.html[junit-platform-launcher] @@ -51,6 +77,15 @@ endif::[] // Platform Suite :suite-api-package: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/package-summary.html[org.junit.platform.suite.api] :junit-platform-suite-engine: {javadoc-root}/org.junit.platform.suite.engine/org/junit/platform/suite/engine/package-summary.html[junit-platform-suite-engine] +:Select: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/Select.html[@Select] +:SelectClasspathResource: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectClasspathResource.html[@SelectClasspathResource] +:SelectClasses: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectClasses.html[@SelectClasses] +:SelectDirectories: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectDirectories.html[@SelectDirectories] +:SelectFile: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectFile.html[@SelectFile] +:SelectMethod: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectMethod.html[@SelectMethod] +:SelectModules: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectModules.html[@SelectModules] +:SelectPackages: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectPackages.html[@SelectPackages] +:SelectUris: {javadoc-root}/org.junit.platform.suite.api/org/junit/platform/suite/api/SelectUris.html[@SelectUris] // Platform Test Kit :testkit-engine-package: {javadoc-root}/org.junit.platform.testkit/org/junit/platform/testkit/engine/package-summary.html[org.junit.platform.testkit.engine] :EngineExecutionResults: {javadoc-root}/org.junit.platform.testkit/org/junit/platform/testkit/engine/EngineExecutionResults.html[EngineExecutionResults] diff --git a/documentation/src/docs/asciidoc/release-notes/index.adoc b/documentation/src/docs/asciidoc/release-notes/index.adoc index fb65ef40c2fa..2b51e9347013 100644 --- a/documentation/src/docs/asciidoc/release-notes/index.adoc +++ b/documentation/src/docs/asciidoc/release-notes/index.adoc @@ -17,6 +17,8 @@ authors as well as build tool and IDE vendors. include::{includedir}/link-attributes.adoc[] +include::{basedir}/release-notes-5.11.0-M2.adoc[] + include::{basedir}/release-notes-5.11.0-M1.adoc[] include::{basedir}/release-notes-5.10.2.adoc[] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M2.adoc new file mode 100644 index 000000000000..fb542107083e --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M2.adoc @@ -0,0 +1,78 @@ +[[release-notes-5.11.0-M2]] +== 5.11.0-M2 + +*Date of Release:* May 17, 2024 + +*Scope:* + +* Repeatable `@..Source` annotations +* Extensible syntax for specifying discovery selectors +* Console Launcher `--version` option +* Improvements to `NamespacedHierarchicalStore` +* Bug fixes + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/74?closed=1+[5.11.0-M2] milestone page in the JUnit +repository on GitHub. + + +[[release-notes-5.11.0-M2-overall-improvements]] +=== Overall Improvements + +[[release-notes-5.11.0-M2-overall-new-features-and-improvements]] +==== New Features and Improvements + +* Java classes in published artifacts are now compiled with the `-parameters` option of + `javac` and thus now contain metadata for reflection on parameters such as their names. + + +[[release-notes-5.11.0-M2-junit-platform]] +=== JUnit Platform + +[[release-notes-5.11.0-M2-junit-platform-bug-fixes]] +==== Bug Fixes + +* Fixed a bug where `TestIdentifier` could cause a `NullPointerException` on deserialize when there is no parent identifier. See link:https://github.com/junit-team/junit5/issues/3819[issue 3819]. + +[[release-notes-5.11.0-M2-junit-platform-new-features-and-improvements]] +==== New Features and Improvements + +* All Platform implementations of `DiscoverySelector` now have a parseable string + representation that can be generated by calling the new + `DiscoverySelector.toIdentifier()` method and `toString()` on the returned + `DiscoverySelectorIdentifier`. This string representation can be used to reconstruct + the original `DiscoverySelector` by calling the new `DiscoverySelectors.parse()` method. + This change will allow build tools and IDEs to provide generic mechanisms for specifying + selectors on the command line or in configuration files without having to support each + selector type individually. + - The Console Launcher supports specifying selectors via their identifiers using the + `--select` option. For example, `--select class:foo.Bar` will run all tests in the + `foo.Bar` class. + - Similarly, the JUnit Platform Suite engine provides a new `@Select(")` + annotation. +* `NamespacedHierarchicalStore` now throws an `IllegalStateException` for any attempt to + modify or query the store after it has been closed. In addition, an attempt to close a + store that has already been closed will have no effect. + - See link:https://github.com/junit-team/junit5/issues/3614[issue 3614] for details. +* The Console Launcher now provides a `--version` option. + +[[release-notes-5.11.0-M2-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.11.0-M2-junit-jupiter-bug-fixes]] +==== Bug Fixes + +* `MethodOrderer.Random` and `ClassOrderer.Random` now use the same default seed that is + generated during class initialization. + +[[release-notes-5.11.0-M2-junit-jupiter-new-features-and-improvements]] +==== New Features and Improvements + +* Support `@..Source` annotations as repeatable for parameterized tests. See the + <<../user-guide/index.adoc#writing-tests-parameterized-repeatable-sources, User Guide>> + for more details. + +[[release-notes-5.11.0-M2-junit-vintage]] +=== JUnit Vintage + +No changes. diff --git a/documentation/src/docs/asciidoc/user-guide/images/writing-tests_execution_mode.svg b/documentation/src/docs/asciidoc/user-guide/images/writing-tests_execution_mode.svg index de0814938cbc..2eb1229ba195 100644 --- a/documentation/src/docs/asciidoc/user-guide/images/writing-tests_execution_mode.svg +++ b/documentation/src/docs/asciidoc/user-guide/images/writing-tests_execution_mode.svg @@ -1,386 +1 @@ -2019-01-012019-01-012019-01-022019-01-022019-01-032019-01-032019-01-042019-01-042019-01-05A.test1() A.test2() B.test1() B.test2() A.test1() A.test2() B.test1() B.test2() A.test1() A.test2() B.test1() B.test2() A.test1() A.test2() B.test1() B.test2() (same_thread, same_thread)(same_thread, concurrent)(concurrent, same_thread)(concurrent, concurrent) \ No newline at end of file +001122334A.test1() A.test1() B.test1() A.test1() A.test2() A.test1() A.test2() B.test1() B.test2() A.test2() A.test2() B.test2() B.test1() B.test2() B.test1() B.test2() (same_thread, same_thread)(same_thread, concurrent)(concurrent, same_thread)(concurrent, concurrent)↓ threads | time → diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index 35aeaab8a962..a9054041df10 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -704,18 +704,21 @@ The `{ConsoleLauncher}` provides the following subcommands: include::{consoleLauncherOptionsFile}[] ---- +[[running-tests-console-launcher-options-discovering-tests]] ===== Discovering tests ---- include::{consoleLauncherDiscoverOptionsFile}[] ---- +[[running-tests-console-launcher-options-executing-tests]] ===== Executing tests ---- include::{consoleLauncherExecuteOptionsFile}[] ---- +[[running-tests-console-launcher-options-listing-test-engines]] ===== Listing test engines ---- @@ -887,6 +890,38 @@ WARNING: Test classes and suites annotated with `@RunWith(JUnitPlatform.class)` documented in some IDEs). Such classes and suites can only be executed using JUnit 4 infrastructure. +[[running-tests-discovery-selectors]] +=== Discovery Selectors + +The JUnit Platform provides a rich set of discovery selectors that can be used to specify +which tests should be discovered or executed. + +Discovery selectors can be created programmatically using the factory methods in the +`{DiscoverySelectors}` class, specified declaratively via annotations when using the +<>, via options of the <>, or +generically as strings via their identifiers. + +The following discovery selectors are provided out of the box: + +|=== +| Java Type | API | Annotation | Console Launcher | Identifier + +| `{ClasspathResourceSelector}` | `{DiscoverySelectors_selectClasspathResource}` | `{SelectClasspathResource}` | `--select-resource /foo.csv` | `resource:/foo.csv` +| `{ClasspathRootSelector}` | `{DiscoverySelectors_selectClasspathRoots}` | -- | `--scan-classpath bin` | `classpath-root:bin` +| `{ClassSelector}` | `{DiscoverySelectors_selectClass}` | `{SelectClasses}` | `--select-class com.acme.Foo` | `class:com.acme.Foo` +| `{DirectorySelector}` | `{DiscoverySelectors_selectDirectory}` | `{SelectDirectories}` | `--select-directory foo/bar` | `directory:foo/bar` +| `{FileSelector}` | `{DiscoverySelectors_selectFile}` | `{SelectFile}` | `--select-file dir/foo.txt` | `file:dir/foo.txt` +| `{IterationSelector}` | `{DiscoverySelectors_selectIteration}` | `{Select}("")` | `--select-iteration method=com.acme.Foo#m[1..2]` | `iteration:method:com.acme.Foo#m[1..2]` +| `{MethodSelector}` | `{DiscoverySelectors_selectMethod}` | `{SelectMethod}` | `--select-method com.acme.Foo#m` | `method:com.acme.Foo#m` +| `{ModuleSelector}` | `{DiscoverySelectors_selectModule}` | `{SelectModules}` | `--select-module com.acme` | `module:com.acme` +| `{NestedClassSelector}` | `{DiscoverySelectors_selectNestedClass}` | `{Select}("")` | `--select ` | `nested-class:com.acme.Foo/Bar` +| `{NestedMethodSelector}` | `{DiscoverySelectors_selectNestedMethod}` | `{Select}("")` | `--select ` | `nested-method:com.acme.Foo/Bar#m` +| `{PackageSelector}` | `{DiscoverySelectors_selectPackage}` | `{SelectPackages}` | `--select-package com.acme.foo` | `package:com.acme.foo` +| `{UniqueIdSelector}` | `{DiscoverySelectors_selectUniqueId}` | `{Select}("")` | `--select ` | `uid:...` +| `{UriSelector}` | `{DiscoverySelectors_selectUri}` | `{SelectUris}` | `--select-uri \file:///foo.txt` | `uri:file:///foo.txt` +|=== + + [[running-tests-config-params]] === Configuration Parameters diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 756722623a2f..ddb0495881cf 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -1713,11 +1713,11 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=multiple_fields_Field ---- It is also possible to provide a `Stream`, `DoubleStream`, `IntStream`, `LongStream`, or -`Iterator` as the source of arguments via a `@FieldSource` field as long as the stream is -wrapped in a `java.util.function.Supplier`. The following example demonstrates how to -provide a `Supplier` of a `Stream` of named arguments. This parameterized test method -will be invoked twice: with the values `"apple"` and `"banana"` and with display names -`Apple` and `Banana`, respectively. +`Iterator` as the source of arguments via a `@FieldSource` field as long as the stream or +iterator is wrapped in a `java.util.function.Supplier`. The following example demonstrates +how to provide a `Supplier` of a `Stream` of named arguments. This parameterized test +method will be invoked twice: with the values `"apple"` and `"banana"` and with display +names `Apple` and `Banana`, respectively. [source,java,indent=0] ---- @@ -1963,6 +1963,34 @@ If you wish to implement a custom `ArgumentsProvider` that also consumes an anno (like built-in providers such as `{ValueArgumentsProvider}` or `{CsvArgumentsProvider}`), you have the possibility to extend the `{AnnotationBasedArgumentsProvider}` class. +[[writing-tests-parameterized-repeatable-sources]] +===== Multiple sources using repeatable annotations +Repeatable annotations provide a convenient way to specify multiple sources from +different providers. + +[source,java,indent=0] +---- +include::{testDir}/example/ParameterizedTestDemo.java[tags=repeatable_annotations] +---- + +Following the above parameterized test, a test case will run for each argument: + +---- +[1] foo +[2] bar +---- + +The following annotations are repeatable: + +* `@ValueSource` +* `@EnumSource` +* `@MethodSource` +* `@FieldSource` +* `@CsvSource` +* `@CsvFileSource` +* `@ArgumentsSource` + + [[writing-tests-parameterized-tests-argument-conversion]] ==== Argument Conversion @@ -2401,8 +2429,8 @@ implementations. `MethodSource` :: If the `URI` contains the `method` scheme and the fully qualified method name (FQMN) -- for example, `method:org.junit.Foo#bar(java.lang.String, java.lang.String[])`. Please - refer to the Javadoc for `DiscoverySelectors.selectMethod(String)` for the supported - formats for a FQMN. + refer to the Javadoc for `{DiscoverySelectors}.{DiscoverySelectors_selectMethod}` for the + supported formats for a FQMN. `ClassSource` :: If the `URI` contains the `class` scheme and the fully qualified class name -- @@ -2642,34 +2670,41 @@ The following diagram illustrates how the execution of two top-level test classe `junit.jupiter.execution.parallel.mode.classes.default` (see labels in first column). //// -Source: https://mermaidjs.github.io/mermaid-live-editor/#/view/eyJjb2RlIjoiZ2FudHRcbiAgICBkYXRlRm9ybWF0ICBZWVlZLU1NLUREXG5cbiAgICBzZWN0aW9uIChzYW1lX3RocmVhZCwgc2FtZV90aHJlYWQpXG4gICAgQS50ZXN0MSgpIDphc3MxLCAyMDE5LTAxLTAxLCAxZFxuICAgIEEudGVzdDIoKSA6YXNzMiwgYWZ0ZXIgYXNzMSwgMWRcbiAgICBCLnRlc3QxKCkgOmJzczEsIGFmdGVyIGFzczIsIDFkXG4gICAgQi50ZXN0MigpIDpic3MyLCBhZnRlciBic3MxLCAxZFxuXG4gICAgc2VjdGlvbiAoc2FtZV90aHJlYWQsIGNvbmN1cnJlbnQpXG4gICAgQS50ZXN0MSgpIDphc2MxLCAyMDE5LTAxLTAxLCAxZFxuICAgIEEudGVzdDIoKSA6YXNjMiwgYWZ0ZXIgYXNjMSwgMWRcbiAgICBCLnRlc3QxKCkgOmJzYzEsIDIwMTktMDEtMDEsIDFkXG4gICAgQi50ZXN0MigpIDpic2MyLCBhZnRlciBic2MxLCAxZFxuXG4gICAgc2VjdGlvbiAoY29uY3VycmVudCwgc2FtZV90aHJlYWQpXG4gICAgQS50ZXN0MSgpIDphY3MxLCAyMDE5LTAxLTAxLCAxZFxuICAgIEEudGVzdDIoKSA6YWNzMiwgMjAxOS0wMS0wMSwgMWRcbiAgICBCLnRlc3QxKCkgOmJjczEsIGFmdGVyIGFjczIsIDFkXG4gICAgQi50ZXN0MigpIDpiY3MyLCBhZnRlciBhY3MyLCAxZFxuXG4gICAgc2VjdGlvbiAoY29uY3VycmVudCwgY29uY3VycmVudClcbiAgICBBLnRlc3QxKCkgOmFjYzEsIDIwMTktMDEtMDEsIDFkXG4gICAgQS50ZXN0MigpIDphY2MyLCAyMDE5LTAxLTAxLCAxZFxuICAgIEIudGVzdDEoKSA6YmNjMSwgMjAxOS0wMS0wMSwgMWRcbiAgICBCLnRlc3QyKCkgOmJjYzIsIDIwMTktMDEtMDEsIDFkXG4iLCJtZXJtYWlkIjp7InRoZW1lIjoibmV1dHJhbCIsImdhbnR0Ijp7ImxlZnRQYWRkaW5nIjoyMjUsImJhckdhcCI6NSwiZ3JpZExpbmVTdGFydFBhZGRpbmciOjEwLCJiYXJIZWlnaHQiOjMwLCJmb250U2l6ZSI6MTV9LCJ0aGVtZUNTUyI6Ii50YXNrVGV4dCwgLnNlY3Rpb25UaXRsZSB7IGZvbnQtZmFtaWx5OiAnT3BlbiBTYW5zJzsgZm9udC1zaXplOjE1cHggfSAuZ3JpZCAudGljayB0ZXh0IHsgZGlzcGxheTpub25lIH0gLmdyaWQgLnRpY2s6bnRoLWNoaWxkKDJuKzEpIHsgZGlzcGxheTpub25lIH0ifX0 +Source: https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFlE1u2zAQha9CEChio7IQKfVGXfUH_QEatICyKAIBwYQaW0QkUiDHhV3X2x4gvWFPUlKUbTmpEq2kN2-GHx403HKhS-QZn81mhSqlbWvYXDopY0I3LQgqVFcq1BIUuS_mnhIIP2jTALHvQYG1tL3ywgaJpLj7rAjND6hZsteoRvb39x9GlUEoLfvltMZL9_4M77EoSGrFJhYavAm-iA0-psH3Jia0lEymLANrk4idR_tjQintS2nEYOE4WLClwfP22H7b6QeP818MPWnvOcwJ_ldPAwutxMoYVPQ_XjHOKwa8YoT3tP0EUwww-_YHmEey52IV47EKH8dDhEAnBmmKR4mnvScdeNLnMJ8MU4yHKcQ45XiGgy4e8Qbdby1LtyNbby04VdhgwTP3qnBFBuqCR6EUdsSVtmFqwWtc0DcoS6mWXk_TebQv3YL5CK1Xk_ODuDSy_CIV5gRm2DiwuL5PKJdVd9DFUV9oRbn82aElc6_uogHxuzwP0DGBvbvCtcs17tO-6vZyy_yI2QIaWW8ydva1RcVyUPbsdahYNz1L5u2a7VjsSVnst5yRG-a6--sjU1rhqSNTVM1EJetykqqXyfSRueCF2rmwYUU63yjBMzIrjPiq9XfNewlLAw3PFlBbp2IpSZvLcHN1F1jEW1DXWu89u3-YPX1X + +--- +displayMode: compact +--- gantt - dateFormat YYYY-MM-DD + dateFormat X + axisFormat %s + tickInterval 1 + title ↓ threads | time → section (same_thread, same_thread) - A.test1() :ass1, 2019-01-01, 1d - A.test2() :ass2, after ass1, 1d - B.test1() :bss1, after ass2, 1d - B.test2() :bss2, after bss1, 1d + A.test1() :ass1, 0, 1 + A.test2() :ass2, after ass1, 2 + B.test1() :bss1, after ass2, 3 + B.test2() :bss2, after bss1, 4 section (same_thread, concurrent) - A.test1() :asc1, 2019-01-01, 1d - A.test2() :asc2, after asc1, 1d - B.test1() :bsc1, 2019-01-01, 1d - B.test2() :bsc2, after bsc1, 1d + A.test1() :asc1, 0, 1 + A.test2() :asc2, after asc1, 2 + B.test1() :bsc1, 0, 1 + B.test2() :bsc2, after bsc1, 2 section (concurrent, same_thread) - A.test1() :acs1, 2019-01-01, 1d - A.test2() :acs2, 2019-01-01, 1d - B.test1() :bcs1, after acs2, 1d - B.test2() :bcs2, after acs2, 1d + A.test1() :acs1, 0, 1 + A.test2() :acs2, 0, 1 + B.test1() :bcs1, after acs1, 2 + B.test2() :bcs2, after acs2, 2 section (concurrent, concurrent) - A.test1() :acc1, 2019-01-01, 1d - A.test2() :acc2, 2019-01-01, 1d - B.test1() :bcc1, 2019-01-01, 1d - B.test2() :bcc2, 2019-01-01, 1d + A.test1() :acc1, 0, 1 + A.test2() :acc2, 0, 1 + B.test1() :bcc1, 0, 1 + B.test2() :bcc2, 0, 1 //// image::writing-tests_execution_mode.svg[caption='',title='Default execution mode configuration combinations'] diff --git a/documentation/src/test/java/example/ParameterizedTestDemo.java b/documentation/src/test/java/example/ParameterizedTestDemo.java index 0ca33aec886d..1b94e053519a 100644 --- a/documentation/src/test/java/example/ParameterizedTestDemo.java +++ b/documentation/src/test/java/example/ParameterizedTestDemo.java @@ -542,4 +542,22 @@ static Stream namedArguments() { } // end::named_arguments[] // @formatter:on + + // tag::repeatable_annotations[] + @DisplayName("A parameterized test that makes use of repeatable annotations") + @ParameterizedTest + @MethodSource("someProvider") + @MethodSource("otherProvider") + void testWithRepeatedAnnotation(String argument) { + assertNotNull(argument); + } + + static Stream someProvider() { + return Stream.of("foo"); + } + + static Stream otherProvider() { + return Stream.of("bar"); + } + // end::repeatable_annotations[] } diff --git a/gradle.properties b/gradle.properties index 6da2d0da332f..617c9a5c5a0a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,13 @@ group = org.junit -version = 5.11.0-M1 +version = 5.11.0-M2 jupiterGroup = org.junit.jupiter platformGroup = org.junit.platform -platformVersion = 1.11.0-M1 +platformVersion = 1.11.0-M2 vintageGroup = org.junit.vintage -vintageVersion = 5.11.0-M1 +vintageVersion = 5.11.0-M2 # We need more metaspace due to apparent memory leak in Asciidoctor/JRuby # The exports are needed due to https://github.com/diffplug/spotless/issues/834 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ffdfd1373b7..8281124a42c8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ asciidoctorj-pdf = "2.3.15" asciidoctor-plugins = "4.0.2" # Check if workaround in documentation.gradle.kts can be removed when upgrading assertj = "3.25.3" bnd = "7.0.0" -checkstyle = "10.15.0" +checkstyle = "10.16.0" eclipse = "4.31.100" gradleVersionsPlugin = "0.51.0" jacoco = "0.8.7" @@ -18,7 +18,7 @@ log4j = "2.23.1" opentest4j = "1.3.0" openTestReporting = "0.1.0-M2" surefire = "3.2.5" -xmlunit = "2.9.1" +xmlunit = "2.10.0" [libraries] ant = { module = "org.apache.ant:ant", version.ref = "ant" } @@ -34,7 +34,7 @@ classgraph = { module = "io.github.classgraph:classgraph", version = "4.8.172" } commons-io = { module = "commons-io:commons-io", version = "2.16.1" } gradle-commonCustomUserData = { module = "com.gradle:common-custom-user-data-gradle-plugin", version = "2.0.1" } gradle-foojayResolver = { module = "org.gradle.toolchains:foojay-resolver", version = "0.8.0" } -gradle-develocity = { module = "com.gradle:develocity-gradle-plugin", version = "3.17.2" } +gradle-develocity = { module = "com.gradle:develocity-gradle-plugin", version = "3.17.3" } gradle-bnd = { module = "biz.aQute.bnd:biz.aQute.bnd.gradle", version.ref = "bnd" } gradle-shadow = { module = "com.github.johnrengelman:shadow", version = "8.1.1" } gradle-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "6.25.0" } @@ -47,19 +47,19 @@ jfrunit = { module = "org.moditect.jfrunit:jfrunit-core", version = "1.0.0.Alpha jimfs = { module = "com.google.jimfs:jimfs", version = "1.3.0" } jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" } jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" } -joox = { module = "org.jooq:joox", version = "2.0.0" } +joox = { module = "org.jooq:joox", version = "2.0.1" } junit4 = { module = "junit:junit", version = { require = "[4.12,)", prefer = "4.13.2" } } -kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.8.0" } +kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.8.1" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } maven = { module = "org.apache.maven:apache-maven", version = "3.9.6" } mavenSurefirePlugin = { module = "org.apache.maven.plugins:maven-surefire-plugin", version.ref = "surefire" } memoryfilesystem = { module = "com.github.marschall:memoryfilesystem", version = "2.8.0" } -mockito = { module = "org.mockito:mockito-junit-jupiter", version = "5.11.0" } +mockito = { module = "org.mockito:mockito-junit-jupiter", version = "5.12.0" } opentest4j = { module = "org.opentest4j:opentest4j", version.ref = "opentest4j" } openTestReporting-events = { module = "org.opentest4j.reporting:open-test-reporting-events", version.ref = "openTestReporting" } openTestReporting-tooling = { module = "org.opentest4j.reporting:open-test-reporting-tooling", version.ref = "openTestReporting" } -picocli = { module = "info.picocli:picocli", version = "4.7.5" } +picocli = { module = "info.picocli:picocli", version = "4.7.6" } slf4j-julBinding = { module = "org.slf4j:slf4j-jdk14", version = "2.0.13" } spock1 = { module = "org.spockframework:spock-core", version = "1.3-groovy-2.5" } univocity-parsers = { module = "com.univocity:univocity-parsers", version = "2.9.1" } diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts index 94006711ab87..2f0eb40780b8 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts +++ b/gradle/plugins/common/src/main/kotlin/junitbuild.java-library-conventions.gradle.kts @@ -238,7 +238,9 @@ tasks.compileJava { // See: https://docs.oracle.com/en/java/javase/12/tools/javac.html options.compilerArgs.addAll(listOf( "-Xlint:all", // Enables all recommended warnings. - "-Werror" // Terminates compilation when warnings occur. + "-Werror", // Terminates compilation when warnings occur. + // Required for compatibility with Java 8's reflection APIs (see https://github.com/junit-team/junit5/issues/3797). + "-parameters", // Generates metadata for reflection on method parameters. )) } diff --git a/gradle/plugins/common/src/main/kotlin/junitbuild/java/UpdateJarAction.kt b/gradle/plugins/common/src/main/kotlin/junitbuild/java/UpdateJarAction.kt index cbdb0e656cd7..ada76dd250ca 100644 --- a/gradle/plugins/common/src/main/kotlin/junitbuild/java/UpdateJarAction.kt +++ b/gradle/plugins/common/src/main/kotlin/junitbuild/java/UpdateJarAction.kt @@ -8,10 +8,22 @@ import org.gradle.api.provider.Property import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.process.ExecOperations import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset import javax.inject.Inject abstract class UpdateJarAction @Inject constructor(private val operations: ExecOperations): Action { + companion object { + // Since ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES is in the default time zone (see its Javadoc), + // we're converting it to the same time in UTC here to make the jar reproducible regardless of the + // build's time zone. + private val CONSTANT_TIME_FOR_ZIP_ENTRIES = LocalDateTime.ofInstant(Instant.ofEpochMilli(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES), ZoneId.systemDefault()) + .toInstant(ZoneOffset.UTC) + .toString() + } + abstract val javaLauncher: Property abstract val args: ListProperty @@ -19,7 +31,8 @@ abstract class UpdateJarAction @Inject constructor(private val operations: ExecO init { args.convention(listOf( "--update", - "--date=${Instant.ofEpochMilli(ZipCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES)}", + // Use a constant time to make the JAR reproducible. + "--date=$CONSTANT_TIME_FOR_ZIP_ENTRIES", )) } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java index 109efd1cc912..3f5df08971a0 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java @@ -3113,11 +3113,16 @@ public static T assertThrowsExactly(Class expectedType, * Assert that execution of the supplied {@code executable} throws * an exception of the {@code expectedType} and return the exception. * - *

If no exception is thrown, or if an exception of a different type is - * thrown, this method will fail. + *

The assertion passes if the thrown exception type is the same as + * {@code expectedType} or a subtype thereof. To check for the exact thrown + * type use {@link #assertThrowsExactly(Class, Executable) assertThrowsExactly}. + * If no exception is thrown, or if an exception of a different type is thrown, + * this method will fail. * *

If you do not want to perform additional checks on the exception instance, * ignore the return value. + * + * @see #assertThrowsExactly(Class, Executable) */ public static T assertThrows(Class expectedType, Executable executable) { return AssertThrows.assertThrows(expectedType, executable); @@ -3127,8 +3132,11 @@ public static T assertThrows(Class expectedType, Execut * Assert that execution of the supplied {@code executable} throws * an exception of the {@code expectedType} and return the exception. * - *

If no exception is thrown, or if an exception of a different type is - * thrown, this method will fail. + *

The assertion passes if the thrown exception type is the same as + * {@code expectedType} or a subtype thereof. To check for the exact thrown + * type use {@link #assertThrowsExactly(Class, Executable, String) assertThrowsExactly}. + * If no exception is thrown, or if an exception of a different type is thrown, + * this method will fail. * *

If you do not want to perform additional checks on the exception instance, * ignore the return value. @@ -3138,6 +3146,8 @@ public static T assertThrows(Class expectedType, Execut * exception. To assert the expected message of the thrown exception, you must * use a separate, subsequent assertion against the exception returned from * this method. + * + * @see #assertThrowsExactly(Class, Executable, String) */ public static T assertThrows(Class expectedType, Executable executable, String message) { return AssertThrows.assertThrows(expectedType, executable, message); @@ -3147,8 +3157,11 @@ public static T assertThrows(Class expectedType, Execut * Assert that execution of the supplied {@code executable} throws * an exception of the {@code expectedType} and return the exception. * - *

If no exception is thrown, or if an exception of a different type is - * thrown, this method will fail. + *

The assertion passes if the thrown exception type is the same as + * {@code expectedType} or a subtype thereof. To check for the exact thrown + * type use {@link #assertThrowsExactly(Class, Executable, Supplier) assertThrowsExactly}. + * If no exception is thrown, or if an exception of a different type is thrown, + * this method will fail. * *

If necessary, the failure message will be retrieved lazily from the * supplied {@code messageSupplier}. Note that the failure message is @@ -3159,6 +3172,8 @@ public static T assertThrows(Class expectedType, Execut * *

If you do not want to perform additional checks on the exception instance, * ignore the return value. + * + * @see #assertThrowsExactly(Class, Executable, Supplier) */ public static T assertThrows(Class expectedType, Executable executable, Supplier messageSupplier) { diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ClassOrderer.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ClassOrderer.java index 10c232ea8163..e27c05abb926 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ClassOrderer.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/ClassOrderer.java @@ -15,7 +15,6 @@ import java.util.Collections; import java.util.Comparator; -import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.logging.Logger; @@ -185,8 +184,8 @@ private static int getOrder(ClassDescriptor descriptor) { *

Custom Seed

* *

By default, the random seed used for ordering classes is the - * value returned by {@link System#nanoTime()} during static initialization - * of this class. In order to support repeatable builds, the value of the + * value returned by {@link System#nanoTime()} during static class + * initialization. In order to support repeatable builds, the value of the * default random seed is logged at {@code CONFIG} level. In addition, a * custom seed (potentially the default seed from the previous test plan * execution) may be specified via the {@value Random#RANDOM_SEED_PROPERTY_NAME} @@ -202,15 +201,8 @@ class Random implements ClassOrderer { private static final Logger logger = LoggerFactory.getLogger(Random.class); - /** - * Default seed, which is generated during initialization of this class - * via {@link System#nanoTime()} for reproducibility of tests. - */ - private static final long DEFAULT_SEED; - static { - DEFAULT_SEED = System.nanoTime(); - logger.config(() -> "ClassOrderer.Random default seed: " + DEFAULT_SEED); + logger.config(() -> "ClassOrderer.Random default seed: " + RandomOrdererUtils.DEFAULT_SEED); } /** @@ -231,7 +223,7 @@ class Random implements ClassOrderer { * * @see MethodOrderer.Random */ - public static final String RANDOM_SEED_PROPERTY_NAME = MethodOrderer.Random.RANDOM_SEED_PROPERTY_NAME; + public static final String RANDOM_SEED_PROPERTY_NAME = RandomOrdererUtils.RANDOM_SEED_PROPERTY_NAME; public Random() { } @@ -243,27 +235,7 @@ public Random() { @Override public void orderClasses(ClassOrdererContext context) { Collections.shuffle(context.getClassDescriptors(), - new java.util.Random(getCustomSeed(context).orElse(DEFAULT_SEED))); - } - - private Optional getCustomSeed(ClassOrdererContext context) { - return context.getConfigurationParameter(RANDOM_SEED_PROPERTY_NAME).map(configurationParameter -> { - Long seed = null; - try { - seed = Long.valueOf(configurationParameter); - logger.config( - () -> String.format("Using custom seed for configuration parameter [%s] with value [%s].", - RANDOM_SEED_PROPERTY_NAME, configurationParameter)); - } - catch (NumberFormatException ex) { - logger.warn(ex, - () -> String.format( - "Failed to convert configuration parameter [%s] with value [%s] to a long. " - + "Using default seed [%s] as fallback.", - RANDOM_SEED_PROPERTY_NAME, configurationParameter, DEFAULT_SEED)); - } - return seed; - }); + new java.util.Random(RandomOrdererUtils.getSeed(context::getConfigurationParameter, logger))); } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/MethodOrderer.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/MethodOrderer.java index c84f87707335..4a71d5944551 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/MethodOrderer.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/MethodOrderer.java @@ -248,11 +248,11 @@ private static int getOrder(MethodDescriptor descriptor) { *

Custom Seed

* *

By default, the random seed used for ordering methods is the - * value returned by {@link System#nanoTime()} during static initialization - * of this class. In order to support repeatable builds, the value of the + * value returned by {@link System#nanoTime()} during static class + * initialization. In order to support repeatable builds, the value of the * default random seed is logged at {@code CONFIG} level. In addition, a * custom seed (potentially the default seed from the previous test plan - * execution) may be specified via the {@value ClassOrderer.Random#RANDOM_SEED_PROPERTY_NAME} + * execution) may be specified via the {@value Random#RANDOM_SEED_PROPERTY_NAME} * configuration parameter which can be supplied via the {@code Launcher} * API, build tools (e.g., Gradle and Maven), a JVM system property, or the JUnit * Platform configuration file (i.e., a file named {@code junit-platform.properties} @@ -265,15 +265,8 @@ class Random implements MethodOrderer { private static final Logger logger = LoggerFactory.getLogger(Random.class); - /** - * Default seed, which is generated during initialization of this class - * via {@link System#nanoTime()} for reproducibility of tests. - */ - private static final long DEFAULT_SEED; - static { - DEFAULT_SEED = System.nanoTime(); - logger.config(() -> "MethodOrderer.Random default seed: " + DEFAULT_SEED); + logger.config(() -> "MethodOrderer.Random default seed: " + RandomOrdererUtils.DEFAULT_SEED); } /** @@ -294,7 +287,7 @@ class Random implements MethodOrderer { * * @see ClassOrderer.Random */ - public static final String RANDOM_SEED_PROPERTY_NAME = "junit.jupiter.execution.order.random.seed"; + public static final String RANDOM_SEED_PROPERTY_NAME = RandomOrdererUtils.RANDOM_SEED_PROPERTY_NAME; public Random() { } @@ -306,28 +299,9 @@ public Random() { @Override public void orderMethods(MethodOrdererContext context) { Collections.shuffle(context.getMethodDescriptors(), - new java.util.Random(getCustomSeed(context).orElse(DEFAULT_SEED))); + new java.util.Random(RandomOrdererUtils.getSeed(context::getConfigurationParameter, logger))); } - private Optional getCustomSeed(MethodOrdererContext context) { - return context.getConfigurationParameter(RANDOM_SEED_PROPERTY_NAME).map(configurationParameter -> { - Long seed = null; - try { - seed = Long.valueOf(configurationParameter); - logger.config( - () -> String.format("Using custom seed for configuration parameter [%s] with value [%s].", - RANDOM_SEED_PROPERTY_NAME, configurationParameter)); - } - catch (NumberFormatException ex) { - logger.warn(ex, - () -> String.format( - "Failed to convert configuration parameter [%s] with value [%s] to a long. " - + "Using default seed [%s] as fallback.", - RANDOM_SEED_PROPERTY_NAME, configurationParameter, DEFAULT_SEED)); - } - return seed; - }); - } } } diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/RandomOrdererUtils.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/RandomOrdererUtils.java new file mode 100644 index 000000000000..35b5dc6208ae --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/RandomOrdererUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2024 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.jupiter.api; + +import java.util.Optional; +import java.util.function.Function; + +import org.junit.platform.commons.logging.Logger; + +/** + * Shared utility methods for ordering test classes and test methods randomly. + * + * @since 5.11 + * @see ClassOrderer.Random + * @see MethodOrderer.Random + */ +class RandomOrdererUtils { + + static final String RANDOM_SEED_PROPERTY_NAME = "junit.jupiter.execution.order.random.seed"; + + static final long DEFAULT_SEED = System.nanoTime(); + + static Long getSeed(Function> configurationParameterLookup, Logger logger) { + return getCustomSeed(configurationParameterLookup, logger).orElse(DEFAULT_SEED); + } + + private static Optional getCustomSeed(Function> configurationParameterLookup, + Logger logger) { + return configurationParameterLookup.apply(RANDOM_SEED_PROPERTY_NAME).map(configurationParameter -> { + try { + logger.config(() -> String.format("Using custom seed for configuration parameter [%s] with value [%s].", + RANDOM_SEED_PROPERTY_NAME, configurationParameter)); + return Long.valueOf(configurationParameter); + } + catch (NumberFormatException ex) { + logger.warn(ex, + () -> String.format( + "Failed to convert configuration parameter [%s] with value [%s] to a long. " + + "Using default seed [%s] as fallback.", + RANDOM_SEED_PROPERTY_NAME, configurationParameter, DEFAULT_SEED)); + return null; + } + }); + } +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDirFactory.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDirFactory.java index c9245a0d36b9..949167f9ef13 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDirFactory.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDirFactory.java @@ -81,7 +81,7 @@ class Standard implements TempDirFactory { public static final TempDirFactory INSTANCE = new Standard(); - private static final String TEMP_DIR_PREFIX = "junit"; + private static final String TEMP_DIR_PREFIX = "junit-"; public Standard() { } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java index 888540a1e0cc..28f76b92e30b 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java @@ -74,7 +74,7 @@ abstract class AbstractExtensionContext implements Ext // @formatter:on } - private NamespacedHierarchicalStore createStore(ExtensionContext parent) { + private static NamespacedHierarchicalStore createStore(ExtensionContext parent) { NamespacedHierarchicalStore parentStore = null; if (parent != null) { parentStore = ((AbstractExtensionContext) parent).valuesStore; diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/RandomlyOrderedTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/RandomlyOrderedTests.java similarity index 84% rename from junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/RandomlyOrderedTests.java rename to junit-jupiter-engine/src/test/java/org/junit/jupiter/api/RandomlyOrderedTests.java index 8c75b56d01a5..f43b586c03de 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/RandomlyOrderedTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/api/RandomlyOrderedTests.java @@ -8,7 +8,7 @@ * https://www.eclipse.org/legal/epl-v20.html */ -package org.junit.jupiter.engine.extension; +package org.junit.jupiter.api; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME; @@ -20,11 +20,6 @@ import java.util.Set; import java.util.stream.IntStream; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.ClassOrderer; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInfo; import org.junit.platform.testkit.engine.EngineTestKit; import org.junit.platform.testkit.engine.Events; @@ -39,7 +34,7 @@ class RandomlyOrderedTests { void randomSeedForClassAndMethodOrderingIsDeterministic() { IntStream.range(0, 20).forEach(i -> { callSequence.clear(); - var tests = executeTests(1618034L); + var tests = executeTests(1618034); tests.assertStatistics(stats -> stats.succeeded(callSequence.size())); assertThat(callSequence).containsExactlyInAnyOrder("B_TestCase#b", "B_TestCase#c", "B_TestCase#a", @@ -47,7 +42,7 @@ void randomSeedForClassAndMethodOrderingIsDeterministic() { }); } - private Events executeTests(long randomSeed) { + private Events executeTests(@SuppressWarnings("SameParameterValue") long randomSeed) { // @formatter:off return EngineTestKit .engine("junit-jupiter") @@ -64,8 +59,8 @@ abstract static class BaseTestCase { @BeforeEach void trackInvocations(TestInfo testInfo) { - var testClass = testInfo.getTestClass().get(); - var testMethod = testInfo.getTestMethod().get(); + var testClass = testInfo.getTestClass().orElseThrow(); + var testMethod = testInfo.getTestMethod().orElseThrow(); callSequence.add(testClass.getSimpleName() + "#" + testMethod.getName()); } @@ -83,12 +78,15 @@ void c() { } } + @SuppressWarnings("NewClassNamingConvention") static class A_TestCase extends BaseTestCase { } + @SuppressWarnings("NewClassNamingConvention") static class B_TestCase extends BaseTestCase { } + @SuppressWarnings("NewClassNamingConvention") static class C_TestCase extends BaseTestCase { } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerContextTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerContextTests.java index 579004b043dd..49b28d2dcc3a 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerContextTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryPerContextTests.java @@ -697,7 +697,7 @@ private static class Factory implements TempDirFactory { @Override public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext) throws Exception { - return Files.createTempDirectory("junit"); + return Files.createTempDirectory("junit-"); } } diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java index ac4fd760523b..3b1d26564592 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTest.java @@ -196,9 +196,10 @@ * The display name to be used for individual invocations of the * parameterized test; never blank or consisting solely of whitespace. * - *

Defaults to {default_display_name}. + *

Defaults to {@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME}. * - *

If the default display name flag ({default_display_name}) + *

If the default display name flag + * ({@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME}) * is not overridden, JUnit will: *

    *
  • Look up the {@value ParameterizedTestExtension#DISPLAY_NAME_PATTERN_KEY} @@ -207,30 +208,30 @@ * Gradle and Maven), a JVM system property, or the JUnit Platform configuration * file (i.e., a file named {@code junit-platform.properties} in the root of * the class path). Consult the User Guide for further information.
  • - *
  • Otherwise, the value of the {@link #DEFAULT_DISPLAY_NAME} constant will - * be used.
  • + *
  • Otherwise, {@value #DEFAULT_DISPLAY_NAME} will be used.
  • *
* *

Supported placeholders

*
    - *
  • {@link #DISPLAY_NAME_PLACEHOLDER}
  • - *
  • {@link #INDEX_PLACEHOLDER}
  • - *
  • {@link #ARGUMENTS_PLACEHOLDER}
  • - *
  • {@link #ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • - *
  • {0}, {1}, etc.: an individual argument (0-based)
  • + *
  • {@value #DISPLAY_NAME_PLACEHOLDER}
  • + *
  • {@value #INDEX_PLACEHOLDER}
  • + *
  • {@value #ARGUMENTS_PLACEHOLDER}
  • + *
  • {@value #ARGUMENTS_WITH_NAMES_PLACEHOLDER}
  • + *
  • "{0}", "{1}", etc.: an individual argument (0-based)
  • *
* *

For the latter, you may use {@link java.text.MessageFormat} patterns - * to customize formatting. Please note that the original arguments are - * passed when formatting, regardless of any implicit or explicit argument - * conversions. + * to customize formatting (for example, {@code {0,number,#.###}}). Please + * note that the original arguments are passed when formatting, regardless + * of any implicit or explicit argument conversions. * - *

Note that {default_display_name} is a flag rather than a - * placeholder. + *

Note that + * {@value ParameterizedTestExtension#DEFAULT_DISPLAY_NAME} is + * a flag rather than a placeholder. * * @see java.text.MessageFormat */ - String name() default "{default_display_name}"; + String name() default ParameterizedTestExtension.DEFAULT_DISPLAY_NAME; /** * Configure whether all arguments of the parameterized test that implement {@link AutoCloseable} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java index dc34c6a62ee5..c329300d4c61 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedTestExtension.java @@ -39,7 +39,7 @@ class ParameterizedTestExtension implements TestTemplateInvocationContextProvide private static final String METHOD_CONTEXT_KEY = "context"; static final String ARGUMENT_MAX_LENGTH_KEY = "junit.jupiter.params.displayname.argument.maxlength"; - private static final String DEFAULT_DISPLAY_NAME = "{default_display_name}"; + static final String DEFAULT_DISPLAY_NAME = "{default_display_name}"; static final String DISPLAY_NAME_PATTERN_KEY = "junit.jupiter.params.displayname.default"; @Override diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java index 0b950ef71d8f..f751b35e3c5c 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProvider.java @@ -13,6 +13,8 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import org.apiguardian.api.API; @@ -39,17 +41,17 @@ public abstract class AnnotationBasedArgumentsProvider public AnnotationBasedArgumentsProvider() { } - private A annotation; + private final List annotations = new ArrayList<>(); @Override public final void accept(A annotation) { Preconditions.notNull(annotation, "annotation must not be null"); - this.annotation = annotation; + annotations.add(annotation); } @Override public final Stream provideArguments(ExtensionContext context) { - return provideArguments(context, this.annotation); + return annotations.stream().flatMap(annotation -> provideArguments(context, annotation)); } /** diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java index 6017132348d5..1798dfc171b3 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -21,9 +22,9 @@ import org.apiguardian.api.API; /** - * {@code @CsvFileSource} is an {@link ArgumentsSource} which is used to load - * comma-separated value (CSV) files from one or more classpath {@link #resources} - * or {@link #files}. + * {@code @CsvFileSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which is used to load comma-separated value (CSV) + * files from one or more classpath {@link #resources} or {@link #files}. * *

The CSV records parsed from these resources and files will be provided as * arguments to the annotated {@code @ParameterizedTest} method. Note that the @@ -63,6 +64,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(CsvFileSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(CsvFileArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java new file mode 100644 index 000000000000..bc6bf3503fc9 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSources.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2024 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.jupiter.params.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @CsvFileSources} is a simple container for one or more + * {@link CsvFileSource} annotations. + * + *

Note, however, that use of the {@code @CsvFileSources} container is completely + * optional since {@code @CsvFileSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see CsvFileSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface CsvFileSources { + + /** + * An array of one or more {@link CsvFileSource @CsvFileSource} + * annotations. + */ + CsvFileSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java index ecf3ca0848ee..ef09eea27ba6 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -21,9 +22,10 @@ import org.apiguardian.api.API; /** - * {@code @CsvSource} is an {@link ArgumentsSource} which reads comma-separated - * values (CSV) from one or more CSV records supplied via the {@link #value} - * attribute or {@link #textBlock} attribute. + * {@code @CsvSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which reads comma-separated values (CSV) from one + * or more CSV records supplied via the {@link #value} attribute or + * {@link #textBlock} attribute. * *

The supplied values will be provided as arguments to the annotated * {@code @ParameterizedTest} method. @@ -64,6 +66,7 @@ */ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) +@Repeatable(CsvSources.class) @Documented @API(status = STABLE, since = "5.7") @ArgumentsSource(CsvArgumentsProvider.class) diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java new file mode 100644 index 000000000000..6c6951a75beb --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvSources.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2024 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.jupiter.params.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @CsvSources} is a simple container for one or more + * {@link CsvSource} annotations. + * + *

Note, however, that use of the {@code @CsvSources} container is completely + * optional since {@code @CsvSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see CsvSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface CsvSources { + + /** + * An array of one or more {@link CsvSource @CsvSource} + * annotations. + */ + CsvSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java index ab05a56cf8ac..3bf7e9b88e5e 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSource.java @@ -16,6 +16,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -29,8 +30,8 @@ import org.junit.platform.commons.util.Preconditions; /** - * {@code @EnumSource} is an {@link ArgumentsSource} for constants of - * an {@link Enum}. + * {@code @EnumSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} for constants of an {@link Enum}. * *

The enum constants will be provided as arguments to the annotated * {@code @ParameterizedTest} method. @@ -49,6 +50,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(EnumSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(EnumArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java new file mode 100644 index 000000000000..22feb5aa46d6 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/EnumSources.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2024 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.jupiter.params.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @EnumSources} is a simple container for one or more + * {@link EnumSource} annotations. + * + *

Note, however, that use of the {@code @EnumSources} container is completely + * optional since {@code @EnumSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see EnumSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface EnumSources { + + /** + * An array of one or more {@link EnumSource @EnumSource} + * annotations. + */ + EnumSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java index 1d3198ceb529..77680a00b7d1 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -22,10 +23,11 @@ import org.junit.jupiter.params.ParameterizedTest; /** - * {@code @FieldSource} is an {@link ArgumentsSource} which provides access to - * values of {@linkplain #value() fields} of the class in which this annotation - * is declared or from static fields in external classes referenced by - * fully qualified field name. + * {@code @FieldSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which provides access to values of + * {@linkplain #value() fields} of the class in which this annotation is declared + * or from static fields in external classes referenced by fully qualified + * field name. * *

Each field must be able to supply a stream of arguments, * and each set of "arguments" within the "stream" will be provided as the physical @@ -112,6 +114,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(FieldSources.class) @API(status = EXPERIMENTAL, since = "5.11") @ArgumentsSource(FieldArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java new file mode 100644 index 000000000000..0b46746db5e4 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/FieldSources.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2024 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.jupiter.params.provider; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @FieldSources} is a simple container for one or more + * {@link FieldSource} annotations. + * + *

Note, however, that use of the {@code @FieldSources} container is completely + * optional since {@code @FieldSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see FieldSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = EXPERIMENTAL, since = "5.11") +public @interface FieldSources { + + /** + * An array of one or more {@link FieldSource @FieldSource} + * annotations. + */ + FieldSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java index 72404ee1063e..977e7555a5d2 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -22,10 +23,11 @@ import org.junit.jupiter.params.ParameterizedTest; /** - * {@code @MethodSource} is an {@link ArgumentsSource} which provides access - * to values returned from {@linkplain #value() factory methods} of the class in - * which this annotation is declared or from static factory methods in external - * classes referenced by fully qualified method name. + * {@code @MethodSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which provides access to values returned from + * {@linkplain #value() factory methods} of the class in which this annotation + * is declared or from static factory methods in external classes referenced + * by fully qualified method name. * *

Each factory method must generate a stream of arguments, * and each set of "arguments" within the "stream" will be provided as the physical @@ -103,6 +105,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(MethodSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(MethodArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java new file mode 100644 index 000000000000..056453f29820 --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/MethodSources.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2024 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.jupiter.params.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @MethodSources} is a simple container for one or more + * {@link MethodSource} annotations. + * + *

Note, however, that use of the {@code @MethodSources} container is completely + * optional since {@code @MethodSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see MethodSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface MethodSources { + + /** + * An array of one or more {@link MethodSource @MethodSource} + * annotations. + */ + MethodSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java index d2ae43eb03e4..bc0ed303e935 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSource.java @@ -14,6 +14,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -21,8 +22,8 @@ import org.apiguardian.api.API; /** - * {@code @ValueSource} is an {@link ArgumentsSource} which provides access to - * an array of literal values. + * {@code @ValueSource} is a {@linkplain Repeatable repeatable} + * {@link ArgumentsSource} which provides access to an array of literal values. * *

Supported types include {@link #shorts}, {@link #bytes}, {@link #ints}, * {@link #longs}, {@link #floats}, {@link #doubles}, {@link #chars}, @@ -40,6 +41,7 @@ @Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented +@Repeatable(ValueSources.class) @API(status = STABLE, since = "5.7") @ArgumentsSource(ValueArgumentsProvider.class) @SuppressWarnings("exports") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java new file mode 100644 index 000000000000..8db4dcc5b01f --- /dev/null +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/ValueSources.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015-2024 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.jupiter.params.provider; + +import static org.apiguardian.api.API.Status.STABLE; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @ValueSources} is a simple container for one or more + * {@link ValueSource} annotations. + * + *

Note, however, that use of the {@code @ValueSources} container is completely + * optional since {@code @ValueSource} is a {@linkplain java.lang.annotation.Repeatable + * repeatable} annotation. + * + * @since 5.11 + * @see ValueSource + * @see java.lang.annotation.Repeatable + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@API(status = STABLE, since = "5.11") +public @interface ValueSources { + + /** + * An array of one or more {@link ValueSource @ValueSource} + * annotations. + */ + ValueSource[] value(); +} diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java index 94814e7c5bf2..9296c70deada 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/support/AnnotationConsumerInitializer.java @@ -11,19 +11,23 @@ package org.junit.jupiter.params.support; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; +import static org.junit.platform.commons.util.AnnotationUtils.findRepeatableAnnotations; import static org.junit.platform.commons.util.ReflectionUtils.HierarchyTraversalMode.BOTTOM_UP; import static org.junit.platform.commons.util.ReflectionUtils.findMethods; import java.lang.annotation.Annotation; +import java.lang.annotation.Repeatable; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; +import java.util.Collections; import java.util.List; import java.util.function.Predicate; import org.apiguardian.api.API; import org.junit.platform.commons.JUnitException; -import org.junit.platform.commons.util.AnnotationUtils; /** * {@code AnnotationConsumerInitializer} is an internal helper class for @@ -47,14 +51,27 @@ private AnnotationConsumerInitializer() { public static T initialize(AnnotatedElement annotatedElement, T annotationConsumerInstance) { if (annotationConsumerInstance instanceof AnnotationConsumer) { Class annotationType = findConsumedAnnotationType(annotationConsumerInstance); - Annotation annotation = AnnotationUtils.findAnnotation(annotatedElement, annotationType) // - .orElseThrow(() -> new JUnitException(annotationConsumerInstance.getClass().getName() - + " must be used with an annotation of type " + annotationType.getName())); - initializeAnnotationConsumer((AnnotationConsumer) annotationConsumerInstance, annotation); + List annotations = findAnnotations(annotatedElement, annotationType); + + if (annotations.isEmpty()) { + throw new JUnitException(annotationConsumerInstance.getClass().getName() + + " must be used with an annotation of type " + annotationType.getName()); + } + + annotations.forEach(annotation -> initializeAnnotationConsumer( + (AnnotationConsumer) annotationConsumerInstance, annotation)); } return annotationConsumerInstance; } + private static List findAnnotations(AnnotatedElement annotatedElement, + Class annotationType) { + + return annotationType.isAnnotationPresent(Repeatable.class) + ? findRepeatableAnnotations(annotatedElement, annotationType) + : findAnnotation(annotatedElement, annotationType).map(Collections::singletonList).orElse(emptyList()); + } + private static Class findConsumedAnnotationType(T annotationConsumerInstance) { Predicate consumesAnnotation = annotationConsumingMethodSignatures.stream() // .map(signature -> (Predicate) signature::isMatchingWith) // diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java index 69a28343706a..6320273e3490 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/ParameterizedTestIntegrationTests.java @@ -51,6 +51,7 @@ import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.NavigableMap; import java.util.NavigableSet; @@ -84,6 +85,7 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.engine.JupiterTestEngine; +import org.junit.jupiter.params.ParameterizedTestIntegrationTests.RepeatableSourcesTestCase.Action; import org.junit.jupiter.params.aggregator.AggregateWith; import org.junit.jupiter.params.aggregator.ArgumentsAccessor; import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; @@ -97,6 +99,7 @@ import org.junit.jupiter.params.provider.CsvFileSource; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EmptySource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.FieldSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; @@ -116,6 +119,13 @@ */ class ParameterizedTestIntegrationTests { + private final Locale originalLocale = Locale.getDefault(Locale.Category.FORMAT); + + @AfterEach + void restoreLocale() { + Locale.setDefault(Locale.Category.FORMAT, originalLocale); + } + @ParameterizedTest @CsvSource(textBlock = """ apple, True @@ -256,6 +266,16 @@ void executesWithCustomName() { .haveExactly(1, event(test(), displayName("bar and 42"), finishedWithFailure(message("bar, 42")))); } + @Test + void executesWithMessageFormat() { + Locale.setDefault(Locale.Category.FORMAT, Locale.ROOT); + + var results = execute("testWithMessageFormat", double.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, + event(test(), displayName("3.1416"), finishedWithFailure(message(String.valueOf(Math.PI))))); + } + /** * @since 5.2 */ @@ -1070,6 +1090,101 @@ private EngineExecutionResults execute(String methodName, Class... methodPara } + @Nested + class RepeatableSourcesIntegrationTests { + + @Test + void executesWithRepeatableCsvFileSource() { + var results = execute("testWithRepeatableCsvFileSource", String.class, String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, + event(test(), displayName("[1] column1=foo, column2=1"), finishedWithFailure(message("foo 1")))) // + .haveExactly(1, event(test(), displayName("[5] column1=FRUIT = apple, column2=RANK = 1"), + finishedWithFailure(message("apple 1")))); + } + + @Test + void executesWithRepeatableCsvSource() { + var results = execute("testWithRepeatableCsvSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=a"), finishedWithFailure(message("a")))) // + .haveExactly(1, event(test(), displayName("[2] argument=b"), finishedWithFailure(message("b")))); + } + + @Test + void executesWithRepeatableMethodSource() { + var results = execute("testWithRepeatableMethodSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, + event(test(), displayName("[1] argument=some"), finishedWithFailure(message("some")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=other"), finishedWithFailure(message("other")))); + } + + @Test + void executesWithRepeatableEnumSource() { + var results = execute("testWithRepeatableEnumSource", Action.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=FOO"), finishedWithFailure(message("FOO")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=BAR"), finishedWithFailure(message("BAR")))); + } + + @Test + void executesWithRepeatableValueSource() { + var results = execute("testWithRepeatableValueSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))); + } + + @Test + void executesWithRepeatableFieldSource() { + var results = execute("testWithRepeatableFieldSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, + event(test(), displayName("[1] argument=some"), finishedWithFailure(message("some")))) // + .haveExactly(1, + event(test(), displayName("[2] argument=other"), finishedWithFailure(message("other")))); + } + + @Test + void executesWithRepeatableArgumentsSource() { + var results = execute("testWithRepeatableArgumentsSource", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=foo"), finishedWithFailure(message("foo")))) // + .haveExactly(1, event(test(), displayName("[2] argument=bar"), finishedWithFailure(message("bar")))) // + .haveExactly(1, event(test(), displayName("[3] argument=foo"), finishedWithFailure(message("foo")))) // + .haveExactly(1, + event(test(), displayName("[4] argument=bar"), finishedWithFailure(message("bar")))); + + } + + @Test + void executesWithSameRepeatableAnnotationMultipleTimes() { + var results = execute("testWithSameRepeatableAnnotationMultipleTimes", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), started())) // + .haveExactly(1, event(test(), finishedWithFailure(message("foo")))); + } + + @Test + void executesWithDifferentRepeatableAnnotations() { + var results = execute("testWithDifferentRepeatableAnnotations", String.class); + results.allEvents().assertThatEvents() // + .haveExactly(1, event(test(), displayName("[1] argument=a"), finishedWithFailure(message("a")))) // + .haveExactly(1, event(test(), displayName("[2] argument=b"), finishedWithFailure(message("b")))) // + .haveExactly(1, event(test(), displayName("[3] argument=c"), finishedWithFailure(message("c")))) // + .haveExactly(1, event(test(), displayName("[4] argument=d"), finishedWithFailure(message("d")))); + } + + private EngineExecutionResults execute(String methodName, Class... methodParameterTypes) { + return ParameterizedTestIntegrationTests.this.execute(RepeatableSourcesTestCase.class, methodName, + methodParameterTypes); + } + } + @Test void closeAutoCloseableArgumentsAfterTest() { var results = execute("testWithAutoCloseableArgument", AutoCloseableArgument.class); @@ -1143,6 +1258,12 @@ void testWithErroneousConverter(@ConvertWith(ErroneousConverter.class) Object ig fail("this should never be called"); } + @ParameterizedTest(name = "{0,number,#.####}") + @ValueSource(doubles = Math.PI) + void testWithMessageFormat(double argument) { + fail(String.valueOf(argument)); + } + @ParameterizedTest @CsvSource({ "ab, cd", "ef, gh" }) void testWithAggregator(@AggregateWith(StringAggregator.class) String concatenation) { @@ -1905,6 +2026,99 @@ static Stream providerMethod() { } + static class RepeatableSourcesTestCase { + + @ParameterizedTest + @CsvFileSource(resources = "two-column.csv") + @CsvFileSource(resources = "two-column-with-headers.csv", delimiter = '|', useHeadersInDisplayName = true, nullValues = "NIL") + void testWithRepeatableCsvFileSource(String column1, String column2) { + fail("%s %s".formatted(column1, column2)); + } + + @ParameterizedTest + @CsvSource({ "a" }) + @CsvSource({ "b" }) + void testWithRepeatableCsvSource(String argument) { + fail(argument); + } + + @ParameterizedTest + @EnumSource(SmartAction.class) + @EnumSource(QuickAction.class) + void testWithRepeatableEnumSource(Action argument) { + fail(argument.toString()); + } + + interface Action { + } + + private enum SmartAction implements Action { + FOO + } + + private enum QuickAction implements Action { + BAR + } + + @ParameterizedTest + @MethodSource("someArgumentsMethodSource") + @MethodSource("otherArgumentsMethodSource") + void testWithRepeatableMethodSource(String argument) { + fail(argument); + } + + public static Stream someArgumentsMethodSource() { + return Stream.of(Arguments.of("some")); + } + + public static Stream otherArgumentsMethodSource() { + return Stream.of(Arguments.of("other")); + } + + @ParameterizedTest + @FieldSource("someArgumentsContainer") + @FieldSource("otherArgumentsContainer") + void testWithRepeatableFieldSource(String argument) { + fail(argument); + } + + static List someArgumentsContainer = List.of("some"); + static List otherArgumentsContainer = List.of("other"); + + @ParameterizedTest + @ValueSource(strings = "foo") + @ValueSource(strings = "bar") + void testWithRepeatableValueSource(String argument) { + fail(argument); + } + + @ParameterizedTest + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + @ValueSource(strings = "foo") + void testWithSameRepeatableAnnotationMultipleTimes(String argument) { + fail(argument); + } + + @ParameterizedTest + @ValueSource(strings = "a") + @ValueSource(strings = "b") + @CsvSource({ "c" }) + @CsvSource({ "d" }) + void testWithDifferentRepeatableAnnotations(String argument) { + fail(argument); + } + + @ParameterizedTest + @ArgumentsSource(TwoSingleStringArgumentsProvider.class) + @ArgumentsSource(TwoUnusedStringArgumentsProvider.class) + void testWithRepeatableArgumentsSource(String argument) { + fail(argument); + } + } + private static class TwoSingleStringArgumentsProvider implements ArgumentsProvider { @Override diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java index 25ffd4e1d484..af6e1eec06bd 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/AnnotationBasedArgumentsProviderTests.java @@ -10,6 +10,7 @@ package org.junit.jupiter.params.provider; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.params.provider.MockCsvAnnotationBuilder.csvSource; import static org.mockito.ArgumentMatchers.eq; @@ -30,7 +31,7 @@ class AnnotationBasedArgumentsProviderTests { private final AnnotationBasedArgumentsProvider annotationBasedArgumentsProvider = new AnnotationBasedArgumentsProvider<>() { @Override protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { - return Stream.empty(); + return Stream.of(Arguments.of(annotation)); } }; @@ -54,4 +55,21 @@ void shouldInvokeTemplateMethodWithTheAnnotationProvidedToAccept() { verify(spiedProvider, atMostOnce()).provideArguments(eq(extensionContext), eq(annotation)); } + @Test + @DisplayName("should invoke the provideArguments template method for every accepted annotation") + void shouldInvokeTemplateMethodForEachAnnotationProvided() { + var extensionContext = mock(ExtensionContext.class); + var foo = csvSource("foo"); + var bar = csvSource("bar"); + + annotationBasedArgumentsProvider.accept(foo); + annotationBasedArgumentsProvider.accept(bar); + + var arguments = annotationBasedArgumentsProvider.provideArguments(extensionContext).toList(); + + assertThat(arguments).hasSize(2); + assertThat(arguments.getFirst().get()[0]).isEqualTo(foo); + assertThat(arguments.get(1).get()[0]).isEqualTo(bar); + } + } diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java index a7ecddec1f7f..0ea40f9c70e9 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java @@ -40,7 +40,7 @@ void throwsExceptionIfNeitherValueNorTextBlockIsDeclared() { var annotation = csvSource().build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessage("@CsvSource must be declared with either `value` or `textBlock` but not both"); } @@ -52,7 +52,7 @@ void throwsExceptionIfValueAndTextBlockAreDeclared() { """).build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessage("@CsvSource must be declared with either `value` or `textBlock` but not both"); } @@ -223,7 +223,7 @@ void throwsExceptionIfBothDelimitersAreSimultaneouslySet() { var annotation = csvSource().delimiter('|').delimiterString("~~~").build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("The delimiter and delimiterString attributes cannot be set simultaneously in")// .withMessageContaining("CsvSource"); } @@ -270,7 +270,7 @@ void throwsExceptionIfSourceExceedsMaxCharsPerColumnConfig() { var annotation = csvSource().lines("413").maxCharsPerColumn(2).build(); assertThatExceptionOfType(CsvParsingException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("Failed to parse CSV input configured via Mock for CsvSource")// .withRootCauseInstanceOf(ArrayIndexOutOfBoundsException.class); } @@ -289,7 +289,7 @@ void throwsExceptionWhenSourceExceedsDefaultMaxCharsPerColumnConfig() { var annotation = csvSource().lines("0".repeat(4097)).delimiter(';').build(); assertThatExceptionOfType(CsvParsingException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("Failed to parse CSV input configured via Mock for CsvSource")// .withRootCauseInstanceOf(ArrayIndexOutOfBoundsException.class); } @@ -308,7 +308,7 @@ void throwsExceptionWhenMaxCharsPerColumnIsNotPositiveNumber() { var annotation = csvSource().lines("41").delimiter(';').maxCharsPerColumn(-1).build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessageStartingWith("maxCharsPerColumn must be a positive number: -1"); } @@ -372,7 +372,7 @@ void throwsExceptionIfColumnCountExceedsHeaderCount() { """).build(); assertThatExceptionOfType(PreconditionViolationException.class)// - .isThrownBy(() -> provideArguments(annotation))// + .isThrownBy(() -> provideArguments(annotation).findAny())// .withMessage( "The number of columns (3) exceeds the number of supplied headers (2) in CSV record: [banana, 2, BOOM!]"); } diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java index e7c061a2ecd4..afdce4749143 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/CsvFileArgumentsProviderTests.java @@ -98,7 +98,8 @@ void throwsExceptionIfBothDelimitersAreSimultaneouslySet() { .delimiterString(";")// .build(); - var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(annotation, "foo")); + var exception = assertThrows(PreconditionViolationException.class, + () -> provideArguments(annotation, "foo").findAny()); assertThat(exception)// .hasMessageStartingWith("The delimiter and delimiterString attributes cannot be set simultaneously in")// @@ -435,7 +436,7 @@ void throwsExceptionWhenMaxCharsPerColumnIsNotPositiveNumber(@TempDir Path tempD .build(); var exception = assertThrows(PreconditionViolationException.class, // - () -> provideArguments(new CsvFileArgumentsProvider(), annotation)); + () -> provideArguments(new CsvFileArgumentsProvider(), annotation).findAny()); assertThat(exception)// .hasMessageStartingWith("maxCharsPerColumn must be a positive number: -1"); diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java index f63514b1be3a..6a5312085776 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/EnumArgumentsProviderTests.java @@ -56,21 +56,21 @@ void provideAllEnumConstantsWithNamingAll() { @Test void duplicateConstantNameIsDetected() { Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(EnumWithTwoConstants.class, "FOO", "BAR", "FOO")); + () -> provideArguments(EnumWithTwoConstants.class, "FOO", "BAR", "FOO").findAny()); assertThat(exception).hasMessageContaining("Duplicate enum constant name(s) found"); } @Test void invalidConstantNameIsDetected() { Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(EnumWithTwoConstants.class, "FO0", "B4R")); + () -> provideArguments(EnumWithTwoConstants.class, "FO0", "B4R").findAny()); assertThat(exception).hasMessageContaining("Invalid enum constant name(s) in"); } @Test void invalidPatternIsDetected() { Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(EnumWithTwoConstants.class, Mode.MATCH_ALL, "(", ")")); + () -> provideArguments(EnumWithTwoConstants.class, Mode.MATCH_ALL, "(", ")").findAny()); assertThat(exception).hasMessageContaining("Pattern compilation failed"); } @@ -90,7 +90,7 @@ void incorrectParameterTypeIsDetected() throws Exception { TestCase.class.getDeclaredMethod("methodWithIncorrectParameter", Object.class)); Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(NullEnum.class)); + () -> provideArguments(NullEnum.class).findAny()); assertThat(exception).hasMessageStartingWith("First parameter must reference an Enum type"); } @@ -100,7 +100,7 @@ void methodsWithoutParametersAreDetected() throws Exception { TestCase.class.getDeclaredMethod("methodWithoutParameters")); Exception exception = assertThrows(PreconditionViolationException.class, - () -> provideArguments(NullEnum.class)); + () -> provideArguments(NullEnum.class).findAny()); assertThat(exception).hasMessageStartingWith("Test method must declare at least one parameter"); } diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java index 86b2221aec94..dea71ff0f727 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/provider/ValueArgumentsProviderTests.java @@ -29,7 +29,7 @@ class ValueArgumentsProviderTests { void multipleInputsAreNotAllowed() { var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(new short[1], new byte[0], new int[1], new long[0], new float[0], new double[0], - new char[0], new boolean[0], new String[0], new Class[0])); + new char[0], new boolean[0], new String[0], new Class[0]).findAny()); assertThat(exception).hasMessageContaining( "Exactly one type of input must be provided in the @ValueSource annotation, but there were 2"); @@ -39,7 +39,7 @@ void multipleInputsAreNotAllowed() { void onlyEmptyInputsAreNotAllowed() { var exception = assertThrows(PreconditionViolationException.class, () -> provideArguments(new short[0], new byte[0], new int[0], new long[0], new float[0], new double[0], - new char[0], new boolean[0], new String[0], new Class[0])); + new char[0], new boolean[0], new String[0], new Class[0]).findAny()); assertThat(exception).hasMessageContaining( "Exactly one type of input must be provided in the @ValueSource annotation, but there were 0"); diff --git a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java index 7e3c34b3ab91..b60da429e03e 100644 --- a/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java +++ b/junit-jupiter-params/src/test/java/org/junit/jupiter/params/support/AnnotationConsumerInitializerTests.java @@ -13,10 +13,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.params.support.AnnotationConsumerInitializer.initialize; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; @@ -52,9 +58,11 @@ void shouldInitializeAnnotationBasedArgumentsProvider() throws NoSuchMethodExcep var method = SubjectClass.class.getDeclaredMethod("foo"); var initialisedAnnotationConsumer = initialize(method, instance); - initialisedAnnotationConsumer.provideArguments(mock()); + initialisedAnnotationConsumer.provideArguments(mock()).findAny(); - assertThat(initialisedAnnotationConsumer.annotation) // + assertThat(initialisedAnnotationConsumer.annotations) // + .hasSize(1) // + .element(0) // .isInstanceOfSatisfying(CsvSource.class, // source -> assertThat(source.value()).containsExactly("a", "b")); } @@ -93,13 +101,23 @@ void shouldThrowExceptionWhenParameterIsNotAnnotated() throws NoSuchMethodExcept assertThatThrownBy(() -> initialize(parameter, instance)).isInstanceOf(JUnitException.class); } + @Test + void shouldInitializeForEachAnnotations() throws NoSuchMethodException { + var instance = spy(new SomeAnnotationBasedArgumentsProvider()); + var method = SubjectClass.class.getDeclaredMethod("repeatableAnnotation", String.class); + + initialize(method, instance); + + verify(instance, times(2)).accept(any(CsvSource.class)); + } + private static class SomeAnnotationBasedArgumentsProvider extends AnnotationBasedArgumentsProvider { - CsvSource annotation; + List annotations = new ArrayList<>(); @Override protected Stream provideArguments(ExtensionContext context, CsvSource annotation) { - this.annotation = annotation; + annotations.add(annotation); return Stream.empty(); } } @@ -138,6 +156,11 @@ void bar(@JavaTimeConversionPattern("pattern") LocalDate date) { void noAnnotation(String param) { } + + @CsvSource("a") + @CsvSource("b") + void repeatableAnnotation(String param) { + } } } diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java index fa70025bda45..20061af9fd2e 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/CollectionUtils.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collector; @@ -55,7 +56,7 @@ private CollectionUtils() { } /** - * Read the only element of a collection of size 1. + * Get the only element of a collection of size 1. * * @param collection the collection to get the element from * @return the only element of the collection @@ -66,7 +67,29 @@ public static T getOnlyElement(Collection collection) { Preconditions.notNull(collection, "collection must not be null"); Preconditions.condition(collection.size() == 1, () -> "collection must contain exactly one element: " + collection); - return collection.iterator().next(); + return firstElement(collection); + } + + /** + * Get the first element of the supplied collection unless it's empty. + * + * @param collection the collection to get the element from + * @return the first element of the collection; empty if the collection is empty + * @throws PreconditionViolationException if the collection is {@code null} + * @since 1.11 + */ + @API(status = INTERNAL, since = "1.11") + public static Optional getFirstElement(Collection collection) { + Preconditions.notNull(collection, "collection must not be null"); + return collection.isEmpty() // + ? Optional.empty() // + : Optional.ofNullable(firstElement(collection)); + } + + private static T firstElement(Collection collection) { + return collection instanceof List // + ? ((List) collection).get(0) // + : collection.iterator().next(); } /** diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java index 58c523f82b18..1e8b3fdd2efb 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/ReflectionUtils.java @@ -929,13 +929,34 @@ public static String getFullyQualifiedMethodName(Class clazz, Method method) * @param methodName the name of the method; never {@code null} or blank * @param parameterTypes the parameter types of the method; may be {@code null} or empty * @return fully qualified method name; never {@code null} - * @see #getFullyQualifiedMethodName(Class, Method) */ public static String getFullyQualifiedMethodName(Class clazz, String methodName, Class... parameterTypes) { Preconditions.notNull(clazz, "Class must not be null"); Preconditions.notBlank(methodName, "Method name must not be null or blank"); - return String.format("%s#%s(%s)", clazz.getName(), methodName, ClassUtils.nullSafeToString(parameterTypes)); + return getFullyQualifiedMethodName(clazz.getName(), methodName, ClassUtils.nullSafeToString(parameterTypes)); + } + + /** + * Build the fully qualified method name for the method described by the + * supplied class, method name, and parameter types. + * + *

Note that the class is not necessarily the class in which the method is + * declared. + * + * @param className the name of the class from which the method should be referenced; never {@code null} + * @param methodName the name of the method; never {@code null} or blank + * @param parameterTypeNames the parameter type names of the method; may be empty but not {@code null} + * @return fully qualified method name; never {@code null} + * @since 1.11 + */ + @API(status = INTERNAL, since = "1.11") + public static String getFullyQualifiedMethodName(String className, String methodName, String parameterTypeNames) { + Preconditions.notBlank(className, "Class name must not be null or blank"); + Preconditions.notBlank(methodName, "Method name must not be null or blank"); + Preconditions.notNull(parameterTypeNames, "Parameter type names must not be null"); + + return String.format("%s#%s(%s)", className, methodName, parameterTypeNames); } /** diff --git a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/StringUtils.java b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/StringUtils.java index e05729f871cb..42dcdeef863b 100644 --- a/junit-platform-commons/src/main/java/org/junit/platform/commons/util/StringUtils.java +++ b/junit-platform-commons/src/main/java/org/junit/platform/commons/util/StringUtils.java @@ -14,6 +14,9 @@ import static org.apiguardian.api.API.Status.INTERNAL; import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; import java.util.regex.Pattern; import org.apiguardian.api.API; @@ -256,4 +259,106 @@ public static String replaceWhitespaceCharacters(String str, String replacement) return str == null ? null : WHITESPACE_PATTERN.matcher(str).replaceAll(replacement); } + /** + * Split the supplied {@link String} into up to two parts using the supplied + * separator character. + * + * @param separator the separator character + * @param value the value to split; never {@code null} + * @since 1.11 + */ + @API(status = INTERNAL, since = "1.11") + public static TwoPartSplitResult splitIntoTwo(char separator, String value) { + Preconditions.notNull(value, "value must not be null"); + return splitIntoTwo(value, value.indexOf(separator), 1); + } + + /** + * Split the supplied {@link String} into up to two parts using the supplied + * separator string. + * + * @param separator the separator string; never {@code null} + * @param value the value to split; never {@code null} + * @since 1.11 + */ + @API(status = INTERNAL, since = "1.11") + public static TwoPartSplitResult splitIntoTwo(String separator, String value) { + Preconditions.notNull(separator, "separator must not be null"); + Preconditions.notNull(value, "value must not be null"); + return splitIntoTwo(value, value.indexOf(separator), separator.length()); + } + + private static TwoPartSplitResult splitIntoTwo(String value, int index, int length) { + if (index == -1) { + return new OnePart(value); + } + return new TwoParts(value.substring(0, index), value.substring(index + length)); + } + + /** + * The result of splitting a string into up to two parts. + * + * @since 1.11 + * @see StringUtils#splitIntoTwo(char, String) + * @see StringUtils#splitIntoTwo(String, String) + */ + @API(status = INTERNAL, since = "1.11") + public interface TwoPartSplitResult { + + /** + * Maps the result of splitting a string into two parts or throw an exception. + * + * @param onePartExceptionCreator the exception creator to use if the string was split into a single part + * @param twoPartsMapper the mapper to use if the string was split into two parts + */ + default T mapTwo(Supplier onePartExceptionCreator, + BiFunction twoPartsMapper) { + Function onePartMapper = __ -> { + throw onePartExceptionCreator.get(); + }; + return map(onePartMapper, twoPartsMapper); + } + + /** + * Maps the result of splitting a string into up to two parts. + * + * @param onePartMapper the mapper to use if the string was split into a single part + * @param twoPartsMapper the mapper to use if the string was split into two parts + */ + T map(Function onePartMapper, BiFunction twoPartsMapper); + + } + + private static final class OnePart implements TwoPartSplitResult { + + private final String value; + + OnePart(String value) { + this.value = value; + } + + @Override + public T map(Function onePartMapper, + BiFunction twoPartsMapper) { + return onePartMapper.apply(value); + } + } + + private static final class TwoParts implements TwoPartSplitResult { + + private final String first; + private final String second; + + TwoParts(String first, String second) { + this.first = first; + this.second = second; + } + + @Override + public T map(Function onePartMapper, + BiFunction twoPartsMapper) { + return twoPartsMapper.apply(first, second); + } + } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java index 2bb6ed9ec8b7..e8e377c27f13 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/BaseCommand.java @@ -34,6 +34,10 @@ abstract class BaseCommand implements Callable { @Option(names = { "-h", "--help" }, usageHelp = true, description = "Display help information.") private boolean helpRequested; + @SuppressWarnings("unused") + @Option(names = "--version", versionHelp = true, description = "Display version information.") + private boolean versionHelpRequested; + void execute(String... args) { toCommandLine().execute(args); } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java index c695043fcc5f..bd7f15214e4b 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/CommandFacade.java @@ -33,6 +33,9 @@ public CommandFacade(ConsoleTestExecutor.Factory consoleTestExecutorFactory) { } public CommandResult run(PrintWriter out, PrintWriter err, String[] args) { + String version = ManifestVersionProvider.getImplementationVersion(); + System.setProperty("junit.docs.version", + version == null ? "current" : (version.endsWith("-SNAPSHOT") ? "snapshot" : version)); return new MainCommand(consoleTestExecutorFactory).run(out, err, args); } } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java index b87755b8e996..54e4f0e4fce6 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/MainCommand.java @@ -37,21 +37,25 @@ description = "Launches the JUnit Platform for test discovery and execution.", // footerHeading = "%n", // footer = "For more information, please refer to the JUnit User Guide at%n" // - + "@|underline https://junit.org/junit5/docs/current/user-guide/|@", // + + "@|underline https://junit.org/junit5/docs/${junit.docs.version}/user-guide/|@", // scope = CommandLine.ScopeType.INHERIT, // exitCodeOnInvalidInput = CommandResult.FAILURE, // - exitCodeOnExecutionException = CommandResult.FAILURE // + exitCodeOnExecutionException = CommandResult.FAILURE, // + versionProvider = ManifestVersionProvider.class // ) class MainCommand implements Callable, IExitCodeGenerator { private final ConsoleTestExecutor.Factory consoleTestExecutorFactory; - @Option(names = { "-h", "--help" }, help = true, hidden = true) + @Option(names = { "-h", "--help" }, help = true, description = "Display help information.") private boolean helpRequested; @Option(names = { "--h", "-help" }, help = true, hidden = true) private boolean helpRequested2; + @Option(names = "--version", versionHelp = true, description = "Display version information.") + private boolean versionHelpRequested; + @Unmatched private final List allParameters = new ArrayList<>(); @@ -71,6 +75,11 @@ public Object call() { commandResult = CommandResult.success(); return null; } + if (versionHelpRequested) { + commandSpec.commandLine().printVersionHelp(commandSpec.commandLine().getOut()); + commandResult = CommandResult.success(); + return null; + } if (allParameters.contains("--list-engines")) { return runCommand("engines", Optional.of("--list-engines")); } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/ManifestVersionProvider.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/ManifestVersionProvider.java new file mode 100644 index 000000000000..44a5339502fc --- /dev/null +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/ManifestVersionProvider.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015-2024 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.platform.console.options; + +import picocli.CommandLine; + +class ManifestVersionProvider implements CommandLine.IVersionProvider { + + public static String getImplementationVersion() { + return ManifestVersionProvider.class.getPackage().getImplementationVersion(); + } + + @Override + public String[] getVersion() { + return new String[] { // + String.format("@|bold JUnit Platform Console Launcher %s|@", getImplementationVersion()), // + "JVM: ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})", // + "OS: ${os.name} ${os.version} ${os.arch}" // + }; + } + +} diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java index a50fad2373fd..3bb444602cb8 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/SelectorConverter.java @@ -14,22 +14,17 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathResource; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectDirectory; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectFile; -import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectModule; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUri; -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.IntStream; - -import org.junit.platform.commons.util.Preconditions; -import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.engine.DiscoverySelectorIdentifier; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.ClasspathResourceSelector; import org.junit.platform.engine.discovery.DirectorySelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.discovery.FileSelector; import org.junit.platform.engine.discovery.IterationSelector; import org.junit.platform.engine.discovery.MethodSelector; @@ -99,51 +94,21 @@ public ClasspathResourceSelector convert(String value) { static class Iteration implements ITypeConverter { - public static final Pattern PATTERN = Pattern.compile( - "(?[a-z]+):(?.*)\\[(?(\\d+)(\\.\\.\\d+)?(\\s*,\\s*(\\d+)(\\.\\.\\d+)?)*)]"); - @Override public IterationSelector convert(String value) { - Matcher matcher = PATTERN.matcher(value); - Preconditions.condition(matcher.matches(), "Invalid format: must be TYPE:VALUE[INDEX(,INDEX)*]"); - DiscoverySelector parentSelector = createParentSelector(matcher.group("type"), matcher.group("value")); - int[] iterationIndices = Arrays.stream(matcher.group("indices").split(",")) // - .flatMapToInt(this::parseIndexDefinition) // - .toArray(); - return selectIteration(parentSelector, iterationIndices); + DiscoverySelectorIdentifier identifier = DiscoverySelectorIdentifier.create( + IterationSelector.IdentifierParser.PREFIX, value); + return (IterationSelector) DiscoverySelectors.parse(identifier) // + .orElseThrow(() -> new PreconditionViolationException("Invalid format: Failed to parse selector")); } - private IntStream parseIndexDefinition(String value) { - String[] parts = value.split("\\.\\.", 2); - int firstIndex = Integer.parseInt(parts[0]); - if (parts.length == 2) { - int lastIndex = Integer.parseInt(parts[1]); - return IntStream.rangeClosed(firstIndex, lastIndex); - } - return IntStream.of(firstIndex); - } + } + + static class Identifier implements ITypeConverter { - private DiscoverySelector createParentSelector(String type, String value) { - switch (type) { - case "module": - return selectModule(value); - case "uri": - return selectUri(value); - case "file": - return selectFile(value); - case "directory": - return selectDirectory(value); - case "package": - return selectPackage(value); - case "class": - return selectClass(value); - case "method": - return selectMethod(value); - case "resource": - return selectClasspathResource(value); - default: - throw new IllegalArgumentException("Unknown type: " + type); - } + @Override + public DiscoverySelectorIdentifier convert(String value) { + return DiscoverySelectorIdentifier.parse(value); } } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java index e1a3f5d8b19e..b14cd519b72c 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptions.java @@ -25,9 +25,11 @@ import org.apiguardian.api.API; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.ClasspathResourceSelector; import org.junit.platform.engine.discovery.DirectorySelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; import org.junit.platform.engine.discovery.FileSelector; import org.junit.platform.engine.discovery.IterationSelector; import org.junit.platform.engine.discovery.MethodSelector; @@ -56,6 +58,7 @@ public class TestDiscoveryOptions { private List selectedMethods = emptyList(); private List selectedClasspathResources = emptyList(); private List selectedIterations = emptyList(); + private List selectorIdentifiers = emptyList(); private List includedClassNamePatterns = singletonList(STANDARD_INCLUDE_PATTERN); private List excludedClassNamePatterns = emptyList(); @@ -176,6 +179,14 @@ public void setSelectedIterations(List selectedIterations) { this.selectedIterations = selectedIterations; } + public List getSelectorIdentifiers() { + return selectorIdentifiers; + } + + public void setSelectorIdentifiers(List selectorIdentifiers) { + this.selectorIdentifiers = selectorIdentifiers; + } + public List getExplicitSelectors() { List selectors = new ArrayList<>(); selectors.addAll(getSelectedUris()); @@ -187,6 +198,7 @@ public List getExplicitSelectors() { selectors.addAll(getSelectedMethods()); selectors.addAll(getSelectedClasspathResources()); selectors.addAll(getSelectedIterations()); + DiscoverySelectors.parseAll(getSelectorIdentifiers()).forEach(selectors::add); return selectors; } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java index a43369fa0e5f..95cff395a6bf 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestDiscoveryOptionsMixin.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; +import org.junit.platform.engine.DiscoverySelectorIdentifier; import org.junit.platform.engine.discovery.ClassNameFilter; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.ClasspathResourceSelector; @@ -39,7 +40,9 @@ class TestDiscoveryOptionsMixin { @ArgGroup(validate = false, order = 2, heading = "%n@|bold SELECTORS|@%n%n") SelectorOptions selectorOptions; - @ArgGroup(validate = false, order = 3, heading = "%n@|bold FILTERS|@%n%n") + @ArgGroup(validate = false, order = 3, heading = "%n For more information on selectors including syntax examples, see" + + "%n @|underline https://junit.org/junit5/docs/current/user-guide/#running-tests-discovery-selectors|@" + + "%n%n@|bold FILTERS|@%n%n") FilterOptions filterOptions; @ArgGroup(validate = false, order = 4, heading = "%n@|bold RUNTIME CONFIGURATION|@%n%n") @@ -128,13 +131,21 @@ static class SelectorOptions { private final List selectedClasspathResources2 = new ArrayList<>(); @Option(names = { "-i", - "--select-iteration" }, paramLabel = "TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]", arity = "1", converter = SelectorConverter.Iteration.class, description = "Select iterations for test discovery (e.g. method:com.acme.Foo#m()[1..2]). This option can be repeated.") + "--select-iteration" }, paramLabel = "PREFIX:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]", arity = "1", converter = SelectorConverter.Iteration.class, // + description = "Select iterations for test discovery via a prefixed identifier and a list of indexes or index ranges " + + "(e.g. method:com.acme.Foo#m()[1..2] selects the first and second iteration of the m() method in the com.acme.Foo class). " + + "This option can be repeated.") private final List selectedIterations = new ArrayList<>(); @Option(names = { "--i", "-select-iteration" }, arity = "1", hidden = true, converter = SelectorConverter.Iteration.class) private final List selectedIterations2 = new ArrayList<>(); + @Option(names = "--select", paramLabel = "PREFIX:VALUE", arity = "1", converter = SelectorConverter.Identifier.class, // + description = "Select via a prefixed identifier (e.g. method:com.acme.Foo#m selects the m() method in the com.acme.Foo class). " + + "This option can be repeated.") + private final List selectorIdentifiers = new ArrayList<>(); + SelectorOptions() { } @@ -152,6 +163,7 @@ private void applyTo(TestDiscoveryOptions result) { result.setSelectedClasspathResources( merge(this.selectedClasspathResources, this.selectedClasspathResources2)); result.setSelectedIterations(merge(this.selectedIterations, this.selectedIterations2)); + result.setSelectorIdentifiers(this.selectorIdentifiers); } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoverySelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoverySelector.java index b1cf4ab0e3f7..342f536ca2ce 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoverySelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoverySelector.java @@ -10,9 +10,13 @@ package org.junit.platform.engine; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; +import java.util.Optional; + import org.apiguardian.api.API; +import org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser; /** * A selector defines what a {@link TestEngine} can use to discover tests @@ -25,4 +29,21 @@ */ @API(status = STABLE, since = "1.0") public interface DiscoverySelector { + + /** + * Return the {@linkplain DiscoverySelectorIdentifier identifier} of this + * selector. + *

+ * The returned identifier has to be parsable by a corresponding + * {@link DiscoverySelectorIdentifierParser}. + * + * @return the identifier of this selector or empty if it is not supported; + * never {@code null} + * @since 1.11 + */ + @API(status = EXPERIMENTAL, since = "1.11") + default Optional toIdentifier() { + return Optional.empty(); + } + } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoverySelectorIdentifier.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoverySelectorIdentifier.java new file mode 100644 index 000000000000..4fd926e9b27b --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/DiscoverySelectorIdentifier.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015-2024 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.platform.engine; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.Objects; + +import org.apiguardian.api.API; +import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.StringUtils; + +/** + * Identifier for {@link DiscoverySelector DiscoverySelectors} with a specific + * prefix. + *

+ * The {@linkplain #toString() string representation} of an identifier is + * intended to be human-readable and is formatted as {@code prefix:value}. + * + * @since 1.11 + * @see org.junit.platform.engine.discovery.DiscoverySelectors#parse(String) + * @see org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser + */ +@API(status = EXPERIMENTAL, since = "1.11") +public final class DiscoverySelectorIdentifier { + + private final String prefix; + private final String value; + + /** + * Create a new {@link DiscoverySelectorIdentifier} with the supplied prefix and + * value. + * + * @param prefix the prefix; never {@code null} or blank + * @param value the value; never {@code null} or blank + */ + public static DiscoverySelectorIdentifier create(String prefix, String value) { + return new DiscoverySelectorIdentifier(prefix, value); + } + + /** + * Parse the supplied string representation of a + * {@link DiscoverySelectorIdentifier} in the format {@code prefix:value}. + * + * @param string the string representation of a {@link DiscoverySelectorIdentifier} + * @return the parsed {@link DiscoverySelectorIdentifier} + * @throws PreconditionViolationException if the supplied string does not + * conform to the expected format + */ + public static DiscoverySelectorIdentifier parse(String string) { + return StringUtils.splitIntoTwo(':', string).mapTwo( // + () -> new PreconditionViolationException("Identifier string must be 'prefix:value', but was " + string), + DiscoverySelectorIdentifier::new // + ); + } + + private DiscoverySelectorIdentifier(String prefix, String value) { + this.prefix = Preconditions.notBlank(prefix, "prefix must not be blank"); + this.value = Preconditions.notBlank(value, "value must not be blank"); + } + + /** + * Get the prefix of this identifier. + * + * @return the prefix; never {@code null} or blank + */ + public String getPrefix() { + return prefix; + } + + /** + * Get the value of this identifier. + * + * @return the value; never {@code null} or blank + */ + public String getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DiscoverySelectorIdentifier that = (DiscoverySelectorIdentifier) o; + return Objects.equals(prefix, that.prefix) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(prefix, value); + } + + /** + * Get the string representation of this identifier in the format + * {@code prefix:value}. + */ + @Override + public String toString() { + return String.format("%s:%s", prefix, value); + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClassSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClassSelector.java index 0d26f8d8648a..2d263b092453 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClassSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClassSelector.java @@ -11,9 +11,11 @@ package org.junit.platform.engine.discovery; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.PreconditionViolationException; @@ -21,6 +23,7 @@ import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a {@link Class} or class name so @@ -133,4 +136,31 @@ public String toString() { // @formatter:on } + @Override + public Optional toIdentifier() { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.className)); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for {@link ClassSelector + * ClassSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "class"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(DiscoverySelectors.selectClass(identifier.getValue())); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java index 8f50e0f2284e..1a17cda9119b 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathResourceSelector.java @@ -10,14 +10,17 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.Objects; import java.util.Optional; import org.apiguardian.api.API; +import org.junit.platform.commons.util.StringUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects the name of a classpath resource @@ -101,4 +104,43 @@ public String toString() { this.position).toString(); } + @Override + public Optional toIdentifier() { + if (this.position == null) { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.classpathResourceName)); + } + else { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, + String.format("%s?%s", this.classpathResourceName, this.position.toQueryPart()))); + } + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for + * {@link ClasspathResourceSelector ClasspathResourceSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "resource"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(StringUtils.splitIntoTwo('?', identifier.getValue()).map( // + DiscoverySelectors::selectClasspathResource, // + (resourceName, query) -> { + FilePosition position = FilePosition.fromQuery(query).orElse(null); + return DiscoverySelectors.selectClasspathResource(resourceName, position); + } // + )); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathRootSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathRootSelector.java index 1bb64c8be3ba..815edd8373ae 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathRootSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ClasspathRootSelector.java @@ -10,14 +10,22 @@ package org.junit.platform.engine.discovery; +import static java.util.Collections.singleton; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; +import static org.junit.platform.commons.util.CollectionUtils.getFirstElement; import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a classpath root so that @@ -42,7 +50,7 @@ public class ClasspathRootSelector implements DiscoverySelector { private final URI classpathRoot; ClasspathRootSelector(URI classpathRoot) { - this.classpathRoot = classpathRoot; + this.classpathRoot = Preconditions.notNull(classpathRoot, "classpathRoot must not be null"); } /** @@ -82,4 +90,32 @@ public String toString() { return new ToStringBuilder(this).append("classpathRoot", this.classpathRoot).toString(); } + @Override + public Optional toIdentifier() { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.classpathRoot.toString())); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for + * {@link ClasspathRootSelector ClasspathRootSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "classpath-root"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + Path path = Paths.get(URI.create(identifier.getValue())); + return getFirstElement(DiscoverySelectors.selectClasspathRoots(singleton(path))); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DirectorySelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DirectorySelector.java index eab53ecff702..976c20ddb695 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DirectorySelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DirectorySelector.java @@ -10,6 +10,7 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.io.File; @@ -18,10 +19,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a directory so that @@ -107,4 +110,31 @@ public String toString() { return new ToStringBuilder(this).append("path", this.path).toString(); } + @Override + public Optional toIdentifier() { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.path)); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for + * {@link DirectorySelector DirectorySelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "directory"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(DiscoverySelectors.selectDirectory(identifier.getValue())); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectorIdentifierParser.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectorIdentifierParser.java new file mode 100644 index 000000000000..8570c6d5c3cc --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectorIdentifierParser.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015-2024 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.platform.engine.discovery; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.Optional; + +import org.apiguardian.api.API; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; + +/** + * Parser for {@link DiscoverySelectorIdentifier DiscoverySelectorIdentifiers} + * with a specific prefix. + *

+ * Implementations of this interface can be registered using the Java service + * loader mechanism to extend the set of supported prefixes for + * {@link DiscoverySelectorIdentifier DiscoverySelectorIdentifiers}. + * + * @since 1.11 + * @see DiscoverySelectors#parse(String) + */ +@API(status = EXPERIMENTAL, since = "1.11") +public interface DiscoverySelectorIdentifierParser { + + /** + * Get the prefix that this parser can handle. + * + * @return the prefix that this parser can handle; never {@code null} + */ + String getPrefix(); + + /** + * Parse the supplied {@link DiscoverySelectorIdentifier}. + *

+ * The JUnit Platform will only invoke this method if the supplied + * {@link DiscoverySelectorIdentifier} has a prefix that matches the value + * returned by {@link #getPrefix()}. + * + * @param identifier the {@link DiscoverySelectorIdentifier} to parse + * @param context the {@link Context} to use for parsing + * @return an {@link Optional} containing the parsed {@link DiscoverySelector}; never {@code null} + */ + Optional parse(DiscoverySelectorIdentifier identifier, Context context); + + /** + * Context for parsing {@link DiscoverySelectorIdentifier DiscoverySelectorIdentifiers}. + */ + interface Context { + + /** + * Parse the supplied selector. + *

+ * This method is intended to be used by implementations of + * {@link DiscoverySelectorIdentifierParser#parse} for selectors that + * contain other selectors. + */ + Optional parse(String selector); + + } + +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectorIdentifierParsers.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectorIdentifierParsers.java new file mode 100644 index 000000000000..2f0d79b46735 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectorIdentifierParsers.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015-2024 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.platform.engine.discovery; + +import static java.util.Collections.unmodifiableMap; +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.stream.Stream; + +import org.junit.platform.commons.util.ClassLoaderUtils; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; + +/** + * Utility class for parsing {@link DiscoverySelectorIdentifier + * DiscoverySelectorIdentifiers}. + * + * @since 1.11 + */ +class DiscoverySelectorIdentifierParsers { + + static Stream parseAll(String... identifiers) { + Preconditions.notNull(identifiers, "identifiers must not be null"); + return Stream.of(identifiers) // + .map(DiscoverySelectorIdentifierParsers::parse) // + .filter(Optional::isPresent) // + .map(Optional::get); + } + + static Stream parseAll(Collection identifiers) { + Preconditions.notNull(identifiers, "identifiers must not be null"); + return identifiers.stream() // + .map(DiscoverySelectorIdentifierParsers::parse) // + .filter(Optional::isPresent) // + .map(Optional::get); + } + + static Optional parse(String identifier) { + Preconditions.notNull(identifier, "identifier must not be null"); + return parse(DiscoverySelectorIdentifier.parse(identifier)); + } + + static Optional parse(DiscoverySelectorIdentifier identifier) { + Preconditions.notNull(identifier, "identifier must not be null"); + DiscoverySelectorIdentifierParser parser = Singleton.INSTANCE.parsersByPrefix.get(identifier.getPrefix()); + Preconditions.notNull(parser, "No parser for prefix: " + identifier.getPrefix()); + + return parser.parse(identifier, DiscoverySelectorIdentifierParsers::parse); + } + + private enum Singleton { + + INSTANCE; + + private final Map parsersByPrefix; + + Singleton() { + Map parsersByPrefix = new HashMap<>(); + Iterable loadedParsers = ServiceLoader.load( + DiscoverySelectorIdentifierParser.class, ClassLoaderUtils.getDefaultClassLoader()); + for (DiscoverySelectorIdentifierParser parser : loadedParsers) { + DiscoverySelectorIdentifierParser previous = parsersByPrefix.put(parser.getPrefix(), parser); + Preconditions.condition(previous == null, + () -> String.format("Duplicate parser for prefix: [%s] candidate a: [%s] candidate b: [%s] ", + parser.getPrefix(), requireNonNull(previous).getClass().getName(), + parser.getClass().getName())); + } + this.parsersByPrefix = unmodifiableMap(parsersByPrefix); + } + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java index 0f3d5aefd57f..5bdbb25fd8d5 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/DiscoverySelectors.java @@ -21,14 +21,18 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.apiguardian.api.API; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; import org.junit.platform.engine.UniqueId; /** @@ -48,6 +52,7 @@ * @see NestedClassSelector * @see NestedMethodSelector * @see UniqueIdSelector + * @see DiscoverySelectorIdentifier */ @API(status = STABLE, since = "1.0") public final class DiscoverySelectors { @@ -937,4 +942,58 @@ public static IterationSelector selectIteration(DiscoverySelector parentSelector return new IterationSelector(parentSelector, iterationIndices); } + /** + * Parse the supplied string representation of a + * {@link DiscoverySelectorIdentifier}. + * + * @param identifier the string representation of a {@code DiscoverySelectorIdentifier}; + * never {@code null} or blank + * @since 1.11 + * @see DiscoverySelectorIdentifierParser + */ + @API(status = EXPERIMENTAL, since = "1.11") + public static Optional parse(String identifier) { + return DiscoverySelectorIdentifierParsers.parse(identifier); + } + + /** + * Parse the supplied {@link DiscoverySelectorIdentifier}. + * + * @param identifier the {@code DiscoverySelectorIdentifier} to parse; + * never {@code null} + * @since 1.11 + * @see DiscoverySelectorIdentifierParser + */ + @API(status = EXPERIMENTAL, since = "1.11") + public static Optional parse(DiscoverySelectorIdentifier identifier) { + return DiscoverySelectorIdentifierParsers.parse(identifier); + } + + /** + * Parse the supplied string representations of + * {@link DiscoverySelectorIdentifier DiscoverySelectorIdentifiers}. + * + * @param identifiers the string representations of + * {@code DiscoverySelectorIdentifiers} to parse; never {@code null} + * @since 1.11 + * @see DiscoverySelectorIdentifierParser + */ + @API(status = EXPERIMENTAL, since = "1.11") + public static Stream parseAll(String... identifiers) { + return DiscoverySelectorIdentifierParsers.parseAll(identifiers); + } + + /** + * Parse the supplied {@link DiscoverySelectorIdentifier + * DiscoverySelectorIdentifiers}. + * + * @param identifiers the {@code DiscoverySelectorIdentifiers} to parse; + * never {@code null} + * @since 1.11 + * @see DiscoverySelectorIdentifierParser + */ + @API(status = EXPERIMENTAL, since = "1.11") + public static Stream parseAll(Collection identifiers) { + return DiscoverySelectorIdentifierParsers.parseAll(identifiers); + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FilePosition.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FilePosition.java index 86871b3eb57a..41a125472cb4 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FilePosition.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FilePosition.java @@ -153,6 +153,14 @@ public Optional getColumn() { return Optional.ofNullable(this.column); } + String toQueryPart() { + StringBuilder builder = new StringBuilder("line=").append(this.line); + if (this.column != null) { + builder.append("&column=").append(this.column); + } + return builder.toString(); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FileSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FileSelector.java index 430be2a865f2..6246fa58e4c3 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FileSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/FileSelector.java @@ -10,6 +10,7 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.io.File; @@ -21,8 +22,10 @@ import java.util.Optional; import org.apiguardian.api.API; +import org.junit.platform.commons.util.StringUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a file so that @@ -117,4 +120,43 @@ public String toString() { return new ToStringBuilder(this).append("path", this.path).append("position", this.position).toString(); } + @Override + public Optional toIdentifier() { + if (this.position == null) { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.path)); + } + else { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, + String.format("%s?%s", this.path, this.position.toQueryPart()))); + } + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for {@link FileSelector + * FileSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "file"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(StringUtils.splitIntoTwo('?', identifier.getValue()).map( // + DiscoverySelectors::selectFile, // + (path, query) -> { + FilePosition position = FilePosition.fromQuery(query).orElse(null); + return DiscoverySelectors.selectFile(path, position); + } // + )); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/IterationSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/IterationSelector.java index 62a309b3baed..965a86cdb379 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/IterationSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/IterationSelector.java @@ -11,18 +11,29 @@ package org.junit.platform.engine.discovery; import static java.util.stream.Collectors.collectingAndThen; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toCollection; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.SortedSet; import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.StringUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects the iterations of a parent @@ -90,4 +101,89 @@ public String toString() { .toString(); // @formatter:on } + + @Override + public Optional toIdentifier() { + return parentSelector.toIdentifier().map(parentSelectorString -> DiscoverySelectorIdentifier.create( // + IdentifierParser.PREFIX, // + String.format("%s[%s]", parentSelectorString, formatIterationIndicesAsRanges())) // + ); + } + + private String formatIterationIndicesAsRanges() { + class Range { + final int start; + int end; + + Range(int start) { + this.start = start; + this.end = start; + } + } + List ranges = new ArrayList<>(); + Range current = new Range(iterationIndices.first()); + ranges.add(current); + for (int n : iterationIndices.tailSet(current.start + 1)) { + if (n == current.end + 1) { + current.end = n; + } + else { + current = new Range(n); + ranges.add(current); + } + } + return ranges.stream() // + .map(r -> { + if (r.start == r.end) { + return String.valueOf(r.start); + } + if (r.start == r.end - 1) { + return r.start + "," + r.end; + } + return r.start + ".." + r.end; + }) // + .collect(joining(",")); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for + * {@link IterationSelector IterationSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + public static final String PREFIX = "iteration"; + + private static final Pattern PATTERN = Pattern.compile( + "(?.+)\\[(?(\\d+)(\\.\\.\\d+)?(\\s*,\\s*(\\d+)(\\.\\.\\d+)?)*)]"); + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + Matcher matcher = PATTERN.matcher(identifier.getValue()); + Preconditions.condition(matcher.matches(), "Invalid format: must be IDENTIFIER[INDEX(,INDEX)*]"); + return context.parse(matcher.group("parentIdentifier")) // + .map(parent -> { + int[] iterationIndices = Arrays.stream(matcher.group("indices").split(",")) // + .flatMapToInt(this::parseIndexDefinition) // + .toArray(); + return DiscoverySelectors.selectIteration(parent, iterationIndices); + }); + } + + private IntStream parseIndexDefinition(String value) { + return StringUtils.splitIntoTwo("..", value).map( // + index -> IntStream.of(Integer.parseInt(index)), // + (firstIndex, lastIndex) -> IntStream.rangeClosed(Integer.parseInt(firstIndex), + Integer.parseInt(lastIndex))// + ); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java index bbb411007049..4a58eb805a8f 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/MethodSelector.java @@ -12,10 +12,12 @@ import static org.apiguardian.api.API.Status.DEPRECATED; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.lang.reflect.Method; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.JUnitException; @@ -25,6 +27,7 @@ import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a {@link Method} or a combination of @@ -308,4 +311,34 @@ public String toString() { // @formatter:on } + @Override + public Optional toIdentifier() { + String fullyQualifiedMethodName = ReflectionUtils.getFullyQualifiedMethodName(this.className, this.methodName, + this.parameterTypeNames); + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, fullyQualifiedMethodName)); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for {@link MethodSelector + * MethodSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "method"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(DiscoverySelectors.selectMethod(identifier.getValue())); + } + + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java index 2ae62e599242..293654370f8e 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/ModuleSelector.java @@ -10,13 +10,16 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a module name so that @@ -73,4 +76,31 @@ public String toString() { return new ToStringBuilder(this).append("moduleName", this.moduleName).toString(); } + @Override + public Optional toIdentifier() { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.moduleName)); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for {@link ModuleSelector + * ModuleSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "module"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(DiscoverySelectors.selectModule(identifier.getValue())); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedClassSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedClassSelector.java index 5fb508268d54..cfa1dd6e2500 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedClassSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedClassSelector.java @@ -10,18 +10,24 @@ package org.junit.platform.engine.discovery; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import static org.junit.platform.commons.util.CollectionUtils.toUnmodifiableList; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; import org.apiguardian.api.API; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a nested {@link Class} @@ -143,4 +149,36 @@ public String toString() { .toString(); } + @Override + public Optional toIdentifier() { + String allClassNames = Stream.concat(enclosingClassSelectors.stream(), Stream.of(nestedClassSelector)) // + .map(ClassSelector::getClassName) // + .collect(joining("/")); + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, allClassNames)); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for + * {@link NestedClassSelector NestedClassSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + static final String PREFIX = "nested-class"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + List parts = Arrays.asList(identifier.getValue().split("/")); + return Optional.of( + DiscoverySelectors.selectNestedClass(parts.subList(0, parts.size() - 1), parts.get(parts.size() - 1))); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java index c97b3ce50172..ae5d4a69fdc0 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/NestedMethodSelector.java @@ -12,16 +12,21 @@ import static org.apiguardian.api.API.Status.DEPRECATED; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.commons.util.ReflectionUtils; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a nested {@link Method} @@ -239,4 +244,45 @@ public String toString() { .toString(); } + @Override + public Optional toIdentifier() { + return nestedClassSelector.toIdentifier() // + .map(parent -> { + String fullyQualifiedMethodName = ReflectionUtils.getFullyQualifiedMethodName(parent.getValue(), + methodSelector.getMethodName(), methodSelector.getParameterTypeNames()); + return DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, fullyQualifiedMethodName); + }); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for + * {@link NestedMethodSelector NestedMethodSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "nested-method"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + List parts = Arrays.asList(identifier.getValue().split("/")); + List enclosingClassNames = parts.subList(0, parts.size() - 1); + + String[] methodParts = ReflectionUtils.parseFullyQualifiedMethodName(parts.get(parts.size() - 1)); + String nestedClassName = methodParts[0]; + String methodName = methodParts[1]; + String parameterTypeNames = methodParts[2]; + + return Optional.of(DiscoverySelectors.selectNestedMethod(enclosingClassNames, nestedClassName, methodName, + parameterTypeNames)); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/PackageSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/PackageSelector.java index 9e2a6c2eb6e7..fc7da6fb5c6f 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/PackageSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/PackageSelector.java @@ -10,13 +10,16 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a package name so that @@ -73,4 +76,31 @@ public String toString() { return new ToStringBuilder(this).append("packageName", this.packageName).toString(); } + @Override + public Optional toIdentifier() { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.packageName)); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for {@link PackageSelector + * PackageSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "package"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(DiscoverySelectors.selectPackage(identifier.getValue())); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UniqueIdSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UniqueIdSelector.java index 4f6bfb08923f..08068b1d9ca6 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UniqueIdSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UniqueIdSelector.java @@ -10,13 +10,16 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; import org.junit.platform.engine.UniqueId; /** @@ -74,4 +77,31 @@ public String toString() { return new ToStringBuilder(this).append("uniqueId", this.uniqueId).toString(); } + @Override + public Optional toIdentifier() { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.uniqueId.toString())); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for + * {@link UniqueIdSelector UniqueIdSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "uid"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(DiscoverySelectors.selectUniqueId(identifier.getValue())); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UriSelector.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UriSelector.java index 826c00d23fa4..9e6ed0c45c8a 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UriSelector.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/discovery/UriSelector.java @@ -10,14 +10,17 @@ package org.junit.platform.engine.discovery; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.net.URI; import java.util.Objects; +import java.util.Optional; import org.apiguardian.api.API; import org.junit.platform.commons.util.ToStringBuilder; import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * A {@link DiscoverySelector} that selects a {@link URI} so that @@ -77,4 +80,31 @@ public String toString() { return new ToStringBuilder(this).append("uri", this.uri).toString(); } + @Override + public Optional toIdentifier() { + return Optional.of(DiscoverySelectorIdentifier.create(IdentifierParser.PREFIX, this.uri.toString())); + } + + /** + * The {@link DiscoverySelectorIdentifierParser} for {@link UriSelector + * UriSelectors}. + */ + @API(status = INTERNAL, since = "1.11") + public static class IdentifierParser implements DiscoverySelectorIdentifierParser { + + private static final String PREFIX = "uri"; + + public IdentifierParser() { + } + + @Override + public String getPrefix() { + return PREFIX; + } + + @Override + public Optional parse(DiscoverySelectorIdentifier identifier, Context context) { + return Optional.of(DiscoverySelectors.selectUri(identifier.getValue())); + } + } } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java index 18745c63019d..965eb9b24439 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java @@ -37,19 +37,25 @@ * {@link #NamespacedHierarchicalStore(NamespacedHierarchicalStore, CloseAction)} * constructor. * - *

This class is thread-safe. + *

This class is thread-safe. Please note, however, that thread safety is + * not guaranteed while the {@link #close()} method is being invoked. * * @param Namespace type - * @since 5.10 + * @since 1.10 */ -@API(status = EXPERIMENTAL, since = "5.10") +@API(status = EXPERIMENTAL, since = "1.10") public final class NamespacedHierarchicalStore implements AutoCloseable { private final AtomicInteger insertOrderSequence = new AtomicInteger(); + private final ConcurrentMap, StoredValue> storedValues = new ConcurrentHashMap<>(4); + private final NamespacedHierarchicalStore parentStore; + private final CloseAction closeAction; + private volatile boolean closed = false; + /** * Create a new store with the supplied parent. * @@ -72,7 +78,7 @@ public NamespacedHierarchicalStore(NamespacedHierarchicalStore parentStore, C } /** - * Create a child store with this store as its parent using the same close + * Create a child store with this store as its parent and this store's close * action. */ public NamespacedHierarchicalStore newChild() { @@ -80,23 +86,46 @@ public NamespacedHierarchicalStore newChild() { } /** - * If a close action is configured, it will be called with all successfully + * Determine if this store has been {@linkplain #close() closed}. + * + * @return {@code true} if this store has been closed + * @since 1.11 + * @see #close() + */ + @API(status = EXPERIMENTAL, since = "1.11") + public boolean isClosed() { + return this.closed; + } + + /** + * If a {@link CloseAction} is configured, it will be called with all successfully * stored values in reverse insertion order. * *

Closing a store does not close its parent or any of its children. + * + *

Invocations of this method after the store has already been closed will + * be ignored. + * + * @see #isClosed() */ @Override public void close() { - if (this.closeAction == null) { - return; + if (!this.closed) { + try { + if (this.closeAction != null) { + ThrowableCollector throwableCollector = new ThrowableCollector(__ -> false); + this.storedValues.entrySet().stream() // + .map(e -> e.getValue().evaluateSafely(e.getKey())) // + .filter(it -> it != null && it.value != null) // + .sorted(EvaluatedValue.REVERSE_INSERT_ORDER) // + .forEach(it -> throwableCollector.execute(() -> it.close(this.closeAction))); + throwableCollector.assertEmpty(); + } + } + finally { + this.closed = true; + } } - ThrowableCollector throwableCollector = new ThrowableCollector(__ -> false); - this.storedValues.entrySet().stream() // - .map(e -> e.getValue().evaluateSafely(e.getKey())) // - .filter(it -> it != null && it.value != null) // - .sorted(EvaluatedValue.REVERSE_INSERT_ORDER) // - .forEach(it -> throwableCollector.execute(() -> it.close(this.closeAction))); - throwableCollector.assertEmpty(); } /** @@ -106,8 +135,11 @@ public void close() { * @param namespace the namespace; never {@code null} * @param key the key; never {@code null} * @return the stored value; may be {@code null} + * @throws NamespacedHierarchicalStoreException if this store has already been + * closed */ public Object get(N namespace, Object key) { + rejectIfClosed(); StoredValue storedValue = getStoredValue(new CompositeKey<>(namespace, key)); return StoredValue.evaluateIfNotNull(storedValue); } @@ -121,9 +153,10 @@ public Object get(N namespace, Object key) { * @param requiredType the required type of the value; never {@code null} * @return the stored value; may be {@code null} * @throws NamespacedHierarchicalStoreException if the stored value cannot - * be cast to the required type + * be cast to the required type, or if this store has already been closed */ public T get(N namespace, Object key, Class requiredType) throws NamespacedHierarchicalStoreException { + rejectIfClosed(); Object value = get(namespace, key); return castToRequiredType(key, value, requiredType); } @@ -137,8 +170,11 @@ public T get(N namespace, Object key, Class requiredType) throws Namespac * @param defaultCreator the function called with the supplied {@code key} * to create a new value; never {@code null} but may return {@code null} * @return the stored value; may be {@code null} + * @throws NamespacedHierarchicalStoreException if this store has already been + * closed */ public Object getOrComputeIfAbsent(N namespace, K key, Function defaultCreator) { + rejectIfClosed(); Preconditions.notNull(defaultCreator, "defaultCreator must not be null"); CompositeKey compositeKey = new CompositeKey<>(namespace, key); StoredValue storedValue = getStoredValue(compositeKey); @@ -161,10 +197,12 @@ public Object getOrComputeIfAbsent(N namespace, K key, Function def * @param requiredType the required type of the value; never {@code null} * @return the stored value; may be {@code null} * @throws NamespacedHierarchicalStoreException if the stored value cannot - * be cast to the required type + * be cast to the required type, or if this store has already been closed */ public V getOrComputeIfAbsent(N namespace, K key, Function defaultCreator, Class requiredType) throws NamespacedHierarchicalStoreException { + + rejectIfClosed(); Object value = getOrComputeIfAbsent(namespace, key, defaultCreator); return castToRequiredType(key, value, requiredType); } @@ -180,10 +218,11 @@ public V getOrComputeIfAbsent(N namespace, K key, Function defaultC * @param key the key; never {@code null} * @param value the value to store; may be {@code null} * @return the previously stored value; may be {@code null} - * @throws NamespacedHierarchicalStoreException if the stored value cannot - * be cast to the required type + * @throws NamespacedHierarchicalStoreException if an error occurs while + * storing the value, or if this store has already been closed */ public Object put(N namespace, Object key, Object value) throws NamespacedHierarchicalStoreException { + rejectIfClosed(); StoredValue oldValue = this.storedValues.put(new CompositeKey<>(namespace, key), storedValue(() -> value)); return StoredValue.evaluateIfNotNull(oldValue); } @@ -198,8 +237,11 @@ public Object put(N namespace, Object key, Object value) throws NamespacedHierar * @param namespace the namespace; never {@code null} * @param key the key; never {@code null} * @return the previously stored value; may be {@code null} + * @throws NamespacedHierarchicalStoreException if this store has already been + * closed */ public Object remove(N namespace, Object key) { + rejectIfClosed(); StoredValue previous = this.storedValues.remove(new CompositeKey<>(namespace, key)); return StoredValue.evaluateIfNotNull(previous); } @@ -216,9 +258,10 @@ public Object remove(N namespace, Object key) { * @param requiredType the required type of the value; never {@code null} * @return the previously stored value; may be {@code null} * @throws NamespacedHierarchicalStoreException if the stored value cannot - * be cast to the required type + * be cast to the required type, or if this store has already been closed */ public T remove(N namespace, Object key, Class requiredType) throws NamespacedHierarchicalStoreException { + rejectIfClosed(); Object value = remove(namespace, key); return castToRequiredType(key, value, requiredType); } @@ -256,6 +299,13 @@ private T castToRequiredType(Object key, Object value, Class requiredType requiredType.getName(), value.getClass().getName(), value)); } + private void rejectIfClosed() { + if (this.closed) { + throw new NamespacedHierarchicalStoreException( + "A NamespacedHierarchicalStore cannot be modified or queried after it has been closed"); + } + } + private static class CompositeKey { private final N namespace; @@ -297,7 +347,7 @@ private static class StoredValue { private EvaluatedValue evaluateSafely(CompositeKey compositeKey) { try { - return new EvaluatedValue<>(compositeKey, order, evaluate()); + return new EvaluatedValue<>(compositeKey, this.order, evaluate()); } catch (Throwable t) { UnrecoverableExceptions.rethrowIfUnrecoverable(t); @@ -306,7 +356,7 @@ private EvaluatedValue evaluateSafely(CompositeKey compositeKey) { } private Object evaluate() { - return supplier.get(); + return this.supplier.get(); } static Object evaluateIfNotNull(StoredValue value) { @@ -393,7 +443,7 @@ public Failure(Throwable throwable) { /** * Called for each successfully stored non-null value in the store when a * {@link NamespacedHierarchicalStore} is - * {@link NamespacedHierarchicalStore#close() closed}. + * {@linkplain NamespacedHierarchicalStore#close() closed}. */ @FunctionalInterface public interface CloseAction { diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreException.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreException.java index c2ecd8d4fcda..bf9dfefa1d16 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreException.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreException.java @@ -18,9 +18,9 @@ /** * Exception thrown by failed {@link NamespacedHierarchicalStore} operations. * - * @since 5.10 + * @since 1.10 */ -@API(status = EXPERIMENTAL, since = "5.10") +@API(status = EXPERIMENTAL, since = "1.10") public class NamespacedHierarchicalStoreException extends JUnitException { private static final long serialVersionUID = 1L; @@ -29,8 +29,8 @@ public NamespacedHierarchicalStoreException(String message) { super(message); } - @SuppressWarnings("unused") public NamespacedHierarchicalStoreException(String message, Throwable cause) { super(message, cause); } + } diff --git a/junit-platform-engine/src/main/resources/META-INF/services/org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser b/junit-platform-engine/src/main/resources/META-INF/services/org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser new file mode 100644 index 000000000000..f8aa801d77fd --- /dev/null +++ b/junit-platform-engine/src/main/resources/META-INF/services/org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser @@ -0,0 +1,13 @@ +org.junit.platform.engine.discovery.ClasspathResourceSelector$IdentifierParser +org.junit.platform.engine.discovery.ClasspathRootSelector$IdentifierParser +org.junit.platform.engine.discovery.ClassSelector$IdentifierParser +org.junit.platform.engine.discovery.DirectorySelector$IdentifierParser +org.junit.platform.engine.discovery.FileSelector$IdentifierParser +org.junit.platform.engine.discovery.IterationSelector$IdentifierParser +org.junit.platform.engine.discovery.MethodSelector$IdentifierParser +org.junit.platform.engine.discovery.ModuleSelector$IdentifierParser +org.junit.platform.engine.discovery.NestedClassSelector$IdentifierParser +org.junit.platform.engine.discovery.NestedMethodSelector$IdentifierParser +org.junit.platform.engine.discovery.PackageSelector$IdentifierParser +org.junit.platform.engine.discovery.UniqueIdSelector$IdentifierParser +org.junit.platform.engine.discovery.UriSelector$IdentifierParser diff --git a/junit-platform-engine/src/module/org.junit.platform.engine/module-info.java b/junit-platform-engine/src/module/org.junit.platform.engine/module-info.java index 46c2069448c2..baabf4794d85 100644 --- a/junit-platform-engine/src/module/org.junit.platform.engine/module-info.java +++ b/junit-platform-engine/src/module/org.junit.platform.engine/module-info.java @@ -31,4 +31,22 @@ exports org.junit.platform.engine.support.filter; exports org.junit.platform.engine.support.hierarchical; exports org.junit.platform.engine.support.store; + + uses org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser; + + provides org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser with + org.junit.platform.engine.discovery.ClassSelector.IdentifierParser, + org.junit.platform.engine.discovery.ClasspathResourceSelector.IdentifierParser, + org.junit.platform.engine.discovery.ClasspathRootSelector.IdentifierParser, + org.junit.platform.engine.discovery.DirectorySelector.IdentifierParser, + org.junit.platform.engine.discovery.FileSelector.IdentifierParser, + org.junit.platform.engine.discovery.IterationSelector.IdentifierParser, + org.junit.platform.engine.discovery.MethodSelector.IdentifierParser, + org.junit.platform.engine.discovery.ModuleSelector.IdentifierParser, + org.junit.platform.engine.discovery.NestedClassSelector.IdentifierParser, + org.junit.platform.engine.discovery.NestedMethodSelector.IdentifierParser, + org.junit.platform.engine.discovery.PackageSelector.IdentifierParser, + org.junit.platform.engine.discovery.UniqueIdSelector.IdentifierParser, + org.junit.platform.engine.discovery.UriSelector.IdentifierParser; + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java index 4a331d6e50b3..ae3cd416469e 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/TestIdentifier.java @@ -284,7 +284,8 @@ private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOEx source = serializedForm.source; tags = serializedForm.tags; type = serializedForm.type; - parentId = UniqueId.parse(serializedForm.parentId); + String parentId = serializedForm.parentId; + this.parentId = parentId == null ? null : UniqueId.parse(parentId); legacyReportingName = serializedForm.legacyReportingName; } @@ -307,7 +308,8 @@ private static class SerializedForm implements Serializable { SerializedForm(TestIdentifier testIdentifier) { this.uniqueId = testIdentifier.uniqueId.toString(); - this.parentId = testIdentifier.parentId.toString(); + UniqueId parentId = testIdentifier.parentId; + this.parentId = parentId == null ? null : parentId.toString(); this.displayName = testIdentifier.displayName; this.legacyReportingName = testIdentifier.legacyReportingName; this.source = testIdentifier.source; diff --git a/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatform.java b/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatform.java index 46fbe6f33e52..d2dea088a62b 100644 --- a/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatform.java +++ b/junit-platform-runner/src/main/java/org/junit/platform/runner/JUnitPlatform.java @@ -38,6 +38,7 @@ import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.IncludePackages; import org.junit.platform.suite.api.IncludeTags; +import org.junit.platform.suite.api.Select; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.SelectDirectories; @@ -87,6 +88,7 @@ * ClassNameFilter#STANDARD_INCLUDE_PATTERN}). * * @since 1.0 + * @see Select * @see SelectClasses * @see SelectClasspathResource * @see SelectDirectories diff --git a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Select.java b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Select.java new file mode 100644 index 000000000000..79c991743f5e --- /dev/null +++ b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Select.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2024 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.platform.suite.api; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @Select} is a {@linkplain Repeatable repeatable} annotation that + * specifies which tests to select based on prefixed + * {@linkplain org.junit.platform.engine.DiscoverySelectorIdentifier selector identifiers}. + * + * @since 1.11 + * @see Suite + * @see org.junit.platform.engine.discovery.DiscoverySelectors#parse(String) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +@Documented +@API(status = EXPERIMENTAL, since = "1.11") +@Repeatable(Selects.class) +public @interface Select { + + /** + * One or more prefixed + * {@linkplain org.junit.platform.engine.DiscoverySelectorIdentifier selector identifiers} + * to select. + */ + String[] value(); + +} diff --git a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Selects.java b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Selects.java new file mode 100644 index 000000000000..44403f81aa21 --- /dev/null +++ b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Selects.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2024 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.platform.suite.api; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * {@code @Selects} is a container for one or more + * {@link Select @Select} declarations. + * + *

Note, however, that use of the {@code @Selects} container is + * completely optional since {@code @Select} is a + * {@linkplain java.lang.annotation.Repeatable repeatable} annotation. + * + * @since 1.11 + * @see Select + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +@Documented +@API(status = EXPERIMENTAL, since = "1.11") +public @interface Selects { + + /** + * An array of one or more {@link Select @Select} declarations. + */ + Select[] value(); + +} diff --git a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Suite.java b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Suite.java index 44075c3d8c2d..670bb7fe9f1a 100644 --- a/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Suite.java +++ b/junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Suite.java @@ -46,6 +46,7 @@ * configuration parameters are taken into account. * * @since 1.8 + * @see Select * @see SelectClasses * @see SelectClasspathResource * @see SelectDirectories diff --git a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java index 9312aac7f664..a66a5545de54 100644 --- a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java +++ b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/AdditionalDiscoverySelectors.java @@ -17,6 +17,7 @@ import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.StringUtils; +import org.junit.platform.engine.DiscoverySelector; import org.junit.platform.engine.discovery.ClassSelector; import org.junit.platform.engine.discovery.ClasspathResourceSelector; import org.junit.platform.engine.discovery.DirectorySelector; @@ -112,6 +113,11 @@ static ClasspathResourceSelector selectClasspathResource(String classpathResourc return DiscoverySelectors.selectClasspathResource(classpathResourceName, FilePosition.from(line, column)); } + static List parseIdentifiers(String[] identifiers) { + return DiscoverySelectors.parseAll(identifiers) // + .collect(Collectors.toList()); + } + private static Stream uniqueStreamOf(T[] elements) { return Arrays.stream(elements).distinct(); } diff --git a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java index d2fa1abaa082..8db5687778cd 100644 --- a/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java +++ b/junit-platform-suite-commons/src/main/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilder.java @@ -55,6 +55,7 @@ import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.IncludePackages; import org.junit.platform.suite.api.IncludeTags; +import org.junit.platform.suite.api.Select; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.SelectDirectories; @@ -393,6 +394,9 @@ public SuiteLauncherDiscoveryRequestBuilder applySelectorsAndFiltersFromSuite(Cl findAnnotationValues(suiteClass, SelectPackages.class, SelectPackages::value) .map(AdditionalDiscoverySelectors::selectPackages) .ifPresent(this::selectors); + findAnnotationValues(suiteClass, Select.class, Select::value) + .map(AdditionalDiscoverySelectors::parseIdentifiers) + .ifPresent(this::selectors); // @formatter:on return this; } diff --git a/platform-tests/platform-tests.gradle.kts b/platform-tests/platform-tests.gradle.kts index dbbbd7385b51..5b199e238b68 100644 --- a/platform-tests/platform-tests.gradle.kts +++ b/platform-tests/platform-tests.gradle.kts @@ -26,6 +26,7 @@ dependencies { testImplementation(testFixtures(projects.junitPlatformEngine)) testImplementation(testFixtures(projects.junitPlatformLauncher)) testImplementation(projects.junitJupiterEngine) + testImplementation(testFixtures(projects.junitJupiterEngine)) testImplementation(libs.apiguardian) testImplementation(libs.jfrunit) { exclude(group = "org.junit.vintage") diff --git a/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java b/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java index a925b75a1388..ddc08e339f93 100644 --- a/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/commons/util/CollectionUtilsTests.java @@ -21,6 +21,7 @@ import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -32,6 +33,7 @@ import java.util.stream.Stream; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; import org.junit.jupiter.api.extension.ParameterContext; @@ -52,256 +54,306 @@ */ class CollectionUtilsTests { - @Test - void getOnlyElementWithNullCollection() { - var exception = assertThrows(PreconditionViolationException.class, () -> CollectionUtils.getOnlyElement(null)); - assertEquals("collection must not be null", exception.getMessage()); - } + @Nested + class OnlyElement { - @Test - void getOnlyElementWithEmptyCollection() { - var exception = assertThrows(PreconditionViolationException.class, - () -> CollectionUtils.getOnlyElement(Set.of())); - assertEquals("collection must contain exactly one element: []", exception.getMessage()); - } + @Test + void nullCollection() { + var exception = assertThrows(PreconditionViolationException.class, + () -> CollectionUtils.getOnlyElement(null)); + assertEquals("collection must not be null", exception.getMessage()); + } - @Test - void getOnlyElementWithSingleElementCollection() { - var expected = new Object(); - var actual = CollectionUtils.getOnlyElement(Set.of(expected)); - assertSame(expected, actual); - } + @Test + void emptyCollection() { + var exception = assertThrows(PreconditionViolationException.class, + () -> CollectionUtils.getOnlyElement(Set.of())); + assertEquals("collection must contain exactly one element: []", exception.getMessage()); + } - @Test - void getOnlyElementWithMultiElementCollection() { - var exception = assertThrows(PreconditionViolationException.class, - () -> CollectionUtils.getOnlyElement(List.of("foo", "bar"))); - assertEquals("collection must contain exactly one element: [foo, bar]", exception.getMessage()); - } + @Test + void singleElementCollection() { + var expected = new Object(); + var actual = CollectionUtils.getOnlyElement(Set.of(expected)); + assertSame(expected, actual); + } - @Test - void toUnmodifiableListThrowsOnMutation() { - var numbers = Stream.of(1).collect(toUnmodifiableList()); - assertThrows(UnsupportedOperationException.class, numbers::clear); + @Test + void multiElementCollection() { + var exception = assertThrows(PreconditionViolationException.class, + () -> CollectionUtils.getOnlyElement(List.of("foo", "bar"))); + assertEquals("collection must contain exactly one element: [foo, bar]", exception.getMessage()); + } } - @ParameterizedTest - @ValueSource(classes = { // - Stream.class, // - DoubleStream.class, // - IntStream.class, // - LongStream.class, // - Collection.class, // - Iterable.class, // - Iterator.class, // - Object[].class, // - String[].class, // - int[].class, // - double[].class, // - char[].class // - }) - void isConvertibleToStreamForSupportedTypes(Class type) { - assertThat(CollectionUtils.isConvertibleToStream(type)).isTrue(); - } + @Nested + class FirstElement { - @ParameterizedTest - @MethodSource("objectsConvertibleToStreams") - void isConvertibleToStreamForSupportedTypesFromObjects(Object object) { - assertThat(CollectionUtils.isConvertibleToStream(object.getClass())).isTrue(); - } + @Test + void nullCollection() { + var exception = assertThrows(PreconditionViolationException.class, + () -> CollectionUtils.getFirstElement(null)); + assertEquals("collection must not be null", exception.getMessage()); + } - static Stream objectsConvertibleToStreams() { - return Stream.of(// - Stream.of("cat", "dog"), // - DoubleStream.of(42.3), // - IntStream.of(99), // - LongStream.of(100000000), // - Set.of(1, 2, 3), // - Arguments.of((Object) new Object[] { 9, 8, 7 }), // - new int[] { 5, 10, 15 }// - ); - } + @Test + void emptyCollection() { + assertThat(CollectionUtils.getFirstElement(Set.of())).isEmpty(); + } - @ParameterizedTest - @ValueSource(classes = { // - void.class, // - Void.class, // - Object.class, // - Integer.class, // - String.class, // - int.class, // - boolean.class // - }) - void isConvertibleToStreamForUnsupportedTypes(Class type) { - assertThat(CollectionUtils.isConvertibleToStream(type)).isFalse(); - } + @Test + void singleElementCollection() { + var expected = new Object(); + assertThat(CollectionUtils.getFirstElement(Set.of(expected))).containsSame(expected); + } - @Test - void isConvertibleToStreamForNull() { - assertThat(CollectionUtils.isConvertibleToStream(null)).isFalse(); + @Test + void multiElementCollection() { + assertThat(CollectionUtils.getFirstElement(List.of("foo", "bar"))).contains("foo"); + } + + @Test + void collectionWithNullValues() { + assertThat(CollectionUtils.getFirstElement(Arrays.asList(new Object[1]))).isEmpty(); + } } - @Test - void toStreamWithNull() { - Exception exception = assertThrows(PreconditionViolationException.class, () -> CollectionUtils.toStream(null)); + @Nested + class UnmodifiableList { - assertThat(exception).hasMessage("Object must not be null"); + @Test + void throwsOnMutation() { + var numbers = Stream.of(1).collect(toUnmodifiableList()); + assertThrows(UnsupportedOperationException.class, numbers::clear); + } } - @Test - void toStreamWithUnsupportedObjectType() { - Exception exception = assertThrows(PreconditionViolationException.class, - () -> CollectionUtils.toStream("unknown")); + @Nested + class StreamConversion { + + @ParameterizedTest + @ValueSource(classes = { // + Stream.class, // + DoubleStream.class, // + IntStream.class, // + LongStream.class, // + Collection.class, // + Iterable.class, // + Iterator.class, // + Object[].class, // + String[].class, // + int[].class, // + double[].class, // + char[].class // + }) + void isConvertibleToStreamForSupportedTypes(Class type) { + assertThat(CollectionUtils.isConvertibleToStream(type)).isTrue(); + } - assertThat(exception).hasMessage("Cannot convert instance of java.lang.String into a Stream: unknown"); - } + @ParameterizedTest + @MethodSource("objectsConvertibleToStreams") + void isConvertibleToStreamForSupportedTypesFromObjects(Object object) { + assertThat(CollectionUtils.isConvertibleToStream(object.getClass())).isTrue(); + } - @Test - void toStreamWithExistingStream() { - var input = Stream.of("foo"); + static Stream objectsConvertibleToStreams() { + return Stream.of(// + Stream.of("cat", "dog"), // + DoubleStream.of(42.3), // + IntStream.of(99), // + LongStream.of(100000000), // + Set.of(1, 2, 3), // + Arguments.of((Object) new Object[] { 9, 8, 7 }), // + new int[] { 5, 10, 15 }// + ); + } - var result = CollectionUtils.toStream(input); + @ParameterizedTest + @ValueSource(classes = { // + void.class, // + Void.class, // + Object.class, // + Integer.class, // + String.class, // + int.class, // + boolean.class // + }) + void isConvertibleToStreamForUnsupportedTypes(Class type) { + assertThat(CollectionUtils.isConvertibleToStream(type)).isFalse(); + } - assertThat(result).isSameAs(input); - } + @Test + void isConvertibleToStreamForNull() { + assertThat(CollectionUtils.isConvertibleToStream(null)).isFalse(); + } - @Test - @SuppressWarnings("unchecked") - void toStreamWithDoubleStream() { - var input = DoubleStream.of(42.23); + @Test + void toStreamWithNull() { + Exception exception = assertThrows(PreconditionViolationException.class, + () -> CollectionUtils.toStream(null)); - var result = (Stream) CollectionUtils.toStream(input); + assertThat(exception).hasMessage("Object must not be null"); + } - assertThat(result).containsExactly(42.23); - } + @Test + void toStreamWithUnsupportedObjectType() { + Exception exception = assertThrows(PreconditionViolationException.class, + () -> CollectionUtils.toStream("unknown")); - @Test - @SuppressWarnings("unchecked") - void toStreamWithIntStream() { - var input = IntStream.of(23, 42); + assertThat(exception).hasMessage("Cannot convert instance of java.lang.String into a Stream: unknown"); + } - var result = (Stream) CollectionUtils.toStream(input); + @Test + void toStreamWithExistingStream() { + var input = Stream.of("foo"); - assertThat(result).containsExactly(23, 42); - } + var result = CollectionUtils.toStream(input); + + assertThat(result).isSameAs(input); + } - @Test - @SuppressWarnings("unchecked") - void toStreamWithLongStream() { - var input = LongStream.of(23L, 42L); + @Test + @SuppressWarnings("unchecked") + void toStreamWithDoubleStream() { + var input = DoubleStream.of(42.23); - var result = (Stream) CollectionUtils.toStream(input); + var result = (Stream) CollectionUtils.toStream(input); - assertThat(result).containsExactly(23L, 42L); - } + assertThat(result).containsExactly(42.23); + } - @Test - @SuppressWarnings({ "unchecked", "serial" }) - void toStreamWithCollection() { - var collectionStreamClosed = new AtomicBoolean(false); - Collection input = new ArrayList<>() { + @Test + @SuppressWarnings("unchecked") + void toStreamWithIntStream() { + var input = IntStream.of(23, 42); - { - add("foo"); - add("bar"); - } + var result = (Stream) CollectionUtils.toStream(input); - @Override - public Stream stream() { - return super.stream().onClose(() -> collectionStreamClosed.set(true)); - } - }; + assertThat(result).containsExactly(23, 42); + } - try (var stream = (Stream) CollectionUtils.toStream(input)) { - var result = stream.collect(toList()); - assertThat(result).containsExactly("foo", "bar"); + @Test + @SuppressWarnings("unchecked") + void toStreamWithLongStream() { + var input = LongStream.of(23L, 42L); + + var result = (Stream) CollectionUtils.toStream(input); + + assertThat(result).containsExactly(23L, 42L); } - assertThat(collectionStreamClosed.get()).describedAs("collectionStreamClosed").isTrue(); - } + @Test + @SuppressWarnings({ "unchecked", "serial" }) + void toStreamWithCollection() { + var collectionStreamClosed = new AtomicBoolean(false); + Collection input = new ArrayList<>() { + + { + add("foo"); + add("bar"); + } + + @Override + public Stream stream() { + return super.stream().onClose(() -> collectionStreamClosed.set(true)); + } + }; + + try (var stream = (Stream) CollectionUtils.toStream(input)) { + var result = stream.collect(toList()); + assertThat(result).containsExactly("foo", "bar"); + } - @Test - @SuppressWarnings("unchecked") - void toStreamWithIterable() { + assertThat(collectionStreamClosed.get()).describedAs("collectionStreamClosed").isTrue(); + } - Iterable input = () -> List.of("foo", "bar").iterator(); + @Test + @SuppressWarnings("unchecked") + void toStreamWithIterable() { - var result = (Stream) CollectionUtils.toStream(input); + Iterable input = () -> List.of("foo", "bar").iterator(); - assertThat(result).containsExactly("foo", "bar"); - } + var result = (Stream) CollectionUtils.toStream(input); - @Test - @SuppressWarnings("unchecked") - void toStreamWithIterator() { - var input = List.of("foo", "bar").iterator(); + assertThat(result).containsExactly("foo", "bar"); + } - var result = (Stream) CollectionUtils.toStream(input); + @Test + @SuppressWarnings("unchecked") + void toStreamWithIterator() { + var input = List.of("foo", "bar").iterator(); - assertThat(result).containsExactly("foo", "bar"); - } + var result = (Stream) CollectionUtils.toStream(input); - @Test - @SuppressWarnings("unchecked") - void toStreamWithArray() { - var result = (Stream) CollectionUtils.toStream(new String[] { "foo", "bar" }); + assertThat(result).containsExactly("foo", "bar"); + } - assertThat(result).containsExactly("foo", "bar"); - } + @Test + @SuppressWarnings("unchecked") + void toStreamWithArray() { + var result = (Stream) CollectionUtils.toStream(new String[] { "foo", "bar" }); - @TestFactory - Stream toStreamWithPrimitiveArrays() { - //@formatter:off - return Stream.of( - dynamicTest("boolean[]", - () -> toStreamWithPrimitiveArray(new boolean[] { true, false })), - dynamicTest("byte[]", - () -> toStreamWithPrimitiveArray(new byte[] { 0, Byte.MIN_VALUE, Byte.MAX_VALUE })), - dynamicTest("char[]", - () -> toStreamWithPrimitiveArray(new char[] { 0, Character.MIN_VALUE, Character.MAX_VALUE })), - dynamicTest("double[]", - () -> toStreamWithPrimitiveArray(new double[] { 0, Double.MIN_VALUE, Double.MAX_VALUE })), - dynamicTest("float[]", - () -> toStreamWithPrimitiveArray(new float[] { 0, Float.MIN_VALUE, Float.MAX_VALUE })), - dynamicTest("int[]", - () -> toStreamWithPrimitiveArray(new int[] { 0, Integer.MIN_VALUE, Integer.MAX_VALUE })), - dynamicTest("long[]", - () -> toStreamWithPrimitiveArray(new long[] { 0, Long.MIN_VALUE, Long.MAX_VALUE })), - dynamicTest("short[]", - () -> toStreamWithPrimitiveArray(new short[] { 0, Short.MIN_VALUE, Short.MAX_VALUE })) - ); - //@formatter:on - } + assertThat(result).containsExactly("foo", "bar"); + } - private void toStreamWithPrimitiveArray(Object primitiveArray) { - assertTrue(primitiveArray.getClass().isArray()); - assertTrue(primitiveArray.getClass().getComponentType().isPrimitive()); - var result = CollectionUtils.toStream(primitiveArray).toArray(); - for (var i = 0; i < result.length; i++) { - assertEquals(Array.get(primitiveArray, i), result[i]); + @TestFactory + Stream toStreamWithPrimitiveArrays() { + //@formatter:off + return Stream.of( + dynamicTest("boolean[]", + () -> toStreamWithPrimitiveArray(new boolean[] { true, false })), + dynamicTest("byte[]", + () -> toStreamWithPrimitiveArray(new byte[] { 0, Byte.MIN_VALUE, Byte.MAX_VALUE })), + dynamicTest("char[]", + () -> toStreamWithPrimitiveArray(new char[] { 0, Character.MIN_VALUE, Character.MAX_VALUE })), + dynamicTest("double[]", + () -> toStreamWithPrimitiveArray(new double[] { 0, Double.MIN_VALUE, Double.MAX_VALUE })), + dynamicTest("float[]", + () -> toStreamWithPrimitiveArray(new float[] { 0, Float.MIN_VALUE, Float.MAX_VALUE })), + dynamicTest("int[]", + () -> toStreamWithPrimitiveArray(new int[] { 0, Integer.MIN_VALUE, Integer.MAX_VALUE })), + dynamicTest("long[]", + () -> toStreamWithPrimitiveArray(new long[] { 0, Long.MIN_VALUE, Long.MAX_VALUE })), + dynamicTest("short[]", + () -> toStreamWithPrimitiveArray(new short[] { 0, Short.MIN_VALUE, Short.MAX_VALUE })) + ); + //@formatter:on + } + + private void toStreamWithPrimitiveArray(Object primitiveArray) { + assertTrue(primitiveArray.getClass().isArray()); + assertTrue(primitiveArray.getClass().getComponentType().isPrimitive()); + var result = CollectionUtils.toStream(primitiveArray).toArray(); + for (var i = 0; i < result.length; i++) { + assertEquals(Array.get(primitiveArray, i), result[i]); + } } } - @ParameterizedTest - @CsvSource(delimiter = '|', nullValues = "N/A", textBlock = """ - foo,bar,baz | baz,bar,foo - foo,bar | bar,foo - foo | foo - N/A | N/A - """) - void iteratesListElementsInReverseOrder(@ConvertWith(CommaSeparator.class) List input, - @ConvertWith(CommaSeparator.class) List expected) { - var result = new ArrayList<>(); + @Nested + class ReverseOrderIteration { - CollectionUtils.forEachInReverseOrder(input, result::add); + @ParameterizedTest + @CsvSource(delimiter = '|', nullValues = "N/A", textBlock = """ + foo,bar,baz | baz,bar,foo + foo,bar | bar,foo + foo | foo + N/A | N/A + """) + void iteratesListElementsInReverseOrder(@ConvertWith(CommaSeparator.class) List input, + @ConvertWith(CommaSeparator.class) List expected) { + var result = new ArrayList<>(); - assertEquals(expected, result); - } + CollectionUtils.forEachInReverseOrder(input, result::add); - private static class CommaSeparator implements ArgumentConverter { - @Override - public Object convert(Object source, ParameterContext context) throws ArgumentConversionException { - return source == null ? List.of() : List.of(((String) source).split(",")); + assertEquals(expected, result); + } + + private static class CommaSeparator implements ArgumentConverter { + @Override + public Object convert(Object source, ParameterContext context) throws ArgumentConversionException { + return source == null ? List.of() : List.of(((String) source).split(",")); + } } } } diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java index b10db9682eb2..ef30a6d3db59 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EmptySource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.platform.console.tasks.ConsoleTestExecutor; @@ -31,14 +32,26 @@ class ConsoleLauncherTests { private final StringWriter stringWriter = new StringWriter(); private final PrintWriter printSink = new PrintWriter(stringWriter); - @ParameterizedTest(name = "{0}") + @ParameterizedTest(name = "cmd={0}") + @EmptySource @MethodSource("commandsWithEmptyOptionExitCodes") void displayHelp(String command) { var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); var exitCode = consoleLauncher.run(command, "--help").getExitCode(); assertEquals(0, exitCode); - assertThat(stringWriter.toString()).contains("--help", "--disable-banner" /* ... */); + assertThat(output()).contains("--help"); + } + + @ParameterizedTest(name = "cmd={0}") + @EmptySource + @MethodSource("commandsWithEmptyOptionExitCodes") + void displayVersion(String command) { + var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); + var exitCode = consoleLauncher.run(command, "--version").getExitCode(); + + assertEquals(0, exitCode); + assertThat(output()).contains("JUnit Platform Console Launcher"); } @ParameterizedTest(name = "{0}") @@ -47,7 +60,7 @@ void displayBanner(String command) { var consoleLauncher = new ConsoleLauncher(ConsoleTestExecutor::new, printSink, printSink); consoleLauncher.run(command); - assertThat(stringWriter.toString()).contains("Thanks for using JUnit!"); + assertThat(output()).contains("Thanks for using JUnit!"); } @ParameterizedTest(name = "{0}") @@ -57,7 +70,7 @@ void disableBanner(String command, int expectedExitCode) { var exitCode = consoleLauncher.run(command, "--disable-banner").getExitCode(); assertEquals(expectedExitCode, exitCode); - assertThat(stringWriter.toString()).doesNotContain("Thanks for using JUnit!"); + assertThat(output()).doesNotContain("Thanks for using JUnit!"); } @ParameterizedTest(name = "{0}") @@ -67,7 +80,11 @@ void executeWithUnknownCommandLineOption(String command) { var exitCode = consoleLauncher.run(command, "--all").getExitCode(); assertEquals(-1, exitCode); - assertThat(stringWriter.toString()).contains("Unknown option: '--all'").contains("Usage:"); + assertThat(output()).contains("Unknown option: '--all'").contains("Usage:"); + } + + private String output() { + return stringWriter.toString(); } @ParameterizedTest(name = "{0}") diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java index 0dd2f08502ae..660d35ab1c77 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java @@ -42,6 +42,8 @@ import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.discovery.DiscoverySelectors; /** * @since 1.10 @@ -549,6 +551,32 @@ void parseInvalidConfigurationParametersWithDuplicateKey(ArgsType type) { assertThat(e.getMessage()).isEqualTo("Duplicate key 'foo' for values 'bar' and 'baz'."); } + @ParameterizedTest + @EnumSource + void parseValidSelectorIdentifier(ArgsType type) { + // @formatter:off + assertAll( + () -> assertEquals(List.of(selectClasspathResource("/foo.csv")), parseIdentifiers(type,"--select resource:/foo.csv")), + () -> assertEquals(List.of(selectMethod("com.acme.Foo#m()")), parseIdentifiers(type,"--select method:com.acme.Foo#m()")), + () -> assertEquals(List.of(selectClass("com.acme.Foo")), parseIdentifiers(type,"--select class:com.acme.Foo")), + () -> assertEquals(List.of(selectPackage("com.acme.foo")), parseIdentifiers(type,"--select package:com.acme.foo")), + () -> assertEquals(List.of(selectModule("com.acme.foo")), parseIdentifiers(type,"--select module:com.acme.foo")), + () -> assertEquals(List.of(selectDirectory("foo/bar")), parseIdentifiers(type,"--select directory:foo/bar")), + () -> assertEquals(List.of(selectFile("foo.txt"), selectUri("file:///foo.txt")), parseIdentifiers(type,"--select file:foo.txt --select uri:file:///foo.txt")) + ); + // @formatter:on + } + + private static List parseIdentifiers(ArgsType type, String argLine) + throws IOException { + return DiscoverySelectors.parseAll(type.parseArgLine(argLine).discovery.getSelectorIdentifiers()).toList(); + } + + @Test + void parseInvalidSelectorIdentifier() { + assertOptionWithMissingRequiredArgumentThrowsException("--select"); + } + private void assertOptionWithMissingRequiredArgumentThrowsException(String... options) { assertAll( Stream.of(options).map(opt -> () -> assertThrows(Exception.class, () -> ArgsType.args.parseArgLine(opt)))); @@ -574,6 +602,7 @@ Result parseArgLine(String argLine) throws IOException { } } }; + abstract Result parseArgLine(String argLine) throws IOException; private static String[] split(String argLine) { diff --git a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java index 21a359327bb8..0218d704458d 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/tasks/DiscoveryRequestCreatorTests.java @@ -303,6 +303,19 @@ void propagatesIterationSelectors() { assertThat(iterationSelectors.get(1).getIterationIndices()).containsExactly(2); } + @Test + void propagatesSelectorIdentifiers() { + var methodSelector = selectMethod("com.acme.Foo#m()"); + var classSelector = selectClass("com.example.Bar"); + options.setSelectorIdentifiers( + List.of(methodSelector.toIdentifier().orElseThrow(), classSelector.toIdentifier().orElseThrow())); + + var request = convert(); + + assertThat(request.getSelectorsByType(MethodSelector.class)).containsExactly(methodSelector); + assertThat(request.getSelectorsByType(ClassSelector.class)).containsExactly(classSelector); + } + @Test @SuppressWarnings("deprecation") void convertsConfigurationParameters() { diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java index 2954fb3577e2..5a1a96506927 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/DiscoverySelectorsTests.java @@ -11,9 +11,12 @@ package org.junit.platform.engine.discovery; import static java.lang.String.join; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.engine.discovery.JupiterUniqueIdBuilder.uniqueIdForMethod; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClasspathResource; @@ -26,6 +29,7 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectNestedClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectNestedMethod; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUri; import java.io.File; @@ -33,7 +37,9 @@ import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -49,6 +55,8 @@ import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.test.TestClassLoader; import org.junit.platform.commons.util.ReflectionUtils; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; /** * Unit tests for {@link DiscoverySelectors}. @@ -73,11 +81,11 @@ void selectUriByName() { } @Test - void selectUriByURI() throws Exception { + void selectUriByURI() { assertViolatesPrecondition(() -> selectUri((URI) null)); assertViolatesPrecondition(() -> selectUri(" ")); - var uri = new URI("https://junit.org"); + var uri = URI.create("https://junit.org"); var selector = selectUri(uri); assertEquals(uri, selector.getUri()); @@ -88,6 +96,15 @@ void selectUriByURI() throws Exception { @Nested class SelectFileTests { + @Test + void parseUriSelector() { + var selector = parseIdentifier(selectUri("https://junit.org")); + assertThat(selector) // + .asInstanceOf(type(UriSelector.class)) // + .extracting(UriSelector::getUri) // + .isEqualTo(URI.create("https://junit.org")); + } + @Test void selectFileByName() { assertViolatesPrecondition(() -> selectFile((String) null)); @@ -113,7 +130,7 @@ void selectFileByNameAndPosition() { assertEquals(path, selector.getRawPath()); assertEquals(new File(path), selector.getFile()); assertEquals(Paths.get(path), selector.getPath()); - assertEquals(filePosition, selector.getPosition().get()); + assertEquals(filePosition, selector.getPosition().orElseThrow()); } @Test @@ -147,7 +164,7 @@ void selectFileByFileReferenceAndPosition() throws Exception { assertEquals(path, selector.getRawPath()); assertEquals(file.getCanonicalFile(), selector.getFile()); assertEquals(Paths.get(path), selector.getPath()); - assertEquals(FilePosition.from(12, 34), selector.getPosition().get()); + assertEquals(FilePosition.from(12, 34), selector.getPosition().orElseThrow()); } } @@ -155,6 +172,59 @@ void selectFileByFileReferenceAndPosition() throws Exception { @Nested class SelectDirectoryTests { + @Test + void parseFileSelectorWithRelativePath() { + var path = "src/test/resources/do_not_delete_me.txt"; + var selector = parseIdentifier(selectFile(path)); + + assertThat(selector) // + .asInstanceOf(type(FileSelector.class)) // + .extracting(FileSelector::getRawPath, FileSelector::getFile, FileSelector::getPath, + FileSelector::getPosition) // + .containsExactly(path, new File(path), Paths.get(path), Optional.empty()); + } + + @Test + void parseFileSelectorWithAbsolutePath() { + var path = "src/test/resources/do_not_delete_me.txt"; + var absolutePath = new File(path).getAbsolutePath(); + var selector = parseIdentifier(selectFile(absolutePath)); + + assertThat(selector) // + .asInstanceOf(type(FileSelector.class)) // + .extracting(FileSelector::getRawPath, FileSelector::getFile, FileSelector::getPath, + FileSelector::getPosition) // + .containsExactly(absolutePath, new File(absolutePath), Paths.get(absolutePath), Optional.empty()); + } + + @Test + void parseFileSelectorWithRelativePathAndFilePosition() { + var path = "src/test/resources/do_not_delete_me.txt"; + var filePosition = FilePosition.from(12, 34); + var selector = parseIdentifier(selectFile(path, filePosition)); + + assertThat(selector) // + .asInstanceOf(type(FileSelector.class)) // + .extracting(FileSelector::getRawPath, FileSelector::getFile, FileSelector::getPath, + FileSelector::getPosition) // + .containsExactly(path, new File(path), Paths.get(path), Optional.of(filePosition)); + } + + @Test + void parseFileSelectorWithAbsolutePathAndFilePosition() { + var path = "src/test/resources/do_not_delete_me.txt"; + var absolutePath = new File(path).getAbsolutePath(); + var filePosition = FilePosition.from(12, 34); + var selector = parseIdentifier(selectFile(absolutePath, filePosition)); + + assertThat(selector) // + .asInstanceOf(type(FileSelector.class)) // + .extracting(FileSelector::getRawPath, FileSelector::getFile, FileSelector::getPath, + FileSelector::getPosition) // + .containsExactly(absolutePath, new File(absolutePath), Paths.get(absolutePath), + Optional.of(filePosition)); + } + @Test void selectDirectoryByName() { assertViolatesPrecondition(() -> selectDirectory((String) null)); @@ -189,6 +259,32 @@ void selectDirectoryByFileReference() throws Exception { @Nested class SelectClasspathResourceTests { + @Test + void parseDirectorySelectorWithRelativePath() { + var path = "src/test/resources"; + + var selector = parseIdentifier(selectDirectory(path)); + + assertThat(selector) // + .asInstanceOf(type(DirectorySelector.class)) // + .extracting(DirectorySelector::getRawPath, DirectorySelector::getDirectory, + DirectorySelector::getPath) // + .containsExactly(path, new File(path), Paths.get(path)); + } + + @Test + void parseDirectorySelectorWithAbsolutePath() { + var path = new File("src/test/resources").getAbsolutePath(); + + var selector = parseIdentifier(selectDirectory(path)); + + assertThat(selector) // + .asInstanceOf(type(DirectorySelector.class)) // + .extracting(DirectorySelector::getRawPath, DirectorySelector::getDirectory, + DirectorySelector::getPath) // + .containsExactly(path, new File(path), Paths.get(path)); + } + @Test void selectClasspathResources() { assertViolatesPrecondition(() -> selectClasspathResource(null)); @@ -216,12 +312,51 @@ void selectClasspathResourcesWithFilePosition() { // with unnecessary "/" prefix var selector = selectClasspathResource("/foo/bar/spec.xml", filePosition); assertEquals("foo/bar/spec.xml", selector.getClasspathResourceName()); - assertEquals(FilePosition.from(12, 34), selector.getPosition().get()); + assertEquals(FilePosition.from(12, 34), selector.getPosition().orElseThrow()); // standard use case selector = selectClasspathResource("A/B/C/spec.json", filePosition); assertEquals("A/B/C/spec.json", selector.getClasspathResourceName()); - assertEquals(filePosition, selector.getPosition().get()); + assertEquals(filePosition, selector.getPosition().orElseThrow()); + } + + @Test + void parseClasspathResources() { + // with unnecessary "/" prefix + var selector = parseIdentifier(selectClasspathResource("/foo/bar/spec.xml")); + assertThat(selector) // + .asInstanceOf(type(ClasspathResourceSelector.class)) // + .extracting(ClasspathResourceSelector::getClasspathResourceName, + ClasspathResourceSelector::getPosition) // + .containsExactly("foo/bar/spec.xml", Optional.empty()); + + // standard use case + selector = parseIdentifier(selectClasspathResource("A/B/C/spec.json")); + assertThat(selector) // + .asInstanceOf(type(ClasspathResourceSelector.class)) // + .extracting(ClasspathResourceSelector::getClasspathResourceName, + ClasspathResourceSelector::getPosition) // + .containsExactly("A/B/C/spec.json", Optional.empty()); + } + + @Test + void parseClasspathResourcesWithFilePosition() { + var filePosition = FilePosition.from(12, 34); + // with unnecessary "/" prefix + var selector = parseIdentifier(selectClasspathResource("/foo/bar/spec.xml", FilePosition.from(12, 34))); + assertThat(selector) // + .asInstanceOf(type(ClasspathResourceSelector.class)) // + .extracting(ClasspathResourceSelector::getClasspathResourceName, + ClasspathResourceSelector::getPosition) // + .containsExactly("foo/bar/spec.xml", Optional.of(filePosition)); + + // standard use case + selector = parseIdentifier(selectClasspathResource("A/B/C/spec.json", FilePosition.from(12, 34))); + assertThat(selector) // + .asInstanceOf(type(ClasspathResourceSelector.class)) // + .extracting(ClasspathResourceSelector::getClasspathResourceName, + ClasspathResourceSelector::getPosition) // + .containsExactly("A/B/C/spec.json", Optional.of(filePosition)); } } @@ -235,6 +370,15 @@ void selectModuleByName() { assertEquals("java.base", selector.getModuleName()); } + @Test + void parseModuleByName() { + var selector = parseIdentifier(selectModule("java.base")); + assertThat(selector) // + .asInstanceOf(type(ModuleSelector.class)) // + .extracting(ModuleSelector::getModuleName) // + .isEqualTo("java.base"); + } + @Test void selectModuleByNamePreconditions() { assertViolatesPrecondition(() -> selectModule(null)); @@ -266,6 +410,15 @@ void selectPackageByName() { assertEquals(getClass().getPackage().getName(), selector.getPackageName()); } + @Test + void parsePackageByName() { + var selector = parseIdentifier(selectPackage(getClass().getPackage().getName())); + assertThat(selector) // + .asInstanceOf(type(PackageSelector.class)) // + .extracting(PackageSelector::getPackageName) // + .isEqualTo(getClass().getPackage().getName()); + } + } @Nested @@ -294,7 +447,7 @@ void selectClasspathRootsWithExistingDirectory(@TempDir Path tempDir) { @Test void selectClasspathRootsWithExistingJarFile() throws Exception { - var jarUri = getClass().getResource("/jartest.jar").toURI(); + var jarUri = requireNonNull(getClass().getResource("/jartest.jar")).toURI(); var jarFile = Paths.get(jarUri); var selectors = selectClasspathRoots(Set.of(jarFile)); @@ -313,6 +466,15 @@ void selectClassByName() { assertEquals(getClass(), selector.getJavaClass()); } + @Test + void pareClassByName() { + var selector = parseIdentifier(selectClass(getClass())); + assertThat(selector) // + .asInstanceOf(type(ClassSelector.class)) // + .extracting(ClassSelector::getJavaClass) // + .isEqualTo(getClass()); + } + @Test void selectClassByNameWithExplicitClassLoader() throws Exception { try (var testClassLoader = TestClassLoader.forClasses(getClass())) { @@ -422,7 +584,8 @@ static Stream invalidFullyQualifiedMethodNames() { void selectMethodByFullyQualifiedName() throws Exception { Class clazz = testClass(); var method = clazz.getDeclaredMethod("myTest"); - assertSelectMethodByFullyQualifiedName(clazz, method); + var selector = assertSelectMethodByFullyQualifiedName(clazz, method); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -445,88 +608,110 @@ void selectMethodByFullyQualifiedNameWithExplicitClassLoader() throws Exception void selectMethodByFullyQualifiedNameForDefaultMethodInInterface() throws Exception { Class clazz = TestCaseWithDefaultMethod.class; var method = clazz.getMethod("myTest"); - assertSelectMethodByFullyQualifiedName(clazz, method); + var selector = assertSelectMethodByFullyQualifiedName(clazz, method); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithPrimitiveParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", int.class); - assertSelectMethodByFullyQualifiedName(testClass(), method, int.class, "int"); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, int.class, "int"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithPrimitiveParameterUsingSourceCodeSyntax() throws Exception { var method = testClass().getDeclaredMethod("myTest", int.class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "int", "int"); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "int", "int"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithObjectParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", String.class); - assertSelectMethodByFullyQualifiedName(testClass(), method, String.class, String.class.getName()); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, String.class, + String.class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithObjectParameterUsingSourceCodeSyntax() throws Exception { var method = testClass().getDeclaredMethod("myTest", String.class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.String", String.class.getName()); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.String", + String.class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithPrimitiveArrayParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", int[].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, int[].class, int[].class.getName()); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, int[].class, + int[].class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithPrimitiveArrayParameterUsingSourceCodeSyntax() throws Exception { var method = testClass().getDeclaredMethod("myTest", int[].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "int[]", "int[]"); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "int[]", "int[]"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithObjectArrayParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", String[].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, String[].class, String[].class.getName()); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, String[].class, + String[].class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithObjectArrayParameterUsingSourceCodeSyntax() throws Exception { var method = testClass().getDeclaredMethod("myTest", String[].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.String[]", "java.lang.String[]"); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.String[]", + "java.lang.String[]"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithTwoDimensionalPrimitiveArrayParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", int[][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, int[][].class, int[][].class.getName()); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, int[][].class, + int[][].class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithTwoDimensionalPrimitiveArrayParameterUsingSourceCodeSyntax() throws Exception { var method = testClass().getDeclaredMethod("myTest", int[][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "int[][]", "int[][]"); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "int[][]", "int[][]"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithTwoDimensionalObjectArrayParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", String[][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, String[][].class, String[][].class.getName()); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, String[][].class, + String[][].class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithTwoDimensionalObjectArrayParameterUsingSourceCodeSyntax() throws Exception { var method = testClass().getDeclaredMethod("myTest", String[][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.String[][]", "java.lang.String[][]"); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.String[][]", + "java.lang.String[][]"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithMultidimensionalPrimitiveArrayParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", int[][][][][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, int[][][][][].class, + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, int[][][][][].class, int[][][][][].class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -534,14 +719,17 @@ void selectMethodByFullyQualifiedNameWithMultidimensionalPrimitiveArrayParameter throws Exception { var method = testClass().getDeclaredMethod("myTest", int[][][][][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "int[][][][][]", "int[][][][][]"); + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "int[][][][][]", + "int[][][][][]"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameWithMultidimensionalObjectArrayParameter() throws Exception { var method = testClass().getDeclaredMethod("myTest", Double[][][][][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, Double[][][][][].class, + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, Double[][][][][].class, Double[][][][][].class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -549,15 +737,16 @@ void selectMethodByFullyQualifiedNameWithMultidimensionalObjectArrayParameterUsi throws Exception { var method = testClass().getDeclaredMethod("myTest", Double[][][][][].class); - assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.Double[][][][][]", + var selector = assertSelectMethodByFullyQualifiedName(testClass(), method, "java.lang.Double[][][][][]", "java.lang.Double[][][][][]"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test void selectMethodByFullyQualifiedNameEndingInOpeningParenthesis() { var className = "org.example.MyClass"; // The following bizarre method name is not permissible in Java source - // code; however, it's permitted by the JVM -- for example, in Groovy + // code; however, it'selector permitted by the JVM -- for example, in Groovy // or Kotlin source code using back ticks. var methodName = ")--("; var fqmn = className + "#" + methodName; @@ -566,6 +755,7 @@ void selectMethodByFullyQualifiedNameEndingInOpeningParenthesis() { assertEquals(className, selector.getClassName()); assertEquals(methodName, selector.getMethodName()); assertEquals("", selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } /** @@ -581,6 +771,7 @@ void selectMethodByFullyQualifiedNameContainingHashtags() { assertEquals(className, selector.getClassName()); assertEquals(methodName, selector.getMethodName()); assertEquals("", selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } /** @@ -597,6 +788,7 @@ void selectMethodByFullyQualifiedNameContainingHashtagsAndWithParameterList() { assertEquals(className, selector.getClassName()); assertEquals(methodName, selector.getMethodName()); assertEquals(methodParameters, selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } /** @@ -613,6 +805,7 @@ void selectMethodByFullyQualifiedNameContainingParentheses() { assertEquals(className, selector.getClassName()); assertEquals(methodName, selector.getMethodName()); assertEquals("", selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } /** @@ -629,6 +822,7 @@ void selectMethodByFullyQualifiedNameEndingWithParentheses() { assertEquals(className, selector.getClassName()); assertEquals(methodName, selector.getMethodName()); assertEquals("", selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } /** @@ -646,19 +840,21 @@ void selectMethodByFullyQualifiedNameEndingWithParenthesesAndWithParameterList() assertEquals(className, selector.getClassName()); assertEquals(methodName, selector.getMethodName()); assertEquals(methodParameters, selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } - private void assertSelectMethodByFullyQualifiedName(Class clazz, Method method) { + private MethodSelector assertSelectMethodByFullyQualifiedName(Class clazz, Method method) { var selector = selectMethod(fqmn(clazz, method.getName())); assertEquals(method, selector.getJavaMethod()); assertEquals(clazz, selector.getJavaClass()); assertEquals(clazz.getName(), selector.getClassName()); assertEquals(method.getName(), selector.getMethodName()); assertEquals("", selector.getParameterTypeNames()); + return selector; } - private void assertSelectMethodByFullyQualifiedName(Class clazz, Method method, Class parameterType, - String expectedParameterTypes) { + private MethodSelector assertSelectMethodByFullyQualifiedName(Class clazz, Method method, + Class parameterType, String expectedParameterTypes) { var selector = selectMethod(fqmn(parameterType)); assertEquals(method, selector.getJavaMethod()); @@ -666,10 +862,11 @@ private void assertSelectMethodByFullyQualifiedName(Class clazz, Method metho assertEquals(clazz.getName(), selector.getClassName()); assertEquals(method.getName(), selector.getMethodName()); assertEquals(expectedParameterTypes, selector.getParameterTypeNames()); + return selector; } - private void assertSelectMethodByFullyQualifiedName(Class clazz, Method method, String parameterName, - String expectedParameterTypes) { + private MethodSelector assertSelectMethodByFullyQualifiedName(Class clazz, Method method, + String parameterName, String expectedParameterTypes) { var selector = selectMethod(fqmnWithParamNames(parameterName)); assertEquals(method, selector.getJavaMethod()); @@ -677,6 +874,7 @@ private void assertSelectMethodByFullyQualifiedName(Class clazz, Method metho assertEquals(clazz.getName(), selector.getClassName()); assertEquals(method.getName(), selector.getMethodName()); assertEquals(expectedParameterTypes, selector.getParameterTypeNames()); + return selector; } @Test @@ -689,6 +887,7 @@ void selectMethodByClassAndMethodName() throws Exception { assertEquals(method, selector.getJavaMethod()); assertEquals("myTest", selector.getMethodName()); assertEquals("", selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -704,6 +903,7 @@ void selectMethodByClassMethodNameAndParameterTypeNames() throws Exception { assertThat(selector.getJavaMethod()).isEqualTo(method); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -719,6 +919,7 @@ void selectMethodByClassNameMethodNameAndParameterTypes() throws Exception { assertThat(selector.getJavaMethod()).isEqualTo(method); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -739,6 +940,7 @@ void selectMethodByClassNameMethodNameAndParameterTypeNamesWithExplicitClassLoad assertThat(selector.getJavaMethod()).isEqualTo(method); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } } @@ -755,6 +957,7 @@ void selectMethodByClassMethodNameAndParameterTypes() throws Exception { assertThat(selector.getJavaMethod()).isEqualTo(method); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -770,6 +973,7 @@ void selectMethodWithParametersByMethodReference() throws Exception { assertThat(selector.getJavaMethod()).isEqualTo(method); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String, boolean[]"); assertThat(selector.getParameterTypes()).containsExactly(String.class, boolean[].class); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -788,6 +992,7 @@ void selectMethodByClassAndNameForSpockSpec() { assertEquals(className, selector.getClassName()); assertEquals(methodName, selector.getMethodName()); assertEquals("", selector.getParameterTypeNames()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } private static Class testClass() { @@ -796,6 +1001,41 @@ private static Class testClass() { } + @Test + void parseClasspathRootsWithNonExistingDirectory() { + var selectorStream = parseIdentifiers(selectClasspathRoots(Set.of(Path.of("some/local/path")))); + assertThat(selectorStream).isEmpty(); + } + + @Test + void parseClasspathRootsWithNonExistingJarFile() { + var selectorStream = parseIdentifiers(selectClasspathRoots(Set.of(Path.of("some.jar")))); + assertThat(selectorStream).isEmpty(); + } + + @Test + void parseClasspathRootsWithExistingDirectory(@TempDir Path tempDir) { + var selectorStream = parseIdentifiers(selectClasspathRoots(Set.of(tempDir))); + var selector = selectorStream.findAny().orElseThrow(); + assertThat(selector) // + .asInstanceOf(type(ClasspathRootSelector.class)) // + .extracting(ClasspathRootSelector::getClasspathRoot) // + .isEqualTo(tempDir.toUri()); + } + + @Test + void parseClasspathRootsWithExistingJarFile() throws Exception { + var jarUri = requireNonNull(getClass().getResource("/jartest.jar")).toURI(); + var jarPath = Path.of(jarUri); + + var selectorStream = parseIdentifiers(selectClasspathRoots(Set.of(jarPath))); + var selector = selectorStream.findAny().orElseThrow(); + assertThat(selector) // + .asInstanceOf(type(ClasspathRootSelector.class)) // + .extracting(ClasspathRootSelector::getClasspathRoot) // + .isEqualTo(jarUri); + } + @Nested class SelectNestedClassAndSelectNestedMethodTests { @@ -813,6 +1053,7 @@ void selectNestedClassByClassNames() { assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -831,6 +1072,7 @@ void selectNestedClassByClassNamesWithExplicitClassLoader() throws Exception { assertThat(selector.getEnclosingClasses()).extracting(Class::getClassLoader).containsOnly( testClassLoader); assertThat(selector.getNestedClass().getClassLoader()).isSameAs(testClassLoader); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } } @@ -845,6 +1087,7 @@ void selectDoubleNestedClassByClassNames() { assertThat(selector.getEnclosingClassNames()).containsExactly(enclosingClassName, nestedClassName); assertThat(selector.getNestedClassName()).isEqualTo(doubleNestedClassName); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -867,6 +1110,7 @@ void selectNestedMethodByEnclosingClassNamesAndMethodName() throws Exception { assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); assertThat(selector.getMethodName()).isEqualTo(methodName); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -888,6 +1132,7 @@ void selectNestedMethodByEnclosingClassNamesAndMethodNameWithExplicitClassLoader assertThat(selector.getNestedClass().getClassLoader()).isSameAs(testClassLoader); assertThat(selector.getMethodName()).isEqualTo(methodName); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } } @@ -903,6 +1148,7 @@ void selectNestedMethodByEnclosingClassesAndMethodName() throws Exception { assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); assertThat(selector.getMethodName()).isEqualTo(methodName); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -918,6 +1164,7 @@ void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypeNames() th assertThat(selector.getEnclosingClassNames()).containsOnly(enclosingClassName); assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); assertThat(selector.getMethodName()).isEqualTo(methodName); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } /** @@ -937,6 +1184,7 @@ void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypes() throws assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); assertThat(selector.getMethodName()).isEqualTo(methodName); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } /** @@ -965,6 +1213,7 @@ void selectNestedMethodByEnclosingClassNamesMethodNameAndParameterTypeNamesWithE assertThat(selector.getMethodName()).isEqualTo(methodName); assertThat(selector.getParameterTypeNames()).isEqualTo(String.class.getName()); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } } @@ -986,6 +1235,7 @@ void selectNestedMethodByEnclosingClassesMethodNameAndParameterTypes() throws Ex assertThat(selector.getNestedClassName()).isEqualTo(nestedClassName); assertThat(selector.getMethodName()).isEqualTo(methodName); assertThat(selector.getParameterTypeNames()).isEqualTo("java.lang.String"); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -1004,6 +1254,7 @@ void selectDoubleNestedMethodByEnclosingClassNamesAndMethodName() throws Excepti assertThat(selector.getEnclosingClassNames()).containsExactly(enclosingClassName, nestedClassName); assertThat(selector.getNestedClassName()).isEqualTo(doubleNestedClassName); assertThat(selector.getMethodName()).isEqualTo(doubleNestedMethodName); + assertThat(parseIdentifier(selector)).isEqualTo(selector); } @Test @@ -1071,6 +1322,7 @@ void selectNestedMethodByEnclosingClassesClassMethodNameAndParameterTypesPrecond static class ClassWithNestedInnerClass { // @Nested + @SuppressWarnings({ "InnerClassMayBeStatic", "unused" }) class NestedClass { // @Test @@ -1093,12 +1345,50 @@ void doubleNestedTest() { } + @Nested + class SelectIterationTests { + @Test + void selectsIteration() throws Exception { + Class clazz = DiscoverySelectorsTests.class; + var method = clazz.getDeclaredMethod("myTest", int.class); + var parentSelector = selectMethod(clazz, method); + var selector = DiscoverySelectors.selectIteration(parentSelector, 23, 42); + assertThat(selector.getParentSelector()).isSameAs(parentSelector); + assertThat(selector.getIterationIndices()).containsExactly(23, 42); + assertThat(parseIdentifier(selector)).isEqualTo(selector); + } + } + + @Nested + class SelectUniqueIdTests { + @Test + void selectsUniqueId() { + var selector = selectUniqueId(uniqueIdForMethod(DiscoverySelectorsTests.class, "myTest(int)")); + assertThat(selector.getUniqueId()).isNotNull(); + assertThat(parseIdentifier(selector)).isEqualTo(selector); + } + } + // ------------------------------------------------------------------------- private void assertViolatesPrecondition(Executable precondition) { assertThrows(PreconditionViolationException.class, precondition); } + private static DiscoverySelector parseIdentifier(DiscoverySelector selector) { + return DiscoverySelectors.parse(toIdentifierString(selector)).orElseThrow(); + } + + private static Stream parseIdentifiers( + Collection selectors) { + return DiscoverySelectors.parseAll( + selectors.stream().map(it -> DiscoverySelectorIdentifier.parse(toIdentifierString(it))).toList()); + } + + private static String toIdentifierString(DiscoverySelector selector) { + return selector.toIdentifier().orElseThrow().toString(); + } + private static String fqmn(Class... params) { return fqmn(DiscoverySelectorsTests.class, "myTest", params); } @@ -1118,37 +1408,48 @@ default void myTest() { } } + private static class TestCaseWithDefaultMethod implements TestInterface { } + @SuppressWarnings("unused") void myTest() { } + @SuppressWarnings("unused") void myTest(int num) { } + @SuppressWarnings("unused") void myTest(int[] nums) { } + @SuppressWarnings("unused") void myTest(int[][] grid) { } + @SuppressWarnings("unused") void myTest(int[][][][][] grid) { } + @SuppressWarnings("unused") void myTest(String info) { } + @SuppressWarnings("unused") void myTest(String info, boolean[] flags) { } + @SuppressWarnings("unused") void myTest(String[] info) { } + @SuppressWarnings("unused") void myTest(String[][] info) { } + @SuppressWarnings("unused") void myTest(Double[][][][][] data) { } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java new file mode 100644 index 000000000000..9eb66c820c2a --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/discovery/IterationSelectorTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2015-2024 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.platform.engine.discovery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectIteration; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Array; +import java.util.Optional; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.aggregator.AggregateWith; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.aggregator.ArgumentsAggregationException; +import org.junit.jupiter.params.aggregator.ArgumentsAggregator; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.engine.DiscoverySelector; +import org.junit.platform.engine.DiscoverySelectorIdentifier; + +class IterationSelectorTests { + + @ParameterizedTest + @CsvSource(delimiter = '|', textBlock = """ + 1 | 1 + 1,2 | 1 | 2 + 1..3 | 1 | 2 | 3 + 1,3 | 1 | 3 + 1..3,5..7 | 1 | 2 | 3 | 5 | 6 | 7 + """) + void collapsesRangesWhenConvertingToIdentifier(String expected, + @AggregateWith(VarargsAggregator.class) int... iterationIndices) { + var parent = "parent:value"; + var parentSelector = selectorWithIdentifier(parent); + var selector = selectIteration(parentSelector, iterationIndices); + + var identifier = selector.toIdentifier().orElseThrow(); + assertEquals("iteration:%s[%s]".formatted(parent, expected), identifier.toString()); + + DiscoverySelectorIdentifierParser.Context context = mock(); + when(context.parse(parent)).thenAnswer(__ -> Optional.of(parentSelector)); + assertEquals(selector, new IterationSelector.IdentifierParser().parse(identifier, context).orElseThrow()); + } + + private static DiscoverySelector selectorWithIdentifier(String identifier) { + DiscoverySelector parent = mock(); + when(parent.toIdentifier()) // + .thenReturn(Optional.of(DiscoverySelectorIdentifier.parse(identifier))); + return parent; + } + + private static class VarargsAggregator implements ArgumentsAggregator { + @Override + public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) + throws ArgumentsAggregationException { + Class parameterType = context.getParameter().getType(); + Preconditions.condition(parameterType.isArray(), () -> "must be an array type, but was " + parameterType); + Class componentType = parameterType.getComponentType(); + IntStream indices = IntStream.range(context.getIndex(), accessor.size()); + if (componentType == int.class) { + return indices.map(accessor::getInteger).toArray(); + } + return indices.mapToObj(index -> accessor.get(index, componentType)).toArray( + size -> (Object[]) Array.newInstance(componentType, size)); + } + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java index c4c87785fe4f..9f18083a924c 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java @@ -11,14 +11,15 @@ package org.junit.platform.engine.support.store; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.platform.commons.test.ConcurrencyTestingUtils.executeConcurrently; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -27,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -370,6 +372,11 @@ void additionNamespacePartMakesADifference() { @Nested class CloseActionTests { + @BeforeEach + void prerequisites() { + assertNotClosed(); + } + @Test void callsCloseActionInReverseInsertionOrderWhenClosingStore() throws Throwable { store.put(namespace, "key1", "value1"); @@ -378,10 +385,14 @@ void callsCloseActionInReverseInsertionOrderWhenClosingStore() throws Throwable verifyNoInteractions(closeAction); store.close(); + assertClosed(); + var inOrder = inOrder(closeAction); inOrder.verify(closeAction).close(namespace, "key3", "value3"); inOrder.verify(closeAction).close(namespace, "key2", "value2"); inOrder.verify(closeAction).close(namespace, "key1", "value1"); + + verifyNoMoreInteractions(closeAction); } @Test @@ -390,6 +401,7 @@ void doesNotCallCloseActionForRemovedValues() { store.remove(namespace, key); store.close(); + assertClosed(); verifyNoInteractions(closeAction); } @@ -400,6 +412,7 @@ void doesNotCallCloseActionForReplacedValues() throws Throwable { store.put(namespace, key, "value2"); store.close(); + assertClosed(); verify(closeAction).close(namespace, key, "value2"); verifyNoMoreInteractions(closeAction); @@ -410,34 +423,137 @@ void doesNotCallCloseActionForNullValues() { store.put(namespace, key, null); store.close(); + assertClosed(); verifyNoInteractions(closeAction); } @Test - void ignoresStoredValuesThatThrewExceptionsDuringCleanup() { - assertThrows(RuntimeException.class, () -> store.getOrComputeIfAbsent(namespace, key, __ -> { + void doesNotCallCloseActionForValuesThatThrowExceptionsDuringCleanup() throws Throwable { + store.put(namespace, "key1", "value1"); + assertThrows(RuntimeException.class, () -> store.getOrComputeIfAbsent(namespace, "key2", __ -> { throw new RuntimeException("boom"); })); + store.put(namespace, "key3", "value3"); - assertDoesNotThrow(store::close); + store.close(); + assertClosed(); - verifyNoInteractions(closeAction); + var inOrder = inOrder(closeAction); + inOrder.verify(closeAction).close(namespace, "key3", "value3"); + inOrder.verify(closeAction).close(namespace, "key1", "value1"); + + verifyNoMoreInteractions(closeAction); } @Test - void doesNotIgnoreStoredValuesThatThrewUnrecoverableFailuresDuringCleanup() { - assertThrows(OutOfMemoryError.class, () -> store.getOrComputeIfAbsent(namespace, key, __ -> { - throw new OutOfMemoryError(); + void abortsCloseIfAnyStoredValueThrowsAnUnrecoverableExceptionDuringCleanup() throws Throwable { + store.put(namespace, "key1", "value1"); + assertThrows(OutOfMemoryError.class, () -> store.getOrComputeIfAbsent(namespace, "key2", __ -> { + throw new OutOfMemoryError("boom"); })); + store.put(namespace, "key3", "value3"); + + assertThrows(OutOfMemoryError.class, store::close); + assertClosed(); + + verifyNoInteractions(closeAction); + + store.close(); + assertClosed(); + } + + @Test + void closesStoreEvenIfCloseActionThrowsException() throws Throwable { + store.put(namespace, key, value); + doThrow(IllegalStateException.class).when(closeAction).close(namespace, key, value); + + assertThrows(IllegalStateException.class, store::close); + assertClosed(); + + verify(closeAction).close(namespace, key, value); + verifyNoMoreInteractions(closeAction); + + store.close(); + assertClosed(); + } + + @Test + void closesStoreEvenIfCloseActionThrowsUnrecoverableException() throws Throwable { + store.put(namespace, key, value); + doThrow(OutOfMemoryError.class).when(closeAction).close(namespace, key, value); assertThrows(OutOfMemoryError.class, store::close); + assertClosed(); + + verify(closeAction).close(namespace, key, value); + verifyNoMoreInteractions(closeAction); + + store.close(); + assertClosed(); + } + + @Test + void closesStoreEvenIfNoCloseActionIsConfigured() { + @SuppressWarnings("resource") + var localStore = new NamespacedHierarchicalStore<>(null); + assertThat(localStore.isClosed()).isFalse(); + localStore.close(); + assertThat(localStore.isClosed()).isTrue(); + } + + @Test + void closeIsIdempotent() throws Throwable { + store.put(namespace, key, value); verifyNoInteractions(closeAction); + + store.close(); + assertClosed(); + + verify(closeAction, times(1)).close(namespace, key, value); + + store.close(); + assertClosed(); + + verifyNoMoreInteractions(closeAction); } + + @Test + void rejectsModificationAfterClose() { + store.close(); + assertClosed(); + + assertThrows(NamespacedHierarchicalStoreException.class, () -> store.put(namespace, "key1", "value1")); + assertThrows(NamespacedHierarchicalStoreException.class, () -> store.remove(namespace, "key1")); + assertThrows(NamespacedHierarchicalStoreException.class, + () -> store.remove(namespace, "key1", Number.class)); + } + + @Test + void rejectsQueryAfterClose() { + store.close(); + assertClosed(); + + assertThrows(NamespacedHierarchicalStoreException.class, () -> store.get(namespace, "key1")); + assertThrows(NamespacedHierarchicalStoreException.class, () -> store.get(namespace, "key1", Integer.class)); + assertThrows(NamespacedHierarchicalStoreException.class, + () -> store.getOrComputeIfAbsent(namespace, "key1", k -> "value")); + assertThrows(NamespacedHierarchicalStoreException.class, + () -> store.getOrComputeIfAbsent(namespace, "key1", k -> 1337, Integer.class)); + } + + private void assertNotClosed() { + assertThat(store.isClosed()).as("closed").isFalse(); + } + + private void assertClosed() { + assertThat(store.isClosed()).as("closed").isTrue(); + } + } - private Object createObject(final String display) { + private static Object createObject(String display) { return new Object() { @Override diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/TestIdentifierTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/TestIdentifierTests.java index d971eb7a8e63..7ba6716495e6 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/TestIdentifierTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/TestIdentifierTests.java @@ -72,6 +72,21 @@ void initialVersionCanBeDeserialized() throws Exception { } } + @Test + void identifierWithNoParentCanBeSerializedAndDeserialized() throws Exception { + TestIdentifier originalIdentifier = TestIdentifier.from( + new AbstractTestDescriptor(UniqueId.root("example", "id"), "Example") { + @Override + public Type getType() { + return Type.CONTAINER; + } + }); + + var deserializedIdentifier = (TestIdentifier) deserialize(serialize(originalIdentifier)); + + assertDeepEquals(originalIdentifier, deserializedIdentifier); + } + private static void assertDeepEquals(TestIdentifier first, TestIdentifier second) { assertEquals(first, second); assertEquals(first.getUniqueId(), second.getUniqueId()); diff --git a/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java b/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java index 346fef033166..b766c4863b3a 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/commons/SuiteLauncherDiscoveryRequestBuilderTests.java @@ -67,6 +67,7 @@ import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.IncludePackages; import org.junit.platform.suite.api.IncludeTags; +import org.junit.platform.suite.api.Select; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.SelectDirectories; @@ -585,6 +586,28 @@ class Suite { assertEquals(Optional.empty(), configurationParameters.get("parent")); } + @Test + void selectByIdentifier() { + // @formatter:off + @Select({ + "class:org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$NonLocalTestCase", + "method:org.junit.platform.suite.commons.SuiteLauncherDiscoveryRequestBuilderTests$NoParameterTestCase#testMethod" + }) + // @formatter:on + class Suite { + } + + LauncherDiscoveryRequest request = builder.applySelectorsAndFiltersFromSuite(Suite.class).build(); + List classSelectors = request.getSelectorsByType(ClassSelector.class); + assertEquals(NonLocalTestCase.class, exactlyOne(classSelectors).getJavaClass()); + List methodSelectors = request.getSelectorsByType(MethodSelector.class); + // @formatter:off + assertThat(exactlyOne(methodSelectors)) + .extracting(MethodSelector::getJavaClass, MethodSelector::getMethodName) + .containsExactly(NoParameterTestCase.class, "testMethod"); + // @formatter:on + } + private static T exactlyOne(List list) { return CollectionUtils.getOnlyElement(list); } diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java index 0c685ad809cd..24d683c3397d 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java @@ -52,6 +52,7 @@ import org.junit.platform.suite.engine.testsuites.MultiEngineSuite; import org.junit.platform.suite.engine.testsuites.MultipleSuite; import org.junit.platform.suite.engine.testsuites.NestedSuite; +import org.junit.platform.suite.engine.testsuites.SelectByIdentifierSuite; import org.junit.platform.suite.engine.testsuites.SelectClassesSuite; import org.junit.platform.suite.engine.testsuites.SelectMethodsSuite; import org.junit.platform.suite.engine.testsuites.SuiteDisplayNameSuite; @@ -423,6 +424,19 @@ void threePartCyclicSuite() { // @formatter:on } + @Test + void selectByIdentifier() { + // @formatter:off + EngineTestKit.engine(ENGINE_ID) + .selectors(selectClass(SelectByIdentifierSuite.class)) + .execute() + .testEvents() + .assertThatEvents() + .haveExactly(1, event(test(SelectByIdentifierSuite.class.getName()), finishedSuccessfully())) + .haveExactly(1, event(test(SingleTestTestCase.class.getName()), finishedSuccessfully())); + // @formatter:on + } + @Suite @SelectClasses(SingleTestTestCase.class) private static class PrivateSuite { diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectByIdentifierSuite.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectByIdentifierSuite.java new file mode 100644 index 000000000000..5aad2b329d60 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectByIdentifierSuite.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015-2024 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.platform.suite.engine.testsuites; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.Select; +import org.junit.platform.suite.api.Suite; + +/** + * @since 1.11 + */ +@Suite +@IncludeClassNamePatterns(".*") +@Select("class:org.junit.platform.suite.engine.testcases.SingleTestTestCase") +public class SelectByIdentifierSuite { +} diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt index 956d780949b2..04812758a63f 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-platform-engine.expected.txt @@ -12,3 +12,5 @@ requires java.base mandated requires org.apiguardian.api static transitive requires org.junit.platform.commons transitive requires org.opentest4j transitive +uses org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser +provides org.junit.platform.engine.discovery.DiscoverySelectorIdentifierParser with org.junit.platform.engine.discovery.ClassSelector$IdentifierParser org.junit.platform.engine.discovery.ClasspathResourceSelector$IdentifierParser org.junit.platform.engine.discovery.ClasspathRootSelector$IdentifierParser org.junit.platform.engine.discovery.DirectorySelector$IdentifierParser org.junit.platform.engine.discovery.FileSelector$IdentifierParser org.junit.platform.engine.discovery.IterationSelector$IdentifierParser org.junit.platform.engine.discovery.MethodSelector$IdentifierParser org.junit.platform.engine.discovery.ModuleSelector$IdentifierParser org.junit.platform.engine.discovery.NestedClassSelector$IdentifierParser org.junit.platform.engine.discovery.NestedMethodSelector$IdentifierParser org.junit.platform.engine.discovery.PackageSelector$IdentifierParser org.junit.platform.engine.discovery.UniqueIdSelector$IdentifierParser org.junit.platform.engine.discovery.UriSelector$IdentifierParser diff --git a/platform-tooling-support-tests/projects/reflection-tests/build.gradle.kts b/platform-tooling-support-tests/projects/reflection-tests/build.gradle.kts new file mode 100644 index 000000000000..653257ae1042 --- /dev/null +++ b/platform-tooling-support-tests/projects/reflection-tests/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + java +} + +// grab jupiter version from system environment +val jupiterVersion: String = System.getenv("JUNIT_JUPITER_VERSION") +val vintageVersion: String = System.getenv("JUNIT_VINTAGE_VERSION") +val platformVersion: String = System.getenv("JUNIT_PLATFORM_VERSION") + +repositories { + maven { url = uri(file(System.getProperty("maven.repo"))) } + mavenCentral() +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter:$jupiterVersion") + testImplementation("org.junit.jupiter:junit-jupiter-engine:$jupiterVersion") + testRuntimeOnly("org.junit.platform:junit-platform-reporting:$platformVersion") +} + +tasks.test { + useJUnitPlatform() + + testLogging { + events("failed") + } + + reports { + html.required = true + } + + val outputDir = reports.junitXml.outputLocation + jvmArgumentProviders += CommandLineArgumentProvider { + listOf( + "-Djunit.platform.reporting.open.xml.enabled=true", + "-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}" + ) + } + + doFirst { + println("Using Java version: ${JavaVersion.current()}") + } +} diff --git a/platform-tooling-support-tests/projects/reflection-tests/settings.gradle.kts b/platform-tooling-support-tests/projects/reflection-tests/settings.gradle.kts new file mode 100644 index 000000000000..af17e8f41649 --- /dev/null +++ b/platform-tooling-support-tests/projects/reflection-tests/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "reflection-tests" diff --git a/platform-tooling-support-tests/projects/reflection-tests/src/test/java/ReflectionTestCase.java b/platform-tooling-support-tests/projects/reflection-tests/src/test/java/ReflectionTestCase.java new file mode 100644 index 000000000000..0692db43426b --- /dev/null +++ b/platform-tooling-support-tests/projects/reflection-tests/src/test/java/ReflectionTestCase.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2024 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 standalone; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import java.util.Arrays; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; +import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.JupiterTestDescriptor; +import org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor; +import org.junit.jupiter.engine.descriptor.NestedClassTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestFactoryTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestTemplateInvocationTestDescriptor; +import org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor; + +class ReflectionTestCase { + + @TestFactory + Stream canReadParameters() { + return Stream.of(JupiterTestDescriptor.class, ClassBasedTestDescriptor.class, ClassTestDescriptor.class, + MethodBasedTestDescriptor.class, TestMethodTestDescriptor.class, TestTemplateTestDescriptor.class, + TestTemplateInvocationTestDescriptor.class, TestFactoryTestDescriptor.class, + NestedClassTestDescriptor.class) // + .map(descriptorClass -> dynamicContainer(descriptorClass.getSimpleName(), + Arrays.stream(descriptorClass.getDeclaredMethods()) // + .map(method -> dynamicTest(method.getName(), + () -> assertDoesNotThrow(method::getParameters))))); + } +} diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ReflectionCompatibilityTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ReflectionCompatibilityTests.java new file mode 100644 index 000000000000..2a0b0f4c2189 --- /dev/null +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ReflectionCompatibilityTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015-2024 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.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static platform.tooling.support.Helper.TOOL_TIMEOUT; + +import java.nio.file.Paths; + +import de.sormuras.bartholdy.tool.GradleWrapper; + +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; + +import platform.tooling.support.Helper; +import platform.tooling.support.MavenRepo; +import platform.tooling.support.Request; + +/** + * @since 1.11 + */ +class ReflectionCompatibilityTests { + + @Test + void gradle_wrapper() { + var request = Request.builder() // + .setTool(new GradleWrapper(Paths.get(".."))) // + .setProject("reflection-tests") // + .addArguments("-Dmaven.repo=" + MavenRepo.dir()) // + .addArguments("build", "--no-daemon", "--stacktrace") // + .setTimeout(TOOL_TIMEOUT) // + .setJavaHome(Helper.getJavaHome("8").orElseThrow(TestAbortedException::new)) // + .build(); + + var result = request.run(); + + assertFalse(result.isTimedOut(), () -> "tool timed out: " + result); + + assertEquals(0, result.getExitCode()); + assertTrue(result.getOutputLines("out").stream().anyMatch(line -> line.contains("BUILD SUCCESSFUL"))); + assertThat(result.getOutput("out")).contains("Using Java version: 1.8"); + } +}