Skip to content

Commit

Permalink
Introduce parseable DiscoverySelector representations (#3737)
Browse files Browse the repository at this point in the history
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("<identifier>)` annotation.

Co-authored-by: Marc Philipp <mail@marcphilipp.de>
  • Loading branch information
leonard84 and marcphilipp committed May 17, 2024
1 parent bc55d0e commit 2a777a8
Show file tree
Hide file tree
Showing 47 changed files with 2,058 additions and 288 deletions.
37 changes: 36 additions & 1 deletion documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ repository on GitHub.
[[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("<identifier>)`
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.
Expand Down
35 changes: 35 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/running-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

----
Expand Down Expand Up @@ -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
<<junit-platform-suite-engine>>, via options of the <<running-tests-console-launcher>>, 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}("<identifier>")` | `--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}("<identifier>")` | `--select <identifier>` | `nested-class:com.acme.Foo/Bar`
| `{NestedMethodSelector}` | `{DiscoverySelectors_selectNestedMethod}` | `{Select}("<identifier>")` | `--select <identifier>` | `nested-method:com.acme.Foo/Bar#m`
| `{PackageSelector}` | `{DiscoverySelectors_selectPackage}` | `{SelectPackages}` | `--select-package com.acme.foo` | `package:com.acme.foo`
| `{UniqueIdSelector}` | `{DiscoverySelectors_selectUniqueId}` | `{Select}("<identifier>")` | `--select <identifier>` | `uid:...`
| `{UriSelector}` | `{DiscoverySelectors_selectUri}` | `{SelectUris}` | `--select-uri \file:///foo.txt` | `uri:file:///foo.txt`
|===


[[running-tests-config-params]]
=== Configuration Parameters

Expand Down
4 changes: 2 additions & 2 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2429,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 --
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -66,7 +67,29 @@ public static <T> T getOnlyElement(Collection<T> 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 <T> Optional<T> getFirstElement(Collection<T> collection) {
Preconditions.notNull(collection, "collection must not be null");
return collection.isEmpty() //
? Optional.empty() //
: Optional.ofNullable(firstElement(collection));
}

private static <T> T firstElement(Collection<T> collection) {
return collection instanceof List //
? ((List<T>) collection).get(0) //
: collection.iterator().next();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <em>fully qualified method name</em> for the method described by the
* supplied class, method name, and parameter types.
*
* <p>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);
}

/**
Expand Down

0 comments on commit 2a777a8

Please sign in to comment.