commit 29d6e92a10e6c5a186e9a1c1748c43e3a0824b40 Author: code-clash <> Date: Thu Apr 30 20:39:39 2020 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40f974f --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Gradle +.gradle +build/ + +# Android built artifacts +*.apk +*.ap_ +*.dex + +# Java build artifacts class files +*.class + +# other generated files +gen/ +target/ + +# local configuration file (for Android sdk path, etc) +local.properties + +# OSX files +.DS_Store + +# Eclipse +/.classpath +/.settings/ +/.project +/.metadata +bin/ + +# IntelliJ +*.iml +*.ipr +*.iws +*.uml +.idea/compiler.xml +.idea/workspace.xml +out/ + +# NDK +obj/ + +# Misc +*.log +*.graphml +coverage.db* diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..ded026c --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..fdc392f --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..aec9256 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/SolutionTest.xml b/.idea/runConfigurations/SolutionTest.xml new file mode 100644 index 0000000..ea1fd4b --- /dev/null +++ b/.idea/runConfigurations/SolutionTest.xml @@ -0,0 +1,22 @@ + + + + + + + false + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..cabb3c1 --- /dev/null +++ b/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java' +} + +wrapper { + distributionType = Wrapper.DistributionType.ALL +} + +allprojects { + + apply plugin: 'java' + defaultTasks 'clean', 'compile' + + repositories { + mavenCentral() + } + + dependencies { + implementation 'org.apache.commons:commons-lang3:3.10' + implementation 'org.apache.commons:commons-collections4:4.4' + implementation 'org.apache.commons:commons-math3:3.6.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.6.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.6.0' + } + + compileJava { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..87b738c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1b2b07c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..af6708f --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..6d57edc --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d5271ef --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'template' diff --git a/src/main/java/Solution.java b/src/main/java/Solution.java new file mode 100644 index 0000000..5c21852 --- /dev/null +++ b/src/main/java/Solution.java @@ -0,0 +1,27 @@ +import java.util.*; +import java.io.*; +import java.math.*; + +import org.apache.commons.collections4.*; +import org.apache.commons.lang3.*; +import org.apache.commons.math3.*; + +/** + * Template code to help you parse the standard input + * according to the problem statement. + **/ +class Solution { + + public static void main( String[] args ) { + // hint: read values via Scanner methods + Scanner in = new Scanner( System.in ); + + // code your solution here + + // use error to write to console + System.err.println( "debug" ); + + // Write result with System.out.println() + System.out.println( "value" ); + } +} diff --git a/src/main/resources/.gitkeep b/src/main/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/BenchmarkExtension.java b/src/test/java/BenchmarkExtension.java new file mode 100644 index 0000000..7caca12 --- /dev/null +++ b/src/test/java/BenchmarkExtension.java @@ -0,0 +1,47 @@ +import org.junit.jupiter.api.extension.*; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; + +import java.util.logging.Logger; + +import static java.lang.System.currentTimeMillis; + +/** + * Based on TimingExtension from JUnit 5 user guide, and BenchmarkExtension from Nicolai Parlog + * https://github.com/CodeFX-org/demo-junit-5/blob/master/src/main/java/org/codefx/demo/junit5/BenchmarkExtension.java + */ +public class BenchmarkExtension implements BeforeAllCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback, AfterAllCallback { + + private static final Namespace NAMESPACE = Namespace.create( BenchmarkExtension.class ); + private static final Logger LOGGER = Logger.getLogger( BenchmarkExtension.class.getName() ); + + @Override + public void beforeAll( ExtensionContext context ) { + context.getStore( NAMESPACE ).put( context.getRequiredTestClass(), currentTimeMillis() ); + } + + @Override + public void afterAll( ExtensionContext context ) { + long launchTime = context.getStore( NAMESPACE ).get( context.getRequiredTestClass(), long.class ); + long elapsedTime = currentTimeMillis() - launchTime; + report( "Test class", context, elapsedTime ); + } + + @Override + public void beforeTestExecution( ExtensionContext context ) { + context.getStore( NAMESPACE ).put( context.getRequiredTestMethod(), currentTimeMillis() ); + } + + @Override + public void afterTestExecution( ExtensionContext context ) { + long launchTime = context.getStore( NAMESPACE ).get( context.getRequiredTestMethod(), long.class ); + long elapsedTime = currentTimeMillis() - launchTime; + report( "Test", context, elapsedTime ); + } + + private static void report( String unit, ExtensionContext context, long elapsedTime ) { + String message = String.format( "%s '%s' took %d ms.", unit, context.getDisplayName(), elapsedTime ); + context.publishReportEntry( "Benchmark", message ); + LOGGER.info( message ); + } + +} \ No newline at end of file diff --git a/src/test/java/SolutionTest.java b/src/test/java/SolutionTest.java new file mode 100644 index 0000000..bb1d61a --- /dev/null +++ b/src/test/java/SolutionTest.java @@ -0,0 +1,63 @@ +import com.github.stefanbirkner.systemlambda.SystemLambda; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.converter.ArgumentConversionException; +import org.junit.jupiter.params.converter.ConvertWith; +import org.junit.jupiter.params.converter.SimpleArgumentConverter; +import org.junit.jupiter.params.provider.CsvFileSource; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith( BenchmarkExtension.class ) +class SolutionTest { + + static class FileContentConverter extends SimpleArgumentConverter { + @Override + protected Object convert( Object source, Class targetType ) throws ArgumentConversionException { + try { + assertEquals( String.class, targetType, "Can only convert to String" ); + final InputStream resource = SolutionTest.class.getResourceAsStream( String.valueOf( source )); + if ( null == resource ) { + return source; + } + + BufferedReader res = new BufferedReader( new InputStreamReader( resource )); + return res.lines().collect( Collectors.joining( System.lineSeparator() )); + } + catch ( Exception e ) { + throw new ArgumentConversionException( e.getLocalizedMessage() ); + } + } + } + + static class MultilineConverter extends SimpleArgumentConverter { + @Override + protected Object convert( Object source, Class targetType ) throws ArgumentConversionException { + try { + assertEquals( String.class, targetType, "Can only convert to String" ); + return String.valueOf( source ).replaceAll( "\\s*[|]\\s*", System.lineSeparator() ); + } + catch ( Exception e ) { + throw new ArgumentConversionException( e.getLocalizedMessage() ); + } + } + } + + @ParameterizedTest + @CsvFileSource( resources = "testdata.csv" ) + void main( @ConvertWith( FileContentConverter.class ) final String input, + @ConvertWith( MultilineConverter.class ) final String expected ) throws Exception { + String output = SystemLambda.tapSystemOut( () -> + SystemLambda.withTextFromSystemIn( input ) + .execute( () -> Solution.main( new String[0] )) + ); + + assertEquals( expected, output.trim() ); + } + +} diff --git a/src/test/java/com/github/stefanbirkner/systemlambda/Statement.java b/src/test/java/com/github/stefanbirkner/systemlambda/Statement.java new file mode 100644 index 0000000..06cc87d --- /dev/null +++ b/src/test/java/com/github/stefanbirkner/systemlambda/Statement.java @@ -0,0 +1,17 @@ +package com.github.stefanbirkner.systemlambda; + +/** + * Code that should be executed by on of the methods of {@link SystemLambda}. + * This code may throw an {@link Exception}. Therefore we cannot use + * {@link Runnable}. + */ +public interface Statement { + + /** + * Execute the statement. + * + * @throws Exception the statement may throw an arbitrary exception. + */ + void execute() throws Exception; + +} \ No newline at end of file diff --git a/src/test/java/com/github/stefanbirkner/systemlambda/SystemLambda.java b/src/test/java/com/github/stefanbirkner/systemlambda/SystemLambda.java new file mode 100644 index 0000000..39d5c41 --- /dev/null +++ b/src/test/java/com/github/stefanbirkner/systemlambda/SystemLambda.java @@ -0,0 +1,1411 @@ +package com.github.stefanbirkner.systemlambda; + +import java.io.*; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.nio.charset.Charset; +import java.security.Permission; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static java.lang.Class.forName; +import static java.lang.System.*; +import static java.nio.charset.Charset.defaultCharset; +import static java.util.Arrays.stream; +import static java.util.Collections.singletonMap; +import static java.util.stream.Collectors.joining; + +/** + * {@code SystemLambda} is a collection of functions for testing code + * that uses {@code java.lang.System}. + * + *

