From bf4f816ecb1ba1c62695966aa713a66c8ea8e13e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 3 Jan 2022 05:21:04 +0100 Subject: [PATCH] PHPUnit 10 | AssertIgnoringLineEndings trait: polyfill the Assert::assertStringEqualsStringIgnoringLineEndings() et al methods PHPUnit 10.0.0 introduces the new `Assert::assertStringEqualsStringIgnoringLineEndings()` and `Assert::assertStringContainsStringIgnoringLineEndings()` methods. This commit: * Adds two traits with the same name. One to polyfill the methods when not available in PHPUnit. The other - an empty trait - to allow for `use`-ing the trait in PHPUnit versions in which the methods are already natively available. * Logic to the custom autoloader which will load the correct trait depending on the PHPUnit version used. * An availability test and limited functional test for the functionality polyfilled. Note: the function name for the `private` `normalizeLineEndings()` method is a little convoluted - `normalizeLineEndingsForIgnoringLineEndingsAssertions()`. This is intentional to prevent potential naming collisions with pre-existing end-user defined methods for the same, which may exist in other traits used in tests, which would be hard to solve due to the method only existing in the non-empty trait. Includes: * Adding the new polyfill to the existing `TestCases` classes. Refs: * https://github.com/sebastianbergmann/phpunit/issues/4641 * https://github.com/sebastianbergmann/phpunit/pull/4670 (and follow up commits) * https://github.com/sebastianbergmann/phpunit/pull/5279 Co-authored-by: Sergei Predvoditelev Co-authored-by: Sebastian Bergmann --- README.md | 52 +++ phpunitpolyfills-autoload.php | 21 + src/Polyfills/AssertIgnoringLineEndings.php | 130 +++++++ .../AssertIgnoringLineEndings_Empty.php | 8 + src/TestCases/TestCasePHPUnitGte8.php | 2 + src/TestCases/TestCasePHPUnitLte7.php | 2 + src/TestCases/XTestCase.php | 2 + .../AssertIgnoringLineEndingsTest.php | 366 ++++++++++++++++++ tests/TestCases/TestCaseTestTrait.php | 9 + 9 files changed, 592 insertions(+) create mode 100644 src/Polyfills/AssertIgnoringLineEndings.php create mode 100644 src/Polyfills/AssertIgnoringLineEndings_Empty.php create mode 100644 tests/Polyfills/AssertIgnoringLineEndingsTest.php diff --git a/README.md b/README.md index 96f006a..cc858e8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Set of polyfills for changed PHPUnit functionality to allow for creating PHPUnit - [PHPUnit support](#phpunit-support) * [Using this library](#using-this-library) - [Supported ways of calling the assertions](#supported-ways-of-calling-the-assertions) + - [Use with PHPUnit < 7.5.0](#use-with-phpunit--750) * [Features](#features) - [Polyfill traits](#polyfill-traits) - [Helper traits](#helper-traits) @@ -147,6 +148,45 @@ For the polyfills to work, a test class is **required** to be a (grand-)child of [four ways of calling assertions]: https://docs.phpunit.de/en/9.6/assertions.html#static-vs-non-static-usage-of-assertion-methods +### Use with PHPUnit < 7.5.0 + +If your library still needs to support PHP < 7.1 and therefore needs PHPUnit < 7 for testing, there are a few caveats when using the traits stand-alone as we then enter "double-polyfill" territory. + +To prevent _"conflicting method names"_ errors when a trait is `use`d multiple times in a class, the traits offered here do not attempt to solve this. + +You will need to make sure to `use` any additional traits needed for the polyfills to work. + +| PHPUnit | When `use`-ing this trait | You also need to `use` this trait | +|-----------|-----------------------------|-----------------------------------| +| 5.7 < 7.5 | `AssertIgnoringLineEndings` | `AssertStringContains` | + +_**Note: this only applies to the stand-alone use of the traits. The [`TestCase` classes](#testcases) provided by this library already take care of this automatically.**_ + +Code example for a test using the `AssertIgnoringLineEndings` trait, which needs to be able to run on PHPUnit 5.7: +```php +assertStringContainsStringIgnoringLineEndings( + "something\nelse", + "this is something\r\nelse" + ); + } +} +``` + Features -------- @@ -354,6 +394,18 @@ The `assertObjectEquals()` assertion was introduced in PHPUnit 9.4.0. [`Assert::assertObjectEquals()`]: https://docs.phpunit.de/en/9.6/assertions.html#assertobjectequals +#### PHPUnit < 10.0.0: `Yoast\PHPUnitPolyfills\Polyfills\AssertIgnoringLineEndings` + +Polyfills the following methods: +| | | +|-----------------------------------------------------------|-------------------------------------------------------------| +| [`Assert::assertStringEqualsStringIgnoringLineEndings()`] | [`Assert::assertStringContainsStringIgnoringLineEndings()`] | + +These methods were introduced in PHPUnit 10.0.0. + +[`Assert::assertStringEqualsStringIgnoringLineEndings()`]: https://docs.phpunit.de/en/main/assertions.html#assertstringequalsstringignoringlineendings +[`Assert::assertStringContainsStringIgnoringLineEndings()`]: https://docs.phpunit.de/en/main/assertions.html#assertstringcontainsstringignoringlineendings + ### Helper traits diff --git a/phpunitpolyfills-autoload.php b/phpunitpolyfills-autoload.php index c8de739..50d2268 100644 --- a/phpunitpolyfills-autoload.php +++ b/phpunitpolyfills-autoload.php @@ -91,6 +91,10 @@ public static function load( $className ) { self::loadAssertObjectEquals(); return true; + case 'Yoast\PHPUnitPolyfills\Polyfills\AssertIgnoringLineEndings': + self::loadAssertIgnoringLineEndings(); + return true; + case 'Yoast\PHPUnitPolyfills\TestCases\TestCase': self::loadTestCase(); return true; @@ -289,6 +293,23 @@ public static function loadAssertObjectEquals() { require_once __DIR__ . '/src/Polyfills/AssertObjectEquals_Empty.php'; } + /** + * Load the AssertIgnoringLineEndings polyfill or an empty trait with the same name + * if a PHPUnit version is used which already contains this functionality. + * + * @return void + */ + public static function loadAssertIgnoringLineEndings() { + if ( \method_exists( Assert::class, 'assertStringEqualsStringIgnoringLineEndings' ) === false ) { + // PHPUnit < 10.0.0. + require_once __DIR__ . '/src/Polyfills/AssertIgnoringLineEndings.php'; + return; + } + + // PHPUnit >= 10.0.0. + require_once __DIR__ . '/src/Polyfills/AssertIgnoringLineEndings_Empty.php'; + } + /** * Load the appropriate TestCase class based on the PHPUnit version being used. * diff --git a/src/Polyfills/AssertIgnoringLineEndings.php b/src/Polyfills/AssertIgnoringLineEndings.php new file mode 100644 index 0000000..8cc4f73 --- /dev/null +++ b/src/Polyfills/AssertIgnoringLineEndings.php @@ -0,0 +1,130 @@ +export( $actual ), + $expected + ); + + if ( $message !== '' ) { + $msg = $message . \PHP_EOL . $msg; + } + + $actual = self::normalizeLineEndingsForIgnoringLineEndingsAssertions( (string) $actual ); + + static::assertSame( $expected, $actual, $msg ); + } + + /** + * Asserts that two variables are equal (ignoring case). + * + * @param string $needle The string to search for. + * @param string $haystack The string to treat as the haystack. + * @param string $message Optional failure message to display. + * + * @return void + * + * @throws TypeError When any of the passed arguments do not meet the required type. + */ + final public static function assertStringContainsStringIgnoringLineEndings( $needle, $haystack, $message = '' ) { + /* + * Parameter input validation. + * In PHPUnit this is done via PHP native type declarations. Emulating this for the polyfill. + * Note: using `is_scalar()` instead of `is_string()` as test files may not be using strict_types. + */ + if ( \is_scalar( $needle ) === false ) { + throw new TypeError( + \sprintf( + 'Argument 1 passed to assertStringContainsStringIgnoringLineEndings() must be of type string, %s given', + \gettype( $needle ) + ) + ); + } + if ( \is_scalar( $haystack ) === false ) { + throw new TypeError( + \sprintf( + 'Argument 2 passed to assertStringContainsStringIgnoringLineEndings() must be of type string, %s given', + \gettype( $haystack ) + ) + ); + } + + $needle = self::normalizeLineEndingsForIgnoringLineEndingsAssertions( (string) $needle ); + $haystack = self::normalizeLineEndingsForIgnoringLineEndingsAssertions( (string) $haystack ); + + static::assertStringContainsString( $needle, $haystack, $message ); + } + + /** + * Normalize line endings. + * + * @param string $value The text to normalize. + * + * @return string + */ + private static function normalizeLineEndingsForIgnoringLineEndingsAssertions( $value ) { + return \strtr( + $value, + [ + "\r\n" => "\n", + "\r" => "\n", + ] + ); + } +} diff --git a/src/Polyfills/AssertIgnoringLineEndings_Empty.php b/src/Polyfills/AssertIgnoringLineEndings_Empty.php new file mode 100644 index 0000000..9099ae2 --- /dev/null +++ b/src/Polyfills/AssertIgnoringLineEndings_Empty.php @@ -0,0 +1,8 @@ += 10.0.0 in which this polyfill is not needed. + */ +trait AssertIgnoringLineEndings {} diff --git a/src/TestCases/TestCasePHPUnitGte8.php b/src/TestCases/TestCasePHPUnitGte8.php index e635bec..49abd0c 100644 --- a/src/TestCases/TestCasePHPUnitGte8.php +++ b/src/TestCases/TestCasePHPUnitGte8.php @@ -6,6 +6,7 @@ use Yoast\PHPUnitPolyfills\Helpers\AssertAttributeHelper; use Yoast\PHPUnitPolyfills\Polyfills\AssertClosedResource; use Yoast\PHPUnitPolyfills\Polyfills\AssertFileEqualsSpecializations; +use Yoast\PHPUnitPolyfills\Polyfills\AssertIgnoringLineEndings; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; use Yoast\PHPUnitPolyfills\Polyfills\AssertObjectEquals; use Yoast\PHPUnitPolyfills\Polyfills\EqualToSpecializations; @@ -25,6 +26,7 @@ abstract class TestCase extends PHPUnit_TestCase { use AssertAttributeHelper; use AssertClosedResource; use AssertFileEqualsSpecializations; + use AssertIgnoringLineEndings; use AssertionRenames; use AssertObjectEquals; use EqualToSpecializations; diff --git a/src/TestCases/TestCasePHPUnitLte7.php b/src/TestCases/TestCasePHPUnitLte7.php index 2943ce0..314b336 100644 --- a/src/TestCases/TestCasePHPUnitLte7.php +++ b/src/TestCases/TestCasePHPUnitLte7.php @@ -7,6 +7,7 @@ use Yoast\PHPUnitPolyfills\Polyfills\AssertClosedResource; use Yoast\PHPUnitPolyfills\Polyfills\AssertEqualsSpecializations; use Yoast\PHPUnitPolyfills\Polyfills\AssertFileEqualsSpecializations; +use Yoast\PHPUnitPolyfills\Polyfills\AssertIgnoringLineEndings; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; use Yoast\PHPUnitPolyfills\Polyfills\AssertIsType; use Yoast\PHPUnitPolyfills\Polyfills\AssertObjectEquals; @@ -30,6 +31,7 @@ abstract class TestCase extends PHPUnit_TestCase { use AssertClosedResource; use AssertEqualsSpecializations; use AssertFileEqualsSpecializations; + use AssertIgnoringLineEndings; use AssertionRenames; use AssertIsType; use AssertObjectEquals; diff --git a/src/TestCases/XTestCase.php b/src/TestCases/XTestCase.php index f1339f4..461862d 100644 --- a/src/TestCases/XTestCase.php +++ b/src/TestCases/XTestCase.php @@ -7,6 +7,7 @@ use Yoast\PHPUnitPolyfills\Polyfills\AssertClosedResource; use Yoast\PHPUnitPolyfills\Polyfills\AssertEqualsSpecializations; use Yoast\PHPUnitPolyfills\Polyfills\AssertFileEqualsSpecializations; +use Yoast\PHPUnitPolyfills\Polyfills\AssertIgnoringLineEndings; use Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames; use Yoast\PHPUnitPolyfills\Polyfills\AssertIsType; use Yoast\PHPUnitPolyfills\Polyfills\AssertObjectEquals; @@ -32,6 +33,7 @@ abstract class XTestCase extends PHPUnit_TestCase { use AssertClosedResource; use AssertEqualsSpecializations; use AssertFileEqualsSpecializations; + use AssertIgnoringLineEndings; use AssertionRenames; use AssertIsType; use AssertObjectEquals; diff --git a/tests/Polyfills/AssertIgnoringLineEndingsTest.php b/tests/Polyfills/AssertIgnoringLineEndingsTest.php new file mode 100644 index 0000000..080ca06 --- /dev/null +++ b/tests/Polyfills/AssertIgnoringLineEndingsTest.php @@ -0,0 +1,366 @@ += 80100 + && \version_compare( PHPUnit_Version::id(), '10.0.0', '>=' ) + ) { + $msg = 'assertStringEqualsStringIgnoringLineEndings(): Argument #1 ($expected) must be of type string, '; + } + else { + // PHP 5/7. + $msg = 'Argument 1 passed to assertStringEqualsStringIgnoringLineEndings() must be of type string, '; + } + + $this->expectException( TypeError::class ); + $this->expectExceptionMessage( $msg ); + + self::assertStringEqualsStringIgnoringLineEndings( $input, 'string' ); + } + + /** + * Verify that the assertStringEqualsStringIgnoringLineEndings() method throws a TypeError + * when the $actual parameter is not a scalar. + * + * @dataProvider dataThrowsTypeErrorOnInvalidType + * + * @param mixed $input Non-string value. + * + * @return void + */ + public function testAssertStringEqualsStringIgnoringLineEndingsThrowsTypeErrorOnInvalidTypeArg2( $input ) { + if ( \PHP_VERSION_ID >= 80100 + && \version_compare( PHPUnit_Version::id(), '10.0.0', '>=' ) + ) { + $msg = 'assertStringEqualsStringIgnoringLineEndings(): Argument #2 ($actual) must be of type string, '; + } + else { + // PHP 5/7. + $msg = 'Argument 2 passed to assertStringEqualsStringIgnoringLineEndings() must be of type string, '; + } + + $this->expectException( TypeError::class ); + $this->expectExceptionMessage( $msg ); + + static::assertStringEqualsStringIgnoringLineEndings( 'string', $input ); + } + + /** + * Verify availability and functionality of the assertStringEqualsStringIgnoringLineEndings() method. + * + * @dataProvider dataAllLineEndingVariations + * @dataProvider dataAssertStringEqualsStringIgnoringLineEndingsTypeVariations + * + * @param mixed $expected Expected value. + * @param mixed $actual The value to test. + * + * @return void + */ + public function testAssertStringEqualsStringIgnoringLineEndings( $expected, $actual ) { + self::assertStringEqualsStringIgnoringLineEndings( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array + */ + public static function dataAllLineEndingVariations() { + return [ + 'lf-crlf' => [ "a\nb", "a\r\nb" ], + 'cr-crlf' => [ "a\rb", "a\r\nb" ], + 'crlf-crlf' => [ "a\r\nb", "a\r\nb" ], + 'lf-cr' => [ "a\nb", "a\rb" ], + 'cr-cr' => [ "a\rb", "a\rb" ], + 'crlf-cr' => [ "a\r\nb", "a\rb" ], + 'lf-lf' => [ "a\nb", "a\nb" ], + 'cr-lf' => [ "a\rb", "a\nb" ], + 'crlf-lf' => [ "a\r\nb", "a\nb" ], + ]; + } + + /** + * Data provider. + * + * @return array + */ + public static function dataAssertStringEqualsStringIgnoringLineEndingsTypeVariations() { + return [ + 'comparing int with string' => [ 10, '10' ], + 'comparing int with float' => [ 10, 10.0 ], + 'comparing bool false with string' => [ false, '' ], + 'comparing bool true with string' => [ true, '1' ], + 'comparing bool true with int' => [ true, 1 ], + ]; + } + + /** + * Verify handling of the lines endings for the assertStringEqualsStringIgnoringLineEndings() method. + * + * @dataProvider dataAssertStringEqualsStringIgnoringLineEndingsFails + * + * @param mixed $expected Expected value. + * @param mixed $actual The value to test. + * + * @return void + */ + public function testAssertStringEqualsStringIgnoringLineEndingsFails( $expected, $actual ) { + + $exporter = \class_exists( Exporter::class ) ? new Exporter() : new Exporter_In_Phar(); + $msg = \sprintf( + 'Failed asserting that %s is equal to "%s" ignoring line endings.', + $exporter->export( $actual ), + self::normalizeLineEndings( $expected ) + ); + + $this->expectException( $this->getAssertionFailedExceptionName() ); + $this->expectExceptionMessage( $msg ); + + $this->assertStringEqualsStringIgnoringLineEndings( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array + */ + public static function dataAssertStringEqualsStringIgnoringLineEndingsFails() { + return [ + 'lf-none' => [ "a\nb", 'ab' ], + 'cr-none' => [ "a\rb", 'ab' ], + 'crlf-none' => [ "a\r\nb", 'ab' ], + 'none-lf' => [ 'ab', "a\nb" ], + 'none-cr' => [ 'ab', "a\rb" ], + 'none-crlf' => [ 'ab', "a\r\nb" ], + ]; + } + + /** + * Verify that the assertStringContainsStringIgnoringLineEndings() method throws a TypeError + * when the $needle parameter is not a scalar. + * + * @dataProvider dataThrowsTypeErrorOnInvalidType + * + * @param mixed $input Non-string value. + * + * @return void + */ + public function testAssertStringContainsStringIgnoringLineEndingsThrowsTypeErrorOnInvalidTypeArg1( $input ) { + if ( \PHP_VERSION_ID >= 80100 + && \version_compare( PHPUnit_Version::id(), '10.0.0', '>=' ) + ) { + $msg = 'assertStringContainsStringIgnoringLineEndings(): Argument #1 ($needle) must be of type string, '; + } + else { + // PHP 5/7. + $msg = 'Argument 1 passed to assertStringContainsStringIgnoringLineEndings() must be of type string, '; + } + + $this->expectException( TypeError::class ); + $this->expectExceptionMessage( $msg ); + + static::assertStringContainsStringIgnoringLineEndings( $input, 'string' ); + } + + /** + * Verify that the assertStringContainsStringIgnoringLineEndings() method throws a TypeError + * when the $haystack parameter is not a scalar. + * + * @dataProvider dataThrowsTypeErrorOnInvalidType + * + * @param mixed $input Non-string value. + * + * @return void + */ + public function testAssertStringContainsStringIgnoringLineEndingsThrowsTypeErrorOnInvalidTypeArg2( $input ) { + if ( \PHP_VERSION_ID >= 80100 + && \version_compare( PHPUnit_Version::id(), '10.0.0', '>=' ) + ) { + $msg = 'assertStringContainsStringIgnoringLineEndings(): Argument #2 ($haystack) must be of type string, '; + } + else { + // PHP 5/7. + $msg = 'Argument 2 passed to assertStringContainsStringIgnoringLineEndings() must be of type string, '; + } + + $this->expectException( TypeError::class ); + $this->expectExceptionMessage( $msg ); + + self::assertStringContainsStringIgnoringLineEndings( 'string', $input ); + } + + /** + * Verify availability and functionality of the assertStringContainsStringIgnoringLineEndings() method. + * + * @dataProvider dataAssertStringContainsStringIgnoringLineEndings + * + * @param string $needle The string to search for. + * @param string $haystack The string to treat as the haystack. + * + * @return void + */ + public function testAssertStringContainsStringIgnoringLineEndings( $needle, $haystack ) { + $this->assertStringContainsStringIgnoringLineEndings( $needle, $haystack ); + } + + /** + * Data provider. + * + * @return array + */ + public static function dataAssertStringContainsStringIgnoringLineEndings() { + return [ + 'needle is empty string' => [ '', "b\r\nc" ], + 'same string' => [ "b\nc", "b\r\nc" ], + 'needle is substring 1' => [ 'b', "a\r\nb\r\nc\r\nd" ], + 'needle is substring 2' => [ "b\nc", "a\r\nb\r\nc\r\nd" ], + 'haystack is scalar non-string' => [ '10', 24310276 ], + 'needle is scalar non-string 1' => [ 10, '24310276' ], + 'needle is scalar non-string 2' => [ false, "something\nelse" ], + 'needle is scalar non-string 3' => [ true, '123' ], + ]; + } + + /** + * Verify that the assertStringContainsStringIgnoringLineEndings() method normalizes the line endings + * of both the haystack and the needle. + * + * @link https://github.com/sebastianbergmann/phpunit/pull/5279 + * + * @dataProvider dataAllLineEndingVariations + * + * @param string $needle The string to search for. + * @param string $haystack The string to treat as the haystack. + * + * @return void + */ + public function testAssertStringContainsStringIgnoringLineEndingsBug5279( $needle, $haystack ) { + if ( \version_compare( PHPUnit_Version::id(), '10.0.0', '>=' ) + && \version_compare( PHPUnit_Version::id(), '10.0.16', '<' ) + ) { + // This bug was fixed in PHPUnit 10.0.16. + $this->markTestSkipped( 'Skipping tests on PHPUnit versions which contained the bug' ); + } + + $this->assertStringContainsStringIgnoringLineEndings( $needle, $haystack ); + } + + /** + * Verify that the assertStringContainsStringIgnoringLineEndings() method fails a test + * when the needle is not found in the haystack. + * + * @dataProvider dataAssertStringContainsStringIgnoringLineEndingsFails + * + * @param string $needle The string to search for. + * @param string $haystack The string to treat as the haystack. + * + * @return void + */ + public function testAssertStringContainsStringIgnoringLineEndingsFails( $needle, $haystack ) { + $exporter = \class_exists( Exporter::class ) ? new Exporter() : new Exporter_In_Phar(); + $msg = \sprintf( + 'Failed asserting that %s contains "%s".', + $exporter->export( $haystack ), + self::normalizeLineEndings( $needle ) + ); + + $this->expectException( $this->getAssertionFailedExceptionName() ); + $this->expectExceptionMessage( $msg ); + + $this->assertStringContainsStringIgnoringLineEndings( $needle, $haystack ); + } + + /** + * Data provider. + * + * @return array + */ + public static function dataAssertStringContainsStringIgnoringLineEndingsFails() { + return [ + 'not substring' => [ 'a', 'bc' ], + 'no line endings in needle' => [ 'bc', "b\nc" ], + 'no line endings in haystack' => [ "b\nc", 'bc' ], + 'different line endings count 1' => [ "b\nc", "b\n\n\nc" ], + 'different line endings count 2' => [ "b\n\n\nc", "b\nc" ], + ]; + } + + /** + * Data provider. + * + * @return array + */ + public static function dataThrowsTypeErrorOnInvalidType() { + return [ + 'null' => [ null ], + 'array' => [ [ 'a' ] ], + 'object' => [ new stdClass() ], + ]; + } + + /** + * Helper function: retrieve the name of the "assertion failed" exception to expect (PHPUnit cross-version). + * + * @return string + */ + public function getAssertionFailedExceptionName() { + $exception = AssertionFailedError::class; + if ( \class_exists( PHPUnit_Framework_AssertionFailedError::class ) ) { + // PHPUnit < 6. + $exception = PHPUnit_Framework_AssertionFailedError::class; + } + + return $exception; + } + + /** + * Normalize line endings. + * + * @param string $value The text to normalize. + * + * @return string + */ + private static function normalizeLineEndings( $value ) { + return \strtr( + $value, + [ + "\r\n" => "\n", + "\r" => "\n", + ] + ); + } +} diff --git a/tests/TestCases/TestCaseTestTrait.php b/tests/TestCases/TestCaseTestTrait.php index 956ee69..aa07588 100644 --- a/tests/TestCases/TestCaseTestTrait.php +++ b/tests/TestCases/TestCaseTestTrait.php @@ -133,4 +133,13 @@ final public function testAvailabilityAssertObjectEquals() { $actual = new ValueObject( 'test' ); $this->assertObjectEquals( $expected, $actual ); } + + /** + * Verify availability of trait polyfilled PHPUnit methods [15]. + * + * @return void + */ + final public function testAvailabilityAssertIgnoringLineEndings() { + self::assertStringContainsStringIgnoringLineEndings( "b\nc", "a\r\nb\r\nc\r\nd" ); + } }