/* MonkeyTalk - a cross-platform functional testing tool Copyright (C) 2013 Gorilla Logic, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.gorillalogic.monkeytalk.shell; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import jline.console.ConsoleReader; import jline.console.completer.ArgumentCompleter; import jline.console.completer.NullCompleter; import jline.console.completer.StringsCompleter; import jline.console.history.FileHistory; import jline.console.history.History; import jline.console.history.History.Entry; import jline.console.history.MemoryHistory; import com.beust.jcommander.DynamicParameter; import com.beust.jcommander.JCommander; import com.beust.jcommander.Parameter; import com.beust.jcommander.converters.FileConverter; import com.gorillalogic.monkeytalk.BuildStamp; import com.gorillalogic.monkeytalk.Command; import com.gorillalogic.monkeytalk.agents.AgentManager; import com.gorillalogic.monkeytalk.agents.AndroidEmulatorAgent; import com.gorillalogic.monkeytalk.agents.IAgent; import com.gorillalogic.monkeytalk.api.meta.API; import com.gorillalogic.monkeytalk.api.meta.Component; import com.gorillalogic.monkeytalk.processor.Globals; import com.gorillalogic.monkeytalk.processor.PlaybackResult; import com.gorillalogic.monkeytalk.processor.Scope; import com.gorillalogic.monkeytalk.processor.ScriptProcessor; import com.gorillalogic.monkeytalk.shell.command.Help; import com.gorillalogic.monkeytalk.shell.command.Ping; import com.gorillalogic.monkeytalk.shell.command.Thinktime; import com.gorillalogic.monkeytalk.shell.command.Timeout; import com.gorillalogic.monkeytalk.shell.command.Tree; import com.gorillalogic.monkeytalk.shell.command.Vars; import com.gorillalogic.monkeytalk.utils.AndroidUtils; /** * MonkeyTalk shell. */ public class Shell { private static final String HISTORY_FILENAME = ".mt_history"; private static final int HISTORY_MAXSIZE = 100; private static final List<String> EXCLUDED_COMPONENTS = Arrays.asList("Suite", "Test", "Setup", "Teardown", "Verifiable", "Debug", "Doc"); public static void main(String[] args) { CommandlineParser parser = null; try { parser = new CommandlineParser(args); } catch (RuntimeException ex) { parser.printUsage(); return; } // color? Print.setColor(parser.color); // print stamp Print.info(BuildStamp.STAMP); // version? if (parser.version) { return; } // help? if (parser.help) { parser.printUsage(); return; } // working dir File dir = new File("").getAbsoluteFile(); Print.info(" workingDir=" + dir.getAbsolutePath()); // init agent IAgent agent = AgentManager.getAgent(parser.agent, parser.host, parser.port); if (parser.adb != null) { agent.setProperty(AndroidEmulatorAgent.ADB_PROP, parser.adb.getAbsolutePath()); } else if (parser.agent != null && parser.agent.equalsIgnoreCase("AndroidEmulator")) { agent.setProperty(AndroidEmulatorAgent.ADB_PROP, AndroidUtils.getAdbPath()); } agent.setProperty(AndroidEmulatorAgent.ADB_SERIAL_PROP, parser.adbSerial); agent.setProperty(AndroidEmulatorAgent.ADB_LOCAL_PORT_PROP, (parser.adbLocalPort > 0 ? Integer.toString(parser.adbLocalPort) : null)); agent.setProperty(AndroidEmulatorAgent.ADB_REMOTE_PORT_PROP, (parser.adbRemotePort > 0 ? Integer.toString(parser.adbRemotePort) : null)); agent.start(); // init processor ScriptProcessor processor = new ScriptProcessor(dir, agent); processor.setGlobalTimeout(parser.timeout); processor.setGlobalThinktime(parser.thinktime); processor.setGlobalScreenshotOnError(parser.screenshotOnError); // init scope Scope scope = new Scope("shell"); try { // init globals Globals.clear(); if (dir != null && dir.exists() && dir.isDirectory()) { Globals.setGlobals(new File(dir, "globals.properties")); } Globals.setGlobals(parser.getGlobals()); // init shell ConsoleReader reader = initShell(dir, parser.color, true); String line = null; while (true) { try { line = reader.readLine(); if (line != null) { line = line.trim(); if (line.toLowerCase().startsWith("/h")) { Print.println(Help.HELP); } else if (line.toLowerCase().startsWith("/hi")) { StringBuilder sb = new StringBuilder("HISTORY:\n"); for (Entry e : reader.getHistory()) { sb.append(' ').append(e.index() + 1).append(": "); sb.append(e.value()).append('\n'); } Print.println(sb); } else if (line.toLowerCase().startsWith("/p")) { Ping ping = new Ping(line, processor); ping.run(); } else if (line.toLowerCase().startsWith("/q")) { break; } else if (line.toLowerCase().startsWith("/th")) { Thinktime thinktime = new Thinktime(line, processor); thinktime.run(); } else if (line.toLowerCase().startsWith("/ti")) { Timeout timeout = new Timeout(line, processor); timeout.run(); } else if (line.toLowerCase().startsWith("/tr")) { Tree tree = new Tree(line, processor); tree.run(); } else if (line.toLowerCase().startsWith("/v")) { Vars vars = new Vars(line, processor, scope); vars.run(); } else { Command cmd = new Command(line); Command full = scope.substituteCommand(cmd); Print.print(full); PlaybackResult result = processor.runScript(full, scope); Print.print(" -> "); Print.println(result); } } } catch (Throwable t) { Print.err("\nERROR" + (t.getMessage() != null ? ": " + t.getMessage() : "")); History historyOld = reader.getHistory(); reader = initShell(dir, parser.color, false); if (historyOld != null) { reader.setHistory(historyOld); } } } // flush history to disk if (reader.getHistory() instanceof FileHistory) { Print.info("...save history"); FileHistory history = (FileHistory) reader.getHistory(); history.flush(); } } catch (IOException ex) { ex.printStackTrace(); } finally { agent.close(); } Print.info("BYE"); } /** * Init the JLine2 reader with an in-memory command history. */ private static ConsoleReader initShell(File dir, boolean color, boolean loadHistory) throws IOException { // reader ConsoleReader reader = new ConsoleReader(); reader.setPrompt(color ? "\u001B[35m" + "mt>" + "\u001B[0m " : "mt> "); // command history if (loadHistory) { reader.setHistory(getHistory(dir)); } // tab completion List<String> components = new ArrayList<String>(); for (Component c : API.getComponents()) { if (!EXCLUDED_COMPONENTS.contains(c.getName())) { components.add(c.getName()); } } reader.addCompleter(new ArgumentCompleter(new StringsCompleter(components), new NullCompleter())); return reader; } private static History getHistory(File dir) { try { String home = System.getenv("HOME"); if (home == null) { // no HOME environment variable, so just use the current working dir home = dir.getAbsolutePath(); } File homeDir = new File(home); if (homeDir.exists() && homeDir.isDirectory()) { File historyFile = new File(homeDir, HISTORY_FILENAME); Print.info(" history=" + historyFile.getAbsolutePath()); FileHistory history = new FileHistory(historyFile); history.setMaxSize(HISTORY_MAXSIZE); history.setAutoTrim(true); history.setIgnoreDuplicates(false); return history; } } catch (Exception ex) { // ignore } // something went wrong, so just use an in-memory history MemoryHistory mem = new MemoryHistory(); mem.setMaxSize(HISTORY_MAXSIZE); mem.setAutoTrim(true); mem.setIgnoreDuplicates(false); return mem; } /** Commandline arg parser based on JCommander. */ private static class CommandlineParser { private JCommander jcommander; @Parameter(names = "-agent", description = "target agent [iOS, Android, AndroidEmulator]", required = true) private String agent; @Parameter(names = "-host", description = "target host, defaults to localhost") private String host = "localhost"; @Parameter(names = "-port", description = "target port, defaults based on agent") private int port = -1; @Parameter(names = "-timeout", description = "global timeout (in ms)") private int timeout = -1; @Parameter(names = "-thinktime", description = "global thinktime (in ms)") private int thinktime = -1; @Parameter(names = "-adb", description = "the path to adb", converter = FileConverter.class) private File adb; @Parameter(names = "-adbSerial", description = "adb serial number") private String adbSerial = null; @Parameter(names = "-adbLocalPort", description = "adb local port") private int adbLocalPort = -1; @Parameter(names = "-adbRemotePort", description = "adb remote port") private int adbRemotePort = -1; @Parameter(names = "-help", description = "print help and exit", help = true) private boolean help = false; @Parameter(names = "-version", description = "print version and exit", help = true) private boolean version = false; @Parameter(names = "-screenshotOnError", description = "global screenshot on error") private boolean screenshotOnError = true; @Parameter(names = "-color", description = "turn on colored prompt and output") private boolean color = false; @DynamicParameter(names = "-D", description = "global variables") private Map<String, String> globals = new LinkedHashMap<String, String>(); public CommandlineParser(String[] args) { jcommander = new JCommander(this, args); } /** Print the auto-generated commandline usage. */ public void printUsage() { jcommander.usage(); } /** Helper to guarantee that global variable values are <i>not</i> quoted. */ private Map<String, String> getGlobals() { Map<String, String> m = new LinkedHashMap<String, String>(); for (Map.Entry<String, String> entry : globals.entrySet()) { String val = entry.getValue(); if (val.startsWith("\"") && val.endsWith("\"")) { val = val.substring(1, val.length() - 1); } m.put(entry.getKey(), val); } return m; } } }