# HG changeset patch # User rfield # Date 1452530460 28800 # Node ID aacf94dab449d2642a7ced6c468dad9efd8b4971 # Parent 416dfba03d33f9943d2b03bd85b67d83b8e4e581 8081845: JShell: Need way to refresh relative to external state Summary: Add the ability to record and replay relevant parts of history Reviewed-by: jlahoda diff -r 416dfba03d33 -r aacf94dab449 langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java --- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java Mon Jan 11 17:08:20 2016 +0100 +++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java Mon Jan 11 08:41:00 2016 -0800 @@ -1,3 +1,4 @@ + /* * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. @@ -90,8 +91,10 @@ import java.util.Optional; import java.util.ResourceBundle; import java.util.Spliterators; +import java.util.function.Function; import java.util.function.Supplier; import static java.util.stream.Collectors.toList; +import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; /** * Command line REPL tool for Java using the JShell API. @@ -102,6 +105,7 @@ private static final Pattern LINEBREAK = Pattern.compile("\\R"); private static final Pattern HISTORY_ALL_START_FILENAME = Pattern.compile( "((?(all|history|start))(\\z|\\p{javaWhitespace}+))?(?.*)"); + private static final String RECORD_SEPARATOR = "\u241E"; final InputStream cmdin; final PrintStream cmdout; @@ -150,9 +154,14 @@ private String cmdlineStartup = null; private String editor = null; - static final Preferences PREFS = Preferences.userRoot().node("tool/REPL"); + // Commands and snippets which should be replayed + private List replayableHistory; + private List replayableHistoryPrevious; + + static final Preferences PREFS = Preferences.userRoot().node("tool/JShell"); static final String STARTUP_KEY = "STARTUP"; + static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE"; static final String DEFAULT_STARTUP = "\n" + @@ -165,11 +174,14 @@ "import java.util.regex.*;\n" + "void printf(String format, Object... args) { System.out.printf(format, args); }\n"; - // Tool id (tid) mapping + // Tool id (tid) mapping: the three name spaces NameSpace mainNamespace; NameSpace startNamespace; NameSpace errorNamespace; + + // Tool id (tid) mapping: the current name spaces NameSpace currentNameSpace; + Map mapSnippet; void debug(String format, Object... args) { @@ -252,6 +264,12 @@ private void start(IOContext in, List loadList) { resetState(); // Initialize + // Read replay history from last jshell session into previous history + String prevReplay = PREFS.get(REPLAY_RESTORE_KEY, null); + if (prevReplay != null) { + replayableHistoryPrevious = Arrays.asList(prevReplay.split(RECORD_SEPARATOR)); + } + for (String loadFile : loadList) { cmdOpen(loadFile); } @@ -370,6 +388,10 @@ mapSnippet = new LinkedHashMap<>(); currentNameSpace = startNamespace; + // Reset the replayable history, saving the old for restore + replayableHistoryPrevious = replayableHistory; + replayableHistory = new ArrayList<>(); + state = JShell.builder() .in(userin) .out(userout) @@ -382,7 +404,8 @@ analysis = state.sourceCodeAnalysis(); shutdownSubscription = state.onShutdown((JShell deadState) -> { if (deadState == state) { - hard("State engine terminated. See /history"); + hard("State engine terminated."); + hard("Restore definitions with: /reload restore"); live = false; } }); @@ -392,7 +415,6 @@ state.addToClasspath(cmdlineClasspath); } - String start; if (cmdlineStartup == null) { start = PREFS.get(STARTUP_KEY, ""); @@ -431,7 +453,7 @@ String incomplete = ""; while (live) { String prompt; - if (in.interactiveOutput() && displayPrompt) { + if (displayPrompt) { prompt = testPrompt ? incomplete.isEmpty() ? "\u0005" //ENQ @@ -480,6 +502,12 @@ } } + private void addToReplayHistory(String s) { + if (currentNameSpace == mainNamespace) { + replayableHistory.add(s); + } + } + private String processSourceCatchingReset(String src) { try { input.beforeUserCode(); @@ -516,7 +544,12 @@ fluff("Type /help for help."); } } else if (candidates.length == 1) { - candidates[0].run.accept(arg); + Command command = candidates[0]; + + // If comand was successful and is of a replayable kind, add it the replayable history + if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) { + addToReplayHistory((command.command + " " + arg).trim()); + } } else { hard("Command: %s is ambiguous: %s", cmd, Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); fluff("Type /help for help."); @@ -546,15 +579,15 @@ public final String command; public final String params; public final String description; - public final Consumer run; + public final Function run; public final CompletionProvider completions; public final CommandKind kind; - public Command(String command, String params, String description, Consumer run, CompletionProvider completions) { + public Command(String command, String params, String description, Function run, CompletionProvider completions) { this(command, params, description, run, completions, CommandKind.NORMAL); } - public Command(String command, String params, String description, Consumer run, CompletionProvider completions, CommandKind kind) { + public Command(String command, String params, String description, Function run, CompletionProvider completions, CommandKind kind) { this.command = command; this.params = params; this.description = description; @@ -571,6 +604,7 @@ enum CommandKind { NORMAL, + REPLAY, HIDDEN, HELP_ONLY; } @@ -602,6 +636,7 @@ private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("all ", "start ", "history "); + private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("restore", "quiet"); private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); private final Map commands = new LinkedHashMap<>(); private void registerCommand(Command cmd) { @@ -674,6 +709,16 @@ }; } + private static CompletionProvider reloadCompletion() { + return (code, cursor, anchor) -> { + List result = new ArrayList<>(); + int pastSpace = code.indexOf(' ') + 1; // zero if no space + result.addAll(RELOAD_OPTIONS_COMPLETION_PROVIDER.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor)); + anchor[0] += pastSpace; + return result; + }; + } + // Table of commands -- with command forms, argument kinds, help message, implementation, ... { @@ -688,7 +733,8 @@ editCompletion())); registerCommand(new Command("/drop", "", "delete a source entry referenced by name or id", arg -> cmdDrop(arg), - editCompletion())); + editCompletion(), + CommandKind.REPLAY)); registerCommand(new Command("/save", "[all|history|start] ", "save: - current source;\n" + " all - source including overwritten, failed, and start-up code;\n" + " history - editing history;\n" + @@ -716,6 +762,9 @@ registerCommand(new Command("/reset", null, "reset everything in the REPL", arg -> cmdReset(), EMPTY_COMPLETION_PROVIDER)); + registerCommand(new Command("/reload", "[restore] [quiet]", "reset and replay relevant history -- current or previous (restore)", + arg -> cmdReload(arg), + reloadCompletion())); registerCommand(new Command("/feedback", "", "feedback information: off, concise, normal, verbose, default, or ?", arg -> cmdFeedback(arg), new FixedCompletionProvider("off", "concise", "normal", "verbose", "default", "?"))); @@ -724,7 +773,8 @@ EMPTY_COMPLETION_PROVIDER)); registerCommand(new Command("/classpath", "", "add a path to the classpath", arg -> cmdClasspath(arg), - classPathCompletion())); + classPathCompletion(), + CommandKind.REPLAY)); registerCommand(new Command("/history", null, "history of what you have typed", arg -> cmdHistory(), EMPTY_COMPLETION_PROVIDER)); @@ -801,25 +851,29 @@ // --- Command implementations --- - void cmdSetEditor(String arg) { + boolean cmdSetEditor(String arg) { if (arg.isEmpty()) { hard("/seteditor requires a path argument"); + return false; } else { editor = arg; fluff("Editor set to: %s", arg); + return true; } } - void cmdClasspath(String arg) { + boolean cmdClasspath(String arg) { if (arg.isEmpty()) { hard("/classpath requires a path argument"); + return false; } else { state.addToClasspath(toPathResolvingUserHome(arg).toString()); fluff("Path %s added to classpath", arg); + return true; } } - void cmdDebug(String arg) { + boolean cmdDebug(String arg) { if (arg.isEmpty()) { debug = !debug; InternalDebugControl.setDebugFlags(state, debug ? InternalDebugControl.DBG_GEN : 0); @@ -860,20 +914,26 @@ default: hard("Unknown debugging option: %c", ch); fluff("Use: 0 r g f c d"); - break; + return false; } } InternalDebugControl.setDebugFlags(state, flags); } + return true; } - private void cmdExit() { + private boolean cmdExit() { regenerateOnDeath = false; live = false; + if (!replayableHistory.isEmpty()) { + PREFS.put(REPLAY_RESTORE_KEY, replayableHistory.stream().reduce( + (a, b) -> a + RECORD_SEPARATOR + b).get()); + } fluff("Goodbye\n"); + return true; } - private void cmdFeedback(String arg) { + private boolean cmdFeedback(String arg) { switch (arg) { case "": case "d": @@ -905,12 +965,13 @@ hard(" default"); hard("You may also use just the first letter, for example: /f c"); hard("In interactive mode 'default' is the same as 'normal', from a file it is the same as 'off'"); - return; + return false; } fluff("Feedback mode: %s", feedback.name().toLowerCase()); + return true; } - void cmdHelp() { + boolean cmdHelp() { int synopsisLen = 0; Map synopsis2Description = new LinkedHashMap<>(); for (Command cmd : new LinkedHashSet<>(commands.values())) { @@ -936,14 +997,16 @@ cmdout.println("Supported shortcuts include:"); cmdout.println(" -- show possible completions for the current text"); cmdout.println("Shift- -- for current method or constructor invocation, show a synopsis of the method/constructor"); + return true; } - private void cmdHistory() { + private boolean cmdHistory() { cmdout.println(); for (String s : input.currentSessionHistory()) { // No number prefix, confusing with snippet ids cmdout.printf("%s\n", s); } + return true; } /** @@ -1010,23 +1073,23 @@ } } - private void cmdDrop(String arg) { + private boolean cmdDrop(String arg) { if (arg.isEmpty()) { hard("In the /drop argument, please specify an import, variable, method, or class to drop."); hard("Specify by id or name. Use /list to see ids. Use /reset to reset all state."); - return; + return false; } Stream stream = argToSnippets(arg, false); if (stream == null) { hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); - return; + return false; } List snippets = stream .filter(sn -> state.status(sn).isActive && sn instanceof PersistentSnippet) .collect(toList()); if (snippets.isEmpty()) { hard("The argument did not specify an active import, variable, method, or class to drop."); - return; + return false; } if (snippets.size() > 1) { hard("The argument references more than one import, variable, method, or class."); @@ -1034,17 +1097,18 @@ for (Snippet sn : snippets) { cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); } - return; + return false; } PersistentSnippet psn = (PersistentSnippet) snippets.get(0); state.drop(psn).forEach(this::handleEvent); + return true; } - private void cmdEdit(String arg) { + private boolean cmdEdit(String arg) { Stream stream = argToSnippets(arg, true); if (stream == null) { hard("No definition or id named %s found. See /classes, /methods, /vars, or /list", arg); - return; + return false; } Set srcSet = new LinkedHashSet<>(); stream.forEachOrdered(sn -> { @@ -1078,6 +1142,7 @@ } else { ExternalEditor.edit(editor, errorHandler, src, saveHandler, input); } + return true; } //where // receives editor requests to save @@ -1135,10 +1200,9 @@ } } - private void cmdList(String arg) { + private boolean cmdList(String arg) { if (arg.equals("history")) { - cmdHistory(); - return; + return cmdHistory(); } Stream stream = argToSnippets(arg, true); if (stream == null) { @@ -1148,7 +1212,7 @@ } else { hard("No definition or id named %s found. There are no active definitions.", arg); } - return; + return false; } // prevent double newline on empty list @@ -1160,38 +1224,72 @@ } cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); }); + return true; } - private void cmdOpen(String filename) { + private boolean cmdOpen(String filename) { if (filename.isEmpty()) { hard("The /open command requires a filename argument."); + return false; } else { try { run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); } catch (FileNotFoundException e) { hard("File '%s' is not found: %s", filename, e.getMessage()); + return false; } catch (Exception e) { hard("Exception while reading file: %s", e); + return false; } } + return true; } - private void cmdPrompt() { + private boolean cmdPrompt() { displayPrompt = !displayPrompt; fluff("Prompt will %sdisplay. Use /prompt to toggle.", displayPrompt ? "" : "NOT "); concise("Prompt: %s", displayPrompt ? "on" : "off"); + return true; + } + + private boolean cmdReset() { + live = false; + fluff("Resetting state."); + return true; } - private void cmdReset() { - live = false; - fluff("Resetting state."); + private boolean cmdReload(String arg) { + Iterable history = replayableHistory; + boolean echo = true; + if (arg.length() > 0) { + if ("restore".startsWith(arg)) { + if (replayableHistoryPrevious == null) { + hard("No previous history to restore\n", arg); + return false; + } + history = replayableHistoryPrevious; + } else if ("quiet".startsWith(arg)) { + echo = false; + } else { + hard("Invalid argument to reload command: %s\nUse 'restore', 'quiet', or no argument\n", arg); + return false; + } + } + fluff("Restarting and restoring %s.", + history == replayableHistoryPrevious + ? "from previous state" + : "state"); + resetState(); + run(new ReloadIOContext(history, + echo? cmdout : null)); + return true; } - private void cmdSave(String arg_filename) { + private boolean cmdSave(String arg_filename) { Matcher mat = HISTORY_ALL_START_FILENAME.matcher(arg_filename); if (!mat.find()) { hard("Malformed argument to the /save command: %s", arg_filename); - return; + return false; } boolean useHistory = false; String saveAll = ""; @@ -1211,7 +1309,7 @@ String filename = mat.group("filename"); if (filename == null ||filename.isEmpty()) { hard("The /save command requires a filename argument."); - return; + return false; } try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), Charset.defaultCharset(), @@ -1234,12 +1332,15 @@ } } catch (FileNotFoundException e) { hard("File '%s' for save is not accessible: %s", filename, e.getMessage()); + return false; } catch (Exception e) { hard("Exception while saving: %s", e); + return false; } + return true; } - private void cmdSetStart(String filename) { + private boolean cmdSetStart(String filename) { if (filename.isEmpty()) { hard("The /setstart command requires a filename argument."); } else { @@ -1249,30 +1350,36 @@ PREFS.put(STARTUP_KEY, init); } catch (AccessDeniedException e) { hard("File '%s' for /setstart is not accessible.", filename); + return false; } catch (NoSuchFileException e) { hard("File '%s' for /setstart is not found.", filename); + return false; } catch (Exception e) { hard("Exception while reading start set file: %s", e); + return false; } } + return true; } - private void cmdVars() { + private boolean cmdVars() { for (VarSnippet vk : state.variables()) { String val = state.status(vk) == Status.VALID ? state.varValue(vk) : "(not-active)"; hard(" %s %s = %s", vk.typeName(), vk.name(), val); } + return true; } - private void cmdMethods() { + private boolean cmdMethods() { for (MethodSnippet mk : state.methods()) { hard(" %s %s", mk.name(), mk.signature()); } + return true; } - private void cmdClasses() { + private boolean cmdClasses() { for (TypeDeclSnippet ck : state.types()) { String kind; switch (ck.subKind()) { @@ -1295,15 +1402,17 @@ } hard(" %s %s", kind, ck.name()); } + return true; } - private void cmdImports() { + private boolean cmdImports() { state.imports().forEach(ik -> { hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); }); + return true; } - private void cmdUseHistoryEntry(int index) { + private boolean cmdUseHistoryEntry(int index) { List keys = state.snippets(); if (index < 0) index += keys.size(); @@ -1313,7 +1422,9 @@ rerunSnippet(keys.get(index)); } else { hard("Cannot find snippet %d", index + 1); + return false; } + return true; } private boolean rerunHistoryEntryById(String id) { @@ -1425,10 +1536,24 @@ private boolean processCompleteSource(String source) throws IllegalStateException { debug("Compiling: %s", source); boolean failed = false; + boolean isActive = false; List events = state.eval(source); for (SnippetEvent e : events) { + // Report the event, recording failure failed |= handleEvent(e); + + // If any main snippet is active, this should be replayable + // also ignore var value queries + isActive |= e.causeSnippet() == null && + e.status().isActive && + e.snippet().subKind() != VAR_VALUE_SUBKIND; } + // If this is an active snippet and it didn't cause the backend to die, + // add it to the replayable history + if (isActive && live) { + addToReplayHistory(source); + } + return failed; } @@ -1784,31 +1909,11 @@ } } -class ScannerIOContext extends IOContext { - - private final Scanner scannerIn; - private final PrintStream pStream; - - public ScannerIOContext(Scanner scannerIn, PrintStream pStream) { - this.scannerIn = scannerIn; - this.pStream = pStream; - } - - @Override - public String readLine(String prompt, String prefix) { - if (pStream != null && prompt != null) { - pStream.print(prompt); - } - if (scannerIn.hasNextLine()) { - return scannerIn.nextLine(); - } else { - return null; - } - } +abstract class NonInteractiveIOContext extends IOContext { @Override public boolean interactiveOutput() { - return true; + return false; } @Override @@ -1817,11 +1922,6 @@ } @Override - public void close() { - scannerIn.close(); - } - - @Override public boolean terminalEditorRunning() { return false; } @@ -1847,19 +1947,62 @@ } } +class ScannerIOContext extends NonInteractiveIOContext { + private final Scanner scannerIn; + + ScannerIOContext(Scanner scannerIn) { + this.scannerIn = scannerIn; + } + + @Override + public String readLine(String prompt, String prefix) { + if (scannerIn.hasNextLine()) { + return scannerIn.nextLine(); + } else { + return null; + } + } + + @Override + public void close() { + scannerIn.close(); + } +} + class FileScannerIOContext extends ScannerIOContext { - public FileScannerIOContext(String fn) throws FileNotFoundException { + FileScannerIOContext(String fn) throws FileNotFoundException { this(new FileReader(fn)); } - public FileScannerIOContext(Reader rdr) throws FileNotFoundException { - super(new Scanner(rdr), null); + FileScannerIOContext(Reader rdr) throws FileNotFoundException { + super(new Scanner(rdr)); + } +} + +class ReloadIOContext extends NonInteractiveIOContext { + private final Iterator it; + private final PrintStream echoStream; + + ReloadIOContext(Iterable history, PrintStream echoStream) { + this.it = history.iterator(); + this.echoStream = echoStream; } @Override - public boolean interactiveOutput() { - return false; + public String readLine(String prompt, String prefix) { + String s = it.hasNext() + ? it.next() + : null; + if (echoStream != null && s != null) { + String p = "-: "; + String p2 = "\n "; + echoStream.printf("%s%s\n", p, s.replace("\n", p2)); + } + return s; + } + + @Override + public void close() { } } - diff -r 416dfba03d33 -r aacf94dab449 langtools/test/jdk/jshell/ToolReloadTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/langtools/test/jdk/jshell/ToolReloadTest.java Mon Jan 11 08:41:00 2016 -0800 @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2015, 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. + */ + +/* + * @test + * @bug 8081845 + * @summary Tests for /reload in JShell tool + * @library /tools/lib + * @build KullaTesting TestingInputStream ToolBox Compiler + * @run testng ToolReloadTest + */ + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Function; + +import org.testng.annotations.Test; + + +@Test +public class ToolReloadTest extends ReplToolTesting { + + public void testReloadSnippets() { + test( + (a) -> assertVariable(a, "int", "x", "5", "5"), + (a) -> assertMethod(a, "int m(int z) { return z * z; }", + "(int)int", "m"), + (a) -> evaluateExpression(a, "int", "m(x)", "25"), + (a) -> assertCommand(a, "/reload", + "| Restarting and restoring state.\n" + + "-: int x = 5;\n" + + "-: int m(int z) { return z * z; }\n" + + "-: m(x)\n"), + (a) -> evaluateExpression(a, "int", "m(x)", "25"), + (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()), + (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()) + ); + } + + public void testReloadClasspath() { + Function prog = (s) -> String.format( + "package pkg; public class A { public String toString() { return \"%s\"; } }\n", s); + Compiler compiler = new Compiler(); + Path outDir = Paths.get("testClasspathDirectory"); + compiler.compile(outDir, prog.apply("A")); + Path classpath = compiler.getPath(outDir); + test( + (a) -> assertCommand(a, "/classpath " + classpath, + String.format("| Path %s added to classpath\n", classpath)), + (a) -> assertMethod(a, "String foo() { return (new pkg.A()).toString(); }", + "()String", "foo"), + (a) -> assertVariable(a, "String", "v", "foo()", "\"A\""), + (a) -> { + if (!a) compiler.compile(outDir, prog.apply("Aprime")); + assertCommand(a, "/reload", + "| Restarting and restoring state.\n" + + "-: /classpath " + classpath + "\n" + + "-: String foo() { return (new pkg.A()).toString(); }\n" + + "-: String v = foo();\n"); + }, + (a) -> assertCommand(a, "v", "| Variable v of type String has value \"Aprime\"\n"), + (a) -> evaluateExpression(a, "String", "foo()", "\"Aprime\""), + (a) -> evaluateExpression(a, "pkg.A", "new pkg.A();", "\"Aprime\"") + ); + } + + public void testReloadDrop() { + test(false, new String[]{"-nostartup"}, + a -> assertVariable(a, "int", "a"), + a -> dropVariable(a, "/dr 1", "int a = 0"), + a -> assertMethod(a, "int b() { return 0; }", "()I", "b"), + a -> dropMethod(a, "/drop b", "b ()I"), + a -> assertClass(a, "class A {}", "class", "A"), + a -> dropClass(a, "/dr A", "class A"), + a -> assertCommand(a, "/reload", + "| Restarting and restoring state.\n" + + "-: int a;\n" + + "-: /drop 1\n" + + "-: int b() { return 0; }\n" + + "-: /drop b\n" + + "-: class A {}\n" + + "-: /drop A\n"), + a -> assertCommandCheckOutput(a, "/vars", assertVariables()), + a -> assertCommandCheckOutput(a, "/methods", assertMethods()), + a -> assertCommandCheckOutput(a, "/classes", assertClasses()), + a -> assertCommandCheckOutput(a, "/imports", assertImports()) + ); + } + + public void testReloadRepeat() { + test(false, new String[]{"-nostartup"}, + (a) -> assertVariable(a, "int", "c", "7", "7"), + (a) -> assertCommand(a, "++c", null), + (a) -> assertCommand(a, "/!", null), + (a) -> assertCommand(a, "/2", null), + (a) -> assertCommand(a, "/-1", null), + (a) -> assertCommand(a, "/reload", + "| Restarting and restoring state.\n" + + "-: int c = 7;\n" + + "-: ++c\n" + + "-: ++c\n" + + "-: ++c\n" + + "-: ++c\n" + ), + (a) -> assertCommand(a, "c", "| Variable c of type int has value 11\n"), + (a) -> assertCommand(a, "$4", "| Variable $4 of type int has value 10\n") + ); + } + + public void testReloadIgnore() { + test(false, new String[]{"-nostartup"}, + (a) -> assertCommand(a, "(-)", null), + (a) -> assertCommand(a, "/list", null), + (a) -> assertCommand(a, "/history", null), + (a) -> assertCommand(a, "/help", null), + (a) -> assertCommand(a, "/vars", null), + (a) -> assertCommand(a, "/save abcd", null), + (a) -> assertCommand(a, "/reload", + "| Restarting and restoring state.\n") + ); + } + + public void testReloadResetRestore() { + test( + (a) -> assertVariable(a, "int", "x", "5", "5"), + (a) -> assertMethod(a, "int m(int z) { return z * z; }", + "(int)int", "m"), + (a) -> evaluateExpression(a, "int", "m(x)", "25"), + (a) -> assertCommand(a, "/reset", "| Resetting state.\n"), + (a) -> assertCommand(a, "/reload restore", + "| Restarting and restoring from previous state.\n" + + "-: int x = 5;\n" + + "-: int m(int z) { return z * z; }\n" + + "-: m(x)\n"), + (a) -> evaluateExpression(a, "int", "m(x)", "25"), + (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()), + (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()) + ); + } + + public void testReloadCrashRestore() { + test( + (a) -> assertVariable(a, "int", "x", "5", "5"), + (a) -> assertMethod(a, "int m(int z) { return z * z; }", + "(int)int", "m"), + (a) -> evaluateExpression(a, "int", "m(x)", "25"), + (a) -> assertCommand(a, "System.exit(1);", + "| State engine terminated.\n" + + "| Restore definitions with: /reload restore\n"), + (a) -> assertCommand(a, "/reload restore", + "| Restarting and restoring from previous state.\n" + + "-: int x = 5;\n" + + "-: int m(int z) { return z * z; }\n" + + "-: m(x)\n"), + (a) -> evaluateExpression(a, "int", "m(x)", "25"), + (a) -> assertCommandCheckOutput(a, "/vars", assertVariables()), + (a) -> assertCommandCheckOutput(a, "/methods", assertMethods()) + ); + } + + public void testReloadExitRestore() { + test(false, new String[]{"-nostartup"}, + (a) -> assertVariable(a, "int", "x", "5", "5"), + (a) -> assertMethod(a, "int m(int z) { return z * z; }", + "(int)int", "m"), + (a) -> evaluateExpression(a, "int", "m(x)", "25") + ); + test(false, new String[]{"-nostartup"}, + (a) -> assertCommand(a, "/reload restore", + "| Restarting and restoring from previous state.\n" + + "-: int x = 5;\n" + + "-: int m(int z) { return z * z; }\n" + + "-: m(x)\n"), + (a) -> evaluateExpression(a, "int", "m(x)", "25") + ); + } +}