/* * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.AbstractPreferences; import java.util.prefs.BackingStoreException; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import org.testng.annotations.BeforeMethod; import jdk.jshell.tool.JavaShellToolBuilder; import static java.util.stream.Collectors.toList; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; public class ReplToolTesting { private final static String DEFAULT_STARTUP_MESSAGE = "| Welcome to"; final static List START_UP_IMPORTS = Stream.of( "java.io.*", "java.math.*", "java.net.*", "java.nio.file.*", "java.util.*", "java.util.concurrent.*", "java.util.function.*", "java.util.prefs.*", "java.util.regex.*", "java.util.stream.*") .map(s -> new ImportInfo("import " + s + ";", "", s)) .collect(toList()); final static List START_UP_METHODS = Stream.of() .collect(toList()); final static List START_UP_CMD_METHOD = Stream.of() .collect(toList()); final static List PRINTING_CMD_METHOD = Stream.of( "| void print(boolean)", "| void print(char)", "| void print(int)", "| void print(long)", "| void print(float)", "| void print(double)", "| void print(char s[])", "| void print(String)", "| void print(Object)", "| void println()", "| void println(boolean)", "| void println(char)", "| void println(int)", "| void println(long)", "| void println(float)", "| void println(double)", "| void println(char s[])", "| void println(String)", "| void println(Object)", "| void printf(java.util.Locale,String,Object...)", "| void printf(String,Object...)") .collect(toList()); final static List START_UP = Collections.unmodifiableList( Stream.concat(START_UP_IMPORTS.stream(), START_UP_METHODS.stream()) .map(s -> s.getSource()) .collect(toList())); private WaitingTestingInputStream cmdin = null; private ByteArrayOutputStream cmdout = null; private ByteArrayOutputStream cmderr = null; private PromptedCommandOutputStream console = null; private TestingInputStream userin = null; private ByteArrayOutputStream userout = null; private ByteArrayOutputStream usererr = null; private List keys; private Map variables; private Map methods; private Map classes; private Map imports; private boolean isDefaultStartUp = true; protected Map prefsMap; private Map envvars; public interface ReplTest { void run(boolean after); } public void setCommandInput(String s) { cmdin.setInput(s); } public void closeCommandInput() { try { cmdin.close(); } catch (IOException ex) { throw new IllegalStateException(ex); } } public final static Pattern idPattern = Pattern.compile("^\\s+(\\d+)"); public Consumer assertList() { return s -> { List lines = Stream.of(s.split("\n")) .filter(l -> !l.isEmpty()) .collect(Collectors.toList()); int previousId = Integer.MIN_VALUE; assertEquals(lines.size(), keys.size(), "Number of keys"); for (int i = 0; i < lines.size(); ++i) { String line = lines.get(i); Matcher matcher = idPattern.matcher(line); assertTrue(matcher.find(), "Snippet id not found: " + line); String src = keys.get(i).getSource(); assertTrue(line.endsWith(src), "Line '" + line + "' does not end with: " + src); int id = Integer.parseInt(matcher.group(1)); assertTrue(previousId < id, String.format("The previous id is not less than the next one: previous: %d, next: %d", previousId, id)); previousId = id; } }; } private final static Pattern extractPattern = Pattern.compile("^\\| *(.*)$"); private Consumer assertMembers(String message, Map set) { return s -> { List lines = Stream.of(s.split("\n")) .filter(l -> !l.isEmpty()) .filter(l -> !l.startsWith("| ")) // error/unresolved info .collect(Collectors.toList()); assertEquals(lines.size(), set.size(), message + " : expected: " + set.keySet() + "\ngot:\n" + lines); for (String line : lines) { Matcher matcher = extractPattern.matcher(line); assertTrue(matcher.find(), line); String src = matcher.group(1); MemberInfo info = set.get(src); assertNotNull(info, "Not found snippet with signature: " + src + ", line: " + line + ", keys: " + set.keySet() + "\n"); } }; } public Consumer assertVariables() { return assertMembers("Variables", variables); } public Consumer assertMethods() { return assertMembers("Methods", methods); } public Consumer assertClasses() { return assertMembers("Classes", classes); } public Consumer assertImports() { return assertMembers("Imports", imports); } public String getCommandOutput() { String s = normalizeLineEndings(cmdout.toString()); cmdout.reset(); return s; } public String getCommandErrorOutput() { String s = normalizeLineEndings(cmderr.toString()); cmderr.reset(); return s; } public void setUserInput(String s) { userin.setInput(s); } public String getUserOutput() { String s = normalizeLineEndings(userout.toString()); userout.reset(); return s; } public String getUserErrorOutput() { String s = normalizeLineEndings(usererr.toString()); usererr.reset(); return s; } public String getTerminalOutput() { String s = normalizeLineEndings("\r\n", console.data.toString()); console.data.reset(); return s; } public void test(ReplTest... tests) { test(new String[0], tests); } public void test(String[] args, ReplTest... tests) { test(true, args, tests); } public void test(String[] args, String expectedErrorOutput, ReplTest... tests) { test(Locale.ROOT, true, args, expectedErrorOutput, DEFAULT_STARTUP_MESSAGE, tests); } public void test(boolean isDefaultStartUp, String[] args, ReplTest... tests) { test(Locale.ROOT, isDefaultStartUp, args, DEFAULT_STARTUP_MESSAGE, tests); } public void testNoStartUp(ReplTest... tests) { test(Locale.ROOT, false, new String[] {"--no-startup"}, DEFAULT_STARTUP_MESSAGE, tests); } public void test(Locale locale, boolean isDefaultStartUp, String[] args, String startUpMessage, ReplTest... tests) { test(locale, isDefaultStartUp, args, "", startUpMessage, tests); } public void test(Locale locale, boolean isDefaultStartUp, String[] args, String expectedErrorOutput, String startUpMessage, ReplTest... tests) { this.isDefaultStartUp = isDefaultStartUp; initSnippets(); ReplTest[] wtests = new ReplTest[tests.length + 3]; wtests[0] = a -> assertCommandCheckOutput(a, "", s -> assertTrue(s.startsWith(startUpMessage), "Expected start-up message '" + startUpMessage + "' Got: " + s)); wtests[1] = a -> assertCommand(a, "/debug 0", null); System.arraycopy(tests, 0, wtests, 2, tests.length); wtests[tests.length + 2] = a -> assertCommand(a, "/exit", null); testRaw(locale, args, expectedErrorOutput, wtests); } private void initSnippets() { keys = new ArrayList<>(); variables = new HashMap<>(); methods = new HashMap<>(); classes = new HashMap<>(); imports = new HashMap<>(); if (isDefaultStartUp) { methods.putAll( START_UP_METHODS.stream() .collect(Collectors.toMap(Object::toString, Function.identity()))); imports.putAll( START_UP_IMPORTS.stream() .collect(Collectors.toMap(Object::toString, Function.identity()))); } } @BeforeMethod public void setUp() { prefsMap = new HashMap<>(); prefsMap.put("INDENT", "0"); envvars = new HashMap<>(); System.setProperty("jshell.test.allow.incomplete.inputs", "true"); } protected void setEnvVar(String name, String value) { envvars.put(name, value); } protected JavaShellToolBuilder builder(Locale locale) { // turn on logging of launch failures Logger.getLogger("jdk.jshell.execution").setLevel(Level.ALL); return JavaShellToolBuilder .builder() .in(cmdin, userin) .out(new PrintStream(cmdout), new PrintStream(console), new PrintStream(userout)) .err(new PrintStream(cmderr), new PrintStream(usererr)) .persistence(prefsMap) .env(envvars) .locale(locale) .promptCapture(true); } private void testRaw(Locale locale, String[] args, String expectedErrorOutput, ReplTest... tests) { testRawInit(tests); testRawRun(locale, Presets.addExecutionIfMissing(args)); testRawCheck(locale, expectedErrorOutput); } private void testRawInit(ReplTest... tests) { cmdin = new WaitingTestingInputStream(); cmdout = new ByteArrayOutputStream(); cmderr = new ByteArrayOutputStream(); console = new PromptedCommandOutputStream(tests); userin = new TestingInputStream(); userout = new ByteArrayOutputStream(); usererr = new ByteArrayOutputStream(); } protected void testRawRun(Locale locale, String[] args) { try { builder(locale) .run(args); } catch (Exception ex) { fail("Repl tool died with exception", ex); } } private void testRawCheck(Locale locale, String expectedErrorOutput) { // perform internal consistency checks on state, if desired String cos = getCommandOutput(); String ceos = getCommandErrorOutput(); String uos = getUserOutput(); String ueos = getUserErrorOutput(); assertTrue((cos.isEmpty() || cos.startsWith("| Goodbye") || !locale.equals(Locale.ROOT)), "Expected a goodbye, but got: " + cos); assertEquals(ceos, expectedErrorOutput, "Expected \"" + expectedErrorOutput + "\" command error output, got: \"" + ceos + "\""); assertTrue(uos.isEmpty(), "Expected empty user output, got: " + uos); assertTrue(ueos.isEmpty(), "Expected empty user error output, got: " + ueos); } public void assertReset(boolean after, String cmd) { assertCommand(after, cmd, "| Resetting state.\n"); initSnippets(); } public void evaluateExpression(boolean after, String type, String expr, String value) { String output = String.format("(\\$\\d+) ==> %s", value); Pattern outputPattern = Pattern.compile(output); assertCommandCheckOutput(after, expr, s -> { Matcher matcher = outputPattern.matcher(s); assertTrue(matcher.find(), "Output: '" + s + "' does not fit pattern: '" + output + "'"); String name = matcher.group(1); VariableInfo tempVar = new TempVariableInfo(expr, type, name, value); variables.put(tempVar.toString(), tempVar); addKey(after, tempVar); }); } public void loadVariable(boolean after, String type, String name) { loadVariable(after, type, name, null, null); } public void loadVariable(boolean after, String type, String name, String expr, String value) { String src = expr == null ? String.format("%s %s", type, name) : String.format("%s %s = %s", type, name, expr); VariableInfo var = expr == null ? new VariableInfo(src, type, name) : new VariableInfo(src, type, name, value); addKey(after, var, variables); addKey(after, var); } public void assertVariable(boolean after, String type, String name) { assertVariable(after, type, name, null, null); } public void assertVariable(boolean after, String type, String name, String expr, String value) { String src = expr == null ? String.format("%s %s", type, name) : String.format("%s %s = %s", type, name, expr); VariableInfo var = expr == null ? new VariableInfo(src, type, name) : new VariableInfo(src, type, name, value); assertCommandCheckOutput(after, src, var.checkOutput()); addKey(after, var, variables); addKey(after, var); } public void loadMethod(boolean after, String src, String signature, String name) { MethodInfo method = new MethodInfo(src, signature, name); addKey(after, method, methods); addKey(after, method); } public void assertMethod(boolean after, String src, String signature, String name) { MethodInfo method = new MethodInfo(src, signature, name); assertCommandCheckOutput(after, src, method.checkOutput()); addKey(after, method, methods); addKey(after, method); } public void loadClass(boolean after, String src, String type, String name) { ClassInfo clazz = new ClassInfo(src, type, name); addKey(after, clazz, classes); addKey(after, clazz); } public void assertClass(boolean after, String src, String type, String name) { ClassInfo clazz = new ClassInfo(src, type, name); assertCommandCheckOutput(after, src, clazz.checkOutput()); addKey(after, clazz, classes); addKey(after, clazz); } public void loadImport(boolean after, String src, String type, String name) { ImportInfo i = new ImportInfo(src, type, name); addKey(after, i, imports); addKey(after, i); } public void assertImport(boolean after, String src, String type, String name) { ImportInfo i = new ImportInfo(src, type, name); assertCommandCheckOutput(after, src, i.checkOutput()); addKey(after, i, imports); addKey(after, i); } private void addKey(boolean after, T memberInfo, Map map) { if (after) { map.entrySet().removeIf(e -> e.getValue().equals(memberInfo)); map.put(memberInfo.toString(), memberInfo); } } private void addKey(boolean after, T memberInfo) { if (after) { for (int i = 0; i < keys.size(); ++i) { MemberInfo m = keys.get(i); if (m.equals(memberInfo)) { keys.set(i, memberInfo); return; } } keys.add(memberInfo); } } private void dropKey(boolean after, String cmd, String name, Map map, String output) { assertCommand(after, cmd, output); if (after) { map.remove(name); for (int i = 0; i < keys.size(); ++i) { MemberInfo m = keys.get(i); if (m.toString().equals(name)) { keys.remove(i); return; } } throw new AssertionError("Key not found: " + name + ", keys: " + keys); } } public void dropVariable(boolean after, String cmd, String name, String output) { dropKey(after, cmd, name, variables, output); } public void dropMethod(boolean after, String cmd, String name, String output) { dropKey(after, cmd, name, methods, output); } public void dropClass(boolean after, String cmd, String name, String output) { dropKey(after, cmd, name, classes, output); } public void dropImport(boolean after, String cmd, String name, String output) { dropKey(after, cmd, name, imports, output); } public void assertCommand(boolean after, String cmd, String out) { assertCommand(after, cmd, out, "", null, "", ""); } public void assertCommandOutputContains(boolean after, String cmd, String... hasThese) { assertCommandCheckOutput(after, cmd, (s) -> assertTrue(Arrays.stream(hasThese) .allMatch(has -> s.contains(has)), "Output: \'" + s + "' does not contain: " + Arrays.stream(hasThese) .filter(has -> !s.contains(has)) .collect(Collectors.joining(", ")))); } public void assertCommandOutputStartsWith(boolean after, String cmd, String starts) { assertCommandCheckOutput(after, cmd, assertStartsWith(starts)); } public void assertCommandCheckOutput(boolean after, String cmd, Consumer check) { if (!after) { assertCommand(false, cmd, null); } else { String got = getCommandOutput(); check.accept(got); assertCommand(true, cmd, null); } } public void assertCommandUserOutputContains(boolean after, String cmd, String... hasThese) { assertCommandCheckUserOutput(after, cmd, (s) -> assertTrue(Arrays.stream(hasThese) .allMatch(has -> s.contains(has)), "User output: \'" + s + "' does not contain: " + Arrays.stream(hasThese) .filter(has -> !s.contains(has)) .collect(Collectors.joining(", ")))); } public void assertCommandCheckUserOutput(boolean after, String cmd, Consumer check) { if (!after) { assertCommand(false, cmd, null); } else { String got = getUserOutput(); check.accept(got); assertCommand(true, cmd, null); } } public void assertCommand(boolean after, String cmd, String out, String err, String userinput, String print, String usererr) { assertCommand(after, cmd, out, err, userinput, print, usererr, null); } public void assertCommand(boolean after, String cmd, String out, String err, String userinput, String print, String usererr, String terminalOut) { if (!after) { if (userinput != null) { setUserInput(userinput); } if (cmd.endsWith("\u0003")) { setCommandInput(cmd); } else { setCommandInput(cmd + "\n"); } } else { assertOutput(getCommandOutput().trim(), out==null? out : out.trim(), "command output: " + cmd); assertOutput(getCommandErrorOutput(), err, "command error: " + cmd); assertOutput(getUserOutput(), print, "user output: " + cmd); assertOutput(getUserErrorOutput(), usererr, "user error: " + cmd); assertOutput(getTerminalOutput(), terminalOut, "terminal output: " + cmd); } } public Consumer assertStartsWith(String prefix) { return (output) -> { if (!output.trim().startsWith(prefix)) { int i = 0; } assertTrue(output.trim().startsWith(prefix), "Output: \'" + output + "' does not start with: " + prefix); }; } public void assertOutput(String got, String expected, String display) { if (expected != null) { assertEquals(got, expected, display + ".\n"); } } private String normalizeLineEndings(String text) { return normalizeLineEndings(System.getProperty("line.separator"), text); } private String normalizeLineEndings(String lineSeparator, String text) { return ANSI_CODE_PATTERN.matcher(text.replace(lineSeparator, "\n")).replaceAll(""); } private static final Pattern ANSI_CODE_PATTERN = Pattern.compile("\033\\[[\060-\077]*[\040-\057]*[\100-\176]"); public static abstract class MemberInfo { public final String source; public final String type; public final String name; public MemberInfo(String source, String type, String name) { this.source = source; this.type = type; this.name = name; } @Override public int hashCode() { return name.hashCode(); } @Override public boolean equals(Object o) { if (o instanceof MemberInfo) { MemberInfo mi = (MemberInfo) o; return name.equals(mi.name); } return false; } public abstract Consumer checkOutput(); public String getSource() { return source; } } public static class VariableInfo extends MemberInfo { public final String value; public final String initialValue; public VariableInfo(String src, String type, String name) { super(src, type, name); this.initialValue = null; switch (type) { case "byte": case "short": case "int": case "long": value = "0"; break; case "boolean": value = "false"; break; case "char": value = "''"; break; case "float": case "double": value = "0.0"; break; default: value = "null"; } } public VariableInfo(String src, String type, String name, String value) { super(src, type, name); this.value = value; this.initialValue = value; } @Override public Consumer checkOutput() { String arrowPattern = String.format("%s ==> %s", name, value); Predicate arrowCheckOutput = Pattern.compile(arrowPattern).asPredicate(); String howeverPattern = String.format("\\| *\\w+ variable %s, however*.", name); Predicate howeverCheckOutput = Pattern.compile(howeverPattern).asPredicate(); return output -> { if (output.startsWith("| ")) { assertTrue(howeverCheckOutput.test(output), "Output: " + output + " does not fit pattern: " + howeverPattern); } else { assertTrue(arrowCheckOutput.test(output), "Output: " + output + " does not fit pattern: " + arrowPattern); } }; } @Override public int hashCode() { return name.hashCode(); } @Override public boolean equals(Object o) { if (o instanceof VariableInfo) { VariableInfo v = (VariableInfo) o; return name.equals(v.name); } return false; } @Override public String toString() { return String.format("%s %s = %s", type, name, value); } @Override public String getSource() { String src = super.getSource(); return src.endsWith(";") ? src : src + ";"; } } public static class TempVariableInfo extends VariableInfo { public TempVariableInfo(String src, String type, String name, String value) { super(src, type, name, value); } @Override public String getSource() { return source; } } public static class MethodInfo extends MemberInfo { public final String signature; public MethodInfo(String source, String signature, String name) { super(source, signature.substring(0, signature.lastIndexOf(')') + 1), name); this.signature = signature; } @Override public Consumer checkOutput() { String expectedOutput = String.format("\\| *\\w+ method %s", name); Predicate checkOutput = Pattern.compile(expectedOutput).asPredicate(); return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s); } @Override public int hashCode() { return (name.hashCode() << 2) ^ type.hashCode() ; } @Override public boolean equals(Object o) { if (o instanceof MemberInfo) { MemberInfo m = (MemberInfo) o; return name.equals(m.name) && type.equals(m.type); } return false; } @Override public String toString() { int i = signature.lastIndexOf(")") + 1; if (i <= 0) { return String.format("%s", name); } else { return String.format("%s %s%s", signature.substring(i), name, signature.substring(0, i)); } } } public static class ClassInfo extends MemberInfo { public ClassInfo(String source, String type, String name) { super(source, type, name); } @Override public Consumer checkOutput() { String fullType = type.equals("@interface")? "annotation interface" : type; String expectedOutput = String.format("\\| *\\w+ %s %s", fullType, name); Predicate checkOutput = Pattern.compile(expectedOutput).asPredicate(); return s -> assertTrue(checkOutput.test(s), "Expected: '" + expectedOutput + "', actual: " + s); } @Override public int hashCode() { return name.hashCode() ; } @Override public boolean equals(Object o) { if (o instanceof ClassInfo) { ClassInfo c = (ClassInfo) o; return name.equals(c.name); } return false; } @Override public String toString() { return String.format("%s %s", type, name); } } public static class ImportInfo extends MemberInfo { public ImportInfo(String source, String type, String fullname) { super(source, type, fullname); } @Override public Consumer checkOutput() { return s -> assertTrue("".equals(s), "Expected: '', actual: " + s); } @Override public int hashCode() { return (name.hashCode() << 2) ^ type.hashCode() ; } @Override public boolean equals(Object o) { if (o instanceof ImportInfo) { ImportInfo i = (ImportInfo) o; return name.equals(i.name) && type.equals(i.type); } return false; } @Override public String toString() { return String.format("import %s%s", type.equals("static") ? "static " : "", name); } } class WaitingTestingInputStream extends TestingInputStream { private boolean closed; @Override synchronized void setInput(String s) { super.setInput(s); notify(); } synchronized void waitForInput() { boolean interrupted = false; try { while (available() == 0 && !closed) { try { wait(); } catch (InterruptedException e) { interrupted = true; // fall through and retry } } } finally { if (interrupted) { Thread.currentThread().interrupt(); } } } @Override public int read() { waitForInput(); return super.read(); } @Override public int read(byte b[], int off, int len) { waitForInput(); return super.read(b, off, len); } @Override public synchronized void close() throws IOException { closed = true; notify(); } } class PromptedCommandOutputStream extends OutputStream { private final ReplTest[] tests; private final ByteArrayOutputStream data = new ByteArrayOutputStream(); private int index = 0; PromptedCommandOutputStream(ReplTest[] tests) { this.tests = tests; } @Override public synchronized void write(int b) { if (b == 5 || b == 6) { if (index < (tests.length - 1)) { tests[index].run(true); tests[index + 1].run(false); } else { fail("Did not exit Repl tool after test"); } ++index; } data.write(b); } @Override public synchronized void write(byte b[], int off, int len) { if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) - b.length > 0)) { throw new IndexOutOfBoundsException(); } for (int i = 0; i < len; ++i) { write(b[off + i]); } } } public static final class MemoryPreferences extends AbstractPreferences { private final Map values = new HashMap<>(); private final Map nodes = new HashMap<>(); public MemoryPreferences() { this(null, ""); } public MemoryPreferences(MemoryPreferences parent, String name) { super(parent, name); } @Override protected void putSpi(String key, String value) { values.put(key, value); } @Override protected String getSpi(String key) { return values.get(key); } @Override protected void removeSpi(String key) { values.remove(key); } @Override protected void removeNodeSpi() throws BackingStoreException { ((MemoryPreferences) parent()).nodes.remove(name()); } @Override protected String[] keysSpi() throws BackingStoreException { return values.keySet().toArray(new String[0]); } @Override protected String[] childrenNamesSpi() throws BackingStoreException { return nodes.keySet().toArray(new String[0]); } @Override protected AbstractPreferences childSpi(String name) { return nodes.computeIfAbsent(name, n -> new MemoryPreferences(this, name)); } @Override protected void syncSpi() throws BackingStoreException { } @Override protected void flushSpi() throws BackingStoreException { } } }