├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── LICENSE.md ├── README.md ├── bin ├── release.sh └── set-version.sh ├── docs └── user-guide.md ├── etc └── jreleaser │ └── changelog.md.tpl ├── junit5-formatted-source-tests ├── pom.xml └── src │ └── test │ └── java │ ├── com │ └── mikemybytes │ │ └── junit5 │ │ └── formatted │ │ └── test │ │ ├── FormattedSourceIndexedPlaceholdersTest.java │ │ ├── FormattedSourcePositionalPlaceholdersTest.java │ │ ├── FormattedSourceTestIndexedPlaceholdersTest.java │ │ └── FormattedSourceTestPositionalPlaceholdersTest.java │ └── module-info.java ├── junit5-formatted-source ├── pom.xml └── src │ ├── main │ └── java │ │ ├── com │ │ └── mikemybytes │ │ │ └── junit5 │ │ │ └── formatted │ │ │ ├── ArgumentsExtractor.java │ │ │ ├── FormatAnalyzer.java │ │ │ ├── FormatAnalyzers.java │ │ │ ├── FormatArgumentMatcherGroup.java │ │ │ ├── FormatSpecification.java │ │ │ ├── FormattedSource.java │ │ │ ├── FormattedSourceArgumentsProvider.java │ │ │ ├── FormattedSourceData.java │ │ │ ├── FormattedSourceTest.java │ │ │ ├── FormattedSourceTestArgumentsProvider.java │ │ │ ├── IndexedArgumentPlaceholdersFormatAnalyzer.java │ │ │ ├── LinePatternFactory.java │ │ │ ├── PositionalArgumentPlaceholdersFormatAnalyzer.java │ │ │ ├── Preconditions.java │ │ │ └── RawArgumentsProcessor.java │ │ └── module-info.java │ └── test │ └── java │ └── com │ └── mikemybytes │ └── junit5 │ └── formatted │ ├── IndexedArgumentPlaceholdersFormatAnalyzerTest.java │ └── PositionalArgumentPlaceholdersFormatAnalyzerTest.java ├── mvnw ├── mvnw.cmd └── pom.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | ignore: 8 | # for compatibility reasons, we stick to the oldest version supporting features required by this library 9 | - dependency-name: "org.junit.jupiter:junit-jupiter" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.md' 9 | - 'LICENSE.md' 10 | - '.gitignore' 11 | - 'docs/**' 12 | - 'etc/jreleaser/changelog.md.tpl' 13 | - 'bin/**' 14 | pull_request: 15 | paths-ignore: 16 | - 'README.md' 17 | - 'LICENSE.md' 18 | - '.gitignore' 19 | - 'docs/**' 20 | - 'etc/jreleaser/changelog.md.tpl' 21 | - 'bin/**' 22 | 23 | jobs: 24 | build: 25 | 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Set up JDK 21 31 | uses: actions/setup-java@v4 32 | with: 33 | java-version: '21' 34 | distribution: 'temurin' 35 | cache: maven 36 | - name: Build with Maven 37 | run: ./mvnw -B clean verify --file pom.xml 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven template 2 | target/ 3 | pom.xml.tag 4 | pom.xml.releaseBackup 5 | pom.xml.versionsBackup 6 | pom.xml.next 7 | release.properties 8 | dependency-reduced-pom.xml 9 | buildNumber.properties 10 | .mvn/timing.properties 11 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 12 | .mvn/wrapper/maven-wrapper.jar 13 | 14 | ### Java template 15 | # Compiled class file 16 | *.class 17 | 18 | # Log file 19 | *.log 20 | 21 | # BlueJ files 22 | *.ctxt 23 | 24 | # Mobile Tools for Java (J2ME) 25 | .mtj.tmp/ 26 | 27 | # Package Files # 28 | *.jar 29 | *.war 30 | *.nar 31 | *.ear 32 | *.zip 33 | *.tar.gz 34 | *.rar 35 | 36 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 37 | hs_err_pid* 38 | 39 | ### JetBrains 40 | .idea/ 41 | **/*.iml 42 | 43 | ### Linux template 44 | *~ 45 | 46 | # temporary files which can be created if a process still has a handle open of a deleted file 47 | .fuse_hidden* 48 | 49 | # KDE directory preferences 50 | .directory 51 | 52 | # Linux trash folder which might appear on any partition or disk 53 | .Trash-* 54 | 55 | # .nfs files are created when an open file is removed but is still being accessed 56 | .nfs* 57 | 58 | ### macOS template 59 | # General 60 | .DS_Store 61 | .AppleDouble 62 | .LSOverride 63 | 64 | # Icon must end with two \r 65 | Icon 66 | 67 | # Thumbnails 68 | ._* 69 | 70 | # Files that might appear in the root of a volume 71 | .DocumentRevisions-V100 72 | .fseventsd 73 | .Spotlight-V100 74 | .TemporaryItems 75 | .Trashes 76 | .VolumeIcon.icns 77 | .com.apple.timemachine.donotpresent 78 | 79 | # Directories potentially created on remote AFP share 80 | .AppleDB 81 | .AppleDesktop 82 | Network Trash Folder 83 | Temporary Items 84 | .apdisk 85 | 86 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mike Kowalski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JUnit 5 FormattedSource 2 | 3 | ![](https://img.shields.io/github/license/mikemybytes/junit5-formatted-source) 4 | ![](https://img.shields.io/github/v/release/mikemybytes/junit5-formatted-source) 5 | ![](https://img.shields.io/maven-central/v/com.mikemybytes/junit5-formatted-source) 6 | ![](https://img.shields.io/github/actions/workflow/status/mikemybytes/junit5-formatted-source/build.yml) 7 | 8 | This library extends [JUnit 5](https://github.com/junit-team/junit5) with a new way of writing [parameterized tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests). 9 | It allows defining test case arguments in a human-readable way, following a user-defined format. Additionally, it automatically 10 | takes care of the test case names, so the input definition is also what will be presented in the test execution output. 11 | 12 | ```java 13 | class CalculatorTest { 14 | 15 | private final Calculator calculator = new Calculator(); 16 | 17 | @FormattedSourceTest(format = "{0} + {1} = {2}", lines = { 18 | "1 + 2 = 3", 19 | "3 + 4 = 7" 20 | }) 21 | void calculatesSum(int a, int b, int expectedSum) { 22 | Assertions.assertEquals(expectedSum, calculator.sum(a, b)); 23 | } 24 | 25 | } 26 | ``` 27 | 28 | Output: 29 | ``` 30 | calculatesSum(int, int, int) ✔ 31 | ├─ 1 + 2 = 3 ✔ 32 | └─ 3 + 4 = 7 ✔ 33 | ``` 34 | 35 | Of course, _JUnit 5 FormattedSource_ can do even more! 36 | 37 | ## Installing 38 | 39 | ### Requirements 40 | - Java 11+ 41 | - JUnit 5.8.0+ 42 | 43 | _Note: The library does not introduce any dependencies other than the JUnit 5._ 44 | 45 | ### Maven 46 | 47 | ```xml 48 | 49 | com.mikemybytes 50 | junit5-formatted-source 51 | 1.0.1 52 | test 53 | 54 | ``` 55 | 56 | ### Gradle 57 | 58 | ```groovy 59 | testImplementation "com.mikemybytes:junit5-formatted-source:1.0.1" 60 | ``` 61 | 62 | ### Java Platform Module System (JPMS) 63 | 64 | Java module name: `com.mikemybytes.junit5.formatted` ([descriptor](junit5-formatted-source/src/main/java/module-info.java), 65 | [example usage](junit5-formatted-source-tests/src/test/java/module-info.java)) 66 | 67 | ## User Guide 68 | 69 | Details and usage examples can be found in the project's [User Guide](docs/user-guide.md). 70 | 71 | ## Yet another argument source? 72 | 73 | The project has been inspired by the built-in [`@CsvSource` annotation](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources-CsvSource), 74 | which allows writing not only [data table tests](https://mikemybytes.com/2021/10/19/parameterize-like-a-pro-with-junit-5-csvsource/) 75 | but also specification-like test case definitions: 76 | 77 | ```java 78 | class CsvSourceSpecificationTest { 79 | 80 | @ParameterizedTest(name = "{0} maps to {1}") 81 | @CsvSource(delimiterString = "maps to", textBlock = """ 82 | 'foo' maps to 'bar' 83 | 'junit' maps to 'jupiter' 84 | """) 85 | void mapsOneValueToAnother(String input, String expectedValue) { 86 | // ... 87 | } 88 | 89 | } 90 | ``` 91 | 92 | Yet, the `@CsvSource` limits the user to only one `delimiterString`, which effectively means supporting 93 | only two arguments at a time. Additionally, selected delimiter must be repeated within the `@ParameterizedTest`'s `name` 94 | parameter in order to appear in the test execution output. 95 | 96 | Using `@FormattedSource` allows to forget about these limitations and write less code: 97 | 98 | ```java 99 | class FormattedSourceSpecificationTest { 100 | 101 | @FormattedSourceTest(format = "{0} maps to {1} using rule {2}", textBlock = """ 102 | 'foo' maps to 'bar' using rule 486 103 | 'junit' maps to 'jupiter' using rule 44 104 | """) 105 | void mapsOneValueToAnother(String input, String expectedValue, int expectedRuleId) { 106 | // ... 107 | } 108 | 109 | } 110 | ``` 111 | 112 | Test case names are automatically generated based on provided specification: 113 | ``` 114 | mapsOneValueToAnother(String, String, int) ✔ 115 | ├─ 'foo' maps to 'bar' using rule 486 ✔ 116 | └─ 'junit' maps to 'jupiter' using rule 44 ✔ 117 | ``` 118 | 119 | ## `@FormattedSourceTest` vs `@FormattedSource` 120 | 121 | The library comes with two annotations. `@FormattedSource` is just a standard [JUnit 5 argument source](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources) 122 | that has to be combined with `@ParameterizedTest`. It also does not influence generated test case names automatically: 123 | 124 | ```java 125 | class DurationEncodingTest { 126 | 127 | @ParameterizedTest(name = "encodes {0} seconds as {1}") 128 | @FormattedSource(format = "encodes {0} seconds as {1}", lines = { 129 | "encodes 15 seconds as 'PT15S'", 130 | "encodes 180 seconds as 'PT3M'", 131 | "encodes 172800 seconds as 'PT48H'" 132 | }) 133 | void encodesDurationAsIso8601(long seconds, String expected) { 134 | // ... 135 | } 136 | 137 | } 138 | ``` 139 | 140 | The equivalent `@FormattedSourceTest` simply results in a less verbose code: 141 | ```java 142 | class DurationEncodingShorterTest { 143 | 144 | // @ParameterizedTest already included (with test case name!) 145 | @FormattedSourceTest(format = "encodes {0} seconds as {1}", lines = { 146 | "encodes 15 seconds as 'PT15S'", 147 | "encodes 180 seconds as 'PT3M'", 148 | "encodes 172800 seconds as 'PT48H'" 149 | }) 150 | void encodesDurationAsIso8601(long seconds, String expected) { 151 | // ... 152 | } 153 | 154 | } 155 | ``` 156 | 157 | ## Building from source 158 | 159 | The project comes with [Maven Wrapper](https://maven.apache.org/wrapper/), so it can be built even without Maven 160 | installed locally. There's no need to pass any additional properties. 161 | 162 | The minimum Java version required to build the project is Java 17. Produced artifacts will be binary-compatible 163 | with Java 11+. 164 | 165 | ### Build 166 | 167 | ``` 168 | ./mvnw clean verify 169 | ``` 170 | 171 | ### Build & install 172 | 173 | ``` 174 | ./mvnw clean install 175 | ``` 176 | 177 | ### License 178 | 179 | The project is distributed under the [MIT license](LICENSE.md). 180 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | function log() { 6 | purple="\033[0;35m" 7 | bold=$(tput bold) 8 | normal=$(tput sgr0) 9 | echo -e "${purple}${bold}>>> ${1}${normal}" 10 | } 11 | 12 | current_version=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout) 13 | log "Current version: ${current_version}" 14 | 15 | next_version=$(echo "${current_version}" | sed 's/-SNAPSHOT//g') 16 | log "Next version: ${next_version}" 17 | 18 | log "Updating README.md" 19 | sed -i '' "s/.*<\/version>/${next_version}<\/version>/" README.md 20 | gradle_prefix='testImplementation "com.mikemybytes:junit5-formatted-source:' 21 | sed -i '' "s/testImplementation.*junit5-formatted-source:.*/${gradle_prefix}${next_version}\"/" README.md 22 | git add README.md 23 | git commit -m "docs: [release] Update artifact coordinates (new version ${next_version})" 24 | 25 | log "Building with Maven" 26 | ./mvnw clean verify 27 | 28 | log "Preparing Maven release" 29 | ./mvnw --batch-mode release:clean release:prepare 30 | 31 | released_version=$(git describe --abbrev=0 --tags) 32 | log "Released version: ${released_version}" 33 | 34 | log "Cleaning up after Maven release" 35 | ./mvnw --batch-mode release:clean 36 | 37 | log "Pushing to git" 38 | git push origin main 39 | git push origin "${released_version}" 40 | 41 | log "Checking out released sourced" 42 | git checkout --quiet "${released_version}" 43 | 44 | log "Staging artifacts" 45 | # ensure all artifacts are staged 46 | # https://jreleaser.org/guide/latest/examples/maven/staging-artifacts.html 47 | ./mvnw deploy -DaltDeploymentRepository=local::file:./target/staging-deploy 48 | 49 | log "Invoking jreleaser" 50 | ./mvnw -pl :junit5-formatted-source-parent jreleaser:full-release 51 | 52 | log "Checking out main" 53 | git checkout main 54 | 55 | log "Successfully released version ${released_version}" 56 | -------------------------------------------------------------------------------- /bin/set-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | fail() { 6 | echo >&2 "$@" 7 | exit 1 8 | } 9 | 10 | function log() { 11 | purple="\033[0;35m" 12 | bold=$(tput bold) 13 | normal=$(tput sgr0) 14 | echo -e "${purple}${bold}>>> ${1}${normal}" 15 | } 16 | 17 | [ "$#" -eq 1 ] || fail "You must specify a version to run this script" 18 | 19 | log "Setting version $1" 20 | ./mvnw versions:set -DgenerateBackupPoms=false -DnewVersion="$1" 21 | log "Project version set to $1" 22 | -------------------------------------------------------------------------------- /docs/user-guide.md: -------------------------------------------------------------------------------- 1 | # JUnit 5 FormattedSource User Guide 2 | 3 | ## The basics 4 | 5 | _JUnit 5 FormattedSource_ extends [JUnit 5](https://github.com/junit-team/junit5) with a new way of writing 6 | [parameterized tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests). 7 | Test cases are represented as strings (similarly to the [@CsvSource](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources-CsvSource) 8 | annotation), following a user-defined format. 9 | 10 | Let's start from an example: 11 | ```java 12 | @FormattedSourceTest( // FormattedSource annotation 13 | format = "{0} + {1} = {2}", // format string 14 | lines = { // test cases definition 15 | "1 + 2 = 3", 16 | "3 + 4 = 7" 17 | } 18 | ) 19 | void calculatesSum(int a, int b, int sum) { 20 | Assertions.assertEquals(sum, a + b); 21 | } 22 | ``` 23 | 24 | The simplest use case contains three elements: 25 | - FormattedSource annotation (`@FormattedSourceTest` or `@FormattedSource`), 26 | - format string, 27 | - test cases definition. 28 | 29 | ### FormattedSource annotations 30 | 31 | `@FormattedSourceTest` turns our `calculatesSum` method into a FormattedSource parameterized test. 32 | In fact, it's a shorthand for: 33 | ```java 34 | @ParameterizedTest(name = "{0} + {1} = {2}") 35 | @FormattedSource( 36 | format = "{0} + {1} = {2}", 37 | lines = { 38 | "1 + 2 = 3", 39 | "3 + 4 = 7" 40 | } 41 | ) 42 | void calculatesSum(int a, int b, int sum) { ... } 43 | ``` 44 | 45 | There's no magic here. `@FormattedSource` is just another [JUnit 5 argument source](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources). 46 | It not only save you from typing `@ParameterizedTest` every time, but also uses the same _format string_ for the 47 | displayed test execution output: 48 | ``` 49 | calculatesSum(int, int, int) ✔ 50 | ├─ 1 + 2 = 3 ✔ 51 | └─ 3 + 4 = 7 ✔ 52 | ``` 53 | 54 | **Both `@FormattedSource` and `@FormattedSourceTest` share all their properties**. This means you can always turn one 55 | into another, depending on the use case. 56 | 57 | Unless you really need additional control (e.g. to further customize the displayed test execution output), 58 | `@FormattedSourceTest` should be preferred over `@FormattedSource`. 59 | 60 | ### Format string 61 | 62 | **The _format string_ defines, how each test case definition has to look**. It instructs the FormattedSource 63 | engine where to find test method argument values. The format string has to be provided using a mandatory `format` 64 | parameter. 65 | 66 | Let's analyze the format string of our example: `{0} + {1} = {2}`. 67 | 68 | **By default, you can reference test method arguments using their indexes (starting from zero) surrounded with braces 69 | (curly brackets, `{` and `}`)**. This means `{0}` refers to the first argument, `{2}` to the third one, etc. 70 | 71 | **The parameters don't have to appear in the same order as the test method's arguments**. For our 72 | `calculatesSum(int a, int b, int sum)` method, an alternative format string like `{2} = {0} + {1}` could be used as 73 | well. 74 | 75 | ### Test cases definition 76 | 77 | **FormattedSource test cases are represented as text, where each line represents a separate test case**. 78 | 79 | There are two alternative ways of defining test cases. **By using `lines`, we can specify them as an array of strings**: 80 | ```java 81 | @FormattedSourceTest( 82 | format = "{0} + {1} = {2}", 83 | lines = { // that's an array! 84 | "1 + 2 = 3", 85 | "3 + 4 = 7" 86 | } 87 | ) 88 | void calculatesSum(int a, int b, int sum) { .. } 89 | ``` 90 | 91 | If you're running on Java 15+ (and you really should be!), you can use Text Block instead and pass it as the `textBlock` 92 | param: 93 | ```java 94 | @FormattedSourceTest( 95 | format = "{0} + {1} = {2}", 96 | textBlock = """ 97 | 1 + 2 = 3 98 | 3 + 4 = 7 99 | """ 100 | ) 101 | void calculatesSum(int a, int b, int sum) { .. } 102 | ``` 103 | 104 | Both approaches (`lines` and `textBlock`) are functionally equivalent and mutually exclusive. 105 | 106 | Similarly to other argument sources like `@ValueSource` or `@CsvSource`, standard [implicit argument conversions](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-argument-conversion) 107 | are fully supported. This means you're not limited only to numbers and strings as the argument values: 108 | ```java 109 | @FormattedSourceTest( 110 | format = "UUID: {0}, Date: {1}, URL: {2}", 111 | lines = { "UUID: f39f1c74-c0f8-4964-a5a0-7ce03c8aff4a, Date: 2023-04-23, URL: https://junit.org/" } 112 | ) 113 | void supportsImplicitArgumentConversions(UUID uuid, LocalDate date, URL url) { .. } 114 | ``` 115 | 116 | ## Advanced features 117 | 118 | ### Using positional arguments syntax 119 | 120 | Usually, the order of arguments in the format string follows the order of the test method's parameters. Using the 121 | default indexed syntax in the format string (e.g. `{0} -> {1}`) may then feel unnecessarily verbose. In cases like 122 | these, a fixed placeholder string can be used instead. 123 | 124 | Specifying the `argumentPlaceholder` parameter disables the default indexed syntax. Instead, the provided string 125 | has to be used to represent all the arguments: 126 | ```java 127 | @FormattedSourceTest( 128 | format = "? + ? = ?", 129 | argumentPlaceholder = "?", 130 | textBlock = """ 131 | 1 + 2 = 3 132 | 3 + 4 = 7 133 | """ 134 | ) 135 | void calculatesSum(int a, int b, int sum) { .. } 136 | ``` 137 | 138 | Note that in this approach, the order of arguments in the format string has to always match the order of the test 139 | method's parameters. 140 | 141 | As the library does not currently support escaping braces in the format string, the positional argument syntax should be 142 | considered a recommended alternative. 143 | 144 | ### Quoting argument values 145 | 146 | Sometimes, test case readability could be improved by quoting specific argument values. By default, you can always use 147 | single quotes (`'`) in order to achieve that: 148 | 149 | ```java 150 | @FormattedSourceTest( 151 | format = "this is a string: {0}, while this is not: {1}", 152 | textBlock = """ 153 | this is a string: 'JUnit is great!', while this is not: 42 154 | this is a string: foo, while this is not: '11' 155 | """ 156 | ) 157 | void verifySomething(String str, int num) { .. } 158 | ``` 159 | As you can see, all types of parameters could be quoted - not only the string ones. 160 | 161 | You can change the character used for quoting using `quoteCharacter` parameter: 162 | ```java 163 | @FormattedSourceTest( 164 | format = "this is a string: {0}, while this is not: {1}", 165 | quoteCharacter = '*', 166 | textBlock = """ 167 | this is a string: *JUnit is great!*, while this is not: 42 168 | this is a string: foo, while this is not: *11* 169 | """ 170 | ) 171 | void verifySomething(String str, int num) { .. } 172 | ``` 173 | 174 | ### Additional whitespaces 175 | 176 | By default, leading and trailing whitespaces will be removed from the argument value. This allows us to improve the readability by visually aligning multiple test cases: 177 | ```java 178 | @FormattedSourceTest( 179 | format = "{0} + {1} = {2}", 180 | textBlock = """ 181 | 1 + 2 = 3 182 | 11 + 98 = 109 183 | 764 + 2 = 766 184 | """ 185 | ) 186 | void calculatesSum(int a, int b, int sum) { .. } 187 | ``` 188 | 189 | You can disable this behaviour entirely by passing `ignoreLeadingAndTrailingWhitespace = false`. 190 | 191 | If you want to preserve leading/trailing whitespaces of a certain value, simply quote it: 192 | ```java 193 | @FormattedSourceTest( 194 | format = "Does {0} starts with a whitespace? {1}", 195 | textBlock = """ 196 | Does JUnit starts with a whitespace? false 197 | Does ' JUnit' starts with a whitespace? true 198 | """ 199 | ) 200 | void startsFromWhitespace(String str, boolean startsFromWhitespace) { .. } 201 | ``` 202 | 203 | ### Null values 204 | 205 | By default, unquoted empty argument values are converted to `null`: 206 | ```java 207 | @FormattedSourceTest( 208 | format = "Is {0} a null? {1}", 209 | lines = { 210 | "Is null a null? false", // non-empty string, no special meaning 211 | "Is '' a null? false", // quoted empty string is just an empty string 212 | "Is a null? true" // unquoted empty string resolves to null 213 | } 214 | ) 215 | void isNull(String str, boolean expectedNull) { ... } 216 | ``` 217 | 218 | You can define your own set of "null values" that should be converted to `null` using `nullValues` parameter: 219 | ```java 220 | @FormattedSourceTest( 221 | format = "Is {0} a null? {1}", 222 | nullValues = { "NULL", "nil" }, // array of strings 223 | lines = { 224 | "Is NULL a null? true", 225 | "Is nil a null? true", 226 | "Is null a null? false", // case-sensitive match 227 | } 228 | ) 229 | void isNull(String str, boolean expectedNull) { ... } 230 | ``` 231 | 232 | ### Custom empty value 233 | 234 | Similarly to the `@CsvSource`, quoted empty values could be substituted with a value passed as the `emptyValue` 235 | parameter: 236 | ```java 237 | @FormattedSourceTest( 238 | emptyValue = "EMPTY", 239 | format = "empty: {0}", 240 | textBlock = """ 241 | empty: '' 242 | """ 243 | ) 244 | void supportsCustomEmptyValue(String str) { 245 | Assertions.assertEquals("EMPTY", str); 246 | } 247 | ``` 248 | 249 | ## Usage ideas (aka the kitchen sink) 250 | 251 | ### Testing mappers and encoders 252 | 253 | ```java 254 | @FormattedSourceTest( 255 | format = "{0} -> {1}", 256 | textBlock = """ 257 | 'foo' -> 'bar' 258 | 'junit' -> 'jupiter' 259 | """ 260 | ) 261 | void mapsOneValueToAnother(String input, String expectedValue) { ... } 262 | ``` 263 | 264 | ```java 265 | @FormattedSourceTest( 266 | format = "{0} maps to {1}", 267 | textBlock = """ 268 | 'foo' maps to 'bar' 269 | 'junit' maps to 'jupiter' 270 | """ 271 | ) 272 | void mapsOneValueToAnother(String input, String expectedValue) { ... } 273 | ``` 274 | 275 | ```java 276 | @FormattedSourceTest( 277 | format = "encodes {0} seconds as {1}", 278 | lines = { 279 | "encodes 15 seconds as 'PT15S'", 280 | "encodes 180 seconds as 'PT3M'", 281 | "encodes 172800 seconds as 'PT48H'" 282 | } 283 | ) 284 | void encodesDurationAsIso8601(long seconds, String expected) { ... } 285 | ``` 286 | 287 | ### Testing validators 288 | 289 | ```java 290 | @FormattedSourceTest( 291 | format = "{0} for {1}", 292 | lines = { 293 | "fails for 48", 294 | "succeeds for +48123456789" 295 | } 296 | ) 297 | void validatesPhoneNumber(String expectedResult, String phoneNumber) { 298 | boolean expected = "succeeds".equals(expectedResult); 299 | Assertions.assertEquals(expected, validator.validate(phoneNumber)); 300 | } 301 | ``` 302 | 303 | ### Testing HTTP APIs 304 | 305 | ```java 306 | enum UserType { CUSTOMER, ADMIN } 307 | 308 | @FormattedSourceTest( 309 | format = "{0} returns {1} for {2}", 310 | textBlock = """ 311 | /api/items returns 200 for CUSTOMER 312 | /api/items returns 404 for ADMIN 313 | /api/admin/inventory returns 401 for CUSTOMER 314 | /api/admin/inventory returns 200 for ADMIN 315 | """ 316 | ) 317 | void protectsEndpointsBasedOnUserType(URI uri, int httpCode, UserType userType) { 318 | User user = TestUserFactory.authenticated(userType); 319 | testHttpClient.get(uri).returnsResponseCode(httpCode); 320 | } 321 | ``` 322 | -------------------------------------------------------------------------------- /etc/jreleaser/changelog.md.tpl: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | {{changelogChanges}} 4 | 5 | {{changelogContributors}} -------------------------------------------------------------------------------- /junit5-formatted-source-tests/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | junit5-formatted-source-parent 5 | com.mikemybytes 6 | 1.0.2-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | junit5-formatted-source-tests 11 | pom 12 | 13 | JUnit 5 FormattedSource (tests) 14 | A set of integration tests for the JUnit 5 FormattedSource library 15 | ${github.project-url} 16 | 17 | 18 | 17 19 | 17 20 | 17 21 | 22 | 23 | 24 | 25 | com.mikemybytes 26 | junit5-formatted-source 27 | ${project.version} 28 | test 29 | 30 | 31 | 32 | org.assertj 33 | assertj-core 34 | test 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | org.apache.maven.plugins 45 | maven-compiler-plugin 46 | 47 | 48 | testCompile 49 | 50 | testCompile 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-surefire-plugin 58 | 59 | 60 | test 61 | 62 | test 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /junit5-formatted-source-tests/src/test/java/com/mikemybytes/junit5/formatted/test/FormattedSourceIndexedPlaceholdersTest.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted.test; 2 | 3 | import com.mikemybytes.junit5.formatted.FormattedSource; 4 | import org.junit.jupiter.api.TestInfo; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class FormattedSourceIndexedPlaceholdersTest { 10 | 11 | @ParameterizedTest(name = "{0} + {1} = {2}") 12 | @FormattedSource(format = "{0} + {1} = {2}", lines = { 13 | "1 + 2 = 3", 14 | "3 + 4 = 7" 15 | }) 16 | void supportsSimpleFormat(int a, int b, int c) { 17 | assertThat(a + b).isEqualTo(c); 18 | } 19 | 20 | @ParameterizedTest(name = "{0} plus {1} is equal to {2}") 21 | @FormattedSource(format = "{0} + {1} = {2}", lines = { 22 | "1 + 2 = 3", 23 | "3 + 4 = 7" 24 | }) 25 | void supportsSimpleFormatWithDisplayName(int a, int b, int c, TestInfo info) { 26 | assertThat(a + b).isEqualTo(c); 27 | assertThat(info.getDisplayName()).isEqualTo("%d plus %d is equal to %d".formatted(a, b, c)); 28 | } 29 | 30 | @ParameterizedTest 31 | @FormattedSource(format = "{0} + {1} = {2}", textBlock = """ 32 | 1 + 2 = 3 33 | 3 + 4 = 7 34 | """ 35 | ) 36 | void supportsSimpleTemplateViaTextBlock(int a, int b, int c) { 37 | assertThat(a + b).isEqualTo(c); 38 | } 39 | 40 | @ParameterizedTest(name = "{0} maps to {1} and gives {2}") 41 | @FormattedSource(format = "{0} maps to {1} and gives {2}", lines = { 42 | "'foo' maps to 'bar' and gives 'xyz'" 43 | }) 44 | void supportsFullTextFormat(String a, String b, String c) { 45 | assertThat(a).isEqualTo("foo"); 46 | assertThat(b).isEqualTo("bar"); 47 | assertThat(c).isEqualTo("xyz"); 48 | } 49 | 50 | @ParameterizedTest(name = "example: {0} maps to {1} and gives {2}") 51 | @FormattedSource(format = "example: {0} maps to {1} and gives {2}", lines = { 52 | "example: 'foo' maps to 'bar' and gives 'xyz'" 53 | }) 54 | void supportsFullTextFormatStartingWithText(String a, String b, String c) { 55 | assertThat(a).isEqualTo("foo"); 56 | assertThat(b).isEqualTo("bar"); 57 | assertThat(c).isEqualTo("xyz"); 58 | } 59 | 60 | @ParameterizedTest(name = "{0} maps to {1} and gives {2} (an example)") 61 | @FormattedSource(format = "{0} maps to {1} and gives {2} (an example)", lines = { 62 | "'foo' maps to 'bar' and gives 'xyz' (an example)" 63 | }) 64 | void supportsFullTextFormatEndingWithText(String a, String b, String c) { 65 | assertThat(a).isEqualTo("foo"); 66 | assertThat(b).isEqualTo("bar"); 67 | assertThat(c).isEqualTo("xyz"); 68 | } 69 | 70 | @ParameterizedTest(name = "appending {0} to {1} gives {2}") 71 | @FormattedSource(format = "appending {0} to {1} gives {2}", quoteCharacter = '"', textBlock = """ 72 | appending "foo" to "bar" gives "foobar" 73 | """) 74 | void supportsCustomQuoteCharacter(String a, String b, String c) { 75 | assertThat(a).isEqualTo("foo"); 76 | assertThat(b).isEqualTo("bar"); 77 | assertThat(c).isEqualTo("foobar"); 78 | } 79 | 80 | @ParameterizedTest(name = "is {0} empty?") 81 | @FormattedSource(format = "is {0} empty?", textBlock = """ 82 | is '' empty? 83 | """) 84 | void supportsEmptyQuotedArguments(String argument) { 85 | assertThat(argument).isEmpty(); 86 | } 87 | 88 | @ParameterizedTest(name = "start {0} -> {1} => {2} > {3} end") 89 | @FormattedSource(format = "start {0} -> {1} => {2} > {3} end", textBlock = """ 90 | start a -> 'b' => c > ' d ' end 91 | """) 92 | void trimsLeadingAndTrailingWhitespacesWhenEnabled(String a, String b, String c, String d) { 93 | assertThat(a).isEqualTo("a"); 94 | assertThat(b).isEqualTo("b"); 95 | assertThat(c).isEqualTo("c"); 96 | assertThat(d).isEqualTo(" d "); 97 | } 98 | 99 | @ParameterizedTest(name = "start {0} -> {1} => {2} > {3} end") 100 | @FormattedSource(format = "start {0} -> {1} => {2} > {3} end", 101 | ignoreLeadingAndTrailingWhitespace = false, textBlock = """ 102 | start a -> 'b' => c > ' d ' end 103 | """) 104 | void doesNotTrimLeadingAndTrailingWhitespacesWhenDisabled(String a, String b, String c, String d) { 105 | assertThat(a).isEqualTo("a "); 106 | assertThat(b).isEqualTo(" 'b' "); 107 | assertThat(c).isEqualTo(" c"); 108 | assertThat(d).isEqualTo(" ' d '"); 109 | } 110 | 111 | @ParameterizedTest(name = "this is null: {0} and this {1} is not!") 112 | @FormattedSource( 113 | format = "this is null: {0} and this {1} is not!", 114 | lines = {"this is null: and this '' is not!"}) 115 | void recognizesEmptyUnquotedValueAsNull(String a, String b) { 116 | assertThat(a).isNull(); 117 | assertThat(b).isEmpty(); 118 | } 119 | 120 | @ParameterizedTest(name = "a: {0}, b: {1}, c: {2}, d: {3}") 121 | @FormattedSource(format = "a: {0}, b: {1}, c: {2}, d: {3}", 122 | nullValues = {"N/A", "null"}, 123 | textBlock = """ 124 | a: N/A, b: 'N/A', c: null, d: 'null' 125 | """) 126 | void supportsCustomNullValues(String a, String b, String c, String d) { 127 | assertThat(a).isNull(); 128 | assertThat(b).isNull(); 129 | assertThat(c).isNull(); 130 | assertThat(d).isNull(); 131 | } 132 | 133 | @ParameterizedTest 134 | @FormattedSource(emptyValue = "EMPTY", format = "a: {0}", lines = {"a: ''"}) 135 | void supportsCustomEmptyValue(String a) { 136 | assertThat(a).isEqualTo("EMPTY"); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /junit5-formatted-source-tests/src/test/java/com/mikemybytes/junit5/formatted/test/FormattedSourcePositionalPlaceholdersTest.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted.test; 2 | 3 | import com.mikemybytes.junit5.formatted.FormattedSource; 4 | import org.junit.jupiter.api.TestInfo; 5 | import org.junit.jupiter.params.ParameterizedTest; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class FormattedSourcePositionalPlaceholdersTest { 10 | 11 | @ParameterizedTest(name = "{0} + {1} = {2}") 12 | @FormattedSource(format = "? + ? = ?", argumentPlaceholder = "?", lines = { 13 | "1 + 2 = 3", 14 | "3 + 4 = 7" 15 | }) 16 | void supportsSimpleFormat(int a, int b, int c) { 17 | assertThat(a + b).isEqualTo(c); 18 | } 19 | 20 | @ParameterizedTest(name = "{0} plus {1} is equal to {2}") 21 | @FormattedSource(format = "? + ? = ?", argumentPlaceholder = "?", lines = { 22 | "1 + 2 = 3", 23 | "3 + 4 = 7" 24 | }) 25 | void supportsSimpleFormatWithDisplayName(int a, int b, int c, TestInfo info) { 26 | assertThat(a + b).isEqualTo(c); 27 | assertThat(info.getDisplayName()).isEqualTo("%d plus %d is equal to %d".formatted(a, b, c)); 28 | } 29 | 30 | @ParameterizedTest 31 | @FormattedSource(format = " + = ", argumentPlaceholder = "", textBlock = """ 32 | 1 + 2 = 3 33 | 3 + 4 = 7 34 | """ 35 | ) 36 | void supportsSimpleTemplateViaTextBlock(int a, int b, int c) { 37 | assertThat(a + b).isEqualTo(c); 38 | } 39 | 40 | @ParameterizedTest(name = "{0} maps to {1} and gives {2}") 41 | @FormattedSource(format = "? maps to ? and gives ?", argumentPlaceholder = "?", lines = { 42 | "'foo' maps to 'bar' and gives 'xyz'" 43 | }) 44 | void supportsFullTextFormat(String a, String b, String c) { 45 | assertThat(a).isEqualTo("foo"); 46 | assertThat(b).isEqualTo("bar"); 47 | assertThat(c).isEqualTo("xyz"); 48 | } 49 | 50 | @ParameterizedTest(name = "example: {0} maps to {1} and gives {2}") 51 | @FormattedSource(format = "example: ? maps to ? and gives ?", argumentPlaceholder = "?", lines = { 52 | "example: 'foo' maps to 'bar' and gives 'xyz'" 53 | }) 54 | void supportsFullTextFormatStartingWithText(String a, String b, String c) { 55 | assertThat(a).isEqualTo("foo"); 56 | assertThat(b).isEqualTo("bar"); 57 | assertThat(c).isEqualTo("xyz"); 58 | } 59 | 60 | @ParameterizedTest(name = "{0} maps to {1} and gives {2} (an example)") 61 | @FormattedSource(format = "? maps to ? and gives ? (an example)", argumentPlaceholder = "?", lines = { 62 | "'foo' maps to 'bar' and gives 'xyz' (an example)" 63 | }) 64 | void supportsFullTextFormatEndingWithText(String a, String b, String c) { 65 | assertThat(a).isEqualTo("foo"); 66 | assertThat(b).isEqualTo("bar"); 67 | assertThat(c).isEqualTo("xyz"); 68 | } 69 | 70 | @ParameterizedTest(name = "appending {0} to {1} gives {2}") 71 | @FormattedSource(format = "appending ? to ? gives ?", argumentPlaceholder = "?", quoteCharacter = '"', textBlock = """ 72 | appending "foo" to "bar" gives "foobar" 73 | """) 74 | void supportsCustomQuoteCharacter(String a, String b, String c) { 75 | assertThat(a).isEqualTo("foo"); 76 | assertThat(b).isEqualTo("bar"); 77 | assertThat(c).isEqualTo("foobar"); 78 | } 79 | 80 | @ParameterizedTest(name = "is {0} empty?") 81 | @FormattedSource(format = "is ??? empty?", argumentPlaceholder = "???", textBlock = """ 82 | is '' empty? 83 | """) 84 | void supportsEmptyQuotedArguments(String argument) { 85 | assertThat(argument).isEmpty(); 86 | } 87 | 88 | @ParameterizedTest(name = "start {0} -> {1} => {2} > {3} end") 89 | @FormattedSource(format = "start ? -> ? => ? > ? end", argumentPlaceholder = "?", textBlock = """ 90 | start a -> 'b' => c > ' d ' end 91 | """) 92 | void trimsLeadingAndTrailingWhitespacesWhenEnabled(String a, String b, String c, String d) { 93 | assertThat(a).isEqualTo("a"); 94 | assertThat(b).isEqualTo("b"); 95 | assertThat(c).isEqualTo("c"); 96 | assertThat(d).isEqualTo(" d "); 97 | } 98 | 99 | @ParameterizedTest(name = "start {0} -> {1} => {2} > {3} end") 100 | @FormattedSource(format = "start ? -> ? => ? > ? end", argumentPlaceholder = "?", 101 | ignoreLeadingAndTrailingWhitespace = false, textBlock = """ 102 | start a -> 'b' => c > ' d ' end 103 | """) 104 | void doesNotTrimLeadingAndTrailingWhitespacesWhenDisabled(String a, String b, String c, String d) { 105 | assertThat(a).isEqualTo("a "); 106 | assertThat(b).isEqualTo(" 'b' "); 107 | assertThat(c).isEqualTo(" c"); 108 | assertThat(d).isEqualTo(" ' d '"); 109 | } 110 | 111 | @ParameterizedTest(name = "this is null: {0} and this {1} is not!") 112 | @FormattedSource( 113 | format = "this is null: {} and this {} is not!", argumentPlaceholder = "{}", 114 | lines = {"this is null: and this '' is not!"}) 115 | void recognizesEmptyUnquotedValueAsNull(String a, String b) { 116 | assertThat(a).isNull(); 117 | assertThat(b).isEmpty(); 118 | } 119 | 120 | @ParameterizedTest(name = "a: {0}, b: {1}, c: {2}, d: {3}") 121 | @FormattedSource(format = "a: {}, b: {}, c: {}, d: {}", argumentPlaceholder = "{}", 122 | nullValues = {"N/A", "null"}, 123 | textBlock = """ 124 | a: N/A, b: 'N/A', c: null, d: 'null' 125 | """) 126 | void supportsCustomNullValues(String a, String b, String c, String d) { 127 | assertThat(a).isNull(); 128 | assertThat(b).isNull(); 129 | assertThat(c).isNull(); 130 | assertThat(d).isNull(); 131 | } 132 | 133 | @ParameterizedTest 134 | @FormattedSource(emptyValue = "EMPTY", format = "a: ?", argumentPlaceholder = "?", lines = {"a: ''"}) 135 | void supportsCustomEmptyValue(String a) { 136 | assertThat(a).isEqualTo("EMPTY"); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /junit5-formatted-source-tests/src/test/java/com/mikemybytes/junit5/formatted/test/FormattedSourceTestIndexedPlaceholdersTest.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted.test; 2 | 3 | import com.mikemybytes.junit5.formatted.FormattedSourceTest; 4 | import org.junit.jupiter.api.TestInfo; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class FormattedSourceTestIndexedPlaceholdersTest { 9 | 10 | @FormattedSourceTest(format = "{0} + {1} = {2}", lines = { 11 | "1 + 2 = 3", 12 | "3 + 4 = 7" 13 | }) 14 | void supportsSimpleFormat(int a, int b, int c, TestInfo info) { 15 | assertThat(a + b).isEqualTo(c); 16 | 17 | assertThat(info.getDisplayName()).isEqualTo("%d + %d = %d".formatted(a, b, c)); 18 | } 19 | 20 | @FormattedSourceTest(format = "{0} + {1} = {2}", textBlock = """ 21 | 1 + 2 = 3 22 | 3 + 4 = 7 23 | """ 24 | ) 25 | void supportsSimpleTemplateViaTextBlock(int a, int b, int c, TestInfo info) { 26 | assertThat(a + b).isEqualTo(c); 27 | 28 | assertThat(info.getDisplayName()).isEqualTo("%d + %d = %d".formatted(a, b, c)); 29 | } 30 | 31 | @FormattedSourceTest(format = "{0} maps to {1} and gives {2}", lines = { 32 | "'foo' maps to 'bar' and gives 'xyz'" 33 | }) 34 | void supportsFullTextFormat(String a, String b, String c, TestInfo info) { 35 | assertThat(a).isEqualTo("foo"); 36 | assertThat(b).isEqualTo("bar"); 37 | assertThat(c).isEqualTo("xyz"); 38 | 39 | assertThat(info.getDisplayName()).isEqualTo("'foo' maps to 'bar' and gives 'xyz'"); 40 | } 41 | 42 | @FormattedSourceTest(format = "example: {0} maps to {1} and gives {2}", lines = { 43 | "example: 'foo' maps to 'bar' and gives 'xyz'" 44 | }) 45 | void supportsFullTextFormatStartingWithText(String a, String b, String c, TestInfo info) { 46 | assertThat(a).isEqualTo("foo"); 47 | assertThat(b).isEqualTo("bar"); 48 | assertThat(c).isEqualTo("xyz"); 49 | 50 | assertThat(info.getDisplayName()).isEqualTo("example: 'foo' maps to 'bar' and gives 'xyz'"); 51 | } 52 | 53 | @FormattedSourceTest(format = "{0} maps to {1} and gives {2} (an example)", lines = { 54 | "'foo' maps to 'bar' and gives 'xyz' (an example)" 55 | }) 56 | void supportsFullTextFormatEndingWithText(String a, String b, String c) { 57 | assertThat(a).isEqualTo("foo"); 58 | assertThat(b).isEqualTo("bar"); 59 | assertThat(c).isEqualTo("xyz"); 60 | } 61 | 62 | @FormattedSourceTest(format = "appending {0} to {1} gives {2}", quoteCharacter = '"', textBlock = """ 63 | appending "foo" to "bar" gives "foobar" 64 | """) 65 | void supportsCustomQuoteCharacter(String a, String b, String c) { 66 | assertThat(a).isEqualTo("foo"); 67 | assertThat(b).isEqualTo("bar"); 68 | assertThat(c).isEqualTo("foobar"); 69 | } 70 | 71 | @FormattedSourceTest(format = "is {0} empty?", textBlock = """ 72 | is '' empty? 73 | """) 74 | void supportsEmptyQuotedArguments(String argument) { 75 | assertThat(argument).isEmpty(); 76 | } 77 | 78 | @FormattedSourceTest(format = "start {0} -> {1} => {2} > {3} end", textBlock = """ 79 | start a -> 'b' => c > ' d ' end 80 | """) 81 | void trimsLeadingAndTrailingWhitespacesWhenEnabled(String a, String b, String c, String d) { 82 | assertThat(a).isEqualTo("a"); 83 | assertThat(b).isEqualTo("b"); 84 | assertThat(c).isEqualTo("c"); 85 | assertThat(d).isEqualTo(" d "); 86 | } 87 | 88 | @FormattedSourceTest(format = "start {0} -> {1} => {2} > {3} end", 89 | ignoreLeadingAndTrailingWhitespace = false, textBlock = """ 90 | start a -> 'b' => c > ' d ' end 91 | """) 92 | void doesNotTrimLeadingAndTrailingWhitespacesWhenDisabled(String a, String b, String c, String d) { 93 | assertThat(a).isEqualTo("a "); 94 | assertThat(b).isEqualTo(" 'b' "); 95 | assertThat(c).isEqualTo(" c"); 96 | assertThat(d).isEqualTo(" ' d '"); 97 | } 98 | 99 | @FormattedSourceTest( 100 | format = "this is null: {0} and this {1} is not!", 101 | lines = {"this is null: and this '' is not!"}) 102 | void recognizesEmptyUnquotedValueAsNull(String a, String b) { 103 | assertThat(a).isNull(); 104 | assertThat(b).isEmpty(); 105 | } 106 | 107 | @FormattedSourceTest(format = "a: {0}, b: {1}, c: {2}, d: {3}", 108 | nullValues = {"N/A", "null"}, 109 | textBlock = """ 110 | a: N/A, b: 'N/A', c: null, d: 'null' 111 | """) 112 | void supportsCustomNullValues(String a, String b, String c, String d) { 113 | assertThat(a).isNull(); 114 | assertThat(b).isNull(); 115 | assertThat(c).isNull(); 116 | assertThat(d).isNull(); 117 | } 118 | 119 | @FormattedSourceTest(emptyValue = "EMPTY", format = "a: {0}", lines = {"a: ''"}) 120 | void supportsCustomEmptyValue(String a) { 121 | assertThat(a).isEqualTo("EMPTY"); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /junit5-formatted-source-tests/src/test/java/com/mikemybytes/junit5/formatted/test/FormattedSourceTestPositionalPlaceholdersTest.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted.test; 2 | 3 | import com.mikemybytes.junit5.formatted.FormattedSourceTest; 4 | import org.junit.jupiter.api.TestInfo; 5 | 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class FormattedSourceTestPositionalPlaceholdersTest { 9 | 10 | @FormattedSourceTest(format = "? + ? = ?", argumentPlaceholder = "?", lines = { 11 | "1 + 2 = 3", 12 | "3 + 4 = 7" 13 | }) 14 | void supportsSimpleFormat(int a, int b, int c, TestInfo info) { 15 | assertThat(a + b).isEqualTo(c); 16 | 17 | assertThat(info.getDisplayName()).isEqualTo("%d + %d = %d".formatted(a, b, c)); 18 | } 19 | 20 | @FormattedSourceTest(format = " + = ", argumentPlaceholder = "", textBlock = """ 21 | 1 + 2 = 3 22 | 3 + 4 = 7 23 | """ 24 | ) 25 | void supportsSimpleTemplateViaTextBlock(int a, int b, int c, TestInfo info) { 26 | assertThat(a + b).isEqualTo(c); 27 | 28 | assertThat(info.getDisplayName()).isEqualTo("%d + %d = %d".formatted(a, b, c)); 29 | } 30 | 31 | @FormattedSourceTest(format = "? maps to ? and gives ?", argumentPlaceholder = "?", lines = { 32 | "'foo' maps to 'bar' and gives 'xyz'" 33 | }) 34 | void supportsFullTextFormat(String a, String b, String c, TestInfo info) { 35 | assertThat(a).isEqualTo("foo"); 36 | assertThat(b).isEqualTo("bar"); 37 | assertThat(c).isEqualTo("xyz"); 38 | 39 | assertThat(info.getDisplayName()).isEqualTo("'foo' maps to 'bar' and gives 'xyz'"); 40 | } 41 | 42 | @FormattedSourceTest(format = "example: ? maps to ? and gives ?", argumentPlaceholder = "?", lines = { 43 | "example: 'foo' maps to 'bar' and gives 'xyz'" 44 | }) 45 | void supportsFullTextFormatStartingWithText(String a, String b, String c, TestInfo info) { 46 | assertThat(a).isEqualTo("foo"); 47 | assertThat(b).isEqualTo("bar"); 48 | assertThat(c).isEqualTo("xyz"); 49 | 50 | assertThat(info.getDisplayName()).isEqualTo("example: 'foo' maps to 'bar' and gives 'xyz'"); 51 | } 52 | 53 | @FormattedSourceTest(format = "? maps to ? and gives ? (an example)", argumentPlaceholder = "?", lines = { 54 | "'foo' maps to 'bar' and gives 'xyz' (an example)" 55 | }) 56 | void supportsFullTextFormatEndingWithText(String a, String b, String c) { 57 | assertThat(a).isEqualTo("foo"); 58 | assertThat(b).isEqualTo("bar"); 59 | assertThat(c).isEqualTo("xyz"); 60 | } 61 | 62 | @FormattedSourceTest(format = "appending ? to ? gives ?", argumentPlaceholder = "?", quoteCharacter = '"', textBlock = """ 63 | appending "foo" to "bar" gives "foobar" 64 | """) 65 | void supportsCustomQuoteCharacter(String a, String b, String c) { 66 | assertThat(a).isEqualTo("foo"); 67 | assertThat(b).isEqualTo("bar"); 68 | assertThat(c).isEqualTo("foobar"); 69 | } 70 | 71 | @FormattedSourceTest(format = "is ??? empty?", argumentPlaceholder = "???", textBlock = """ 72 | is '' empty? 73 | """) 74 | void supportsEmptyQuotedArguments(String argument) { 75 | assertThat(argument).isEmpty(); 76 | } 77 | 78 | @FormattedSourceTest(format = "start ? -> ? => ? > ? end", argumentPlaceholder = "?", textBlock = """ 79 | start a -> 'b' => c > ' d ' end 80 | """) 81 | void trimsLeadingAndTrailingWhitespacesWhenEnabled(String a, String b, String c, String d) { 82 | assertThat(a).isEqualTo("a"); 83 | assertThat(b).isEqualTo("b"); 84 | assertThat(c).isEqualTo("c"); 85 | assertThat(d).isEqualTo(" d "); 86 | } 87 | 88 | @FormattedSourceTest(format = "start ? -> ? => ? > ? end", argumentPlaceholder = "?", 89 | ignoreLeadingAndTrailingWhitespace = false, textBlock = """ 90 | start a -> 'b' => c > ' d ' end 91 | """) 92 | void doesNotTrimLeadingAndTrailingWhitespacesWhenDisabled(String a, String b, String c, String d) { 93 | assertThat(a).isEqualTo("a "); 94 | assertThat(b).isEqualTo(" 'b' "); 95 | assertThat(c).isEqualTo(" c"); 96 | assertThat(d).isEqualTo(" ' d '"); 97 | } 98 | 99 | @FormattedSourceTest( 100 | format = "this is null: {} and this {} is not!", argumentPlaceholder = "{}", 101 | lines = {"this is null: and this '' is not!"}) 102 | void recognizesEmptyUnquotedValueAsNull(String a, String b) { 103 | assertThat(a).isNull(); 104 | assertThat(b).isEmpty(); 105 | } 106 | 107 | @FormattedSourceTest(format = "a: {}, b: {}, c: {}, d: {}", argumentPlaceholder = "{}", 108 | nullValues = {"N/A", "null"}, 109 | textBlock = """ 110 | a: N/A, b: 'N/A', c: null, d: 'null' 111 | """) 112 | void supportsCustomNullValues(String a, String b, String c, String d) { 113 | assertThat(a).isNull(); 114 | assertThat(b).isNull(); 115 | assertThat(c).isNull(); 116 | assertThat(d).isNull(); 117 | } 118 | 119 | @FormattedSourceTest(emptyValue = "EMPTY", format = "a: ?", argumentPlaceholder = "?", lines = {"a: ''"}) 120 | void supportsCustomEmptyValue(String a) { 121 | assertThat(a).isEqualTo("EMPTY"); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /junit5-formatted-source-tests/src/test/java/module-info.java: -------------------------------------------------------------------------------- 1 | open module junit5.formatted.source.tests { 2 | 3 | requires org.junit.platform.engine; 4 | requires org.junit.jupiter.engine; 5 | requires org.assertj.core; 6 | 7 | requires com.mikemybytes.junit5.formatted; 8 | 9 | } -------------------------------------------------------------------------------- /junit5-formatted-source/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | junit5-formatted-source-parent 5 | com.mikemybytes 6 | 1.0.2-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | junit5-formatted-source 11 | 12 | JUnit 5 FormattedSource 13 | Allows defining JUnit 5 ParameterizedTest cases using a human-readable format 14 | ${github.project-url} 15 | 16 | 17 | 11 18 | 11 19 | 11 20 | 21 | 22 | 23 | 24 | org.junit.jupiter 25 | junit-jupiter 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-surefire-plugin 34 | 35 | 36 | 37 | --add-exports org.junit.platform.commons/org.junit.platform.commons.util=ALL-UNNAMED 38 | --add-exports org.junit.platform.commons/org.junit.platform.commons.logging=ALL-UNNAMED 39 | 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-source-plugin 45 | 46 | 47 | attach-sources 48 | 49 | jar-no-fork 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-javadoc-plugin 57 | 58 | 59 | attach-javadocs 60 | 61 | jar 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-javadoc-plugin 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/ArgumentsExtractor.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | import org.junit.jupiter.params.provider.Arguments; 4 | 5 | import java.util.Comparator; 6 | import java.util.List; 7 | import java.util.stream.Collectors; 8 | 9 | import static com.mikemybytes.junit5.formatted.Preconditions.require; 10 | 11 | /** 12 | * Extracts test arguments from the provided input. 13 | */ 14 | class ArgumentsExtractor { 15 | 16 | private final FormattedSourceData sourceData; 17 | private final FormatSpecification formatSpecification; 18 | private final RawArgumentsProcessor rawArgumentsProcessor; 19 | 20 | ArgumentsExtractor( 21 | FormattedSourceData sourceData, 22 | FormatSpecification formatSpecification, 23 | RawArgumentsProcessor rawArgumentsProcessor) { 24 | this.sourceData = sourceData; 25 | this.formatSpecification = formatSpecification; 26 | this.rawArgumentsProcessor = rawArgumentsProcessor; 27 | } 28 | 29 | Arguments extract(String line) { 30 | var formatMatcher = formatSpecification.getPattern().matcher(line); 31 | 32 | require(formatMatcher.matches(), "Input does not match the expected format"); 33 | 34 | List args = formatSpecification.getArgumentsOrder().stream() 35 | .map(argIndex -> new ArgumentIndexMatcher(argIndex, new FormatArgumentMatcherGroup(argIndex))) 36 | .sorted(Comparator.comparing(ArgumentIndexMatcher::getIndex)) 37 | .map(aim -> formatMatcher.group(aim.getMatcherGroup().getName())) 38 | .map(this::processArgumentValue) 39 | .collect(Collectors.toList()); 40 | 41 | List processedArgs = rawArgumentsProcessor.apply(line, args); 42 | 43 | return Arguments.of(processedArgs.toArray()); 44 | } 45 | 46 | private String processArgumentValue(String rawValue) { 47 | require(rawValue != null, "Argument's raw value can't be null"); 48 | 49 | String value = rawValue; 50 | if (sourceData.isIgnoreWhitespaces()) { 51 | value = value.strip(); 52 | } 53 | 54 | if (value.isEmpty()) { 55 | // interpreting unquoted empty value as null just like @CsvSource does 56 | return null; 57 | } 58 | 59 | String quote = "" + sourceData.getQuoteCharacter(); 60 | if (value.startsWith(quote) && value.endsWith(quote)) { 61 | value = value.substring(1, value.length() - 1); 62 | } 63 | 64 | if (sourceData.getNullValues().contains(value)) { 65 | return null; 66 | } 67 | 68 | if(value.isEmpty()) { 69 | return sourceData.getEmptyValue(); 70 | } 71 | 72 | return value; 73 | } 74 | 75 | /** 76 | * Represents a tuple containing argument's index (position, starting from zero) and it's respective 77 | * {@link FormatArgumentMatcherGroup}, to allow capturing its value. 78 | */ 79 | private static class ArgumentIndexMatcher { 80 | private final int index; 81 | private final FormatArgumentMatcherGroup matcherGroup; 82 | 83 | public ArgumentIndexMatcher(int index, FormatArgumentMatcherGroup matcherGroup) { 84 | this.index = index; 85 | this.matcherGroup = matcherGroup; 86 | } 87 | 88 | public int getIndex() { 89 | return index; 90 | } 91 | 92 | public FormatArgumentMatcherGroup getMatcherGroup() { 93 | return matcherGroup; 94 | } 95 | 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormatAnalyzer.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | /** 4 | * Analyzes given format string (defined within {@link FormattedSource#format} or {@link FormattedSourceTest#format}), 5 | * in order to build a {@link FormatSpecification} that could be then used to match specific test argument values. 6 | */ 7 | interface FormatAnalyzer { 8 | 9 | FormatSpecification analyze(String formatString, int methodParameterCount); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormatAnalyzers.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | class FormatAnalyzers { 4 | 5 | private FormatAnalyzers() { 6 | // static only 7 | } 8 | 9 | static FormatAnalyzer from(FormattedSourceData formattedSourceData) { 10 | if (formattedSourceData.getArgumentPlaceholder().isEmpty()) { 11 | return new IndexedArgumentPlaceholdersFormatAnalyzer(); // default 12 | } else { 13 | String argumentPlaceholder = formattedSourceData.getArgumentPlaceholder().get(); 14 | return new PositionalArgumentPlaceholdersFormatAnalyzer(argumentPlaceholder); 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormatArgumentMatcherGroup.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | /** 4 | * Object representing {@link java.util.regex.Pattern} "named capturing group" of the specific argument. Allows building 5 | * regular expressions matching specific argument's value. 6 | */ 7 | class FormatArgumentMatcherGroup { 8 | 9 | /** 10 | * Prefix of the {@link java.util.regex.Pattern} "named capturing group" that will be used for all arguments (in 11 | * order to contain something more than just a number). 12 | */ 13 | private static final String GROUP_NAME_PREFIX = "a"; 14 | 15 | /** 16 | * Index of the related argument (starting from zero). 17 | */ 18 | private final int index; 19 | 20 | FormatArgumentMatcherGroup(int index) { 21 | this.index = index; 22 | } 23 | 24 | /** 25 | * Returns name of the "named capturing group" for the associated argument, allowing its value to be matched. 26 | */ 27 | String getName() { 28 | return GROUP_NAME_PREFIX + index; 29 | } 30 | 31 | /** 32 | * Returns regular expression representing argument's "named capturing group". 33 | */ 34 | String getRegex() { 35 | return "(?.*)"; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormatSpecification.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | import java.util.List; 4 | import java.util.regex.Pattern; 5 | 6 | class FormatSpecification { 7 | 8 | private final Pattern pattern; 9 | private final List argumentsOrder; 10 | 11 | FormatSpecification(Pattern pattern, List argumentsOrder) { 12 | this.pattern = pattern; 13 | this.argumentsOrder = argumentsOrder; 14 | } 15 | 16 | Pattern getPattern() { 17 | return pattern; 18 | } 19 | 20 | List getArgumentsOrder() { 21 | return argumentsOrder; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormattedSource.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | import org.junit.jupiter.params.provider.ArgumentsSource; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * {@code @FormattedSource} is an {@link ArgumentsSource} which reads test case arguments represented as {@link #lines} 9 | * or {@link #textBlock} in the user-defined {@link #format}. 10 | * 11 | *

