// 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.slimTables;
import fitnesse.responders.run.TestSummary;
import fitnesse.responders.run.slimResponder.SlimTestContext;
import fitnesse.responders.run.slimResponder.SlimTestSystem;
import fitnesse.wikitext.Utils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Character.isLetterOrDigit;
import static java.lang.Character.toUpperCase;
import static util.ListUtility.list;
public abstract class SlimTable {
private String tableName;
private int instructionNumber = 0;
private List<SlimTable> children = new ArrayList<SlimTable>();
private SlimTable parent = null;
private SlimTestContext testContext;
private TestSummary testSummary = new TestSummary();
protected Table table;
protected String id;
protected List<Object> instructions;
protected static final Pattern symbolAssignmentPattern = Pattern.compile("\\A\\s*\\$(\\w+)\\s*=\\s*\\Z");
public SlimTable(Table table, String id, SlimTestContext testContext) {
this.id = id;
this.table = table;
this.testContext = testContext;
tableName = getTableType() + "_" + id;
instructions = new ArrayList<Object>();
}
SlimTable(SlimTestContext testContext) {
this.testContext = testContext;
}
public SlimTable getParent() {
return parent;
}
public void addChildTable(SlimTable slimtable, int row) throws Exception {
slimtable.id = id + "." + children.size();
slimtable.tableName = makeInstructionTag(instructionNumber) + "/" + slimtable.tableName;
instructionNumber++;
slimtable.parent = this;
children.add(slimtable);
Table parentTable = getTable();
Table childTable = slimtable.getTable();
childTable.setName(slimtable.tableName);
parentTable.appendChildTable(row, childTable);
}
public SlimTable getChild(int i) {
return children.get(i);
}
protected void addExpectation(Expectation e) {
testContext.addExpectation(e);
}
public String replaceSymbols(String s) {
return new SymbolReplacer(s).replace();
}
public String replaceSymbolsWithFullExpansion(String s) {
return new FullExpansionSymbolReplacer(s).replace();
}
protected abstract String getTableType();
public void appendInstructions(List<Object> instructions) {
try {
this.instructions = instructions;
appendInstructions();
} catch (Throwable e) {
String tableName = table.getCellContents(0, 0);
table.setCell(0, 0, fail(String.format("%s: Bad table: <br/><pre>%s</pre>", tableName, Utils.getStackTrace(e))));
}
}
public abstract void appendInstructions();
protected List<Object> prepareInstruction() {
List<Object> instruction = new ArrayList<Object>();
instruction.add(makeInstructionTag(instructionNumber));
instructionNumber++;
return instruction;
}
protected String makeInstructionTag(int instructionNumber) {
return String.format("%s_%d", tableName, instructionNumber);
}
protected String getInstructionTag() {
return makeInstructionTag(instructionNumber);
}
public String getTableName() {
return tableName;
}
protected void addInstruction(List<Object> instruction) {
instructions.add(instruction);
}
public abstract void evaluateReturnValues(Map<String, Object> returnValues) throws Exception;
public String getSymbol(String variableName) {
return testContext.getSymbol(variableName);
}
public void setSymbol(String variableName, String value) {
testContext.setSymbol(variableName, value);
}
public Table getTable() {
return table;
}
protected void constructFixture() {
String fixtureName = getFixtureName();
constructFixture(fixtureName);
}
protected void constructFixture(String fixtureName) {
constructInstance(getTableName(), fixtureName, 0, 0);
}
protected String getFixtureName() {
String tableHeader = table.getCellContents(0, 0);
String fixtureName = getFixtureName(tableHeader);
String disgracedFixtureName = Disgracer.disgraceClassName(fixtureName);
return disgracedFixtureName;
}
protected String getFixtureName(String tableHeader) {
if (tableHeader.indexOf(":") == -1)
return tableHeader;
return tableHeader.split(":")[1];
}
protected void constructInstance(String instanceName, String className, int classNameColumn, int row) {
Expectation expectation = new ConstructionExpectation(getInstructionTag(), classNameColumn, row);
addExpectation(expectation);
List<Object> makeInstruction = prepareInstruction();
makeInstruction.add("make");
makeInstruction.add(instanceName);
makeInstruction.add(className);
addArgsToInstruction(makeInstruction, gatherConstructorArgumentsStartingAt(classNameColumn + 1, row));
addInstruction(makeInstruction);
}
protected Object[] gatherConstructorArgumentsStartingAt(int startingColumn, int row) {
int columnCount = table.getColumnCountInRow(row);
List<String> arguments = new ArrayList<String>();
for (int col = startingColumn; col < columnCount; col++) {
arguments.add(table.getUnescapedCellContents(col, row));
addExpectation(new VoidReturnExpectation(getInstructionTag(), col, row));
}
return arguments.toArray(new String[0]);
}
protected void addCall(List<Object> instruction, String instanceName, String functionName) {
String disgracedFunctionName = Disgracer.disgraceMethodName(functionName);
List<String> callHeader = list("call", instanceName, disgracedFunctionName);
instruction.addAll(callHeader);
}
protected String callFunction(String instanceName, String functionName, Object... args) {
List<Object> callInstruction = prepareInstruction();
addCall(callInstruction, instanceName, functionName);
addArgsToInstruction(callInstruction, args);
addInstruction(callInstruction);
return (String) callInstruction.get(0);
}
private void addArgsToInstruction(List<Object> instruction, Object... args) {
for (Object arg : args)
instruction.add(arg);
}
protected String callAndAssign(String symbolName, String instanceName, String functionName, String... args) {
List<Object> callAndAssignInstruction = prepareInstruction();
String disgracedFunctionName = Disgracer.disgraceMethodName(functionName);
List<String> callAndAssignHeader = list("callAndAssign", symbolName, instanceName, disgracedFunctionName);
callAndAssignInstruction.addAll(callAndAssignHeader);
addArgsToInstruction(callAndAssignInstruction, (Object[]) args);
addInstruction(callAndAssignInstruction);
return (String) callAndAssignInstruction.get(0);
}
protected void failMessage(int col, int row, String failureMessage) {
String contents = table.getCellContents(col, row);
String failingContents = failMessage(contents, failureMessage);
table.setCell(col, row, failingContents);
}
protected void fail(int col, int row, String value) {
String failingContents = fail(value);
table.setCell(col, row, failingContents);
}
protected void ignore(int col, int row, String value) {
String content = ignore(value);
table.setCell(col, row, content);
}
protected void pass(int col, int row) {
String contents = table.getCellContents(col, row);
String passingContents = pass(contents);
table.setCell(col, row, passingContents);
}
protected void pass(int col, int row, String passMessage) {
String passingContents = pass(passMessage);
table.setCell(col, row, passingContents);
}
protected void expected(int col, int tableRow, String actual) {
String contents = table.getCellContents(col, tableRow);
String failureMessage = expected(actual, contents);
table.setCell(col, tableRow, failureMessage);
}
public String expected(String actual, String expected) {
return failMessage(actual, String.format("expected [%s]", expected));
}
protected String fail(String value) {
testSummary.wrong = testSummary.getWrong() + 1;
return table.fail(value);
}
protected String failMessage(String value, String message) {
return String.format("[%s] %s", value, fail(message));
}
protected String pass(String value) {
testSummary.right = testSummary.getRight() + 1;
return passUncounted(value);
}
private String passUncounted(String value) {
return table.pass(value);
}
protected String error(String value) {
testSummary.exceptions = testSummary.getExceptions() + 1;
return table.error(value);
}
protected String ignore(String value) {
testSummary.ignores++;
return table.ignore(value);
}
protected ReturnedValueExpectation makeReturnedValueExpectation(
String instructionTag, int col, int row) {
return new ReturnedValueExpectation(instructionTag, col, row);
}
public static boolean approximatelyEqual(String standard, String candidate) {
try {
double candidateValue = Double.parseDouble(candidate);
double standardValue = Double.parseDouble(standard);
int point = standard.indexOf(".");
int precision = 0;
if (point != -1)
precision = standard.length() - point - 1;
double roundingFactor = 0.5;
while (precision-- > 0)
roundingFactor /= 10;
return Math.abs(candidateValue - standardValue) <= roundingFactor;
} catch (NumberFormatException e) {
return false;
}
}
public TestSummary getTestSummary() {
return testSummary;
}
protected String makeExeptionMessage(String value) {
if (value.startsWith(SlimTestSystem.MESSAGE_FAIL))
return fail(value.substring(SlimTestSystem.MESSAGE_FAIL.length()));
else
return error(value.substring(SlimTestSystem.MESSAGE_ERROR.length()));
}
protected boolean isExceptionMessage(String value) {
return value != null && (value.startsWith(SlimTestSystem.MESSAGE_FAIL) || value.startsWith(SlimTestSystem.MESSAGE_ERROR));
}
public boolean shouldIgnoreException(String resultKey, String resultString) {
return false;
}
protected String ifSymbolAssignment(int row, int col) {
String expected = table.getCellContents(col, row);
Matcher matcher = symbolAssignmentPattern.matcher(expected);
return matcher.find() ? matcher.group(1) : null;
}
protected void callAndAssign(String symbolName, String functionName) {
List<Object> callAndAssignInstruction = prepareInstruction();
callAndAssignInstruction.add("callAndAssign");
callAndAssignInstruction.add(symbolName);
callAndAssignInstruction.add(getTableName());
callAndAssignInstruction.add(Disgracer.disgraceMethodName(functionName));
addInstruction(callAndAssignInstruction);
}
public SlimTestContext getTestContext() {
return testContext;
}
protected List<Object> tableAsList() {
List<Object> tableArgument = list();
int rows = table.getRowCount();
for (int row = 1; row < rows; row++)
tableArgument.add(tableRowAsList(row));
return tableArgument;
}
private List<Object> tableRowAsList(int row) {
List<Object> rowList = list();
int cols = table.getColumnCountInRow(row);
for (int col = 0; col < cols; col++)
rowList.add(table.getCellContents(col, row));
return rowList;
}
public List<SlimTable> getChildren() {
return children;
}
static class Disgracer {
public boolean capitalizeNextWord;
public StringBuffer disgracedName;
private String name;
public Disgracer(String name) {
this.name = name;
}
public static String disgraceClassName(String name) {
return new Disgracer(name).disgraceClassNameIfNecessary();
}
public static String disgraceMethodName(String name) {
return new Disgracer(name).disgraceMethodNameIfNecessary();
}
private String disgraceMethodNameIfNecessary() {
if (isGraceful()) {
return disgraceMethodName();
} else {
return name;
}
}
private String disgraceMethodName() {
capitalizeNextWord = false;
return disgraceName();
}
private String disgraceClassNameIfNecessary() {
if (nameHasDotsBeforeEnd() || nameHasDollars())
return name;
else if (isGraceful()) {
return disgraceClassName();
} else {
return name;
}
}
private boolean nameHasDollars() {
return name.indexOf("$") != -1;
}
private String disgraceClassName() {
capitalizeNextWord = true;
return disgraceName();
}
private boolean nameHasDotsBeforeEnd() {
int dotIndex = name.indexOf(".");
return dotIndex != -1 && dotIndex != name.length() - 1;
}
private String disgraceName() {
disgracedName = new StringBuffer();
for (char c : name.toCharArray())
appendCharInProperCase(c);
return disgracedName.toString();
}
private void appendCharInProperCase(char c) {
if (isGraceful(c)) {
capitalizeNextWord = true;
} else {
appendProperlyCapitalized(c);
}
}
private void appendProperlyCapitalized(char c) {
disgracedName.append(capitalizeNextWord ? toUpperCase(c) : c);
capitalizeNextWord = false;
}
private boolean isGraceful() {
boolean isGraceful = false;
for (char c : name.toCharArray()) {
if (isGraceful(c))
isGraceful = true;
}
return isGraceful;
}
private boolean isGraceful(char c) {
return !(isLetterOrDigit(c) || c == '_');
}
}
public abstract class Expectation {
private int col;
private int row;
private String instructionTag;
private String actual;
private String expected;
private String evaluationMessage;
public Expectation(String instructionTag, int col, int row) {
this.row = row;
this.instructionTag = instructionTag;
this.col = col;
}
public void evaluateExpectation(Map<String, Object> returnValues) {
Object returnValue = returnValues.get(instructionTag);
String evaluationMessage;
if (returnValue == null) {
String originalContent = table.getCellContents(col, row);
evaluationMessage = originalContent + " " + ignore("Test not run");
returnValues.put(instructionTag, "Test not run");
} else {
String value;
value = returnValue.toString();
String originalContent = table.getCellContents(col, row);
evaluationMessage = evaluationMessage(value, originalContent);
}
if (evaluationMessage != null)
table.setCell(col, row, evaluationMessage);
}
String evaluationMessage(String actual, String expected) {
this.actual = actual;
this.expected = expected;
String evaluationMessage;
if (isExceptionMessage(actual))
evaluationMessage = expected + " " + makeExeptionMessage(actual);
else
evaluationMessage = createEvaluationMessage(actual, expected);
this.evaluationMessage = HtmlTable.colorize(evaluationMessage);
return evaluationMessage;
}
protected abstract String createEvaluationMessage(String actual, String expected);
public int getCol() {
return col;
}
public int getRow() {
return row;
}
public String getInstructionTag() {
return instructionTag;
}
public String getActual() {
return actual;
}
public String getExpected() {
return expected;
}
public String getEvaluationMessage() {
return evaluationMessage == null ? "" : evaluationMessage;
}
}
class SymbolReplacer {
protected String replacedString;
private Matcher symbolMatcher;
private final Pattern symbolPattern = Pattern.compile("\\$([a-zA-Z]\\w*)");
private int startingPosition;
SymbolReplacer(String s) {
this.replacedString = s;
symbolMatcher = symbolPattern.matcher(s);
}
String replace() {
replaceAllSymbols();
return replacedString;
}
private void replaceAllSymbols() {
startingPosition = 0;
while (symbolFound())
replaceSymbol();
}
private void replaceSymbol() {
String symbolName = symbolMatcher.group(1);
String value = formatSymbol(symbolName);
String prefix = replacedString.substring(0, symbolMatcher.start());
String suffix = replacedString.substring(symbolMatcher.end());
replacedString = prefix + value + suffix;
int replacementEnd = symbolMatcher.start() + value.length();
startingPosition = Math.min(replacementEnd, replacedString.length());
}
private String formatSymbol(String symbolName) {
String value = getSymbol(symbolName);
if (value == null) {
for (int i = symbolName.length() - 1; i > 0; i--) {
String str = symbolName.substring(0, i);
if ((value = getSymbol(str)) != null)
return formatSymbolValue(str, value) + symbolName.substring(i, symbolName.length());
}
return "$" + symbolName;
} else
return formatSymbolValue(symbolName, value);
}
private boolean symbolFound() {
symbolMatcher = symbolPattern.matcher(replacedString);
return symbolMatcher.find(startingPosition);
}
protected String formatSymbolValue(String name, String value) {
return value;
}
}
class FullExpansionSymbolReplacer extends SymbolReplacer {
FullExpansionSymbolReplacer(String s) {
super(s);
}
protected String formatSymbolValue(String name, String value) {
return String.format("$%s->[%s]", name, value);
}
}
public static class SyntaxError extends Error {
private static final long serialVersionUID = 1L;
public SyntaxError(String message) {
super(message);
}
}
class VoidReturnExpectation extends Expectation {
public VoidReturnExpectation(String instructionTag, int col, int row) {
super(instructionTag, col, row);
}
protected String createEvaluationMessage(String actual, String expected) {
return replaceSymbolsWithFullExpansion(expected);
}
}
class SilentReturnExpectation extends Expectation {
public SilentReturnExpectation(String instructionTag, int col, int row) {
super(instructionTag, col, row);
}
protected String createEvaluationMessage(String actual, String expected) {
return null;
}
}
class ConstructionExpectation extends Expectation {
public ConstructionExpectation(String instructionTag, int col, int row) {
super(instructionTag, col, row);
}
protected String createEvaluationMessage(String actual, String expected) {
if ("OK".equalsIgnoreCase(actual))
return passUncounted(replaceSymbolsWithFullExpansion(expected));
else
return "!style_error(Unknown construction message:) " + actual;
}
}
class SymbolAssignmentExpectation extends Expectation {
private String symbolName;
SymbolAssignmentExpectation(String symbolName, String instructionTag, int col, int row) {
super(instructionTag, col, row);
this.symbolName = symbolName;
}
protected String createEvaluationMessage(String actual, String expected) {
setSymbol(symbolName, actual);
return String.format("$%s<-[%s]", symbolName, actual);
}
}
public static interface ExpectationPassFailReporter {
String pass(String message);
String fail(String message);
}
class ReturnedValueExpectation extends Expectation implements ExpectationPassFailReporter {
public ReturnedValueExpectation(String instructionTag, int col, int row) {
super(instructionTag, col, row);
}
protected String createEvaluationMessage(String actual, String expected) {
String evaluationMessage;
String replacedExpected = Utils.unescapeHTML(replaceSymbols(expected));
if (actual == null)
evaluationMessage = fail("null"); //todo can't be right message.
else if (actual.equals(replacedExpected))
evaluationMessage = pass(announceBlank(replaceSymbolsWithFullExpansion(expected)));
else if (replacedExpected.length() == 0)
evaluationMessage = ignore(actual);
else {
String expressionMessage = new Comparator(this, replacedExpected, actual, expected).evaluate();
if (expressionMessage != null)
evaluationMessage = expressionMessage;
else if (actual.indexOf("Exception:") != -1) {
evaluationMessage = error(actual);
} else
evaluationMessage = failMessage(actual,
String.format("%s [%s]", expectationAdjective(), replaceSymbolsWithFullExpansion(expected))
);
}
return evaluationMessage;
}
protected String expectationAdjective() {
return "expected";
}
private String announceBlank(String originalValue) {
return originalValue.length() == 0 ? "BLANK" : originalValue;
}
@Override
public String pass(String message) {
return SlimTable.this.pass(message);
}
@Override
public String fail(String message) {
return SlimTable.this.fail(message);
}
protected String failMessage(String value, String message) {
return String.format("[%s] %s", value, fail(message));
}
}
class RejectedValueExpectation extends ReturnedValueExpectation {
public RejectedValueExpectation(String instructionTag, int col, int row) {
super(instructionTag, col, row);
}
@Override
protected String expectationAdjective() {
return "is not";
}
public String pass(String message) {
return super.fail(message);
}
public String fail(String message) {
return super.pass(message);
}
}
class Comparator {
private String expression;
private String actual;
private String expected;
private Pattern simpleComparison = Pattern.compile(
"\\A\\s*_?\\s*(!?(?:(?:[<>]=?)|(?:[~]?=)))\\s*(-?\\d*\\.?\\d+)\\s*\\Z"
);
private Pattern range = Pattern.compile(
"\\A\\s*(-?\\d*\\.?\\d+)\\s*<(=?)\\s*_\\s*<(=?)\\s*(-?\\d*\\.?\\d+)\\s*\\Z"
);
private Pattern regexPattern = Pattern.compile("\\s*=~/(.*)/");
private double v;
private double arg1;
private double arg2;
public String operation;
private String arg1Text;
private ExpectationPassFailReporter passFailReporter;
boolean match = false;
public Comparator(String actual, String expected) {
this.passFailReporter = new ExpectationPassFailReporter() {
public String pass(String message) {
return message;
}
public String fail(String message) {
return message;
}
};
this.expression = Utils.unescapeHTML(replaceSymbols(expected));
this.actual = actual;
this.expected = expected;
}
public Comparator(ExpectationPassFailReporter passFailReporter, String expression, String actual, String expected) {
this.passFailReporter = passFailReporter;
this.expression = expression;
this.actual = actual;
this.expected = expected;
}
private String pass(String message) {
match = true;
return passFailReporter.pass(message);
}
private String fail(String message) {
match = false;
return passFailReporter.fail(message);
}
public boolean matches() {
return match;
}
public String evaluate() {
String message = evaluateRegularExpressionIfPresent();
if (message != null)
return message;
operation = matchSimpleComparison();
if (operation != null)
return doSimpleComparison();
Matcher matcher = range.matcher(expression);
if (matcher.matches() && canUnpackRange(matcher)) {
return doRange(matcher);
} else
return null;
}
private String evaluateRegularExpressionIfPresent() {
Matcher regexMatcher = regexPattern.matcher(expression);
String message = null;
if (regexMatcher.matches()) {
String pattern = regexMatcher.group(1);
message = evaluateRegularExpression(pattern);
}
return message;
}
private String evaluateRegularExpression(String pattern) {
String message;
Matcher patternMatcher = Pattern.compile(pattern).matcher(actual);
if (patternMatcher.find()) {
message = pass(String.format("/%s/ found in: %s", pattern, actual));
} else {
message = fail(String.format("/%s/ not found in: %s", pattern, actual));
}
return message;
}
private String doRange(Matcher matcher) {
boolean closedLeft = matcher.group(2).equals("=");
boolean closedRight = matcher.group(3).equals("=");
boolean pass = (arg1 < v && v < arg2) || (closedLeft && arg1 == v) || (closedRight && arg2 == v);
return rangeMessage(pass);
}
private String rangeMessage(boolean pass) {
String[] fragments = expected.replaceAll(" ", "").split("_");
String message = String.format("%s%s%s", fragments[0], actual, fragments[1]);
message = replaceSymbolsWithFullExpansion(message);
return pass ? pass(message) : fail(message);
}
private boolean canUnpackRange(Matcher matcher) {
try {
arg1 = Double.parseDouble(matcher.group(1));
arg2 = Double.parseDouble(matcher.group(4));
v = Double.parseDouble(actual);
} catch (NumberFormatException e) {
return false;
}
return true;
}
private String doSimpleComparison() {
if (operation.equals("<") || operation.equals("!>="))
return simpleComparisonMessage(v < arg1);
else if (operation.equals(">") || operation.equals("!<="))
return simpleComparisonMessage(v > arg1);
else if (operation.equals(">=") || operation.equals("!<"))
return simpleComparisonMessage(v >= arg1);
else if (operation.equals("<=") || operation.equals("!>"))
return simpleComparisonMessage(v <= arg1);
else if (operation.equals("!="))
return simpleComparisonMessage(v != arg1);
else if (operation.equals("="))
return simpleComparisonMessage(v == arg1);
else if (operation.equals("~="))
return simpleComparisonMessage(approximatelyEqual(arg1Text, actual));
else if (operation.equals("!~="))
return simpleComparisonMessage(!approximatelyEqual(arg1Text, actual));
else
return null;
}
private String simpleComparisonMessage(boolean pass) {
String message = String.format("%s%s", actual, expected.replaceAll(" ", ""));
message = replaceSymbolsWithFullExpansion(message);
return pass ? pass(message) : fail(message);
}
private String matchSimpleComparison() {
Matcher matcher = simpleComparison.matcher(expression);
if (matcher.matches()) {
try {
v = Double.parseDouble(actual);
arg1Text = matcher.group(2);
arg1 = Double.parseDouble(arg1Text);
return matcher.group(1);
} catch (NumberFormatException e1) {
return null;
}
}
return null;
}
}
}