diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..9d886be --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +buy-and-sell-n-times \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 2174bb4..61f0c30 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,5 +4,5 @@ - - + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..8d3e42f 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,5 +1,10 @@ + + + diff --git a/build.gradle b/build.gradle index 27e6d58..cabb3c1 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,9 @@ allprojects { } 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' diff --git a/src/main/java/Solution.java b/src/main/java/Solution.java index 887d93d..67e71fa 100644 --- a/src/main/java/Solution.java +++ b/src/main/java/Solution.java @@ -2,56 +2,61 @@ import java.util.*; import java.util.stream.IntStream; class Solution { - - public static void main( String[] args ) { - // read values with in.next...() methods - var scanner = new Scanner( System.in ); - var sellCount = scanner.nextInt(); - var prices = scanner.tokens() - .mapToInt( Integer::valueOf ) - .toArray(); - - // Write result with System.out.println() - System.out.println( maxProfits( sellCount, prices )); - } - - // code your solution here - // 0 - 1 | ... | x-sellCount - x --> 0 - x-1 | ... | x-1 - x - private static int maxProfits( int sellCount, int[] prices ) { - return IntStream.range( 0, sellCount ) - .parallel() - .map( sale -> { - // recursion end condition - if ( sellCount == 1 ) { - return maxProfit( prices ); - } - else { - return IntStream.range( 2, prices.length ) - .parallel() - .map( index -> { - var firstRange = Arrays.copyOfRange( prices, 0, index ); - final int max1 = maxProfit( firstRange ); - - var secondRange = Arrays.copyOfRange( prices, index - 1, prices.length ); - // reduce range sizes recursively - final int max2 = maxProfits( sellCount - 1, secondRange ); - - return max1 + max2; - }).max().orElse( 0 ); - } - }) - .max().orElse( 0 ); - } - - private static int maxProfit( int[] prices ){ - return IntStream.range( 0, prices.length ) - .map( index -> { - var range = Arrays.copyOfRange( prices, Math.min( index + 1, prices.length - 1 ), prices.length ); - int max = IntStream.of( range ).max().orElse( 0 ); - // difference between current price and future maximum - return max - prices[index]; - }) - .max().orElse( 0 ); - } - + + public static void main( String[] args ) { + // read values with in.next...() methods + var scanner = new Scanner( System.in ); + var sellCount = scanner.nextInt(); + var prices = scanner.tokens() + .mapToInt( Integer::valueOf ) + .toArray(); + + // Write result with System.out.println() + System.out.println( maxProfits( sellCount, prices )); + } + + // code your solution here + // 0 - 1 | ... | x-sellCount - x --> 0 - x-1 | ... | x-1 - x + private static int maxProfits( int sellCount, int[] prices ) { + return IntStream.range( 0, sellCount ) + .parallel() + .map( sale -> { + // recursion end condition + if ( sellCount == 1 ) { + return maxProfit( prices ); + } + else { + return IntStream.range( 2, prices.length ) + .parallel() + .map( index -> { + var firstRange = Arrays.copyOfRange( prices, 0, index ); + final int max1 = maxProfit( firstRange ); + + var secondRange = Arrays.copyOfRange( prices, index - 1, + prices.length + ); + // reduce range sizes recursively + final int max2 = maxProfits( sellCount - 1, secondRange ); + + return max1 + max2; + }) + .max().orElse( 0 ); + } + }) + .max().orElse( 0 ); + } + + private static int maxProfit( int[] prices ) { + return IntStream.range( 0, prices.length ) + .map( index -> { + var range = Arrays.copyOfRange( prices, Math.min( index + 1, prices.length - 1 ), + prices.length + ); + int max = IntStream.of( range ).max().orElse( 0 ); + // difference between current price and future maximum + return max - prices[index]; + }) + .max().orElse( 0 ); + } + } diff --git a/src/test/java/BenchmarkExtension.java b/src/test/java/BenchmarkExtension.java new file mode 100644 index 0000000..ab126aa --- /dev/null +++ b/src/test/java/BenchmarkExtension.java @@ -0,0 +1,51 @@ +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +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 index 6c6b8e2..ac642f9 100644 --- a/src/test/java/SolutionTest.java +++ b/src/test/java/SolutionTest.java @@ -1,77 +1,23 @@ -import java.io.*; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import com.github.stefanbirkner.systemlambda.SystemLambda; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.*; +@ExtendWith( BenchmarkExtension.class ) class SolutionTest { - - static long totalDuration; - - @BeforeAll - static void beforeAll() { - totalDuration = 0; - } - - @AfterAll - static void afterAll() { - System.out.println( String.format( "All solutions took %01d.%03d secs", totalDuration / 1000, totalDuration % 1000 )); - } - - @ParameterizedTest - @CsvFileSource( resources = "testdata.csv", numLinesToSkip = 1 ) - void main( final String sellCount, final String input, final String expected ) throws IOException { - // keep original streams - InputStream oldIn = System.in; - PrintStream oldOut = System.out; - PrintStream oldErr = System.err; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - try { - redirectStreams( - new ByteArrayInputStream( (sellCount + " " + input).getBytes( UTF_8 )), - new PrintStream( bos, true, UTF_8 ), - new PrintStream( new ByteArrayOutputStream(), true, UTF_8 ) - ); - - // start time tracking - long start = System.currentTimeMillis(); - - Solution.main( new String[0] ); - - // stop time tracking - long duration = System.currentTimeMillis() - start; - totalDuration += duration; - - // restore streams - redirectStreams( oldIn, oldOut, oldErr ); - - System.out.println( String.format( "Solution took %01d.%03d secs", duration / 1000, duration % 1000 )); - - try (BufferedReader chk = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( bos.toByteArray() ), UTF_8 ))) { - String[] expectedLines = expected.split( "\\s*[|]\\s*" ); - - int lineCount = 0; - String line; - for ( ; (line = chk.readLine()) != null; lineCount++ ) { - assertEquals( expectedLines[ lineCount ], line ); - } - assertEquals( expectedLines.length, lineCount ); - } - } - finally { - // restore streams - redirectStreams( oldIn, oldOut, oldErr ); - } - } - - static void redirectStreams( final InputStream input, final PrintStream output, final PrintStream error ) { - System.setIn( input ); - System.setOut( output ); - System.setErr( error ); - } + + @ParameterizedTest + @CsvFileSource( resources = "testdata.csv" ) + void main( final String sellCount, final String input, final String expected ) throws Exception { + String output = SystemLambda.tapSystemOutNormalized( () -> + SystemLambda.withTextFromSystemIn( sellCount + " " + input ) + .execute( () -> Solution.main( new String[0] )) + ); + + assertEquals( expected, StringUtils.chomp( 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..733b3ae --- /dev/null +++ b/src/test/java/com/github/stefanbirkner/systemlambda/Statement.java @@ -0,0 +1,15 @@ +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 + *

    + *
  • Test code that calls {@code System.exit(int)}: + * {@link #catchSystemExit(Statement)}
  • + *
+ */ +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 index a5724f4..462b97a 100644 --- a/src/test/resources/testdata.csv +++ b/src/test/resources/testdata.csv @@ -1,4 +1,3 @@ -sellCount, prices, maxProfit 2, 12 11 13 9 12 8 14 13 15, 10 3, 12 11 13 9 12 8 14 13 15, 12 2, 310 315 275 295 260 270 290 230 255 250, 55