diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverter.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverter.java index d6673f0c9..e790c4756 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverter.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverter.java @@ -103,7 +103,7 @@ private Stream ansiSequenceFromPrettyPrinter(ValueConverter valueConverter, P printer.printObject(value); printer.flushCurrentLine(); - Stream result = Stream.empty(); + Stream.Builder result = Stream.builder(); String indentation = StringUtils.createIndentation(currentLine.hasNewLine() ? currentLine.findWidthOfTheLastEffectiveLine(): @@ -118,24 +118,24 @@ private Stream ansiSequenceFromPrettyPrinter(ValueConverter valueConverter, P boolean isLastLine = idx == printer.getNumberOfLines() - 1; PrettyPrinterLine line = printer.getLine(idx); - if (isFirstLine) { - result = Stream.concat(result, line.getStyleAndValues().stream()); - } else { - result = Stream.concat(result, Stream.concat( - Stream.of(indentation), - line.getStyleAndValues().stream())); + if (!isFirstLine) { + result.add(indentation); } + line.getStyleAndValues().forEach(result::add); + if (!isLastLine) { - result = Stream.concat(result, Stream.of("\n")); + result.add("\n"); } } if (numberOfLinesToPrint > 1 && numberOfLinesToPrint != printer.getNumberOfLines()) { - result = Stream.concat(result, Stream.of(PrettyPrinter.DELIMITER_COLOR, indentation, "...")); + result.add(PrettyPrinter.DELIMITER_COLOR); + result.add(indentation); + result.add("..."); } - return result; + return result.build(); } private void associateDefaultTokens() { diff --git a/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/WebTauStep.java b/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/WebTauStep.java index 72124a667..9e7194616 100644 --- a/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/WebTauStep.java +++ b/webtau-core/src/main/java/org/testingisdocumenting/webtau/reporter/WebTauStep.java @@ -306,7 +306,20 @@ public void setValueConverter(ValueConverter valueConverter) { } public Stream collectOutputs() { - return Stream.concat(outputs.stream(), children.stream().flatMap(WebTauStep::collectOutputs)); + List result = new ArrayList<>(); + Deque stack = new ArrayDeque<>(); + stack.push(this); + + while (!stack.isEmpty()) { + WebTauStep step = stack.pop(); + result.addAll(step.outputs); + + for (int i = step.children.size() - 1; i >= 0; i--) { + stack.push(step.children.get(i)); + } + } + + return result.stream(); } @SuppressWarnings("unchecked") @@ -321,10 +334,22 @@ public boolean hasOutput(Class type) { } public Stream stepsWithClassifier(String classifier) { - Stream self = this.classifier.equals(classifier) ? Stream.of(this) : Stream.empty(); - Stream children = children().flatMap(childStep -> childStep.stepsWithClassifier(classifier)); + List result = new ArrayList<>(); + Deque stack = new ArrayDeque<>(); + stack.push(this); + + while (!stack.isEmpty()) { + WebTauStep step = stack.pop(); + if (step.classifier.equals(classifier)) { + result.add(step); + } + + for (int i = step.children.size() - 1; i >= 0; i--) { + stack.push(step.children.get(i)); + } + } - return Stream.concat(self, children); + return result.stream(); } public boolean hasFailedChildrenSteps() { diff --git a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverterTest.groovy b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverterTest.groovy index f731295b7..719db89cd 100644 --- a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverterTest.groovy +++ b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/TokenizedMessageToAnsiConverterTest.groovy @@ -41,4 +41,18 @@ class TokenizedMessageToAnsiConverterTest { def ansiSequence = new AutoResetAnsiString(valuesAndStyles.stream()).toString() actual(ansiSequence).should(equal("\u001B[1m\u001B[36mhello \u001B[0m\u001B[34mworld \u001B[1m\u001B[33mworld\u001B[0m")) } + + @Test + void "should handle large pretty printed value without stack overflow"() { + def converter = TokenizedMessageToAnsiConverter.DEFAULT + def largeList = (1..6000).collect { it } + def message = new TokenizedMessage().value(largeList) + + def valuesAndStyles = converter.convert(ValueConverter.EMPTY, message, 0) + + def ansiString = new AutoResetAnsiString(valuesAndStyles.stream()).toString() + def plainString = ansiString.replaceAll(/\u001B\[[;0-9]*m/, "") + def expected = "[\n " + largeList.join(",\n ") + "\n]" + actual(plainString).should(equal(expected)) + } } diff --git a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/WebTauStepTest.groovy b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/WebTauStepTest.groovy index 842a50aad..6e5e10534 100644 --- a/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/WebTauStepTest.groovy +++ b/webtau-core/src/test/groovy/org/testingisdocumenting/webtau/reporter/WebTauStepTest.groovy @@ -269,6 +269,31 @@ class WebTauStepTest { } } + @Test + void "recursion methods should not cause stack overflow on deep trees"() { + def root = createStep("root") + def current = root + + int depth = 2000 + depth.times { idx -> + def child = WebTauStep.createStepWithExplicitParent(current, 0, + tokenizedMessage().action("child #" + idx), + { -> tokenizedMessage().action("done child #" + idx) }, + { -> }) + child.setClassifier("test") + child.addOutput(new OutputA(id: "out" + idx)) + current = child + } + + def steps = root.stepsWithClassifier("test").collect(toList()) + assert steps.size() == depth + assert steps.inProgressMessage*.toString() == (0..