ReplToolTesting.java revision 3062:15bdc18525ff
1/*
2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24import java.io.ByteArrayOutputStream;
25import java.io.OutputStream;
26import java.io.PrintStream;
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.HashMap;
30import java.util.Iterator;
31import java.util.List;
32import java.util.Map;
33import java.util.Map.Entry;
34import java.util.function.Consumer;
35import java.util.function.Predicate;
36import java.util.regex.Matcher;
37import java.util.regex.Pattern;
38import java.util.stream.Collectors;
39import java.util.stream.Stream;
40
41import jdk.internal.jshell.tool.JShellTool;
42import jdk.jshell.SourceCodeAnalysis.Suggestion;
43
44import static org.testng.Assert.assertEquals;
45import static org.testng.Assert.assertNotNull;
46import static org.testng.Assert.assertTrue;
47import static org.testng.Assert.fail;
48
49public class ReplToolTesting {
50
51    private final static String DEFAULT_STARTUP_MESSAGE = "|  Welcome to";
52
53    private WaitingTestingInputStream cmdin = null;
54    private ByteArrayOutputStream cmdout = null;
55    private ByteArrayOutputStream cmderr = null;
56    private PromptedCommandOutputStream console = null;
57    private TestingInputStream userin = null;
58    private ByteArrayOutputStream userout = null;
59    private ByteArrayOutputStream usererr = null;
60
61    private List<MemberInfo> keys;
62    private Map<String, VariableInfo> variables;
63    private Map<String, MethodInfo> methods;
64    private Map<String, ClassInfo> classes;
65    private boolean isDefaultStartUp = true;
66
67    public JShellTool repl = null;
68
69    public interface ReplTest {
70        void run(boolean after);
71    }
72
73    public void setCommandInput(String s) {
74        cmdin.setInput(s);
75    }
76
77    public final static Pattern idPattern = Pattern.compile("^\\s+(\\d+)");
78    public Consumer<String> assertList() {
79        return s -> {
80            List<String> lines = Stream.of(s.split("\n"))
81                    .filter(l -> !l.isEmpty())
82                    .collect(Collectors.toList());
83            int previousId = Integer.MIN_VALUE;
84            assertEquals(lines.size(), keys.size(), "Number of keys");
85            for (int i = 0; i < lines.size(); ++i) {
86                String line = lines.get(i);
87                Matcher matcher = idPattern.matcher(line);
88                assertTrue(matcher.find(), "Snippet id not found: " + line);
89                String src = keys.get(i).getSource();
90                assertTrue(line.endsWith(src), "Line '" + line + "' does not end with: " + src);
91                int id = Integer.parseInt(matcher.group(1));
92                assertTrue(previousId < id,
93                        String.format("The previous id is not less than the next one: previous: %d, next: %d",
94                                previousId, id));
95                previousId = id;
96            }
97        };
98    }
99
100    private final static Pattern extractPattern = Pattern.compile("^\\| *(.*)$");
101    private Consumer<String> assertMembers(String message, Map<String, ? extends MemberInfo> set) {
102        return s -> {
103            List<String> lines = Stream.of(s.split("\n"))
104                    .filter(l -> !l.isEmpty())
105                    .collect(Collectors.toList());
106            assertEquals(lines.size(), set.size(), message + " : expected: " + set.keySet() + "\ngot:\n" + lines);
107            for (String line : lines) {
108                Matcher matcher = extractPattern.matcher(line);
109                assertTrue(matcher.find(), line);
110                String src = matcher.group(1);
111                MemberInfo info = set.get(src);
112                assertNotNull(info, "Not found snippet with signature: " + src + ", line: "
113                        + line + ", keys: " + set.keySet() + "\n");
114            }
115        };
116    }
117
118    public Consumer<String> assertVariables() {
119        return assertMembers("Variables", variables);
120    }
121
122    public Consumer<String> assertMethods() {
123        return assertMembers("Methods", methods);
124    }
125
126    public Consumer<String> assertClasses() {
127        return assertMembers("Classes", classes);
128    }
129
130    public String getCommandOutput() {
131        String s = cmdout.toString();
132        cmdout.reset();
133        return s;
134    }
135
136    public String getCommandErrorOutput() {
137        String s = cmderr.toString();
138        cmderr.reset();
139        return s;
140    }
141
142    public void setUserInput(String s) {
143        userin.setInput(s);
144    }
145
146    public String getUserOutput() {
147        String s = userout.toString();
148        userout.reset();
149        return s;
150    }
151
152    public String getUserErrorOutput() {
153        String s = usererr.toString();
154        usererr.reset();
155        return s;
156    }
157
158    public void test(ReplTest... tests) {
159        test(new String[0], tests);
160    }
161
162    public void test(String[] args, ReplTest... tests) {
163        test(true, args, tests);
164    }
165
166    public void test(boolean isDefaultStartUp, String[] args, ReplTest... tests) {
167        test(isDefaultStartUp, args, DEFAULT_STARTUP_MESSAGE, tests);
168    }
169
170    public void test(boolean isDefaultStartUp, String[] args, String startUpMessage, ReplTest... tests) {
171        this.isDefaultStartUp = isDefaultStartUp;
172        initSnippets();
173        ReplTest[] wtests = new ReplTest[tests.length + 3];
174        wtests[0] = a -> assertCommandCheckOutput(a, "<start>",
175                s -> assertTrue(s.startsWith(startUpMessage), "Expected start-up message '" + startUpMessage + "' Got: " + s));
176        wtests[1] = a -> assertCommand(a, "/debug 0", null);
177        System.arraycopy(tests, 0, wtests, 2, tests.length);
178        wtests[tests.length + 2] = a -> assertCommand(a, "/exit", null);
179        testRaw(args, wtests);
180    }
181
182    private void initSnippets() {
183        keys = new ArrayList<>();
184        variables = new HashMap<>();
185        methods = new HashMap<>();
186        classes = new HashMap<>();
187        if (isDefaultStartUp) {
188            methods.put("printf (String,Object...)void",
189                    new MethodInfo("", "(String,Object...)void", "printf"));
190        }
191    }
192
193    public void testRaw(String[] args, ReplTest... tests) {
194        cmdin = new WaitingTestingInputStream();
195        cmdout = new ByteArrayOutputStream();
196        cmderr = new ByteArrayOutputStream();
197        console = new PromptedCommandOutputStream(tests);
198        userin = new TestingInputStream();
199        userout = new ByteArrayOutputStream();
200        usererr = new ByteArrayOutputStream();
201        repl = new JShellTool(
202                cmdin,
203                new PrintStream(cmdout),
204                new PrintStream(cmderr),
205                new PrintStream(console),
206                userin,
207                new PrintStream(userout),
208                new PrintStream(usererr));
209        repl.testPrompt = true;
210        try {
211            repl.start(args);
212        } catch (Exception ex) {
213            fail("Repl tool died with exception", ex);
214        }
215        // perform internal consistency checks on state, if desired
216        String cos = getCommandOutput();
217        String ceos = getCommandErrorOutput();
218        String uos = getUserOutput();
219        String ueos = getUserErrorOutput();
220        assertTrue((cos.isEmpty() || cos.startsWith("|  Goodbye")),
221                "Expected a goodbye, but got: " + cos);
222        assertTrue(ceos.isEmpty(), "Expected empty error output, got: " + ceos);
223        assertTrue(uos.isEmpty(), "Expected empty output, got: " + uos);
224        assertTrue(ueos.isEmpty(), "Expected empty error output, got: " + ueos);
225    }
226
227    public void assertReset(boolean after, String cmd) {
228        assertCommand(after, cmd, "|  Resetting state.\n");
229        initSnippets();
230    }
231
232    public void evaluateExpression(boolean after, String type, String expr, String value) {
233        String output = String.format("\\| *Expression values is: %s\n|" +
234                " *.*temporary variable (\\$\\d+) of type %s", value, type);
235        Pattern outputPattern = Pattern.compile(output);
236        assertCommandCheckOutput(after, expr, s -> {
237            Matcher matcher = outputPattern.matcher(s);
238            assertTrue(matcher.find(), "Output: '" + s + "' does not fit pattern: '" + output + "'");
239            String name = matcher.group(1);
240            VariableInfo tempVar = new TempVariableInfo(expr, type, name, value);
241            variables.put(tempVar.toString(), tempVar);
242            addKey(after, tempVar);
243        });
244    }
245
246    public void loadVariable(boolean after, String type, String name) {
247        loadVariable(after, type, name, null, null);
248    }
249
250    public void loadVariable(boolean after, String type, String name, String expr, String value) {
251        String src = expr == null
252                ? String.format("%s %s", type, name)
253                : String.format("%s %s = %s", type, name, expr);
254        VariableInfo var = expr == null
255                ? new VariableInfo(src, type, name)
256                : new VariableInfo(src, type, name, value);
257        addKey(after, var, variables);
258        addKey(after, var);
259    }
260
261    public void assertVariable(boolean after, String type, String name) {
262        assertVariable(after, type, name, null, null);
263    }
264
265    public void assertVariable(boolean after, String type, String name, String expr, String value) {
266        String src = expr == null
267                ? String.format("%s %s", type, name)
268                : String.format("%s %s = %s", type, name, expr);
269        VariableInfo var = expr == null
270                ? new VariableInfo(src, type, name)
271                : new VariableInfo(src, type, name, value);
272        assertCommandCheckOutput(after, src, var.checkOutput());
273        addKey(after, var, variables);
274        addKey(after, var);
275    }
276
277    public void loadMethod(boolean after, String src, String signature, String name) {
278        MethodInfo method = new MethodInfo(src, signature, name);
279        addKey(after, method, methods);
280        addKey(after, method);
281    }
282
283    public void assertMethod(boolean after, String src, String signature, String name) {
284        MethodInfo method = new MethodInfo(src, signature, name);
285        assertCommandCheckOutput(after, src, method.checkOutput());
286        addKey(after, method, methods);
287        addKey(after, method);
288    }
289
290    public void loadClass(boolean after, String src, String type, String name) {
291        ClassInfo clazz = new ClassInfo(src, type, name);
292        addKey(after, clazz, classes);
293        addKey(after, clazz);
294    }
295
296    public void assertClass(boolean after, String src, String type, String name) {
297        ClassInfo clazz = new ClassInfo(src, type, name);
298        assertCommandCheckOutput(after, src, clazz.checkOutput());
299        addKey(after, clazz, classes);
300        addKey(after, clazz);
301    }
302
303    private <T extends MemberInfo> void addKey(boolean after, T memberInfo, Map<String, T> map) {
304        if (after) {
305            map.entrySet().removeIf(e -> e.getValue().equals(memberInfo));
306            map.put(memberInfo.toString(), memberInfo);
307        }
308    }
309
310    private <T extends MemberInfo> void addKey(boolean after, T memberInfo) {
311        if (after) {
312            for (int i = 0; i < keys.size(); ++i) {
313                MemberInfo m = keys.get(i);
314                if (m.equals(memberInfo)) {
315                    keys.set(i, memberInfo);
316                    return;
317                }
318            }
319            keys.add(memberInfo);
320        }
321    }
322
323    private void dropKey(boolean after, String cmd, String name, Map<String, ? extends MemberInfo> map) {
324        assertCommand(after, cmd, "");
325        if (after) {
326            map.remove(name);
327            for (int i = 0; i < keys.size(); ++i) {
328                MemberInfo m = keys.get(i);
329                if (m.toString().equals(name)) {
330                    keys.remove(i);
331                    return;
332                }
333            }
334            throw new AssertionError("Key not found: " + name + ", keys: " + keys);
335        }
336    }
337
338    public void dropVariable(boolean after, String cmd, String name) {
339        dropKey(after, cmd, name, variables);
340    }
341
342    public void dropMethod(boolean after, String cmd, String name) {
343        dropKey(after, cmd, name, methods);
344    }
345
346    public void dropClass(boolean after, String cmd, String name) {
347        dropKey(after, cmd, name, classes);
348    }
349
350    public void assertCommand(boolean after, String cmd, String out) {
351        assertCommand(after, cmd, out, "", null, "", "");
352    }
353
354    public void assertCommandCheckOutput(boolean after, String cmd, Consumer<String> check) {
355        if (!after) {
356            assertCommand(false, cmd, null);
357        } else {
358            String got = getCommandOutput();
359            check.accept(got);
360            assertCommand(true, cmd, null);
361        }
362    }
363
364    public void assertCommand(boolean after, String cmd, String out, String err,
365            String userinput, String print, String usererr) {
366        if (!after) {
367            if (userinput != null) {
368                setUserInput(userinput);
369            }
370            setCommandInput(cmd + "\n");
371        } else {
372            assertOutput(getCommandOutput(), out, "command");
373            assertOutput(getCommandErrorOutput(), err, "command error");
374            assertOutput(getUserOutput(), print, "user");
375            assertOutput(getUserErrorOutput(), usererr, "user error");
376        }
377    }
378
379    public void assertCompletion(boolean after, String code, boolean isSmart, String... expected) {
380        if (!after) {
381            setCommandInput("\n");
382        } else {
383            assertCompletion(code, isSmart, expected);
384        }
385    }
386
387    public void assertCompletion(String code, boolean isSmart, String... expected) {
388        List<String> completions = computeCompletions(code, isSmart);
389        assertEquals(completions, Arrays.asList(expected), "Command: " + code + ", output: " +
390                completions.toString());
391    }
392
393    private List<String> computeCompletions(String code, boolean isSmart) {
394        JShellTool repl = this.repl != null ? this.repl
395                                      : new JShellTool(null, null, null, null, null, null, null);
396        int cursor =  code.indexOf('|');
397        code = code.replace("|", "");
398        assertTrue(cursor > -1, "'|' not found: " + code);
399        List<Suggestion> completions =
400                repl.commandCompletionSuggestions(code, cursor, new int[1]); //XXX: ignoring anchor for now
401        return completions.stream()
402                          .filter(s -> isSmart == s.isSmart)
403                          .map(s -> s.continuation)
404                          .distinct()
405                          .collect(Collectors.toList());
406    }
407
408    public Consumer<String> assertStartsWith(String prefix) {
409        return (output) -> assertTrue(output.startsWith(prefix), "Output: \'" + output + "' does not start with: " + prefix);
410    }
411
412    public void assertOutput(String got, String expected, String kind) {
413        if (expected != null) {
414            assertEquals(got, expected, "Kind: " + kind + ".\n");
415        }
416    }
417
418    public static abstract class MemberInfo {
419        public final String source;
420        public final String type;
421        public final String name;
422
423        public MemberInfo(String source, String type, String name) {
424            this.source = source;
425            this.type = type;
426            this.name = name;
427        }
428
429        @Override
430        public int hashCode() {
431            return name.hashCode();
432        }
433
434        public abstract Consumer<String> checkOutput();
435
436        public String getSource() {
437            return source;
438        }
439    }
440
441    public static class VariableInfo extends MemberInfo {
442
443        public final String value;
444        public final String initialValue;
445
446        public VariableInfo(String src, String type, String name) {
447            super(src, type, name);
448            this.initialValue = null;
449            switch (type) {
450                case "byte":
451                case "short":
452                case "int":
453                case "long":
454                    value = "0";
455                    break;
456                case "boolean":
457                    value = "false";
458                    break;
459                case "char":
460                    value = "''";
461                    break;
462                case "float":
463                case "double":
464                    value = "0.0";
465                    break;
466                default:
467                    value = "null";
468            }
469        }
470
471        public VariableInfo(String src, String type, String name, String value) {
472            super(src, type, name);
473            this.value = value;
474            this.initialValue = value;
475        }
476
477        @Override
478        public Consumer<String> checkOutput() {
479            String pattern = String.format("\\| *\\w+ variable %s of type %s", name, type);
480            if (initialValue != null) {
481                pattern += " with initial value " + initialValue;
482            }
483            Predicate<String> checkOutput = Pattern.compile(pattern).asPredicate();
484            final String finalPattern = pattern;
485            return output -> assertTrue(checkOutput.test(output),
486                    "Output: " + output + " does not fit pattern: " + finalPattern);
487        }
488
489        @Override
490        public boolean equals(Object o) {
491            if (o instanceof VariableInfo) {
492                VariableInfo v = (VariableInfo) o;
493                return name.equals(v.name);
494            }
495            return false;
496        }
497
498        @Override
499        public String toString() {
500            return String.format("%s %s = %s", type, name, value);
501        }
502
503        @Override
504        public String getSource() {
505            String src = super.getSource();
506            return src.endsWith(";") ? src : src + ";";
507        }
508    }
509
510    public static class TempVariableInfo extends VariableInfo {
511
512        public TempVariableInfo(String src, String type, String name, String value) {
513            super(src, type, name, value);
514        }
515
516        @Override
517        public String getSource() {
518            return source;
519        }
520    }
521
522    public static class MethodInfo extends MemberInfo {
523
524        public final String signature;
525
526        public MethodInfo(String source, String signature, String name) {
527            super(source, signature.substring(0, signature.lastIndexOf(')') + 1), name);
528            this.signature = signature;
529        }
530
531        @Override
532        public Consumer<String> checkOutput() {
533            String expectedOutput = String.format("\\| *\\w+ method %s", name);
534            Predicate<String> checkOutput = Pattern.compile(expectedOutput).asPredicate();
535            return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s);
536        }
537
538
539        @Override
540        public boolean equals(Object o) {
541            if (o instanceof MemberInfo) {
542                MemberInfo m = (MemberInfo) o;
543                return name.equals(m.name) && type.equals(m.type);
544            }
545            return false;
546        }
547
548        @Override
549        public String toString() {
550            return String.format("%s %s", name, signature);
551        }
552    }
553
554    public static class ClassInfo extends MemberInfo {
555
556        public ClassInfo(String source, String type, String name) {
557            super(source, type, name);
558        }
559
560        @Override
561        public Consumer<String> checkOutput() {
562            String fullType = type.equals("@interface")? "annotation interface" : type;
563            String expectedOutput = String.format("\\| *\\w+ %s %s", fullType, name);
564            Predicate<String> checkOutput = Pattern.compile(expectedOutput).asPredicate();
565            return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s);
566        }
567
568        @Override
569        public boolean equals(Object o) {
570            if (o instanceof ClassInfo) {
571                ClassInfo c = (ClassInfo) o;
572                return name.equals(c.name);
573            }
574            return false;
575        }
576
577        @Override
578        public String toString() {
579            return String.format("%s %s", type, name);
580        }
581    }
582
583    class WaitingTestingInputStream extends TestingInputStream {
584
585        @Override
586        synchronized void setInput(String s) {
587            super.setInput(s);
588            notify();
589        }
590
591        synchronized void waitForInput() {
592            boolean interrupted = false;
593            try {
594                while (available() == 0) {
595                    try {
596                        wait();
597                    } catch (InterruptedException e) {
598                        interrupted = true;
599                        // fall through and retry
600                    }
601                }
602            } finally {
603                if (interrupted) {
604                    Thread.currentThread().interrupt();
605                }
606            }
607        }
608
609        @Override
610        public int read() {
611            waitForInput();
612            return super.read();
613        }
614
615        @Override
616        public int read(byte b[], int off, int len) {
617            waitForInput();
618            return super.read(b, off, len);
619        }
620    }
621
622    class PromptedCommandOutputStream extends OutputStream {
623        private final ReplTest[] tests;
624        private int index = 0;
625        PromptedCommandOutputStream(ReplTest[] tests) {
626            this.tests = tests;
627        }
628
629        @Override
630        public synchronized void write(int b) {
631            if (b == 5 || b == 6) {
632                if (index < (tests.length - 1)) {
633                    tests[index].run(true);
634                    tests[index + 1].run(false);
635                } else {
636                    fail("Did not exit Repl tool after test");
637                }
638                ++index;
639            } // For now, anything else is thrown away
640        }
641
642        @Override
643        public synchronized void write(byte b[], int off, int len) {
644            if ((off < 0) || (off > b.length) || (len < 0)
645                    || ((off + len) - b.length > 0)) {
646                throw new IndexOutOfBoundsException();
647            }
648            for (int i = 0; i < len; ++i) {
649                write(b[off + i]);
650            }
651        }
652    }
653}
654