/* 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.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.gorillalogic.monkeytalk.Command;
import com.gorillalogic.monkeytalk.CommandWorld;
import com.gorillalogic.monkeytalk.agents.AgentManager;
import com.gorillalogic.monkeytalk.agents.IAgent;
import com.gorillalogic.monkeytalk.finder.Finder;
import com.gorillalogic.monkeytalk.processor.report.Report;
/**
* Run a suite and output an JUnit-compatible XML report.
*/
public class SuiteProcessor extends BaseProcessor {
private final static String BAD_SUITE_COMMAND = "only Test, Setup, Teardown, and Suite are allowed";
private ScriptProcessor processor;
private SuiteListener listener;
private File reportDir;
private File reportFile;
private boolean abortByRequest = false;
/**
* Default suite listener -- all callbacks do nothing.
*/
private static final SuiteListener DEFAULT_LISTENER = 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) {
}
@Override
public void onTestComplete(PlaybackResult result, Report report) {
}
@Override
public void onSuiteStart(int total) {
}
@Override
public void onSuiteComplete(PlaybackResult result, Report report) {
}
};
/**
* Default playback result.
*/
private static final PlaybackResult PLAYBACK_OK = new PlaybackResult();
/**
* Instantiate a suite processor with the given host, port, and project root folder.
*
* @param host
* the target host
* @param port
* the target port
* @param rootDir
* the project root directory
*/
public SuiteProcessor(String host, int port, File rootDir) {
this(rootDir, AgentManager.getDefaultAgent(host, port));
}
/**
* Instantiate a suite processor with the given project root folder and agent.
*
* @param rootDir
* the project root directory
* @param agent
* the agent
*/
public SuiteProcessor(File rootDir, IAgent agent) {
this(rootDir, agent, null);
}
/**
* Instantiate a suite processor with the given project root folder, agent, and ScriptProcessor.
*
* @param rootDir
* the project root directory
* @param agent
* the agent Wparam processor the {@code ScriptProcessor} to use. If null, a newly
* created ScriptProcessor will be used.
*/
public SuiteProcessor(File rootDir, IAgent agent, ScriptProcessor processor) {
super(rootDir, agent);
this.processor = (processor == null ? new ScriptProcessor(rootDir, agent) : processor);
}
/**
* Get the suite listener callbacks. If not set, return the default suite listener and never
* return {@code null}.
*
* @see ScriptProcessor#DEFAULT_LISTENER
*
* @return the playback listener
*/
public SuiteListener getSuiteListener() {
if (listener == null) {
listener = DEFAULT_LISTENER;
}
return listener;
}
/**
* Get the script processor used by this suite processor.
*
* @return the script processor
*
*/
public ScriptProcessor getScriptProcessor() {
return processor;
}
/**
* Set the suite listener.
*
* @param listener
* the suite listener
*/
public void setSuiteListener(SuiteListener listener) {
this.listener = listener;
}
/**
* Set the test report output directory (aka the directory where the XML report will be saved).
*
* @param reportDir
* the report directory
*/
public void setReportDir(File reportDir) {
this.reportDir = reportDir;
}
/**
* Get the test report file.
*
* @return the test report file
*/
public File getReportFile() {
return reportFile;
}
/**
* Run the given suite and output a JUnit-compatible XML report named
* <code>TEST-<filename>.xml</code>. The XML report is saved to the {@code reportDir} if it
* exists, otherwise it is saved to the same directory as the suite.
*
* @see Report
*
* @param filename
* the script (or suite) filename
* @return the playback result (OK, ERROR, or FAILURE)
*/
public PlaybackResult runSuite(String filename) {
long startTime = System.currentTimeMillis();
int total = new SuiteFlattener(this.getWorld()).flatten(filename);
getSuiteListener().onRunStart(total);
Report report = new Report(filename);
PlaybackResult result = null;
result = runSuite(filename, report, result);
if (result.getStatus() == PlaybackStatus.ERROR) {
getSuiteListener().onRunComplete(result, report);
return result;
}
try {
report.saveReport((reportDir != null ? reportDir : world.getRootDir()));
reportFile = report.getReportFile();
} catch (IOException ex) {
result = errorResult("failed to save XML report - " + ex.getMessage(), new Scope(
filename), startTime);
getSuiteListener().onRunComplete(result, report);
return result;
}
getSuiteListener().onRunComplete(PLAYBACK_OK, report);
return result;
}
protected PlaybackResult runSuite(String filename, Report report, PlaybackResult result) {
long startTime = System.currentTimeMillis();
getAgent().start();
reportFile = null;
if (filename == null) {
return errorResult("suite filename is null", startTime);
}
List<Command> commands = world.getSuite(filename);
Scope scope = new Scope(filename);
if (commands == null) {
if (filename.toLowerCase().endsWith(CommandWorld.SCRIPT_EXT)) {
return errorResult("running script '" + filename + "' as a suite is not allowed",
scope, startTime);
}
return errorResult("suite '" + filename + "' not found", scope, startTime);
} else if (commands.size() == 0) {
getSuiteListener().onSuiteStart(0);
result = errorResult("suite '" + filename + "' is empty", scope, startTime);
getSuiteListener().onSuiteComplete(result, report);
return result;
}
List<Command> setupArray = Finder.findCommandsByComponentType(commands, "setup");
List<Command> teardownArray = Finder.findCommandsByComponentType(commands, "teardown");
int total = new SuiteFlattener(this.getWorld()).flatten(filename);
getSuiteListener().onSuiteStart(total);
List<Step> steps = new ArrayList<Step>();
int stepNumber = 1;
scope.setCurrentIndex(0);
for (Command cmd : commands) {
Command full = scope.substituteCommand(cmd);
Step step = new Step(full, scope, scope.getCurrentIndex());
steps.add(step);
result = runSuiteCommand(full, scope, report, stepNumber++, total, setupArray,
teardownArray);
step.setResult(result);
if (shouldAbort(result)) {
break;
}
}
getSuiteListener().onSuiteComplete(result, report);
PlaybackResult suiteResult = new PlaybackResult(PlaybackStatus.OK);
if (result != null && shouldAbort(result)) {
suiteResult = copyResult(result, scope, startTime);
}
suiteResult.setStartTime(startTime);
suiteResult.setStopTime(System.currentTimeMillis());
suiteResult.setScope(scope);
suiteResult.setSteps(steps);
return suiteResult;
}
private boolean shouldAbort(PlaybackResult result) {
if (result == null || result.getStatus() == null) {
return false;
}
if (abortByRequest) {
// user abort, so halt the suite!
result.setStatus(PlaybackStatus.ERROR);
result.setMessage(ABORT_BY_REQUEST);
return true;
}
if (result.getStatus() == PlaybackStatus.ERROR) {
if (result.getMessage() != null) {
if (result.getMessage().contains(BAD_SUITE_COMMAND)) {
// if we error with a bad command, then halt the suite!
return true;
}
}
}
return false;
}
protected PlaybackResult runSuiteCommand(Command full, Scope scope, Report report,
int stepNumber, int total, List<Command> setupArray, List<Command> teardownArray) {
// capture start time
long startTime = System.currentTimeMillis();
if (scope != null) {
scope.setCurrentCommand(full);
}
PlaybackResult result = null;
if ("test".equalsIgnoreCase(full.getComponentType()) && full.isIgnored()) {
report.startTest(full);
getSuiteListener().onTestStart(report.getCurrentTest().getName(), stepNumber, total);
result = new PlaybackResult(PlaybackStatus.OK, "ignored", scope);
report.stopTest(full, result);
getSuiteListener().onTestComplete(result, report);
} else if ("test.run".equalsIgnoreCase(full.getCommandName())) {
report.startTest(full);
getSuiteListener().onTestStart(report.getCurrentTest().getName(), stepNumber, total);
result = runTest(full, stepNumber, setupArray, teardownArray, scope, null);
report.stopTest(full, result);
getSuiteListener().onTestComplete(result, report);
} else if ("test.runwith".equalsIgnoreCase(full.getCommandName())) {
if (full.getArgs().size() == 0) {
report.startTest(full);
getSuiteListener()
.onTestStart(report.getCurrentTest().getName(), stepNumber, total);
result = new PlaybackResult(PlaybackStatus.ERROR,
"datafile arg missing in command '" + full + "'", scope);
report.stopTest(full, result);
getSuiteListener().onTestComplete(result, report);
} else {
String datafile = full.getArgs().get(0);
List<Map<String, String>> data = world.getData(datafile);
if (data == null) {
report.startTest(full);
getSuiteListener().onTestStart(report.getCurrentTest().getName(), stepNumber,
total);
result = new PlaybackResult(PlaybackStatus.ERROR, "datafile '" + datafile
+ "' not found", scope);
report.stopTest(full, result);
getSuiteListener().onTestComplete(result, report);
} else if (data.size() == 0) {
report.startTest(full);
getSuiteListener().onTestStart(report.getCurrentTest().getName(), stepNumber,
total);
result = new PlaybackResult(PlaybackStatus.ERROR, "datafile '" + datafile
+ "' has no data", scope);
report.stopTest(full, result);
getSuiteListener().onTestComplete(result, report);
} else {
result = new PlaybackResult(PlaybackStatus.OK);
result.setScope(scope);
List<Step> steps = new ArrayList<Step>();
result.setSteps(steps);
int dataIndex = 1;
for (Map<String, String> datum : data) {
report.startTest(full, datum);
getSuiteListener().onTestStart(report.getCurrentTest().getName(),
dataIndex, total);
Command stepCommand = new Command(full.getCommand().replaceAll(datafile,
datafile + "\\[\\@" + dataIndex + "\\]"));
Step step = new Step(stepCommand, scope, dataIndex);
steps.add(step);
PlaybackResult r = runTest(full, dataIndex, setupArray, teardownArray,
scope, datum);
step.setResult(r);
report.stopTest(full, r);
getSuiteListener().onTestComplete(r, report);
dataIndex++;
if (shouldAbort(r)) {
break;
}
}
}
}
} else if ("suite".equalsIgnoreCase(full.getComponentType())
&& full.getModifiers().containsKey(Command.IGNORE_MODIFIER)) {
// entire sub-suite is ignored
} else if ("suite.run".equalsIgnoreCase(full.getCommandName())) {
Report recurseReport = new Report(full.getMonkeyId());
result = this.runSuite(full.getMonkeyId(), recurseReport, null);
report.getMainSuite().addSuite(recurseReport.getMainSuite());
} else if ("suite.runwith".equalsIgnoreCase(full.getCommandName())) {
result = errorResult("command '" + full.getCommandName()
+ "' is illegal -- only suite.run is allowed", scope, startTime);
return result;
} else if (full.isComment()) {
// ignore comments
} else if ("setup".equalsIgnoreCase(full.getComponentType())) {
// ignore setup
} else if ("teardown".equalsIgnoreCase(full.getComponentType())) {
// ignore teardown
} else {
result = errorResult("command '" + full.getCommandName() + "' is illegal -- "
+ BAD_SUITE_COMMAND, scope, startTime);
return result;
}
if (result != null) {
result.setStartTime(startTime);
result.setStopTime(System.currentTimeMillis());
}
return result;
}
protected PlaybackResult errorResult(String msg) {
return errorResult(msg, null, -1);
}
protected PlaybackResult errorResult(String msg, long startTime) {
return errorResult(msg, null, startTime);
}
protected PlaybackResult errorResult(String msg, Scope scope) {
return errorResult(msg, scope, -1);
}
protected PlaybackResult errorResult(String msg, Scope scope, long startTime) {
if (startTime == -1) {
startTime = System.currentTimeMillis();
}
PlaybackResult result = new PlaybackResult(PlaybackStatus.ERROR, msg, scope);
result.setScope(scope);
result.setStartTime(startTime);
result.setStopTime(System.currentTimeMillis());
return result;
}
/*
* Run the given test command, deferring to the {@link ScriptProcessor} to do the actual work.
* Also, run the {@code Setup} command before and the {@code Teardown} command after.
*
* @param cmd the test command
*
* @param setupArray the setup commands
*
* @param teardownArray the teardown commands
*
* @param scope the scope
*
* @param datum the named variables (if the script is being data-driven)
*
* @return
*/
protected PlaybackResult runTest(Command cmd, int stepNumber, List<Command> setupArray,
List<Command> teardownArray, Scope scope, Map<String, String> datum) {
PlaybackResult setupResult = null;
PlaybackResult teardownResult = null;
PlaybackResult testResult = null;
long runTestStartTime = System.currentTimeMillis();
List<Step> setupSteps = new ArrayList<Step>();
if (setupArray != null && !cmd.isIgnored("setup")) {
setupResult = runSetupOrTeardown(setupArray, scope, setupSteps);
}
// abort after setup?
if (setupResult != null && setupResult.getStatus() != PlaybackStatus.OK) {
PlaybackResult runTestResult = copyResult(setupResult, scope, runTestStartTime);
runTestResult.setSteps(setupSteps);
runTestResult.setStopTime(System.currentTimeMillis());
return runTestResult;
}
testResult = processor.runScript(cmd.getMonkeyId(), new Scope(cmd, scope, datum));
List<Step> teardownSteps = new ArrayList<Step>();
if (teardownArray != null && !cmd.isIgnored("teardown")) {
teardownResult = runSetupOrTeardown(teardownArray, scope, teardownSteps);
}
if (testResult != null) {
// add in steps from Setup and Teardown, if they occurred
if (setupResult != null || teardownResult != null) {
List<Step> testSteps = new ArrayList<Step>();
for (Step setupStep : setupSteps) {
testSteps.add(setupStep);
}
if (testResult.getSteps() != null) {
for (Step testStep : testResult.getSteps()) {
testSteps.add(testStep);
}
}
for (Step teardownStep : teardownSteps) {
testSteps.add(teardownStep);
}
if (teardownResult != null && !teardownResult.getStatus().equals(PlaybackStatus.OK)
&& testResult.getStatus().equals(PlaybackStatus.OK)) {
// error in testdown but no error in
testResult = copyResult(teardownResult, scope, runTestStartTime);
}
testResult.setSteps(testSteps);
}
} else {
// no test result - should not happen
testResult = errorResult("No valid result returned for command: " + cmd.getCommand());
testResult.setScope(scope);
}
testResult.setStartTime(runTestStartTime);
testResult.setStopTime(System.currentTimeMillis());
return testResult;
}
protected PlaybackResult runSetupOrTeardown(List<Command> commands, Scope scope,
List<Step> stepsForOverallTestResult) {
if (commands == null || commands.size() == 0) {
return null;
}
PlaybackResult result = new PlaybackResult(PlaybackStatus.OK);
for (Command cmd : commands) { // while we have another setup-or-teardown command, run it
long startTime = System.currentTimeMillis();
Scope fixtureScope = new Scope(cmd, scope);
Step fixtureStep = new Step(cmd, fixtureScope, scope.getCurrentIndex());
stepsForOverallTestResult.add(fixtureStep);
if ("run".equalsIgnoreCase(cmd.getAction())) {
result = processor.runScript(cmd.getMonkeyId(), fixtureScope);
fixtureStep.setResult(result);
} else if ("runwith".equalsIgnoreCase(cmd.getAction())) {
if (cmd.getArgs().size() == 0) {
result = errorResult("datafile arg missing in command '" + cmd + "'",
fixtureScope, startTime);
} else {
String datafile = cmd.getArgs().get(0);
List<Map<String, String>> data = world.getData(datafile);
if (data == null) {
result = errorResult("datafile '" + datafile + "' not found", fixtureScope,
startTime);
} else if (data.size() == 0) {
result = errorResult("datafile '" + datafile + "' has no data",
fixtureScope, startTime);
} else {
result = new PlaybackResult(PlaybackStatus.OK);
result.setStartTime(System.currentTimeMillis());
result.setScope(fixtureScope);
List<Step> runWithSteps = new ArrayList<Step>();
result.setSteps(runWithSteps);
int dataIndex = 1;
for (Map<String, String> dataFixture : data) {
Command stepCommand = new Command(cmd.getCommand().replaceAll(datafile,
datafile + "\\[\\@" + dataIndex + "\\]"));
Step runWithStep = new Step(stepCommand, fixtureScope, dataIndex++);
runWithSteps.add(runWithStep);
PlaybackResult r = processor.runScript(cmd.getMonkeyId(), new Scope(
cmd, scope, dataFixture));
runWithStep.setResult(r);
if (r.getStatus() != PlaybackStatus.OK) {
// error during data-driven setup, so abort
result.setStatus(r.getStatus());
result.setMessage(r.getMessage());
break;
}
}
result.setStopTime(System.currentTimeMillis());
}
}
fixtureStep.setResult(result);
} else {
result = errorResult("command '" + cmd.getCommandName()
+ "' is illegal -- only Teardown.Run and Teardown.RunWith are allowed",
fixtureScope, startTime);
fixtureStep.setResult(result);
}
}
return result;
}
@Override
public void setGlobalTimeout(int timeout) {
processor.setGlobalTimeout(timeout);
super.setGlobalTimeout(timeout);
}
@Override
public void setGlobalThinktime(int thinktime) {
processor.setGlobalThinktime(thinktime);
super.setGlobalThinktime(thinktime);
}
@Override
public void setTakeAfterMetrics(boolean takeAfterMetrics) {
processor.setTakeAfterMetrics(takeAfterMetrics);
super.setTakeAfterMetrics(takeAfterMetrics);
}
@Override
public void setTakeAfterScreenshot(boolean takeAfterScreenshot) {
processor.setTakeAfterScreenshot(takeAfterScreenshot);
super.setTakeAfterScreenshot(takeAfterScreenshot);
}
/**
* Stop the running suite as soon as possible by setting the {@code abortByRequest} flag to halt
* suite execution. Also call {@link ScriptProcessor#abort()} to halt the underlying test
* execution.
*/
public void abort() {
abortByRequest = true;
// and also abort the running processor
if (processor != null) {
processor.abort();
}
}
@Override
public String toString() {
return "SuiteProcessor:\n" + super.toString();
}
}