Skip to content

Commit bae67c1

Browse files
sbrannenrunningcode
authored andcommitted
Treat text blocks as files in @CsvSource
PR junit-team#2721 introduced experimental support for text blocks in @CsvSource; however, there was room for improvement. Prior to this commit, a CSV record within a text block could not contain a new line (\n), even if it was within a quoted string; whereas, this is supported when using @CsvSource's value attribute. In addition, comments do not make sense in a single string supplied via @CsvSource's value attribute, but they do make sense within a text block. This commit refines the text block support in @CsvSource by treating text blocks as complete CSV files, including support for comments beginning with a `#` symbol as well as support for new lines within quoted strings. Closes junit-team#2734
1 parent 45f7153 commit bae67c1

File tree

8 files changed

+212
-86
lines changed

8 files changed

+212
-86
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.8.2.adoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
*Date of Release:* ❓
55

6-
*Scope:* ❓
6+
*Scope:*
7+
8+
* Text blocks in `@CsvSource` are treated as CSV files
9+
* Custom quote character support in `@CsvSource`
710

811
For a complete list of all _closed_ issues and pull requests for this release, consult the
912
link:{junit5-repo}+/milestone/60?closed=1+[5.8.2] milestone page in the JUnit repository on
@@ -39,6 +42,11 @@ GitHub.
3942

4043
==== New Features and Improvements
4144

