Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce parseable DiscoverySelector representations #3737

Merged
merged 43 commits into from May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
64be199
Initial PoC of DiscoverySelectors.parse
leonard84 Dec 15, 2022
21e4886
Consistently use DiscoverySelectors methods to reuse validation logic
leonard84 Dec 16, 2022
44bca4c
Add a DiscoverySelector.toSelectorString() ..
leonard84 Dec 16, 2022
9eebabd
Use URLEncode for all characters in UniqueId segment values
leonard84 Dec 16, 2022
d824ba7
Add SelectorParserContext
leonard84 Sep 13, 2023
3c00a82
Fix deprecations and code style issues
leonard84 Mar 15, 2024
5b6d01b
spotlessApply
leonard84 Mar 15, 2024
a51c09b
Introduce TBD as replacement of URI
leonard84 Mar 15, 2024
0bd2e21
Revert "Use URLEncode for all characters in UniqueId segment values"
leonard84 Mar 15, 2024
360bacc
Remove UriEncoding again
leonard84 Mar 15, 2024
d64e235
Rename TBD to DiscoverySelectorIdentifier
marcphilipp Mar 21, 2024
9312afe
Allow covariant return types to avoid casting
marcphilipp Mar 21, 2024
a5545e4
Merge branch 'main'
marcphilipp Mar 21, 2024
9d91596
Make selectors return identifiers
marcphilipp Mar 21, 2024
a114a29
Add more tests and fix implementations
marcphilipp Mar 21, 2024
e0a3f45
Introduce --select and revamp --select-iteration to use identifiers
marcphilipp Mar 21, 2024
0ffc4c9
Reuse fully-qualified method name utilities for method selectors
marcphilipp Mar 22, 2024
9e8ffd8
Introduce StringUtils.splitIntoTwo to reduce duplication
marcphilipp Mar 22, 2024
ee4707f
Change parser to return Optional rather than Stream
marcphilipp Mar 22, 2024
6f542f0
Reuse selectMethod(String)
marcphilipp Mar 22, 2024
f391f83
Add Javadoc
marcphilipp Mar 22, 2024
a80e505
Fix module descriptor
marcphilipp Mar 22, 2024
4fdca80
Revert accidental side effect of rename
marcphilipp Mar 22, 2024
fe9cd57
Add TODO
marcphilipp Mar 22, 2024
720583b
Polish formatting
marcphilipp Mar 22, 2024
252f02f
Mark parsers internal
marcphilipp Mar 22, 2024
b0343b7
Mark toIdentifier() experimental
marcphilipp Mar 22, 2024
8d4644a
Polishing
marcphilipp Mar 22, 2024
bc80531
Polishing
marcphilipp May 1, 2024
9aeec78
Add tests for CollectionUtils#getFirstElement
marcphilipp May 1, 2024
5b3f23a
Merge remote-tracking branch 'origin/main' into leo/discovery-selecto…
marcphilipp May 1, 2024
9ce4e20
Add to release notes
marcphilipp May 1, 2024
9967df6
Load parsers at most once per class loader
marcphilipp May 15, 2024
0c14cc6
Add explicit anchors
marcphilipp May 15, 2024
6143133
Document discovery selectors in User Guide
marcphilipp May 15, 2024
4dc3a7c
Add todo
marcphilipp May 15, 2024
27d1542
Reduce table width
marcphilipp May 15, 2024
964c27b
Add links to Javadoc
marcphilipp May 15, 2024
a31381c
Delete empty paragraph
marcphilipp May 15, 2024
81b6694
Add type-level Javadoc
marcphilipp May 15, 2024
0688942
Introduce `Select` annotation for test suites
marcphilipp May 16, 2024
2bd52e1
Collapse index ranges when formatting iteration selector identifiers
marcphilipp May 16, 2024
a7f4868
Link to suite annotation Javadoc
marcphilipp May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.of(firstElement(collection));
}

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

