├── .editorconfig
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .mvn
└── wrapper
│ ├── MavenWrapperDownloader.java
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── CHANGELOG.md
├── LICENSE
├── README.md
├── appveyor.yml
├── doc
└── Decision Records
│ └── 1 Changelog.md
├── findbugs-exclude.xml
├── mvnw
├── mvnw.cmd
├── pom.xml
├── scripts
├── mvn_in_docker.sh
└── test.sh
└── src
├── main
└── java
│ └── com
│ └── github
│ └── stefanbirkner
│ └── systemlambda
│ ├── Statement.java
│ └── SystemLambda.java
└── test
└── java
└── com
└── github
└── stefanbirkner
└── systemlambda
├── AssertNothingWrittenToSystemErrTest.java
├── AssertNothingWrittenToSystemOutTest.java
├── CallableMock.java
├── CatchSystemExitTest.java
├── MethodUnderTest.java
├── MuteSystemErrTest.java
├── MuteSystemOutTest.java
├── RestoreSystemErrChecks.java
├── RestoreSystemOutChecks.java
├── RestoreSystemPropertiesTest.java
├── SecurityManagerMock.java
├── StatementMock.java
├── TapSystemErrAndOutNormalizedTest.java
├── TapSystemErrAndOutTest.java
├── TapSystemErrNormalizedTest.java
├── TapSystemErrTest.java
├── TapSystemOutNormalizedTest.java
├── TapSystemOutTest.java
├── WithEnvironmentVariableTest.java
├── WithSecurityManagerTest.java
└── WithTextFromSystemInTest.java
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Configuration file for EditorConfig: http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = tab
8 | insert_final_newline = true
9 | tab_width = 4
10 | trim_trailing_whitespace = true
11 |
12 | [*.{sh,xml,yml}]
13 | indent_size = 2
14 | indent_style = space
15 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI with different Java versions
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-18.04
9 |
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v2
13 | - name: Build and Test
14 | run: scripts/test.sh
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .flattened-pom.xml
2 | target
3 |
--------------------------------------------------------------------------------
/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import java.net.*;
17 | import java.io.*;
18 | import java.nio.channels.*;
19 | import java.util.Properties;
20 |
21 | public class MavenWrapperDownloader {
22 |
23 | private static final String WRAPPER_VERSION = "0.5.6";
24 | /**
25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
26 | */
27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
29 |
30 | /**
31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
32 | * use instead of the default one.
33 | */
34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
35 | ".mvn/wrapper/maven-wrapper.properties";
36 |
37 | /**
38 | * Path where the maven-wrapper.jar will be saved to.
39 | */
40 | private static final String MAVEN_WRAPPER_JAR_PATH =
41 | ".mvn/wrapper/maven-wrapper.jar";
42 |
43 | /**
44 | * Name of the property which should be used to override the default download url for the wrapper.
45 | */
46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
47 |
48 | public static void main(String args[]) {
49 | System.out.println("- Downloader started");
50 | File baseDirectory = new File(args[0]);
51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
52 |
53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
54 | // wrapperUrl parameter.
55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
56 | String url = DEFAULT_DOWNLOAD_URL;
57 | if(mavenWrapperPropertyFile.exists()) {
58 | FileInputStream mavenWrapperPropertyFileInputStream = null;
59 | try {
60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
61 | Properties mavenWrapperProperties = new Properties();
62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
64 | } catch (IOException e) {
65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
66 | } finally {
67 | try {
68 | if(mavenWrapperPropertyFileInputStream != null) {
69 | mavenWrapperPropertyFileInputStream.close();
70 | }
71 | } catch (IOException e) {
72 | // Ignore ...
73 | }
74 | }
75 | }
76 | System.out.println("- Downloading from: " + url);
77 |
78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
79 | if(!outputFile.getParentFile().exists()) {
80 | if(!outputFile.getParentFile().mkdirs()) {
81 | System.out.println(
82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
83 | }
84 | }
85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
86 | try {
87 | downloadFileFromURL(url, outputFile);
88 | System.out.println("Done");
89 | System.exit(0);
90 | } catch (Throwable e) {
91 | System.out.println("- Error downloading");
92 | e.printStackTrace();
93 | System.exit(1);
94 | }
95 | }
96 |
97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
99 | String username = System.getenv("MVNW_USERNAME");
100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
101 | Authenticator.setDefault(new Authenticator() {
102 | @Override
103 | protected PasswordAuthentication getPasswordAuthentication() {
104 | return new PasswordAuthentication(username, password);
105 | }
106 | });
107 | }
108 | URL website = new URL(urlString);
109 | ReadableByteChannel rbc;
110 | rbc = Channels.newChannel(website.openStream());
111 | FileOutputStream fos = new FileOutputStream(destination);
112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
113 | fos.close();
114 | rbc.close();
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stefanbirkner/system-lambda/18e1f1ed60d1b4346371cdf09ad21c172f438309/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | This project adheres to
6 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 |
9 | ## 1.2.1 – 2021-12-28
10 |
11 | Replace `assertThrownBy` with `assertThrows` in documentation.
12 |
13 | `assertThrows` is the correct method name of JUnit Jupiter's assertion for
14 | exceptions. All examples use JUnit Jupiter for assertions.
15 |
16 |
17 | ## 1.2.0 – 2021-01-17
18 |
19 | Add methods for tapping `System.err` and `System.out` simultaneously.
20 |
21 | The output of command-line applications is usually the output to the standard
22 | output stream and the error output stream intermixed. The new methods allow
23 | capturing the intermixed output.
24 |
25 | @Test
26 | void application_writes_text_to_System_err_and_out(
27 | ) throws Exception {
28 | String text = tapSystemErrAndOut(() -> {
29 | System.err.print("text from err");
30 | System.out.print("text from out");
31 | });
32 | assertEquals("text from errtext from out", text);
33 | }
34 |
35 | @Test
36 | void application_writes_mutliple_lines_to_System_err_and_out(
37 | ) throws Exception {
38 | String text = tapSystemErrAndOutNormalized(() -> {
39 | System.err.println("text from err");
40 | System.out.println("text from out");
41 | });
42 | assertEquals("text from err\ntext from out\n", text);
43 | }
44 |
45 |
46 | ## 1.1.1 – 2020-10-12
47 |
48 | Fix examples for tapping `System.err` and `System.out`.
49 |
50 |
51 | ## 1.1.0 – 2020-08-17
52 |
53 | Support Callable for `withEnvironmentVariable`.
54 |
55 | This allows to write more precise and readable tests. The assertion can be
56 | outside the `withEnvironmentVariable` statement and `execute` can be called with
57 | a one-line Lambda expression. E.g.
58 |
59 | @Test
60 | void execute_code_with_environment_variables(
61 | ) throws Exception {
62 | String value = withEnvironmentVariable("key", "the value")
63 | .execute(() -> System.getenv("key"));
64 | assertEquals("the value", value);
65 | }
66 |
67 |
68 | ## 1.0.0 – 2020-05-17
69 |
70 | Initial release with the methods
71 |
72 | - assertNothingWrittenToSystemErr
73 | - assertNothingWrittenToSystemOut
74 | - catchSystemExit
75 | - muteSystemErr
76 | - muteSystemOut
77 | - restoreSystemProperties
78 | - tapSystemErr
79 | - tapSystemErrNormalized
80 | - tapSystemOut
81 | - tapSystemOutNormalized
82 | - withEnvironmentVariable
83 | - withSecurityManager
84 | - withTextFromSystemIn
85 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Stefan Birkner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # System Lambda
2 |
3 |  [](https://ci.appveyor.com/project/stefanbirkner/system-lambda)
4 |
5 | System Lambda is a collection of functions for testing code which uses
6 | `java.lang.System`.
7 |
8 | System Lambda is published under the
9 | [MIT license](http://opensource.org/licenses/MIT). It requires at least Java 8.
10 |
11 | For JUnit 4 there is an alternative to System Lambda. Its name is
12 | [System Rules](http://stefanbirkner.github.io/system-rules/index.html).
13 |
14 | ## Installation
15 |
16 | System Lambda is available from
17 | [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.github.stefanbirkner%22%20AND%20a%3A%22system-lambda%22).
18 |
19 | ```xml
20 |
21 | com.github.stefanbirkner
22 | system-lambda
23 | 1.2.1
24 |
25 | ```
26 |
27 | Please don't forget to add the scope `test` if you're using System Lambda for
28 | tests only. The [changelog](CHANGELOG.md) lists all the changes of System
29 | Lambda.
30 |
31 |
32 | ## Usage
33 |
34 | Import System Lambda's functions by adding
35 |
36 | ```java
37 | import static com.github.stefanbirkner.systemlambda.SystemLambda.*;
38 | ```
39 |
40 | to your tests.
41 |
42 |
43 | ### System.exit
44 |
45 | Command-line applications terminate by calling `System.exit` with some status
46 | code. If you test such an application then the JVM that executes the test exits
47 | when the application under test calls `System.exit`. You can avoid this with
48 | the method `catchSystemExit` which also returns the status code of the
49 | `System.exit` call.
50 |
51 | ```java
52 | @Test
53 | void application_exits_with_status_42(
54 | ) throws Exception {
55 | int statusCode = catchSystemExit(() -> {
56 | System.exit(42);
57 | });
58 | assertEquals(42, statusCode);
59 | }
60 | ```
61 |
62 | The method `catchSystemExit` throws an `AssertionError` if the code under test
63 | does not call `System.exit`. Therefore your test fails with the failure message
64 | "System.exit has not been called."
65 |
66 |
67 | ### Environment Variables
68 |
69 | The method `withEnvironmentVariable` allows you to set environment variables
70 | within your test code that are removed after your code under test is executed.
71 |
72 | ```java
73 | @Test
74 | void execute_code_with_environment_variables(
75 | ) throws Exception {
76 | List values = withEnvironmentVariable("first", "first value")
77 | .and("second", "second value")
78 | .execute(() -> asList(
79 | System.getenv("first"),
80 | System.getenv("second")
81 | ));
82 | assertEquals(
83 | asList("first value", "second value"),
84 | values
85 | );
86 | }
87 | ```
88 |
89 |
90 | ### System Properties
91 |
92 | The method `restoreSystemProperties` guarantees that after executing the test
93 | code each System property has the same value like before. Therefore you can
94 | modify System properties inside of the test code without having an impact on
95 | other tests.
96 |
97 | ```java
98 | @Test
99 | void execute_code_that_manipulates_system_properties(
100 | ) throws Exception {
101 | restoreSystemProperties(() -> {
102 | System.setProperty("some.property", "some value");
103 | //code under test that reads properties (e.g. "some.property") or
104 | //modifies them.
105 | });
106 |
107 | //Here the value of "some.property" is the same like before.
108 | //E.g. it is not set.
109 | }
110 | ```
111 |
112 |
113 | ### System.out and System.err
114 |
115 | Command-line applications usually write to the console. If you write such
116 | applications you need to test the output of these applications. The methods
117 | `tapSystemErr`, `tapSystemErrNormalized`, `tapSystemOut`,
118 | `tapSystemOutNormalized`, `tapSystemErrAndOut` and
119 | `tapSystemErrAndOutNormalized` allow you to tap the text that is written to
120 | `System.err`/`System.out`. The methods with the suffix `Normalized` normalize
121 | line breaks to `\n` so that you can run tests with the same assertions on
122 | different operating systems.
123 |
124 | ```java
125 | @Test
126 | void application_writes_text_to_System_err(
127 | ) throws Exception {
128 | String text = tapSystemErr(() -> {
129 | System.err.print("some text");
130 | });
131 | assertEquals("some text", text);
132 | }
133 |
134 | @Test
135 | void application_writes_mutliple_lines_to_System_err(
136 | ) throws Exception {
137 | String text = tapSystemErrNormalized(() -> {
138 | System.err.println("first line");
139 | System.err.println("second line");
140 | });
141 | assertEquals("first line\nsecond line\n", text);
142 | }
143 |
144 | @Test
145 | void application_writes_text_to_System_out(
146 | ) throws Exception {
147 | String text = tapSystemOut(() -> {
148 | System.out.print("some text");
149 | });
150 | assertEquals("some text", text);
151 | }
152 |
153 | @Test
154 | void application_writes_mutliple_lines_to_System_out(
155 | ) throws Exception {
156 | String text = tapSystemOutNormalized(() -> {
157 | System.out.println("first line");
158 | System.out.println("second line");
159 | });
160 | assertEquals("first line\nsecond line\n", text);
161 | }
162 |
163 | @Test
164 | void application_writes_text_to_System_err_and_out(
165 | ) throws Exception {
166 | String text = tapSystemErrAndOut(() -> {
167 | System.err.print("text from err");
168 | System.out.print("text from out");
169 | });
170 | assertEquals("text from errtext from out", text);
171 | }
172 |
173 | @Test
174 | void application_writes_mutliple_lines_to_System_err_and_out(
175 | ) throws Exception {
176 | String text = tapSystemErrAndOutNormalized(() -> {
177 | System.err.println("text from err");
178 | System.out.println("text from out");
179 | });
180 | assertEquals("text from err\ntext from out\n", text);
181 | }
182 | ```
183 |
184 | You can assert that nothing is written to `System.err`/`System.out` by wrapping
185 | code with the function
186 | `assertNothingWrittenToSystemErr`/`assertNothingWrittenToSystemOut`. E.g. the
187 | following tests fail:
188 |
189 | ```java
190 | @Test
191 | void fails_because_something_is_written_to_System_err(
192 | ) throws Exception {
193 | assertNothingWrittenToSystemErr(() -> {
194 | System.err.println("some text");
195 | });
196 | }
197 |
198 | @Test
199 | void fails_because_something_is_written_to_System_out(
200 | ) throws Exception {
201 | assertNothingWrittenToSystemOut(() -> {
202 | System.out.println("some text");
203 | });
204 | }
205 | ```
206 |
207 | If the code under test writes text to `System.err`/`System.out` then it is
208 | intermixed with the output of your build tool. Therefore you may want to avoid
209 | that the code under test writes to `System.err`/`System.out`. You can achieve
210 | this with the function `muteSystemErr`/`muteSystemOut`. E.g. the following tests
211 | don't write anything to `System.err`/`System.out`:
212 |
213 | ```java
214 | @Test
215 | void nothing_is_written_to_System_err(
216 | ) throws Exception {
217 | muteSystemErr(() -> {
218 | System.err.println("some text");
219 | });
220 | }
221 |
222 | @Test
223 | void nothing_is_written_to_System_out(
224 | ) throws Exception {
225 | muteSystemOut(() -> {
226 | System.out.println("some text");
227 | });
228 | }
229 | ```
230 |
231 | ### System.in
232 |
233 | Interactive command-line applications read from `System.in`. If you write such
234 | applications you need to provide input to these applications. You can specify
235 | the lines that are available from `System.in` with the method
236 | `withTextFromSystemIn`
237 |
238 | ```java
239 | @Test
240 | void Scanner_reads_text_from_System_in(
241 | ) throws Exception {
242 | withTextFromSystemIn("first line", "second line")
243 | .execute(() -> {
244 | Scanner scanner = new Scanner(System.in);
245 | scanner.nextLine();
246 | assertEquals("second line", scanner.nextLine());
247 | });
248 | }
249 | ```
250 |
251 | For a complete test coverage you may also want to simulate `System.in` throwing
252 | exceptions when the application reads from it. You can specify such an
253 | exception (either `RuntimeException` or `IOException`) after specifying the
254 | text. The exception will be thrown by the next `read` after the text has been
255 | consumed.
256 |
257 | ```java
258 | @Test
259 | void System_in_throws_IOException(
260 | ) throws Exception {
261 | withTextFromSystemIn("first line", "second line")
262 | .andExceptionThrownOnInputEnd(new IOException())
263 | .execute(() -> {
264 | Scanner scanner = new Scanner(System.in);
265 | scanner.nextLine();
266 | scanner.nextLine();
267 | assertThrows(
268 | IOException.class,
269 | () -> scanner.readLine()
270 | );
271 | });
272 | }
273 |
274 | @Test
275 | void System_in_throws_RuntimeException(
276 | ) throws Exception {
277 | withTextFromSystemIn("first line", "second line")
278 | .andExceptionThrownOnInputEnd(new RuntimeException())
279 | .execute(() -> {
280 | Scanner scanner = new Scanner(System.in);
281 | scanner.nextLine();
282 | scanner.nextLine();
283 | assertThrows(
284 | RuntimeException.class,
285 | () -> scanner.readLine()
286 | );
287 | });
288 | }
289 | ```
290 |
291 | You can write a test that throws an exception immediately by not providing any
292 | text.
293 |
294 | ```java
295 | withTextFromSystemIn()
296 | .andExceptionThrownOnInputEnd(...)
297 | .execute(() -> {
298 | Scanner scanner = new Scanner(System.in);
299 | assertThrows(
300 | ...,
301 | () -> scanner.readLine()
302 | );
303 | });
304 | ```
305 |
306 | ### Security Manager
307 |
308 | The function `withSecurityManager` lets you specify the `SecurityManager` that
309 | is returned by `System.getSecurityManger()` while your code under test is
310 | executed.
311 |
312 | ```java
313 | @Test
314 | void execute_code_with_specific_SecurityManager(
315 | ) throws Exception {
316 | SecurityManager securityManager = new ASecurityManager();
317 | withSecurityManager(
318 | securityManager,
319 | () -> {
320 | //code under test
321 | //e.g. the following assertion is met
322 | assertSame(
323 | securityManager,
324 | System.getSecurityManager()
325 | );
326 | }
327 | );
328 | }
329 | ```
330 |
331 | After `withSecurityManager(...)` is executed`System.getSecurityManager()`
332 | returns the original security manager again.
333 |
334 |
335 | ## Contributing
336 |
337 | You have three options if you have a feature request, found a bug or
338 | simply have a question about System Lambda.
339 |
340 | * [Write an issue.](https://github.com/stefanbirkner/system-lambda/issues/new)
341 | * Create a pull request. (See [Understanding the GitHub Flow](https://guides.github.com/introduction/flow/index.html))
342 | * [Write a mail to mail@stefan-birkner.de](mailto:mail@stefan-birkner.de)
343 |
344 |
345 | ## Development Guide
346 |
347 | System Lambda is built with [Maven](http://maven.apache.org/) and must be
348 | compiled under JDK 8. If you want to contribute code then
349 |
350 | * Please write a test for your change.
351 | * Ensure that you didn't break the build by running `mvnw clean verify -Dgpg.skip`.
352 | * Fork the repo and create a pull request. (See [Understanding the GitHub Flow](https://guides.github.com/introduction/flow/index.html))
353 |
354 | The basic coding style is described in the
355 | [EditorConfig](http://editorconfig.org/) file `.editorconfig`.
356 |
357 | System Lambda supports [GitHub Actions](https://help.github.com/en/actions)
358 | (Linux) and [AppVeyor](http://www.appveyor.com/) (Windows) for continuous
359 | integration. Each pull request is automatically built by both CI servers.
360 | GitHub Actions tests with several Java versions (see Enhanced Testing) while
361 | AppVeyor tests with Java 8 only.
362 |
363 | ### Project Decisions
364 |
365 | There are decision records for some decisions that had been made for System
366 | Lambda. They are stored in the folder
367 | [doc/Decision Records](doc/Decision%20Records)
368 |
369 | ### Build with Docker
370 |
371 | The script
372 |
373 | ./scripts/mvn_in_docker.sh clean verify -Dgpg.skip
374 |
375 | builds System Lambda inside a Docker container. It uses your Maven local
376 | repository. This is helpful if you have a Linux or macOS without JDK 8.
377 |
378 | ### Enhanced Testing
379 |
380 | System Lambda is built with Java 8 and relies on JDK internals that are not
381 | available from Java 16 on (unless you run Java with additional flags). We verify
382 | that System Lambda works with newer Java versions by building it with OpenJDK 8
383 | and executing tests with newer versions of OpenJDK. All this work is put into a
384 | script that you can run with
385 |
386 | ./scripts/test.sh
387 |
388 | The script uses Docker for running the tests.
389 |
390 |
391 | ## Release Guide
392 |
393 | * Select a new version according to the
394 | [Semantic Versioning 2.0.0 Standard](http://semver.org/).
395 | * Update `Changelog.md`.
396 | * Set the new version in `pom.xml` and in the `Installation` section of
397 | this readme.
398 | * Commit the modified `Changelog.md`, `pom.xml` and `README.md`.
399 | * Run `mvnw clean deploy` with JDK 8.
400 | * Add a tag for the release: `git tag system-lambda-X.X.X`
401 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: '{build}'
2 | os: Windows Server 2012
3 | build_script:
4 | - mvnw clean package -DskipTest -Dgpg.skip
5 | test_script:
6 | - mvnw clean verify -Dgpg.skip
7 | cache:
8 | - C:\Users\appveyor\.m2
9 |
--------------------------------------------------------------------------------
/doc/Decision Records/1 Changelog.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | Status: Accepted
4 |
5 |
6 | ## Context
7 |
8 | Sometimes I (Stefan) publish a new release of System Lambda.
9 |
10 | Users find out about new releases, e.g. because I tweet about it or their
11 | dependency management tool tells them about a new version. They want to know
12 | what changed in the new version (or maybe several versions). Some are curious
13 | about new features or bugfixes. Others need this information in order to decide
14 | whether they change their software to use a newer version of System Lambda or
15 | not.
16 |
17 |
18 | ## Decision
19 |
20 | I provide a changelog that is stored in the file CHANGELOG.md.
21 |
22 | A curated list of the changes is the fastest way for users to find out about
23 | changes in System Lambda.
24 |
25 | CHANGELOG.md is a common filename for changelogs. Users who are used to this
26 | convention may look at this file without further guideline. For other users
27 | there is a link to the changelog in the readme file.
28 |
29 | The file is a text file that uses Markdown, because it provides lightweight text
30 | formatting that is nicely rendered by some viewers (e.g. GitHub which hosts
31 | this project) while it still can be viewed and edited by standard text editors.
32 |
33 |
34 | ## Consequences
35 |
36 | Developers and project stakeholders can see the changes without viewing the git
37 | log.
38 |
39 | The changelog must be updated before a new release is published.
40 |
41 | Information may be duplicated between docs, changelog and git log.
42 |
--------------------------------------------------------------------------------
/findbugs-exclude.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Mingw, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | fi
118 |
119 | if [ -z "$JAVA_HOME" ]; then
120 | javaExecutable="`which javac`"
121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
122 | # readlink(1) is not available as standard on Solaris 10.
123 | readLink=`which readlink`
124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
125 | if $darwin ; then
126 | javaHome="`dirname \"$javaExecutable\"`"
127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
128 | else
129 | javaExecutable="`readlink -f \"$javaExecutable\"`"
130 | fi
131 | javaHome="`dirname \"$javaExecutable\"`"
132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
133 | JAVA_HOME="$javaHome"
134 | export JAVA_HOME
135 | fi
136 | fi
137 | fi
138 |
139 | if [ -z "$JAVACMD" ] ; then
140 | if [ -n "$JAVA_HOME" ] ; then
141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
142 | # IBM's JDK on AIX uses strange locations for the executables
143 | JAVACMD="$JAVA_HOME/jre/sh/java"
144 | else
145 | JAVACMD="$JAVA_HOME/bin/java"
146 | fi
147 | else
148 | JAVACMD="`which java`"
149 | fi
150 | fi
151 |
152 | if [ ! -x "$JAVACMD" ] ; then
153 | echo "Error: JAVA_HOME is not defined correctly." >&2
154 | echo " We cannot execute $JAVACMD" >&2
155 | exit 1
156 | fi
157 |
158 | if [ -z "$JAVA_HOME" ] ; then
159 | echo "Warning: JAVA_HOME environment variable is not set."
160 | fi
161 |
162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
163 |
164 | # traverses directory structure from process work directory to filesystem root
165 | # first directory with .mvn subdirectory is considered project base directory
166 | find_maven_basedir() {
167 |
168 | if [ -z "$1" ]
169 | then
170 | echo "Path not specified to find_maven_basedir"
171 | return 1
172 | fi
173 |
174 | basedir="$1"
175 | wdir="$1"
176 | while [ "$wdir" != '/' ] ; do
177 | if [ -d "$wdir"/.mvn ] ; then
178 | basedir=$wdir
179 | break
180 | fi
181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
182 | if [ -d "${wdir}" ]; then
183 | wdir=`cd "$wdir/.."; pwd`
184 | fi
185 | # end of workaround
186 | done
187 | echo "${basedir}"
188 | }
189 |
190 | # concatenates all lines of a file
191 | concat_lines() {
192 | if [ -f "$1" ]; then
193 | echo "$(tr -s '\n' ' ' < "$1")"
194 | fi
195 | }
196 |
197 | BASE_DIR=`find_maven_basedir "$(pwd)"`
198 | if [ -z "$BASE_DIR" ]; then
199 | exit 1;
200 | fi
201 |
202 | ##########################################################################################
203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
204 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
205 | ##########################################################################################
206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
207 | if [ "$MVNW_VERBOSE" = true ]; then
208 | echo "Found .mvn/wrapper/maven-wrapper.jar"
209 | fi
210 | else
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
213 | fi
214 | if [ -n "$MVNW_REPOURL" ]; then
215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
216 | else
217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
218 | fi
219 | while IFS="=" read key value; do
220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
221 | esac
222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
223 | if [ "$MVNW_VERBOSE" = true ]; then
224 | echo "Downloading from: $jarUrl"
225 | fi
226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
227 | if $cygwin; then
228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
229 | fi
230 |
231 | if command -v wget > /dev/null; then
232 | if [ "$MVNW_VERBOSE" = true ]; then
233 | echo "Found wget ... using wget"
234 | fi
235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
236 | wget "$jarUrl" -O "$wrapperJarPath"
237 | else
238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
239 | fi
240 | elif command -v curl > /dev/null; then
241 | if [ "$MVNW_VERBOSE" = true ]; then
242 | echo "Found curl ... using curl"
243 | fi
244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
245 | curl -o "$wrapperJarPath" "$jarUrl" -f
246 | else
247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
248 | fi
249 |
250 | else
251 | if [ "$MVNW_VERBOSE" = true ]; then
252 | echo "Falling back to using Java to download"
253 | fi
254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
255 | # For Cygwin, switch paths to Windows format before running javac
256 | if $cygwin; then
257 | javaClass=`cygpath --path --windows "$javaClass"`
258 | fi
259 | if [ -e "$javaClass" ]; then
260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
261 | if [ "$MVNW_VERBOSE" = true ]; then
262 | echo " - Compiling MavenWrapperDownloader.java ..."
263 | fi
264 | # Compiling the Java class
265 | ("$JAVA_HOME/bin/javac" "$javaClass")
266 | fi
267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
268 | # Running the downloader
269 | if [ "$MVNW_VERBOSE" = true ]; then
270 | echo " - Running MavenWrapperDownloader.java ..."
271 | fi
272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
273 | fi
274 | fi
275 | fi
276 | fi
277 | ##########################################################################################
278 | # End of extension
279 | ##########################################################################################
280 |
281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
282 | if [ "$MVNW_VERBOSE" = true ]; then
283 | echo $MAVEN_PROJECTBASEDIR
284 | fi
285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
286 |
287 | # For Cygwin, switch paths to Windows format before running java
288 | if $cygwin; then
289 | [ -n "$M2_HOME" ] &&
290 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
291 | [ -n "$JAVA_HOME" ] &&
292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
293 | [ -n "$CLASSPATH" ] &&
294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
295 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
297 | fi
298 |
299 | # Provide a "standardized" way to retrieve the CLI args that will
300 | # work with both Windows and non-Windows executions.
301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
302 | export MAVEN_CMD_LINE_ARGS
303 |
304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
305 |
306 | exec "$JAVACMD" \
307 | $MAVEN_OPTS \
308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
311 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM http://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 | com.github.stefanbirkner
7 | lib-parent
8 | 16
9 |
10 |
11 | system-lambda
12 | 1.2.1
13 | jar
14 |
15 | System Lambda
16 | A collection of functions for testing code which uses java.lang.System.
17 | https://github.com/stefanbirkner/system-lambda/
18 | 2020
19 |
20 |
21 | MIT License
22 | http://opensource.org/licenses/MIT
23 | repo
24 |
25 |
26 |
27 |
28 | scm:git:git://github.com/stefanbirkner/system-lambda.git
29 | scm:git:git@github.com:stefanbirkner/system-lambda.git
30 | https://github.com/stefanbirkner/system-lambda/
31 |
32 |
33 |
34 | findbugs-exclude.xml
35 | 1.8
36 | 1.8
37 |
38 |
39 |
40 |
41 | com.github.stefanbirkner
42 | fishbowl
43 | 1.4.1
44 | test
45 |
46 |
47 | org.apache.commons
48 | commons-lang3
49 | 3.10
50 | test
51 |
52 |
53 | org.assertj
54 | assertj-core
55 | 3.16.1
56 | test
57 |
58 |
59 | org.junit.jupiter
60 | junit-jupiter
61 | 5.6.2
62 | test
63 |
64 |
65 |
66 |
67 |
68 |
69 | maven-surefire-plugin
70 | 3.0.0-M5
71 |
72 | 1
73 | false
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/scripts/mvn_in_docker.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # Executes Maven inside a Docker container with Java 8. The image has to be
4 | # executed with options, phases and goals just like the original mvn command.
5 | # E.g. instead of "mvn verify" you have to execute
6 | #
7 | # scripts/mvn_in_docker.sh verify
8 | #
9 | # from the project directory. The user's Maven local repository is used by the
10 | # container.
11 |
12 | set -euxo pipefail
13 |
14 | readonly group=`id -g`
15 | readonly user=`id -u`
16 |
17 | mkdir -p ~/.m2
18 | docker run \
19 | --rm \
20 | -e MAVEN_CONFIG=/var/maven/.m2 \
21 | -u "$user:$group" \
22 | -v "$(pwd)":/usr/src/system-lambda \
23 | -v ~/.m2:/var/maven/.m2 \
24 | -w /usr/src/system-lambda \
25 | maven:3.8.1-openjdk-8-slim \
26 | mvn -Duser.home=/var/maven "$@"
27 | rm -r '?/' # I don't know why this directory is created.
28 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | #
3 | # Verify that System Lambda can be used with different JDKs.
4 |
5 | set -euo pipefail
6 |
7 | readonly group=`id -g`
8 | readonly user=`id -u`
9 | readonly test_dir="target/test_in_docker"
10 |
11 | build_System_Lambda()
12 | {
13 | ./scripts/mvn_in_docker.sh -Dgpg.skip clean install
14 | }
15 |
16 | replace_line()
17 | {
18 | local file=$1
19 | local line_number=$2
20 | local replacement=$3
21 |
22 | sed \
23 | -i \
24 | "${line_number}s/.*/${replacement}/" \
25 | $file
26 | }
27 |
28 | clear_line()
29 | {
30 | local file=$1
31 | local line_number=$2
32 |
33 | replace_line $file $line_number ""
34 | }
35 |
36 | clear_lines()
37 | {
38 | local file=$1
39 | local range=($(echo $2 | tr "-" "\n"))
40 | local start=${range[0]}
41 | local end=${range[1]}
42 |
43 | for line_number in $(seq $start $end)
44 | do
45 | clear_line "$file" "$line_number"
46 | done
47 | }
48 |
49 | delete_line()
50 | {
51 | local file=$1
52 | local line_number=$2
53 |
54 | sed -i "${line_number}d" $file
55 | }
56 |
57 | delete_lines()
58 | {
59 | local file=$1
60 | local range=($(echo $2 | tr "-" "\n"))
61 | local start=${range[0]}
62 | local end=${range[1]}
63 |
64 | for line_number in $(seq $start $end)
65 | do
66 | delete_line "$file" "$start"
67 | done
68 | }
69 |
70 | insert_line()
71 | {
72 | local file=$1
73 | local line_number=$2
74 | local line=$3
75 |
76 | sed -i "${line_number}i\\${line}\\" $file
77 | }
78 |
79 | # The purpose of the test pom.xml is to run the System Lambda tests against a
80 | # System Lambda that was build with Java 8. The Java 8 System Lambda has to be
81 | # published to the local Maven repository. The test pom.xml has a dependency to
82 | # this artifact and also the original test dependencies of System Lambda.
83 | # I decided to build the test pom.xml by copying and modifying System Lambda's
84 | # pom.xml so that I keep System Lambda's test setup.
85 | create_test_pom()
86 | {
87 | local java_version=$1
88 | local test_pom="$test_dir/pom.xml"
89 |
90 | cp pom.xml $test_pom
91 |
92 | # Delete everything after 4.0.0 and before the
93 | # first dependency except groupId, artifactId and version of System Lambda.
94 | delete_line $test_pom 5 # Line with
95 | delete_lines $test_pom "6-9"
96 | delete_lines $test_pom "8-34"
97 |
98 | # Wrap groupId, artifactId and version of System Lambda with
99 | # ...
100 | insert_line $test_pom 5 " "
101 | insert_line $test_pom 9 " "
102 |
103 | # Add pom.xml "header"
104 | insert_line $test_pom 5 " dummy"
105 | insert_line $test_pom 6 " dummy"
106 | insert_line $test_pom 7 " 0-SNAPSHOT"
107 | insert_line $test_pom 8 " jar"
108 | insert_line $test_pom 9 ""
109 | insert_line $test_pom 10 " "
110 | insert_line $test_pom 11 " ${java_version}"
111 | insert_line $test_pom 12 " ${java_version}"
112 | insert_line $test_pom 13 " UTF-8"
113 | insert_line $test_pom 14 " "
114 | insert_line $test_pom 15 ""
115 | insert_line $test_pom 16 " "
116 | }
117 |
118 | # Some methods of SecurityManager had been removed after Java 8. We need to
119 | # delete code that calls or overrides these methods so that we can compile the
120 | # tests with newer JDKs.
121 | remove_code_from_test_classes_that_does_not_work_with_newer_JDKs()
122 | {
123 | local dir_with_classes="$test_dir/src/test/java/com/github/stefanbirkner/systemlambda"
124 |
125 | # Some methods of SecurityManager had been removed after Java 8. We need to
126 | # delete code that calls or overrides these methods so that we can compile
127 | # the tests with newer JDKs.
128 | clear_lines "${dir_with_classes}/CatchSystemExitTest.java" "242-330"
129 | clear_lines "${dir_with_classes}/SecurityManagerMock.java" "158-162"
130 | clear_lines "${dir_with_classes}/SecurityManagerMock.java" "164-168"
131 | clear_lines "${dir_with_classes}/SecurityManagerMock.java" "203-210"
132 | clear_lines "${dir_with_classes}/SecurityManagerMock.java" "224-228"
133 | clear_lines "${dir_with_classes}/SecurityManagerMock.java" "236-241"
134 | }
135 |
136 | copy_test_classes()
137 | {
138 | mkdir -p "$test_dir/src"
139 | cp -R src/test "$test_dir/src/test"
140 | }
141 |
142 | create_test_project()
143 | {
144 | local java_version=$1
145 |
146 | copy_test_classes
147 | remove_code_from_test_classes_that_does_not_work_with_newer_JDKs
148 | create_test_pom $java_version
149 | }
150 |
151 | print_headline()
152 | {
153 | local java_version=$1
154 | set +u
155 | local options="$2"
156 | set -u
157 |
158 | echo ""
159 | echo "Test System Lambda with Java $java_version $options"
160 | echo ""
161 | }
162 |
163 | test_with_JDK()
164 | {
165 | local java_version=$1
166 | set +u
167 | local options="$2"
168 | set -u
169 |
170 | print_headline $java_version $options
171 | create_test_project $java_version
172 | docker run \
173 | --rm \
174 | -e MAVEN_CONFIG=/var/maven/.m2 \
175 | -u "$user:$group" \
176 | -v "$(pwd)/$test_dir":/usr/src/system-lambda \
177 | -v ~/.m2:/var/maven/.m2 \
178 | -w /usr/src/system-lambda \
179 | "maven:3.8.3-openjdk-$java_version-slim" \
180 | mvn -Dgpg.skip \
181 | -D"maven.compiler.source=$java_version" \
182 | -D"maven.compiler.target=$java_version" \
183 | -Duser.home=/var/maven \
184 | clean test $options
185 | }
186 |
187 | # We test with all LTS releases and the latest non-LTS release
188 | build_System_Lambda
189 | test_with_JDK 11
190 | test_with_JDK 17
191 | test_with_JDK 17 "-DargLine=--add-opens=java.base/java.util=ALL-UNNAMED"
192 |
--------------------------------------------------------------------------------
/src/main/java/com/github/stefanbirkner/systemlambda/Statement.java:
--------------------------------------------------------------------------------
1 | package com.github.stefanbirkner.systemlambda;
2 |
3 | /**
4 | * Code that should be executed by on of the methods of {@link SystemLambda}.
5 | * This code may throw an {@link Exception}. Therefore we cannot use
6 | * {@link Runnable}.
7 | */
8 | public interface Statement {
9 | /**
10 | * Execute the statement.
11 | *
12 | * @throws Exception the statement may throw an arbitrary exception.
13 | */
14 | void execute() throws Exception;
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/github/stefanbirkner/systemlambda/SystemLambda.java:
--------------------------------------------------------------------------------
1 | package com.github.stefanbirkner.systemlambda;
2 |
3 | import java.io.*;
4 | import java.lang.reflect.Field;
5 | import java.net.InetAddress;
6 | import java.security.Permission;
7 | import java.util.*;
8 | import java.util.concurrent.Callable;
9 |
10 | import static java.lang.Class.forName;
11 | import static java.lang.System.*;
12 | import static java.nio.charset.Charset.defaultCharset;
13 | import static java.util.Arrays.stream;
14 | import static java.util.Collections.singletonMap;
15 | import static java.util.stream.Collectors.joining;
16 |
17 | /**
18 | * {@code SystemLambda} is a collection of functions for testing code
19 | * that uses {@code java.lang.System}.
20 | *
21 | *
System.exit
22 | *
Command-line applications terminate by calling {@code System.exit} with
23 | * some status code. If you test such an application then the JVM that runs the
24 | * test exits when the application under test calls {@code System.exit}. You can
25 | * avoid this with the method
26 | * {@link #catchSystemExit(Statement) catchSystemExit} which also returns the
27 | * status code of the {@code System.exit} call.
28 | *
29 | *
39 | *
40 | * The method {@code catchSystemExit} throws an {@code AssertionError} if the
41 | * code under test does not call {@code System.exit}. Therefore your test fails
42 | * with the failure message "System.exit has not been called."
43 | *
44 | *
Environment Variables
45 | *
46 | *
The method
47 | * {@link #withEnvironmentVariable(String, String) withEnvironmentVariable}
48 | * allows you to set environment variables within your test code that are
49 | * removed after your code under test is executed.
50 | *
The function
69 | * {@link #restoreSystemProperties(Statement) restoreSystemProperties}
70 | * guarantees that after executing the test code each System property has the
71 | * same value like before. Therefore you can modify System properties inside of
72 | * the test code without having an impact on other tests.
73 | *
Command-line applications usually write to the console. If you write such
87 | * applications you need to test the output of these applications. The methods
88 | * {@link #tapSystemErr(Statement) tapSystemErr},
89 | * {@link #tapSystemErrNormalized(Statement) tapSystemErrNormalized},
90 | * {@link #tapSystemOut(Statement) tapSystemOut},
91 | * {@link #tapSystemOutNormalized(Statement) tapSystemOutNormalized},
92 | * {@link #tapSystemErrAndOut(Statement) tapSystemErrAndOut} and
93 | * {@link #tapSystemErrAndOutNormalized(Statement) tapSystemErrAndOutNormalized}
94 | * allow you
95 | * to tap the text that is written to {@code System.err}/{@code System.out}. The
96 | * methods with the suffix {@code Normalized} normalize line breaks to
97 | * {@code \n} so that you can run tests with the same assertions on different
98 | * operating systems.
99 | *
100 | *
You can assert that nothing is written to
160 | * {@code System.err}/{@code System.out} by wrapping code with the function
161 | * {@link #assertNothingWrittenToSystemErr(Statement)
162 | * assertNothingWrittenToSystemErr}/{@link #assertNothingWrittenToSystemOut(Statement)
163 | * assertNothingWrittenToSystemOut}. E.g. the following tests fail:
164 | *
If the code under test writes text to
183 | * {@code System.err}/{@code System.out} then it is intermixed with the output
184 | * of your build tool. Therefore you may want to avoid that the code under test
185 | * writes to {@code System.err}/{@code System.out}. You can achieve this with
186 | * the function {@link #muteSystemErr(Statement)
187 | * muteSystemErr}/{@link #muteSystemOut(Statement) muteSystemOut}. E.g. the
188 | * following tests don't write anything to
189 | * {@code System.err}/{@code System.out}:
190 | *
Interactive command-line applications read from {@code System.in}. If you
211 | * write such applications you need to provide input to these applications. You
212 | * can specify the lines that are available from {@code System.in} with the
213 | * method {@link #withTextFromSystemIn(String...) withTextFromSystemIn}
214 | *
For complete test coverage you may also want to simulate {@code System.in}
228 | * throwing exceptions when the application reads from it. You can specify such
229 | * an exception (either {@code RuntimeException} or {@code IOException}) after
230 | * specifying the text. The exception will be thrown by the next {@code read}
231 | * after the text has been consumed.
232 | *
The function
283 | * {@link #withSecurityManager(SecurityManager, Statement) withSecurityManager}
284 | * lets you specify the {@code SecurityManager} that is returned by
285 | * {@code System.getSecurityManger()} while your code under test is executed.
286 | *
358 | * The test fails with the failure "Tried to write 's' to System.out
359 | * although this is not allowed."
360 | *
361 | * @param statement an arbitrary piece of code.
362 | * @throws AssertionError if the statements tries to write to
363 | * {@code System.out}.
364 | * @throws Exception any exception thrown by the statement.
365 | * @see #assertNothingWrittenToSystemErr(Statement)
366 | * @since 1.0.0
367 | */
368 | public static void assertNothingWrittenToSystemOut(
369 | Statement statement
370 | ) throws Exception {
371 | executeWithSystemOutReplacement(
372 | new DisallowWriteStream(),
373 | statement
374 | );
375 | }
376 |
377 | /**
378 | * Executes the statement and returns the status code that is provided to
379 | * {@code System.exit(int)} within the statement. Additionally it avoids
380 | * that the JVM is shut down because of a call to {@code System.exit(int)}.
381 | *
391 | * @param statement an arbitrary piece of code.
392 | * @return the status code provided to {@code System.exit(int)}.
393 | * @throws AssertionError if the statement does not call
394 | * {@code System.exit(int)}.
395 | * @throws Exception any exception thrown by the statement.
396 | * @since 1.0.0
397 | */
398 | public static int catchSystemExit(
399 | Statement statement
400 | ) throws Exception {
401 | NoExitSecurityManager noExitSecurityManager
402 | = new NoExitSecurityManager(getSecurityManager());
403 | try {
404 | withSecurityManager(noExitSecurityManager, statement);
405 | } catch (CheckExitCalled ignored) {
406 | }
407 | return checkSystemExit(noExitSecurityManager);
408 | }
409 |
410 | /**
411 | * Executes the statement and suppresses the output of the statement to
412 | * {@code System.err}. Use this to avoid that the output of your build tool
413 | * gets mixed with the output of the code under test.
414 | *
424 | *
425 | * @param statement an arbitrary piece of code.
426 | * @throws Exception any exception thrown by the statement.
427 | * @see #muteSystemOut(Statement)
428 | * @since 1.0.0
429 | */
430 | public static void muteSystemErr(
431 | Statement statement
432 | ) throws Exception {
433 | executeWithSystemErrReplacement(
434 | new NoopStream(),
435 | statement
436 | );
437 | }
438 |
439 | /**
440 | * Executes the statement and suppresses the output of the statement to
441 | * {@code System.out}. Use this to avoid that the output of your build tool
442 | * gets mixed with the output of the code under test.
443 | *
452 | *
453 | * @param statement an arbitrary piece of code.
454 | * @throws Exception any exception thrown by the statement.
455 | * @see #muteSystemErr(Statement)
456 | * @since 1.0.0
457 | */
458 | public static void muteSystemOut(
459 | Statement statement
460 | ) throws Exception {
461 | executeWithSystemOutReplacement(
462 | new NoopStream(),
463 | statement
464 | );
465 | }
466 |
467 | /**
468 | * Executes the statement and restores the system properties after the
469 | * statement has been executed. This allows you to set or clear system
470 | * properties within the statement without affecting other tests.
471 | *
530 | *
531 | * @param statement an arbitrary piece of code.
532 | * @return text that is written to {@code System.err} by the statement.
533 | * @throws Exception any exception thrown by the statement.
534 | * @see #tapSystemOut(Statement)
535 | * @see #tapSystemErrAndOut(Statement)
536 | * @see #tapSystemErrAndOutNormalized(Statement)
537 | * @since 1.0.0
538 | */
539 | public static String tapSystemErr(
540 | Statement statement
541 | ) throws Exception {
542 | TapStream tapStream = new TapStream();
543 | executeWithSystemErrReplacement(
544 | tapStream,
545 | statement
546 | );
547 | return tapStream.textThatWasWritten();
548 | }
549 |
550 | /**
551 | * Executes the statement and returns the text that was written to
552 | * {@code System.err} by the statement. New line characters are replaced
553 | * with a single {@code \n}.
554 | *
667 | *
668 | * @param statement an arbitrary piece of code.
669 | * @return text that is written to {@code System.out} by the statement.
670 | * @throws Exception any exception thrown by the statement.
671 | * @see #tapSystemErr(Statement)
672 | * @see #tapSystemErrAndOut(Statement)
673 | * @see #tapSystemErrAndOutNormalized(Statement)
674 | * @since 1.0.0
675 | */
676 | public static String tapSystemOut(
677 | Statement statement
678 | ) throws Exception {
679 | TapStream tapStream = new TapStream();
680 | executeWithSystemOutReplacement(
681 | tapStream,
682 | statement
683 | );
684 | return tapStream.textThatWasWritten();
685 | }
686 |
687 | /**
688 | * Executes the statement and returns the text that was written to
689 | * {@code System.out} by the statement. New line characters are replaced
690 | * with a single {@code \n}.
691 | *
You cannot specify the value of an an environment variable twice. An
740 | * {@code IllegalArgumentException} is thrown when you try.
741 | *
Warning: This method uses reflection for modifying internals of the
742 | * environment variables map. It fails if your {@code SecurityManager} forbids
743 | * such modifications.
744 | * @param name the name of the environment variable.
745 | * @param value the value of the environment variable.
746 | * @return an {@link WithEnvironmentVariables} instance that can be used to
747 | * set more variables and run a statement with the specified environment
748 | * variables.
749 | * @since 1.0.0
750 | * @see WithEnvironmentVariables#and(String, String)
751 | * @see WithEnvironmentVariables#execute(Callable)
752 | * @see WithEnvironmentVariables#execute(Statement)
753 | */
754 | public static WithEnvironmentVariables withEnvironmentVariable(
755 | String name,
756 | String value
757 | ) {
758 | return new WithEnvironmentVariables(
759 | singletonMap(name, value)
760 | );
761 | }
762 |
763 | /**
764 | * Executes the statement with the provided security manager.
765 | *
780 | * The specified security manager is only present during the test.
781 | * @param securityManager the security manager that is used while the
782 | * statement is executed.
783 | * @param statement an arbitrary piece of code.
784 | * @throws Exception any exception thrown by the statement.
785 | * @since 1.0.0
786 | */
787 | public static void withSecurityManager(
788 | SecurityManager securityManager,
789 | Statement statement
790 | ) throws Exception {
791 | SecurityManager originalSecurityManager = getSecurityManager();
792 | setSecurityManager(securityManager);
793 | try {
794 | statement.execute();
795 | } finally {
796 | setSecurityManager(originalSecurityManager);
797 | }
798 | }
799 |
800 | /**
801 | * Executes the statement and lets {@code System.in} provide the specified
802 | * text during the execution. In addition several Exceptions can be
803 | * specified that are thrown when {@code System.in#read} is called.
804 | *
805 | *
If you provide text as parameters of {@code withTextFromSystemIn(...)}
850 | * in addition then the exception is thrown after the text has been read
851 | * from {@code System.in}.
852 | * @param lines the lines that are available from {@code System.in}.
853 | * @return an {@link SystemInStub} instance that is used to execute a
854 | * statement with its {@link SystemInStub#execute(Statement) execute}
855 | * method. In addition it can be used to specify an exception that is thrown
856 | * after the text is read.
857 | * @since 1.0.0
858 | * @see SystemInStub#execute(Statement)
859 | * @see SystemInStub#andExceptionThrownOnInputEnd(IOException)
860 | * @see SystemInStub#andExceptionThrownOnInputEnd(RuntimeException)
861 | */
862 | public static SystemInStub withTextFromSystemIn(
863 | String... lines
864 | ) {
865 | String text = stream(lines)
866 | .map(line -> line + lineSeparator())
867 | .collect(joining());
868 | return new SystemInStub(text);
869 | }
870 |
871 | private static Properties copyOf(
872 | Properties source
873 | ) {
874 | Properties copy = new Properties();
875 | copy.putAll(source);
876 | return copy;
877 | }
878 |
879 | private static void executeWithSystemErrReplacement(
880 | OutputStream replacementForErr,
881 | Statement statement
882 | ) throws Exception {
883 | PrintStream originalStream = err;
884 | try {
885 | setErr(wrap(replacementForErr));
886 | statement.execute();
887 | } finally {
888 | setErr(originalStream);
889 | }
890 | }
891 |
892 | private static void executeWithSystemOutReplacement(
893 | OutputStream replacementForOut,
894 | Statement statement
895 | ) throws Exception {
896 | PrintStream originalStream = out;
897 | try {
898 | setOut(wrap(replacementForOut));
899 | statement.execute();
900 | } finally {
901 | setOut(originalStream);
902 | }
903 | }
904 |
905 | private static PrintStream wrap(
906 | OutputStream outputStream
907 | ) throws UnsupportedEncodingException {
908 | return new PrintStream(
909 | outputStream,
910 | AUTO_FLUSH,
911 | DEFAULT_ENCODING
912 | );
913 | }
914 |
915 | private static class DisallowWriteStream extends OutputStream {
916 | @Override
917 | public void write(
918 | int b
919 | ) {
920 | throw new AssertionError(
921 | "Tried to write '"
922 | + (char) b
923 | + "' although this is not allowed."
924 | );
925 | }
926 | }
927 |
928 | private static class NoopStream extends OutputStream {
929 | @Override
930 | public void write(
931 | int b
932 | ) {
933 | }
934 | }
935 |
936 | /**
937 | * A stub that defines the text provided by {@code System.in}. The methods
938 | * {@link #andExceptionThrownOnInputEnd(IOException)} and
939 | * {@link #andExceptionThrownOnInputEnd(RuntimeException)} can be used to
940 | * simulate a {@code System.in} that throws an exception.
941 | *
The specified behaviour of {@code System.in} is applied to an
942 | * arbitrary piece of code that is provided to {@link #execute(Statement)}.
943 | */
944 | public static class SystemInStub {
945 | private IOException ioException;
946 | private RuntimeException runtimeException;
947 | private final String text;
948 |
949 | private SystemInStub(
950 | String text
951 | ) {
952 | this.text = text;
953 | }
954 |
955 | /**
956 | * Sets an exception that is thrown after the text is read.
957 | * @param exception the {@code IOException} to be thrown.
958 | * @return the {@code SystemInStub} itself.
959 | * @throws IllegalStateException if a {@code RuntimeException} was
960 | * already set by
961 | * {@link #andExceptionThrownOnInputEnd(RuntimeException)}
962 | */
963 | public SystemInStub andExceptionThrownOnInputEnd(
964 | IOException exception
965 | ) {
966 | if (runtimeException != null)
967 | throw new IllegalStateException("You cannot call"
968 | + " andExceptionThrownOnInputEnd(IOException) because"
969 | + " andExceptionThrownOnInputEnd(RuntimeException) has"
970 | + " already been called.");
971 | this.ioException = exception;
972 | return this;
973 | }
974 |
975 | /**
976 | * Sets an exception that is thrown after the text is read.
977 | * @param exception the {@code RuntimeException} to be thrown.
978 | * @return the {@code SystemInStub} itself.
979 | * @throws IllegalStateException if an {@code IOException} was already
980 | * set by {@link #andExceptionThrownOnInputEnd(IOException)}
981 | */
982 | public SystemInStub andExceptionThrownOnInputEnd(
983 | RuntimeException exception
984 | ) {
985 | if (ioException != null)
986 | throw new IllegalStateException("You cannot call"
987 | + " andExceptionThrownOnInputEnd(RuntimeException) because"
988 | + " andExceptionThrownOnInputEnd(IOException) has already"
989 | + " been called.");
990 | this.runtimeException = exception;
991 | return this;
992 | }
993 |
994 | /**
995 | * Executes the statement and lets {@code System.in} provide the
996 | * specified text during the execution. After the text was read it
997 | * throws and exception when {@code System.in#read} is called and an
998 | * exception was specified by
999 | * {@link #andExceptionThrownOnInputEnd(IOException)} or
1000 | * {@link #andExceptionThrownOnInputEnd(RuntimeException)}.
1001 | * @param statement an arbitrary piece of code.
1002 | * @throws Exception any exception thrown by the statement.
1003 | */
1004 | public void execute(
1005 | Statement statement
1006 | ) throws Exception {
1007 | InputStream stubStream = new ReplacementInputStream(
1008 | text, ioException, runtimeException
1009 | );
1010 | InputStream originalIn = System.in;
1011 | try {
1012 | setIn(stubStream);
1013 | statement.execute();
1014 | } finally {
1015 | setIn(originalIn);
1016 | }
1017 | }
1018 |
1019 |
1020 | private static class ReplacementInputStream extends InputStream {
1021 | private final StringReader reader;
1022 | private final IOException ioException;
1023 | private final RuntimeException runtimeException;
1024 |
1025 | ReplacementInputStream(
1026 | String text,
1027 | IOException ioException,
1028 | RuntimeException runtimeException
1029 | ) {
1030 | this.reader = new StringReader(text);
1031 | this.ioException = ioException;
1032 | this.runtimeException = runtimeException;
1033 | }
1034 |
1035 | @Override
1036 | public int read(
1037 | ) throws IOException {
1038 | int character = reader.read();
1039 | if (character == -1)
1040 | handleEmptyReader();
1041 | return character;
1042 | }
1043 |
1044 | private void handleEmptyReader(
1045 | ) throws IOException {
1046 | if (ioException != null)
1047 | throw ioException;
1048 | else if (runtimeException != null)
1049 | throw runtimeException;
1050 | }
1051 |
1052 | @Override
1053 | public int read(
1054 | byte[] buffer,
1055 | int offset,
1056 | int len
1057 | ) throws IOException {
1058 | if (buffer == null)
1059 | throw new NullPointerException();
1060 | else if (offset < 0 || len < 0 || len > buffer.length - offset)
1061 | throw new IndexOutOfBoundsException();
1062 | else if (len == 0)
1063 | return 0;
1064 | else
1065 | return readNextLine(buffer, offset, len);
1066 | }
1067 |
1068 | private int readNextLine(
1069 | byte[] buffer,
1070 | int offset,
1071 | int len
1072 | ) throws IOException {
1073 | int c = read();
1074 | if (c == -1)
1075 | return -1;
1076 | buffer[offset] = (byte) c;
1077 |
1078 | int i = 1;
1079 | for (; (i < len) && !isCompleteLineWritten(buffer, i - 1); ++i) {
1080 | byte read = (byte) read();
1081 | if (read == -1)
1082 | break;
1083 | else
1084 | buffer[offset + i] = read;
1085 | }
1086 | return i;
1087 | }
1088 |
1089 | private boolean isCompleteLineWritten(
1090 | byte[] buffer,
1091 | int indexLastByteWritten
1092 | ) {
1093 | byte[] separator = getProperty("line.separator")
1094 | .getBytes(defaultCharset());
1095 | int indexFirstByteOfSeparator = indexLastByteWritten
1096 | - separator.length + 1;
1097 | return indexFirstByteOfSeparator >= 0
1098 | && contains(buffer, separator, indexFirstByteOfSeparator);
1099 | }
1100 |
1101 | private boolean contains(
1102 | byte[] array,
1103 | byte[] pattern,
1104 | int indexStart
1105 | ) {
1106 | for (int i = 0; i < pattern.length; ++i)
1107 | if (array[indexStart + i] != pattern[i])
1108 | return false;
1109 | return true;
1110 | }
1111 | }
1112 | }
1113 |
1114 | private static class TapStream extends OutputStream {
1115 | final ByteArrayOutputStream text = new ByteArrayOutputStream();
1116 |
1117 | @Override
1118 | public void write(
1119 | int b
1120 | ) {
1121 | text.write(b);
1122 | }
1123 |
1124 | String textThatWasWritten() {
1125 | return text.toString();
1126 | }
1127 | }
1128 |
1129 | /**
1130 | * A collection of values for environment variables. New values can be
1131 | * added by {@link #and(String, String)}. The {@code EnvironmentVariables}
1132 | * object is then used to execute an arbitrary piece of code with these
1133 | * environment variables being present.
1134 | */
1135 | public static final class WithEnvironmentVariables {
1136 | private final Map variables;
1137 |
1138 | private WithEnvironmentVariables(
1139 | Map variables
1140 | ) {
1141 | this.variables = variables;
1142 | }
1143 |
1144 | /**
1145 | * Creates a new {@code WithEnvironmentVariables} object that
1146 | * additionally stores the value for an additional environment variable.
1147 | *
You cannot specify the value of an environment variable twice. An
1148 | * {@code IllegalArgumentException} when you try.
1149 | * @param name the name of the environment variable.
1150 | * @param value the value of the environment variable.
1151 | * @return a new {@code WithEnvironmentVariables} object.
1152 | * @throws IllegalArgumentException when a value for the environment
1153 | * variable {@code name} is already specified.
1154 | * @see #withEnvironmentVariable(String, String)
1155 | * @see #execute(Statement)
1156 | */
1157 | public WithEnvironmentVariables and(
1158 | String name,
1159 | String value
1160 | ) {
1161 | validateNotSet(name, value);
1162 | HashMap moreVariables = new HashMap<>(variables);
1163 | moreVariables.put(name, value);
1164 | return new WithEnvironmentVariables(moreVariables);
1165 | }
1166 |
1167 | private void validateNotSet(
1168 | String name,
1169 | String value
1170 | ) {
1171 | if (variables.containsKey(name)) {
1172 | String currentValue = variables.get(name);
1173 | throw new IllegalArgumentException(
1174 | "The environment variable '" + name + "' cannot be set to "
1175 | + format(value) + " because it was already set to "
1176 | + format(currentValue) + "."
1177 | );
1178 | }
1179 | }
1180 |
1181 | private String format(
1182 | String text
1183 | ) {
1184 | if (text == null)
1185 | return "null";
1186 | else
1187 | return "'" + text + "'";
1188 | }
1189 |
1190 | /**
1191 | * Executes a {@code Callable} with environment variable values
1192 | * according to what was set before. It exposes the return value of the
1193 | * {@code Callable}. All changes to environment variables are reverted
1194 | * after the {@code Callable} has been executed.
1195 | *
Warning: This method uses reflection for modifying internals of the
1214 | * environment variables map. It fails if your {@code SecurityManager} forbids
1215 | * such modifications.
1216 | * @param the type of {@code callable}'s result
1217 | * @param callable an arbitrary piece of code.
1218 | * @return the return value of {@code callable}.
1219 | * @throws Exception any exception thrown by the callable.
1220 | * @since 1.1.0
1221 | * @see #withEnvironmentVariable(String, String)
1222 | * @see #and(String, String)
1223 | * @see #execute(Statement)
1224 | */
1225 | public T execute(
1226 | Callable callable
1227 | ) throws Exception {
1228 | Map originalVariables = new HashMap<>(getenv());
1229 | try {
1230 | setEnvironmentVariables();
1231 | return callable.call();
1232 | } finally {
1233 | restoreOriginalVariables(originalVariables);
1234 | }
1235 | }
1236 |
1237 | /**
1238 | * Executes a statement with environment variable values according to
1239 | * what was set before. All changes to environment variables are
1240 | * reverted after the statement has been executed.
1241 | *