The supplied values will be provided as arguments to the test method annotated with 12 | * {@link org.junit.jupiter.params.ParameterizedTest}.

13 | */ 14 | @Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Documented 17 | @ArgumentsSource(FormattedSourceArgumentsProvider.class) 18 | public @interface FormattedSource { 19 | 20 | /** 21 | * The definition of the arguments format. By default, specific test method arguments must be referenced 22 | * by their position (starting from zero). E.g. {@code {2}} represents the 3rd argument of the test method. 23 | *

Setting {@link #argumentPlaceholder()} disables the default behavior, allowing to use a fixed placeholder 24 | * string instead. As there's no braces (curly brackets) escaping, switching to the fixed argument placeholder 25 | * allows using them in the format string.

26 | * 27 | * @return The definition of the arguments format. 28 | */ 29 | String format(); 30 | 31 | /** 32 | * Test case input represented as lines in the defined {@link #format}. Each line represents a separate test case 33 | * of the {@link org.junit.jupiter.params.ParameterizedTest}. Lines must not contain newline characters like 34 | * {@code \n}. 35 | * 36 | *

Defaults to an empty string. Note: the test case input must be supplied either via {@link #lines()} or 37 | * {@link #textBlock()}.

38 | * 39 | * @return Test case input represented as lines in the defined format. 40 | */ 41 | String[] lines() default {}; 42 | 43 | /** 44 | * Test case input represented as a single Java Text Block (available since Java 15). Each line represents a 45 | * separate test case of the {@link org.junit.jupiter.params.ParameterizedTest}. Lines must not contain newline 46 | * characters like {@code \n}. 47 | * 48 | *

When running on Java version less than 15, using {@link #lines} is recommended instead.

49 | * 50 | *

Defaults to an empty string. Note: the test case input must be supplied either via {@link #lines()} or 51 | * {@link #textBlock()}.

52 | * 53 | * @return Test case input represented as a single Java Text Block. 54 | */ 55 | String textBlock() default ""; 56 | 57 | /** 58 | * The quote character that could be used to separate argument's value from the rest of the input. 59 | * As there's no escaping support, a different quote character should be chosen in case of a conflict. 60 | * 61 | *

Defaults to a single quote ({@code '}).

62 | * 63 | * @return Arguments quote character. 64 | */ 65 | char quoteCharacter() default '\''; 66 | 67 | /** 68 | * Specifies fixed argument placeholder string that should be used instead of the default indexed syntax. 69 | * Each placeholder's occurrence corresponds to the next argument of the annotated test method 70 | * (positional arguments). 71 | * 72 | * @since 1.0.0 73 | * @return Custom argument placeholder string. 74 | */ 75 | String argumentPlaceholder() default ""; 76 | 77 | /** 78 | * Allows to ignore (or not) leading and trailing whitespace characters identified in the argument values. 79 | * 80 | *

Defaults to {@code true}.

81 | * @return {@code true} if leading and trailing whitespaces should be ignored, {@code false} otherwise. 82 | */ 83 | boolean ignoreLeadingAndTrailingWhitespace() default true; 84 | 85 | /** 86 | * A list of strings that should be interpreted as {@code null} references. 87 | * 88 | *

Provided values (e.g. {@code "null"}, {@code "N/A"}, {@code "NONE"}) will be converted to {@code null} 89 | * references, no matter if quoted ({@link #quoteCharacter()}) or not.

90 | * 91 | *

Regardless of the value of this attribute, unquoted empty values will always be interpreted as 92 | * {@code null}.

93 | * 94 | *

Defaults to {@code {}}.

95 | * @return A list of strings that should be interpreted as {@code null} references. 96 | */ 97 | String[] nullValues() default {}; 98 | 99 | /** 100 | * A value used to substitute quoted empty strings read from the input. 101 | * 102 | *

Defaults to empty string ({@code ""}).

103 | * @return A value used to substitute quoted empty strings read from the input. 104 | */ 105 | String emptyValue() default ""; 106 | 107 | } 108 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormattedSourceArgumentsProvider.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 FormattedSource} annotation. 15 | */ 16 | class FormattedSourceArgumentsProvider implements ArgumentsProvider, AnnotationConsumer { 17 | 18 | private FormattedSourceData sourceData; 19 | 20 | @Override 21 | public void accept(FormattedSource annotation) { 22 | sourceData = FormattedSourceData.from(annotation); 23 | } 24 | 25 | @Override 26 | public Stream 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 argumentsExtractor = new ArgumentsExtractor(sourceData, specification, RawArgumentsProcessor.passThrough()); 33 | return sourceData.getLines().stream() 34 | .map(argumentsExtractor::extract); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/com/mikemybytes/junit5/formatted/FormattedSourceData.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | import java.util.*; 4 | import java.util.stream.Collectors; 5 | 6 | /** 7 | * Shared container for parameter values of both {@link FormattedSource} and {@link FormattedSourceTest} annotations. 8 | * Represents specific test configuration without any processing (raw values). 9 | */ 10 | class FormattedSourceData { 11 | 12 | /** 13 | * Format string of the related test. 14 | */ 15 | private final String formatString; 16 | /** 17 | * Test input lines. 18 | */ 19 | private final List lines; 20 | /** 21 | * Character used for quoting test arguments. 22 | */ 23 | private final char quoteCharacter; 24 | 25 | /** 26 | * Fixed placeholder string to be used instead of the default indexed syntax. 27 | */ 28 | private final String argumentPlaceholder; 29 | 30 | /** 31 | * Defines whether to ignore leading and trailing whitespaces in argument values. 32 | */ 33 | private final boolean ignoreWhitespaces; 34 | /** 35 | * A set of strings that should be interpreted as {@code null} references. 36 | */ 37 | private final Set nullValues; 38 | 39 | /** 40 | * A value used to substitute quoted empty strings. 41 | */ 42 | private final String emptyValue; 43 | 44 | static FormattedSourceData from(FormattedSource annotation) { 45 | List lines = extractLines(annotation.lines(), annotation.textBlock()); 46 | return new FormattedSourceData( 47 | annotation.format(), 48 | lines, 49 | annotation.quoteCharacter(), 50 | annotation.argumentPlaceholder(), 51 | annotation.ignoreLeadingAndTrailingWhitespace(), 52 | toSet(annotation.nullValues()), 53 | annotation.emptyValue() 54 | ); 55 | } 56 | 57 | static FormattedSourceData from(FormattedSourceTest annotation) { 58 | List lines = extractLines(annotation.lines(), annotation.textBlock()); 59 | return new FormattedSourceData( 60 | annotation.format(), 61 | lines, 62 | annotation.quoteCharacter(), 63 | annotation.argumentPlaceholder(), 64 | annotation.ignoreLeadingAndTrailingWhitespace(), 65 | toSet(annotation.nullValues()), 66 | annotation.emptyValue() 67 | ); 68 | } 69 | 70 | private static List extractLines(String[] lines, String textBlock) { 71 | if (!textBlock.isEmpty()) { 72 | return textBlock.lines().collect(Collectors.toList()); 73 | } else { 74 | return toList(lines); 75 | } 76 | } 77 | 78 | private static List toList(String[] array) { 79 | return array != null ? Arrays.asList(array) : Collections.emptyList(); 80 | } 81 | 82 | private static Set toSet(String[] array) { 83 | return Set.copyOf(toList(array)); 84 | } 85 | 86 | private FormattedSourceData( 87 | String formatString, 88 | List lines, 89 | char quoteCharacter, 90 | String argumentPlaceholder, 91 | boolean ignoreWhitespaces, 92 | Set nullValues, 93 | String emptyValue) { 94 | this.formatString = formatString; 95 | this.lines = lines; 96 | this.quoteCharacter = quoteCharacter; 97 | this.argumentPlaceholder = argumentPlaceholder; 98 | this.ignoreWhitespaces = ignoreWhitespaces; 99 | this.nullValues = nullValues; 100 | this.emptyValue = emptyValue; 101 | } 102 | 103 | String getFormatString() { 104 | return formatString; 105 | } 106 | 107 | List getLines() { 108 | return lines; 109 | } 110 | 111 | char getQuoteCharacter() { 112 | return quoteCharacter; 113 | } 114 | 115 | Optional getArgumentPlaceholder() { 116 | if (argumentPlaceholder == null || argumentPlaceholder.isBlank()) { 117 | return Optional.empty(); 118 | } 119 | return Optional.of(argumentPlaceholder); 120 | } 121 | 122 | public boolean isIgnoreWhitespaces() { 123 | return ignoreWhitespaces; 124 | } 125 | 126 | public Set 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 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> { 13 | 14 | /** 15 | * Simple, pass through processor, that does nothing with the raw test case arguments. 16 | */ 17 | static RawArgumentsProcessor passThrough() { 18 | return (rawInput, rawArguments) -> new ArrayList<>(rawArguments); 19 | } 20 | 21 | /** 22 | * Arguments processor wrapping the first argument of the test method with {@link Named} in order to customize 23 | * test case name. See the documentation of {@link FormattedSourceTest} for more information. 24 | */ 25 | static RawArgumentsProcessor testCaseName() { 26 | return (rawInput, rawArguments) -> { 27 | Preconditions.require(!rawArguments.isEmpty()); 28 | 29 | List namedArgs = new ArrayList<>(rawArguments); 30 | namedArgs.set(0, Named.of(rawInput, namedArgs.get(0))); 31 | return namedArgs; 32 | }; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /junit5-formatted-source/src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * JUnit 5 Formatted Source introduces new way of writing parameterized tests. It allows defining test case arguments in 3 | * a human-readable way, following a user-defined format. 4 | */ 5 | module com.mikemybytes.junit5.formatted { 6 | 7 | requires transitive org.junit.jupiter.api; 8 | requires transitive org.junit.jupiter.params; 9 | 10 | exports com.mikemybytes.junit5.formatted; 11 | 12 | // reflective access required while executing tests 13 | opens com.mikemybytes.junit5.formatted to org.junit.platform.commons; 14 | 15 | } -------------------------------------------------------------------------------- /junit5-formatted-source/src/test/java/com/mikemybytes/junit5/formatted/IndexedArgumentPlaceholdersFormatAnalyzerTest.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.converter.ConvertWith; 6 | import org.junit.jupiter.params.converter.SimpleArgumentConverter; 7 | import org.junit.jupiter.params.provider.CsvSource; 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | import static org.junit.jupiter.api.Assertions.*; 14 | 15 | class IndexedArgumentPlaceholdersFormatAnalyzerTest { 16 | 17 | private final IndexedArgumentPlaceholdersFormatAnalyzer analyzer = new IndexedArgumentPlaceholdersFormatAnalyzer(); 18 | 19 | @ParameterizedTest 20 | @CsvSource(delimiter = '|', value = { 21 | // format | params | matchingInput 22 | "{0} | 1 | 1", 23 | "prefix {0} {1} suffix | 2 | prefix ABC DEF suffix", 24 | "{0} {1} {2} {3} {4} | 5 | Lorem ipsum dolor sit amet", 25 | "'' | 0 | '' " 26 | }) 27 | void formatMatchesInput(String format, int parameterCount, String matchingInput) { 28 | FormatSpecification specification = analyzer.analyze(format, parameterCount); 29 | assertTrue(specification.getPattern().matcher(matchingInput).matches()); 30 | } 31 | 32 | @ParameterizedTest 33 | @CsvSource(delimiter = '|', value = { 34 | // format | params | argumentsOrder 35 | "{0} | 1 | [0]", 36 | "{0} {1} {2} | 3 | [0,1,2]", 37 | "{2} {0} {1} | 3 | [2,0,1]" 38 | }) 39 | void recognizesArgumentsOrder( 40 | String format, 41 | int parameterCount, 42 | @ConvertWith(ListIntArgumentConverter.class) List argumentsOrder) { 43 | FormatSpecification specification = analyzer.analyze(format, parameterCount); 44 | assertEquals(argumentsOrder, specification.getArgumentsOrder()); 45 | } 46 | 47 | @Test 48 | void failsWhenNotEnoughMethodParameters() { 49 | // given 50 | String format = "{0} {1} {2} {3} {4}"; 51 | int parameterCount = 3; 52 | // when & then 53 | assertThrows(IllegalArgumentException.class, () -> analyzer.analyze(format, parameterCount)); 54 | } 55 | 56 | /** 57 | * This ensures that additional special parameters like {@link org.junit.jupiter.api.TestInfo} can still be provided. 58 | */ 59 | @Test 60 | void supportsMoreMethodParametersThanArgumentPlaceholders() { 61 | // given 62 | String format = "{0} {1} {2}"; 63 | int parameterCount = 5; 64 | String input = "a b c"; 65 | // when 66 | FormatSpecification specification = analyzer.analyze(format, parameterCount); 67 | // then 68 | assertTrue(specification.getPattern().matcher(input).matches()); 69 | } 70 | 71 | @Test 72 | void failsOnInvalidArgumentPlaceholder() { 73 | // given 74 | String format = "{4}"; 75 | int parameterCount = 1; 76 | // when & then 77 | assertThrows(IllegalArgumentException.class, () -> analyzer.analyze(format, parameterCount)); 78 | } 79 | 80 | static class ListIntArgumentConverter extends SimpleArgumentConverter { 81 | 82 | @Override 83 | protected Object convert(Object source, Class targetType) { 84 | assertEquals(List.class, targetType, "Can only convert to List"); 85 | var noBrackets = String.valueOf(source).replaceAll("[\\[\\]]", ""); 86 | return Arrays.stream(noBrackets.split(",")).map(Integer::valueOf).collect(Collectors.toList()); 87 | } 88 | 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /junit5-formatted-source/src/test/java/com/mikemybytes/junit5/formatted/PositionalArgumentPlaceholdersFormatAnalyzerTest.java: -------------------------------------------------------------------------------- 1 | package com.mikemybytes.junit5.formatted; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.CsvSource; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | class PositionalArgumentPlaceholdersFormatAnalyzerTest { 10 | 11 | @ParameterizedTest 12 | @CsvSource(delimiter = '|', value = { 13 | "? | ? | 1 | 5", 14 | "? | prefix ? ? suffix | 2 | prefix ABC DEF suffix", 15 | " | | 5 | Lorem ipsum dolor sit amet", 16 | "? | '' | 0 | '' " 17 | }) 18 | void formatMatchesInput(String argumentPlaceholder, String format, int parameterCount, String matchingInput) { 19 | FormatSpecification specification = analyzer(argumentPlaceholder).analyze(format, parameterCount); 20 | assertTrue(specification.getPattern().matcher(matchingInput).matches()); 21 | } 22 | 23 | @Test 24 | void failsWhenNotEnoughMethodParameters() { 25 | // given 26 | String format = "??? ??? ??? ??? ???"; 27 | int parameterCount = 3; 28 | // when & then 29 | assertThrows( 30 | IllegalArgumentException.class, 31 | () -> analyzer("???").analyze(format, parameterCount) 32 | ); 33 | } 34 | 35 | /** 36 | * This ensures that additional special parameters like {@link org.junit.jupiter.api.TestInfo} can still be provided. 37 | */ 38 | @Test 39 | void supportsMoreMethodParametersThanArgumentPlaceholders() { 40 | // given 41 | String format = "??? ??? ???"; 42 | int parameterCount = 5; 43 | String input = "a b c"; 44 | // when 45 | FormatSpecification specification = analyzer("???").analyze(format, parameterCount); 46 | // then 47 | assertTrue(specification.getPattern().matcher(input).matches()); 48 | } 49 | 50 | private PositionalArgumentPlaceholdersFormatAnalyzer analyzer(String argumentPlaceholder) { 51 | return new PositionalArgumentPlaceholdersFormatAnalyzer(argumentPlaceholder); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.1.1 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM Provide a "standardized" way to retrieve the CLI args that will 157 | @REM work with both Windows and non-Windows executions. 158 | set MAVEN_CMD_LINE_ARGS=%* 159 | 160 | %MAVEN_JAVA_EXE% ^ 161 | %JVM_CONFIG_MAVEN_PROPS% ^ 162 | %MAVEN_OPTS% ^ 163 | %MAVEN_DEBUG_OPTS% ^ 164 | -classpath %WRAPPER_JAR% ^ 165 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 166 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 167 | if ERRORLEVEL 1 goto error 168 | goto end 169 | 170 | :error 171 | set ERROR_CODE=1 172 | 173 | :end 174 | @endlocal & set ERROR_CODE=%ERROR_CODE% 175 | 176 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 177 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 178 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 179 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 180 | :skipRcPost 181 | 182 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 183 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 184 | 185 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 186 | 187 | cmd /C exit /B %ERROR_CODE% 188 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.mikemybytes 6 | junit5-formatted-source-parent 7 | pom 8 | 1.0.2-SNAPSHOT 9 | 10 | JUnit 5 FormattedSource (parent) 11 | Parent project for all JUnit 5 FormattedSource library modules 12 | ${github.project-url} 13 | 14 | 15 | 16 | MIT License 17 | https://github.com/mikemybytes/junit5-formatted-source/blob/main/LICENSE.md 18 | 19 | 20 | 21 | 22 | 23 | Mike Kowalski 24 | mike [at] mikemybytes.com 25 | mikemybytes 26 | https://mikemybytes.com/ 27 | 28 | 29 | 30 | 31 | scm:git:git@github.com:mikemybytes/junit5-formatted-source.git 32 | scm:git:ssh://github.com:mikemybytes/junit5-formatted-source.git 33 | HEAD 34 | https://github.com/mikemybytes/junit5-formatted-source 35 | 36 | 37 | 38 | junit5-formatted-source 39 | junit5-formatted-source-tests 40 | 41 | 42 | 43 | UTF-8 44 | 45 | 46 | 5.8.0 47 | 48 | 3.27.2 49 | 50 | 51 | 3.14.0 52 | 3.1.4 53 | 3.4.2 54 | 3.3.1 55 | 3.21.0 56 | 3.11.2 57 | 3.9.0 58 | 3.5.2 59 | 2.18.0 60 | 3.1.1 61 | 3.1.4 62 | 1.10.0 63 | 3.5.1 64 | 3.5.0 65 | 66 | https://s01.oss.sonatype.org/service/local 67 | 68 | https://github.com/mikemybytes/junit5-formatted-source 69 | junit5-formatted-source 70 | mikemybytes 71 | 72 | 73 | (17,) 74 | (3.9.0,) 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-compiler-plugin 83 | ${maven-compiler-plugin.version} 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-install-plugin 88 | ${maven-install-plugin.version} 89 | 90 | 91 | org.apache.maven.plugins 92 | maven-jar-plugin 93 | ${maven-jar-plugin.version} 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-source-plugin 98 | ${maven-source-plugin.version} 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-javadoc-plugin 103 | ${maven-javadoc-plugin.version} 104 | 105 | 106 | org.apache.maven.plugins 107 | maven-site-plugin 108 | ${maven-site-plugin.version} 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-project-info-reports-plugin 113 | ${maven-project-info-reports-plugin} 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-surefire-plugin 118 | ${maven-surefire-plugin.version} 119 | 120 | 121 | org.codehaus.mojo 122 | versions-maven-plugin 123 | ${maven-versions-plugin.version} 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-release-plugin 128 | ${maven-release-plugin.version} 129 | 130 | true 131 | release 132 | @{project.version} 133 | deploy 134 | false 135 | true 136 | chore: [maven-release-plugin] 137 | 138 | 139 | 140 | org.apache.maven.plugins 141 | maven-help-plugin 142 | ${maven-help-plugin.version} 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-enforcer-plugin 147 | ${maven-enforcer-plugin.version} 148 | 149 | 150 | 151 | 152 | 153 | 154 | org.apache.maven.plugins 155 | maven-deploy-plugin 156 | ${maven-deploy-plugin.version} 157 | 158 | 159 | org.jreleaser 160 | jreleaser-maven-plugin 161 | ${jreleaser-maven-plugin.version} 162 | false 163 | 164 | 165 | 166 | junit5-formatted-source 167 | MIT 168 | mikemybytes 169 | 170 | 171 | ALWAYS 172 | FILE 173 | true 174 | 175 | 176 | 177 | 178 | 179 | ALWAYS 180 | ${maven-central.url} 181 | true 182 | true 183 | target/staging-deploy 184 | 185 | 186 | 187 | 188 | 189 | 190 | ${github.owner} 191 | ${github.project-name} 192 | ${github.owner} 193 | main 194 | true 195 | {{projectVersion}} 196 | {{projectVersion}} 197 | 198 | conventional-commits 199 | ALWAYS 200 | etc/jreleaser/changelog.md.tpl 201 | true 202 | 203 | - {{contributorName}}{{#contributorUsernameAsLink}} ({{.}}){{/contributorUsernameAsLink}} 204 | 205 | 206 | 207 | [maven-release-plugin] 208 | 209 | 210 | 211 | [release] 212 | 213 | 214 | 215 | maven-release-plugin,release 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | org.apache.maven.plugins 224 | maven-enforcer-plugin 225 | 226 | 227 | enforce-maven 228 | 229 | enforce 230 | 231 | 232 | 233 | 234 | ${enforcer-java-version} 235 | 236 | 237 | ${enforcer-maven-version} 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | org.junit.jupiter 252 | junit-jupiter 253 | ${junit.jupiter.version} 254 | 255 | 256 | 257 | 258 | org.assertj 259 | assertj-core 260 | ${assertj.version} 261 | 262 | 263 | 264 | 265 | 266 | 267 | ossrh 268 | https://s01.oss.sonatype.org/content/repositories/snapshots 269 | 270 | 271 | 272 | --------------------------------------------------------------------------------