getNullValues() {
127 | return nullValues;
128 | }
129 |
130 | public String getEmptyValue() {
131 | return emptyValue;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormattedSourceTest.java:
--------------------------------------------------------------------------------
1 | package com.mikemybytes.junit5.formatted;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.ArgumentsSource;
5 |
6 | import java.lang.annotation.*;
7 |
8 | /**
9 | * {@code @FormattedSourceTest} combines {@link ParameterizedTest} annotation with the behaviour of {@link FormattedSource}
10 | * in order to reduce code verbosity. Additionally, it automatically sets the test case name (controlled via
11 | * {@link ParameterizedTest#name}) to the formatted input string.
12 | *
13 | * The following {@code @FormattedSourceTest} annotation:
14 | *
15 | * {@literal @}FormattedSourceTest(format = "{0} + {1} = {2}", lines = {
16 | * "3 + 4 = 7",
17 | * "7 + 1 = 8"
18 | * })
19 | * void calculatesSum(int x, int y, int expectedSum) {
20 | * // ...
21 | * }
22 | *
23 | * is equivalent to the following combination of {@link ParameterizedTest} and {@link FormattedSource}:
24 | *
25 | * {@literal @}ParameterizedTest(name = "{0} + {1} = {2}")
26 | * {@literal @}FormattedSource(format = "{0} + {1} = {2}", lines = {
27 | * "3 + 4 = 7",
28 | * "7 + 1 = 8"
29 | * })
30 | * void calculatesSum(int x, int y, int expectedSum) {
31 | * // ...
32 | * }
33 | *
34 | * Please note, that in both cases test cases names will be (respectively): {@code 3 + 4 = 7} and {@code 7 + 1 = 8}.
35 | *
36 | * As JUnit 5 {@link ParameterizedTest} doesn't allow to wrap {@link org.junit.jupiter.params.provider.Arguments}
37 | * with {@link org.junit.jupiter.api.Named}, the value of the first argument (the only one that always has to be present)
38 | * is being wrapped with {@link org.junit.jupiter.api.Named} containing the whole formatted input string. Then, it is
39 | * being used as a test case name via {@code @ParameterizedTest(name = "{0}")}.
40 | */
41 | @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
42 | @Retention(RetentionPolicy.RUNTIME)
43 | @Documented
44 | @ParameterizedTest(name = "{0}")
45 | @ArgumentsSource(FormattedSourceTestArgumentsProvider.class)
46 | public @interface FormattedSourceTest {
47 |
48 | /**
49 | * The definition of the arguments format. By default, specific test method arguments must be referenced
50 | * by their position (starting from zero). E.g. {@code {2}} represents the 3rd argument of the test method.
51 | * Setting {@link #argumentPlaceholder()} disables the default behavior, allowing to use a fixed placeholder
52 | * string instead. As there's no braces (curly brackets) escaping, switching to the fixed argument placeholder
53 | * allows using them in the format string.
54 | *
55 | * @return The definition of the arguments format.
56 | */
57 | String format();
58 |
59 | /**
60 | * Test case input represented as lines in the defined {@link #format}. Each line represents a separate test case
61 | * of the {@link org.junit.jupiter.params.ParameterizedTest}. Lines must not contain newline characters like
62 | * {@code \n}.
63 | *
64 | * Defaults to an empty string. Note: the test case input must be supplied either via {@link #lines()} or
65 | * {@link #textBlock()}.
66 | *
67 | * @return Test case input represented as lines in the defined format.
68 | */
69 | String[] lines() default {};
70 |
71 | /**
72 | * Test case input represented as a single Java Text Block (available since Java 15). Each line represents a
73 | * separate test case of the {@link org.junit.jupiter.params.ParameterizedTest}. Lines must not contain newline
74 | * characters like {@code \n}.
75 | *
76 | * When running on Java version less than 15, using {@link #lines} is recommended instead.
77 | *
78 | * Defaults to an empty string. Note: the test case input must be supplied either via {@link #lines()} or
79 | * {@link #textBlock()}.
80 | *
81 | * @return Test case input represented as a single Java Text Block.
82 | */
83 | String textBlock() default "";
84 |
85 | /**
86 | * The quote character that could be used to separate argument's value from the rest of the input.
87 | * As there's no escaping support, a different quote character should be chosen in case of a conflict.
88 | *
89 | * Defaults to a single quote ({@code '}).
90 | *
91 | * @return Arguments quote character.
92 | */
93 | char quoteCharacter() default '\'';
94 |
95 | /**
96 | * Specifies fixed argument placeholder string that should be used instead of the default indexed syntax.
97 | * Each placeholder's occurrence corresponds to the next argument of the annotated test method
98 | * (positional arguments).
99 | *
100 | * @since 1.0.0
101 | * @return Custom argument placeholder string.
102 | */
103 | String argumentPlaceholder() default "";
104 |
105 | /**
106 | * Allows to ignore (or not) leading and trailing whitespace characters identified in the argument values.
107 | *
108 | * Defaults to {@code true}.
109 | * @return {@code true} if leading and trailing whitespaces should be ignored, {@code false} otherwise.
110 | */
111 | boolean ignoreLeadingAndTrailingWhitespace() default true;
112 |
113 | /**
114 | * A list of strings that should be interpreted as {@code null} references.
115 | *
116 | * Provided values (e.g. {@code "null"}, {@code "N/A"}, {@code "NONE"}) will be converted to {@code null}
117 | * references, no matter if quoted ({@link #quoteCharacter()}) or not.
118 | *
119 | * Regardless of the value of this attribute, unquoted empty values will always be interpreted as
120 | * {@code null}.
121 | *
122 | * Defaults to {@code {}}.
123 | * @return A list of strings that should be interpreted as {@code null} references.
124 | */
125 | String[] nullValues() default {};
126 |
127 | /**
128 | * A value used to substitute quoted empty strings read from the input.
129 | *
130 | * Defaults to empty string ({@code ""}).
131 | * @return A value used to substitute quoted empty strings read from the input.
132 | */
133 | String emptyValue() default "";
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormattedSourceTestArgumentsProvider.java:
--------------------------------------------------------------------------------
1 | package com.mikemybytes.junit5.formatted;
2 |
3 | import org.junit.jupiter.api.extension.ExtensionContext;
4 | import org.junit.jupiter.params.provider.Arguments;
5 | import org.junit.jupiter.params.provider.ArgumentsProvider;
6 | import org.junit.jupiter.params.support.AnnotationConsumer;
7 |
8 | import java.util.stream.Stream;
9 |
10 | import static com.mikemybytes.junit5.formatted.Preconditions.require;
11 |
12 | /**
13 | * {@code FormattedSourceArgumentsProvider} is an {@link ArgumentsProvider} implementation capable of extracting
14 | * argument values from {@link FormattedSourceTest} annotation.
15 | */
16 | class FormattedSourceTestArgumentsProvider implements ArgumentsProvider, AnnotationConsumer {
17 |
18 | private FormattedSourceData sourceData;
19 |
20 | @Override
21 | public void accept(FormattedSourceTest annotation) {
22 | sourceData = FormattedSourceData.from(annotation);
23 | }
24 |
25 | @Override
26 | public Stream extends Arguments> provideArguments(ExtensionContext context) {
27 | require(sourceData != null);
28 |
29 | int expectedParameterCount = context.getRequiredTestMethod().getParameterCount();
30 | FormatSpecification specification = FormatAnalyzers.from(sourceData)
31 | .analyze(sourceData.getFormatString(), expectedParameterCount);
32 | var processor = new ArgumentsExtractor(sourceData, specification, RawArgumentsProcessor.testCaseName());
33 | return sourceData.getLines()
34 | .stream()
35 | .map(processor::extract);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/IndexedArgumentPlaceholdersFormatAnalyzer.java:
--------------------------------------------------------------------------------
1 | package com.mikemybytes.junit5.formatted;
2 |
3 | import java.util.HashSet;
4 | import java.util.List;
5 | import java.util.regex.MatchResult;
6 | import java.util.regex.Pattern;
7 | import java.util.stream.Collectors;
8 | import java.util.stream.IntStream;
9 |
10 | import static com.mikemybytes.junit5.formatted.Preconditions.require;
11 |
12 | /**
13 | * Creates {@link FormatSpecification} for indexed argument placeholders. Every argument is represented as {@code {x}},
14 | * where {@code x} is its index (counting from zero). For example, {@code {0}} represents the first argument, while
15 | * {@code {3}} represents the fourth one.
16 | *
17 | * Inspired by the JUnit 5 convention used for customizing display names.
18 | *
19 | */
20 | class IndexedArgumentPlaceholdersFormatAnalyzer implements FormatAnalyzer {
21 |
22 | private static final Pattern formatArgumentPlaceholderPattern = Pattern.compile("\\{(\\d+)}");
23 |
24 | @Override
25 | public FormatSpecification analyze(String formatString, int methodParameterCount) {
26 | List matchResults = matchFormatArgumentPlaceholders(formatString);
27 |
28 | List formatArgumentsOrder = extractTemplateArguments(matchResults, methodParameterCount);
29 | Pattern linePattern = LinePatternFactory.create(formatString, matchResults, formatArgumentsOrder);
30 |
31 | return new FormatSpecification(linePattern, formatArgumentsOrder);
32 | }
33 |
34 | private List matchFormatArgumentPlaceholders(String formatString) {
35 | return formatArgumentPlaceholderPattern.matcher(formatString)
36 | .results()
37 | .collect(Collectors.toList());
38 | }
39 |
40 | private List extractTemplateArguments(
41 | List matchingFormatArgumentPlaceholders,
42 | int methodParameterCount) {
43 | List templateArguments = matchingFormatArgumentPlaceholders.stream()
44 | .map(r -> {
45 | require(r.groupCount() == 1);
46 | return Integer.valueOf(r.group(1));
47 | })
48 | .collect(Collectors.toList());
49 |
50 | int formatParameterCount = templateArguments.size();
51 |
52 | require(
53 | methodParameterCount >= formatParameterCount,
54 | () -> "Number of method arguments is less than the number of format arguments"
55 | );
56 |
57 | List expectedIndexes = IntStream.range(0, formatParameterCount)
58 | .boxed()
59 | .collect(Collectors.toList());
60 |
61 | boolean validArguments = new HashSet<>(templateArguments).containsAll(expectedIndexes)
62 | && templateArguments.size() == expectedIndexes.size();
63 | require(
64 | validArguments,
65 | () -> "Arguments provided in the format string are invalid: expected " + expectedIndexes
66 | + " but got " + templateArguments.stream().sorted().collect(Collectors.toList())
67 | );
68 |
69 | return templateArguments;
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/LinePatternFactory.java:
--------------------------------------------------------------------------------
1 | package com.mikemybytes.junit5.formatted;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.regex.MatchResult;
6 | import java.util.regex.Pattern;
7 |
8 | class LinePatternFactory {
9 |
10 | private LinePatternFactory() {
11 | // static only
12 | }
13 |
14 | /**
15 | * Creates {@link Pattern} that could be used to extract argument values out of the given input line.
16 | *
17 | * @param formatString Format string as defined in one of the annotations
18 | * @param matchingFormatArgumentPlaceholders {@link MatchResult} of the argument placeholders in the format string
19 | * @param formatArgumentsOrder the order of arguments represented as the order of their indexes
20 | */
21 | static Pattern create(
22 | String formatString,
23 | List matchingFormatArgumentPlaceholders,
24 | List formatArgumentsOrder) {
25 |
26 | List textParts = tokenize(formatString, matchingFormatArgumentPlaceholders);
27 |
28 | StringBuilder lineRegex = new StringBuilder();
29 | for (int i = 0; i < textParts.size(); i++) {
30 | if (!textParts.get(i).isEmpty()) {
31 | lineRegex.append(Pattern.quote(textParts.get(i)));
32 | }
33 | if (i < formatArgumentsOrder.size()) {
34 | var group = new FormatArgumentMatcherGroup(formatArgumentsOrder.get(i));
35 | lineRegex.append(group.getRegex());
36 | }
37 | }
38 |
39 | return Pattern.compile(lineRegex.toString());
40 | }
41 |
42 | private static List tokenize(String formatString, List matchingFormatArgumentPlaceholders) {
43 | List tokens = new ArrayList<>();
44 |
45 | int startIndex = 0;
46 | for (var placeholder : matchingFormatArgumentPlaceholders) {
47 | int endIndex = placeholder.start();
48 | if (startIndex == endIndex) {
49 | tokens.add("");
50 | } else if (startIndex < formatString.length()) {
51 | var part = formatString.substring(startIndex, endIndex);
52 | tokens.add(part);
53 | }
54 | startIndex = placeholder.end();
55 | }
56 | if (startIndex == formatString.length()) {
57 | tokens.add("");
58 | } else {
59 | tokens.add(formatString.substring(startIndex));
60 | }
61 |
62 | return tokens;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/PositionalArgumentPlaceholdersFormatAnalyzer.java:
--------------------------------------------------------------------------------
1 | package com.mikemybytes.junit5.formatted;
2 |
3 | import java.util.List;
4 | import java.util.regex.MatchResult;
5 | import java.util.regex.Pattern;
6 | import java.util.stream.Collectors;
7 | import java.util.stream.IntStream;
8 |
9 | import static com.mikemybytes.junit5.formatted.Preconditions.require;
10 |
11 | /**
12 | * Creates {@link FormatSpecification} for positional argument placeholders. The order of arguments is determined
13 | * based on their order of appearance. The same (provided) placeholder string is used to represent them.
14 | */
15 | class PositionalArgumentPlaceholdersFormatAnalyzer implements FormatAnalyzer {
16 |
17 | private final String argumentPlaceholder;
18 |
19 | PositionalArgumentPlaceholdersFormatAnalyzer(String argumentPlaceholder) {
20 | this.argumentPlaceholder = argumentPlaceholder;
21 | }
22 |
23 | @Override
24 | public FormatSpecification analyze(String formatString, int methodParameterCount) {
25 | List matchResults = matchFormatArgumentPlaceholders(formatString);
26 |
27 | int formatParameterCount = matchResults.size();
28 | require(
29 | methodParameterCount >= formatParameterCount,
30 | () -> "Number of method arguments is less than the number of format arguments"
31 | );
32 |
33 | List formatArgumentsOrder = IntStream.range(0, formatParameterCount)
34 | .boxed()
35 | .collect(Collectors.toList());
36 |
37 | Pattern linePattern = LinePatternFactory.create(formatString, matchResults, formatArgumentsOrder);
38 |
39 | return new FormatSpecification(linePattern, formatArgumentsOrder);
40 | }
41 |
42 | private List matchFormatArgumentPlaceholders(String formatString) {
43 | return Pattern.compile(Pattern.quote(argumentPlaceholder))
44 | .matcher(formatString)
45 | .results()
46 | .collect(Collectors.toList());
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/Preconditions.java:
--------------------------------------------------------------------------------
1 | package com.mikemybytes.junit5.formatted;
2 |
3 | import java.util.function.Supplier;
4 |
5 | /**
6 | * Utility class for expressing various preconditions in the code.
7 | */
8 | final class Preconditions {
9 |
10 | private Preconditions() {
11 | // static only
12 | }
13 |
14 | /**
15 | * Requires a given boolean condition to be true. Throws {@link IllegalArgumentException} otherwise.
16 | *
17 | * @param condition Evaluated boolean condition.
18 | */
19 | static void require(boolean condition) {
20 | if (!condition) {
21 | throw new IllegalStateException("Unexpected precondition check failure");
22 | }
23 | }
24 |
25 | /**
26 | * Requires a given boolean condition to be true. Throws {@link IllegalArgumentException} otherwise.
27 | *
28 | * @param condition Evaluated boolean condition.
29 | * @param message Error message to be presented when condition is not met.
30 | */
31 | static void require(boolean condition, String message) {
32 | if (!condition) {
33 | throw new IllegalArgumentException(message);
34 | }
35 | }
36 |
37 | /**
38 | * Requires a given boolean condition to be true. Throws {@link IllegalArgumentException} otherwise.
39 | *
40 | * @param condition Evaluated boolean condition.
41 | * @param message Lazy error message to be presented when condition is not met.
42 | */
43 | static void require(boolean condition, Supplier message) {
44 | if (!condition) {
45 | throw new IllegalArgumentException(message.get());
46 | }
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/RawArgumentsProcessor.java:
--------------------------------------------------------------------------------
1 | package com.mikemybytes.junit5.formatted;
2 |
3 | import org.junit.jupiter.api.Named;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.function.BiFunction;
8 |
9 | /**
10 | * Defines method of processing raw arguments coming from the provided test case input string.
11 | */
12 | interface RawArgumentsProcessor extends BiFunction, List