45+
* Text blocks in `@CsvSource` are now treated as complete CSV files, including support for
46+
comments beginning with a `+++#+++` symbol as well as support for new lines within
47+
quoted strings. See the
48+
<<../user-guide/index.adoc#writing-tests-parameterized-tests-sources-CsvSource, User
49+
Guide>> for details and examples.
4250
* The quote character for _quoted strings_ in `@CsvSource` is now configurable via the new
4351
`quoteCharacter` attribute, which defaults to a single quote (`'`) for backward
4452
compatibility.

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

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,32 +1332,13 @@ include::{testDir}/example/ExternalMethodSourceDemo.java[tags=external_MethodSou
13321332

13331333
`@CsvSource` allows you to express argument lists as comma-separated values (i.e., CSV
13341334
`String` literals). Each string provided via the `value` attribute in `@CsvSource`
1335-
represents a CSV line and results in one invocation of the parameterized test.
1335+
represents a CSV record and results in one invocation of the parameterized test.
13361336

13371337
[source,java,indent=0]
13381338
----
13391339
include::{testDir}/example/ParameterizedTestDemo.java[tags=CsvSource_example]
13401340
----
13411341

1342-
If the programming language you are using supports _text blocks_ -- for example, Java SE
1343-
15 or higher -- you can alternatively use the `textBlock` attribute of `@CsvSource`. Each
1344-
line within a text block represents a CSV line and results in one invocation of the
1345-
parameterized test. Using a text block, the previous example can be implemented as follows.
1346-
1347-
[source,java,indent=0]
1348-
----
1349-
@ParameterizedTest
1350-
@CsvSource(textBlock = """
1351-
apple, 1
1352-
banana, 2
1353-
'lemon, lime', 0xF1
1354-
strawberry, 700_000
1355-
""")
1356-
void testWithCsvSource(String fruit, int rank) {
1357-
// ...
1358-
}
1359-
----
1360-
13611342
The default delimiter is a comma (`,`), but you can use another character by setting the
13621343
`delimiter` attribute. Alternatively, the `delimiterString` attribute allows you to use a
13631344
`String` delimiter instead of a single character. However, both delimiter attributes
@@ -1391,11 +1372,69 @@ by default. This behavior can be changed by setting the
13911372
| `@CsvSource(value = { " apple , banana" }, ignoreLeadingAndTrailingWhitespace = false)` | `" apple "`, `" banana"`
13921373
|===
13931374

1375+
If the programming language you are using supports _text blocks_ -- for example, Java SE
1376+
15 or higher -- you can alternatively use the `textBlock` attribute of `@CsvSource`. Each
1377+
record within a text block represents a CSV record and results in one invocation of the
1378+
parameterized test. Using a text block, the previous example can be implemented as follows.
1379+
1380+
[source,java,indent=0]
1381+
----
1382+
@ParameterizedTest
1383+
@CsvSource(textBlock = """
1384+
apple, 1
1385+
banana, 2
1386+
'lemon, lime', 0xF1
1387+
strawberry, 700_000
1388+
""")
1389+
void testWithCsvSource(String fruit, int rank) {
1390+
// ...
1391+
}
1392+
----
1393+
1394+
In contrast to CSV records supplied via the `value` attribute, a text block can contain
1395+
comments. Any line beginning with a `+++#+++` symbol will be treated as a comment and
1396+
ignored. Note, however, that the `+++#+++` symbol must be the first character on the line
1397+
without any leading whitespace. It is therefore recommended that the closing text block
1398+
delimiter `"""` be placed either at the end of the last line of input or on the following
1399+
line, left aligned with the rest of the input (as can be seen in the example below which
1400+
demonstrates formatting similar to a table).
1401+
1402+
[source,java,indent=0]
1403+
----
1404+
@ParameterizedTest
1405+
@CsvSource(delimiter = '|', quoteCharacter = '"', textBlock = """
1406+
#-----------------------------
1407+
# FRUIT | RANK
1408+
#-----------------------------
1409+
apple | 1
1410+
#-----------------------------
1411+
banana | 2
1412+
#-----------------------------
1413+
"lemon lime" | 0xF1
1414+
#-----------------------------
1415+
strawberry | 700_000
1416+
#-----------------------------
1417+
""")
1418+
void testWithCsvSource(String fruit, int rank) {
1419+
// ...
1420+
}
1421+
----
1422+
1423+
[NOTE]
1424+
====
1425+
Java's https://docs.oracle.com/en/java/javase/15/text-blocks/index.html[text block]
1426+
feature automatically removes _incidental whitespace_ when the code is compiled.
1427+
However other JVM languages such as Groovy and Kotlin do not. Thus, if you are using a
1428+
programming language other than Java and your text block contains comments or new lines
1429+
within quoted strings, you will need to ensure that there is no leading whitespace within
1430+
your text block.
1431+
====
1432+
13941433
[[writing-tests-parameterized-tests-sources-CsvFileSource]]
13951434
===== @CsvFileSource
13961435

13971436
`@CsvFileSource` lets you use comma-separated value (CSV) files from the classpath or the
1398-
local file system. Each line from a CSV file results in one invocation of the
1437+
local file system. Each record from a CSV file results in one invocation of the
13991438
parameterized test.
14001439

14011440
The default delimiter is a comma (`,`), but you can use another character by setting the
@@ -1404,8 +1443,8 @@ The default delimiter is a comma (`,`), but you can use another character by set
14041443
cannot be set simultaneously.
14051444

14061445
.Comments in CSV files
1407-
NOTE: Any line beginning with a `#` symbol will be interpreted as a comment and will be
1408-
ignored.
1446+
NOTE: Any line beginning with a `+++#+++` symbol will be interpreted as a comment and will
1447+
be ignored.
14091448

14101449
[source,java,indent=0]
14111450
----
@@ -1418,13 +1457,13 @@ include::{testDir}/example/ParameterizedTestDemo.java[tags=CsvFileSource_example
14181457
include::{testResourcesDir}/two-column.csv[]
14191458
----
14201459

1421-
In contrast to the syntax used in `@CsvSource`, `@CsvFileSource` uses a double quote `"`
1422-
as the quote character. See the `"United States of America"` value in the example above.
1423-
An empty, quoted value `""` results in an empty `String` unless the `emptyValue` attribute
1424-
is set; whereas, an entirely _empty_ value is interpreted as a `null` reference. By
1425-
specifying one or more `nullValues`, a custom value can be interpreted as a `null`
1426-
reference. An `ArgumentConversionException` is thrown if the target type of a `null`
1427-
reference is a primitive type.
1460+
In contrast to the default syntax used in `@CsvSource`, `@CsvFileSource` uses a double
1461+
quote `"` as the quote character. See the `"United States of America"` value in the
1462+
example above. An empty, quoted value `""` results in an empty `String` unless the
1463+
`emptyValue` attribute is set; whereas, an entirely _empty_ value is interpreted as a
1464+
`null` reference. By specifying one or more `nullValues`, a custom value can be
1465+
interpreted as a `null` reference. An `ArgumentConversionException` is thrown if the
1466+
target type of a `null` reference is a primitive type.
14281467

14291468
NOTE: An _unquoted_ empty value will always be converted to a `null` reference regardless
14301469
of any custom values configured via the `nullValues` attribute.

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvArgumentsProvider.java

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
import static org.junit.jupiter.params.provider.CsvParserFactory.createParserFor;
1414
import static org.junit.platform.commons.util.CollectionUtils.toSet;
1515

16+
import java.io.StringReader;
1617
import java.lang.annotation.Annotation;
1718
import java.util.Arrays;
19+
import java.util.List;
1820
import java.util.Set;
1921
import java.util.concurrent.atomic.AtomicInteger;
20-
import java.util.regex.Pattern;
2122
import java.util.stream.Stream;
2223

2324
import com.univocity.parsers.csv.CsvParser;
@@ -33,8 +34,6 @@
3334
*/
3435
class CsvArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<CsvSource> {
3536

36-
private static final Pattern NEW_LINE_REGEX = Pattern.compile("\\n");
37-
3837
private static final String LINE_SEPARATOR = "\n";
3938

4039
private CsvSource annotation;
@@ -54,22 +53,35 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
5453
Preconditions.condition(this.annotation.value().length > 0 ^ textBlockDeclared,
5554
() -> "@CsvSource must be declared with either `value` or `textBlock` but not both");
5655

57-
String[] lines;
5856
if (textBlockDeclared) {
59-
lines = NEW_LINE_REGEX.split(this.annotation.textBlock(), 0);
60-
}
61-
else {
62-
lines = this.annotation.value();
57+
return parseTextBlock(this.annotation.textBlock()).stream().map(Arguments::of);
6358
}
6459

65-
AtomicInteger index = new AtomicInteger(1);
60+
AtomicInteger index = new AtomicInteger(0);
6661
// @formatter:off
67-
return Arrays.stream(lines)
68-
.map(line -> parseLine(line, index.getAndIncrement()))
62+
return Arrays.stream(this.annotation.value())
63+
.map(line -> parseLine(line, index.incrementAndGet()))
6964
.map(Arguments::of);
7065
// @formatter:on
7166
}
7267

68+
private List<String[]> parseTextBlock(String textBlock) {
69+
try {
70+
AtomicInteger index = new AtomicInteger(0);
71+
List<String[]> csvRecords = this.csvParser.parseAll(new StringReader(textBlock));
72+
for (String[] csvRecord : csvRecords) {
73+
index.incrementAndGet();
74+
Preconditions.notNull(csvRecord,
75+
() -> "Line at index " + index.get() + " contains invalid CSV: \"\"\"\n" + textBlock + "\n\"\"\"");
76+
processNullValues(csvRecord, this.nullValues);
77+
}
78+
return csvRecords;
79+
}
80+
catch (Throwable throwable) {
81+
throw handleCsvException(throwable, this.annotation);
82+
}
83+
}
84+
7385
private String[] parseLine(String line, int index) {
7486
try {
7587
String[] csvRecord = this.csvParser.parseLine(line + LINE_SEPARATOR);

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvFileSource.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,19 @@
2323

2424
/**
2525
* {@code @CsvFileSource} is an {@link ArgumentsSource} which is used to load
26-
* comma-separated value (CSV) files from one or more classpath {@link #resources
27-
* resources} or {@link #files}.
26+
* comma-separated value (CSV) files from one or more classpath {@link #resources}
27+
* or {@link #files}.
2828
*
29-
* <p>The lines of these CSV files will be provided as arguments to the annotated
30-
* {@code @ParameterizedTest} method.
29+
* <p>The CSV records parsed from these resources and files will be provided as
30+
* arguments to the annotated {@code @ParameterizedTest} method.
3131
*
3232
* <p>Any line beginning with a {@code #} symbol will be interpreted as a comment
3333
* and will be ignored.
3434
*
35-
* <p>The column delimiter (defaults to comma) can be customized with either
36-
* {@link #delimiter} or {@link #delimiterString}.
35+
* <p>The column delimiter (which defaults to a comma ({@code ,})) can be customized
36+
* via either {@link #delimiter} or {@link #delimiterString}.
3737
*
38-
* <p>In contrast to the syntax used in {@code @CsvSource}, {@code @CsvFileSource}
38+
* <p>In contrast to the default syntax used in {@code @CsvSource}, {@code @CsvFileSource}
3939
* uses a double quote ({@code "}) as its quote character (see the User Guide for
4040
* examples). An empty, quoted value ({@code ""}) results in an empty {@link String}
4141
* unless the {@link #emptyValue} attribute is set; whereas, an entirely <em>empty</em>
@@ -89,7 +89,7 @@
8989

9090
/**
9191
* The line separator to use when reading the CSV files; must consist of 1
92-
* or 2 characters.
92+
* or 2 characters, typically {@code "\r"}, {@code "\n"}, or {@code "\r\n"}.
9393
*
9494
* <p>Defaults to {@code "\n"}.
9595
*/
@@ -159,7 +159,7 @@
159159
String[] nullValues() default {};
160160

161161
/**
162-
* The maximum characters of per CSV column allowed.
162+
* The maximum number of characters allowed per CSV column.
163163
*
164164
* <p>Must be a positive number.
165165
*
@@ -171,13 +171,14 @@
171171
int maxCharsPerColumn() default 4096;
172172

173173
/**
174-
* Identifies whether leading and trailing whitespace characters of
175-
* unquoted CSV columns should be ignored.
174+
* Controls whether leading and trailing whitespace characters of unquoted
175+
* CSV columns should be ignored.
176176
*
177177
* <p>Defaults to {@code true}.
178178
*
179179
* @since 5.8
180180
*/
181181
@API(status = EXPERIMENTAL, since = "5.8")
182182
boolean ignoreLeadingAndTrailingWhitespace() default true;
183+
183184
}

junit-jupiter-params/src/main/java/org/junit/jupiter/params/provider/CsvParserFactory.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,13 @@ class CsvParserFactory {
2626
private static final String LINE_SEPARATOR = "\n";
2727
private static final char DOUBLE_QUOTE = '"';
2828
private static final char EMPTY_CHAR = '\0';
29-
private static final boolean COMMENT_PROCESSING_FOR_CSV_SOURCE = false;
3029
private static final boolean COMMENT_PROCESSING_FOR_CSV_FILE_SOURCE = true;
3130

3231
static CsvParser createParserFor(CsvSource annotation) {
3332
String delimiter = selectDelimiter(annotation, annotation.delimiter(), annotation.delimiterString());
33+
boolean commentProcessingEnabled = !annotation.textBlock().isEmpty();
3434
return createParser(delimiter, LINE_SEPARATOR, annotation.quoteCharacter(), annotation.emptyValue(),
35-
annotation.maxCharsPerColumn(), COMMENT_PROCESSING_FOR_CSV_SOURCE,
36-
annotation.ignoreLeadingAndTrailingWhitespace());
35+
annotation.maxCharsPerColumn(), commentProcessingEnabled, annotation.ignoreLeadingAndTrailingWhitespace());
3736
}
3837

3938
static CsvParser createParserFor(CsvFileSource annotation) {

0 commit comments

Comments
 (0)