summaryrefslogtreecommitdiff
path: root/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-08 14:10:02 -0800
commit44505aa5b75b1046f13f2bc2a52fe0b16d975838 (patch)
treecc558485c807ac3842cd73943cf5e294c4a307de /src/test/java/com/seaofnodes/simple/TerminalInputTest.java
parent2560096fb4636263798ae56c88687973c72cdf6e (diff)
downloadSimple-linear-chapter30.tar.gz
Simple-linear-chapter30.zip
Chapter 30: Text Formatting and Diagnosticslinear-chapter30
Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/test/java/com/seaofnodes/simple/TerminalInputTest.java')
-rw-r--r--src/test/java/com/seaofnodes/simple/TerminalInputTest.java184
1 files changed, 184 insertions, 0 deletions
diff --git a/src/test/java/com/seaofnodes/simple/TerminalInputTest.java b/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
new file mode 100644
index 00000000..bd3831b4
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
@@ -0,0 +1,184 @@
+package com.seaofnodes.simple;
+
+import com.seaofnodes.simple.codegen.CodeGen;
+import com.seaofnodes.simple.util.Ary;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class TerminalInputTest {
+ @Test public void testLinkOnlyAndEnterEscape() throws Exception {
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ String output="build/objs/linkOnlyGame/";
+ // Linking must return without displaying the game or waiting for input.
+ assertEquals("",TestC.exec(20,java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"--link","-L",TestC.RELEASE_SYS_DIR,"-o",output,
+ "docs/examples/TicTacToe.smp"));
+ Path exe=Path.of(output,"TicTacToe"+(TestC.OS.startsWith("Windows") ? ".exe" : ""));
+ assertTrue(Files.isRegularFile(exe));
+ Process child=new ProcessBuilder(exe.toString()).redirectErrorStream(true).start();
+ try {
+ child.getOutputStream().write(new byte[]{'\r','\n',27});
+ child.getOutputStream().flush(); // Keep stdin open: Escape must cause the exit.
+ assertTrue("Escape did not quit",child.waitFor(10,TimeUnit.SECONDS));
+ String text=new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8);
+ assertEquals(text,0,child.exitValue());
+ assertTrue(text,text.endsWith("Your move (1-9): \nGoodbye!\n"));
+ assertFalse(text,text.contains("Choose a square"));
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+
+ @Test public void testRedirectedBytesAndEOF() throws IOException {
+ TestC.run("""
+ val main={ u8[~][] args ->
+ if(sys.io.getch()!='5') return 1;
+ if(sys.io.getch()!='q') return 2;
+ if(sys.io.getch()!=-1) return 3;
+ if(sys.libc.errno()!=sys.libc.EOF) return 4;
+ sys.io.p("ok\n"); return 0;
+ };
+ ""","getchBytes",new Ary<>(new String[]{TestC.RELEASE_SYS_DIR}),
+ TestC.CALL_CONVENTION,null,null,"5q","ok\n",-1);
+ }
+
+ @Test public void testReadError() throws IOException {
+ TestC.runSYS("""
+ val main={ u8[~][] args ->
+ sys.io.close(sys.libc.STDIN);
+ if(sys.io.getch()!=-1) return 1;
+ if(sys.libc.errno()<=0) return 2;
+ return 0;
+ };
+ ""","getchError","",-1);
+ }
+
+ @Test public void testGuardsStayInsideFunction() {
+ String src="""
+ val first={ int n -> while(true) { if(n) return 0; return 1; } };
+ val second={ -> int sum=0; for(int i=0; i<3; i++) sum+=i; return sum; };
+ return first(((arg<<32)>>32))+second();
+ """;
+ for(long seed:new long[]{1,123,456}) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("4",Eval2.eval(code,0));
+ assertEquals("3",Eval2.eval(code,1));
+ }
+ }
+
+ @Test public void testForwardReferenceAfterContinue() {
+ String src="""
+ val run={ int n ->
+ while(n) {
+ if(n==1) { n=0; continue; }
+ if(later(n)) return 7;
+ n=0;
+ }
+ return 3;
+ };
+ val later={ int n -> n==2; };
+ return run(((arg<<32)>>32));
+ """;
+ for(long seed:new long[]{1,123,456}) {
+ CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("3",Eval2.eval(code,1));
+ assertEquals("7",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ // The same merge is used by break, including the implicit loop exit.
+ String breaking=src.replace("return 7;","break;").replace("return 3;","return n+3;");
+ CodeGen code2=new CodeGen(breaking,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("3",Eval2.eval(code2,1));
+ assertEquals("5",Eval2.eval(code2,2));
+ assertEquals("3",Eval2.eval(code2,3));
+ }
+ }
+
+ @Test public void testForwardReferenceWithLoopLocal() {
+ String src="""
+ val run={ int n ->
+ while(n) {
+ int key=n;
+ n--;
+ if(key==1) continue;
+ if(later(key)) continue;
+ return key;
+ }
+ return 0;
+ };
+ val later={ int key -> key==2; };
+ return run(((arg<<32)>>32));
+ """;
+ String nested=src.replace("if(later(key)) continue;",
+ "if(later(key)) { int local=key+1; continue; }");
+ String breaking=nested.replace("int local=key+1; continue;","int local=key+1; break;")
+ .replace("return 0;","return n+10;");
+ String twoRefs=src.replace("return key;","if(other(key)) continue; return key;")
+ .replace("return run(((arg<<32)>>32));","val other={ int key -> key==3; }; return run(((arg<<32)>>32));");
+ for(long seed:new long[]{1,123,456}) {
+ for(String variant:new String[]{src,nested}) {
+ CodeGen code=new CodeGen(variant,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code,1));
+ assertEquals("0",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ }
+ CodeGen code=new CodeGen(breaking,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("10",Eval2.eval(code,1));
+ assertEquals("11",Eval2.eval(code,2));
+ assertEquals("3",Eval2.eval(code,3));
+ CodeGen code2=new CodeGen(twoRefs,seed,true).driver(CodeGen.Phase.TypeCheck);
+ assertEquals("0",Eval2.eval(code2,3));
+ assertEquals("4",Eval2.eval(code2,4));
+ }
+ }
+
+ @Test public void testInteractiveCLIShowsPromptBeforeInput() throws Exception {
+ // Pipe input rather than a terminal: this catches CLI buffering and
+ // requires exactly one byte, with no Enter.
+ String java=Path.of(System.getProperty("java.home"),"bin","java").toString();
+ Process child=new ProcessBuilder(java,"-ea","-cp",System.getProperty("java.class.path"),
+ Simple.class.getName(),"-L",TestC.RELEASE_SYS_DIR,"-o","build/objs/interactiveGame/",
+ "docs/examples/TicTacToe.smp").redirectErrorStream(true).start();
+ try(var reader=Executors.newSingleThreadExecutor()) {
+ try {
+ var prompt=reader.submit(() -> readPrompt(child));
+ String opening=prompt.get(20,TimeUnit.SECONDS);
+ assertTrue(opening,opening.contains("You go first."));
+ assertTrue(opening,opening.contains(" | | \n"));
+ assertTrue(child.isAlive());
+ child.getOutputStream().write('5');
+ child.getOutputStream().flush();
+ String turn=reader.submit(() -> readPrompt(child)).get(20,TimeUnit.SECONDS);
+ assertEquals("5\n | | \n---+---+---\n | X | \n---+---+---\n | | \n"
+ +"CPU plays 1.\n O | | \n---+---+---\n | X | \n---+---+---\n | | \n"
+ +"Your move (1-9): ",turn);
+ assertTrue(child.isAlive());
+ child.getOutputStream().write(27);
+ child.getOutputStream().flush();
+ assertTrue("Escape did not quit",child.waitFor(10,TimeUnit.SECONDS));
+ assertEquals("\nGoodbye!\n",new String(child.getInputStream().readAllBytes(),StandardCharsets.UTF_8));
+ assertEquals(0,child.exitValue());
+ } finally {
+ child.destroyForcibly();
+ }
+ }
+ }
+
+ private static String readPrompt(Process child) throws IOException {
+ var bytes=new ByteArrayOutputStream();
+ while(bytes.size()<8192) {
+ int ch=child.getInputStream().read();
+ if(ch<0) throw new IOException("Exited before prompt: "+bytes.toString(StandardCharsets.UTF_8));
+ bytes.write(ch);
+ String text=bytes.toString(StandardCharsets.UTF_8);
+ if(text.endsWith("Your move (1-9): ")) return text;
+ }
+ throw new IOException("No prompt: "+bytes.toString(StandardCharsets.UTF_8));
+ }
+}