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