diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java index 28a1bf469c77..132055bed2e4 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java @@ -10,6 +10,7 @@ package org.junit.jupiter.params.provider; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.lang.annotation.Documented; @@ -35,8 +36,8 @@ * that the first record may optionally be used to supply CSV headers (see * {@link #useHeadersInDisplayName}). * - *
Any line beginning with a {@code #} symbol will be interpreted as a comment - * and will be ignored. + *
Any line beginning with a {@link #commentCharacter()} + * will be interpreted as a comment and will be ignored. * *
The column delimiter (which defaults to a comma ({@code ,})) can be customized * via either {@link #delimiter} or {@link #delimiterString}. @@ -63,6 +64,10 @@ * column is trimmed by default. This behavior can be changed by setting the * {@link #ignoreLeadingAndTrailingWhitespace} attribute to {@code true}. * + *
Note that {@link #delimiter} (or {@link #delimiterString}), {@link #quoteCharacter}, + * and {@link #commentCharacter} are treated as control characters + * and must all be distinct. + * *
This annotation is inherited to subclasses. @@ -235,4 +240,20 @@ @API(status = STABLE, since = "5.10") boolean ignoreLeadingAndTrailingWhitespace() default true; + /** + * The character used to denote comments when reading the CSV files. + * + *
Any line that begins with this character will be treated as a comment and ignored + * during parsing. + * + *
Note that the comment character must be the first character on the line + * without any leading whitespace. + * + *
Defaults to a hashtag {@code #}.
+ *
+ * @since 6.0.1
+ */
+ @API(status = EXPERIMENTAL, since = "6.0.1")
+ char commentCharacter() default '#';
+
}
diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvReaderFactory.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvReaderFactory.java
index 3a1bc58ddf04..5fd9c5ac827f 100644
--- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvReaderFactory.java
+++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvReaderFactory.java
@@ -18,7 +18,9 @@
import java.nio.charset.Charset;
import java.util.Set;
import java.util.UUID;
+import java.util.stream.Stream;
+import de.siegmar.fastcsv.reader.CommentStrategy;
import de.siegmar.fastcsv.reader.CsvCallbackHandler;
import de.siegmar.fastcsv.reader.CsvReader;
import de.siegmar.fastcsv.reader.CsvRecord;
@@ -65,7 +67,12 @@ private static void validateDelimiter(char delimiter, String delimiterString, An
static CsvReader extends CsvRecord> createReaderFor(CsvSource csvSource, String data) {
String delimiter = selectDelimiter(csvSource.delimiter(), csvSource.delimiterString());
+ var commentStrategy = csvSource.textBlock().isEmpty() ? NONE : SKIP;
// @formatter:off
+ validateControlCharactersDiffer(
+ delimiter, csvSource.quoteCharacter(), csvSource.commentCharacter(), commentStrategy
+ );
+
var builder = CsvReader.builder()
.skipEmptyLines(SKIP_EMPTY_LINES)
.trimWhitespacesAroundQuotes(TRIM_WHITESPACES_AROUND_QUOTES)
@@ -73,7 +80,8 @@ static CsvReader extends CsvRecord> createReaderFor(CsvSource csvSource, Strin
.allowMissingFields(ALLOW_MISSING_FIELDS)
.fieldSeparator(delimiter)
.quoteCharacter(csvSource.quoteCharacter())
- .commentStrategy(csvSource.textBlock().isEmpty() ? NONE : SKIP);
+ .commentStrategy(commentStrategy)
+ .commentCharacter(csvSource.commentCharacter());
var callbackHandler = createCallbackHandler(
csvSource.emptyValue(),
@@ -90,7 +98,12 @@ static CsvReader extends CsvRecord> createReaderFor(CsvFileSource csvFileSourc
Charset charset) {
String delimiter = selectDelimiter(csvFileSource.delimiter(), csvFileSource.delimiterString());
+ var commentStrategy = SKIP;
// @formatter:off
+ validateControlCharactersDiffer(
+ delimiter, csvFileSource.quoteCharacter(), csvFileSource.commentCharacter(), commentStrategy
+ );
+
var builder = CsvReader.builder()
.skipEmptyLines(SKIP_EMPTY_LINES)
.trimWhitespacesAroundQuotes(TRIM_WHITESPACES_AROUND_QUOTES)
@@ -98,7 +111,8 @@ static CsvReader extends CsvRecord> createReaderFor(CsvFileSource csvFileSourc
.allowMissingFields(ALLOW_MISSING_FIELDS)
.fieldSeparator(delimiter)
.quoteCharacter(csvFileSource.quoteCharacter())
- .commentStrategy(SKIP);
+ .commentStrategy(commentStrategy)
+ .commentCharacter(csvFileSource.commentCharacter());
var callbackHandler = createCallbackHandler(
csvFileSource.emptyValue(),
@@ -121,6 +135,26 @@ private static String selectDelimiter(char delimiter, String delimiterString) {
return DEFAULT_DELIMITER;
}
+ private static void validateControlCharactersDiffer(String delimiter, char quoteCharacter, char commentCharacter,
+ CommentStrategy commentStrategy) {
+
+ if (commentStrategy == NONE) {
+ Preconditions.condition(stringValuesUnique(delimiter, quoteCharacter),
+ () -> ("delimiter or delimiterString: '%s', and quoteCharacter: '%s' " + //
+ "must differ").formatted(delimiter, quoteCharacter));
+ }
+ else {
+ Preconditions.condition(stringValuesUnique(delimiter, quoteCharacter, commentCharacter),
+ () -> ("delimiter or delimiterString: '%s', quoteCharacter: '%s', and commentCharacter: '%s' " + //
+ "must all differ").formatted(delimiter, quoteCharacter, commentCharacter));
+ }
+ }
+
+ private static boolean stringValuesUnique(Object... values) {
+ long uniqueCount = Stream.of(values).map(String::valueOf).distinct().count();
+ return uniqueCount == values.length;
+ }
+
private static CsvCallbackHandler extends CsvRecord> createCallbackHandler(String emptyValue,
Set Note that {@link #delimiter} (or {@link #delimiterString}), {@link #quoteCharacter},
+ * and {@link #commentCharacter} (when {@link #textBlock} is used) are treated
+ * as control characters and must all be distinct.
+ *
* This annotation is inherited to subclasses.
@@ -132,9 +137,9 @@
* {@link #useHeadersInDisplayName}).
*
* In contrast to CSV records supplied via {@link #value}, a text block
- * can contain comments. Any line beginning with a hash tag ({@code #}) will
- * be treated as a comment and ignored. Note, however, that the {@code #}
- * symbol must be the first character on the line without any leading
+ * can contain comments. Any line beginning with a {@link #commentCharacter()} will
+ * be treated as a comment and ignored. Note, however, that the comment character
+ * must be the first character on the line without any leading
* whitespace. It is therefore recommended that the closing text block
* delimiter {@code """} be placed either at the end of the last line of
* input or on the following line, vertically aligned with the rest of the
@@ -142,7 +147,7 @@
*
* Java's text block
* feature automatically removes incidental whitespace when the code
- * is compiled. However other JVM languages such as Groovy and Kotlin do not.
+ * is compiled. However, other JVM languages such as Groovy and Kotlin do not.
* Thus, if you are using a programming language other than Java and your text
* block contains comments or new lines within quoted strings, you will need
* to ensure that there is no leading whitespace within your text block.
@@ -296,4 +301,20 @@
@API(status = STABLE, since = "5.10")
boolean ignoreLeadingAndTrailingWhitespace() default true;
+ /**
+ * The character used to denote comments in a {@linkplain #textBlock text block}.
+ *
+ * Any line that begins with this character will be treated as a comment and ignored
+ * during parsing.
+ *
+ * Note that the comment character must be the first character on the line
+ * without any leading whitespace.
+ *
+ * Defaults to a hashtag {@code #}.
+ *
+ * @since 6.0.1
+ */
+ @API(status = EXPERIMENTAL, since = "6.0.1")
+ char commentCharacter() default '#';
+
}
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java
index 49e98ea4be36..f8a02678f291 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/provider/CsvArgumentsProviderTests.java
@@ -400,6 +400,81 @@ void honorsCommentCharacterWhenUsingTextBlockAttribute() {
assertThat(arguments).containsExactly(array("bar", "#baz"), array("#bar", "baz"));
}
+ @Test
+ void honorsCustomCommentCharacter() {
+ var annotation = csvSource().textBlock("""
+ *foo
+ bar, *baz
+ '*bar', baz
+ """).commentCharacter('*').build();
+
+ var arguments = provideArguments(annotation);
+
+ assertThat(arguments).containsExactly(array("bar", "*baz"), array("*bar", "baz"));
+ }
+
+ @Test
+ void doesNotThrowExceptionWhenDelimiterAndCommentCharacterTheSameWhenUsingValueAttribute() {
+ var annotation = csvSource().lines("foo#bar").delimiter('#').commentCharacter('#').build();
+
+ var arguments = provideArguments(annotation);
+
+ assertThat(arguments).containsExactly(array("foo", "bar"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("invalidDelimiterAndQuoteCharacterCombinations")
+ void throwsExceptionWhenControlCharactersTheSameWhenUsingValueAttribute(Object delimiter, char quoteCharacter) {
+ var builder = csvSource().lines("foo").quoteCharacter(quoteCharacter);
+
+ var annotation = delimiter instanceof Character c //
+ ? builder.delimiter(c).build() //
+ : builder.delimiterString(delimiter.toString()).build();
+
+ var message = "delimiter or delimiterString: '%s', and quoteCharacter: '%s' must differ";
+ assertPreconditionViolationFor(() -> provideArguments(annotation).findAny()) //
+ .withMessage(message.formatted(delimiter, quoteCharacter));
+ }
+
+ static StreamInheritance
*
*