diff --git a/gclc-test/pom.xml b/gclc-test/pom.xml index 4fbf936..9630ae3 100644 --- a/gclc-test/pom.xml +++ b/gclc-test/pom.xml @@ -56,6 +56,11 @@ junit 4.12 + + net.bigeon.test + junitmt + 1.0.2 + diff --git a/gclc-test/src/main/java/net/bigeon/gclc/test/InputContract.java b/gclc-test/src/main/java/net/bigeon/gclc/test/InputContract.java index a314f96..4e4bf65 100644 --- a/gclc-test/src/main/java/net/bigeon/gclc/test/InputContract.java +++ b/gclc-test/src/main/java/net/bigeon/gclc/test/InputContract.java @@ -8,18 +8,19 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; +import java.io.InterruptedIOException; import java.util.function.Supplier; import net.bigeon.gclc.manager.ConsoleInput; +import net.bigeon.test.junitmt.FunctionalTestRunnable; +import net.bigeon.test.junitmt.TestFunction; +import net.bigeon.test.junitmt.ThreadTest; -/** - * @author Emmanuel Bigeon - * - */ +/** @author Emmanuel Bigeon */ public final class InputContract { public void testInputContract(final Supplier 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); + } }