/**
Expand Down
Expand Up @@ -887,13 +887,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
Expand Up @@ -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;
Expand Down Expand Up @@ -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> T mapTwo(Supplier<? extends RuntimeException> onePartExceptionCreator,
BiFunction<String, String, ? extends T> twoPartsMapper) {
Function<String, ? extends T> 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> T map(Function<String, ? extends T> onePartMapper, BiFunction<String, String, ? extends T> twoPartsMapper);

}

private static final class OnePart implements TwoPartSplitResult {

private final String value;

OnePart(String value) {
this.value = value;
}

@Override
public <T> T map(Function<String, ? extends T> onePartMapper,
BiFunction<String, String, ? extends T> 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> T map(Function<String, ? extends T> onePartMapper,
BiFunction<String, String, ? extends T> twoPartsMapper) {
return twoPartsMapper.apply(first, second);
}
}

}
Expand Up @@ -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;
Expand Down Expand Up @@ -99,51 +94,21 @@ public ClasspathResourceSelector convert(String value) {

static class Iteration implements ITypeConverter<IterationSelector> {

public static final Pattern PATTERN = Pattern.compile(
"(?<type>[a-z]+):(?<value>.*)\\[(?<indices>(\\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<DiscoverySelectorIdentifier> {

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);
}
}

Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -56,6 +58,7 @@ public class TestDiscoveryOptions {
private List<MethodSelector> selectedMethods = emptyList();
private List<ClasspathResourceSelector> selectedClasspathResources = emptyList();
private List<IterationSelector> selectedIterations = emptyList();
private List<DiscoverySelectorIdentifier> selectorIdentifiers = emptyList();

private List<String> includedClassNamePatterns = singletonList(STANDARD_INCLUDE_PATTERN);
private List<String> excludedClassNamePatterns = emptyList();
Expand Down Expand Up @@ -176,6 +179,14 @@ public void setSelectedIterations(List<IterationSelector> selectedIterations) {
this.selectedIterations = selectedIterations;
}

public List<DiscoverySelectorIdentifier> getSelectorIdentifiers() {
return selectorIdentifiers;
}

public void setSelectorIdentifiers(List<DiscoverySelectorIdentifier> selectorIdentifiers) {
this.selectorIdentifiers = selectorIdentifiers;
}

public List<DiscoverySelector> getExplicitSelectors() {
List<DiscoverySelector> selectors = new ArrayList<>();
selectors.addAll(getSelectedUris());
Expand All @@ -187,6 +198,7 @@ public List<DiscoverySelector> getExplicitSelectors() {
selectors.addAll(getSelectedMethods());
selectors.addAll(getSelectedClasspathResources());
selectors.addAll(getSelectedIterations());
DiscoverySelectors.parseAll(getSelectorIdentifiers()).forEach(selectors::add);
return selectors;
}

Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -135,6 +136,10 @@ static class SelectorOptions {
"-select-iteration" }, arity = "1", hidden = true, converter = SelectorConverter.Iteration.class)
private final List<IterationSelector> selectedIterations2 = new ArrayList<>();

@Option(names = {
"--select" }, arity = "1", converter = SelectorConverter.Identifier.class, description = "Select via a prefixed identifier. This option can be repeated.")
private final List<DiscoverySelectorIdentifier> selectorIdentifiers = new ArrayList<>();

SelectorOptions() {
}

Expand All @@ -152,6 +157,7 @@ private void applyTo(TestDiscoveryOptions result) {
result.setSelectedClasspathResources(
merge(this.selectedClasspathResources, this.selectedClasspathResources2));
result.setSelectedIterations(merge(this.selectedIterations, this.selectedIterations2));
result.setSelectorIdentifiers(this.selectorIdentifiers);
}
}

Expand Down
Expand Up @@ -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
Expand All @@ -25,4 +29,21 @@
*/
@API(status = STABLE, since = "1.0")
public interface DiscoverySelector {

/**
* Return the {@linkplain DiscoverySelectorIdentifier identifier} of this
* selector.
* <p>
* The returned identifier has to be parsable by a corresponding
* {@link DiscoverySelectorIdentifierParser}.
* <p>
* @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<DiscoverySelectorIdentifier> toIdentifier() {
return Optional.empty();
}

}