/* 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.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import com.gorillalogic.monkeytalk.BuildStamp;
import com.gorillalogic.monkeytalk.CommandWorld;
import com.gorillalogic.monkeytalk.agents.AgentManager;
import com.gorillalogic.monkeytalk.agents.AndroidEmulatorAgent;
import com.gorillalogic.monkeytalk.agents.IAgent;
import com.gorillalogic.monkeytalk.processor.report.Report;
import com.gorillalogic.monkeytalk.processor.report.detail.DetailReportHtml;
import com.gorillalogic.monkeytalk.processor.report.detail.ScriptReportHelper;
import com.gorillalogic.monkeytalk.sender.Response;
import com.gorillalogic.monkeytalk.server.ServerConfig;
/**
* Run the given script or suite against the given target agent.
*/
public class Runner {
protected String agentName;
protected IAgent agent;
protected String host;
protected int port;
protected File adb;
protected String adbSerial;
protected File reportdir;
protected PlaybackListener scriptListener;
protected SuiteListener suiteListener;
protected boolean verbose;
protected int thinktime = -1;
protected int timeout = -1;
private boolean screenshotOnError = true;
private boolean takeAfterScreenshot = false;
private boolean takeAfterMetrics = false;
private ScriptProcessor scriptProcessor;
private SuiteProcessor suiteProcessor;
private static PrintStream OUT;
static {
// System.out isn't UTF-8 by default, make it so!
try {
OUT = new PrintStream(System.out, true, "UTF-8");
} catch (UnsupportedEncodingException ex) {
OUT = null;
}
System.setOut(OUT);
}
private final PlaybackListener defaultScriptListener = new PlaybackListener() {
@Override
public void onScriptStart(Scope scope) {
}
@Override
public void onScriptComplete(Scope scope, PlaybackResult result) {
}
@Override
public void onStart(Scope scope) {
if (verbose && !"debug".equalsIgnoreCase(scope.getCurrentCommand().getComponentType())) {
onPrint(scope.getCurrentCommand().toString());
if ("script".equalsIgnoreCase(scope.getCurrentCommand().getComponentType())) {
onPrint("\n");
}
}
}
@Override
public void onComplete(Scope scope, Response response) {
if (verbose && !"debug".equalsIgnoreCase(scope.getCurrentCommand().getComponentType())) {
onPrint(" -> "
+ response.getStatus()
+ (response.getMessage() != null && response.getMessage().length() > 0 ? " : "
+ response.getMessage()
: "") + "\n");
}
}
@Override
public void onPrint(String message) {
System.out.print(message);
}
};
private final SuiteListener defaultSuiteListener = new SuiteListener() {
@Override
public void onRunStart(int total) {
}
@Override
public void onRunComplete(PlaybackResult result, Report report) {
}
@Override
public void onTestStart(String name, int num, int total) {
System.out.println(num + " : " + name);
}
@Override
public void onTestComplete(PlaybackResult result, Report report) {
System.out.println("test result: "
+ result.getStatus()
+ (result.getMessage() != null && result.getMessage().length() > 0 ? " : "
+ result.getMessage() : ""));
}
@Override
public void onSuiteStart(int total) {
System.out.println("running suite : " + total + (total == 1 ? " test" : " tests"));
}
@Override
public void onSuiteComplete(PlaybackResult result, Report report) {
}
};
public Runner() {
this(AgentManager.getDefaultAgent());
}
public Runner(String agent) {
this(agent, null, -1);
}
public Runner(String agent, int port) {
this(agent, null, port);
}
public Runner(String agentName, String host, int port) {
this(AgentManager.getAgent(agentName, host, port));
}
public Runner(IAgent agent) {
this.agentName = agent.getName();
this.host = agent.getHost();
this.port = agent.getPort();
verbose = false;
}
/**
* Run the given script or suite with the given map of global variables.
*
* @param in
* the script or suite
* @param globals
* the global variables
* @return the result
*/
public PlaybackResult run(File in, Map<String, String> globals) {
if (getAgent() == null) {
throw new RuntimeException("You must specify an agent, allowed values are: "
+ getAgentNames());
}
getAgent().start();
System.out.println(BuildStamp.STAMP);
PlaybackResult result = null;
if (in == null) {
throw new RuntimeException("Bad input script.");
} else if (!in.exists()) {
throw new RuntimeException("Bad input script. File not found: " + in.getAbsolutePath());
} else if (!in.isFile()) {
throw new RuntimeException("Bad input script. Not a file: " + in.getAbsolutePath());
} else {
try {
initGlobals(in.getParentFile(), globals);
} catch (IOException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
if (in.getName().toLowerCase().endsWith(CommandWorld.SCRIPT_EXT)
|| in.getName().toLowerCase().endsWith(CommandWorld.JS_EXT)) {
result = runScript(in);
} else if (in.getName().toLowerCase().endsWith(CommandWorld.SUITE_EXT)) {
result = runSuite(in);
} else {
throw new RuntimeException(
"Unrecognized input script file extension. Allowed values are: "
+ CommandWorld.SCRIPT_EXT + ", " + CommandWorld.SUITE_EXT + ", "
+ CommandWorld.JS_EXT);
}
}
System.out.println("result: " + result);
File dir = getReportDir();
if (dir == null) {
dir = in.getParentFile();
}
writeDetailReport(result, new Scope(in.getName()), dir, in.getParentFile());
return result;
}
/**
* Helper to init the globals for the current run via a simple three step process. First, we
* init the globals map to empty. Second, we load the globals.properties file if it exists.
* Last, we add any passed-in globals (typically from the commandline) which will override any
* globals from the file.
*
* @param dir
* the project dir
* @param globals
* the passed in globals
*/
private void initGlobals(File dir, Map<String, String> globals) throws IOException {
Globals.clear();
if (dir != null && dir.exists() && dir.isDirectory()) {
Globals.setGlobals(new File(dir, "globals.properties"));
}
if (globals != null) {
Globals.setGlobals(globals);
}
}
protected void writeDetailReport(PlaybackResult result, Scope scope, File reportDir,
File projectDir) {
String detailXml = getDetailReportXml(result, scope, projectDir, reportDir);
FileWriter fw = null;
File detailReportFile = new File(reportDir,
ScriptReportHelper.getXMLDetailReportFilename(scope.getFilename()));
try {
fw = new FileWriter(detailReportFile);
fw.write(detailXml);
} catch (IOException e) {
System.err.println("error writing detail report to file '" + detailReportFile.getPath()
+ "': " + e.getMessage());
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
}
}
}
String detailHtml = getDetailReportHtml(result, detailXml);
fw = null;
detailReportFile = new File(reportDir, ScriptReportHelper.getHTMLDetailReportFilename(scope
.getFilename()));
try {
fw = new FileWriter(detailReportFile);
fw.write(detailHtml);
} catch (IOException e) {
System.err.println("error writing detail report to file '" + detailReportFile.getPath()
+ "': " + e.getMessage());
} finally {
if (fw != null) {
try {
fw.close();
} catch (IOException e) {
}
}
}
}
protected String getDetailReportHtml(PlaybackResult result, String detailXml) {
String report = null;
try {
report = new DetailReportHtml().createDetailReportHtml(result, detailXml);
} catch (Exception ex) {
report = "<html><head><title>ERROR</title></head>" + "<body><h1>REPORTING ERROR</h1>"
+ "<p>" + ex.getMessage() + "</p></body></html>";
ex.printStackTrace();
}
return report;
}
protected String getDetailReportXml(PlaybackResult result, Scope scope, File projectDir,
File reportDir) {
String report = null;
try {
report = new ScriptReportHelper().createDetailReport(result, scope, projectDir,
reportDir, getRunnerVersion(),
getAgent().getName() + " v" + getAgent().getAgentVersion()).toXMLDocument();
} catch (Exception ex) {
ex.printStackTrace();
report = "<detail><msg><![CDATA[" + "REPORTING ERROR : " + ex.getMessage()
+ "]]></msg></detail>";
}
return report;
}
protected String getRunnerVersion() {
return this.getClass().getSimpleName()
+ " v"
+ BuildStamp.VERSION
+ (BuildStamp.BUILD_NUMBER != null && BuildStamp.BUILD_NUMBER.length() > 0 ? "_"
+ BuildStamp.BUILD_NUMBER : "") + " - " + BuildStamp.TIMESTAMP;
}
protected String getAgentNames() {
return AgentManager.getAgentNames().toString();
}
public void setReportdir(File reportdir) {
this.reportdir = reportdir;
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
public String getHost() {
return (host == null ? ServerConfig.DEFAULT_PLAYBACK_HOST : host.trim());
}
public int getPort() {
return (port < 1 ? ServerConfig.getPlaybackPort(agentName) : port);
}
public IAgent getAgent() {
if (agent == null) {
agent = AgentManager.getAgent(agentName, host, port);
}
return agent;
}
public String getAgentName() {
return getAgent().getName();
}
public void setAgentProperty(String key, String val) {
getAgent().setProperty(key, val);
}
public void setAdb(File adb) {
setAgentProperty(AndroidEmulatorAgent.ADB_PROP, adb == null ? null : adb.getAbsolutePath());
}
/**
* Set the global timeout.
*
* @param timeout
* the timeout
*/
public void setGlobalTimeout(int timeout) {
this.timeout = timeout;
}
/**
* Set the global thinktime.
*
* @param thinktime
* the thinktime
*/
public void setGlobalThinktime(int thinktime) {
this.thinktime = thinktime;
}
/**
* Set screenshot on error, true to turn on, false to turn off.
*
* @param screenshotOnError
* true to turn on screenshot on error
*/
public void setGlobalScreenshotOnError(boolean screenshotOnError) {
this.screenshotOnError = screenshotOnError;
}
/**
* Set take after screenshots on every command, true to turn on, false to turn off.
*
* @param takeAfterScreenshot
* true to turn on take after screenshot
*/
public void setTakeAfterScreenshot(boolean takeAfterScreenshot) {
this.takeAfterScreenshot = takeAfterScreenshot;
}
/**
* Set take after metrics on every command, true to turn on, false to turn off.
*
* @param takeAfterMetrics
* true to turn on take after metrics
*/
public void setTakeAfterMetrics(boolean takeAfterMetrics) {
this.takeAfterMetrics = takeAfterMetrics;
}
protected PlaybackListener getScriptListener() {
return (scriptListener != null ? scriptListener : defaultScriptListener);
}
public void setScriptListener(PlaybackListener scriptListener) {
this.scriptListener = scriptListener;
}
protected SuiteListener getSuiteListener() {
return (suiteListener != null ? suiteListener : defaultSuiteListener);
}
public void setSuiteListener(SuiteListener suiteListener) {
this.suiteListener = suiteListener;
}
/** Get a new script processor with the given project dir */
private ScriptProcessor getScriptProcessor(File dir) {
scriptProcessor = new ScriptProcessor(dir, getAgent());
scriptProcessor.setPlaybackListener(getScriptListener());
scriptProcessor.setGlobalTimeout(timeout);
scriptProcessor.setGlobalThinktime(thinktime);
scriptProcessor.setGlobalScreenshotOnError(screenshotOnError);
scriptProcessor.setTakeAfterMetrics(takeAfterMetrics);
scriptProcessor.setTakeAfterScreenshot(takeAfterScreenshot);
return scriptProcessor;
}
/** Run the given script with a new script processor */
protected PlaybackResult runScript(File script) {
File dir = script.getAbsoluteFile().getParentFile();
scriptProcessor = getScriptProcessor(dir);
return scriptProcessor.runScript(script.getName());
}
/** Run the given suite with a new suite processor (and new script processor) */
protected PlaybackResult runSuite(File suite) {
File dir = suite.getAbsoluteFile().getParentFile();
suiteProcessor = new SuiteProcessor(dir, getAgent(), getScriptProcessor(dir));
suiteProcessor.setSuiteListener(getSuiteListener());
suiteProcessor.setGlobalTimeout(timeout);
suiteProcessor.setGlobalThinktime(thinktime);
suiteProcessor.setGlobalScreenshotOnError(screenshotOnError);
if (getReportDir() != null) {
suiteProcessor.setReportDir(getReportDir());
}
return suiteProcessor.runSuite(suite.getName());
}
protected File getReportDir() {
if (reportdir != null) {
if (!reportdir.exists()) {
boolean success = reportdir.mkdirs();
if (!success) {
throw new RuntimeException("Failed to make reportdir: "
+ reportdir.getAbsolutePath());
}
}
if (!reportdir.isDirectory()) {
throw new RuntimeException("You must specify a valid reportdir. Not a directory.");
}
}
return reportdir;
}
/**
* Poll until the agent is up and running in the app under test for the given timeout (in
* seconds). Up and running means the agent returns OK in response to a ping message.
*
* @param timeout
* the timeout (in seconds)
* @return true if the agent is up and running, otherwise false
*/
public boolean waitUntilReady(long timeout) {
return getAgent().waitUntilReady(timeout * 1000);
}
/**
* Stop the running suite as soon as possible by calling {@link SuiteProcessor#abort()} (or
* script by calling {@link ScriptProcessor#abort()}).
*/
public void abort() {
if (suiteProcessor != null) {
suiteProcessor.abort();
} else if (scriptProcessor != null) {
scriptProcessor.abort();
}
}
}