diff options
Diffstat (limited to 'chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java')
| -rw-r--r-- | chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java b/chapter30/src/test/java/com/seaofnodes/simple/TerminalInputTest.java new file mode 100644 index 00000000..bd3831b4 --- /dev/null +++ b/chapter30/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)); + } +} |
