package gherkin.formatter;
import gherkin.formatter.model.Background;
import gherkin.formatter.model.BasicStatement;
import gherkin.formatter.model.CellResult;
import gherkin.formatter.model.Comment;
import gherkin.formatter.model.DescribedStatement;
import gherkin.formatter.model.DocString;
import gherkin.formatter.model.Examples;
import gherkin.formatter.model.Feature;
import gherkin.formatter.model.Match;
import gherkin.formatter.model.Result;
import gherkin.formatter.model.Row;
import gherkin.formatter.model.Scenario;
import gherkin.formatter.model.ScenarioOutline;
import gherkin.formatter.model.Step;
import gherkin.formatter.model.Tag;
import gherkin.formatter.model.TagStatement;
import gherkin.util.Mapper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import static gherkin.util.FixJava.join;
import static gherkin.util.FixJava.map;
/**
* This class pretty prints feature files like they were in the source, only
* prettier. That is, with consistent indentation. This class is also a {@link Reporter},
* which means it can be used to print execution results - highlighting arguments,
* printing source information and exception information.
*/
public class PrettyFormatter implements Reporter, Formatter {
private final StepPrinter stepPrinter = new StepPrinter();
private final NiceAppendable out;
private final boolean executing;
private String uri;
private Mapper<Tag, String> tagNameMapper = new Mapper<Tag, String>() {
@Override
public String map(Tag tag) {
return tag.getName();
}
};
private Formats formats;
private Match match;
private int[][] cellLengths;
private int[] maxLengths;
private int rowIndex;
private List<? extends Row> rows;
private Integer rowHeight = null;
private boolean rowsAbove = false;
private List<Step> steps = new ArrayList<Step>();
private List<Integer> indentations = new ArrayList<Integer>();
private List<MatchResultPair> matchesAndResults = new ArrayList<MatchResultPair>();
private DescribedStatement statement;
public PrettyFormatter(Appendable out, boolean monochrome, boolean executing) {
this.out = new NiceAppendable(out);
this.executing = executing;
setMonochrome(monochrome);
}
public void setMonochrome(boolean monochrome) {
if (monochrome) {
formats = new MonochromeFormats();
} else {
formats = new AnsiFormats();
}
}
@Override
public void uri(String uri) {
this.uri = uri;
}
@Override
public void feature(Feature feature) {
printComments(feature.getComments(), "");
printTags(feature.getTags(), "");
out.println(feature.getKeyword() + ": " + feature.getName());
printDescription(feature.getDescription(), " ", false);
}
@Override
public void background(Background background) {
replay();
statement = background;
}
@Override
public void scenario(Scenario scenario) {
replay();
statement = scenario;
}
@Override
public void scenarioOutline(ScenarioOutline scenarioOutline) {
replay();
statement = scenarioOutline;
}
@Override
public void startOfScenarioLifeCycle(Scenario scenario) {
// NoOp
}
@Override
public void endOfScenarioLifeCycle(Scenario scenario) {
// NoOp
}
private void replay() {
addAnyOrphanMatch();
printStatement();
printSteps();
}
private void printSteps() {
while (!steps.isEmpty()) {
if (matchesAndResults.isEmpty()) {
printStep("skipped", Collections.<Argument>emptyList(), null);
} else {
MatchResultPair matchAndResult = matchesAndResults.remove(0);
printStep(matchAndResult.getResultStatus(), matchAndResult.getMatchArguments(),
matchAndResult.getMatchLocation());
if (matchAndResult.hasResultErrorMessage()) {
printError(matchAndResult.result);
}
}
}
}
private void printStatement() {
if (statement == null) {
return;
}
calculateLocationIndentations();
out.println();
printComments(statement.getComments(), " ");
if (statement instanceof TagStatement) {
printTags(((TagStatement) statement).getTags(), " ");
}
StringBuilder buffer = new StringBuilder(" ");
buffer.append(statement.getKeyword());
buffer.append(": ");
buffer.append(statement.getName());
String location = executing ? uri + ":" + statement.getLine() : null;
buffer.append(indentedLocation(location));
out.println(buffer);
printDescription(statement.getDescription(), " ", true);
statement = null;
}
private String indentedLocation(String location) {
StringBuilder sb = new StringBuilder();
int indentation = indentations.isEmpty() ? 0 : indentations.remove(0);
if (location == null) {
return "";
}
for (int i = 0; i < indentation; i++) {
sb.append(' ');
}
sb.append(' ');
sb.append(getFormat("comment").text("# " + location));
return sb.toString();
}
@Override
public void examples(Examples examples) {
replay();
out.println();
printComments(examples.getComments(), " ");
printTags(examples.getTags(), " ");
out.println(" " + examples.getKeyword() + ": " + examples.getName());
printDescription(examples.getDescription(), " ", true);
table(examples.getRows());
}
@Override
public void step(Step step) {
steps.add(step);
}
@Override
public void match(Match match) {
addAnyOrphanMatch();
this.match = match;
}
private void addAnyOrphanMatch() {
if (this.match != null) {
matchesAndResults.add(new MatchResultPair(this.match, null));
}
}
@Override
public void embedding(String mimeType, byte[] data) {
// Do nothing
}
@Override
public void write(String text) {
out.println(getFormat("output").text(text));
}
@Override
public void result(Result result) {
matchesAndResults.add(new MatchResultPair(match, result));
match = null;
}
@Override
public void before(Match match, Result result) {
printHookFailure(match, result, true);
}
@Override
public void after(Match match, Result result) {
printHookFailure(match, result, false);
}
private void printHookFailure(Match match, Result result, boolean isBefore) {
if (result.getStatus().equals(Result.FAILED)) {
Format format = getFormat(result.getStatus());
StringBuffer context = new StringBuffer("Failure in ");
if (isBefore) {
context.append("before");
} else {
context.append("after");
}
context.append(" hook:");
out.println(format.text(context.toString()) + format.text(match.getLocation()));
out.println(format.text("Message: ") + format.text(result.getErrorMessage()));
if (result.getError() != null) {
printError(result);
}
}
}
private void printStep(String status, List<Argument> arguments, String location) {
Step step = steps.remove(0);
Format textFormat = getFormat(status);
Format argFormat = getArgFormat(status);
printComments(step.getComments(), " ");
StringBuilder buffer = new StringBuilder(" ");
buffer.append(textFormat.text(step.getKeyword()));
stepPrinter.writeStep(new NiceAppendable(buffer), textFormat, argFormat, step.getName(), arguments);
buffer.append(indentedLocation(location));
out.println(buffer);
if (step.getRows() != null) {
table(step.getRows());
} else if (step.getDocString() != null) {
docString(step.getDocString());
}
}
private Format getFormat(String key) {
return formats.get(key);
}
private Format getArgFormat(String key) {
return formats.get(key + "_arg");
}
public void table(List<? extends Row> rows) {
prepareTable(rows);
if (!executing) {
for (Row row : rows) {
row(row.createResults("skipped"));
nextRow();
}
}
}
private void prepareTable(List<? extends Row> rows) {
this.rows = rows;
// find the largest row
int columnCount = 0;
for (Row row : rows) {
if (columnCount < row.getCells().size()) {
columnCount = row.getCells().size();
}
}
cellLengths = new int[rows.size()][columnCount];
maxLengths = new int[columnCount];
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
Row row = rows.get(rowIndex);
final List<String> cells = row.getCells();
for (int colIndex = 0; colIndex < columnCount; colIndex++) {
final String cell = getCellSafely(cells, colIndex);
final int length = escapeCell(cell).length();
cellLengths[rowIndex][colIndex] = length;
maxLengths[colIndex] = Math.max(maxLengths[colIndex], length);
}
}
rowIndex = 0;
}
private String getCellSafely(final List<String> cells, final int colIndex) {
return (colIndex < cells.size()) ? cells.get(colIndex) : "";
}
public void row(List<CellResult> cellResults) {
StringBuilder buffer = new StringBuilder();
Row row = rows.get(rowIndex);
if (rowsAbove) {
buffer.append(formats.up(rowHeight));
} else {
rowsAbove = true;
}
rowHeight = 1;
for (Comment comment : row.getComments()) {
buffer.append(" ");
buffer.append(comment.getValue());
buffer.append("\n");
rowHeight++;
}
switch (row.getDiffType()) {
case NONE:
buffer.append(" | ");
break;
case DELETE:
buffer.append(" ").append(formats.get("skipped").text("-")).append(" | ");
break;
case INSERT:
buffer.append(" ").append(formats.get("comment").text("+")).append(" | ");
break;
}
for (int colIndex = 0; colIndex < maxLengths.length; colIndex++) {
String cellText = escapeCell(getCellSafely(row.getCells(), colIndex));
String status = null;
switch (row.getDiffType()) {
case NONE:
status = cellResults.size() < colIndex ? cellResults.get(colIndex).getStatus() : "skipped";
break;
case DELETE:
status = "skipped";
break;
case INSERT:
status = "comment";
break;
}
Format format = formats.get(status);
buffer.append(format.text(cellText));
int padding = maxLengths[colIndex] - cellLengths[rowIndex][colIndex];
padSpace(buffer, padding);
if (colIndex < maxLengths.length - 1) {
buffer.append(" | ");
} else {
buffer.append(" |");
}
}
out.println(buffer);
rowHeight++;
Set<Result> seenResults = new HashSet<Result>();
for (CellResult cellResult : cellResults) {
for (Result result : cellResult.getResults()) {
if (result.getErrorMessage() != null && !seenResults.contains(result)) {
printError(result);
rowHeight += result.getErrorMessage().split("\n").length;
seenResults.add(result);
}
}
}
}
private void printError(Result result) {
Format failed = formats.get("failed");
out.println(indent(failed.text(result.getErrorMessage()), " "));
}
public void nextRow() {
rowIndex++;
rowsAbove = false;
}
@Override
public void syntaxError(String state, String event, List<String> legalEvents, String uri, Integer line) {
throw new UnsupportedOperationException();
}
@Override
public void done() {
// We're *not* closing the stream here.
// https://github.com/cucumber/gherkin/issues/151
// https://github.com/cucumber/cucumber-jvm/issues/96
}
@Override
public void close() {
out.close();
}
private String escapeCell(String cell) {
return cell.replaceAll("\\\\(?!\\|)", "\\\\\\\\").replaceAll("\\n", "\\\\n").replaceAll("\\|", "\\\\|");
}
public void docString(DocString docString) {
out.println(" \"\"\"");
out.println(escapeTripleQuotes(indent(docString.getValue(), " ")));
out.println(" \"\"\"");
}
public void eof() {
replay();
}
private void calculateLocationIndentations() {
int[] lineWidths = new int[steps.size() + 1];
int i = 0;
List<BasicStatement> statements = new ArrayList<BasicStatement>();
statements.add(statement);
statements.addAll(steps);
int maxLineWidth = 0;
for (BasicStatement statement : statements) {
int stepWidth = statement.getKeyword().length() + statement.getName().length();
lineWidths[i++] = stepWidth;
maxLineWidth = Math.max(maxLineWidth, stepWidth);
}
for (int lineWidth : lineWidths) {
indentations.add(maxLineWidth - lineWidth);
}
}
private void padSpace(StringBuilder buffer, int indent) {
for (int i = 0; i < indent; i++) {
buffer.append(" ");
}
}
private void printComments(List<Comment> comments, String indent) {
for (Comment comment : comments) {
out.println(indent + comment.getValue());
}
}
private void printTags(List<Tag> tags, String indent) {
if (tags.isEmpty()) return;
out.println(indent + join(map(tags, tagNameMapper), " "));
}
private void printDescription(String description, String indentation, boolean newline) {
if (!"".equals(description)) {
out.println(indent(description, indentation));
if (newline) out.println();
}
}
private static final Pattern START = Pattern.compile("^", Pattern.MULTILINE);
private static String indent(String s, String indentation) {
return START.matcher(s).replaceAll(indentation);
}
private static final Pattern TRIPLE_QUOTES = Pattern.compile("\"\"\"", Pattern.MULTILINE);
private static final String ESCAPED_TRIPLE_QUOTES = "\\\\\"\\\\\"\\\\\"";
private static String escapeTripleQuotes(String s) {
return TRIPLE_QUOTES.matcher(s).replaceAll(ESCAPED_TRIPLE_QUOTES);
}
}
class MatchResultPair {
public final Match match;
public final Result result;
public MatchResultPair(Match match, Result result) {
this.match = match;
this.result = result;
}
public List<Argument> getMatchArguments() {
if (match != null) {
return match.getArguments();
}
return Collections.<Argument>emptyList();
}
public String getMatchLocation() {
if (match != null) {
return match.getLocation();
}
return null;
}
public String getResultStatus() {
if (result != null) {
return result.getStatus();
}
return "skipped";
}
public boolean hasResultErrorMessage() {
return result != null && result.getErrorMessage() != null;
}
}