├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── VERSION.txt
├── Version1.xInstructions.md
├── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
└── java
│ └── com
│ └── ginsberg
│ └── junit
│ └── exit
│ ├── ExitPreventerStrategy.java
│ ├── ExpectSystemExit.java
│ ├── ExpectSystemExitWithStatus.java
│ ├── FailOnSystemExit.java
│ ├── SystemExitExtension.java
│ ├── SystemExitPreventedException.java
│ ├── agent
│ ├── AgentSystemExitHandlerStrategy.java
│ ├── DoNotRewriteExitCalls.java
│ └── Junit5SystemExitAgent.java
│ └── assertions
│ └── SystemExitAssertion.java
└── test
└── java
└── com
└── ginsberg
└── junit
└── exit
├── ExpectSystemExitTest.java
├── ExpectSystemExitWithStatusTest.java
├── FailOnSystemExitTest.java
├── TestUtils.java
├── WithParameterizedTest.java
└── assertions
└── SystemExitAssertionTest.java
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build and Test
2 |
3 | on:
4 | [ pull_request, push ]
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | contents: read
11 | packages: write
12 | pull-requests: write
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: Set up JDK 17
18 | uses: actions/setup-java@v4
19 | with:
20 | java-version: '17'
21 | distribution: 'temurin'
22 | cache: 'gradle'
23 |
24 | - name: Build
25 | run: ./gradlew build --no-daemon
26 |
27 | - name: Publish
28 | if: github.ref != 'refs/heads/master' && !(github.event_name == 'pull_request' && github.base_ref == 'master')
29 | run: ./gradlew publish --no-daemon
30 | env:
31 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
32 | SONATYPE_TOKEN: ${{ secrets.SONATYPE_TOKEN }}
33 | SONATYPE_SIGNING_KEY: ${{ secrets.SONATYPE_SIGNING_KEY }}
34 | SONATYPE_SIGNING_PASSPHRASE: ${{ secrets.SONATYPE_SIGNING_PASSPHRASE }}
35 |
36 | - name: Add coverage to PR
37 | if: github.event_name == 'pull_request'
38 | id: jacoco
39 | uses: madrapps/jacoco-report@v1.6.1
40 | with:
41 | paths: ${{ github.workspace }}/**/build/reports/jacoco/test/jacocoTestReport.xml
42 | token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /build/
3 | *.class
4 | *.jar
5 | .idea
6 | /out/
7 | gradle.properties
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log for `junit5-system-exit`
2 |
3 | ### 2.0.2
4 | - Bugfix: [[#24]](https://github.com/tginsberg/junit5-system-exit/issues/24): Reset state between assertion calls.
5 |
6 | ### 2.0.1
7 | - Bugfix: [[#20]](https://github.com/tginsberg/junit5-system-exit/issues/20): Multiple calls to `System.exit()` do not always report the first exit status code.
8 |
9 | ### 2.0.0
10 | - Remove terminally deprecated `SecurityManager` approach for preventing `System.exit()` calls.
11 | - Add Java Agent-based approach. Calls to `System.exit()` are rewritten as classes are loaded.
12 | - Add AssertJ-style fluid assertions for cases when test authors do not want to use annotations, or want to write assertions after a `System.exit()` is detected.
13 |
14 | ### 1.1.2
15 | - Bugfix: [[#12]](https://github.com/tginsberg/junit5-system-exit/issues/12) ParameterizedTest does not work accurately.
16 |
17 | ### 1.1.1
18 | - Make `SystemExitPreventedException` public and add a `statusCode` getter. This should help with testing CLIs.
19 | - Add new `@FailOnSystemExit` annotation. When a test calls `System.exit()` the JVM running the test will terminate (in most setups). Annotating a test with `@FailOnSystemExit` will catch this condition and fail the test, rather than exiting the JVM the test is executing on.
20 |
21 | ### 1.1.0
22 | - Do Not Use. Prefer 1.1.1 or 1.1.2 please.
23 |
24 | ### 1.0.0
25 | - Initial Release.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Todd Ginsberg
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 | # JUnit5 System.exit() Extension
2 |
3 | This [JUnit 5 Extension](https://junit.org/junit5/docs/current/user-guide/#extensions) helps you write tests for code that calls `System.exit()`. It requires Java 17 or greater,
4 | and has one dependency ([ASM](https://asm.ow2.io/)).
5 |
6 | ## Differences Between Version 1.x and 2.x
7 |
8 | **Version 1.x** used an approach that [replaced the system `SecurityManager`](https://todd.ginsberg.com/post/testing-system-exit/).
9 | That worked fine until Java 17 where the `SecurityManager` was deprecated for removal. As of Java 18, a property to
10 | explicitly enable programmatic access to the `SecurityManager` is required. This method works for now but will eventually
11 | stop working. If you are still on 1.x and cannot move to 2.x, [you can still find the instructions here](Version1.xInstructions.md).
12 |
13 | **Version 2.x** uses a Java Agent to rewrite bytecode as the JVM loads classes. Whenever a call to `System.exit()` is detected,
14 | the Junit 5 System Exit Agent replaces that call with a function that records an attempt to exit, preventing the JVM from exiting.
15 | As a consequence of rewriting bytecode, this library now has one dependency - [ASM](https://asm.ow2.io/).
16 | When the [Java Class-File API](https://openjdk.org/jeps/457) is released, I will explore using that instead (or in addition to).
17 |
18 | Version 2 also supports AssertJ-style fluid assertions in addition to the annotation-driven approach that came with Version 1.
19 | Other than enabling the Java Agent (see below), your code should not change when upgrading from Version 1.x to Version 2.x.
20 |
21 | ## Installing
22 |
23 | Installing involves two steps: adding the `junit5-system-exit` library to your build, and adding its Java Agent to
24 | your test task. Please consult the [FAQ](#faq) below if you run into problems.
25 |
26 | ### Gradle
27 |
28 | #### 1. Copy the following into your `build.gradle` or `build.gradle.kts`.
29 |
30 | ```groovy
31 | testImplementation("com.ginsberg:junit5-system-exit:2.0.2")
32 | ```
33 |
34 | #### 2. Enable the Java Agent
35 |
36 | It is important to add the Junit5 System Exit Java Agent in a way that makes it come after other agents which
37 | you may be using, such as JaCoCo. See the notes below for details on why.
38 |
39 | If you use the Groovy DSL, add this code to your `build.gradle` file in the `test` task...
40 |
41 | ```groovy
42 | // Groovy DSL
43 |
44 | test {
45 | useJUnitPlatform()
46 |
47 | def junit5SystemExit = configurations.testRuntimeClasspath.files
48 | .find { it.name.contains('junit5-system-exit') }
49 | jvmArgumentProviders.add({["-javaagent:$junit5SystemExit"]} as CommandLineArgumentProvider)
50 | }
51 | ```
52 |
53 | or if you use the Kotlin DSL...
54 |
55 | ```kotlin
56 | // Kotlin DSL
57 | test {
58 | useJUnitPlatform()
59 |
60 | jvmArgumentProviders.add(CommandLineArgumentProvider {
61 | listOf("-javaagent:${configurations.testRuntimeClasspath.get().files.find {
62 | it.name.contains("junit5-system-exit") }
63 | }")
64 | })
65 | }
66 | ```
67 |
68 | ### Maven
69 |
70 | #### 1. Copy the following into your `pom.xml`
71 |
72 | ```xml
73 |
74 | com.ginsberg
75 | junit5-system-exit
76 | 2.0.2
77 | test
78 |
79 | ```
80 |
81 | #### 2. Enable the Java Agent
82 |
83 | In `pom.xml`, add the `properties` goal to `maven-dependency-plugin`, in order to create properties for each dependency.
84 |
85 | ```xml
86 |
87 | maven-dependency-plugin
88 |
89 |
90 |
91 | properties
92 |
93 |
94 |
95 |
96 | ```
97 |
98 | Then add the following `` to `maven-surefire-plugin`, which references the property we just created for
99 | this library. This should account for other Java Agents and run after any others you may have, such as JaCoCo.
100 |
101 | ```xml
102 |
103 | org.apache.maven.plugins
104 | maven-surefire-plugin
105 |
106 | @{argLine} -javaagent:${com.ginsberg:junit5-system-exit:jar}
107 |
108 |
109 | ```
110 |
111 | And
112 |
113 | ## Use Cases - Annotation-based
114 |
115 | **A Test that expects `System.exit()` to be called, with any status code:**
116 |
117 | ```java
118 | public class MyTestCases {
119 |
120 | @Test
121 | @ExpectSystemExit
122 | void thatSystemExitIsCalled() {
123 | System.exit(1);
124 | }
125 | }
126 | ```
127 |
128 | **A Test that expects `System.exit(1)` to be called, with a specific status code:**
129 |
130 | ```java
131 | public class MyTestCases {
132 |
133 | @Test
134 | @ExpectSystemExitWithStatus(1)
135 | void thatSystemExitIsCalled() {
136 | System.exit(1);
137 | }
138 | }
139 | ```
140 |
141 | **A Test that should not expect `System.exit()` to be called, and fails the test if it does:**
142 |
143 | ```java
144 | public class MyTestCases {
145 |
146 | @Test
147 | @FailOnSystemExit
148 | void thisTestWillFail() {
149 | System.exit(1); // !!!
150 | }
151 | }
152 | ```
153 |
154 | The `@ExpectSystemExit`, `@ExpectSystemExitWithStatus`, and `@FailOnSystemExit` annotations can be applied to methods, classes, or annotations (to act as meta-annotations).
155 |
156 | ## Use Cases - Assertion-based
157 |
158 | **A Test that expects `System.exit()` to be called, with any status code:**
159 |
160 | ```java
161 | public class MyTestClasses {
162 |
163 | @Test
164 | void thatSystemExitIsCalled() {
165 | assertThatCallsSystemExit(() ->
166 | System.exit(42)
167 | );
168 | }
169 | }
170 | ```
171 |
172 | **A Test that expects `System.exit(1)` to be called, with a specific status code:**
173 |
174 | ```java
175 | public class MyTestClasses {
176 |
177 | @Test
178 | void thatSystemExitIsCalled() {
179 | assertThatCallsSystemExit(() ->
180 | System.exit(42)
181 | ).withExitCode(42);
182 | }
183 | }
184 | ```
185 |
186 | **A Test that expects `System.exit(1)` to be called, with status code in a specified range (inclusive):**
187 |
188 | ```java
189 | public class MyTestClasses {
190 |
191 | @Test
192 | void thatSystemExitIsCalled() {
193 | assertThatCallsSystemExit(() ->
194 | System.exit(42)
195 | ).withExitCodeInRange(1, 100);
196 | }
197 | }
198 | ```
199 |
200 | **A Test that should not expect `System.exit()` to be called, and fails the assertion if it does:**
201 |
202 | ```java
203 | public class MyTestClasses {
204 |
205 | @Test
206 | void thisTestWillFail() {
207 | assertThatDoesNotCallSystemExit(() ->
208 | System.exit(42) // !!!
209 | );
210 | }
211 | }
212 | ```
213 |
214 | ## FAQ
215 |
216 | ### :question: I don't want `Junit5-System-Exit` to rewrite the bytecode of a specific class or method that calls `System.exit()`.
217 |
218 | This is supported. When this library detects any annotation called `@DoNotRewriteExitCalls` on any method or class, bytecode
219 | rewriting will be skipped. While this library ships with its own implementation of `@DoNotRewriteExitCalls`, you'll probably
220 | want to write your own so you don't have to use this library outside the test scope. It is a marker annotation like this:
221 |
222 | ```java
223 | @Retention(RetentionPolicy.RUNTIME)
224 | @Target({ElementType.TYPE, ElementType.METHOD})
225 | public @interface DoNotRewriteExitCalls {
226 | }
227 | ```
228 |
229 | ### :question: JaCoCo issues a warning - "Execution data for class does not match"
230 |
231 | This happens when JaCoCo's Java Agent runs after this one. The instructions above _should_ put this agent after JaCoCo
232 | but things change over time, and there are probably more gradle configurations that I have not tested. If you run into
233 | this and are confident that you followed the instructions above, please reach out to me with a minimal example and I will
234 | see what I can do.
235 |
236 | ### :question: JaCoCo coverage is not accurate
237 |
238 | Because this Java Agent rewrites bytecode and must run after JaCoCo, there will be discrepancies in the JaCoCo Report.
239 | I have not found a way to account for this, but if you discover something please let me know!
240 |
241 | ## Contributing and Issues
242 |
243 | Please feel free to file issues for change requests or bugs. If you would like to contribute new functionality, please contact me first!
244 |
245 | Copyright © 2021-2024 by Todd Ginsberg
246 |
--------------------------------------------------------------------------------
/VERSION.txt:
--------------------------------------------------------------------------------
1 | 2.0.2
--------------------------------------------------------------------------------
/Version1.xInstructions.md:
--------------------------------------------------------------------------------
1 | # JUnit5 System.exit() Extension Version 1.x
2 |
3 | This [JUnit 5 Extension](https://junit.org/junit5/docs/current/user-guide/#extensions) helps you write tests for code
4 | that calls `System.exit()`.
5 |
6 | **Note** These instructions cover Version 1.x of this library, which is no longer supported. Please upgrade
7 | to [Version 2.x](https://github.com/tginsberg/junit5-system-exit) if you can.
8 |
9 | ## Installing
10 |
11 | Copy the following into your `build.gradle` or `build.xml`.
12 |
13 | **Gradle**
14 |
15 | ```groovy
16 | testImplementation("com.ginsberg:junit5-system-exit:1.1.2")
17 | ```
18 |
19 | **Maven**
20 |
21 | ```xml
22 |
23 | com.ginsberg
24 | junit5-system-exit
25 | 1.1.2
26 | test
27 |
28 | ```
29 |
30 | ## Notes On Compatibility With Newer JVMs
31 |
32 | **I highly recommend you upgrade this library to version 2.x if you can**
33 |
34 | Starting with **Java 17**, the use of the `SecurityManager` class ([see details of its use](https://todd.ginsberg.com/post/testing-system-exit/)) is deprecated
35 | and will emit a warning. [This is a known issue](https://github.com/tginsberg/junit5-system-exit/issues/10) and once an alternative is available, I will
36 | explore the possibility of upgrading this library.
37 |
38 | Starting with **Java 18**, the default behavior of the JVM was changed from allowing the `SecurityManager` to be changed at runtime to disallowing it by default.
39 | To re-enable the pre-Java 18 behavior, set the system property `java.security.manager` to the string `allow`.
40 | The details [can be read here](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/SecurityManager.html#set-security-manager) if you are interested.
41 |
42 | So, for Gradle:
43 |
44 | ```groovy
45 | test {
46 | systemProperty 'java.security.manager', 'allow'
47 | }
48 | ```
49 |
50 | And for Maven:
51 |
52 | ```xml
53 |
54 | org.apache.maven.plugins
55 | maven-surefire-plugin
56 |
57 |
58 | allow
59 |
60 |
61 |
62 | ```
63 |
64 | **Why doesn't `junit5-system-exit` set this property for me?**
65 |
66 | Good question, it certainly could! However, given the decision to terminally deprecate the `SecurityManager`, overriding this decision in a way that is not completely
67 | obvious to the people using is not something I want to do. I feel that users should make a conscious positive decision to override this behavior and take the time
68 | to evaluate the needs of their project. I don't want somebody caught by surprise that the `SecurityManager` is behaving in a non-default way because they happen to be
69 | using a testing library.
70 |
71 | ## Use cases
72 |
73 | **A Test that expects `System.exit()` to be called, with any status code:**
74 |
75 | ```java
76 | public class MyTestCases {
77 |
78 | @Test
79 | @ExpectSystemExit
80 | public void thatSystemExitIsCalled() {
81 | System.exit(1);
82 | }
83 | }
84 | ```
85 |
86 | **A Test that expects `System.exit(1)` to be called, with a specific status code:**
87 |
88 | ```java
89 | public class MyTestCases {
90 |
91 | @Test
92 | @ExpectSystemExitWithStatus(1)
93 | public void thatSystemExitIsCalled() {
94 | System.exit(1);
95 | }
96 | }
97 | ```
98 |
99 | **A Test that should not expect `System.exit(1)` to be called, and fails the test if it does:**
100 |
101 | ```java
102 | public class MyTestCases {
103 |
104 | @Test
105 | @FailOnSystemExit
106 | public void thisTestWillFail() {
107 | System.exit(1);
108 | }
109 | }
110 | ```
111 |
112 | The `@ExpectSystemExit`, `@ExpectSystemExitWithStatus`, and `@FailOnSystemExit` annotations can be applied to methods, classes, or annotations (to act as meta-annotations).
113 |
114 | ## Contributing and Issues
115 |
116 | Please feel free to file issues for change requests or bugs. If you would like to contribute new functionality, please contact me first!
117 |
118 | Copyright © 2021-2024 by Todd Ginsberg
119 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | import java.io.IOException
26 | import java.net.URI
27 |
28 | plugins {
29 | id("com.adarshr.test-logger") version "4.0.0"
30 | id("jacoco")
31 | id("java-library")
32 | id("org.barfuin.gradle.jacocolog") version "3.1.0"
33 | id("maven-publish")
34 | id("signing")
35 | }
36 |
37 | description = "A JUnit5 Extension to help write tests that call System.exit()"
38 | group = "com.ginsberg"
39 | version = file("VERSION.txt").readLines().first()
40 |
41 | val gitBranch = gitBranch()
42 | val junit5SystemExitVersion = if (gitBranch == "main" || gitBranch.startsWith("release/")) version.toString()
43 | else "${gitBranch.substringAfterLast("/")}-SNAPSHOT"
44 |
45 | val asmVersion by extra("9.7")
46 | val junitVersion by extra("5.11.0")
47 | val junitPlatformLauncherVersion by extra("1.11.0")
48 |
49 | java {
50 | toolchain {
51 | languageVersion = JavaLanguageVersion.of(17)
52 | }
53 | withJavadocJar()
54 | withSourcesJar()
55 | }
56 |
57 | repositories {
58 | mavenCentral()
59 | }
60 |
61 | dependencies {
62 | compileOnly("org.junit.jupiter:junit-jupiter-api:$junitVersion") {
63 | because("This library compiles against JUnit, but consumers will bring their own implementation")
64 | }
65 |
66 | implementation("org.ow2.asm:asm:$asmVersion") {
67 | because("Calls to System.exit() are rewritten in the agent using ASM.")
68 | }
69 |
70 | testRuntimeOnly("org.junit.platform:junit-platform-launcher:$junitPlatformLauncherVersion") {
71 | because("Starting in Gradle 9.0, this needs to be an explicitly declared dependency")
72 | }
73 |
74 | testImplementation("org.assertj:assertj-core:3.26.3")
75 | testImplementation("org.junit.jupiter:junit-jupiter:$junitVersion")
76 | testImplementation("org.junit.jupiter:junit-jupiter-params:$junitVersion")
77 | testImplementation("org.junit.platform:junit-platform-launcher:$junitPlatformLauncherVersion")
78 | }
79 |
80 | publishing {
81 | publications {
82 | create("junit5-system-exit") {
83 | from(components["java"])
84 | pom {
85 | name = "Junit5 System Exit"
86 | description = project.description
87 | version = junit5SystemExitVersion
88 | url = "https://github.com/tginsberg/junit5-system-exit"
89 | organization {
90 | name = "com.ginsberg"
91 | url = "https://github.com/tginsberg"
92 | }
93 | issueManagement {
94 | system = "GitHub"
95 | url = "https://github.com/tginsberg/junit5-system-exit/issues"
96 | }
97 | licenses {
98 | license {
99 | name = "The Apache License, Version 2.0"
100 | url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
101 | }
102 | }
103 | developers {
104 | developer {
105 | id = "tginsberg"
106 | name = "Todd Ginsberg"
107 | email = "todd@ginsberg.com"
108 | }
109 | }
110 | scm {
111 | connection = "scm:git:https://github.com/tginsberg/junit5-system-exit.git"
112 | developerConnection = "scm:git:https://github.com/tginsberg/junit5-system-exit.git"
113 | url = "https://github.com/tginsberg/junit5-system-exit"
114 | }
115 | }
116 | }
117 | }
118 | repositories {
119 | maven {
120 | url = if (version.toString().endsWith("-SNAPSHOT")) URI("https://oss.sonatype.org/content/repositories/snapshots/")
121 | else URI("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
122 | credentials {
123 | username = System.getenv("SONATYPE_USERNAME")
124 | password = System.getenv("SONATYPE_TOKEN")
125 | }
126 | }
127 | }
128 | }
129 |
130 | signing {
131 | useInMemoryPgpKeys(System.getenv("SONATYPE_SIGNING_KEY") , System.getenv("SONATYPE_SIGNING_PASSPHRASE"))
132 | sign(publishing.publications["junit5-system-exit"])
133 | }
134 |
135 | tasks {
136 | jacocoTestReport {
137 | dependsOn(test)
138 | reports {
139 | xml.required = true
140 | }
141 | }
142 |
143 | jar {
144 | manifest {
145 | attributes(
146 | "Implementation-Title" to "Junit5 System Exit",
147 | "Implementation-Version" to archiveVersion,
148 | "Premain-Class" to "com.ginsberg.junit.exit.agent.Junit5SystemExitAgent"
149 | )
150 | }
151 | }
152 |
153 | javadoc {
154 | (options as CoreJavadocOptions).apply {
155 | addStringOption("source", rootProject.java.toolchain.languageVersion.get().toString())
156 | addStringOption("Xdoclint:none", "-quiet")
157 | }
158 | }
159 |
160 | publish {
161 | doLast {
162 | println("Project Version: $version")
163 | println("Publish Version: $junit5SystemExitVersion")
164 | }
165 | }
166 |
167 | test {
168 | useJUnitPlatform()
169 | dependsOn(jar)
170 | finalizedBy(jacocoTestReport)
171 | jvmArgumentProviders.add(CommandLineArgumentProvider {
172 | listOf("-javaagent:${jar.get().archiveFile.get().asFile.absolutePath}")
173 | })
174 |
175 | doLast {
176 | println(
177 | """
178 | Note: Several tests were skipped during this run, by design.
179 | The skipped tests are run manually within other tests, to test instrumenting behavior.
180 | """.trimIndent()
181 | )
182 | }
183 | }
184 |
185 | }
186 |
187 | fun gitBranch(): String =
188 | ProcessBuilder("git rev-parse --abbrev-ref HEAD".split(" "))
189 | .redirectOutput(ProcessBuilder.Redirect.PIPE)
190 | .redirectError(ProcessBuilder.Redirect.PIPE)
191 | .start()
192 | .run {
193 | val error = errorStream.bufferedReader().readText()
194 | if (error.isNotEmpty()) throw IOException(error)
195 | inputStream.bufferedReader().readText().trim()
196 | }
197 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tginsberg/junit5-system-exit/90561b449cc314800399c3920d88349c6124ebc0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #
2 | # MIT License
3 | #
4 | # Copyright (c) 2024 Todd Ginsberg
5 | #
6 | # Permission is hereby granted, free of charge, to any person obtaining a copy
7 | # of this software and associated documentation files (the "Software"), to deal
8 | # in the Software without restriction, including without limitation the rights
9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | # copies of the Software, and to permit persons to whom the Software is
11 | # furnished to do so, subject to the following conditions:
12 | #
13 | # The above copyright notice and this permission notice shall be included in all
14 | # copies or substantial portions of the Software.
15 | #
16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | # SOFTWARE.
23 | #
24 |
25 | distributionBase=GRADLE_USER_HOME
26 | distributionPath=wrapper/dists
27 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
28 | zipStoreBase=GRADLE_USER_HOME
29 | zipStorePath=wrapper/dists
30 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | rootProject.name = "junit5-system-exit"
26 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/ExitPreventerStrategy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.ginsberg.junit.exit;
25 |
26 | public interface ExitPreventerStrategy {
27 |
28 | Integer firstExitStatusCode();
29 |
30 | default void beforeTest() {
31 | }
32 |
33 | default void afterTest() {
34 | }
35 |
36 | default void resetBetweenTests() {
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/ExpectSystemExit.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import org.junit.jupiter.api.extension.ExtendWith;
28 |
29 | import java.lang.annotation.ElementType;
30 | import java.lang.annotation.Retention;
31 | import java.lang.annotation.RetentionPolicy;
32 | import java.lang.annotation.Target;
33 |
34 | /**
35 | * This is a marker annotation that indicates the given test method or class is expected
36 | * to call System.exit()
37 | */
38 | @Retention(RetentionPolicy.RUNTIME)
39 | @Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
40 | @ExtendWith(SystemExitExtension.class)
41 | public @interface ExpectSystemExit {
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/ExpectSystemExitWithStatus.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import org.junit.jupiter.api.extension.ExtendWith;
28 |
29 | import java.lang.annotation.ElementType;
30 | import java.lang.annotation.Retention;
31 | import java.lang.annotation.RetentionPolicy;
32 | import java.lang.annotation.Target;
33 |
34 | /**
35 | * This is a marker annotation that indicates the given test method or class is expected
36 | * to call System.exit() with a specific code
37 | */
38 | @Retention(RetentionPolicy.RUNTIME)
39 | @Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
40 | @ExtendWith(SystemExitExtension.class)
41 | public @interface ExpectSystemExitWithStatus {
42 | int value();
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/FailOnSystemExit.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import org.junit.jupiter.api.extension.ExtendWith;
28 |
29 | import java.lang.annotation.ElementType;
30 | import java.lang.annotation.Retention;
31 | import java.lang.annotation.RetentionPolicy;
32 | import java.lang.annotation.Target;
33 |
34 | /**
35 | * This is a marker annotation that indicates the given test method or class is not expected
36 | * to call System.exit(). By annotating a test or a class with this annotation, we can prevent
37 | * an inadvertent System.exit() call from tearing down the test infrastructure.
38 | */
39 | @Retention(RetentionPolicy.RUNTIME)
40 | @Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
41 | @ExtendWith(SystemExitExtension.class)
42 | public @interface FailOnSystemExit {
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/SystemExitExtension.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import com.ginsberg.junit.exit.agent.AgentSystemExitHandlerStrategy;
28 | import com.ginsberg.junit.exit.agent.DoNotRewriteExitCalls;
29 | import org.junit.jupiter.api.extension.AfterEachCallback;
30 | import org.junit.jupiter.api.extension.BeforeEachCallback;
31 | import org.junit.jupiter.api.extension.ExtensionContext;
32 | import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
33 |
34 | import java.lang.annotation.Annotation;
35 | import java.util.Optional;
36 |
37 | import static org.junit.jupiter.api.Assertions.assertEquals;
38 | import static org.junit.jupiter.api.Assertions.assertNotNull;
39 | import static org.junit.jupiter.api.Assertions.assertNull;
40 | import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
41 |
42 | /**
43 | * Entry point for JUnit tests. This class is responsible for installing the preferred `ExitPreventerStrategy`
44 | * and interpreting the results.
45 | */
46 | @DoNotRewriteExitCalls
47 | public class SystemExitExtension implements BeforeEachCallback, AfterEachCallback, TestExecutionExceptionHandler {
48 | private Integer expectedStatusCode;
49 | private boolean failOnSystemExit;
50 | private final ExitPreventerStrategy exitPreventerStrategy;
51 |
52 | public SystemExitExtension() {
53 | if(AgentSystemExitHandlerStrategy.isLoadedFromAgent()) {
54 | exitPreventerStrategy = new AgentSystemExitHandlerStrategy();
55 | } else {
56 | throw new IllegalStateException("SystemExitExtension Agent not loaded, please see documentation");
57 | }
58 | }
59 |
60 | @Override
61 | public void afterEach(ExtensionContext context) {
62 | exitPreventerStrategy.afterTest();
63 |
64 | try {
65 | if (failOnSystemExit) {
66 | assertNull(
67 | exitPreventerStrategy.firstExitStatusCode(),
68 | "Unexpected System.exit(" + exitPreventerStrategy.firstExitStatusCode() + ") caught"
69 | );
70 | } else if (expectedStatusCode == null) {
71 | assertNotNull(
72 | exitPreventerStrategy.firstExitStatusCode(),
73 | "Expected System.exit() to be called, but it was not"
74 | );
75 | } else {
76 | assertEquals(
77 | expectedStatusCode,
78 | exitPreventerStrategy.firstExitStatusCode(),
79 | "Expected System.exit(" + expectedStatusCode + ") to be called, but it was not."
80 | );
81 | }
82 | } finally {
83 | // Clear state so if this is run as part of a @ParameterizedTest, the next time through we'll have the
84 | // correct state
85 | expectedStatusCode = null;
86 | failOnSystemExit = false;
87 | exitPreventerStrategy.resetBetweenTests();
88 | }
89 | }
90 |
91 | @Override
92 | public void beforeEach(final ExtensionContext context) {
93 | // Should we fail on a System.exit() rather than letting it bubble out?
94 | failOnSystemExit = getAnnotation(context, FailOnSystemExit.class).isPresent();
95 |
96 | // Get the expected exit status code, if any
97 | getAnnotation(context, ExpectSystemExitWithStatus.class).ifPresent(code -> expectedStatusCode = code.value());
98 |
99 | // Allow the strategy to do pre-test housekeeping
100 | exitPreventerStrategy.beforeTest();
101 | }
102 |
103 | /**
104 | * This is here so we can catch exceptions thrown by the `ExitPreventerStrategy` and prevent them from
105 | * stopping the annotated test. If anything other than our own exception comes through, throw it because
106 | * the `ExitPreventerStrategy` has encountered some other test-failing exception.
107 | *
108 | * @param context the current extension context; never {@code null}
109 | * @param throwable the {@code Throwable} to handle; never {@code null}
110 | * @throws Throwable if the throwable argument is not a SystemExitPreventedException
111 | */
112 | @Override
113 | public void handleTestExecutionException(
114 | final ExtensionContext context,
115 | final Throwable throwable
116 | ) throws Throwable {
117 | if (!(throwable instanceof SystemExitPreventedException)) {
118 | throw throwable;
119 | }
120 | }
121 |
122 | // Find the annotation on a method, or failing that, a class.
123 | private Optional getAnnotation(
124 | final ExtensionContext context,
125 | final Class annotationClass
126 | ) {
127 | final Optional method = findAnnotation(context.getTestMethod(), annotationClass);
128 | return method.isPresent() ? method : findAnnotation(context.getTestClass(), annotationClass);
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/SystemExitPreventedException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | /**
28 | * A marker exception so we know that a System.exit()
call was intercepted and prevented.
29 | */
30 | public class SystemExitPreventedException extends SecurityException {
31 |
32 | private final int statusCode;
33 |
34 | public SystemExitPreventedException(int statusCode) {
35 | this.statusCode = statusCode;
36 | }
37 |
38 | public int getStatusCode() {
39 | return statusCode;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/agent/AgentSystemExitHandlerStrategy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit.agent;
26 |
27 | import com.ginsberg.junit.exit.ExitPreventerStrategy;
28 | import com.ginsberg.junit.exit.SystemExitPreventedException;
29 |
30 | @DoNotRewriteExitCalls
31 | public class AgentSystemExitHandlerStrategy implements ExitPreventerStrategy {
32 |
33 | private static Integer firstExitStatusCode;
34 | private static boolean loadedFromAgent = false;
35 | private static boolean isRunningTest = false;
36 |
37 | public static void handleExit(final int status) {
38 | if(isRunningTest) {
39 | if (firstExitStatusCode == null) {
40 | firstExitStatusCode = status;
41 | }
42 | throw new SystemExitPreventedException(firstExitStatusCode);
43 | } else {
44 | System.exit(status);
45 | }
46 | }
47 |
48 | public static void agentInit() {
49 | loadedFromAgent = true;
50 | }
51 |
52 | public static boolean isLoadedFromAgent() {
53 | return loadedFromAgent;
54 | }
55 |
56 | @Override
57 | public Integer firstExitStatusCode() {
58 | return firstExitStatusCode;
59 | }
60 |
61 | @Override
62 | public void beforeTest() {
63 | isRunningTest = true;
64 | }
65 |
66 | @Override
67 | public void afterTest() {
68 | isRunningTest = false;
69 | }
70 |
71 | @Override
72 | public void resetBetweenTests() {
73 | firstExitStatusCode = null;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/agent/DoNotRewriteExitCalls.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.ginsberg.junit.exit.agent;
25 |
26 | import java.lang.annotation.ElementType;
27 | import java.lang.annotation.Retention;
28 | import java.lang.annotation.RetentionPolicy;
29 | import java.lang.annotation.Target;
30 |
31 | @Retention(RetentionPolicy.RUNTIME)
32 | @Target({ElementType.TYPE, ElementType.METHOD})
33 | public @interface DoNotRewriteExitCalls {
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/agent/Junit5SystemExitAgent.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.ginsberg.junit.exit.agent;
25 |
26 | import org.objectweb.asm.AnnotationVisitor;
27 | import org.objectweb.asm.ClassReader;
28 | import org.objectweb.asm.ClassVisitor;
29 | import org.objectweb.asm.ClassWriter;
30 | import org.objectweb.asm.MethodVisitor;
31 | import org.objectweb.asm.Opcodes;
32 |
33 | import java.lang.instrument.ClassFileTransformer;
34 | import java.lang.instrument.Instrumentation;
35 | import java.security.ProtectionDomain;
36 | import java.util.Set;
37 | import java.util.logging.Logger;
38 |
39 | public class Junit5SystemExitAgent {
40 |
41 | private final static Logger log = Logger.getLogger(Junit5SystemExitAgent.class.getName());
42 |
43 | private Junit5SystemExitAgent() {
44 |
45 | }
46 |
47 | private final static Set disallowedClassPrefixes = Set.of(
48 | "com/sun/", "java/", "jdk/", "worker/org/gradle/", "sun/"
49 | );
50 |
51 | private final static String SKIP_ANNOTATION = "/DoNotRewriteExitCalls;";
52 |
53 | public static void premain(final String agentArgs, final Instrumentation inst) {
54 | AgentSystemExitHandlerStrategy.agentInit();
55 | inst.addTransformer(new SystemExitClassTransformer());
56 | }
57 |
58 | static class SystemExitClassTransformer implements ClassFileTransformer {
59 | @Override
60 | public byte[] transform(final ClassLoader loader,
61 | final String className,
62 | final Class> classBeingRedefined,
63 | final ProtectionDomain protectionDomain,
64 | final byte[] classFileBuffer) {
65 | if (disallowedClassPrefixes.stream().anyMatch(className::startsWith)) {
66 | return null;
67 | }
68 | final ClassReader classReader = new ClassReader(classFileBuffer);
69 | final ClassWriter classWriter = new ClassWriter(classReader, 0);
70 | classReader.accept(new SystemExitClassVisitor(className, classWriter), 0);
71 | return classWriter.toByteArray();
72 | }
73 | }
74 |
75 | static class SystemExitClassVisitor extends ClassVisitor {
76 | private final String className;
77 |
78 | public SystemExitClassVisitor(final String className, final ClassVisitor cv) {
79 | super(Opcodes.ASM9, cv);
80 | this.className = className;
81 | }
82 |
83 | private boolean hasSkipAnnotation = false;
84 |
85 | @Override
86 | public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
87 | if(descriptor.endsWith(SKIP_ANNOTATION)) {
88 | hasSkipAnnotation = true;
89 | }
90 | return super.visitAnnotation(descriptor, visible);
91 | }
92 |
93 | @Override
94 | public MethodVisitor visitMethod(final int access,
95 | final String name,
96 | final String descriptor,
97 | final String signature,
98 | final String[] exceptions) {
99 | if(hasSkipAnnotation) {
100 | return super.visitMethod(access, name, descriptor, signature, exceptions);
101 | }
102 | return new SystemExitMethodVisitor(
103 | className,
104 | name,
105 | super.visitMethod(access, name, descriptor, signature, exceptions)
106 | );
107 | }
108 | }
109 |
110 | static class SystemExitMethodVisitor extends MethodVisitor {
111 | private boolean hasSkipAnnotation = false;
112 | private final String className;
113 | private final String methodName;
114 |
115 | public SystemExitMethodVisitor(final String className, final String methodName, final MethodVisitor mv) {
116 | super(Opcodes.ASM9, mv);
117 | this.className = className;
118 | this.methodName = methodName;
119 | }
120 |
121 | @Override
122 | public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
123 | if(descriptor.endsWith(SKIP_ANNOTATION)) {
124 | hasSkipAnnotation = true;
125 | }
126 | return super.visitAnnotation(descriptor, visible);
127 | }
128 |
129 | @Override
130 | public void visitMethodInsn(final int opcode,
131 | final String owner,
132 | final String name,
133 | final String descriptor,
134 | final boolean isInterface) {
135 | if (!hasSkipAnnotation && owner.equals("java/lang/System") && name.equals("exit")) {
136 | log.fine("Replacing System.exit() call in: " + className + "." + methodName);
137 | super.visitMethodInsn(
138 | Opcodes.INVOKESTATIC,
139 | "com/ginsberg/junit/exit/agent/AgentSystemExitHandlerStrategy",
140 | "handleExit",
141 | descriptor,
142 | false
143 | );
144 | } else {
145 | if(hasSkipAnnotation) {
146 | log.fine("Not replacing System.exit() call in: " + className + "." + methodName + " due to presence of 'skip this' annotation");
147 | }
148 | super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
149 | }
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/main/java/com/ginsberg/junit/exit/assertions/SystemExitAssertion.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit.assertions;
26 |
27 | import com.ginsberg.junit.exit.ExitPreventerStrategy;
28 | import com.ginsberg.junit.exit.SystemExitPreventedException;
29 | import com.ginsberg.junit.exit.agent.AgentSystemExitHandlerStrategy;
30 |
31 | import static org.junit.jupiter.api.Assertions.assertEquals;
32 | import static org.junit.jupiter.api.Assertions.assertTrue;
33 | import static org.junit.jupiter.api.Assertions.fail;
34 |
35 |
36 | public class SystemExitAssertion {
37 | private final SystemExitPreventedException theException;
38 |
39 | public SystemExitAssertion(SystemExitPreventedException theException) {
40 | this.theException = theException;
41 | }
42 |
43 | public static SystemExitAssertion assertThatCallsSystemExit(final Runnable function) {
44 | return new SystemExitAssertion(catchSystemExitFrom(function)).calledSystemExit();
45 | }
46 |
47 | public static void assertThatDoesNotCallSystemExit(final Runnable function) {
48 | new SystemExitAssertion(catchSystemExitFrom(function)).didNotCallSystemExit();
49 | }
50 |
51 | private SystemExitAssertion calledSystemExit() {
52 | if (theException == null) {
53 | fail("Expected call to System.exit() did not happen");
54 | }
55 | return this;
56 | }
57 |
58 | private SystemExitAssertion didNotCallSystemExit() {
59 | if (theException != null) {
60 | fail("Unexpected call to System.exit() with exit code " + theException.getStatusCode(), theException);
61 | }
62 | return this;
63 | }
64 |
65 | private static SystemExitPreventedException catchSystemExitFrom(final Runnable function) {
66 | final ExitPreventerStrategy exitPreventerStrategy = new AgentSystemExitHandlerStrategy();
67 | try {
68 | exitPreventerStrategy.resetBetweenTests();
69 | exitPreventerStrategy.beforeTest();
70 | function.run();
71 | } catch (SystemExitPreventedException e) {
72 | return e;
73 | } catch (Exception e) {
74 | throw new RuntimeException(e);
75 | } finally {
76 | exitPreventerStrategy.afterTest();
77 | }
78 | return null;
79 | }
80 |
81 | public SystemExitAssertion withExitCode(final int code) {
82 | assertEquals(code, theException.getStatusCode(), "Wrong exit code found");
83 | return this;
84 | }
85 |
86 | public SystemExitAssertion withExitCodeInRange(final int startInclusive, final int endInclusive) {
87 | assertTrue(startInclusive < endInclusive, "Start must come before end");
88 | final int code = theException.getStatusCode();
89 | assertTrue(
90 | startInclusive <= code && code <= endInclusive,
91 | "Exit code expected in range (" + startInclusive + " .. " + endInclusive + ") but was " + code
92 | );
93 | return this;
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/src/test/java/com/ginsberg/junit/exit/ExpectSystemExitTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import com.ginsberg.junit.exit.agent.AgentSystemExitHandlerStrategy;
28 | import org.junit.jupiter.api.DisplayName;
29 | import org.junit.jupiter.api.Nested;
30 | import org.junit.jupiter.api.Test;
31 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
32 |
33 | import java.lang.reflect.Field;
34 |
35 | import static com.ginsberg.junit.exit.TestUtils.assertTestFails;
36 | import static org.junit.jupiter.api.Assertions.assertThrows;
37 |
38 | class ExpectSystemExitTest {
39 |
40 | @Nested
41 | @DisplayName("Success Cases")
42 | class HappyPath {
43 | @Test
44 | @DisplayName("System.exit() is caught and detected")
45 | @ExpectSystemExit
46 | void detectSystemExit() {
47 | System.exit(1234);
48 | }
49 |
50 | @Test
51 | @DisplayName("System.exit() is caught and detected within a thread")
52 | @ExpectSystemExit
53 | void detectSystemExitInThread() throws InterruptedException {
54 | final Thread t = new Thread(() -> System.exit(1234));
55 | t.start();
56 | t.join();
57 | }
58 |
59 | @Nested
60 | @DisplayName("Class Success")
61 | @ExpectSystemExit
62 | class ExpectedSuccessClassLevel {
63 | @Test
64 | @DisplayName("Method in class annotated with ExpectSystemExit succeeds")
65 | void classLevelExpect() {
66 | System.exit(123456);
67 | }
68 | }
69 | }
70 |
71 | @Nested
72 | @DisplayName("Failure Cases")
73 | class FailurePath {
74 | @Test
75 | @DisplayName("System.exit() is expected for method but not called")
76 | void expectSystemExitThatDoesNotHappenMethod() {
77 | assertTestFails(ExpectedFailuresAtTestLevelTest.class, "doNotCallSystemExit");
78 | }
79 |
80 | @Test
81 | @DisplayName("System.exit() is expected for class but not called")
82 | void expectSystemExitThatDoesNotHappenClass() {
83 | assertTestFails(ExpectedFailuresAtClassLevelTest.class);
84 | }
85 |
86 | @Test
87 | @DisplayName("Extension fails when agent instrumentation has not run")
88 | void expectAgentInstrumentation() throws NoSuchFieldException, IllegalAccessException {
89 | // Arrange
90 | final Field field = AgentSystemExitHandlerStrategy.class.getDeclaredField("loadedFromAgent");
91 | field.setAccessible(true);
92 | field.setBoolean(AgentSystemExitHandlerStrategy.class, false);
93 |
94 | // Act
95 | try {
96 | assertThrows(IllegalStateException.class, SystemExitExtension::new);
97 | } finally {
98 | field.setBoolean(AgentSystemExitHandlerStrategy.class, true);
99 | }
100 | }
101 | }
102 |
103 | @SuppressWarnings("JUnitMalformedDeclaration")
104 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
105 | static class ExpectedFailuresAtTestLevelTest {
106 | @Test
107 | @ExpectSystemExit
108 | void doNotCallSystemExit() {
109 | // Done! :)
110 | }
111 | }
112 |
113 | @SuppressWarnings("JUnitMalformedDeclaration")
114 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
115 | @ExpectSystemExit
116 | static class ExpectedFailuresAtClassLevelTest {
117 | @Test
118 | void doNotCallSystemExit() {
119 | // Done! :)
120 | }
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/test/java/com/ginsberg/junit/exit/ExpectSystemExitWithStatusTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import org.junit.jupiter.api.DisplayName;
28 | import org.junit.jupiter.api.Nested;
29 | import org.junit.jupiter.api.Test;
30 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
31 |
32 | import static com.ginsberg.junit.exit.TestUtils.assertTestFails;
33 | import static com.ginsberg.junit.exit.TestUtils.assertTestFailsExceptionally;
34 |
35 | class ExpectSystemExitWithStatusTest {
36 |
37 | @Nested
38 | @DisplayName("Success Cases")
39 | class HappyPath {
40 | @Test
41 | @DisplayName("System.exit(1234) is caught and detected")
42 | @ExpectSystemExitWithStatus(1234)
43 | void detectSystemExit() {
44 | System.exit(1234);
45 | }
46 |
47 | @Test
48 | @DisplayName("System.exit(1234) is caught and detected within a thread")
49 | @ExpectSystemExitWithStatus(1234)
50 | void detectSystemExitInThread() throws InterruptedException {
51 | final Thread t = new Thread(() -> System.exit(1234));
52 | t.start();
53 | t.join();
54 | }
55 |
56 | @Nested
57 | @DisplayName("Class Success")
58 | @ExpectSystemExitWithStatus(123456)
59 | class ExpectedSuccessClassLevel {
60 | @Test
61 | @DisplayName("Method in class annotated with ExpectSystemExitWithStatus(123456) succeeds")
62 | void classLevelExpect() {
63 | System.exit(123456);
64 | }
65 | }
66 | }
67 |
68 | @Nested
69 | @DisplayName("Failure Cases")
70 | class FailurePath {
71 | @Test
72 | @DisplayName("System.exit(1234) is expected but not called at all, on method")
73 | void expectSystemExitThatDoesNotHappenMethod() {
74 | assertTestFails(ExpectedFailuresAtMethodLevelTest.class, "doNotCallSystemExit");
75 | }
76 |
77 | @Test
78 | @DisplayName("System.exit(1234) is expected but another code was used, on method")
79 | void expectSystemExitWithDifferentCodeMethod() {
80 | assertTestFails(ExpectedFailuresAtMethodLevelTest.class, "exitWith4567");
81 | }
82 |
83 | @Test
84 | @DisplayName("System.exit(1234) is expected but not called at all, on method")
85 | void expectSystemExitThatDoesNotHappenClass() {
86 | assertTestFails(ExpectedFailuresAtClassLevelNoExitTest.class);
87 | }
88 |
89 | @Test
90 | @DisplayName("System.exit(1234) is expected but another code was used, on class")
91 | void expectSystemExitWithDifferentCodeClass() {
92 | assertTestFails(ExpectedFailuresAtClassLevelDifferentCodeTest.class);
93 | }
94 |
95 | @Test
96 | @DisplayName("Exceptions other than SystemExitPreventedException bubble out")
97 | void expectNonSystemExitExceptionsToBubbleOutAndFailTest() {
98 | assertTestFailsExceptionally(
99 | ExpectedExceptionalFailureTest.class,
100 | "throwInsteadOfExit",
101 | IllegalStateException.class
102 | );
103 | }
104 | }
105 |
106 | @SuppressWarnings("JUnitMalformedDeclaration")
107 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
108 | static class ExpectedFailuresAtMethodLevelTest {
109 |
110 | @Test
111 | @ExpectSystemExitWithStatus(1234)
112 | void doNotCallSystemExit() {
113 | // Done! :)
114 | }
115 |
116 | @Test
117 | @ExpectSystemExitWithStatus(1234)
118 | void exitWith4567() {
119 | System.exit(4567);
120 | }
121 | }
122 |
123 | @SuppressWarnings("JUnitMalformedDeclaration")
124 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
125 | @ExpectSystemExitWithStatus(1234)
126 | static class ExpectedFailuresAtClassLevelNoExitTest {
127 |
128 | @Test
129 | void doNotCallSystemExit() {
130 | // Done! :)
131 | }
132 |
133 | }
134 |
135 | @SuppressWarnings("JUnitMalformedDeclaration")
136 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
137 | @ExpectSystemExitWithStatus(1234)
138 | static class ExpectedFailuresAtClassLevelDifferentCodeTest {
139 | @Test
140 | void exitWith4567() {
141 | System.exit(4567);
142 | }
143 | }
144 |
145 | @SuppressWarnings("JUnitMalformedDeclaration")
146 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
147 | @ExpectSystemExit
148 | static class ExpectedExceptionalFailureTest {
149 | @Test
150 | void throwInsteadOfExit() {
151 | throw new IllegalStateException();
152 | }
153 | }
154 |
155 | }
156 |
--------------------------------------------------------------------------------
/src/test/java/com/ginsberg/junit/exit/FailOnSystemExitTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import org.junit.jupiter.api.DisplayName;
28 | import org.junit.jupiter.api.Test;
29 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
30 |
31 | import static com.ginsberg.junit.exit.TestUtils.assertTestFails;
32 | import static com.ginsberg.junit.exit.TestUtils.assertTestSucceeds;
33 |
34 | class FailOnSystemExitTest {
35 |
36 | @Test
37 | @DisplayName("@FailOnSystemExit on method - exception caught and fails test")
38 | void failOnSystemExitOnMethod() {
39 | assertTestFails(FailOnSystemExitAtTestLevelTest.class, "callsSystemExit");
40 | }
41 |
42 | @Test
43 | @DisplayName("@FailOnSystemExit on method - System.exit not called")
44 | void succeedWhenNotCallingSystemExitInMethod() {
45 | assertTestSucceeds(FailOnSystemExitAtTestLevelTest.class, "doesNotCallSystemExit");
46 | }
47 |
48 | @Test
49 | @DisplayName("@FailOnSystemExit on class - exception caught and fails test")
50 | void failOnSystemExitOnClass() {
51 | assertTestFails(FailOnSystemExitAtClassLevelTest.class);
52 | }
53 |
54 | @Test
55 | @DisplayName("@FailOnSystemExit on class - System.exit not called")
56 | void succeedWhenNotCallingSystemExitOnClass() {
57 | assertTestSucceeds(FailOnSystemExitAtClassLevelWithoutSystemExitTest.class);
58 | }
59 |
60 | @SuppressWarnings("JUnitMalformedDeclaration")
61 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
62 | static class FailOnSystemExitAtTestLevelTest {
63 | @Test
64 | @FailOnSystemExit
65 | void callsSystemExit() {
66 | System.exit(42);
67 | }
68 |
69 | @Test
70 | @FailOnSystemExit
71 | void doesNotCallSystemExit() {
72 | // Nothing to do
73 | }
74 | }
75 |
76 | @SuppressWarnings("JUnitMalformedDeclaration")
77 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
78 | @FailOnSystemExit
79 | static class FailOnSystemExitAtClassLevelTest {
80 | @Test
81 | void callsSystemExit() {
82 | System.exit(42);
83 | }
84 | }
85 |
86 | @SuppressWarnings("JUnitMalformedDeclaration")
87 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
88 | @FailOnSystemExit
89 | static class FailOnSystemExitAtClassLevelWithoutSystemExitTest {
90 | @Test
91 | void doesNotCallSystemExit() {
92 | // Nothing to do
93 | }
94 | }
95 |
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/test/java/com/ginsberg/junit/exit/TestUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import org.junit.platform.launcher.LauncherDiscoveryRequest;
28 | import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
29 | import org.junit.platform.launcher.core.LauncherFactory;
30 | import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
31 | import org.junit.platform.launcher.listeners.TestExecutionSummary;
32 |
33 | import java.util.HashSet;
34 | import java.util.Set;
35 |
36 | import static org.junit.jupiter.api.Assertions.assertEquals;
37 | import static org.junit.jupiter.api.Assertions.fail;
38 | import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
39 | import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
40 |
41 | class TestUtils {
42 |
43 | static void assertTestFails(final Class> clazz, final String testMethod) {
44 | final SummaryGeneratingListener listener = executeTest(clazz, testMethod);
45 |
46 | assertEquals(1, listener.getSummary().getTestsFoundCount(), "Should have found one test");
47 | assertEquals(1, listener.getSummary().getTestsFailedCount(), "Single test should have failed");
48 | }
49 |
50 | static void assertTestFails(final Class> clazz) {
51 | final SummaryGeneratingListener listener = executeTest(clazz, null);
52 |
53 | assertEquals(1, listener.getSummary().getTestsFoundCount(), "Should have found one test");
54 | assertEquals(1, listener.getSummary().getTestsFailedCount(), "Single test should have failed");
55 | }
56 |
57 | static void assertTestFailsExceptionally(
58 | final Class> clazz,
59 | final String testMethod,
60 | final Class extends Exception> expectedExceptionClass
61 | ) {
62 | final SummaryGeneratingListener listener = executeTest(clazz, testMethod);
63 | assertEquals(1, listener.getSummary().getFailures().size(), "Test did not finish exceptionally");
64 | assertEquals(
65 | expectedExceptionClass,
66 | listener.getSummary().getFailures().get(0).getException().getClass(),
67 | "Failed exceptionally but not because of the expected exception"
68 | );
69 |
70 | assertEquals(1, listener.getSummary().getTestsFoundCount(), "Should have found one test");
71 | assertEquals(1, listener.getSummary().getTestsFailedCount(), "Single test should have failed");
72 | }
73 |
74 | static void assertParameterizedTestFails(final Class> clazz, final Boolean... expectedSuccess) {
75 | final SummaryGeneratingListener listener = executeTest(clazz, null);
76 | final Set failedTestNumbers = new HashSet<>();
77 | for(final TestExecutionSummary.Failure failure : listener.getSummary().getFailures()) {
78 | failedTestNumbers.add(
79 | Integer.parseInt(failure.getTestIdentifier().getDisplayName()) -1
80 | );
81 | }
82 |
83 | assertEquals(expectedSuccess.length, listener.getSummary().getTestsFoundCount(), "Wrong number of tests found");
84 | for(int testNumber = 0; testNumber < expectedSuccess.length; testNumber++) {
85 | if(expectedSuccess[testNumber] && failedTestNumbers.contains(testNumber)) {
86 | fail(String.format("Test %d should have succeeded, but didn't", testNumber));
87 | } else if(!expectedSuccess[testNumber] && !failedTestNumbers.contains(testNumber)) {
88 | fail(String.format("Test %d should have failed, but didn't", testNumber));
89 | }
90 | }
91 |
92 | }
93 |
94 | static void assertTestSucceeds(final Class> clazz, final String testMethod) {
95 | final SummaryGeneratingListener listener = executeTest(clazz, testMethod);
96 |
97 | assertEquals(1, listener.getSummary().getTestsFoundCount(), "Should have found one test");
98 | assertEquals(1, listener.getSummary().getTestsSucceededCount(), "Single test should have succeeded");
99 | }
100 |
101 | static void assertTestSucceeds(final Class> clazz) {
102 | final SummaryGeneratingListener listener = executeTest(clazz, null);
103 |
104 | assertEquals(1, listener.getSummary().getTestsFoundCount(), "Should have found one test");
105 | assertEquals(1, listener.getSummary().getTestsSucceededCount(), "Single test should have succeeded");
106 | }
107 |
108 | /**
109 | * Execute the given test and then return a summary of its execution. This is used for tests that
110 | * succeed when other tests fail ("Test that a test decorated with X fails when...")
111 | */
112 | private static SummaryGeneratingListener executeTest(final Class> clazz, final String testMethod) {
113 | final SummaryGeneratingListener listener = new SummaryGeneratingListener();
114 | try {
115 | System.setProperty("running_within_test", "true");
116 | final LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
117 | .selectors(
118 | testMethod == null ?
119 | selectClass(clazz) :
120 | selectMethod(clazz, testMethod)
121 | )
122 | .build();
123 |
124 | LauncherFactory.create().execute(request, listener);
125 | return listener;
126 | } finally {
127 | System.clearProperty("running_within_test");
128 | }
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/test/java/com/ginsberg/junit/exit/WithParameterizedTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024 Todd Ginsberg
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | package com.ginsberg.junit.exit;
26 |
27 | import org.junit.jupiter.api.DisplayName;
28 | import org.junit.jupiter.api.Test;
29 | import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
30 | import org.junit.jupiter.params.ParameterizedTest;
31 | import org.junit.jupiter.params.provider.ValueSource;
32 |
33 | import static com.ginsberg.junit.exit.TestUtils.assertParameterizedTestFails;
34 |
35 | public class WithParameterizedTest {
36 |
37 | @Test
38 | @DisplayName("@ParameterizedTest on method should reset state between tests")
39 | void failOnSystemExitOnClass() {
40 | assertParameterizedTestFails(WithParameterizedTest.SucceedsAndThenFailsTest.class, true, false, true);
41 | }
42 |
43 | @EnabledIfSystemProperty(named = "running_within_test", matches = "true")
44 | static class SucceedsAndThenFailsTest {
45 | @ParameterizedTest(name = "{index}")
46 | @ValueSource(booleans = {true, false, true})
47 | @ExpectSystemExit
48 | public void testBasicAssumptions(boolean shouldSucceed) {
49 | if(shouldSucceed) {
50 | System.exit(1); // This test expects a system exit to succeed
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/test/java/com/ginsberg/junit/exit/assertions/SystemExitAssertionTest.java:
--------------------------------------------------------------------------------
1 | package com.ginsberg.junit.exit.assertions;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.opentest4j.AssertionFailedError;
5 |
6 | import static com.ginsberg.junit.exit.assertions.SystemExitAssertion.assertThatCallsSystemExit;
7 | import static com.ginsberg.junit.exit.assertions.SystemExitAssertion.assertThatDoesNotCallSystemExit;
8 | import static org.assertj.core.api.Assertions.assertThat;
9 | import static org.junit.jupiter.api.Assertions.assertEquals;
10 | import static org.junit.jupiter.api.Assertions.fail;
11 |
12 | class SystemExitAssertionTest {
13 |
14 | @Test
15 | void catchesExit() {
16 | assertThatCallsSystemExit(() -> System.exit(1));
17 | }
18 |
19 | @Test
20 | void catchesExitWithCode() {
21 | assertThatCallsSystemExit(() -> System.exit(2)).withExitCode(2);
22 | }
23 |
24 | @Test
25 | void catchesExitWithCodeInRange() {
26 | assertThatCallsSystemExit(() -> System.exit(3)).withExitCodeInRange(1, 3);
27 | }
28 |
29 | @Test
30 | void catchesMultipleExits() {
31 | assertThatCallsSystemExit(() -> {
32 | System.exit(4);
33 | System.exit(5);
34 | System.exit(6);
35 | }).withExitCode(4);
36 | }
37 |
38 | @Test
39 | void exitCodeDoesNotMatch() {
40 | try {
41 | assertThatCallsSystemExit(() -> System.exit(5)).withExitCode(6);
42 | fail("Should have failed test when System.exit was not called but expected");
43 | } catch (AssertionFailedError e) {
44 | assertThat(e.getMessage()).startsWith("Wrong exit code found");
45 | }
46 | }
47 |
48 | @Test
49 | void exitCodeNotInRangeHigh() {
50 | try {
51 | assertThatCallsSystemExit(() -> System.exit(7)).withExitCodeInRange(1, 6);
52 | fail("Should have failed test when System.exit was not in range");
53 | } catch (AssertionFailedError e) {
54 | assertThat(e.getMessage()).startsWith("Exit code expected in range (1 .. 6) but was 7");
55 | }
56 | }
57 |
58 | @Test
59 | void exitCodeNotInRangeLow() {
60 | try {
61 | assertThatCallsSystemExit(() -> System.exit(8)).withExitCodeInRange(9, 11);
62 | fail("Should have failed test when System.exit was not in range");
63 | } catch (AssertionFailedError e) {
64 | assertThat(e.getMessage()).startsWith("Exit code expected in range (9 .. 11) but was 8");
65 | }
66 | }
67 |
68 | @Test
69 | void expectingNoExit() {
70 | assertThatDoesNotCallSystemExit(System::currentTimeMillis);
71 | }
72 |
73 | @Test
74 | void expectingNoExitWhenExitHappens() {
75 | try {
76 | assertThatDoesNotCallSystemExit(() ->
77 | System.exit(9)
78 | );
79 | fail("Should have failed test when System.exit was called but not expected");
80 | } catch (AssertionFailedError e) {
81 | assertThat(e.getMessage()).startsWith("Unexpected call to System.exit()");
82 | }
83 | }
84 |
85 | @Test
86 | void expectingSystemExitButSomethingElseThrown() {
87 | try {
88 | assertThatCallsSystemExit(() -> {
89 | throw new IllegalStateException();
90 | }).withExitCode(10);
91 | } catch (final Exception e) {
92 | assertEquals(IllegalStateException.class, e.getCause().getClass());
93 | }
94 | }
95 |
96 | @Test
97 | void failsWhenNoExit() {
98 | try {
99 | assertThatCallsSystemExit(System::currentTimeMillis).withExitCode(11);
100 | fail("Should have failed test when System.exit was not called but expected");
101 | } catch (AssertionFailedError e) {
102 | assertThat(e.getMessage()).startsWith("Expected call to System.exit() did not happen");
103 | }
104 | }
105 | }
--------------------------------------------------------------------------------