summaryrefslogtreecommitdiff
path: root/chapter31/src/test/java/com/seaofnodes/simple/TerminalInputTest.java
blob: bd3831b4a8b85ea7142f4f2710f9ea347629c602 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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));
    }
}