Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions features/behat-steps.feature
Original file line number Diff line number Diff line change
Expand Up @@ -695,3 +695,64 @@ Feature: Test that WP-CLI Behat steps work as expected
| user_login | user_email |
| admin | admin@example.com |
| user2 | user2@example.com |

Scenario: Test line endings normalization for command output and files
Given an empty directory

# 1. Test STDOUT / STDERR assertions with CRLF
When I run `php -r "echo 'line1' . \"\r\n\" . 'line2' . \"\r\n\";"`
Then STDOUT should be:
"""
line1
line2
"""
And STDOUT should contain:
"""
line1
line2
"""
And STDOUT should match /line1\nline2/

# 2. Test JSON assertions with CRLF
When I run `php -r "echo json_encode(['foo' => \"bar\r\nbaz\r\n\"]) . PHP_EOL;"`
Then STDOUT should be JSON containing:
"""
{"foo":"bar\nbaz\n"}
"""

# 3. Test JSON array assertions with CRLF
When I run `php -r "echo json_encode([\"foo\r\nbar\", \"baz\r\nqux\"]) . PHP_EOL;"`
Then STDOUT should be a JSON array containing:
"""
["foo\nbar", "baz\nqux"]
"""

# 4. Test Table assertions with CRLF
When I run `php -r "echo 'name' . \"\t\" . 'value' . \"\r\n\" . 'foo' . \"\t\" . 'bar' . \"\r\n\";"`
Then STDOUT should be a table containing rows:
| name | value |
| foo | bar |

# 5. Test CSV assertions with CRLF
When I run `php -r "echo 'name,value' . \"\r\n\" . 'foo,bar' . \"\r\n\";"`
Then STDOUT should be CSV containing:
| name | value |
| foo | bar |

# 6. Test YAML assertions with CRLF
When I run `php -r "echo 'foo: bar' . \"\r\n\" . 'baz: qux' . \"\r\n\";"`
Then STDOUT should be YAML containing:
"""
foo: bar
baz: qux
"""

# 7. Test File assertions with CRLF
When I run `php -r "file_put_contents('crlf.txt', 'crlf1' . \"\r\n\" . 'crlf2' . \"\r\n\");"`
Then the crlf.txt file should contain:
"""
crlf1
crlf2
"""
And the contents of the crlf.txt file should match /crlf1\ncrlf2/

