// Copyright (C) 2003-2009 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the CPL Common Public License version 1.0.
package fitnesse.responders.run.slimResponder;
import fitnesse.components.CommandRunner;
import fitnesse.responders.run.ExecutionLog;
import fitnesse.responders.run.TestSummary;
import fitnesse.responders.run.TestSystem;
import fitnesse.responders.run.TestSystemListener;
import fitnesse.slim.SlimClient;
import fitnesse.slim.SlimError;
import fitnesse.slim.SlimServer;
import fitnesse.slim.SlimService;
import fitnesse.slimTables.*;
import fitnesse.testutil.MockCommandRunner;
import fitnesse.wiki.*;
import fitnesse.wikitext.parser.Parser;
import fitnesse.wikitext.parser.Symbol;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.ServerSocket;
import java.net.SocketException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class SlimTestSystem extends TestSystem implements SlimTestContext {
public static final String MESSAGE_ERROR = "!error:";
public static final String MESSAGE_FAIL = "!fail:";
public static final SlimTable START_OF_TEST = null;
public static final SlimTable END_OF_TEST = null;
private CommandRunner slimRunner;
private String slimCommand;
private SlimClient slimClient;
protected Map<String, Object> allInstructionResults = new HashMap<String, Object>();
protected List<SlimTable> allTables = new ArrayList<SlimTable>();
protected List<Object> allInstructions = new ArrayList<Object>();
protected List<SlimTable.Expectation> allExpectations = new ArrayList<SlimTable.Expectation>();
protected List<Object> instructions;
private boolean started;
protected ReadOnlyPageData testResults;
protected TableScanner tableScanner;
protected Map<String, Object> instructionResults;
protected List<SlimTable> testTables = new ArrayList<SlimTable>();
protected ExceptionList exceptions = new ExceptionList();
private Map<String, String> symbols = new HashMap<String, String>();
protected TestSummary testSummary;
private static AtomicInteger slimSocketOffset = new AtomicInteger(0);
private int slimSocket;
protected final Pattern exceptionMessagePattern = Pattern.compile("message:<<(.*)>>");
private Map<String, ScenarioTable> scenarios = new HashMap<String, ScenarioTable>();
protected List<SlimTable.Expectation> expectations = new ArrayList<SlimTable.Expectation>();
private SlimTableFactory slimTableFactory = new SlimTableFactory();
private Symbol preparsedScenarioLibrary;
public SlimTestSystem(WikiPage page, TestSystemListener listener) {
super(page, listener);
testSummary = new TestSummary(0, 0, 0, 0);
}
public String getSymbol(String symbolName) {
return symbols.get(symbolName);
}
public void setSymbol(String symbolName, String value) {
symbols.put(symbolName, value);
}
public void addScenario(String scenarioName, ScenarioTable scenarioTable) {
scenarios.put(scenarioName, scenarioTable);
}
public ScenarioTable getScenario(String scenarioName) {
return scenarios.get(scenarioName);
}
public void addExpectation(SlimTable.Expectation e) {
expectations.add(e);
}
public boolean isSuccessfullyStarted() {
return started;
}
public void kill() throws IOException {
if (slimRunner != null)
slimRunner.kill();
if (slimClient != null)
slimClient.close();
}
String getSlimFlags() {
String slimFlags = page.readOnlyData().getVariable("SLIM_FLAGS");
if (slimFlags == null)
slimFlags = "";
return slimFlags;
}
protected ExecutionLog createExecutionLog(String classPath, Descriptor descriptor) throws SocketException {
String slimFlags = getSlimFlags();
slimSocket = getNextSlimSocket();
String slimArguments = String.format("%s %d", slimFlags, slimSocket);
String slimCommandPrefix = buildCommand(descriptor, classPath);
slimCommand = String.format("%s %s", slimCommandPrefix, slimArguments);
if (fastTest) {
slimRunner = new MockCommandRunner();
createSlimService(slimArguments);
}
else if (manualStart) {
slimSocket = getSlimPortBase();
slimRunner = new MockCommandRunner();
} else {
slimRunner = new CommandRunner(slimCommand, "", createClasspathEnvironment(classPath));
}
return new ExecutionLog(page, slimRunner, descriptor.pageFactory);
}
public int findFreePort() {
int port;
try {
ServerSocket socket = new ServerSocket(0);
port = socket.getLocalPort();
socket.close();
} catch (Exception e) {
port = -1;
}
return port;
}
public int getNextSlimSocket() {
int base = getSlimPortBase();
if (base == 0) {
return findFreePort();
}
synchronized (slimSocketOffset) {
int offset = slimSocketOffset.get();
offset = (offset + 1) % 10;
slimSocketOffset.set(offset);
return offset + base;
}
}
private int getSlimPortBase() {
int base = 8085;
try {
String slimPort = page.readOnlyData().getVariable("SLIM_PORT");
if (slimPort != null) {
int slimPortInt = Integer.parseInt(slimPort);
base = slimPortInt;
}
} catch (Exception e) {
}
return base;
}
public void start() throws IOException {
slimRunner.asynchronousStart();
slimClient = new SlimClient(determineSlimHost(), slimSocket);
try {
waitForConnection();
started = true;
} catch (SlimError e) {
testSystemListener.exceptionOccurred(e);
}
}
String determineSlimHost() {
String slimHost = page.readOnlyData().getVariable("SLIM_HOST");
return slimHost == null ? "localhost" : slimHost;
}
public String getCommandLine() {
return slimCommand;
}
public void bye() throws IOException {
slimClient.sendBye();
if (!fastTest && !manualStart) {
slimRunner.join();
}
if (fastTest) {
slimRunner.kill();
}
}
//For testing only. Makes responder faster.
void createSlimService(String args) throws SocketException {
while (!tryCreateSlimService(args))
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private boolean tryCreateSlimService(String args) throws SocketException {
try {
SlimService.main(args.trim().split(" "));
return true;
} catch (SocketException e) {
throw e;
} catch (Exception e) {
return false;
}
}
void waitForConnection() {
while (!isConnected())
try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private boolean isConnected() {
try {
slimClient.connect();
return true;
} catch (Exception e) {
return false;
}
}
public String runTestsAndGenerateHtml(ReadOnlyPageData pageData) throws IOException {
initializeTest();
checkForAndReportVersionMismatch(pageData);
String html = processAllTablesOnPage(pageData);
testComplete(testSummary);
return html;
}
private void initializeTest() {
symbols.clear();
scenarios.clear();
testSummary.clear();
allExpectations.clear();
allInstructionResults.clear();
allInstructions.clear();
allTables.clear();
exceptions.resetForNewTest();
}
private void checkForAndReportVersionMismatch(ReadOnlyPageData pageData) {
double expectedVersionNumber = getExpectedSlimVersion(pageData);
double serverVersionNumber = slimClient.getServerVersion();
if (serverVersionNumber < expectedVersionNumber)
exceptions.addException("Slim Protocol Version Error",
String.format("Expected V%s but was V%s", expectedVersionNumber, serverVersionNumber));
}
private double getExpectedSlimVersion(ReadOnlyPageData pageData) {
double expectedVersionNumber = SlimClient.MINIMUM_REQUIRED_SLIM_VERSION;
String pageSpecificSlimVersion = pageData.getVariable("SLIM_VERSION");
if (pageSpecificSlimVersion != null) {
try {
double pageSpecificSlimVersionDouble = Double.parseDouble(pageSpecificSlimVersion);
expectedVersionNumber = pageSpecificSlimVersionDouble;
} catch (NumberFormatException e) {
}
}
return expectedVersionNumber;
}
protected abstract String createHtmlResults(SlimTable startAfterTable, SlimTable lastWrittenTable);
String processAllTablesOnPage(ReadOnlyPageData pageData) throws IOException {
tableScanner = scanTheTables(pageData);
allTables = createSlimTables(tableScanner);
testResults = pageData;
boolean runAllTablesAtOnce = false;
String htmlResults = "";
if (runAllTablesAtOnce || (allTables.size() == 0)) {
htmlResults = processTablesAndGetHtml(allTables, START_OF_TEST, END_OF_TEST);
} else {
List<SlimTable> oneTableList = new ArrayList<SlimTable>(1);
for (int index = 0; index < allTables.size(); index++) {
SlimTable theTable = allTables.get(index);
SlimTable startWithTable = (index == 0) ? START_OF_TEST : theTable;
SlimTable nextTable = (index + 1 < allTables.size()) ? allTables.get(index + 1) : END_OF_TEST;
oneTableList.add(theTable);
htmlResults += processTablesAndGetHtml(oneTableList, startWithTable, nextTable);
oneTableList.clear();
}
}
return htmlResults;
}
protected abstract TableScanner scanTheTables(ReadOnlyPageData pageData);
private String processTablesAndGetHtml(List<SlimTable> tables, SlimTable startWithTable, SlimTable nextTable) throws IOException {
expectations.clear();
testTables = tables;
instructions = createInstructions(tables);
if (!exceptions.stopTestCalled()) {
instructionResults = slimClient.invokeAndGetResponse(instructions);
}
String html = createHtmlResults(startWithTable, nextTable);
acceptOutputFirst(html);
// update all lists
allExpectations.addAll(expectations);
allInstructions.addAll(instructions);
allInstructionResults.putAll(instructionResults);
return html;
}
private List<Object> createInstructions(List<SlimTable> tables) {
List<Object> instructions = new ArrayList<Object>();
for (SlimTable table : tables) {
table.appendInstructions(instructions);
}
return instructions;
}
private List<SlimTable> createSlimTables(TableScanner tableScanner) {
List<SlimTable> allTables = new LinkedList<SlimTable>();
for (Table table : tableScanner)
createSlimTable(allTables, table);
return allTables;
}
private void createSlimTable(List<SlimTable> allTables, Table table) {
String tableId = "" + allTables.size();
SlimTable slimTable = slimTableFactory.makeSlimTable(table, tableId, this);
if (slimTable != null) {
allTables.add(slimTable);
}
}
static String translateExceptionMessage(String exceptionMessage) {
String tokens[] = exceptionMessage.split(" ");
if (tokens[0].equals("COULD_NOT_INVOKE_CONSTRUCTOR"))
return "Could not invoke constructor for " + tokens[1];
else if (tokens[0].equals("NO_METHOD_IN_CLASS"))
return String.format("Method %s not found in %s", tokens[1], tokens[2]);
else if (tokens[0].equals("NO_CONSTRUCTOR"))
return String.format("Could not find constructor for %s", tokens[1]);
else if (tokens[0].equals("NO_CONVERTER_FOR_ARGUMENT_NUMBER"))
return String.format("No converter for %s", tokens[1]);
else if (tokens[0].equals("NO_INSTANCE"))
return String.format("The instance %s does not exist", tokens[1]);
else if (tokens[0].equals("NO_CLASS"))
return String.format("Could not find class %s", tokens[1]);
else if (tokens[0].equals("MALFORMED_INSTRUCTION"))
return String.format("The instruction %s is malformed", exceptionMessage.substring(exceptionMessage.indexOf(" ") + 1));
return exceptionMessage;
}
public ReadOnlyPageData getTestResults() {
return testResults;
}
public static String exceptionToString(Throwable e) {
StringWriter stringWriter = new StringWriter();
PrintWriter pw = new PrintWriter(stringWriter);
e.printStackTrace(pw);
return SlimServer.EXCEPTION_TAG + stringWriter.toString();
}
public TestSummary getTestSummary() {
return testSummary;
}
protected void evaluateExpectations() {
for (SlimTable.Expectation e : expectations) {
try {
e.evaluateExpectation(instructionResults);
} catch (Throwable ex) {
exceptions.addException("ABORT", exceptionToString(ex));
exceptionOccurred(ex);
}
}
}
protected void evaluateTables() {
evaluateExpectations();
for (SlimTable table : testTables)
evaluateTable(table);
}
private void evaluateTable(SlimTable table) {
try {
table.evaluateReturnValues(instructionResults);
testSummary.add(table.getTestSummary());
} catch (Throwable e) {
exceptions.addException("ABORT", exceptionToString(e));
exceptionOccurred(e);
}
}
protected void replaceExceptionsWithLinks() {
Set<String> resultKeys = instructionResults.keySet();
for (String resultKey : resultKeys)
replaceExceptionWithExceptionLink(resultKey);
}
private void replaceExceptionWithExceptionLink(String resultKey) {
Object result = instructionResults.get(resultKey);
if (result instanceof String)
replaceIfUnignoredException(resultKey, (String) result);
}
private void replaceIfUnignoredException(String resultKey, String resultString) {
if (resultString.indexOf(SlimServer.EXCEPTION_TAG) != -1) {
if (shouldReportException(resultKey, resultString))
processException(resultKey, resultString);
}
}
private boolean shouldReportException(String resultKey, String resultString) {
for (SlimTable table : testTables) {
if (table.shouldIgnoreException(resultKey, resultString))
return false;
}
return true;
}
private void processException(String resultKey, String resultString) {
testSummary.exceptions++;
boolean isStopTestException = resultString.contains(SlimServer.EXCEPTION_STOP_TEST_TAG);
if (isStopTestException) {
exceptions.setStopTestCalled();
}
Matcher exceptionMessageMatcher = exceptionMessagePattern.matcher(resultString);
if (exceptionMessageMatcher.find()) {
String prefix = (isStopTestException) ? MESSAGE_FAIL : MESSAGE_ERROR;
String exceptionMessage = exceptionMessageMatcher.group(1);
instructionResults.put(resultKey, prefix + translateExceptionMessage(exceptionMessage));
} else {
exceptions.addException(resultKey, resultString);
instructionResults.put(resultKey, exceptionResult(resultKey));
}
}
private String exceptionResult(String resultKey) {
return String.format("Exception: <a href=#%s>%s</a>", resultKey, resultKey);
}
public Map<String, ScenarioTable> getScenarios() {
return scenarios;
}
public static void clearSlimPortOffset() {
slimSocketOffset.set(0);
}
public List<SlimTable> getTestTables() {
return allTables;
}
public List<Object> getInstructions() {
return allInstructions;
}
public Map<String, Object> getInstructionResults() {
return allInstructionResults;
}
public List<SlimTable.Expectation> getExpectations() {
return allExpectations;
}
public Symbol getPreparsedScenarioLibrary() {
if (preparsedScenarioLibrary == null) {
preparsedScenarioLibrary = Parser.make(page, getScenarioLibraryContent()).parse();
}
return preparsedScenarioLibrary;
}
private String getScenarioLibraryContent() {
String content = "!*> Precompiled Libraries\n\n";
content += includeUncleLibraries();
content += "*!\n";
return content;
}
private String includeUncleLibraries() {
String content = "";
List<WikiPage> uncles = PageCrawlerImpl.getAllUncles("ScenarioLibrary", page);
Collections.reverse(uncles);
for (WikiPage uncle : uncles)
content += include(page.getPageCrawler().getFullPath(uncle));
return content;
}
private String include(WikiPagePath path) {
return "!include -c ." + path + "\n";
}
}