/* MonkeyTalk - a cross-platform functional testing tool Copyright (C) 2012 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.processor; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.gorillalogic.monkeytalk.Command; /** * Class for managing the context of a running MonkeyTalk script. Every recursive MonkeyTalk command * ({@code Script}, {@code Setup}, and {@code Teardown}) creates a new scope. Variables, both * built-in and named, are not recursive -- they only exist intra-scope. */ public class Scope implements Cloneable { private Scope parentScope; private String filename; private String componentType; private String monkeyId; private String action; private List<String> args; private Map<String, String> variables; private Command currentCommand; private int currentIndex; /** * Instantiate an empty scope object. */ public Scope() { this((String) null, null); } /** * Instantiate a new scope with just the current script filename. * * @param filename * the MonkeyTalk script filename */ public Scope(String filename) { this(filename, null); } /** * Instantiate a new scope with the current script filename and the parent scope (which implies * we are moving down the scope stack). * * @param filename * the MonkeyTalk script filename * @param parentScope * the parent scope (above us on the scope stack) */ public Scope(String filename, Scope parentScope) { this(filename, parentScope, null, null, null, null, null); } /** * Instantiate a new scope with the parent command and parent scope. * * @param command * the parent command * @param parentScope * the parent scope (above us on the scope stack) */ public Scope(Command command, Scope parentScope) { this(command.getMonkeyId(), parentScope, command.getComponentType(), command.getMonkeyId(), command.getAction(), command.getArgs(), null); } /** * Instantiate a new scope with the parent command, parent scope, and a map of named variables. * The only reason to create a scope with variables is when the script is being data-driven. * * @param command * the parent command * @param parentScope * the parent scope (above us on the scope stack) * @param variables * the named variables (used when data-driving) */ public Scope(Command command, Scope parentScope, Map<String, String> variables) { this(command.getMonkeyId(), parentScope, command.getComponentType(), command.getMonkeyId(), command.getAction(), command.getArgs(), variables); } /** * Instantiate a new scope with the current script filename, parent scope, built-in variables, * and a map of named variables. The only reason to create a scope with variables is when the * script is being data-driven. * * @param filename * the MonkeyTalk script filename * @param parentScope * the parent scope (above us on the scope stack) * @param componentType * the parent MonkeyTalk command componentType * @param monkeyId * the parent MonkeyTalk command monkeyId * @param action * the parent MonkeyTalk command action * @param args * the parent MonkeyTalk command args * @param variables * the named variables (used when data-driving) */ public Scope(String filename, Scope parentScope, String componentType, String monkeyId, String action, List<String> args, Map<String, String> variables) { this.parentScope = parentScope; this.filename = filename; this.componentType = componentType; this.monkeyId = monkeyId; this.action = action; this.args = args; this.variables = variables; this.currentCommand = null; this.currentIndex = 0; } /** * Get the parent scope. Returns {@code null} if this is the top of the scope stak (aka no * parent exists). * * @return the parent scope */ public Scope getParentScope() { return parentScope; } /** * Get the current script filename. * * @return the script filename */ public String getFilename() { return filename; } /** * Get the parent MonkeyTalk command componentType. * * @return the componentType */ public String getComponentType() { return componentType; } /** * Get the parent MonkeyTalk command monkeyId. * * @return the monkeyId */ public String getMonkeyId() { return monkeyId; } /** * Get the parent MonkeyTalk command action. * * @return the action */ public String getAction() { return action; } /** * Get the parent MonkeyTalk command args. * * @return the args */ public List<String> getArgs() { if (args == null) { args = new ArrayList<String>(); } return args; } /** * Get the current named variables in the scope. This never returns {@code null}. * * @return the named variables */ public Map<String, String> getVariables() { if (variables == null) { variables = new LinkedHashMap<String, String>(); } return variables; } /** * Helper to add a single named variable to the scope. * * @param key * the variable key (aka it's name) * @param val * the variable value */ public void addVariable(String key, String val) { getVariables().put(key, val); } /** * Helper to add some named variables to the scope. * * @param vars * the named variables */ public void addVariables(Map<String, String> vars) { if (vars != null) { getVariables().putAll(vars); } } /** * Get the current MonkeyTalk command. * * @return the command */ public Command getCurrentCommand() { return currentCommand; } /** * Get the current MonkeyTalk command index. This is the 1-based index of commands run in the * current scope, which usually corresponds to the line number in the current script file. * * @see Scope#setCurrentCommand(Command, int) * * @return the command index */ public int getCurrentIndex() { return currentIndex; } /** * Set the given MonkeyTalk command as the current command, and also increment the command * index. * * @param command * the MonkeyTalk {@link Command} */ public void setCurrentCommand(Command command) { setCurrentCommand(command, currentIndex + 1); } /** * Set the given MonkeyTalk command as the current command, and set the given index as the * current command index. * * @param command * the MonkeyTalk {@link Command} * @param index * the command index */ public void setCurrentCommand(Command command, int index) { currentCommand = command; currentIndex = index; } /** * Set the given index as the current command index. * * @param index * the command index */ public void setCurrentIndex(int index) { currentIndex = index; } /** * Substitute all variables found in the given command, and return a new fully-substituted * {@link Command} object. If the given command is a comment, do nothing. * * @param command * the MonkeyTalk {@link Command} object * @return a new full-substituted {@link Command} */ public Command substituteCommand(Command command) { if (command.isComment()) { return command.clone(); } else { return command.clone().substitute(componentType, monkeyId, action, args, merge(Globals.getGlobals(), variables)); } } /** Helper to merge two maps (with m2 overriding m1) */ private Map<String, String> merge(Map<String, String> m1, Map<String, String> m2) { Map<String, String> m = new LinkedHashMap<String, String>(m1); if (m2 != null) { for (Map.Entry<String, String> entry : m2.entrySet()) { m.put(entry.getKey(), entry.getValue()); } } return Collections.unmodifiableMap(m); } /** * Get the complete script hierarchy by recursing down the scope stack, and return it as a * delimited string (delimiter = <code>" > "</code>). * * @return the script stack as a delimited string */ public String getScopeHierarchy() { return getScopeHierarchy(this, " > ", false); } /** * Get the complete script hierarchy by recursing down the given scope's scope stack, and return * it as a delimited string. * * @param scope * the scope * @param delim * the script filename delimiter * @return the script stack as a delimited string */ public String getScopeHierarchy(Scope scope, String delim) { return getScopeHierarchy(scope, delim, false); } /** * Get the complete script hierarchy by recursing down the scope stack, and return it as a * delimited string. * * @param delim * the script filename delimiter * @param withIndex * if true return filename plus index, otherwise just the filename * @return the script stack, and index, as a delimited string */ public String getScopeHierarchy(String delim, boolean withIndex) { return getScopeHierarchy(this, delim, withIndex); } /** * Get the complete script hierarchy by recursing down the given scope's scope stack, and return * it as a delimited string. * * @param scope * the scope * @param delim * the script filename delimiter * @param withIndex * if true return filename plus index, otherwise just the filename * @return the script stack, and index, as a delimited string */ public String getScopeHierarchy(Scope scope, String delim, boolean withIndex) { if (scope == null) { return ""; } else { return (scope.getParentScope() != null ? getScopeHierarchy(scope.getParentScope(), delim, withIndex) + delim : "") + (scope.getFilename() == null ? "<commands>" : scope.getFilename()) + (withIndex ? ":" + scope.getCurrentIndex() : ""); } } public String getScopeTrace() { String file = (filename == null ? "<commands>" : filename); String cmd = (currentCommand == null ? "<unknown command>" : currentCommand.toString()); StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : getVariables().entrySet()) { sb.append(entry.getKey()).append('=').append(entry.getValue()).append(" "); } String vars = (sb.length() != 0 ? " [" + sb.substring(0, sb.length() - 1) + "]" : ""); return " at " + cmd + vars + " (" + file + " : cmd #" + currentIndex + ")" + (parentScope != null ? "\n" + parentScope.getScopeTrace() : ""); } @Override public Scope clone() { List<String> argsClone = new ArrayList<String>(); for (String arg : getArgs()) { argsClone.add(arg); } Map<String, String> variablesClone = new LinkedHashMap<String, String>(); for (Map.Entry<String, String> var : getVariables().entrySet()) { variablesClone.put(var.getKey(), var.getValue()); } Scope parentClone = (parentScope == null ? null : parentScope.clone()); return new Scope(filename, parentClone, componentType, monkeyId, action, argsClone, variablesClone); } @Override public String toString() { return "Scope: hierarchy=" + getScopeHierarchy() + " curr=" + currentIndex + ":" + currentCommand; } }