Skip to content

Commit

Permalink
Improve emulation of System.in.
Browse files Browse the repository at this point in the history
`System.in.read()` blocks until it faces the EOL character(s). This
cannot be easily emulated because that behaviour is implemented in
native code.

The first attempt was sending -1 after the end of each text or line.
This is an insufficient solution because -1 indicates the end of the
stream. It seems that the blocking behaviour is mainly needed for code
that uses Scanner (which uses `System.in.read(byte[], int, int)`
internally.) Hence the -1 hack was replaced by an enhanced
implementation of `read(byte[], int, int)` that returns the whole next
line.

Fixes #35.
  • Loading branch information
stefanbirkner committed Dec 9, 2015
1 parent eb8f080 commit ec50f4d
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
import static java.lang.System.getProperty;
import static java.lang.System.in;
import static java.lang.System.setIn;
import static java.util.Arrays.asList;
import static java.nio.charset.Charset.defaultCharset;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Iterator;
import java.util.List;

import org.junit.rules.ExternalResource;

Expand All @@ -26,42 +24,14 @@
*
* @Test
* public void readTextFromStandardInputStream() {
* systemInMock.provideText("foo");
* Scanner scanner = new Scanner(System.in);
* assertEquals("foo", scanner.nextLine());
* systemInMock.provideLines("foo", "bar");
* Scanner firstScanner = new Scanner(System.in);
* scanner.nextLine();
* assertEquals("bar", scanner.nextLine());
* }
* }
* </pre>
*
* <h3>Multiple Texts</h3>
* You can simulate a user that stops typing and continues afterwards
* by providing multiple texts.
* <pre>
* &#064;Test
* public void readTextFromStandardInputStream() {
* systemInMock.provideText("foo\n", "bar\n");
* Scanner firstScanner = new Scanner(System.in);
* scanner.nextLine();
* Scanner secondScanner = new Scanner(System.in);
* assertEquals("bar", scanner.nextLine());
* }
* </pre>
*
* <p>If every text is a single line then you can use the method
* {@link #provideLines(String...)} that appends the end of line
* characters according to {@code System.getProperty("line.separator")}
* to each text.
* <pre>
* &#064;Test
* public void readTextFromStandardInputStream() {
* systemInMock.provideLines("foo", "bar");
* Scanner firstScanner = new Scanner(System.in);
* scanner.nextLine();
* Scanner secondScanner = new Scanner(System.in);
* assertEquals("bar", scanner.nextLine());
* }
* </pre>
*
* <h3>Throwing Exceptions</h3>
* <p>{@code TextFromStandardInputStream} can also simulate a {@code System.in}
* that throws an {@code IOException} or {@code RuntimeException}. Use
Expand All @@ -85,7 +55,7 @@ public static TextFromStandardInputStream emptyStandardInputStream() {
* specified text.
*
* @param text this text is return by {@code System.in}.
* @deprecated use {@link #provideText(String...)}
* @deprecated use {@link #provideLines(String...)}
*/
@Deprecated
public TextFromStandardInputStream(String text) {
Expand All @@ -95,27 +65,26 @@ public TextFromStandardInputStream(String text) {
/**
* Set the text that is returned by {@code System.in}. You can
* provide multiple texts. In that case {@code System.in.read()}
* returns -1 once when the end of a single text is reached and
* continues with the next text afterwards.
* continues with the next text after a text has been completely
* read.
*
* @param texts a list of texts.
* @deprecated please use {@link #provideLines(String...)}
*/
@Deprecated
public void provideText(String... texts) {
systemInMock.provideText(asList(texts));
systemInMock.provideText(join(texts));
}

/**
* Set the lines that are returned by {@code System.in}.
* {@code System.getProperty("line.separator")} is used for the end
* of line. {@code System.in.read()} returns -1 once when the end
* of a single line is reached and continues with the next line
* afterwards.
* of line.
*
* @param lines a list of lines.
*/
public void provideLines(String... lines) {
String[] texts = appendEndOfLineToLines(lines);
provideText(texts);
systemInMock.provideText(joinLines(lines));
}

/**
Expand Down Expand Up @@ -148,11 +117,18 @@ public void throwExceptionOnInputEnd(RuntimeException exception) {
systemInMock.throwExceptionOnInputEnd(exception);
}

private String[] appendEndOfLineToLines(String[] lines) {
String[] texts = new String[lines.length];
for (int index = 0; index < lines.length; ++index)
texts[index] = lines[index] + getProperty("line.separator");
return texts;
private String join(String[] texts) {
StringBuilder sb = new StringBuilder();
for (String text: texts)
sb.append(text);
return sb.toString();
}

private String joinLines(String[] lines) {
StringBuilder sb = new StringBuilder();
for (String line: lines)
sb.append(line).append(getProperty("line.separator"));
return sb.toString();
}

@Override
Expand All @@ -167,14 +143,12 @@ protected void after() {
}

private static class SystemInMock extends InputStream {
private Iterator<String> texts;
private StringReader currentReader;
private IOException ioException;
private RuntimeException runtimeException;

void provideText(List<String> texts) {
this.texts = texts.iterator();
updateReader();
void provideText(String text) {
currentReader = new StringReader(text);
}

void throwExceptionOnInputEnd(IOException exception) {
Expand Down Expand Up @@ -203,18 +177,53 @@ public int read() throws IOException {
return character;
}

@Override
public int read(byte[] b, int offset, int len) throws IOException {
if (b == null)
throw new NullPointerException();
else if (offset < 0 || len < 0 || len > b.length - offset)
throw new IndexOutOfBoundsException();
else if (len == 0)
return 0;
else
return readUntilNextLineBreak(b, offset, len);
}

private int readUntilNextLineBreak(byte[] b, int offset, int len) throws IOException {
int c = read();
if (c == -1)
return -1;
b[offset] = (byte) c;

int i = 1;
for (; (i < len) && !lineEnded(b, i); ++i) {
byte read = (byte) read();
if (read == -1)
break;
else
b[offset + i] = read;
}
return i;
}

private boolean lineEnded(byte[] buffer, int posAfterEndOfLine) {
byte[] separator = getProperty("line.separator")
.getBytes(defaultCharset());
int posFirstCharEndOfLine = posAfterEndOfLine - separator.length;
if (posFirstCharEndOfLine < 0)
return false;
else
for (int i = 0; i < separator.length; ++i)
if (buffer[posFirstCharEndOfLine + i] != separator[i])
return false;
return true;
}

private void handleEmptyReader() throws IOException {
if (texts.hasNext())
updateReader();
else if (ioException != null)
if (ioException != null)
throw ioException;
else if (runtimeException != null)
throw runtimeException;
}

private void updateReader() {
if (texts.hasNext())
currentReader = new StringReader(texts.next());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.github.stefanbirkner.fishbowl.Fishbowl.exceptionThrownBy;
import static java.lang.System.in;
import static org.assertj.core.api.Assertions.allOf;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.contrib.java.lang.system.Executor.exceptionThrownWhenTestIsExecutedWithRule;
import static org.junit.contrib.java.lang.system.Executor.executeTestWithRule;
Expand All @@ -12,12 +13,16 @@
import java.io.InputStream;
import java.util.Scanner;

import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;
import org.junit.runners.model.Statement;

public class TextFromStandardInputStreamTest {
private static final byte[] DUMMY_ARRAY = new byte[1024];
private static final int VALID_OFFSET = 2;
private static final int VALID_READ_LENGTH = 100;
private static final IOException DUMMY_IO_EXCEPTION = new IOException();
private static final RuntimeException DUMMY_RUNTIME_EXCEPTION = new RuntimeException();
private static final com.github.stefanbirkner.fishbowl.Statement READ_NEXT_BYTE
Expand All @@ -27,11 +32,14 @@ public void evaluate() throws Throwable {
}
};

@Rule
public final Timeout timeout = new Timeout(1000);

private final TextFromStandardInputStream systemInMock = emptyStandardInputStream();

@BeforeClass
public static void checkArrayConstants() {
assertThat(VALID_OFFSET).isBetween(0, DUMMY_ARRAY.length);
assertThat(VALID_READ_LENGTH).isBetween(0, DUMMY_ARRAY.length - VALID_OFFSET);
}

@Test
public void provided_text_is_available_from_system_in() {
executeTestWithRule(new Statement() {
Expand Down Expand Up @@ -193,6 +201,107 @@ public void after_the_test_system_in_is_same_as_before() {
assertThat(in).isSameAs(originalSystemIn);
}

@Test
public void system_in_throws_NullPointerException_when_read_is_called_with_null_array() {
//this is default behaviour of an InputStream according to its JavaDoc
Throwable exception = exceptionThrownWhenTestIsExecutedWithRule(
new Statement() {
@Override
public void evaluate() throws Throwable {
System.in.read(null);
}
}, systemInMock);
assertThat(exception).isInstanceOf(NullPointerException.class);
}

@Test
public void system_in_throws_IndexOutOfBoundsException_when_read_is_called_with_negative_offset() {
//this is default behaviour of an InputStream according to its JavaDoc
Throwable exception = exceptionThrownWhenTestIsExecutedWithRule(
new Statement() {
@Override
public void evaluate() throws Throwable {
System.in.read(DUMMY_ARRAY, -1, VALID_READ_LENGTH);
}
}, systemInMock);
assertThat(exception).isInstanceOf(IndexOutOfBoundsException.class);
}

@Test
public void system_in_throws_IndexOutOfBoundsException_when_read_is_called_with_negative_length() {
//this is default behaviour of an InputStream according to its JavaDoc
Throwable exception = exceptionThrownWhenTestIsExecutedWithRule(
new Statement() {
@Override
public void evaluate() throws Throwable {
System.in.read(DUMMY_ARRAY, VALID_OFFSET, -1);
}
}, systemInMock);
assertThat(exception).isInstanceOf(IndexOutOfBoundsException.class);
}

@Test
public void system_in_throws_IndexOutOfBoundsException_when_read_is_called_with_oversized_length() {
//this is default behaviour of an InputStream according to its JavaDoc
Throwable exception = exceptionThrownWhenTestIsExecutedWithRule(
new Statement() {
@Override
public void evaluate() throws Throwable {
int oversizedLength = DUMMY_ARRAY.length - VALID_OFFSET + 1;
System.in.read(DUMMY_ARRAY, VALID_OFFSET, oversizedLength);
}
}, systemInMock);
assertThat(exception).isInstanceOf(IndexOutOfBoundsException.class);
}

@Test
public void system_in_reads_zero_bytes_even_mock_should_throw_IOException_on_input_end() {
executeTestWithRule(new Statement() {
@Override
public void evaluate() throws Throwable {
systemInMock.throwExceptionOnInputEnd(DUMMY_IO_EXCEPTION);
int numBytesRead = System.in.read(DUMMY_ARRAY, VALID_OFFSET, 0);
assertThat(numBytesRead).isZero();
}
}, systemInMock);
}

@Test
public void system_in_reads_zero_bytes_even_mock_should_throw_RuntimeException_on_input_end() {
executeTestWithRule(new Statement() {
@Override
public void evaluate() throws Throwable {
systemInMock.throwExceptionOnInputEnd(DUMMY_RUNTIME_EXCEPTION);
int numBytesRead = System.in.read(DUMMY_ARRAY, VALID_OFFSET, 0);
assertThat(numBytesRead).isZero();
}
}, systemInMock);
}

@Test
public void system_in_read_bytes_throws_specified_IOException_on_input_end() {
Throwable exception = exceptionThrownWhenTestIsExecutedWithRule(new Statement() {
@Override
public void evaluate() throws Throwable {
systemInMock.throwExceptionOnInputEnd(DUMMY_IO_EXCEPTION);
System.in.read(DUMMY_ARRAY, VALID_OFFSET, VALID_READ_LENGTH);
}
}, systemInMock);
assertThat(exception).isSameAs(DUMMY_IO_EXCEPTION);
}

@Test
public void system_in_read_bytes_throws_specified_RuntimeException_on_input_end() {
Throwable exception = exceptionThrownWhenTestIsExecutedWithRule(new Statement() {
@Override
public void evaluate() throws Throwable {
systemInMock.throwExceptionOnInputEnd(DUMMY_RUNTIME_EXCEPTION);
System.in.read(DUMMY_ARRAY, VALID_OFFSET, VALID_READ_LENGTH);
}
}, systemInMock);
assertThat(exception).isSameAs(DUMMY_RUNTIME_EXCEPTION);
}

private void assertSystemInProvidesText(String text) throws IOException {
for (char c : text.toCharArray())
assertThat((char) System.in.read()).isSameAs(c);
Expand Down

0 comments on commit ec50f4d

Please sign in to comment.