From d5c98e4ffa7aa1bfd5e927608b736b85bc09b5e1 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Thu, 16 May 2024 11:10:52 +0200 Subject: [PATCH] Introduce `Select` annotation for test suites --- .../release-notes-5.11.0-M2.adoc | 5 ++ .../asciidoc/user-guide/running-tests.adoc | 10 ++-- .../DiscoverySelectorIdentifierParsers.java | 8 +++ .../engine/discovery/DiscoverySelectors.java | 14 ++++++ .../junit/platform/runner/JUnitPlatform.java | 2 + .../org/junit/platform/suite/api/Select.java | 49 +++++++++++++++++++ .../org/junit/platform/suite/api/Selects.java | 47 ++++++++++++++++++ .../org/junit/platform/suite/api/Suite.java | 1 + .../commons/AdditionalDiscoverySelectors.java | 6 +++ .../SuiteLauncherDiscoveryRequestBuilder.java | 4 ++ ...eLauncherDiscoveryRequestBuilderTests.java | 23 +++++++++ .../suite/engine/SuiteEngineTests.java | 14 ++++++ .../testsuites/SelectByIdentifierSuite.java | 24 +++++++++ 13 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Select.java create mode 100644 junit-platform-suite-api/src/main/java/org/junit/platform/suite/api/Selects.java create mode 100644 platform-tests/src/test/java/org/junit/platform/suite/engine/testsuites/SelectByIdentifierSuite.java 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 index 95d1392278e5..31776a3c2b9f 100644 --- 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 @@ -44,6 +44,11 @@ repository on GitHub. 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. diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index a6e61c8fad21..1393e4d26ba2 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -904,20 +904,20 @@ generically as strings via their identifiers. The following discovery selectors are provided out of the box: |=== -| Java Type | API | Annotation | Console Launcher | Identifier +| 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-iteration method=com.acme.Foo#m[1..2]` | `iteration:method:com.acme.Foo#m[1..2]` +| `{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 ` | `nested-class:com.acme.Foo/Bar` -| `{NestedMethodSelector}` | `{DiscoverySelectors_selectNestedMethod}` | -- | `--select ` | `nested-method:com.acme.Foo/Bar#m` +| `{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 ` | `uid:...` +| `{UniqueIdSelector}` | `{DiscoverySelectors_selectUniqueId}` | `@Select("")` | `--select ` | `uid:...` | `{UriSelector}` | `{DiscoverySelectors_selectUri}` | `@SelectUris` | `--select-uri \file:///foo.txt` | `uri:file:///foo.txt` |=== 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 index c25895bf810d..2f0d79b46735 100644 --- 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 @@ -33,6 +33,14 @@ */ 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() // 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 7aa4bdec8f6d..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 @@ -969,6 +969,20 @@ public static Optional parse(DiscoverySelectorIdent 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}. 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 26ba5ee8f3b5..62d4b346b434 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/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 { +}