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                 userin,
 251                 new PrintStream(userout),
 252                 new PrintStream(usererr),
 253                 prefs,
 254                 locale);
 255         repl.testPrompt = true;
 256         try {
 257             repl.start(args);
 258         } catch (Exception ex) {
 259             fail("Repl tool died with exception", ex);
 260         }
 261         // perform internal consistency checks on state, if desired
 262         String cos = getCommandOutput();
 263         String ceos = getCommandErrorOutput();
 264         String uos = getUserOutput();
 265         String ueos = getUserErrorOutput();
 266         assertTrue((cos.isEmpty() || cos.startsWith("|  Goodbye") || !locale.equals(Locale.ROOT)),
 267                 "Expected a goodbye, but got: " + cos);
 268         assertTrue(ceos.isEmpty(), "Expected empty error output, got: " + ceos);
 269         assertTrue(uos.isEmpty(), "Expected empty output, got: " + uos);
 270         assertTrue(ueos.isEmpty(), "Expected empty error output, got: " + ueos);
 271     }
 272 
 273     public void assertReset(boolean after, String cmd) {
 274         assertCommand(after, cmd, "|  Resetting state.\n");
 275         initSnippets();
 276     }
 277 
 278     public void evaluateExpression(boolean after, String type, String expr, String value) {
 279         String output = String.format("(\\$\\d+) ==> %s", value);
 280         Pattern outputPattern = Pattern.compile(output);
 281         assertCommandCheckOutput(after, expr, s -> {
 282             Matcher matcher = outputPattern.matcher(s);
 283             assertTrue(matcher.find(), "Output: '" + s + "' does not fit pattern: '" + output + "'");
 284             String name = matcher.group(1);
 285             VariableInfo tempVar = new TempVariableInfo(expr, type, name, value);
 286             variables.put(tempVar.toString(), tempVar);
 287             addKey(after, tempVar);
 288         });
 289     }
 290 
 291     public void loadVariable(boolean after, String type, String name) {
 292         loadVariable(after, type, name, null, null);
 293     }
 294 
 295     public void loadVariable(boolean after, String type, String name, String expr, String value) {
 296         String src = expr == null
 297                 ? String.format("%s %s", type, name)
 298                 : String.format("%s %s = %s", type, name, expr);
 299         VariableInfo var = expr == null
 300                 ? new VariableInfo(src, type, name)
 301                 : new VariableInfo(src, type, name, value);
 302         addKey(after, var, variables);
 303         addKey(after, var);
 304     }
 305 
 306     public void assertVariable(boolean after, String type, String name) {
 307         assertVariable(after, type, name, null, null);
 308     }
 309 
 310     public void assertVariable(boolean after, String type, String name, String expr, String value) {
 311         String src = expr == null
 312                 ? String.format("%s %s", type, name)
 313                 : String.format("%s %s = %s", type, name, expr);
 314         VariableInfo var = expr == null
 315                 ? new VariableInfo(src, type, name)
 316                 : new VariableInfo(src, type, name, value);
 317         assertCommandCheckOutput(after, src, var.checkOutput());
 318         addKey(after, var, variables);
 319         addKey(after, var);
 320     }
 321 
 322     public void loadMethod(boolean after, String src, String signature, String name) {
 323         MethodInfo method = new MethodInfo(src, signature, name);
 324         addKey(after, method, methods);
 325         addKey(after, method);
 326     }
 327 
 328     public void assertMethod(boolean after, String src, String signature, String name) {
 329         MethodInfo method = new MethodInfo(src, signature, name);
 330         assertCommandCheckOutput(after, src, method.checkOutput());
 331         addKey(after, method, methods);
 332         addKey(after, method);
 333     }
 334 
 335     public void loadClass(boolean after, String src, String type, String name) {
 336         ClassInfo clazz = new ClassInfo(src, type, name);
 337         addKey(after, clazz, classes);
 338         addKey(after, clazz);
 339     }
 340 
 341     public void assertClass(boolean after, String src, String type, String name) {
 342         ClassInfo clazz = new ClassInfo(src, type, name);
 343         assertCommandCheckOutput(after, src, clazz.checkOutput());
 344         addKey(after, clazz, classes);
 345         addKey(after, clazz);
 346     }
 347 
 348     public void loadImport(boolean after, String src, String type, String name) {
 349         ImportInfo i = new ImportInfo(src, type, name);
 350         addKey(after, i, imports);
 351         addKey(after, i);
 352     }
 353 
 354     public void assertImport(boolean after, String src, String type, String name) {
 355         ImportInfo i = new ImportInfo(src, type, name);
 356         assertCommandCheckOutput(after, src, i.checkOutput());
 357         addKey(after, i, imports);
 358         addKey(after, i);
 359     }
 360 
 361     private <T extends MemberInfo> void addKey(boolean after, T memberInfo, Map<String, T> map) {
 362         if (after) {
 363             map.entrySet().removeIf(e -> e.getValue().equals(memberInfo));
 364             map.put(memberInfo.toString(), memberInfo);
 365         }
 366     }
 367 
 368     private <T extends MemberInfo> void addKey(boolean after, T memberInfo) {
 369         if (after) {
 370             for (int i = 0; i < keys.size(); ++i) {
 371                 MemberInfo m = keys.get(i);
 372                 if (m.equals(memberInfo)) {
 373                     keys.set(i, memberInfo);
 374                     return;
 375                 }
 376             }
 377             keys.add(memberInfo);
 378         }
 379     }
 380 
 381     private void dropKey(boolean after, String cmd, String name, Map<String, ? extends MemberInfo> map, String output) {
 382         assertCommand(after, cmd, output);
 383         if (after) {
 384             map.remove(name);
 385             for (int i = 0; i < keys.size(); ++i) {
 386                 MemberInfo m = keys.get(i);
 387                 if (m.toString().equals(name)) {
 388                     keys.remove(i);
 389                     return;
 390                 }
 391             }
 392             throw new AssertionError("Key not found: " + name + ", keys: " + keys);
 393         }
 394     }
 395 
 396     public void dropVariable(boolean after, String cmd, String name, String output) {
 397         dropKey(after, cmd, name, variables, output);
 398     }
 399 
 400     public void dropMethod(boolean after, String cmd, String name, String output) {
 401         dropKey(after, cmd, name, methods, output);
 402     }
 403 
 404     public void dropClass(boolean after, String cmd, String name, String output) {
 405         dropKey(after, cmd, name, classes, output);
 406     }
 407 
 408     public void dropImport(boolean after, String cmd, String name, String output) {
 409         dropKey(after, cmd, name, imports, output);
 410     }
 411 
 412     public void assertCommand(boolean after, String cmd, String out) {
 413         assertCommand(after, cmd, out, "", null, "", "");
 414     }
 415 
 416     public void assertCommandOutputContains(boolean after, String cmd, String has) {
 417         assertCommandCheckOutput(after, cmd, (s) ->
 418                         assertTrue(s.contains(has), "Output: \'" + s + "' does not contain: " + has));
 419     }
 420 
 421     public void assertCommandOutputStartsWith(boolean after, String cmd, String starts) {
 422         assertCommandCheckOutput(after, cmd, assertStartsWith(starts));
 423     }
 424 
 425     public void assertCommandCheckOutput(boolean after, String cmd, Consumer<String> check) {
 426         if (!after) {
 427             assertCommand(false, cmd, null);
 428         } else {
 429             String got = getCommandOutput();
 430             check.accept(got);
 431             assertCommand(true, cmd, null);
 432         }
 433     }
 434 
 435     public void assertCommand(boolean after, String cmd, String out, String err,
 436             String userinput, String print, String usererr) {
 437         if (!after) {
 438             if (userinput != null) {
 439                 setUserInput(userinput);
 440             }
 441             setCommandInput(cmd + "\n");
 442         } else {
 443             assertOutput(getCommandOutput().trim(), out==null? out : out.trim(), "command output: " + cmd);
 444             assertOutput(getCommandErrorOutput(), err, "command error: " + cmd);
 445             assertOutput(getUserOutput(), print, "user output: " + cmd);
 446             assertOutput(getUserErrorOutput(), usererr, "user error: " + cmd);
 447         }
 448     }
 449 
 450     public void assertCompletion(boolean after, String code, boolean isSmart, String... expected) {
 451         if (!after) {
 452             setCommandInput("\n");
 453         } else {
 454             assertCompletion(code, isSmart, expected);
 455         }
 456     }
 457 
 458     public void assertCompletion(String code, boolean isSmart, String... expected) {
 459         List<String> completions = computeCompletions(code, isSmart);
 460         assertEquals(completions, Arrays.asList(expected), "Command: " + code + ", output: " +
 461                 completions.toString());
 462     }
 463 
 464     private List<String> computeCompletions(String code, boolean isSmart) {
 465         JShellTool js = this.repl != null ? this.repl
 466                                       : new JShellTool(null, null, null, null, null, null, null, prefs, Locale.ROOT);
 467         int cursor =  code.indexOf('|');
 468         code = code.replace("|", "");
 469         assertTrue(cursor > -1, "'|' not found: " + code);
 470         List<Suggestion> completions =
 471                 js.commandCompletionSuggestions(code, cursor, new int[1]); //XXX: ignoring anchor for now
 472         return completions.stream()
 473                           .filter(s -> isSmart == s.matchesType())
 474                           .map(s -> s.continuation())
 475                           .distinct()
 476                           .collect(Collectors.toList());
 477     }
 478 
 479     public Consumer<String> assertStartsWith(String prefix) {
 480         return (output) -> assertTrue(output.startsWith(prefix), "Output: \'" + output + "' does not start with: " + prefix);
 481     }
 482 
 483     public void assertOutput(String got, String expected, String display) {
 484         if (expected != null) {
 485             assertEquals(got, expected, display + ".\n");
 486         }
 487     }
 488 
 489     private String normalizeLineEndings(String text) {
 490         return text.replace(System.getProperty("line.separator"), "\n");
 491     }
 492 
 493     public static abstract class MemberInfo {
 494         public final String source;
 495         public final String type;
 496         public final String name;
 497 
 498         public MemberInfo(String source, String type, String name) {
 499             this.source = source;
 500             this.type = type;
 501             this.name = name;
 502         }
 503 
 504         @Override
 505         public int hashCode() {
 506             return name.hashCode();
 507         }
 508 
 509         @Override
 510         public boolean equals(Object o) {
 511             if (o instanceof MemberInfo) {
 512                 MemberInfo mi = (MemberInfo) o;
 513                 return name.equals(mi.name);
 514             }
 515             return false;
 516         }
 517 
 518         public abstract Consumer<String> checkOutput();
 519 
 520         public String getSource() {
 521             return source;
 522         }
 523     }
 524 
 525     public static class VariableInfo extends MemberInfo {
 526 
 527         public final String value;
 528         public final String initialValue;
 529 
 530         public VariableInfo(String src, String type, String name) {
 531             super(src, type, name);
 532             this.initialValue = null;
 533             switch (type) {
 534                 case "byte":
 535                 case "short":
 536                 case "int":
 537                 case "long":
 538                     value = "0";
 539                     break;
 540                 case "boolean":
 541                     value = "false";
 542                     break;
 543                 case "char":
 544                     value = "''";
 545                     break;
 546                 case "float":
 547                 case "double":
 548                     value = "0.0";
 549                     break;
 550                 default:
 551                     value = "null";
 552             }
 553         }
 554 
 555         public VariableInfo(String src, String type, String name, String value) {
 556             super(src, type, name);
 557             this.value = value;
 558             this.initialValue = value;
 559         }
 560 
 561         @Override
 562         public Consumer<String> checkOutput() {
 563             String arrowPattern = String.format("%s ==> %s", name, value);
 564             Predicate<String> arrowCheckOutput = Pattern.compile(arrowPattern).asPredicate();
 565             String howeverPattern = String.format("\\| *\\w+ variable %s, however*.", name);
 566             Predicate<String> howeverCheckOutput = Pattern.compile(howeverPattern).asPredicate();
 567             return output -> {
 568                 if (output.startsWith("|  ")) {
 569                     assertTrue(howeverCheckOutput.test(output),
 570                     "Output: " + output + " does not fit pattern: " + howeverPattern);
 571                 } else {
 572                     assertTrue(arrowCheckOutput.test(output),
 573                     "Output: " + output + " does not fit pattern: " + arrowPattern);
 574                 }
 575             };
 576         }
 577 
 578         @Override
 579         public int hashCode() {
 580             return name.hashCode();
 581         }
 582 
 583         @Override
 584         public boolean equals(Object o) {
 585             if (o instanceof VariableInfo) {
 586                 VariableInfo v = (VariableInfo) o;
 587                 return name.equals(v.name);
 588             }
 589             return false;
 590         }
 591 
 592         @Override
 593         public String toString() {
 594             return String.format("%s %s = %s", type, name, value);
 595         }
 596 
 597         @Override
 598         public String getSource() {
 599             String src = super.getSource();
 600             return src.endsWith(";") ? src : src + ";";
 601         }
 602     }
 603 
 604     public static class TempVariableInfo extends VariableInfo {
 605 
 606         public TempVariableInfo(String src, String type, String name, String value) {
 607             super(src, type, name, value);
 608         }
 609 
 610         @Override
 611         public String getSource() {
 612             return source;
 613         }
 614     }
 615 
 616     public static class MethodInfo extends MemberInfo {
 617 
 618         public final String signature;
 619 
 620         public MethodInfo(String source, String signature, String name) {
 621             super(source, signature.substring(0, signature.lastIndexOf(')') + 1), name);
 622             this.signature = signature;
 623         }
 624 
 625         @Override
 626         public Consumer<String> checkOutput() {
 627             String expectedOutput = String.format("\\| *\\w+ method %s", name);
 628             Predicate<String> checkOutput = Pattern.compile(expectedOutput).asPredicate();
 629             return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s);
 630         }
 631 
 632         @Override
 633         public int hashCode() {
 634             return (name.hashCode() << 2) ^ type.hashCode() ;
 635         }
 636 
 637         @Override
 638         public boolean equals(Object o) {
 639             if (o instanceof MemberInfo) {
 640                 MemberInfo m = (MemberInfo) o;
 641                 return name.equals(m.name) && type.equals(m.type);
 642             }
 643             return false;
 644         }
 645 
 646         @Override
 647         public String toString() {
 648             return String.format("%s %s", name, signature);
 649         }
 650     }
 651 
 652     public static class ClassInfo extends MemberInfo {
 653 
 654         public ClassInfo(String source, String type, String name) {
 655             super(source, type, name);
 656         }
 657 
 658         @Override
 659         public Consumer<String> checkOutput() {
 660             String fullType = type.equals("@interface")? "annotation interface" : type;
 661             String expectedOutput = String.format("\\| *\\w+ %s %s", fullType, name);
 662             Predicate<String> checkOutput = Pattern.compile(expectedOutput).asPredicate();
 663             return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s);
 664         }
 665 
 666         @Override
 667         public int hashCode() {
 668             return name.hashCode() ;
 669         }
 670 
 671         @Override
 672         public boolean equals(Object o) {
 673             if (o instanceof ClassInfo) {
 674                 ClassInfo c = (ClassInfo) o;
 675                 return name.equals(c.name);
 676             }
 677             return false;
 678         }
 679 
 680         @Override
 681         public String toString() {
 682             return String.format("%s %s", type, name);
 683         }
 684     }
 685 
 686     public static class ImportInfo extends MemberInfo {
 687         public ImportInfo(String source, String type, String fullname) {
 688             super(source, type, fullname);
 689         }
 690 
 691         @Override
 692         public Consumer<String> checkOutput() {
 693             return s -> assertTrue("".equals(s), "Expected: '', actual: " + s);
 694         }
 695 
 696         @Override
 697         public int hashCode() {
 698             return (name.hashCode() << 2) ^ type.hashCode() ;
 699         }
 700 
 701         @Override
 702         public boolean equals(Object o) {
 703             if (o instanceof ImportInfo) {
 704                 ImportInfo i = (ImportInfo) o;
 705                 return name.equals(i.name) && type.equals(i.type);
 706             }
 707             return false;
 708         }
 709 
 710         @Override
 711         public String toString() {
 712             return String.format("import %s%s", type.equals("static") ? "static " : "", name);
 713         }
 714     }
 715 
 716     class WaitingTestingInputStream extends TestingInputStream {
 717 
 718         @Override
 719         synchronized void setInput(String s) {
 720             super.setInput(s);
 721             notify();
 722         }
 723 
 724         synchronized void waitForInput() {
 725             boolean interrupted = false;
 726             try {
 727                 while (available() == 0) {
 728                     try {
 729                         wait();
 730                     } catch (InterruptedException e) {
 731                         interrupted = true;
 732                         // fall through and retry
 733                     }
 734                 }
 735             } finally {
 736                 if (interrupted) {
 737                     Thread.currentThread().interrupt();
 738                 }
 739             }
 740         }
 741 
 742         @Override
 743         public int read() {
 744             waitForInput();
 745             return super.read();
 746         }
 747 
 748         @Override
 749         public int read(byte b[], int off, int len) {
 750             waitForInput();
 751             return super.read(b, off, len);
 752         }
 753     }
 754 
 755     class PromptedCommandOutputStream extends OutputStream {
 756         private final ReplTest[] tests;
 757         private int index = 0;
 758         PromptedCommandOutputStream(ReplTest[] tests) {
 759             this.tests = tests;
 760         }
 761 
 762         @Override
 763         public synchronized void write(int b) {
 764             if (b == 5 || b == 6) {
 765                 if (index < (tests.length - 1)) {
 766                     tests[index].run(true);
 767                     tests[index + 1].run(false);
 768                 } else {
 769                     fail("Did not exit Repl tool after test");
 770                 }
 771                 ++index;
 772             } // For now, anything else is thrown away
 773         }
 774 
 775         @Override
 776         public synchronized void write(byte b[], int off, int len) {
 777             if ((off < 0) || (off > b.length) || (len < 0)
 778                     || ((off + len) - b.length > 0)) {
 779                 throw new IndexOutOfBoundsException();
 780             }
 781             for (int i = 0; i < len; ++i) {
 782                 write(b[off + i]);
 783             }
 784         }
 785     }
 786 
 787     public static final class MemoryPreferences extends AbstractPreferences {
 788 
 789         private final Map<String, String> values = new HashMap<>();
 790         private final Map<String, MemoryPreferences> nodes = new HashMap<>();
 791 
 792         public MemoryPreferences() {
 793             this(null, "");
 794         }
 795 
 796         public MemoryPreferences(MemoryPreferences parent, String name) {
 797             super(parent, name);
 798         }
 799 
 800         @Override
 801         protected void putSpi(String key, String value) {
 802             values.put(key, value);
 803         }
 804 
 805         @Override
 806         protected String getSpi(String key) {
 807             return values.get(key);
 808         }
 809 
 810         @Override
 811         protected void removeSpi(String key) {
 812             values.remove(key);
 813         }
 814 
 815         @Override
 816         protected void removeNodeSpi() throws BackingStoreException {
 817             ((MemoryPreferences) parent()).nodes.remove(name());
 818         }
 819 
 820         @Override
 821         protected String[] keysSpi() throws BackingStoreException {
 822             return values.keySet().toArray(new String[0]);
 823         }
 824 
 825         @Override
 826         protected String[] childrenNamesSpi() throws BackingStoreException {
 827             return nodes.keySet().toArray(new String[0]);
 828         }
 829 
 830         @Override
 831         protected AbstractPreferences childSpi(String name) {
 832             return nodes.computeIfAbsent(name, n -> new MemoryPreferences(this, name));
 833         }
 834 
 835         @Override
 836         protected void syncSpi() throws BackingStoreException {
 837         }
 838 
 839         @Override
 840         protected void flushSpi() throws BackingStoreException {
 841         }
 842 
 843     }
 844 }