2 changes: 1 addition & 1 deletion features/http-mocking.feature
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Feature: HTTP request mocking
Mocked file contents on disk!
"""

When I try `wp eval 'WP_CLI\Utils\http_request("GET", "https://example.com/mocked-file.txt", null, [], ["filename" => "downloaded.txt"]);' --skip-wordpress`
When I try `wp eval "WP_CLI\Utils\http_request('GET', 'https://example.com/mocked-file.txt', null, [], ['filename' => 'downloaded.txt']);" --skip-wordpress`
Then the return code should be 0
And the downloaded.txt file should contain:
"""
Expand Down
47 changes: 43 additions & 4 deletions src/Context/Support.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ protected function assert_not_numeric( $actual ): void {
* @throws Exception
*/
protected function check_string( $output, $expected, $action, $message = false, $strictly = false ): void {
$output = $this->normalize_line_endings( $output );
$expected = $this->normalize_line_endings( $expected );

// Strip ANSI color codes before comparing strings.
if ( ! $strictly ) {
$output = preg_replace( '/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $output );
Expand Down Expand Up @@ -236,8 +239,8 @@ protected function compare_contents( $expected, $actual ) {
* the contents of 'array' does not include 3
*/
protected function check_that_json_string_contains_json_string( $actual_json, $expected_json ) {
$actual_value = json_decode( $actual_json );
$expected_value = json_decode( $expected_json );
$actual_value = $this->normalize_line_endings_in_data( json_decode( $actual_json ) );
$expected_value = $this->normalize_line_endings_in_data( json_decode( $expected_json ) );

if ( ! $actual_value ) {
return false;
Expand Down Expand Up @@ -311,8 +314,8 @@ static function ( $str ) {
* @return bool whether or not $actual_yaml contains $expected_json
*/
protected function check_that_yaml_string_contains_yaml_string( $actual_yaml, $expected_yaml ) {
$actual_value = Spyc::YAMLLoad( $actual_yaml );
$expected_value = Spyc::YAMLLoad( $expected_yaml );
$actual_value = $this->normalize_line_endings_in_data( Spyc::YAMLLoad( $actual_yaml ) );
$expected_value = $this->normalize_line_endings_in_data( Spyc::YAMLLoad( $expected_yaml ) );

if ( ! $actual_value ) {
return false;
Expand All @@ -336,4 +339,40 @@ protected function generate_diff( string $expected, string $actual ): string {
$differ = new Differ( $builder );
return $differ->diff( $expected, $actual );
}

/**
* Normalize line endings of a string to Unix-style LF (\n).
*
* @param string $str The string to normalize.
* @return string The normalized string.
*/
protected function normalize_line_endings( string $str ): string {
return str_replace( "\r\n", "\n", $str );
}
Comment thread
swissspidy marked this conversation as resolved.

/**
* Recursively normalize line endings in a data structure (array, object, or string).
*
* @param mixed $data The data structure to normalize.
* @return mixed The normalized data structure.
*/
protected function normalize_line_endings_in_data( $data ) {
if ( is_string( $data ) ) {
return $this->normalize_line_endings( $data );
}

if ( is_array( $data ) ) {
return array_map( [ $this, 'normalize_line_endings_in_data' ], $data );
}

if ( is_object( $data ) ) {
$normalized = new \stdClass();
foreach ( get_object_vars( $data ) as $key => $value ) {
$normalized->$key = $this->normalize_line_endings_in_data( $value );
}
return $normalized;
}

return $data;
}
}
41 changes: 23 additions & 18 deletions src/Context/ThenStepDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ public function then_stdout_stderr_should_contain( $stream, $strictly, $action,
public function then_stdout_stderr_should_be_a_number( $stream ): void {

$stream = strtolower( $stream );
$output = $this->normalize_line_endings( $this->result->$stream );

$this->assert_numeric( trim( $this->result->$stream, "\n" ) );
$this->assert_numeric( trim( $output, "\n" ) );
}

/**
Expand All @@ -118,8 +119,9 @@ public function then_stdout_stderr_should_be_a_number( $stream ): void {
public function then_stdout_stderr_should_not_be_a_number( $stream ): void {

$stream = strtolower( $stream );
$output = $this->normalize_line_endings( $this->result->$stream );

$this->assert_not_numeric( trim( $this->result->$stream, "\n" ) );
$this->assert_not_numeric( trim( $output, "\n" ) );
}

/**
Expand All @@ -140,7 +142,7 @@ public function then_stdout_stderr_should_not_be_a_number( $stream ): void {
* @Then /^STDOUT should be a table containing rows:$/
*/
public function then_stdout_should_be_a_table_containing_rows( TableNode $expected ): void {
$output = $this->result->stdout;
$output = $this->normalize_line_endings( $this->result->stdout );
$actual_rows = explode( "\n", rtrim( $output, "\n" ) );

$expected_rows = array();
Expand Down Expand Up @@ -176,7 +178,7 @@ public function then_stdout_should_be_a_table_containing_rows( TableNode $expect
* @Then /^STDOUT should end with a table containing rows:$/
*/
public function then_stdout_should_end_with_a_table_containing_rows( TableNode $expected ): void {
$output = $this->result->stdout;
$output = $this->normalize_line_endings( $this->result->stdout );
$actual_rows = explode( "\n", rtrim( $output, "\n" ) );

$expected_rows = array();
Expand Down Expand Up @@ -212,8 +214,8 @@ public function then_stdout_should_end_with_a_table_containing_rows( TableNode $
* @Then /^STDOUT should be JSON containing:$/
*/
public function then_stdout_should_be_json_containing( PyStringNode $expected ): void {
$output = $this->result->stdout;
$expected = $this->replace_variables( (string) $expected );
$output = $this->normalize_line_endings( $this->result->stdout );
$expected = $this->normalize_line_endings( $this->replace_variables( (string) $expected ) );

if ( ! $this->check_that_json_string_contains_json_string( $output, $expected ) ) {
$message = (string) $this->result;
Expand Down Expand Up @@ -253,11 +255,11 @@ public function then_stdout_should_be_json_containing( PyStringNode $expected ):
* @Then /^STDOUT should be a JSON array containing:$/
*/
public function then_stdout_should_be_a_json_array_containing( PyStringNode $expected ): void {
$output = $this->result->stdout;
$expected = $this->replace_variables( (string) $expected );
$output = $this->normalize_line_endings( $this->result->stdout );
$expected = $this->normalize_line_endings( $this->replace_variables( (string) $expected ) );

$actual_values = json_decode( $output );
$expected_values = json_decode( $expected );
$actual_values = $this->normalize_line_endings_in_data( json_decode( $output ) );
$expected_values = $this->normalize_line_endings_in_data( json_decode( $expected ) );

$missing = array_diff( $expected_values, $actual_values );
if ( ! empty( $missing ) ) {
Expand Down Expand Up @@ -293,7 +295,7 @@ public function then_stdout_should_be_a_json_array_containing( PyStringNode $exp
* @Then /^STDOUT should be CSV containing:$/
*/
public function then_stdout_should_be_csv_containing( TableNode $expected ): void {
$output = $this->result->stdout;
$output = $this->normalize_line_endings( $this->result->stdout );

$expected_rows = $expected->getRows();
foreach ( $expected_rows as &$row ) {
Expand Down Expand Up @@ -336,8 +338,8 @@ public function then_stdout_should_be_csv_containing( TableNode $expected ): voi
* @Then /^STDOUT should be YAML containing:$/
*/
public function then_stdout_should_be_yaml_containing( PyStringNode $expected ): void {
$output = $this->result->stdout;
$expected = $this->replace_variables( (string) $expected );
$output = $this->normalize_line_endings( $this->result->stdout );
$expected = $this->normalize_line_endings( $this->replace_variables( (string) $expected ) );

if ( ! $this->check_that_yaml_string_contains_yaml_string( $output, $expected ) ) {
$message = (string) $this->result;
Expand Down Expand Up @@ -392,8 +394,9 @@ public function then_stdout_stderr_should_be_empty( $stream ): void {
public function then_stdout_stderr_should_not_be_empty( $stream ): void {

$stream = strtolower( $stream );
$output = $this->normalize_line_endings( $this->result->$stream );

if ( '' === rtrim( $this->result->$stream, "\n" ) ) {
if ( '' === rtrim( $output, "\n" ) ) {
throw new Exception( $this->result );
}
}
Expand All @@ -419,7 +422,8 @@ public function then_stdout_stderr_should_not_be_empty( $stream ): void {
public function then_stdout_stderr_should_be_a_specific_version_string( $stream, $operator, $goal_ver ): void {
$goal_ver = $this->replace_variables( $goal_ver );
$stream = strtolower( $stream );
if ( false === version_compare( trim( $this->result->$stream, "\n" ), $goal_ver, $operator ) ) {
$output = $this->normalize_line_endings( $this->result->$stream );
if ( false === version_compare( trim( $output, "\n" ), $goal_ver, $operator ) ) {
throw new Exception( $this->result );
}
}
Expand Down Expand Up @@ -530,7 +534,7 @@ public function then_the_contents_of_a_specific_file_should_match( $path, $not,
if ( '/' !== $path[0] ) {
$path = $this->variables['RUN_DIR'] . "/$path";
}
$contents = file_get_contents( $path );
$contents = $this->normalize_line_endings( file_get_contents( $path ) );
if ( $not ) {
$this->assert_not_regex( $expected, $contents );
} else {
Expand Down Expand Up @@ -559,10 +563,11 @@ public function then_stdout_stderr_should_match_a_string( $stream, $not, $expect
$expected = $this->replace_variables( $expected );

$stream = strtolower( $stream );
$output = $this->normalize_line_endings( $this->result->$stream );
if ( $not ) {
$this->assert_not_regex( $expected, $this->result->$stream );
$this->assert_not_regex( $expected, $output );
} else {
$this->assert_regex( $expected, $this->result->$stream );
$this->assert_regex( $expected, $output );
}
}

Expand Down
Loading