ReplToolTesting.java revision 3335:697549008e7f
1/*
2 * Copyright (c) 2015, 2016, 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.Collections;
30import java.util.HashMap;
31import java.util.List;
32import java.util.Locale;
33import java.util.Map;
34import java.util.function.Consumer;
35import java.util.function.Function;
36import java.util.function.Predicate;
37import java.util.prefs.AbstractPreferences;
38import java.util.prefs.BackingStoreException;
39import java.util.prefs.Preferences;
40import java.util.regex.Matcher;
41import java.util.regex.Pattern;
42import java.util.stream.Collectors;
43import java.util.stream.Stream;
44
45import jdk.internal.jshell.tool.JShellTool;
46import jdk.jshell.SourceCodeAnalysis.Suggestion;
47
48import org.testng.annotations.BeforeMethod;
49
50import static java.util.stream.Collectors.toList;
51import static org.testng.Assert.assertEquals;
52import static org.testng.Assert.assertNotNull;
53import static org.testng.Assert.assertTrue;
54import static org.testng.Assert.fail;
55
56public class ReplToolTesting {
57
58    private final static String DEFAULT_STARTUP_MESSAGE = "|  Welcome to";
59    final static List<ImportInfo> START_UP_IMPORTS = Stream.of(
60                    "java.util.*",
61                    "java.io.*",
62                    "java.math.*",
63                    "java.net.*",
64                    "java.util.concurrent.*",
65                    "java.util.prefs.*",
66                    "java.util.regex.*")
67                    .map(s -> new ImportInfo("import " + s + ";", "", s))
68                    .collect(toList());
69    final static List<MethodInfo> START_UP_METHODS = Stream.of(
70                    new MethodInfo("void printf(String format, Object... args) { System.out.printf(format, args); }",
71                            "(String,Object...)void", "printf"))
72                    .collect(toList());
73    final static List<String> START_UP = Collections.unmodifiableList(
74            Stream.concat(START_UP_IMPORTS.stream(), START_UP_METHODS.stream())
75            .map(s -> s.getSource())
76            .collect(toList()));
77
78    private WaitingTestingInputStream cmdin = null;
79    private ByteArrayOutputStream cmdout = null;
80    private ByteArrayOutputStream cmderr = null;
81    private PromptedCommandOutputStream console = null;
82    private TestingInputStream userin = null;
83    private ByteArrayOutputStream userout = null;
84    private ByteArrayOutputStream usererr = null;
85
86    private List<MemberInfo> keys;
87    private Map<String, VariableInfo> variables;
88    private Map<String, MethodInfo> methods;
89    private Map<String, ClassInfo> classes;
90    private Map<String, ImportInfo> imports;
91    private boolean isDefaultStartUp = true;
92    private Preferences prefs;
93
94    public JShellTool repl = null;
95
96    public interface ReplTest {
97        void run(boolean after);
98    }
99
100    public void setCommandInput(String s) {
101        cmdin.setInput(s);
102    }
103
104    public final static Pattern idPattern = Pattern.compile("^\\s+(\\d+)");
105    public Consumer<String> assertList() {
106        return s -> {
107            List<String> lines = Stream.of(s.split("\n"))
108                    .filter(l -> !l.isEmpty())
109                    .collect(Collectors.toList());
110            int previousId = Integer.MIN_VALUE;
111            assertEquals(lines.size(), keys.size(), "Number of keys");
112            for (int i = 0; i < lines.size(); ++i) {
113                String line = lines.get(i);
114                Matcher matcher = idPattern.matcher(line);
115                assertTrue(matcher.find(), "Snippet id not found: " + line);
116                String src = keys.get(i).getSource();
117                assertTrue(line.endsWith(src), "Line '" + line + "' does not end with: " + src);
118                int id = Integer.parseInt(matcher.group(1));
119                assertTrue(previousId < id,
120                        String.format("The previous id is not less than the next one: previous: %d, next: %d",
121                                previousId, id));
122                previousId = id;
123            }
124        };
125    }
126
127    private final static Pattern extractPattern = Pattern.compile("^\\| *(.*)$");
128    private Consumer<String> assertMembers(String message, Map<String, ? extends MemberInfo> set) {
129        return s -> {
130            List<String> lines = Stream.of(s.split("\n"))
131                    .filter(l -> !l.isEmpty())
132                    .collect(Collectors.toList());
133            assertEquals(lines.size(), set.size(), message + " : expected: " + set.keySet() + "\ngot:\n" + lines);
134            for (String line : lines) {
135                Matcher matcher = extractPattern.matcher(line);
136                assertTrue(matcher.find(), line);
137                String src = matcher.group(1);
138                MemberInfo info = set.get(src);
139                assertNotNull(info, "Not found snippet with signature: " + src + ", line: "
140                        + line + ", keys: " + set.keySet() + "\n");
141            }
142        };
143    }
144
145    public Consumer<String> assertVariables() {
146        return assertMembers("Variables", variables);
147    }
148
149    public Consumer<String> assertMethods() {
150        return assertMembers("Methods", methods);
151    }
152
153    public Consumer<String> assertClasses() {
154        return assertMembers("Classes", classes);
155    }
156
157    public Consumer<String> assertImports() {
158        return assertMembers("Imports", imports);
159    }
160
161    public String getCommandOutput() {
162        String s = normalizeLineEndings(cmdout.toString());
163        cmdout.reset();
164        return s;
165    }
166
167    public String getCommandErrorOutput() {
168        String s = normalizeLineEndings(cmderr.toString());
169        cmderr.reset();
170        return s;
171    }
172
173    public void setUserInput(String s) {
174        userin.setInput(s);
175    }
176
177    public String getUserOutput() {
178        String s = normalizeLineEndings(userout.toString());
179        userout.reset();
180        return s;
181    }
182
183    public String getUserErrorOutput() {
184        String s = normalizeLineEndings(usererr.toString());
185        usererr.reset();
186        return s;
187    }
188
189    public void test(ReplTest... tests) {
190        test(new String[0], tests);
191    }
192
193    public void test(String[] args, ReplTest... tests) {
194        test(true, args, tests);
195    }
196
197    public void test(boolean isDefaultStartUp, String[] args, ReplTest... tests) {
198        test(Locale.ROOT, isDefaultStartUp, args, DEFAULT_STARTUP_MESSAGE, tests);
199    }
200
201    public void test(Locale locale, boolean isDefaultStartUp, String[] args, String startUpMessage, ReplTest... tests) {
202        this.isDefaultStartUp = isDefaultStartUp;
203        initSnippets();
204        ReplTest[] wtests = new ReplTest[tests.length + 3];
205        wtests[0] = a -> assertCommandCheckOutput(a, "<start>",
206                s -> assertTrue(s.startsWith(startUpMessage), "Expected start-up message '" + startUpMessage + "' Got: " + s));
207        wtests[1] = a -> assertCommand(a, "/debug 0", null);
208        System.arraycopy(tests, 0, wtests, 2, tests.length);
209        wtests[tests.length + 2] = a -> assertCommand(a, "/exit", null);
210        testRaw(locale, args, wtests);
211    }
212
213    private void initSnippets() {
214        keys = new ArrayList<>();
215        variables = new HashMap<>();
216        methods = new HashMap<>();
217        classes = new HashMap<>();
218        imports = new HashMap<>();
219        if (isDefaultStartUp) {
220            methods.putAll(
221                START_UP_METHODS.stream()
222                    .collect(Collectors.toMap(Object::toString, Function.identity())));
223            imports.putAll(
224                START_UP_IMPORTS.stream()
225                    .collect(Collectors.toMap(Object::toString, Function.identity())));
226        }
227    }
228
229    @BeforeMethod
230    public void setUp() {
231        prefs = new MemoryPreferences();
232    }
233
234    public void testRaw(Locale locale, String[] args, ReplTest... tests) {
235        cmdin = new WaitingTestingInputStream();
236        cmdout = new ByteArrayOutputStream();
237        cmderr = new ByteArrayOutputStream();
238        console = new PromptedCommandOutputStream(tests);
239        userin = new TestingInputStream();
240        userout = new ByteArrayOutputStream();
241        usererr = new ByteArrayOutputStream();
242        repl = new JShellTool(
243                cmdin,
244                new PrintStream(cmdout),
245                new PrintStream(cmderr),
246                new PrintStream(console),
247                userin,
248                new PrintStream(userout),
249                new PrintStream(usererr),
250                prefs,
251                locale);
252        repl.testPrompt = true;
253        try {
254            repl.start(args);
255        } catch (Exception ex) {
256            fail("Repl tool died with exception", ex);
257        }
258        // perform internal consistency checks on state, if desired
259        String cos = getCommandOutput();
260        String ceos = getCommandErrorOutput();
261        String uos = getUserOutput();
262        String ueos = getUserErrorOutput();
263        assertTrue((cos.isEmpty() || cos.startsWith("|  Goodbye") || !locale.equals(Locale.ROOT)),
264                "Expected a goodbye, but got: " + cos);
265        assertTrue(ceos.isEmpty(), "Expected empty error output, got: " + ceos);
266        assertTrue(uos.isEmpty(), "Expected empty output, got: " + uos);
267        assertTrue(ueos.isEmpty(), "Expected empty error output, got: " + ueos);
268    }
269
270    public void assertReset(boolean after, String cmd) {
271        assertCommand(after, cmd, "|  Resetting state.\n");
272        initSnippets();
273    }
274
275    public void evaluateExpression(boolean after, String type, String expr, String value) {
276        String output = String.format("\\| *Expression values is: %s\n|" +
277                " *.*temporary variable (\\$\\d+) of type %s", value, type);
278        Pattern outputPattern = Pattern.compile(output);
279        assertCommandCheckOutput(after, expr, s -> {
280            Matcher matcher = outputPattern.matcher(s);
281            assertTrue(matcher.find(), "Output: '" + s + "' does not fit pattern: '" + output + "'");
282            String name = matcher.group(1);
283            VariableInfo tempVar = new TempVariableInfo(expr, type, name, value);
284            variables.put(tempVar.toString(), tempVar);
285            addKey(after, tempVar);
286        });
287    }
288
289    public void loadVariable(boolean after, String type, String name) {
290        loadVariable(after, type, name, null, null);
291    }
292
293    public void loadVariable(boolean after, String type, String name, String expr, String value) {
294        String src = expr == null
295                ? String.format("%s %s", type, name)
296                : String.format("%s %s = %s", type, name, expr);
297        VariableInfo var = expr == null
298                ? new VariableInfo(src, type, name)
299                : new VariableInfo(src, type, name, value);
300        addKey(after, var, variables);
301        addKey(after, var);
302    }
303
304    public void assertVariable(boolean after, String type, String name) {
305        assertVariable(after, type, name, null, null);
306    }
307
308    public void assertVariable(boolean after, String type, String name, String expr, String value) {
309        String src = expr == null
310                ? String.format("%s %s", type, name)
311                : String.format("%s %s = %s", type, name, expr);
312        VariableInfo var = expr == null
313                ? new VariableInfo(src, type, name)
314                : new VariableInfo(src, type, name, value);
315        assertCommandCheckOutput(after, src, var.checkOutput());
316        addKey(after, var, variables);
317        addKey(after, var);
318    }
319
320    public void loadMethod(boolean after, String src, String signature, String name) {
321        MethodInfo method = new MethodInfo(src, signature, name);
322        addKey(after, method, methods);
323        addKey(after, method);
324    }
325
326    public void assertMethod(boolean after, String src, String signature, String name) {
327        MethodInfo method = new MethodInfo(src, signature, name);
328        assertCommandCheckOutput(after, src, method.checkOutput());
329        addKey(after, method, methods);
330        addKey(after, method);
331    }
332
333    public void loadClass(boolean after, String src, String type, String name) {
334        ClassInfo clazz = new ClassInfo(src, type, name);
335        addKey(after, clazz, classes);
336        addKey(after, clazz);
337    }
338
339    public void assertClass(boolean after, String src, String type, String name) {
340        ClassInfo clazz = new ClassInfo(src, type, name);
341        assertCommandCheckOutput(after, src, clazz.checkOutput());
342        addKey(after, clazz, classes);
343        addKey(after, clazz);
344    }
345
346    public void loadImport(boolean after, String src, String type, String name) {
347        ImportInfo i = new ImportInfo(src, type, name);
348        addKey(after, i, imports);
349        addKey(after, i);
350    }
351
352    public void assertImport(boolean after, String src, String type, String name) {
353        ImportInfo i = new ImportInfo(src, type, name);
354        assertCommandCheckOutput(after, src, i.checkOutput());
355        addKey(after, i, imports);
356        addKey(after, i);
357    }
358
359    private <T extends MemberInfo> void addKey(boolean after, T memberInfo, Map<String, T> map) {
360        if (after) {
361            map.entrySet().removeIf(e -> e.getValue().equals(memberInfo));
362            map.put(memberInfo.toString(), memberInfo);
363        }
364    }
365
366    private <T extends MemberInfo> void addKey(boolean after, T memberInfo) {
367        if (after) {
368            for (int i = 0; i < keys.size(); ++i) {
369                MemberInfo m = keys.get(i);
370                if (m.equals(memberInfo)) {
371                    keys.set(i, memberInfo);
372                    return;
373                }
374            }
375            keys.add(memberInfo);
376        }
377    }
378
379    private void dropKey(boolean after, String cmd, String name, Map<String, ? extends MemberInfo> map, String output) {
380        assertCommand(after, cmd, output);
381        if (after) {
382            map.remove(name);
383            for (int i = 0; i < keys.size(); ++i) {
384                MemberInfo m = keys.get(i);
385                if (m.toString().equals(name)) {
386                    keys.remove(i);
387                    return;
388                }
389            }
390            throw new AssertionError("Key not found: " + name + ", keys: " + keys);
391        }
392    }
393
394    public void dropVariable(boolean after, String cmd, String name, String output) {
395        dropKey(after, cmd, name, variables, output);
396    }
397
398    public void dropMethod(boolean after, String cmd, String name, String output) {
399        dropKey(after, cmd, name, methods, output);
400    }
401
402    public void dropClass(boolean after, String cmd, String name, String output) {
403        dropKey(after, cmd, name, classes, output);
404    }
405
406    public void dropImport(boolean after, String cmd, String name, String output) {
407        dropKey(after, cmd, name, imports, output);
408    }
409
410    public void assertCommand(boolean after, String cmd, String out) {
411        assertCommand(after, cmd, out, "", null, "", "");
412    }
413
414    public void assertCommandOutputContains(boolean after, String cmd, String has) {
415        assertCommandCheckOutput(after, cmd, (s) ->
416                        assertTrue(s.contains(has), "Output: \'" + s + "' does not contain: " + has));
417    }
418
419    public void assertCommandOutputStartsWith(boolean after, String cmd, String starts) {
420        assertCommandCheckOutput(after, cmd, assertStartsWith(starts));
421    }
422
423    public void assertCommandCheckOutput(boolean after, String cmd, Consumer<String> check) {
424        if (!after) {
425            assertCommand(false, cmd, null);
426        } else {
427            String got = getCommandOutput();
428            check.accept(got);
429            assertCommand(true, cmd, null);
430        }
431    }
432
433    public void assertCommand(boolean after, String cmd, String out, String err,
434            String userinput, String print, String usererr) {
435        if (!after) {
436            if (userinput != null) {
437                setUserInput(userinput);
438            }
439            setCommandInput(cmd + "\n");
440        } else {
441            assertOutput(getCommandOutput().trim(), out==null? out : out.trim(), "command output: " + cmd);
442            assertOutput(getCommandErrorOutput(), err, "command error: " + cmd);
443            assertOutput(getUserOutput(), print, "user output: " + cmd);
444            assertOutput(getUserErrorOutput(), usererr, "user error: " + cmd);
445        }
446    }
447
448    public void assertCompletion(boolean after, String code, boolean isSmart, String... expected) {
449        if (!after) {
450            setCommandInput("\n");
451        } else {
452            assertCompletion(code, isSmart, expected);
453        }
454    }
455
456    public void assertCompletion(String code, boolean isSmart, String... expected) {
457        List<String> completions = computeCompletions(code, isSmart);
458        assertEquals(completions, Arrays.asList(expected), "Command: " + code + ", output: " +
459                completions.toString());
460    }
461
462    private List<String> computeCompletions(String code, boolean isSmart) {
463        JShellTool js = this.repl != null ? this.repl
464                                      : new JShellTool(null, null, null, null, null, null, null, prefs, Locale.ROOT);
465        int cursor =  code.indexOf('|');
466        code = code.replace("|", "");
467        assertTrue(cursor > -1, "'|' not found: " + code);
468        List<Suggestion> completions =
469                js.commandCompletionSuggestions(code, cursor, new int[1]); //XXX: ignoring anchor for now
470        return completions.stream()
471                          .filter(s -> isSmart == s.isSmart)
472                          .map(s -> s.continuation)
473                          .distinct()
474                          .collect(Collectors.toList());
475    }
476
477    public Consumer<String> assertStartsWith(String prefix) {
478        return (output) -> assertTrue(output.startsWith(prefix), "Output: \'" + output + "' does not start with: " + prefix);
479    }
480
481    public void assertOutput(String got, String expected, String display) {
482        if (expected != null) {
483            assertEquals(got, expected, display + ".\n");
484        }
485    }
486
487    private String normalizeLineEndings(String text) {
488        return text.replace(System.getProperty("line.separator"), "\n");
489    }
490
491    public static abstract class MemberInfo {
492        public final String source;
493        public final String type;
494        public final String name;
495
496        public MemberInfo(String source, String type, String name) {
497            this.source = source;
498            this.type = type;
499            this.name = name;
500        }
501
502        @Override
503        public int hashCode() {
504            return name.hashCode();
505        }
506
507        @Override
508        public boolean equals(Object o) {
509            if (o instanceof MemberInfo) {
510                MemberInfo mi = (MemberInfo) o;
511                return name.equals(mi.name);
512            }
513            return false;
514        }
515
516        public abstract Consumer<String> checkOutput();
517
518        public String getSource() {
519            return source;
520        }
521    }
522
523    public static class VariableInfo extends MemberInfo {
524
525        public final String value;
526        public final String initialValue;
527
528        public VariableInfo(String src, String type, String name) {
529            super(src, type, name);
530            this.initialValue = null;
531            switch (type) {
532                case "byte":
533                case "short":
534                case "int":
535                case "long":
536                    value = "0";
537                    break;
538                case "boolean":
539                    value = "false";
540                    break;
541                case "char":
542                    value = "''";
543                    break;
544                case "float":
545                case "double":
546                    value = "0.0";
547                    break;
548                default:
549                    value = "null";
550            }
551        }
552
553        public VariableInfo(String src, String type, String name, String value) {
554            super(src, type, name);
555            this.value = value;
556            this.initialValue = value;
557        }
558
559        @Override
560        public Consumer<String> checkOutput() {
561            String pattern = String.format("\\| *\\w+ variable %s of type %s", name, type);
562            if (initialValue != null) {
563                pattern += " with initial value " + initialValue;
564            }
565            Predicate<String> checkOutput = Pattern.compile(pattern).asPredicate();
566            final String finalPattern = pattern;
567            return output -> assertTrue(checkOutput.test(output),
568                    "Output: " + output + " does not fit pattern: " + finalPattern);
569        }
570
571        @Override
572        public int hashCode() {
573            return name.hashCode();
574        }
575
576        @Override
577        public boolean equals(Object o) {
578            if (o instanceof VariableInfo) {
579                VariableInfo v = (VariableInfo) o;
580                return name.equals(v.name);
581            }
582            return false;
583        }
584
585        @Override
586        public String toString() {
587            return String.format("%s %s = %s", type, name, value);
588        }
589
590        @Override
591        public String getSource() {
592            String src = super.getSource();
593            return src.endsWith(";") ? src : src + ";";
594        }
595    }
596
597    public static class TempVariableInfo extends VariableInfo {
598
599        public TempVariableInfo(String src, String type, String name, String value) {
600            super(src, type, name, value);
601        }
602
603        @Override
604        public String getSource() {
605            return source;
606        }
607    }
608
609    public static class MethodInfo extends MemberInfo {
610
611        public final String signature;
612
613        public MethodInfo(String source, String signature, String name) {
614            super(source, signature.substring(0, signature.lastIndexOf(')') + 1), name);
615            this.signature = signature;
616        }
617
618        @Override
619        public Consumer<String> checkOutput() {
620            String expectedOutput = String.format("\\| *\\w+ method %s", name);
621            Predicate<String> checkOutput = Pattern.compile(expectedOutput).asPredicate();
622            return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s);
623        }
624
625        @Override
626        public int hashCode() {
627            return (name.hashCode() << 2) ^ type.hashCode() ;
628        }
629
630        @Override
631        public boolean equals(Object o) {
632            if (o instanceof MemberInfo) {
633                MemberInfo m = (MemberInfo) o;
634                return name.equals(m.name) && type.equals(m.type);
635            }
636            return false;
637        }
638
639        @Override
640        public String toString() {
641            return String.format("%s %s", name, signature);
642        }
643    }
644
645    public static class ClassInfo extends MemberInfo {
646
647        public ClassInfo(String source, String type, String name) {
648            super(source, type, name);
649        }
650
651        @Override
652        public Consumer<String> checkOutput() {
653            String fullType = type.equals("@interface")? "annotation interface" : type;
654            String expectedOutput = String.format("\\| *\\w+ %s %s", fullType, name);
655            Predicate<String> checkOutput = Pattern.compile(expectedOutput).asPredicate();
656            return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s);
657        }
658
659        @Override
660        public int hashCode() {
661            return name.hashCode() ;
662        }
663
664        @Override
665        public boolean equals(Object o) {
666            if (o instanceof ClassInfo) {
667                ClassInfo c = (ClassInfo) o;
668                return name.equals(c.name);
669            }
670            return false;
671        }
672
673        @Override
674        public String toString() {
675            return String.format("%s %s", type, name);
676        }
677    }
678
679    public static class ImportInfo extends MemberInfo {
680        public ImportInfo(String source, String type, String fullname) {
681            super(source, type, fullname);
682        }
683
684        @Override
685        public Consumer<String> checkOutput() {
686            return s -> assertTrue("".equals(s), "Expected: '', actual: " + s);
687        }
688
689        @Override
690        public int hashCode() {
691            return (name.hashCode() << 2) ^ type.hashCode() ;
692        }
693
694        @Override
695        public boolean equals(Object o) {
696            if (o instanceof ImportInfo) {
697                ImportInfo i = (ImportInfo) o;
698                return name.equals(i.name) && type.equals(i.type);
699            }
700            return false;
701        }
702
703        @Override
704        public String toString() {
705            return String.format("import %s%s", type.equals("static") ? "static " : "", name);
706        }
707    }
708
709    class WaitingTestingInputStream extends TestingInputStream {
710
711        @Override
712        synchronized void setInput(String s) {
713            super.setInput(s);
714            notify();
715        }
716
717        synchronized void waitForInput() {
718            boolean interrupted = false;
719            try {
720                while (available() == 0) {
721                    try {
722                        wait();
723                    } catch (InterruptedException e) {
724                        interrupted = true;
725                        // fall through and retry
726                    }
727                }
728            } finally {
729                if (interrupted) {
730                    Thread.currentThread().interrupt();
731                }
732            }
733        }
734
735        @Override
736        public int read() {
737            waitForInput();
738            return super.read();
739        }
740
741        @Override
742        public int read(byte b[], int off, int len) {
743            waitForInput();
744            return super.read(b, off, len);
745        }
746    }
747
748    class PromptedCommandOutputStream extends OutputStream {
749        private final ReplTest[] tests;
750        private int index = 0;
751        PromptedCommandOutputStream(ReplTest[] tests) {
752            this.tests = tests;
753        }
754
755        @Override
756        public synchronized void write(int b) {
757            if (b == 5 || b == 6) {
758                if (index < (tests.length - 1)) {
759                    tests[index].run(true);
760                    tests[index + 1].run(false);
761                } else {
762                    fail("Did not exit Repl tool after test");
763                }
764                ++index;
765            } // For now, anything else is thrown away
766        }
767
768        @Override
769        public synchronized void write(byte b[], int off, int len) {
770            if ((off < 0) || (off > b.length) || (len < 0)
771                    || ((off + len) - b.length > 0)) {
772                throw new IndexOutOfBoundsException();
773            }
774            for (int i = 0; i < len; ++i) {
775                write(b[off + i]);
776            }
777        }
778    }
779
780    public static final class MemoryPreferences extends AbstractPreferences {
781
782        private final Map<String, String> values = new HashMap<>();
783        private final Map<String, MemoryPreferences> nodes = new HashMap<>();
784
785        public MemoryPreferences() {
786            this(null, "");
787        }
788
789        public MemoryPreferences(MemoryPreferences parent, String name) {
790            super(parent, name);
791        }
792
793        @Override
794        protected void putSpi(String key, String value) {
795            values.put(key, value);
796        }
797
798        @Override
799        protected String getSpi(String key) {
800            return values.get(key);
801        }
802
803        @Override
804        protected void removeSpi(String key) {
805            values.remove(key);
806        }
807
808        @Override
809        protected void removeNodeSpi() throws BackingStoreException {
810            ((MemoryPreferences) parent()).nodes.remove(name());
811        }
812
813        @Override
814        protected String[] keysSpi() throws BackingStoreException {
815            return values.keySet().toArray(new String[0]);
816        }
817
818        @Override
819        protected String[] childrenNamesSpi() throws BackingStoreException {
820            return nodes.keySet().toArray(new String[0]);
821        }
822
823        @Override
824        protected AbstractPreferences childSpi(String name) {
825            return nodes.computeIfAbsent(name, n -> new MemoryPreferences(this, name));
826        }
827
828        @Override
829        protected void syncSpi() throws BackingStoreException {
830        }
831
832        @Override
833        protected void flushSpi() throws BackingStoreException {
834        }
835
836    }
837}
838