inputs)
- throws IOException {
+ throws IOException, InterruptedException {
// Test close contract
final ConsoleInput input = inputs.get();
assertFalse("An input should not initially be closed", input.isClosed());
@@ -33,5 +34,29 @@ public final class InputContract {
} catch (final IOException e) {
// ok
}
+
+ // Test interruption contract
+ final ConsoleInput input2 = inputs.get();
+ final FunctionalTestRunnable prompting = new FunctionalTestRunnable(
+ new TestFunction() {
+
+ @Override
+ public void apply() throws Exception {
+ try {
+ input2.prompt();
+ fail("Interrupted prompt should throw INterruptedIOException");
+ } catch (final InterruptedIOException e) {
+ // ok
+ }
+ }
+ });
+ final Thread th = new Thread(prompting);
+ th.start();
+// while (!input2.isPrompting()) {
+ th.join(200);
+//
+// }
+ input2.interruptPrompt();
+ ThreadTest.assertRuns(th, prompting);
}
}
diff --git a/gclc/src/main/java/net/bigeon/gclc/manager/ConsoleInput.java b/gclc/src/main/java/net/bigeon/gclc/manager/ConsoleInput.java
index f292a06..63b3945 100644
--- a/gclc/src/main/java/net/bigeon/gclc/manager/ConsoleInput.java
+++ b/gclc/src/main/java/net/bigeon/gclc/manager/ConsoleInput.java
@@ -94,8 +94,7 @@ public interface ConsoleInput extends AutoCloseable {
/** Indicate to the input that is should interrompt the prompting, if possible.
*
* The pending {@link #prompt()} or {@link #prompt(String)} operations should
- * return immediatly. However the returned value can be anything (from the
- * partial prompt content to an empty string or even a null pointer). */
+ * return immediately by throwing an InterruptedIOException. */
void interruptPrompt();
/** Test if the input is closed.
diff --git a/gclc/src/main/java/net/bigeon/gclc/utils/ReadingRunnable.java b/gclc/src/main/java/net/bigeon/gclc/utils/ReadingRunnable.java
index 4095129..0572e7e 100644
--- a/gclc/src/main/java/net/bigeon/gclc/utils/ReadingRunnable.java
+++ b/gclc/src/main/java/net/bigeon/gclc/utils/ReadingRunnable.java
@@ -111,6 +111,7 @@ public final class ReadingRunnable implements Runnable {
private final Map messageBlocker = new ConcurrentHashMap<>();
/** The lock. */
private final Object messageBlockerLock = new Object();
+ private boolean interrupting = false;
/** Create a reading runnable.
*
* @param reader the input to read from */
@@ -146,8 +147,7 @@ public final class ReadingRunnable implements Runnable {
public String getMessage() throws IOException {
synchronized (lock) {
if (!messages.isEmpty()) {
- notifyMessage(messages.peek());
- return messages.poll();
+ return pollMessage();
}
if (!running) {
throw new IOException(CLOSED_PIPE);
@@ -156,8 +156,7 @@ public final class ReadingRunnable implements Runnable {
waitMessage(TIMEOUT);
LOGGER.log(Level.FINEST, "Polled: {0}", messages.peek()); //$NON-NLS-1$
waiting = false;
- notifyMessage(messages.peek());
- return messages.poll();
+ return pollMessage();
}
}
@@ -169,8 +168,7 @@ public final class ReadingRunnable implements Runnable {
public String getNextMessage(final long timeout) throws IOException {
synchronized (lock) {
if (!messages.isEmpty()) {
- notifyMessage(messages.peek());
- return messages.poll();
+ return pollMessage();
}
if (!running) {
throw new IOException(CLOSED_PIPE);
@@ -181,10 +179,18 @@ public final class ReadingRunnable implements Runnable {
if (messages.isEmpty()) {
return null;
}
- notifyMessage(messages.peek());
- return messages.poll();
+ return pollMessage();
}
}
+
+ private String pollMessage() throws InterruptedIOException {
+ final String msg = messages.poll();
+ if (msg.isEmpty() && interrupting) {
+ throw new InterruptedIOException();
+ }
+ notifyMessage(msg);
+ return msg;
+ }
/** Wait for a message to be delivered.
*
@@ -221,7 +227,8 @@ public final class ReadingRunnable implements Runnable {
public void interrupt() {
synchronized (lock) {
if (waiting) {
- messages.offer(""); //$NON-NLS-1$
+ messages.offer("");
+ interrupting = true;
lock.notifyAll();
}
}
diff --git a/gclc/src/test/java/net/bigeon/gclc/manager/ReadingRunnableTest.java b/gclc/src/test/java/net/bigeon/gclc/manager/ReadingRunnableTest.java
index ec47eb8..1cddc6a 100644
--- a/gclc/src/test/java/net/bigeon/gclc/manager/ReadingRunnableTest.java
+++ b/gclc/src/test/java/net/bigeon/gclc/manager/ReadingRunnableTest.java
@@ -80,6 +80,7 @@ import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.InterruptedIOException;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
@@ -90,6 +91,9 @@ import org.junit.Before;
import org.junit.Test;
import net.bigeon.gclc.utils.ReadingRunnable;
+import net.bigeon.test.junitmt.FunctionalTestRunnable;
+import net.bigeon.test.junitmt.TestFunction;
+import net.bigeon.test.junitmt.ThreadTest;
/**
*
@@ -198,9 +202,11 @@ public class ReadingRunnableTest {
public final void testGetPendingMessages() throws IOException, InterruptedException {
final PipedOutputStream out = new PipedOutputStream();
- final BufferedReader reader = new BufferedReader(new InputStreamReader(new PipedInputStream(out), StandardCharsets.UTF_8));
+ final BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new PipedInputStream(out), StandardCharsets.UTF_8));
final ReadingRunnable runnable = new ReadingRunnable(reader);
- final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
+ final BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(out, StandardCharsets.UTF_8));
writer.write("one");
writer.newLine();
writer.write("two");
@@ -222,4 +228,30 @@ public class ReadingRunnableTest {
// ok
}
}
+
+ @Test
+ public void testInterruption() throws IOException, InterruptedException {
+ final PipedOutputStream out = new PipedOutputStream();
+ final BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new PipedInputStream(out), StandardCharsets.UTF_8));
+ final ReadingRunnable runnable = new ReadingRunnable(reader);
+
+ final FunctionalTestRunnable test = new FunctionalTestRunnable(
+ new TestFunction() {
+ @Override
+ public void apply() throws IOException {
+ try {
+ runnable.getMessage();
+ fail("Message interruption should cause an exception");
+ } catch (final InterruptedIOException e) {
+ // ok
+ }
+ }
+ });
+ final Thread th = new Thread(test, "Prompt wait");
+ th.start();
+ th.join(200);
+ runnable.interrupt();
+ ThreadTest.assertRuns(th, test);
+ }
}