From 6a3a0e79ea368ab009fe48764cbe2cb976829ab9 Mon Sep 17 00:00:00 2001 From: Mathias Leppich Date: Sun, 15 Feb 2015 23:34:01 +0100 Subject: [PATCH 1/3] use own copy of PrettyFormater to better allow enhancements --- .../editor/editors/GherkinFormatterUtil.java | 1 - .../editor/editors/PrettyFormatter.java | 521 ++++++++++++++++++ 2 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java index b237d8c7..6b8db931 100644 --- a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java @@ -1,7 +1,6 @@ package cucumber.eclipse.editor.editors; import gherkin.formatter.Formatter; -import gherkin.formatter.PrettyFormatter; import gherkin.lexer.LexingError; import gherkin.parser.ParseError; import gherkin.parser.Parser; diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java new file mode 100644 index 00000000..dba9e1a3 --- /dev/null +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java @@ -0,0 +1,521 @@ +package cucumber.eclipse.editor.editors; + +import static gherkin.util.FixJava.join; +import static gherkin.util.FixJava.map; +import gherkin.formatter.AnsiFormats; +import gherkin.formatter.Argument; +import gherkin.formatter.Format; +import gherkin.formatter.Formats; +import gherkin.formatter.Formatter; +import gherkin.formatter.MonochromeFormats; +import gherkin.formatter.NiceAppendable; +import gherkin.formatter.Reporter; +import gherkin.formatter.StepPrinter; +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; + +/** + * 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 tagNameMapper = new Mapper() { + @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 rows; + private Integer rowHeight = null; + private boolean rowsAbove = false; + + private List steps = new ArrayList(); + private List indentations = new ArrayList(); + private List matchesAndResults = new ArrayList(); + 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.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 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 rows) { + prepareTable(rows); + if (!executing) { + for (Row row : rows) { + row(row.createResults("skipped")); + nextRow(); + } + } + } + + private void prepareTable(List 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 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 cells, final int colIndex) { + return (colIndex < cells.size()) ? cells.get(colIndex) : ""; + } + + public void row(List 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 seenResults = new HashSet(); + 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 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 statements = new ArrayList(); + 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 comments, String indent) { + for (Comment comment : comments) { + out.println(indent + comment.getValue()); + } + } + + private void printTags(List 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 getMatchArguments() { + if (match != null) { + return match.getArguments(); + } + return Collections.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; + } +} \ No newline at end of file From 90196fe59ec5f5e1ca5146c8792588d13942f943 Mon Sep 17 00:00:00 2001 From: Mathias Leppich Date: Mon, 16 Feb 2015 09:18:02 +0100 Subject: [PATCH 2/3] remove stub-methods that are too new for our gherkin-2.11.6 lib --- .../eclipse/editor/editors/PrettyFormatter.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java index dba9e1a3..0d33a594 100644 --- a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java @@ -113,16 +113,6 @@ public void scenarioOutline(ScenarioOutline scenarioOutline) { statement = scenarioOutline; } - @Override - public void startOfScenarioLifeCycle(Scenario scenario) { - // NoOp - } - - @Override - public void endOfScenarioLifeCycle(Scenario scenario) { - // NoOp - } - private void replay() { addAnyOrphanMatch(); printStatement(); From 6ac70b4d4f95519b28ea6cdfaf0f0a578a6bf3d4 Mon Sep 17 00:00:00 2001 From: Mathias Leppich Date: Sun, 15 Feb 2015 23:34:01 +0100 Subject: [PATCH 3/3] right alignment of numeric values in tables --- .../editor/editors/GherkinFormatterUtil.java | 4 +-- .../editor/editors/PrettyFormatter.java | 31 +++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java index 6b8db931..a902873c 100644 --- a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/GherkinFormatterUtil.java @@ -21,8 +21,8 @@ public static String format(String contents) { // set up StringWriter output = new StringWriter(); PrintWriter out = new PrintWriter(output); - Formatter formatter = new PrettyFormatter(out, true, false); - + Formatter formatter = new PrettyFormatter(out, true, false, true); + // parse new Parser(formatter).parse(contents, "", 0); diff --git a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java index 0d33a594..7809c0ad 100644 --- a/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java +++ b/cucumber.eclipse.editor/src/main/java/cucumber/eclipse/editor/editors/PrettyFormatter.java @@ -29,6 +29,8 @@ import gherkin.formatter.model.TagStatement; import gherkin.util.Mapper; +import java.text.NumberFormat; +import java.text.ParsePosition; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -68,12 +70,23 @@ public String map(Tag tag) { private List matchesAndResults = new ArrayList(); private DescribedStatement statement; + private boolean rightAlignNumeric; + public PrettyFormatter(Appendable out, boolean monochrome, boolean executing) { + this(out,monochrome,executing,false); + } + + public PrettyFormatter(Appendable out, boolean monochrome, boolean executing, boolean rightAlignNumeric) { this.out = new NiceAppendable(out); this.executing = executing; setMonochrome(monochrome); + setRightAlignNumericValues(rightAlignNumeric); } + public void setRightAlignNumericValues(boolean rightAlignNumeric) { + this.rightAlignNumeric = rightAlignNumeric; + } + public void setMonochrome(boolean monochrome) { if (monochrome) { formats = new MonochromeFormats(); @@ -355,9 +368,15 @@ public void row(List cellResults) { break; } Format format = formats.get(status); - buffer.append(format.text(cellText)); int padding = maxLengths[colIndex] - cellLengths[rowIndex][colIndex]; - padSpace(buffer, padding); + boolean rightAligned = rightAlignNumeric && isNumeric(cellText); + if (rightAligned) { + padSpace(buffer, padding); + } + buffer.append(format.text(cellText)); + if (!rightAligned) { + padSpace(buffer, padding); + } if (colIndex < maxLengths.length - 1) { buffer.append(" | "); } else { @@ -378,6 +397,14 @@ public void row(List cellResults) { } } + public static boolean isNumeric(String str) + { + NumberFormat formatter = NumberFormat.getInstance(); + ParsePosition pos = new ParsePosition(0); + formatter.parse(str, pos); + return str.length() == pos.getIndex(); + } + private void printError(Result result) { Format failed = formats.get("failed"); out.println(indent(failed.text(result.getErrorMessage()), " "));