diff --git a/README.md b/README.md index 264c473..48a6d27 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,25 @@ default: DrevOps\BehatFormatProgressFail\FormatExtension: ~ ``` +or + +>behat.yml +```yaml +default: + extensions: + DrevOps\BehatFormatProgressFail\FormatExtension: + show_output: in-summary # Supported values: yes | no | on-fail +``` + +#### `show_output` + +Show output from within test steps. "Output" is `print`, `echo`, `var_dump`, etc. + +- `yes` - always show the output +- `no` - do not show the output +- `on-fail` - only show the output if there are test fails +- `in-summary` - only show in the summary if there are test fails + ## Maintenance ### Lint code diff --git a/composer.json b/composer.json index 2b58b55..1425dcb 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ }, "require": { "php": ">=8.1", - "behat/behat": "^3.3" + "behat/behat": "^3.18" }, "require-dev": { "behat/mink": "^1.8", diff --git a/src/DrevOps/BehatFormatProgressFail/FormatExtension.php b/src/DrevOps/BehatFormatProgressFail/FormatExtension.php index 0fc9d75..37c0449 100644 --- a/src/DrevOps/BehatFormatProgressFail/FormatExtension.php +++ b/src/DrevOps/BehatFormatProgressFail/FormatExtension.php @@ -5,6 +5,7 @@ namespace DrevOps\BehatFormatProgressFail; use Behat\Behat\Output\Node\EventListener\AST\StepListener; +use Behat\Config\Formatter\ShowOutputOption; use DrevOps\BehatFormatProgressFail\Printer\PrinterProgressFail; use Behat\Testwork\Output\NodeEventListeningFormatter; use Behat\Testwork\Output\Node\EventListener\ChainEventListener; @@ -72,6 +73,7 @@ public function initialize(ExtensionManager $extensionManager): void { public function configure(ArrayNodeDefinition $builder): void { $builder->children()->scalarNode('name')->defaultValue(self::MOD_ID); $builder->children()->scalarNode('base_path')->defaultValue(self::BASE_PATH); + $builder->children()->scalarNode(ShowOutputOption::OPTION_NAME)->defaultValue(ShowOutputOption::InSummary->value); } /** @@ -80,73 +82,50 @@ public function configure(ArrayNodeDefinition $builder): void { public function load(ContainerBuilder $container, array $config): void { $name = is_string($config['name']) ? $config['name'] : self::MOD_ID; - $definition = new Definition( - StepListener::class, [ - new Reference('output.printer.' . $name), - ] - ); + $definition = new Definition(StepListener::class, [ + new Reference('output.printer.' . $name), + ]); $container->setDefinition(self::ROOT_LISTENER_ID, $definition); - $definition = new Definition( - PrinterProgressFail::class, [ - new Reference(self::RESULT_TO_STRING_CONVERTER_ID), - $config['base_path'], - ] - ); - $container->setDefinition( - 'output.printer.' . $name, $definition - ); - - $definition = new Definition( - NodeEventListeningFormatter::class, [ - $config['name'], - 'Prints one character per step and fail view pretty.', - ['timer' => TRUE], - $this->createOutputPrinterDefinition(), - new Definition( - ChainEventListener::class, [ - [ - new Reference(self::ROOT_LISTENER_ID), - new Definition( - StatisticsListener::class, [ - new Reference('output.progress.statistics'), - new Reference( - 'output.node.printer.progress.statistics' - ), - ] - ), - new Definition( - ScenarioStatsListener::class, [ - new Reference('output.progress.statistics'), - ] - ), - new Definition( - StepStatsListener::class, [ - new Reference('output.progress.statistics'), - new Reference( - ExceptionExtension::PRESENTER_ID - ), - ] - ), - new Definition( - HookStatsListener::class, [ - new Reference('output.progress.statistics'), - new Reference( - ExceptionExtension::PRESENTER_ID - ), - ] - ), - ], - ] - ), - ] - ); - $definition->addTag( - OutputExtension::FORMATTER_TAG, ['priority' => 100] - ); - $container->setDefinition( - OutputExtension::FORMATTER_TAG . '.' . $name, $definition - ); + $definition = new Definition(PrinterProgressFail::class, [ + new Reference(self::RESULT_TO_STRING_CONVERTER_ID), + $config['base_path'], + ]); + $container->setDefinition('output.printer.' . $name, $definition); + + $definition = new Definition(NodeEventListeningFormatter::class, [ + $config['name'], + 'Prints one character per step and fail view pretty.', + [ + 'timer' => TRUE, + ShowOutputOption::OPTION_NAME => $config[ShowOutputOption::OPTION_NAME], + ], + $this->createOutputPrinterDefinition(), + new Definition(ChainEventListener::class, [ + [ + new Reference(self::ROOT_LISTENER_ID), + new Definition(StatisticsListener::class, [ + new Reference('output.progress.statistics'), + new Reference('output.node.printer.progress.statistics'), + ]), + new Definition(ScenarioStatsListener::class, [ + new Reference('output.progress.statistics'), + ]), + new Definition(StepStatsListener::class, [ + new Reference('output.progress.statistics'), + new Reference(ExceptionExtension::PRESENTER_ID), + ]), + new Definition(HookStatsListener::class, [ + new Reference('output.progress.statistics'), + new Reference(ExceptionExtension::PRESENTER_ID), + ]), + ], + ]), + ]); + + $definition->addTag(OutputExtension::FORMATTER_TAG, ['priority' => 100]); + + $container->setDefinition(OutputExtension::FORMATTER_TAG . '.' . $name, $definition); } /** @@ -156,13 +135,9 @@ public function load(ContainerBuilder $container, array $config): void { * The output printer definition. */ protected function createOutputPrinterDefinition(): Definition { - return new Definition( - StreamOutputPrinter::class, [ - new Definition( - ConsoleOutputFactory::class - ), - ] - ); + return new Definition(StreamOutputPrinter::class, [ + new Definition(ConsoleOutputFactory::class), + ]); } } diff --git a/src/DrevOps/BehatFormatProgressFail/Printer/PrinterProgressFail.php b/src/DrevOps/BehatFormatProgressFail/Printer/PrinterProgressFail.php index b775581..8982e0e 100644 --- a/src/DrevOps/BehatFormatProgressFail/Printer/PrinterProgressFail.php +++ b/src/DrevOps/BehatFormatProgressFail/Printer/PrinterProgressFail.php @@ -9,9 +9,11 @@ use Behat\Behat\Output\Node\Printer\StepPrinter; use Behat\Behat\Tester\Result\ExecutedStepResult; use Behat\Behat\Tester\Result\StepResult; +use Behat\Config\Formatter\ShowOutputOption; use Behat\Gherkin\Node\ScenarioLikeInterface as Scenario; use Behat\Gherkin\Node\StepNode; use Behat\Testwork\Output\Formatter; +use Behat\Testwork\Output\Printer\OutputPrinter; use Behat\Testwork\Tester\Result\TestResult; /** @@ -34,7 +36,7 @@ public function __construct(private readonly ResultToStringConverter $resultConv * {@inheritdoc} */ public function printStep(Formatter $formatter, Scenario $scenario, StepNode $step, StepResult $result): void { - $lineWidth = 70; + $line_width = 70; $printer = $formatter->getOutputPrinter(); $style = $this->resultConverter->convertResultToString($result); @@ -60,7 +62,13 @@ public function printStep(Formatter $formatter, Scenario $scenario, StepNode $st break; } - if (0 === ++$this->stepsPrinted % $lineWidth) { + $show_output = $formatter->getParameter(ShowOutputOption::OPTION_NAME); + if ($show_output === ShowOutputOption::Yes || + ($show_output === ShowOutputOption::OnFail && !$result->isPassed())) { + $this->printStdOut($formatter->getOutputPrinter(), $result); + } + + if (0 === ++$this->stepsPrinted % $line_width) { $printer->writeln(' ' . $this->stepsPrinted); } } @@ -81,12 +89,12 @@ protected function printFailure(StepResult $result, StepNode $step): string { $output = ''; - $fileName = ''; - $callResult = $result->getCallResult(); - $call = $callResult->getCall(); + $file_name = ''; + $call_result = $result->getCallResult(); + $call = $call_result->getCall(); if ($call instanceof DefinitionCall) { $feature = $call->getFeature(); - $fileName = $this->relativizePaths($feature->getFile() ?? ''); + $file_name = $this->relativizePaths($feature->getFile() ?? ''); } $fileLine = $step->getLine(); @@ -94,22 +102,22 @@ protected function printFailure(StepResult $result, StepNode $step): string { $output .= sprintf('{+%s}--- FAIL ---{-%s}', $style, $style); $output .= PHP_EOL; - $output .= sprintf(sprintf(' {+%s}%%s %%s{-%s} {+comment}# (%%s):%%s{-comment}', $style, $style), $step->getKeyword(), $step->getText(), $fileName, $fileLine); + $output .= sprintf(sprintf(' {+%s}%%s %%s{-%s} {+comment}# (%%s):%%s{-comment}', $style, $style), $step->getKeyword(), $step->getText(), $file_name, $fileLine); $output .= PHP_EOL; - $stepArguments = $step->getArguments(); - $stepArguments = array_map(static function ($item) { + $step_arguments = $step->getArguments(); + $step_arguments = array_map(static function ($item) { if (method_exists($item, '__toString')) { - return $item->__toString(); + return $item->__toString(); } - return ''; - }, $stepArguments); + return ''; + }, $step_arguments); - $stepArguments = array_filter($stepArguments); + $step_arguments = array_filter($step_arguments); - if (count($stepArguments) > 0) { - $output .= sprintf(sprintf(' {+%s}%%s{-%s}', $style, $style), implode(PHP_EOL, $stepArguments)); + if (count($step_arguments) > 0) { + $output .= sprintf(sprintf(' {+%s}%%s{-%s}', $style, $style), implode(PHP_EOL, $step_arguments)); $output .= PHP_EOL; } @@ -124,6 +132,28 @@ protected function printFailure(StepResult $result, StepNode $step): string { return $output . PHP_EOL; } + /** + * Prints step output (if has one). + */ + protected function printStdOut(OutputPrinter $printer, StepResult $result): void { + if (!$result instanceof ExecutedStepResult || NULL === $result->getCallResult()->getStdOut()) { + return; + } + + $step_definition = $result->getStepDefinition(); + if (!$step_definition) { + return; + } + + $printer->writeln("\n" . $step_definition->getPath() . ':'); + $call_result = $result->getCallResult(); + $pad = function ($line): string { + return sprintf(' | {+stdout}%s{-stdout}', $line); + }; + + $printer->write(implode("\n", array_map($pad, explode("\n", (string) $call_result->getStdOut())))); + } + /** * Transforms path to relative. * @@ -132,8 +162,8 @@ protected function printFailure(StepResult $result, StepNode $step): string { */ protected function relativizePaths(string $path): string { return $this->basePath === '' || $this->basePath === '0' ? $path : str_replace( - $this->basePath . DIRECTORY_SEPARATOR, '', $path - ); + $this->basePath . DIRECTORY_SEPARATOR, '', $path + ); } } diff --git a/tests/behat/features/format-progress-fail.feature b/tests/behat/features/format.feature similarity index 51% rename from tests/behat/features/format-progress-fail.feature rename to tests/behat/features/format.feature index c6de715..7da5c3b 100644 --- a/tests/behat/features/format-progress-fail.feature +++ b/tests/behat/features/format.feature @@ -1,5 +1,6 @@ -Feature: behat-format-progress-fail - Behat output formatter to show progress as TAP and fails inline. +Feature: Format + + Assert that the output format work as expected. Background: Given a file named "features/bootstrap/FeatureContextTest.php" with: @@ -8,8 +9,8 @@ Feature: behat-format-progress-fail use Behat\Behat\Context\CustomSnippetAcceptingContext, Behat\Behat\Tester\Exception\PendingException; - use Behat\Gherkin\Node\PyStringNode, - Behat\Gherkin\Node\TableNode; + use Behat\Gherkinode\PyStringNode, + Behat\Gherkinode\TableNode; use PHPUnit\Framework\Assert; class FeatureContextTest implements CustomSnippetAcceptingContext @@ -51,6 +52,14 @@ Feature: behat-format-progress-fail Assert::assertEquals(intval($count), $this->apples); } + /** + * @Then /^I should have (\d+) apples verbose$/ + */ + public function iShouldHaveApplesVerbose($count) { + print "I show you $this->apples apples"; + Assert::assertEquals(intval($count), $this->apples); + } + /** * @Then /^context parameter "([^"]*)" should be equal to "([^"]*)"$/ */ @@ -67,7 +76,9 @@ Feature: behat-format-progress-fail } } """ - And a file named "behat.yml" with: + + Scenario: Failures during the test formatted correctly + Given a file named "behat.yml" with: """ default: suites: @@ -122,9 +133,6 @@ Feature: behat-format-progress-fail | col1 | col2 | | val1 | val2 | """ - - - Scenario: 2 formats, write first to file When I run "behat --no-colors --strict -f progress_fail" Then it should fail with: """ @@ -173,3 +181,154 @@ Feature: behat-format-progress-fail throw new PendingException(); } """ + + Scenario: Output should be shown only for failed steps + Given a file named "behat.yml" with: + """ + default: + suites: + default: + contexts: + - FeatureContextTest + formatters: + progress_fail: + show_output: on-fail + extensions: + DrevOps\BehatFormatProgressFail\FormatExtension: ~ + """ + And a file named "features/apples.feature" with: + """ + Feature: Apples story + In order to eat apple + As a little kid + I need to have an apple in my pocket + + Background: + Given I have 3 apples + + Scenario: I'm little hungry + When I ate 1 apple + Then I should have 3 apples verbose + """ + When I run "behat --no-colors --strict -f progress_fail" + Then it should fail with: + """ + .. + --- FAIL --- + Then I should have 3 apples verbose # (features/apples.feature):11 + Failed asserting that 2 matches expected 3. + ------------ + + FeatureContextTest::iShouldHaveApplesVerbose(): + | I show you 2 apples + + --- Failed steps: + + 001 Scenario: I'm little hungry # features/apples.feature:9 + Then I should have 3 apples verbose # features/apples.feature:11 + │ I show you 2 apples + Failed asserting that 2 matches expected 3. + + 1 scenario (1 failed) + 3 steps (2 passed, 1 failed) + """ + + Scenario: Output should always be shown + Given a file named "behat.yml" with: + """ + default: + suites: + default: + contexts: + - FeatureContextTest + formatters: + progress_fail: + show_output: yes + extensions: + DrevOps\BehatFormatProgressFail\FormatExtension: ~ + """ + And a file named "features/apples.feature" with: + """ + Feature: Apples story + In order to eat apple + As a little kid + I need to have an apple in my pocket + + Background: + Given I have 3 apples + + Scenario: I'm little hungry + When I ate 1 apple + Then I should have 2 apples verbose + Then I should have 3 apples + """ + When I run "behat --no-colors --strict -f progress_fail" + Then it should fail with: + """ + ... + FeatureContextTest::iShouldHaveApplesVerbose(): + | I show you 2 apples + --- FAIL --- + Then I should have 3 apples # (features/apples.feature):12 + Failed asserting that 2 matches expected 3. + ------------ + + + --- Failed steps: + + 001 Scenario: I'm little hungry # features/apples.feature:9 + Then I should have 3 apples # features/apples.feature:12 + Failed asserting that 2 matches expected 3. + + 1 scenario (1 failed) + 4 steps (3 passed, 1 failed) + """ + + Scenario: Output should not be shown only when not allowed + Given a file named "behat.yml" with: + """ + default: + suites: + default: + contexts: + - FeatureContextTest + formatters: + progress_fail: + show_output: no + extensions: + DrevOps\BehatFormatProgressFail\FormatExtension: ~ + """ + And a file named "features/apples.feature" with: + """ + Feature: Apples story + In order to eat apple + As a little kid + I need to have an apple in my pocket + + Background: + Given I have 3 apples + + Scenario: I'm little hungry + When I ate 1 apple + Then I should have 3 apples verbose + """ + When I run "behat --no-colors --strict -f progress_fail" + Then it should fail with: + """ + .. + --- FAIL --- + Then I should have 3 apples verbose # (features/apples.feature):11 + Failed asserting that 2 matches expected 3. + ------------ + + + --- Failed steps: + + 001 Scenario: I'm little hungry # features/apples.feature:9 + Then I should have 3 apples verbose # features/apples.feature:11 + Failed asserting that 2 matches expected 3. + + 1 scenario (1 failed) + 3 steps (2 passed, 1 failed) + """ +