Security Manager

+ * + *

The function + * {@link #withSecurityManager(SecurityManager, Statement) withSecurityManager} + * lets you specify which {@code SecurityManager} is returned by + * {@code System.getSecurityManger()} while your code under test is executed. + *

+ * @Test
+ * void execute_code_with_specific_SecurityManager() {
+ *   SecurityManager securityManager = new ASecurityManager();
+ *   withSecurityManager(
+ *     securityManager,
+ *     () -> {
+ *       //code under test
+ *       //e.g. the following assertion is met
+ *       assertSame(
+ *         securityManager,
+ *         System.getSecurityManager()
+ *       );
+ *     }
+ *   );
+ * }
+ * 
+ *

After the statement {@code withSecurityManager(...)} is executed + * {@code System.getSecurityManager()} will return the original security manager + * again. + * + *

Environment Variables

+ * + *

The method + * {@link #withEnvironmentVariable(String, String) withEnvironmentVariable} + * allows you to set environment variables within your test code that are + * removed after you code under test is executed. + *

+ * @Test
+ * void set_environment_variables() throws Exception {
+ *   withEnvironmentVariable("first", "first value")
+ *     .and("second", "second value")
+ *     .execute(() -> {
+ *       assertEquals("first value", System.getenv("first"));
+ *       assertEquals("second value", System.getenv("second"));
+ *     });
+ * }
+ * + *

System Properties

+ * + *

The function + * {@link #restoreSystemProperties(Statement) restoreSystemProperties} + * guarantees that after executing the test code each System property has the + * same value like before. Therefore you can modify System properties inside of + * the test code without having an impact on other tests. + *

+ * @Test
+ * void execute_code_that_manipulates_system_properties() {
+ *   restoreSystemProperties(
+ *     () -> {
+ *       System.setProperty("some.property", "some value");
+ *       //code under test that reads properties (e.g. "some.property") or
+ *       //modifies them.
+ *     }
+ *   );
+ * }
+ * 
+ * + *

System.exit

+ *

Command-line applications terminate by calling {@code System.exit} with + * some status code. If you test such an application then the JVM that runs the + * test would exit when the application under test calls {@code System.exit}. + * You can avoid this with the method + * {@link #catchSystemExit(Statement) catchSystemExit} which also provides the + * status code of the {@code System.exit} call. + * + *

+ * @Test
+ * public void catch_status_code_of_System_exit() {
+ *   int statusCode = catchSystemExit(() -> {
+ *     System.exit(42);
+ *   });
+ *   assertEquals(42, statusCode);
+ * }
+ * 
+ * + * The method `{@code catchSystemExit} throws an {@code AssertionError} if the + * code under test does not call {@code System.exit}. Therefore your test fails + * with a failure message "System.exit has not been called." + * + *

System.out and System.err

+ *

Command-line applications usually write to the console. If you write such + * applications you need to test the output of these applications. The methods + * {@link #tapSystemErr(Statement) tapSystemErr}, + * {@link #tapSystemErrNormalized(Statement) tapSystemErrNormalized}, + * {@link #tapSystemOut(Statement) tapSystemOut} and + * {@link #tapSystemOutNormalized(Statement) tapSystemOutNormalized} allow you + * to tap the text that is written to {@code System.err}/{@code System.out}. The + * methods with the suffix {@code Normalized} normalize line breaks to + * {@code \n} so that you can run tests with the same assertions on Linux, + * macOS and Windows. + * + *

+ * @Test
+ * void check_text_written_to_System_err() throws Exception {
+ *   String text = tapSystemErr(
+ *     () -> System.err.println("some text")
+ *   );
+ *   assertEquals(text, "some text");
+ * }
+ *
+ * @Test
+ * void check_multiple_lines_written_to_System_err() throws Exception {
+ *   String text = tapSystemErrNormalized(
+ *     () -> {
+ *       System.err.println("first line");
+ *       System.err.println("second line");
+ *     }
+ *   );
+ *   assertEquals(text, "first line\nsecond line");
+ * }
+ *
+ * @Test
+ * void check_text_written_to_System_out() throws Exception {
+ *   String text = tapSystemOut(
+ *     () -> System.out.println("some text")
+ *   );
+ *   assertEquals(text, "some text");
+ * }
+ *
+ * @Test
+ * void check_multiple_lines_written_to_System_out() throws Exception {
+ *   String text = tapSystemOutNormalized(
+ *     () -> {
+ *       System.out.println("first line");
+ *       System.out.println("second line");
+ *     }
+ *   );
+ *   assertEquals(text, "first line\nsecond line");
+ * }
+ * + *

You can assert that nothing is written to + * {@code System.err}/{@code System.out} by wrapping code with the function + * {@link #assertNothingWrittenToSystemErr(Statement) + * assertNothingWrittenToSystemErr}/{@link #assertNothingWrittenToSystemOut(Statement) + * assertNothingWrittenToSystemOut}. E.g. the following tests fail: + *

+ * @Test
+ * void fails_because_something_is_written_to_System_err() {
+ *   assertNothingWrittenToSystemErr(
+ *     () -> {
+ *        System.err.println("some text");
+ *     }
+ *   );
+ * }
+ *
+ * @Test
+ * void fails_because_something_is_written_to_System_out() {
+ *   assertNothingWrittenToSystemOut(
+ *     () -> {
+ *        System.out.println("some text");
+ *     }
+ *   );
+ * }
+ * 
+ * + *

If the code under test writes text to + * {@code System.err}/{@code System.out} then it is intermixed with the output + * of your build tool. Therefore you may want to avoid that the code under test + * writes to {@code System.err}/{@code System.out}. You can achieve this with + * the function {@link #muteSystemErr(Statement) + * muteSystemErr}/{@link #muteSystemOut(Statement) muteSystemOut}. E.g. the + * following tests don't write anything to + * {@code System.err}/{@code System.out}: + *

+ * @Test
+ * void nothing_is_written_to_System_err() {
+ *   muteSystemErr(
+ *     () -> {
+ *        System.err.println("some text");
+ *     }
+ *   );
+ * }
+ *
+ * @Test
+ * void nothing_is_written_to_System_out() {
+ *   muteSystemOut(
+ *     () -> {
+ *        System.out.println("some text");
+ *     }
+ *   );
+ * }
+ * 
+ * + *

System.in

+ * + *

Interactive command-line applications read from {@code System.in}. If you + * write such applications you need to provide input to these applications. You + * can specify the lines that are available from `{@code System.in} with the + * method {@link #withTextFromSystemIn(String...) withTextFromSystemIn} + *

+ * @Test
+ * void readTextFromSystemIn() {
+ *   withTextFromSystemIn("first line", "second line")
+ *     .execute(() -> {
+ *       Scanner scanner = new Scanner(System.in);
+ *       scanner.nextLine();
+ *       assertEquals("first line", scanner.nextLine());
+ *     });
+ * }
+ * 
+ * + * For a complete test coverage you may also want to simulate `System.in` throwing + * exceptions when the application reads from it. You can specify such an + * exception (either `RuntimeException` or `IOException` after specifying the text. + * The exception will be thrown by the next `read` after the text has been + * consumed. + *
+ * @Test
+ * void readTextFromSystemInWithIOException() {
+ *   withTextFromSystemIn("first line", "second line")
+ *     .andExceptionThrownOnInputEnd(new IOException())
+ *     .execute(() -> {
+ *       Scanner scanner = new Scanner(System.in);
+ *       scanner.nextLine();
+ *       scanner.nextLine();
+ *       assertThrownBy(
+ *         IOException.class,
+ *         () -> scanner.readLine()
+ *       );
+ *   });
+ * }
+ *
+ * @Test
+ * void readTextFromSystemInFailsWithRuntimeException() {
+ *   withTextFromSystemIn("first line", "second line")
+ *     .andExceptionThrownOnInputEnd(new RuntimeException())
+ *     .execute(() -> {
+ *       Scanner scanner = new Scanner(System.in);
+ *       scanner.nextLine();
+ *       scanner.nextLine();
+ *       assertThrownBy(
+ *	        RuntimeException.class,
+ *          () -> scanner.readLine()
+ *       );
+ * 	   });
+ * }
+ * 
+ * + * You can write a test that throws an exception immediately by not providing any + * text. + *
+ * withTextFromSystemIn()
+ *   .andExceptionThrownOnInputEnd(...)
+ *   .execute(() -> {
+ *     Scanner scanner = new Scanner(System.in);
+ *     assertThrownBy(
+ *     ...,
+ *     () -> scanner.readLine()
+ *   );
+ * });
+ * 
+ * + *

It provides support for + *

+ */ +public class SystemLambda { + + private static final boolean AUTO_FLUSH = true; + private static final String DEFAULT_ENCODING = Charset.defaultCharset().name(); + + /** + * Executes the statement and fails (throws an {@code AssertionError}) if + * the statement tries to write to {@code System.err}. + *

The following test fails + *

+	 * @Test
+	 * public void fails_because_something_is_written_to_System_err() {
+	 *   assertNothingWrittenToSystemErr(
+	 *     () -> {
+	 *       System.err.println("some text");
+	 *     }
+	 *   );
+	 * }
+	 * 
+ * The test fails with the failure "Tried to write 's' to System.err + * although this is not allowed." + * + * @param statement an arbitrary piece of code. + * @throws Exception any exception thrown by the statement or an + * {@code AssertionError} if the statement tries to write + * to {@code System.err}. + * @see #assertNothingWrittenToSystemOut(Statement) + * @since 1.0.0 + */ + public static void assertNothingWrittenToSystemErr( + Statement statement + ) throws Exception { + executeWithSystemErrReplacement( + new DisallowWriteStream(), + statement + ); + } + + /** + * Executes the statement and fails (throws an {@code AssertionError}) if + * the statement tries to write to {@code System.out}. + *

The following test fails + *

+	 * @Test
+	 * public void fails_because_something_is_written_to_System_out() {
+	 *   assertNothingWrittenToSystemOut(
+	 *     () -> {
+	 *       System.out.println("some text");
+	 *     }
+	 *   );
+	 * }
+	 * 
+ * The test fails with the failure "Tried to write 's' to System.out + * although this is not allowed." + * + * @param statement an arbitrary piece of code. + * @throws Exception any exception thrown by the statement or an + * {@code AssertionError} if the statement tries to write + * to {@code System.out}. + * @see #assertNothingWrittenToSystemErr(Statement) + * @since 1.0.0 + */ + public static void assertNothingWrittenToSystemOut( + Statement statement + ) throws Exception { + executeWithSystemOutReplacement( + new DisallowWriteStream(), + statement + ); + } + + /** + * Usually the output of a test to {@code System.err} does not have to be + * visible. It may even slowdown the test. {@code muteSystemErr} can be + * used to suppress this output. + *
+	 * @Test
+	 * public void nothing_is_written_to_System_err() {
+	 *   muteSystemErr(
+	 *     () -> {
+	 *       System.err.println("some text");
+	 *     }
+	 *   );
+	 * }
+	 * 
+ * + * @param statement an arbitrary piece of code. + * @throws Exception any exception thrown by the statement. + * @see #muteSystemOut(Statement) + * @since 1.0.0 + */ + public static void muteSystemErr( + Statement statement + ) throws Exception { + executeWithSystemErrReplacement( + new NoopStream(), + statement + ); + } + + /** + * Usually the output of a test to {@code System.out} does not have to be + * visible. It may even slowdown the test. {@code muteSystemOut} can be + * used to suppress this output. + *
+	 * @Test
+	 * public void nothing_is_written_to_System_out() {
+	 *   muteSystemOut(
+	 *     () -> {
+	 *       System.out.println("some text");
+	 *     }
+	 *   );
+	 * }
+	 * 
+ * + * @param statement an arbitrary piece of code. + * @throws Exception any exception thrown by the statement. + * @see #muteSystemErr(Statement) + * @since 1.0.0 + */ + public static void muteSystemOut( + Statement statement + ) throws Exception { + executeWithSystemOutReplacement( + new NoopStream(), + statement + ); + } + + /** + * {@code tapSystemErr} returns a String with the text that is written to + * {@code System.err} by the provided piece of code. + *
+	 * @Test
+	 * public void check_the_text_that_is_written_to_System_err() {
+	 *   String textWrittenToSystemErr = tapSystemErr(
+	 *     () -> {
+	 *       System.err.print("some text");
+	 *     }
+	 *   );
+	 *   assertEquals("some text", textWrittenToSystemErr);
+	 * }
+	 * 
+ * + * @param statement an arbitrary piece of code. + * @return text that is written to {@code System.err} by the statement. + * @throws Exception any exception thrown by the statement. + * @see #tapSystemOut(Statement) + * @since 1.0.0 + */ + public static String tapSystemErr( + Statement statement + ) throws Exception { + TapStream tapStream = new TapStream(); + executeWithSystemErrReplacement( + tapStream, + statement + ); + return tapStream.textThatWasWritten(); + } + + /** + * {@code tapSystemOut} returns a String with the text that is written to + * {@code System.out} by the provided piece of code. + *
+	 * @Test
+	 * public void check_the_text_that_is_written_to_System_out() {
+	 *   String textWrittenToSystemOut = tapSystemOut(
+	 *     () -> {
+	 *       System.out.print("some text");
+	 *     }
+	 *   );
+	 *   assertEquals("some text", textWrittenToSystemOut);
+	 * }
+	 * 
+ * + * @param statement an arbitrary piece of code. + * @return text that is written to {@code System.out} by the statement. + * @throws Exception any exception thrown by the statement. + * @see #tapSystemErr(Statement) + * @since 1.0.0 + */ + public static String tapSystemOut( + Statement statement + ) throws Exception { + TapStream tapStream = new TapStream(); + executeWithSystemOutReplacement( + tapStream, + statement + ); + return tapStream.textThatWasWritten(); + } + + /** + * {@code tapSystemErrNormalized} returns a String with the text that is + * written to {@code System.err} by the provided piece of code. New line + * characters are replaced with a single {@code \n}. + *
+	 * @Test
+	 * public void check_the_text_that_is_written_to_System_err() {
+	 *   String textWrittenToSystemErr = tapSystemErrNormalized(
+	 *     () -> {
+	 *       System.err.println("some text");
+	 *     }
+	 *   );
+	 *   assertEquals("some text\n", textWrittenToSystemErr);
+	 * }
+	 * 
+ * + * @param statement an arbitrary piece of code. + * @return text that is written to {@code System.err} by the statement. + * @throws Exception any exception thrown by the statement. + * @see #tapSystemOut(Statement) + * @since 1.0.0 + */ + public static String tapSystemErrNormalized( + Statement statement + ) throws Exception { + return tapSystemErr(statement) + .replace(lineSeparator(), "\n"); + } + + /** + * {@code tapSystemOutNormalized} returns a String with the text that is + * written to {@code System.out} by the provided piece of code. New line + * characters are replaced with a single {@code \n}. + *
+	 * @Test
+	 * public void check_the_text_that_is_written_to_System_out() {
+	 *   String textWrittenToSystemOut = tapSystemOutNormalized(
+	 *     () -> {
+	 *       System.out.println("some text");
+	 *     }
+	 *   );
+	 *   assertEquals("some text\n", textWrittenToSystemOut);
+	 * }
+	 * 
+ * + * @param statement an arbitrary piece of code. + * @return text that is written to {@code System.out} by the statement. + * @throws Exception any exception thrown by the statement. + * @see #tapSystemErr(Statement) + * @since 1.0.0 + */ + public static String tapSystemOutNormalized( + Statement statement + ) throws Exception { + return tapSystemOut(statement) + .replace(lineSeparator(), "\n"); + } + + /** + * Executes the statement and restores the system properties after the + * statement has been executed. This allows you to set or clear system + * properties within the statement without affecting other tests. + *
+	 * @Test
+	 * public void execute_code_that_manipulates_system_properties(
+	 * ) throws Exception {
+	 *   System.clearProperty("some property");
+	 *   System.setProperty("another property", "value before test");
+	 *
+	 *   restoreSystemProperties(
+	 *     () -> {
+	 *       System.setProperty("some property", "some value");
+	 *       assertEquals(
+	 *         "some value",
+	 *         System.getProperty("some property")
+	 *       );
+	 *
+	 *       System.clearProperty("another property");
+	 *       assertNull(
+	 *         System.getProperty("another property")
+	 *       );
+	 *     }
+	 *   );
+	 *
+	 *   //values are restored after test
+	 *   assertNull(
+	 *     System.getProperty("some property")
+	 *   );
+	 *   assertEquals(
+	 *     "value before test",
+	 *     System.getProperty("another property")
+	 *   );
+	 * }
+	 * 
+ * @param statement an arbitrary piece of code. + * @throws Exception any exception thrown by the statement. + * @since 1.0.0 + */ + public static void restoreSystemProperties( + Statement statement + ) throws Exception { + Properties originalProperties = getProperties(); + setProperties(copyOf(originalProperties)); + try { + statement.execute(); + } finally { + setProperties(originalProperties); + } + } + + private static Properties copyOf(Properties source) { + Properties copy = new Properties(); + copy.putAll(source); + return copy; + } + + /** + * Executes a statement with the specified environment variables. All + * changes to environment variables are reverted after the statement has + * been executed. + *
+	 * @Test
+	 * public void execute_code_with_environment_variables(
+	 * ) throws Exception {
+	 *   withEnvironmentVariable("first", "first value")
+	 *     .and("second", "second value")
+	 *     .and("third", null)
+	 *     .execute(
+	 *       () -> {
+	 *         assertEquals(
+	 *           "first value",
+	 *           System.getenv("first")
+	 *         );
+	 *         assertEquals(
+	 *           "second value",
+	 *           System.getenv("second")
+	 *         );
+	 *         assertNull(
+	 *           System.getenv("third")
+	 *         );
+	 *       }
+	 *     );
+	 * }
+	 * 
+ *

You cannot specify the value of an an environment variable twice. An + * {@code IllegalArgumentException} when you try. + *

Warning: This method uses reflection for modifying internals of the + * environment variables map. It fails if your {@code SecurityManager} forbids + * such modifications. + * @param name the name of the environment variable. + * @param value the value of the environment variable. + * @return an {@link WithEnvironmentVariables} instance that can be used to + * set more variables and run a statement with the specified environment + * variables. + * @since 1.0.0 + * @see WithEnvironmentVariables#and(String, String) + * @see WithEnvironmentVariables#execute(Statement) + */ + public static WithEnvironmentVariables withEnvironmentVariable( + String name, + String value + ) { + return new WithEnvironmentVariables( + singletonMap(name, value)); + } + + /** + * Executes the statement with the provided security manager. + *

+     * @Test
+     * public void execute_code_with_specific_SecurityManager() {
+     *   SecurityManager securityManager = new ASecurityManager();
+     *   withSecurityManager(
+     *     securityManager,
+     *     () -> {
+     *       assertSame(securityManager, System.getSecurityManager());
+     *     }
+     *   );
+     * }
+     * 
+ * The specified security manager is only present during the test. + * @param securityManager the security manager that is used while the + * statement is executed. + * @param statement an arbitrary piece of code. + * @throws Exception any exception thrown by the statement. + * @since 1.0.0 + */ + public static void withSecurityManager( + SecurityManager securityManager, + Statement statement + ) throws Exception { + SecurityManager originalSecurityManager = getSecurityManager(); + setSecurityManager(securityManager); + try { + statement.execute(); + } finally { + setSecurityManager(originalSecurityManager); + } + } + + /** + * Executes the statement and lets {@code System.in} provide the specified + * text during the execution. In addition several Exceptions can be + * specified that are thrown when {@code System.in#read} is called. + * + *
+	 *   public void MyTest {
+	 *
+	 *     @Test
+	 *     public void readTextFromStandardInputStream() {
+	 *       withTextFromSystemIn("first line", "second line")
+	 *         .execute(() -> {
+	 *           Scanner scanner = new Scanner(System.in);
+	 *           scanner.nextLine();
+	 *           assertEquals("first line", scanner.nextLine());
+	 *         });
+	 *     }
+	 *   }
+	 * 
+ * + *

Throwing Exceptions

+ *

You can also simulate a {@code System.in} that throws an + * {@code IOException} or {@code RuntimeException}. Use + * + *

+	 *   public void MyTest {
+	 *
+	 *     @Test
+	 *     public void readTextFromStandardInputStreamFailsWithIOException() {
+	 *       withTextFromSystemIn()
+	 *         .andExceptionThrownOnInputEnd(new IOException())
+	 *         .execute(() -> {
+	 *           assertThrownBy(
+	 *             IOException.class,
+	 *             () -> new Scanner(System.in).readLine())
+	 *           );
+	 *         )};
+	 *     }
+	 *
+	 *     @Test
+	 *     public void readTextFromStandardInputStreamFailsWithRuntimeException() {
+	 *       withTextFromSystemIn()
+	 *         .andExceptionThrownOnInputEnd(new RuntimeException())
+	 *         .execute(() -> {
+	 *           assertThrownBy(
+	 *             RuntimeException.class,
+	 *             () -> new Scanner(System.in).readLine())
+	 *           );
+	 *         )};
+	 *     }
+	 *   }
+	 * 
+ *

If you provide text as parameters of {@code withTextFromSystemIn(...)} + * in addition then the exception is thrown after the text has been read + * from {@code System.in}. + * @param lines the lines that are available from {@code System.in}. + * @return an {@link SystemInStub} instance that is used to execute a + * statement with its {@link SystemInStub#execute(Statement) execute} + * method. In addition it can be used to specify an exception that is thrown + * after the text is read. + * @since 1.0.0 + * @see SystemInStub#execute(Statement) + * @see SystemInStub#andExceptionThrownOnInputEnd(IOException) + * @see SystemInStub#andExceptionThrownOnInputEnd(RuntimeException) + */ + public static SystemInStub withTextFromSystemIn( + String... lines + ) { + String text = stream(lines) + .map(line -> line + getProperty("line.separator")) + .collect(joining()); + return new SystemInStub(text); + } + + private static void executeWithSystemErrReplacement( + OutputStream replacementForErr, + Statement statement + ) throws Exception { + PrintStream originalStream = err; + try { + setErr(wrap(replacementForErr)); + statement.execute(); + } finally { + setErr(originalStream); + } + } + + private static void executeWithSystemOutReplacement( + OutputStream replacementForOut, + Statement statement + ) throws Exception { + PrintStream originalStream = out; + try { + setOut(wrap(replacementForOut)); + statement.execute(); + } finally { + setOut(originalStream); + } + } + + private static PrintStream wrap( + OutputStream outputStream + ) throws UnsupportedEncodingException { + return new PrintStream( + outputStream, + AUTO_FLUSH, + DEFAULT_ENCODING + ); + } + + private static class DisallowWriteStream extends OutputStream { + @Override + public void write(int b) { + throw new AssertionError( + "Tried to write '" + + (char) b + + "' although this is not allowed." + ); + } + } + + private static class NoopStream extends OutputStream { + @Override + public void write( + int b + ) { + } + } + + private static class TapStream extends OutputStream { + final ByteArrayOutputStream text = new ByteArrayOutputStream(); + + @Override + public void write( + int b + ) { + text.write(b); + } + + String textThatWasWritten() { + return text.toString(); + } + } + + /** + * A collection of values for environment variables. New values can be + * added by {@link #and(String, String)}. The {@code EnvironmentVariables} + * object is then used to execute an arbitrary piece of code with these + * environment variables being present. + */ + public static final class WithEnvironmentVariables { + private final Map variables; + + private WithEnvironmentVariables( + Map variables + ) { + this.variables = variables; + } + + /** + * Creates a new {@code WithEnvironmentVariables} object that + * additionally stores the value for an additional environment variable. + *

You cannot specify the value of an environment variable twice. An + * {@code IllegalArgumentException} when you try. + * @param name the name of the environment variable. + * @param value the value of the environment variable. + * @return a new {@code WithEnvironmentVariables} object. + * @throws IllegalArgumentException when a value for the environment + * variable {@code name} is already specified. + * @see #withEnvironmentVariable(String, String) + * @see #execute(Statement) + */ + public WithEnvironmentVariables and( + String name, + String value + ) { + validateNotSet(name, value); + HashMap moreVariables = new HashMap<>(variables); + moreVariables.put(name, value); + return new WithEnvironmentVariables(moreVariables); + } + + private void validateNotSet( + String name, + String value + ) { + if (variables.containsKey(name)) { + String currentValue = variables.get(name); + throw new IllegalArgumentException( + "The environment variable '" + name + "' cannot be set to " + + format(value) + " because it was already set to " + + format(currentValue) + "." + ); + } + } + + private String format( + String text + ) { + if (text == null) + return "null"; + else + return "'" + text + "'"; + } + + /** + * Executes a statement with environment variable values according to + * what was set before. All changes to environment variables are + * reverted after the statement has been executed. + *

+		 * @Test
+		 * public void execute_code_with_environment_variables(
+		 * ) throws Exception {
+		 *   withEnvironmentVariable("first", "first value")
+		 *     .and("second", "second value")
+		 *     .and("third", null)
+		 *     .execute(
+		 *       () -> {
+		 *         assertEquals(
+		 *           "first value",
+		 *           System.getenv("first")
+		 *         );
+		 *         assertEquals(
+		 *           "second value",
+		 *           System.getenv("second")
+		 *         );
+		 *         assertNull(
+		 *           System.getenv("third")
+		 *         );
+		 *       }
+		 *     );
+		 * }
+		 * 
+ *

Warning: This method uses reflection for modifying internals of the + * environment variables map. It fails if your {@code SecurityManager} forbids + * such modifications. + * @throws Exception any exception thrown by the statement. + * @since 1.0.0 + * @see #withEnvironmentVariable(String, String) + * @see WithEnvironmentVariables#and(String, String) + */ + public void execute( + Statement statement + ) throws Exception { + Map originalVariables = new HashMap<>(getenv()); + try { + setEnvironmentVariables(); + statement.execute(); + } finally { + restoreOriginalVariables(originalVariables); + } + } + + private void setEnvironmentVariables() { + overrideVariables( + getEditableMapOfVariables() + ); + overrideVariables( + getTheCaseInsensitiveEnvironment() + ); + } + + private void overrideVariables( + Map existingVariables + ) { + if (existingVariables != null) //theCaseInsensitiveEnvironment may be null + variables.forEach( + (name, value) -> set(existingVariables, name, value) + ); + } + + private void set( + Map variables, + String name, + String value + ) { + if (value == null) + variables.remove(name); + else + variables.put(name, value); + } + + void restoreOriginalVariables( + Map originalVariables + ) { + restoreVariables( + getEditableMapOfVariables(), + originalVariables + ); + restoreVariables( + getTheCaseInsensitiveEnvironment(), + originalVariables + ); + } + + void restoreVariables( + Map variables, + Map originalVariables + ) { + if (variables != null) {//theCaseInsensitiveEnvironment may be null + variables.clear(); + variables.putAll(originalVariables); + } + } + + private static Map getEditableMapOfVariables() { + Class classOfMap = getenv().getClass(); + try { + return getFieldValue(classOfMap, getenv(), "m"); + } catch (IllegalAccessException e) { + throw new RuntimeException("System Rules cannot access the field" + + " 'm' of the map System.getenv().", e); + } catch (NoSuchFieldException e) { + throw new RuntimeException("System Rules expects System.getenv() to" + + " have a field 'm' but it has not.", e); + } + } + + /* + * The names of environment variables are case-insensitive in Windows. + * Therefore it stores the variables in a TreeMap named + * theCaseInsensitiveEnvironment. + */ + private static Map getTheCaseInsensitiveEnvironment() { + try { + Class processEnvironment = forName("java.lang.ProcessEnvironment"); + return getFieldValue( + processEnvironment, null, "theCaseInsensitiveEnvironment"); + } catch (ClassNotFoundException e) { + throw new RuntimeException("System Rules expects the existence of" + + " the class java.lang.ProcessEnvironment but it does not" + + " exist.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("System Rules cannot access the static" + + " field 'theCaseInsensitiveEnvironment' of the class" + + " java.lang.ProcessEnvironment.", e); + } catch (NoSuchFieldException e) { + //this field is only available for Windows + return null; + } + } + + private static Map getFieldValue( + Class klass, + Object object, + String name + ) throws NoSuchFieldException, IllegalAccessException { + Field field = klass.getDeclaredField(name); + field.setAccessible(true); + return (Map) field.get(object); + } + } + + public static class SystemInStub { + private IOException ioException; + private RuntimeException runtimeException; + private final String text; + + private SystemInStub(String text) { + this.text = text; + } + + public SystemInStub andExceptionThrownOnInputEnd( + IOException exception + ) { + if (runtimeException != null) + throw new IllegalStateException("You cannot call" + + " andExceptionThrownOnInputEnd(IOException) because" + + " andExceptionThrownOnInputEnd(RuntimeException) has" + + " already been called."); + this.ioException = exception; + return this; + } + + public SystemInStub andExceptionThrownOnInputEnd( + RuntimeException exception + ) { + if (ioException != null) + throw new IllegalStateException("You cannot call" + + " andExceptionThrownOnInputEnd(RuntimeException) because" + + " andExceptionThrownOnInputEnd(IOException) has already" + + " been called."); + this.runtimeException = exception; + return this; + } + + public void execute( + Statement statement + ) throws Exception { + InputStream stubStream = new ReplacementInputStream( + text, ioException, runtimeException + ); + InputStream originalIn = System.in; + try { + setIn(stubStream); + statement.execute(); + } finally { + setIn(originalIn); + } + } + + + private static class ReplacementInputStream extends InputStream { + private final StringReader reader; + private final IOException ioException; + private final RuntimeException runtimeException; + + ReplacementInputStream( + String text, + IOException ioException, + RuntimeException runtimeException + ) { + this.reader = new StringReader(text); + this.ioException = ioException; + this.runtimeException = runtimeException; + } + + @Override + public int read() throws IOException { + int character = reader.read(); + if (character == -1) + handleEmptyReader(); + return character; + } + + private void handleEmptyReader() throws IOException { + if (ioException != null) + throw ioException; + else if (runtimeException != null) + throw runtimeException; + } + + @Override + public int read(byte[] buffer, int offset, int len) throws IOException { + if (buffer == null) + throw new NullPointerException(); + else if (offset < 0 || len < 0 || len > buffer.length - offset) + throw new IndexOutOfBoundsException(); + else if (len == 0) + return 0; + else + return readNextLine(buffer, offset, len); + } + + private int readNextLine(byte[] buffer, int offset, int len) + throws IOException { + int c = read(); + if (c == -1) + return -1; + buffer[offset] = (byte) c; + + int i = 1; + for (; (i < len) && !isCompleteLineWritten(buffer, i - 1); ++i) { + byte read = (byte) read(); + if (read == -1) + break; + else + buffer[offset + i] = read; + } + return i; + } + + private boolean isCompleteLineWritten(byte[] buffer, + int indexLastByteWritten) { + byte[] separator = getProperty("line.separator") + .getBytes(defaultCharset()); + int indexFirstByteOfSeparator = indexLastByteWritten + - separator.length + 1; + return indexFirstByteOfSeparator >= 0 + && contains(buffer, separator, indexFirstByteOfSeparator); + } + + private boolean contains(byte[] array, byte[] pattern, int indexStart) { + for (int i = 0; i < pattern.length; ++i) + if (array[indexStart + i] != pattern[i]) + return false; + return true; + } + } + } + + /** + * Executes the statement and returns the status code that is provided to + * {@code System.exit(int)} within the statement. Additionally it avoids + * that the JVM is shut down because of a call to {@code System.exit(int)}. + *

+     *{@literal @Test}
+     * public void catch_status_code_of_System_exit() {
+     *   int statusCode = catchSystemExit((){@literal ->} {
+     *     System.exit(42);
+     *   });
+     *   assertEquals(42, statusCode);
+     * }
+     * 
+ * @param statement an arbitrary piece of code. + * @return the status code provided to {@code System.exit(int)}. + * @throws Exception any exception thrown by the statement. + * @throws AssertionError if the statement does not call + * {@code System.exit(int)}. + * @since 1.0.0 + */ + public static int catchSystemExit( + Statement statement + ) throws Exception { + NoExitSecurityManager noExitSecurityManager + = new NoExitSecurityManager(getSecurityManager()); + try { + withSecurityManager(noExitSecurityManager, statement); + } catch (CheckExitCalled ignored) { + } + return checkSystemExit(noExitSecurityManager); + } + + private static int checkSystemExit( + NoExitSecurityManager securityManager + ) { + if (securityManager.isCheckExitCalled()) + return securityManager.getStatusOfFirstCheckExitCall(); + else + throw new AssertionError("System.exit has not been called."); + } + + private static class CheckExitCalled extends SecurityException { + private static final long serialVersionUID = 159678654L; + } + + /** + * A {@code NoExitSecurityManager} throws a {@link CheckExitCalled} exception + * whenever {@link #checkExit(int)} is called. All other method calls are + * delegated to the original security manager. + */ + private static class NoExitSecurityManager extends SecurityManager { + private final SecurityManager originalSecurityManager; + private Integer statusOfFirstExitCall = null; + + NoExitSecurityManager( + SecurityManager originalSecurityManager + ) { + this.originalSecurityManager = originalSecurityManager; + } + + @Override + public void checkExit( + int status + ) { + if (statusOfFirstExitCall == null) + statusOfFirstExitCall = status; + throw new CheckExitCalled(); + } + + boolean isCheckExitCalled() { + return statusOfFirstExitCall != null; + } + + int getStatusOfFirstCheckExitCall() { + if (isCheckExitCalled()) + return statusOfFirstExitCall; + else + throw new IllegalStateException( + "checkExit(int) has not been called."); + } + + @Override + public Object getSecurityContext() { + return (originalSecurityManager == null) ? super.getSecurityContext() + : originalSecurityManager.getSecurityContext(); + } + + @Override + public void checkPermission(Permission perm) { + if (originalSecurityManager != null) + originalSecurityManager.checkPermission(perm); + } + + @Override + public void checkPermission(Permission perm, Object context) { + if (originalSecurityManager != null) + originalSecurityManager.checkPermission(perm, context); + } + + @Override + public void checkCreateClassLoader() { + if (originalSecurityManager != null) + originalSecurityManager.checkCreateClassLoader(); + } + + @Override + public void checkAccess(Thread t) { + if (originalSecurityManager != null) + originalSecurityManager.checkAccess(t); + } + + @Override + public void checkAccess(ThreadGroup g) { + if (originalSecurityManager != null) + originalSecurityManager.checkAccess(g); + } + + @Override + public void checkExec(String cmd) { + if (originalSecurityManager != null) + originalSecurityManager.checkExec(cmd); + } + + @Override + public void checkLink(String lib) { + if (originalSecurityManager != null) + originalSecurityManager.checkLink(lib); + } + + @Override + public void checkRead(FileDescriptor fd) { + if (originalSecurityManager != null) + originalSecurityManager.checkRead(fd); + } + + @Override + public void checkRead(String file) { + if (originalSecurityManager != null) + originalSecurityManager.checkRead(file); + } + + @Override + public void checkRead(String file, Object context) { + if (originalSecurityManager != null) + originalSecurityManager.checkRead(file, context); + } + + @Override + public void checkWrite(FileDescriptor fd) { + if (originalSecurityManager != null) + originalSecurityManager.checkWrite(fd); + } + + @Override + public void checkWrite(String file) { + if (originalSecurityManager != null) + originalSecurityManager.checkWrite(file); + } + + @Override + public void checkDelete(String file) { + if (originalSecurityManager != null) + originalSecurityManager.checkDelete(file); + } + + @Override + public void checkConnect(String host, int port) { + if (originalSecurityManager != null) + originalSecurityManager.checkConnect(host, port); + } + + @Override + public void checkConnect(String host, int port, Object context) { + if (originalSecurityManager != null) + originalSecurityManager.checkConnect(host, port, context); + } + + @Override + public void checkListen(int port) { + if (originalSecurityManager != null) + originalSecurityManager.checkListen(port); + } + + @Override + public void checkAccept(String host, int port) { + if (originalSecurityManager != null) + originalSecurityManager.checkAccept(host, port); + } + + @Override + public void checkMulticast(InetAddress maddr) { + if (originalSecurityManager != null) + originalSecurityManager.checkMulticast(maddr); + } + + @Override + public void checkMulticast(InetAddress maddr, byte ttl) { + if (originalSecurityManager != null) + originalSecurityManager.checkMulticast(maddr, ttl); + } + + @Override + public void checkPropertiesAccess() { + if (originalSecurityManager != null) + originalSecurityManager.checkPropertiesAccess(); + } + + @Override + public void checkPropertyAccess(String key) { + if (originalSecurityManager != null) + originalSecurityManager.checkPropertyAccess(key); + } + + @Override + public void checkPrintJobAccess() { + if (originalSecurityManager != null) + originalSecurityManager.checkPrintJobAccess(); + } + + @Override + public void checkPackageAccess(String pkg) { + if (originalSecurityManager != null) + originalSecurityManager.checkPackageAccess(pkg); + } + + @Override + public void checkPackageDefinition(String pkg) { + if (originalSecurityManager != null) + originalSecurityManager.checkPackageDefinition(pkg); + } + + @Override + public void checkSetFactory() { + if (originalSecurityManager != null) + originalSecurityManager.checkSetFactory(); + } + + @Override + public void checkSecurityAccess(String target) { + if (originalSecurityManager != null) + originalSecurityManager.checkSecurityAccess(target); + } + + @Override + public ThreadGroup getThreadGroup() { + return (originalSecurityManager == null) ? super.getThreadGroup() + : originalSecurityManager.getThreadGroup(); + } + } +} diff --git a/src/test/resources/testdata.csv b/src/test/resources/testdata.csv new file mode 100644 index 0000000..e69de29