Skip to content

Commit 430f2f6

Browse files
committed
Introduce and enforce syntax for tags
Prior to this commit, tags were trimmed (#942) but were otherwise allowed to contain any type of character, including whitespace or ISO control characters. However, this degree of flexibility can lead to subtle configuration errors and also makes it challenging to reason about a tag expression language as proposed in #927. This commit addresses these issues by introducing and enforcing a syntax for tags. Specifically, all tags must now conform to the following syntax rules. * A tag must not be null or blank. * A trimmed tag must not contain whitespace. * A trimmed tag must not contain ISO control characters. Consequently, the following changes make it impossible to create a TestTag or TagFilter based on a tag that is not syntactically valid. * The TagFilter.includeTags(), TagFilter.excludeTags(), and TestTag.create() factory methods now throw a PreconditionViolationException if a supplied tag is not syntactically valid. * @tag declarations containing invalid tag syntax will now be logged as a warning but effectively ignored. Furthermore, a new TestTag.isValid(String) method has been introduced for checking if a tag is syntactically valid. This allows test engines to preemptively validate the syntax of a tag provided by a user before attempting to register such an invalid tag. Issue: #956
1 parent c450484 commit 430f2f6

File tree

14 files changed

+316
-78
lines changed

14 files changed

+316
-78
lines changed

documentation/src/docs/asciidoc/release-notes-5.0.0-M6.adoc

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,21 @@ on GitHub.
2020

2121
===== Bug Fixes
2222

23-
* All tags are now trimmed in order to remove leading and trailing whitespace. This
23+
* All tags are now _trimmed_ in order to remove leading and trailing whitespace. This
2424
applies to any tag supplied _directly_ to the `Launcher` via `TagFilter.includeTags()`
2525
and `TagFilter.excludeTags()` or _indirectly_ via `@IncludeTags`, `@ExcludeTags`, the
2626
JUnit Platform Console Launcher, the JUnit Platform Gradle plugin, and the JUnit
27-
Platform Maven Surefire provider. Furthermore, blank tags are now ignored.
27+
Platform Maven Surefire provider.
2828

2929
===== Deprecations and Breaking Changes
3030

31+
* All tags must now conform to the following syntax rules:
32+
** A tag must not be `null` or _blank_.
33+
** A _trimmed_ tag must not contain whitespace.
34+
** A _trimmed_ tag must not contain ISO control characters.
35+
* The `TagFilter.includeTags()`, `TagFilter.excludeTags()`, and `TestTag.create()`
36+
factory methods now throw a `PreconditionViolationException` if a supplied tag is not
37+
syntactically valid.
3138
* The method `getDiscoveryFiltersByType` of `EngineDiscoveryRequest` has been renamed to
3239
`getFiltersByType`.
3340
* The method `getSegments()` of `UniqueId` now returns an immutable list.
@@ -36,9 +43,9 @@ on GitHub.
3643

3744
===== New Features and Improvements
3845

46+
* New `TestTag.isValid(String)` method for checking if a tag is syntactically valid.
3947
* The `findAnnotation()` method in `AnnotationSupport` now searches on interfaces
4048
implemented by a class if the supplied element is a class.
41-
4249
* The following methods of `org.junit.platform.commons.util.ReflectionUtils` are now
4350
exposed via `org.junit.platform.commons.support.ReflectionSupport`:
4451
** `public static Optional<Class<?>> loadClass(String name)`
@@ -48,13 +55,14 @@ on GitHub.
4855
** `public static Object invokeMethod(Method method, Object target, Object... args)`
4956
** `public static List<Class<?>> findNestedClasses(Class<?> clazz, Predicate<Class<?>> predicate)`
5057

58+
5159
[[release-notes-5.0.0-m6-junit-jupiter]]
5260
==== JUnit Jupiter
5361

5462
===== Bug Fixes
5563

5664
* All tags declared via `@Tag` are now trimmed in order to remove leading and trailing
57-
whitespace. Furthermore, blank tags are now ignored.
65+
whitespace.
5866
* All primitive array types (from `boolean[]` to `short[]`) are now supported as a
5967
return type of static argument providing methods for parameterized tests.
6068

@@ -79,7 +87,8 @@ on GitHub.
7987
method. This helps to debug errors resulting from a method being simultaneously
8088
annotated with multiple competing annotations such as `@Test`, `@RepeatedTest`,
8189
`@ParameterizedTest`, `@TestFactory`, etc.
82-
90+
* `@Tag` declarations containing invalid tag syntax will now be logged as a warning but
91+
effectively ignored.
8392

8493

8594
[[release-notes-5.0.0-m6-junit-vintage]]

documentation/src/docs/asciidoc/writing-tests.adoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,15 @@ include::{testDir}/example/DisabledTestsDemo.java[tags=user_guide]
154154
Test classes and methods can be tagged. Those tags can later be used to filter
155155
<<running-tests,test discovery and execution>>.
156156

157+
==== Syntax Rules for Tags
158+
159+
* A tag must not be `null` or _blank_.
160+
* A _trimmed_ tag must not contain whitespace.
161+
* A _trimmed_ tag must not contain ISO control characters.
162+
163+
NOTE: In the above context, "trimmed" means that leading and trailing whitespace
164+
characters have been removed.
165+
157166
[source,java,indent=0]
158167
----
159168
include::{testDir}/example/TaggingDemo.java[tags=user_guide]

documentation/src/test/java/example/TestInfoDemo.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ void init(TestInfo testInfo) {
3535

3636
@Test
3737
@DisplayName("TEST 1")
38-
@Tag("my tag")
38+
@Tag("my-tag")
3939
void test1(TestInfo testInfo) {
4040
assertEquals("TEST 1", testInfo.getDisplayName());
41-
assertTrue(testInfo.getTags().contains("my tag"));
41+
assertTrue(testInfo.getTags().contains("my-tag"));
4242
}
4343

4444
@Test

junit-jupiter-api/src/main/java/org/junit/jupiter/api/Tag.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
* a list of tags to be used for the current test plan, potentially
3232
* dependent on the current environment.
3333
*
34+
* <h3>Syntax Rules for Tags</h3>
35+
* <ul>
36+
* <li>A tag must not be blank.</li>
37+
* <li>A trimmed tag must not contain whitespace.</li>
38+
* <li>A trimmed tag must not contain ISO control characters.</li>
39+
* </ul>
40+
*
3441
* @since 5.0
3542
* @see Tags
3643
* @see Test
@@ -45,8 +52,10 @@
4552
/**
4653
* The <em>tag</em>.
4754
*
48-
* <p>Note: the tag will be {@linkplain String#trim() trimmed},
49-
* and a blank value will be ignored.
55+
* <p>Note: the tag will first be {@linkplain String#trim() trimmed}. If the
56+
* supplied tag is syntactically invalid after trimming, the error will be
57+
* logged as a warning, and the invalid tag will be effectively ignored. See
58+
* {@linkplain #Tag Syntax Rules for Tags}.
5059
*/
5160
String value();
5261

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptor.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,20 @@ protected static Set<TestTag> getTags(AnnotatedElement element) {
6464
// @formatter:off
6565
return findRepeatableAnnotations(element, Tag.class).stream()
6666
.map(Tag::value)
67-
.filter(StringUtils::isNotBlank)
67+
.filter(tag -> {
68+
boolean isValid = TestTag.isValid(tag);
69+
if (!isValid) {
70+
// TODO [#242] Replace logging with precondition check once we have a proper mechanism for
71+
// handling validation exceptions during the TestEngine discovery phase.
72+
//
73+
// As an alternative to a precondition check here, we could catch any
74+
// PreconditionViolationException thrown by TestTag::create.
75+
logger.warning(String.format(
76+
"Configuration error: invalid tag syntax in @Tag(\"%s\") declaration on [%s]. Tag will be ignored.",
77+
tag, element));
78+
}
79+
return isValid;
80+
})
6881
.map(TestTag::create)
6982
.collect(toCollection(LinkedHashSet::new));
7083
// @formatter:on

junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/descriptor/JupiterTestDescriptorTests.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ void constructFromMethodWithCustomTestAnnotation() throws Exception {
8888

8989
assertEquals(testMethod, descriptor.getTestMethod());
9090
assertEquals("custom name", descriptor.getDisplayName(), "display name:");
91-
assertThat(descriptor.getTags()).containsExactly(TestTag.create("custom tag"));
91+
assertThat(descriptor.getTags()).containsExactly(TestTag.create("custom-tag"));
9292
}
9393

9494
@Test
@@ -181,6 +181,7 @@ void test(String[][][][][] info) {
181181
@DisplayName("custom test name")
182182
@Tag("methodTag1")
183183
@Tag("methodTag2")
184+
@Tag("tag containing whitespace")
184185
void foo() {
185186
}
186187

@@ -192,7 +193,7 @@ void customTestAnnotation() {
192193

193194
@Test
194195
@DisplayName("custom name")
195-
@Tag("custom tag")
196+
@Tag(" custom-tag ")
196197
@Target(ElementType.METHOD)
197198
@Retention(RetentionPolicy.RUNTIME)
198199
@interface CustomTestAnnotation {

junit-platform-engine/src/main/java/org/junit/platform/engine/TestTag.java

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
import java.util.Objects;
1717

1818
import org.junit.platform.commons.meta.API;
19+
import org.junit.platform.commons.util.PreconditionViolationException;
1920
import org.junit.platform.commons.util.Preconditions;
21+
import org.junit.platform.commons.util.StringUtils;
2022

2123
/**
2224
* Immutable value object for a <em>tag</em> that is assigned to a test or
2325
* container.
2426
*
2527
* @since 1.0
28+
* @see #isValid(String)
29+
* @see #create(String)
2630
*/
2731
@API(Experimental)
2832
public final class TestTag implements Serializable {
@@ -31,24 +35,70 @@ public final class TestTag implements Serializable {
3135

3236
private final String name;
3337

38+
/**
39+
* Determine if the supplied tag name is valid with regard to the supported
40+
* syntax for tags.
41+
*
42+
* <h3>Syntax Rules for Tags</h3>
43+
* <ul>
44+
* <li>A tag must not be {@code null}.</li>
45+
* <li>A tag must not be blank.</li>
46+
* <li>A trimmed tag must not contain whitespace.</li>
47+
* <li>A trimmed tag must not contain ISO control characters.</li>
48+
* </ul>
49+
*
50+
* <p>If this method returns {@code true} for a given name, it is then a
51+
* valid candidate for the {@link TestTag#create(String) create()} factory
52+
* method.
53+
*
54+
* @param name the name of the tag to validate; may be {@code null} or blank
55+
* @return {@code true} if the supplied tag name conforms to the supported
56+
* syntax for tags
57+
* @see StringUtils#isNotBlank(String)
58+
* @see String#trim()
59+
* @see StringUtils#doesNotContainWhitespace(String)
60+
* @see StringUtils#doesNotContainIsoControlCharacter(String)
61+
* @see TestTag#create(String)
62+
*/
63+
public static boolean isValid(String name) {
64+
if (name == null) {
65+
return false;
66+
}
67+
name = name.trim();
68+
69+
return !name.isEmpty() && //
70+
StringUtils.doesNotContainWhitespace(name) && //
71+
StringUtils.doesNotContainIsoControlCharacter(name);
72+
}
73+
3474
/**
3575
* Create a {@code TestTag} from the supplied {@code name}.
3676
*
77+
* <p>Consider checking whether the syntax of the supplied {@code name}
78+
* is {@linkplain #isValid(String) valid} before attempting to create a
79+
* {@code TestTag} using this factory method.
80+
*
3781
* <p>Note: the supplied {@code name} will be {@linkplain String#trim() trimmed}.
3882
*
39-
* @param name the name of the tag; must not be {@code null} or blank
83+
* @param name the name of the tag; must be syntactically <em>valid</em>
84+
* @throws PreconditionViolationException if the supplied tag name is not
85+
* syntactically <em>valid</em>
86+
* @see TestTag#isValid(String)
4087
*/
41-
public static TestTag create(String name) {
88+
public static TestTag create(String name) throws PreconditionViolationException {
4289
return new TestTag(name);
4390
}
4491

4592
private TestTag(String name) {
46-
Preconditions.notBlank(name, "name must not be null or blank");
93+
Preconditions.condition(TestTag.isValid(name),
94+
() -> String.format("Tag name [%s] must be syntactically valid", name));
4795
this.name = name.trim();
4896
}
4997

5098
/**
5199
* Get the name of this tag.
100+
*
101+
* @return the name of this tag; never {@code null} or blank
52102
*/
53103
public String getName() {
54104
return this.name;

junit-platform-launcher/src/main/java/org/junit/platform/launcher/TagFilter.java

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@
1515
import static org.junit.platform.commons.meta.API.Usage.Experimental;
1616

1717
import java.util.List;
18-
import java.util.stream.Stream;
1918

2019
import org.junit.platform.commons.meta.API;
20+
import org.junit.platform.commons.util.PreconditionViolationException;
2121
import org.junit.platform.commons.util.Preconditions;
22-
import org.junit.platform.commons.util.StringUtils;
2322
import org.junit.platform.engine.FilterResult;
24-
import org.junit.platform.engine.TestDescriptor;
2523
import org.junit.platform.engine.TestTag;
2624

2725
/**
@@ -44,91 +42,87 @@ private TagFilter() {
4442
/**
4543
* Create an <em>include</em> filter based on the supplied {@code tags}.
4644
*
47-
* <p>Note: each tag will be {@linkplain String#trim() trimmed}, and any
48-
* blank tag will be ignored.
45+
* <p>Note: each tag will be {@linkplain String#trim() trimmed}.
4946
*
5047
* <p>Containers and tests will only be executed if they are tagged with
5148
* at least one of the supplied <em>included</em> tags.
5249
*
5350
* @param tags the included tags; never {@code null} or empty
51+
* @throws PreconditionViolationException if the supplied tags array is
52+
* {@code null} or empty, or if any individual tag is not syntactically
53+
* valid
5454
* @see #includeTags(List)
55+
* @see TestTag#isValid(String)
5556
*/
56-
public static PostDiscoveryFilter includeTags(String... tags) {
57+
public static PostDiscoveryFilter includeTags(String... tags) throws PreconditionViolationException {
5758
Preconditions.notNull(tags, "tags array must not be null");
5859
return includeTags(asList(tags));
5960
}
6061

6162
/**
6263
* Create an <em>include</em> filter based on the supplied {@code tags}.
6364
*
64-
* <p>Note: each tag will be {@linkplain String#trim() trimmed}, and any
65-
* blank tag will be ignored.
65+
* <p>Note: each tag will be {@linkplain String#trim() trimmed}.
6666
*
6767
* <p>Containers and tests will only be executed if they are tagged with
6868
* at least one of the supplied <em>included</em> tags.
6969
*
7070
* @param tags the included tags; never {@code null} or empty
71+
* @throws PreconditionViolationException if the supplied tags list is
72+
* {@code null} or empty, or if any individual tag is not syntactically
73+
* valid
7174
* @see #includeTags(String...)
75+
* @see TestTag#isValid(String)
7276
*/
73-
public static PostDiscoveryFilter includeTags(List<String> tags) {
77+
public static PostDiscoveryFilter includeTags(List<String> tags) throws PreconditionViolationException {
7478
Preconditions.notEmpty(tags, "tags list must not be null or empty");
75-
Preconditions.containsNoNullElements(tags, "individual tags must not be null");
76-
List<String> trimmedTags = trimmedCopyOf(tags);
77-
return descriptor -> FilterResult.includedIf(trimmedTagsOf(descriptor).anyMatch(trimmedTags::contains));
79+
List<TestTag> activeTags = toTestTags(tags);
80+
return descriptor -> FilterResult.includedIf(descriptor.getTags().stream().anyMatch(activeTags::contains));
7881
}
7982

8083
/**
8184
* Create an <em>exclude</em> filter based on the supplied {@code tags}.
8285
*
83-
* <p>Note: each tag will be {@linkplain String#trim() trimmed}, and any
84-
* blank tag will be ignored.
86+
* <p>Note: each tag will be {@linkplain String#trim() trimmed}.
8587
*
8688
* <p>Containers and tests will only be executed if they are <em>not</em>
8789
* tagged with any of the supplied <em>excluded</em> tags.
8890
*
8991
* @param tags the excluded tags; never {@code null} or empty
92+
* @throws PreconditionViolationException if the supplied tags array is
93+
* {@code null} or empty, or if any individual tag is not syntactically
94+
* valid
9095
* @see #excludeTags(List)
96+
* @see TestTag#isValid(String)
9197
*/
92-
public static PostDiscoveryFilter excludeTags(String... tags) {
98+
public static PostDiscoveryFilter excludeTags(String... tags) throws PreconditionViolationException {
9399
Preconditions.notNull(tags, "tags array must not be null");
94100
return excludeTags(asList(tags));
95101
}
96102

97103
/**
98104
* Create an <em>exclude</em> filter based on the supplied {@code tags}.
99105
*
100-
* <p>Note: each tag will be {@linkplain String#trim() trimmed}, and any
101-
* blank tag will be ignored.
106+
* <p>Note: each tag will be {@linkplain String#trim() trimmed}.
102107
*
103108
* <p>Containers and tests will only be executed if they are <em>not</em>
104109
* tagged with any of the supplied <em>excluded</em> tags.
105110
*
106111
* @param tags the excluded tags; never {@code null} or empty
112+
* @throws PreconditionViolationException if the supplied tags list is
113+
* {@code null} or empty, or if any individual tag is not syntactically
114+
* valid
107115
* @see #excludeTags(String...)
116+
* @see TestTag#isValid(String)
108117
*/
109-
public static PostDiscoveryFilter excludeTags(List<String> tags) {
118+
public static PostDiscoveryFilter excludeTags(List<String> tags) throws PreconditionViolationException {
110119
Preconditions.notEmpty(tags, "tags list must not be null or empty");
111-
Preconditions.containsNoNullElements(tags, "individual tags must not be null");
112-
List<String> trimmedTags = trimmedCopyOf(tags);
113-
return descriptor -> FilterResult.includedIf(trimmedTagsOf(descriptor).noneMatch(trimmedTags::contains));
120+
List<TestTag> activeTags = toTestTags(tags);
121+
return descriptor -> FilterResult.includedIf(descriptor.getTags().stream().noneMatch(activeTags::contains));
114122
}
115123

116-
private static List<String> trimmedCopyOf(List<String> tags) {
117-
// @formatter:off
118-
return tags.stream()
119-
.filter(StringUtils::isNotBlank)
120-
.map(String::trim)
121-
.collect(toList());
122-
// @formatter:on
123-
}
124-
125-
private static Stream<String> trimmedTagsOf(TestDescriptor descriptor) {
126-
// @formatter:off
127-
return descriptor.getTags().stream()
128-
.map(TestTag::getName)
129-
.filter(StringUtils::isNotBlank)
130-
.map(String::trim);
131-
// @formatter:on
124+
private static List<TestTag> toTestTags(List<String> tags) {
125+
return tags.stream().map(TestTag::create).collect(toList());
132126
}
133127

134128
}

0 commit comments

Comments
 (0)