diff --git a/src/Context/Support.php b/src/Context/Support.php index cd3f34ca6..c68e2879c 100644 --- a/src/Context/Support.php +++ b/src/Context/Support.php @@ -8,6 +8,8 @@ use Behat\Behat\Exception\PendingException; use Exception; use Mustangostang\Spyc; +use SebastianBergmann\Diff\Differ; +use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; trait Support { @@ -120,6 +122,12 @@ protected function check_string( $output, $expected, $action, $message = false, if ( false === $message ) { $message = $output; } + + $diff = $this->generate_diff( $expected, rtrim( $output, "\n" ) ); + if ( ! empty( $diff ) ) { + $message .= "\n\n" . $diff; + } + throw new Exception( $message ); } } @@ -133,7 +141,10 @@ protected function check_string( $output, $expected, $action, $message = false, protected function compare_tables( $expected_rows, $actual_rows, $output ): void { // The first row is the header and must be present. if ( $expected_rows[0] !== $actual_rows[0] ) { - throw new Exception( $output ); + $expected_table = implode( "\n", $expected_rows ); + $actual_table = implode( "\n", $actual_rows ); + $diff = $this->generate_diff( $expected_table, $actual_table ); + throw new Exception( $output . "\n\n" . $diff ); } unset( $actual_rows[0] ); @@ -141,7 +152,10 @@ protected function compare_tables( $expected_rows, $actual_rows, $output ): void $missing_rows = array_diff( $expected_rows, $actual_rows ); if ( ! empty( $missing_rows ) ) { - throw new Exception( $output ); + $expected_table = implode( "\n", $expected_rows ); + $actual_table = implode( "\n", $actual_rows ); + $diff = $this->generate_diff( $expected_table, $actual_table ); + throw new Exception( $output . "\n\n" . $diff ); } } @@ -290,4 +304,20 @@ protected function check_that_yaml_string_contains_yaml_string( $actual_yaml, $e return $this->compare_contents( $expected_value, $actual_value ); } + + /** + * Generate a unified diff between two strings. + * + * @param string $expected The expected string. + * @param string $actual The actual string. + * @return string The unified diff output. + */ + protected function generate_diff( string $expected, string $actual ): string { + $builder = new UnifiedDiffOutputBuilder( + "--- Expected\n+++ Actual\n", + false + ); + $differ = new Differ( $builder ); + return $differ->diff( $expected, $actual ); + } } diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index 9dc877b8e..bc2cb3f5b 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -215,7 +215,21 @@ public function then_stdout_should_be_json_containing( PyStringNode $expected ): $expected = $this->replace_variables( (string) $expected ); if ( ! $this->check_that_json_string_contains_json_string( $output, $expected ) ) { - throw new Exception( $this->result ); + $message = (string) $this->result; + // Pretty print JSON for better diff readability. + $expected_decoded = json_decode( $expected ); + $actual_decoded = json_decode( $output ); + if ( null !== $expected_decoded && null !== $actual_decoded ) { + $expected_json = json_encode( $expected_decoded, JSON_PRETTY_PRINT ); + $actual_json = json_encode( $actual_decoded, JSON_PRETTY_PRINT ); + if ( false !== $expected_json && false !== $actual_json ) { + $diff = $this->generate_diff( $expected_json, $actual_json ); + if ( ! empty( $diff ) ) { + $message .= "\n\n" . $diff; + } + } + } + throw new Exception( $message ); } } @@ -246,7 +260,19 @@ public function then_stdout_should_be_a_json_array_containing( PyStringNode $exp $missing = array_diff( $expected_values, $actual_values ); if ( ! empty( $missing ) ) { - throw new Exception( $this->result ); + $message = (string) $this->result; + // Pretty print JSON arrays for better diff readability. + if ( null !== $expected_values && null !== $actual_values ) { + $expected_json = json_encode( $expected_values, JSON_PRETTY_PRINT ); + $actual_json = json_encode( $actual_values, JSON_PRETTY_PRINT ); + if ( false !== $expected_json && false !== $actual_json ) { + $diff = $this->generate_diff( $expected_json, $actual_json ); + if ( ! empty( $diff ) ) { + $message .= "\n\n" . $diff; + } + } + } + throw new Exception( $message ); } } @@ -269,19 +295,29 @@ public function then_stdout_should_be_csv_containing( TableNode $expected ): voi $output = $this->result->stdout; $expected_rows = $expected->getRows(); - foreach ( $expected as &$row ) { + foreach ( $expected_rows as &$row ) { foreach ( $row as &$value ) { $value = $this->replace_variables( $value ); } } if ( ! $this->check_that_csv_string_contains_values( $output, $expected_rows ) ) { - throw new Exception( $this->result ); + $message = (string) $this->result; + // Convert expected rows to CSV format for diff. + $expected_csv = ''; + foreach ( $expected_rows as $row ) { + $expected_csv .= implode( ',', array_map( 'trim', $row ) ) . "\n"; + } + $diff = $this->generate_diff( trim( $expected_csv ), trim( $output ) ); + if ( ! empty( $diff ) ) { + $message .= "\n\n" . $diff; + } + throw new Exception( $message ); } } /** - * Expect STDOUT to be YAML containig certain content. + * Expect STDOUT to be YAML containing certain content. * * ``` * Scenario: My example scenario @@ -303,7 +339,12 @@ public function then_stdout_should_be_yaml_containing( PyStringNode $expected ): $expected = $this->replace_variables( (string) $expected ); if ( ! $this->check_that_yaml_string_contains_yaml_string( $output, $expected ) ) { - throw new Exception( $this->result ); + $message = (string) $this->result; + $diff = $this->generate_diff( $expected, $output ); + if ( ! empty( $diff ) ) { + $message .= "\n\n" . $diff; + } + throw new Exception( $message ); } }