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