diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d79a8557 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1b6cf0d0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +tests/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.php_cs export-ignore +.travis.yml export-ignore +build.xml export-ignore +phpunit.xml.dist export-ignore +* text=auto eol=lf +*.php text whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,tabwidth=4 diff=php +*.json text whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,tabwidth=4 +*.yml text whitespace=blank-at-eol,blank-at-eof,space-before-tab,tab-in-indent,tabwidth=4 +*.md text whitespace=blank-at-eol,blank-at-eof diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..10f0353f --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +DO NOT PUBLISH SECURITY REPORTS PUBLICLY. + +If you found any issues that might have security implications, please send a report to dariusz.ruminski+php-cs-fixer[at]gmail.com . + +Report security bugs in third-party libraries directly to the group maintaining that library. diff --git a/.gitignore b/.gitignore index cd879d7b..5cf9a2cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -/.idea /composer.lock -/vendor -/.php_cs.cache -/from.txt.orig \ No newline at end of file +/phpunit.xml +/vendor/ diff --git a/.travis.yml b/.travis.yml index 90cd7269..49e3311b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,34 @@ language: php -php: - - 7.0 - - 7.0snapshot - - 7.1 - - 7.1snapshot - - master +git: + depth: 1 sudo: false -before_install: - - composer self-update - - composer clear-cache +matrix: + fast_finish: true + include: + - php: 5.6 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" + - php: 7.0 + env: COMPOSER_FLAGS="--prefer-stable" + - php: 7.1 + env: COMPOSER_FLAGS="--prefer-stable" + - php: 7.2 + env: COMPOSER_FLAGS="--prefer-stable" + - php: 7.3 + env: COMPOSER_FLAGS="--prefer-stable" + - php: 7.4 + env: COMPOSER_FLAGS="--prefer-stable" + - php: nightly + env: COMPOSER_FLAGS="--dev --ignore-platform-reqs" + allow_failures: + - php: nightly install: - - travis_retry composer update --no-interaction --no-ansi --no-progress --no-suggest --optimize-autoloader --prefer-stable + - travis_retry composer update $COMPOSER_FLAGS --no-interaction -v + - composer info -D | sort script: - - ./vendor/bin/phpunit --coverage-clover=coverage.xml - -after_success: - - bash <(curl -s https://codecov.io/bash) - -notifications: - email: false - + - phpenv config-rm xdebug.ini || return 0 + - ./vendor/bin/phpunit --verbose diff --git a/ChangeLog.md b/ChangeLog.md deleted file mode 100644 index 4f7eca66..00000000 --- a/ChangeLog.md +++ /dev/null @@ -1,22 +0,0 @@ -# ChangeLog - -All notable changes are documented in this file using the [Keep a CHANGELOG](http://keepachangelog.com/) principles. - -## [2.0.1] - 2017-08-03 - -### Fixed - -* Fixed [#66](https://github.com/sebastianbergmann/diff/pull/66): Restored backwards compatibility for PHPUnit 6.1.4, 6.2.0, 6.2.1, 6.2.2, and 6.2.3 - -## [2.0.0] - 2017-07-11 [YANKED] - -### Added - -* Implemented [#64](https://github.com/sebastianbergmann/diff/pull/64): Show line numbers for chunks of a diff - -### Removed - -* This component is no longer supported on PHP 5.6 - -[2.0.1]: https://github.com/sebastianbergmann/diff/compare/c341c98ce083db77f896a0aa64f5ee7652915970...2.0.1 -[2.0.0]: https://github.com/sebastianbergmann/diff/compare/1.4...c341c98ce083db77f896a0aa64f5ee7652915970 diff --git a/LICENSE b/LICENSE index e1ddf136..93cf0088 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,3 @@ -sebastian/diff - Copyright (c) 2002-2017, Sebastian Bergmann . All rights reserved. diff --git a/README.md b/README.md index 956038b4..19cddca7 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,10 @@ -# sebastian/diff +# PHP-CS-Fixer/diff -Diff implementation for PHP, factored out of PHPUnit into a stand-alone component. +This version is for PHP CS Fixer only! Do not use it! -## Installation +Code from `sebastian/diff` has been forked a republished by permission of Sebastian Bergmann. +Main change is to make the package compatible with older PHP engines. +Licenced with BSD-3-Clause @ see LICENSE, copyright (c) Sebastian Bergmann +https://github.com/sebastianbergmann/diff -You can add this library as a local, per-project dependency to your project using [Composer](https://getcomposer.org/): - - composer require sebastian/diff - -If you only need this library during development, for instance to run your project's test suite, then you should add it as a development-time dependency: - - composer require --dev sebastian/diff - -### Usage - -The `Differ` class can be used to generate a textual representation of the difference between two strings: - -```php -use SebastianBergmann\Diff\Differ; - -$differ = new Differ; -print $differ->diff('foo', 'bar'); -``` - -The code above yields the output below: - - --- Original - +++ New - @@ @@ - -foo - +bar - -The `Parser` class can be used to parse a unified diff into an object graph: - -```php -use SebastianBergmann\Diff\Parser; -use SebastianBergmann\Git; - -$git = new Git('/usr/local/src/money'); - -$diff = $git->getDiff( - '948a1a07768d8edd10dcefa8315c1cbeffb31833', - 'c07a373d2399f3e686234c4f7f088d635eb9641b' -); - -$parser = new Parser; - -print_r($parser->parse($diff)); -``` - -The code above yields the output below: - - Array - ( - [0] => SebastianBergmann\Diff\Diff Object - ( - [from:SebastianBergmann\Diff\Diff:private] => a/tests/MoneyTest.php - [to:SebastianBergmann\Diff\Diff:private] => b/tests/MoneyTest.php - [chunks:SebastianBergmann\Diff\Diff:private] => Array - ( - [0] => SebastianBergmann\Diff\Chunk Object - ( - [start:SebastianBergmann\Diff\Chunk:private] => 87 - [startRange:SebastianBergmann\Diff\Chunk:private] => 7 - [end:SebastianBergmann\Diff\Chunk:private] => 87 - [endRange:SebastianBergmann\Diff\Chunk:private] => 7 - [lines:SebastianBergmann\Diff\Chunk:private] => Array - ( - [0] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 3 - [content:SebastianBergmann\Diff\Line:private] => * @covers SebastianBergmann\Money\Money::add - ) - - [1] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 3 - [content:SebastianBergmann\Diff\Line:private] => * @covers SebastianBergmann\Money\Money::newMoney - ) - - [2] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 3 - [content:SebastianBergmann\Diff\Line:private] => */ - ) - - [3] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 2 - [content:SebastianBergmann\Diff\Line:private] => public function testAnotherMoneyWithSameCurrencyObjectCanBeAdded() - ) - - [4] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 1 - [content:SebastianBergmann\Diff\Line:private] => public function testAnotherMoneyObjectWithSameCurrencyCanBeAdded() - ) - - [5] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 3 - [content:SebastianBergmann\Diff\Line:private] => { - ) - - [6] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 3 - [content:SebastianBergmann\Diff\Line:private] => $a = new Money(1, new Currency('EUR')); - ) - - [7] => SebastianBergmann\Diff\Line Object - ( - [type:SebastianBergmann\Diff\Line:private] => 3 - [content:SebastianBergmann\Diff\Line:private] => $b = new Money(2, new Currency('EUR')); - ) - - ) - - ) - - ) - - ) - - ) +For questions visit us @ https://gitter.im/PHP-CS-Fixer/Lobby diff --git a/composer.json b/composer.json index 5aaa7161..910267f0 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { - "name": "sebastian/diff", - "description": "Diff implementation", + "name": "php-cs-fixer/diff", + "description": "sebastian/diff v3 backport support for PHP 5.6+", "keywords": ["diff"], - "homepage": "https://github.com/sebastianbergmann/diff", + "homepage": "https://github.com/PHP-CS-Fixer", "license": "BSD-3-Clause", "authors": [ { @@ -15,19 +15,24 @@ } ], "require": { - "php": "^7.0" + "php": "^5.6 || ^7.0 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", + "symfony/process": "^3.3" + }, + "config": { + "optimize-autoloader": true, + "sort-packages": true }, "autoload": { "classmap": [ "src/" ] }, - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" + "autoload-dev": { + "psr-4": { + "PhpCsFixer\\Diff\\": "tests" } } } diff --git a/phpunit.xml b/phpunit.xml.dist similarity index 100% rename from phpunit.xml rename to phpunit.xml.dist diff --git a/src/Chunk.php b/src/Chunk.php index 53339fce..49c77575 100644 --- a/src/Chunk.php +++ b/src/Chunk.php @@ -1,4 +1,4 @@ -start = $start; $this->startRange = $startRange; @@ -46,33 +46,45 @@ public function __construct(int $start = 0, int $startRange = 1, int $end = 0, i $this->lines = $lines; } - public function getStart(): int + public function getStart() { return $this->start; } - public function getStartRange(): int + public function getStartRange() { return $this->startRange; } - public function getEnd(): int + public function getEnd() { return $this->end; } - public function getEndRange(): int + public function getEndRange() { return $this->endRange; } - public function getLines(): array + /** + * @return Line[] + */ + public function getLines() { return $this->lines; } + /** + * @param Line[] $lines + */ public function setLines(array $lines) { + foreach ($lines as $line) { + if (!$line instanceof Line) { + throw new InvalidArgumentException; + } + } + $this->lines = $lines; } } diff --git a/src/Diff.php b/src/Diff.php index b94fd6f6..a458d274 100644 --- a/src/Diff.php +++ b/src/Diff.php @@ -1,4 +1,4 @@ -from = $from; $this->to = $to; $this->chunks = $chunks; } - public function getFrom(): string + public function getFrom() { return $this->from; } - public function getTo(): string + public function getTo() { return $this->to; } @@ -52,7 +52,7 @@ public function getTo(): string /** * @return Chunk[] */ - public function getChunks(): array + public function getChunks() { return $this->chunks; } diff --git a/src/Differ.php b/src/Differ.php index 6e75b96e..d252e397 100644 --- a/src/Differ.php +++ b/src/Differ.php @@ -1,4 +1,4 @@ -validateDiffInput($from); - $to = $this->validateDiffInput($to); - $diff = $this->diffToArray($from, $to, $lcs); + $diff = $this->diffToArray( + $this->normalizeDiffInput($from), + $this->normalizeDiffInput($to), + $lcs + ); return $this->outputBuilder->getDiff($diff); } - /** - * Casts variable to string if it is not a string or array. - * - * @param mixed $input - * - * @return string - */ - private function validateDiffInput($input): string - { - if (!\is_array($input) && !\is_string($input)) { - return (string) $input; - } - - return $input; - } - /** * Returns the diff between two arrays or strings as array. * @@ -100,18 +92,18 @@ private function validateDiffInput($input): string * * @return array */ - public function diffToArray($from, $to, LongestCommonSubsequenceCalculator $lcs = null): array + public function diffToArray($from, $to, LongestCommonSubsequenceCalculator $lcs = null) { if (\is_string($from)) { $from = $this->splitStringByLines($from); } elseif (!\is_array($from)) { - throw new \InvalidArgumentException('"from" must be an array or string.'); + throw new InvalidArgumentException('"from" must be an array or string.'); } if (\is_string($to)) { $to = $this->splitStringByLines($to); } elseif (!\is_array($to)) { - throw new \InvalidArgumentException('"to" must be an array or string.'); + throw new InvalidArgumentException('"to" must be an array or string.'); } list($from, $to, $start, $end) = self::getArrayDiffParted($from, $to); @@ -124,7 +116,7 @@ public function diffToArray($from, $to, LongestCommonSubsequenceCalculator $lcs $diff = []; foreach ($start as $token) { - $diff[] = [$token, 0 /* OLD */]; + $diff[] = [$token, self::OLD]; } \reset($from); @@ -132,38 +124,54 @@ public function diffToArray($from, $to, LongestCommonSubsequenceCalculator $lcs foreach ($common as $token) { while (($fromToken = \reset($from)) !== $token) { - $diff[] = [\array_shift($from), 2 /* REMOVED */]; + $diff[] = [\array_shift($from), self::REMOVED]; } while (($toToken = \reset($to)) !== $token) { - $diff[] = [\array_shift($to), 1 /* ADDED */]; + $diff[] = [\array_shift($to), self::ADDED]; } - $diff[] = [$token, 0 /* OLD */]; + $diff[] = [$token, self::OLD]; \array_shift($from); \array_shift($to); } while (($token = \array_shift($from)) !== null) { - $diff[] = [$token, 2 /* REMOVED */]; + $diff[] = [$token, self::REMOVED]; } while (($token = \array_shift($to)) !== null) { - $diff[] = [$token, 1 /* ADDED */]; + $diff[] = [$token, self::ADDED]; } foreach ($end as $token) { - $diff[] = [$token, 0 /* OLD */]; + $diff[] = [$token, self::OLD]; } if ($this->detectUnmatchedLineEndings($diff)) { - \array_unshift($diff, ["#Warning: Strings contain different line endings!\n", 3]); + \array_unshift($diff, ["#Warnings contain different line endings!\n", self::DIFF_LINE_END_WARNING]); } return $diff; } + /** + * Casts variable to string if it is not a string or array. + * + * @param mixed $input + * + * @return array|string + */ + private function normalizeDiffInput($input) + { + if (!\is_array($input) && !\is_string($input)) { + return (string) $input; + } + + return $input; + } + /** * Checks if input is string, if so it will split it line-by-line. * @@ -171,7 +179,7 @@ public function diffToArray($from, $to, LongestCommonSubsequenceCalculator $lcs * * @return array */ - private function splitStringByLines(string $input): array + private function splitStringByLines($input) { return \preg_split('/(.*\R)/', $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); } @@ -182,7 +190,7 @@ private function splitStringByLines(string $input): array * * @return LongestCommonSubsequenceCalculator */ - private function selectLcsImplementation(array $from, array $to): LongestCommonSubsequenceCalculator + private function selectLcsImplementation(array $from, array $to) { // We do not want to use the time-efficient implementation if its memory // footprint will probably exceed this value. Note that the footprint @@ -203,7 +211,7 @@ private function selectLcsImplementation(array $from, array $to): LongestCommonS * @param array $from * @param array $to * - * @return int|float + * @return float|int */ private function calculateEstimatedFootprint(array $from, array $to) { @@ -219,19 +227,19 @@ private function calculateEstimatedFootprint(array $from, array $to) * * @return bool */ - private function detectUnmatchedLineEndings(array $diff): bool + private function detectUnmatchedLineEndings(array $diff) { $newLineBreaks = ['' => true]; $oldLineBreaks = ['' => true]; foreach ($diff as $entry) { - if (0 === $entry[1]) { /* OLD */ + if (self::OLD === $entry[1]) { $ln = $this->getLinebreak($entry[0]); $oldLineBreaks[$ln] = true; $newLineBreaks[$ln] = true; - } elseif (1 === $entry[1]) { /* ADDED */ + } elseif (self::ADDED === $entry[1]) { $newLineBreaks[$this->getLinebreak($entry[0])] = true; - } elseif (2 === $entry[1]) { /* REMOVED */ + } elseif (self::REMOVED === $entry[1]) { $oldLineBreaks[$this->getLinebreak($entry[0])] = true; } } @@ -257,7 +265,7 @@ private function detectUnmatchedLineEndings(array $diff): bool return false; } - private function getLinebreak($line): string + private function getLinebreak($line) { if (!\is_string($line)) { return ''; @@ -279,7 +287,7 @@ private function getLinebreak($line): string return "\n"; } - private static function getArrayDiffParted(array &$from, array &$to): array + private static function getArrayDiffParted(array &$from, array &$to) { $start = []; $end = []; diff --git a/src/Exception/ConfigurationException.php b/src/Exception/ConfigurationException.php new file mode 100644 index 00000000..0cd0f889 --- /dev/null +++ b/src/Exception/ConfigurationException.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff; + +final class ConfigurationException extends InvalidArgumentException +{ + /** + * @param string $option + * @param string $expected + * @param mixed $value + * @param int $code + * @param null|\Exception $previous + */ + public function __construct( + $option, + $expected, + $value, + $code = 0, + \Exception $previous = null + ) { + parent::__construct( + \sprintf( + 'Option "%s" must be %s, got "%s".', + $option, + $expected, + \is_object($value) ? \get_class($value) : (null === $value ? '' : \gettype($value) . '#' . $value) + ), + $code, + $previous + ); + } +} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php index 249a2ba0..ba4a6097 100644 --- a/src/Exception/Exception.php +++ b/src/Exception/Exception.php @@ -1,4 +1,4 @@ -type = $type; $this->content = $content; } - public function getContent(): string + public function getContent() { return $this->content; } - public function getType(): int + public function getType() { return $this->type; } diff --git a/src/LongestCommonSubsequenceCalculator.php b/src/LongestCommonSubsequenceCalculator.php index 7eb1707e..d344851c 100644 --- a/src/LongestCommonSubsequenceCalculator.php +++ b/src/LongestCommonSubsequenceCalculator.php @@ -1,4 +1,4 @@ -header = $header; } - public function getDiff(array $diff): string + public function getDiff(array $diff) { $buffer = \fopen('php://memory', 'r+b'); @@ -37,11 +40,11 @@ public function getDiff(array $diff): string } foreach ($diff as $diffEntry) { - if ($diffEntry[1] === 1 /* ADDED */) { + if ($diffEntry[1] === Differ::ADDED) { \fwrite($buffer, '+' . $diffEntry[0]); - } elseif ($diffEntry[1] === 2 /* REMOVED */) { + } elseif ($diffEntry[1] === Differ::REMOVED) { \fwrite($buffer, '-' . $diffEntry[0]); - } elseif ($diffEntry[1] === 3 /* WARNING */) { + } elseif ($diffEntry[1] === Differ::DIFF_LINE_END_WARNING) { \fwrite($buffer, ' ' . $diffEntry[0]); continue; // Warnings should not be tested for line break, it will always be there diff --git a/src/Output/DiffOutputBuilderInterface.php b/src/Output/DiffOutputBuilderInterface.php index 0e18f9f2..d348ed1f 100644 --- a/src/Output/DiffOutputBuilderInterface.php +++ b/src/Output/DiffOutputBuilderInterface.php @@ -1,4 +1,4 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +use PhpCsFixer\Diff\ConfigurationException; +use PhpCsFixer\Diff\Differ; + +/** + * Strict Unified diff output builder. + * + * Generates (strict) Unified diff's (unidiffs) with hunks. + */ +final class StrictUnifiedDiffOutputBuilder implements DiffOutputBuilderInterface +{ + /** + * @var bool + */ + private $changed; + + /** + * @var bool + */ + private $collapseRanges; + + /** + * @var int >= 0 + */ + private $commonLineThreshold; + + /** + * @var string + */ + private $header; + + /** + * @var int >= 0 + */ + private $contextLines; + + private static $default = [ + 'collapseRanges' => true, // ranges of length one are rendered with the trailing `,1` + 'commonLineThreshold' => 6, // number of same lines before ending a new hunk and creating a new one (if needed) + 'contextLines' => 3, // like `diff: -u, -U NUM, --unified[=NUM]`, for patch/git apply compatibility best to keep at least @ 3 + 'fromFile' => null, + 'fromFileDate' => null, + 'toFile' => null, + 'toFileDate' => null, + ]; + + public function __construct(array $options = []) + { + $options = \array_merge(self::$default, $options); + + if (!\is_bool($options['collapseRanges'])) { + throw new ConfigurationException('collapseRanges', 'a bool', $options['collapseRanges']); + } + + if (!\is_int($options['contextLines']) || $options['contextLines'] < 0) { + throw new ConfigurationException('contextLines', 'an int >= 0', $options['contextLines']); + } + + if (!\is_int($options['commonLineThreshold']) || $options['commonLineThreshold'] <= 0) { + throw new ConfigurationException('commonLineThreshold', 'an int > 0', $options['commonLineThreshold']); + } + + foreach (['fromFile', 'toFile'] as $option) { + if (!\is_string($options[$option])) { + throw new ConfigurationException($option, 'a string', $options[$option]); + } + } + + foreach (['fromFileDate', 'toFileDate'] as $option) { + if (null !== $options[$option] && !\is_string($options[$option])) { + throw new ConfigurationException($option, 'a string or ', $options[$option]); + } + } + + $this->header = \sprintf( + "--- %s%s\n+++ %s%s\n", + $options['fromFile'], + null === $options['fromFileDate'] ? '' : "\t" . $options['fromFileDate'], + $options['toFile'], + null === $options['toFileDate'] ? '' : "\t" . $options['toFileDate'] + ); + + $this->collapseRanges = $options['collapseRanges']; + $this->commonLineThreshold = $options['commonLineThreshold']; + $this->contextLines = $options['contextLines']; + } + + public function getDiff(array $diff) + { + if (0 === \count($diff)) { + return ''; + } + + $this->changed = false; + + $buffer = \fopen('php://memory', 'r+b'); + \fwrite($buffer, $this->header); + + $this->writeDiffHunks($buffer, $diff); + + if (!$this->changed) { + \fclose($buffer); + + return ''; + } + + $diff = \stream_get_contents($buffer, -1, 0); + + \fclose($buffer); + + // If the last char is not a linebreak: add it. + // This might happen when both the `from` and `to` do not have a trailing linebreak + $last = \substr($diff, -1); + + return "\n" !== $last && "\r" !== $last + ? $diff . "\n" + : $diff + ; + } + + private function writeDiffHunks($output, array $diff) + { + // detect "No newline at end of file" and insert into `$diff` if needed + + $upperLimit = \count($diff); + + if (0 === $diff[$upperLimit - 1][1]) { + $lc = \substr($diff[$upperLimit - 1][0], -1); + if ("\n" !== $lc) { + \array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); + } + } else { + // search back for the last `+` and `-` line, + // check if has trailing linebreak, else add under it warning under it + $toFind = [1 => true, 2 => true]; + for ($i = $upperLimit - 1; $i >= 0; --$i) { + if (isset($toFind[$diff[$i][1]])) { + unset($toFind[$diff[$i][1]]); + $lc = \substr($diff[$i][0], -1); + if ("\n" !== $lc) { + \array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); + } + + if (!\count($toFind)) { + break; + } + } + } + } + + // write hunks to output buffer + + $cutOff = \max($this->commonLineThreshold, $this->contextLines); + $hunkCapture = false; + $sameCount = $toRange = $fromRange = 0; + $toStart = $fromStart = 1; + + foreach ($diff as $i => $entry) { + if (0 === $entry[1]) { // same + if (false === $hunkCapture) { + ++$fromStart; + ++$toStart; + + continue; + } + + ++$sameCount; + ++$toRange; + ++$fromRange; + + if ($sameCount === $cutOff) { + $contextStartOffset = ($hunkCapture - $this->contextLines) < 0 + ? $hunkCapture + : $this->contextLines + ; + + // note: $contextEndOffset = $this->contextLines; + // + // because we never go beyond the end of the diff. + // with the cutoff/contextlines here the follow is never true; + // + // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) { + // $contextEndOffset = count($diff) - 1; + // } + // + // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop + + $this->writeHunk( + $diff, + $hunkCapture - $contextStartOffset, + $i - $cutOff + $this->contextLines + 1, + $fromStart - $contextStartOffset, + $fromRange - $cutOff + $contextStartOffset + $this->contextLines, + $toStart - $contextStartOffset, + $toRange - $cutOff + $contextStartOffset + $this->contextLines, + $output + ); + + $fromStart += $fromRange; + $toStart += $toRange; + + $hunkCapture = false; + $sameCount = $toRange = $fromRange = 0; + } + + continue; + } + + $sameCount = 0; + + if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) { + continue; + } + + $this->changed = true; + + if (false === $hunkCapture) { + $hunkCapture = $i; + } + + if (Differ::ADDED === $entry[1]) { // added + ++$toRange; + } + + if (Differ::REMOVED === $entry[1]) { // removed + ++$fromRange; + } + } + + if (false === $hunkCapture) { + return; + } + + // we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk, + // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold + + $contextStartOffset = $hunkCapture - $this->contextLines < 0 + ? $hunkCapture + : $this->contextLines + ; + + // prevent trying to write out more common lines than there are in the diff _and_ + // do not write more than configured through the context lines + $contextEndOffset = \min($sameCount, $this->contextLines); + + $fromRange -= $sameCount; + $toRange -= $sameCount; + + $this->writeHunk( + $diff, + $hunkCapture - $contextStartOffset, + $i - $sameCount + $contextEndOffset + 1, + $fromStart - $contextStartOffset, + $fromRange + $contextStartOffset + $contextEndOffset, + $toStart - $contextStartOffset, + $toRange + $contextStartOffset + $contextEndOffset, + $output + ); + } + + private function writeHunk( + array $diff, + $diffStartIndex, + $diffEndIndex, + $fromStart, + $fromRange, + $toStart, + $toRange, + $output + ) { + \fwrite($output, '@@ -' . $fromStart); + + if (!$this->collapseRanges || 1 !== $fromRange) { + \fwrite($output, ',' . $fromRange); + } + + \fwrite($output, ' +' . $toStart); + if (!$this->collapseRanges || 1 !== $toRange) { + \fwrite($output, ',' . $toRange); + } + + \fwrite($output, " @@\n"); + + for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) { + if ($diff[$i][1] === Differ::ADDED) { + $this->changed = true; + \fwrite($output, '+' . $diff[$i][0]); + } elseif ($diff[$i][1] === Differ::REMOVED) { + $this->changed = true; + \fwrite($output, '-' . $diff[$i][0]); + } elseif ($diff[$i][1] === Differ::OLD) { + \fwrite($output, ' ' . $diff[$i][0]); + } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) { + $this->changed = true; + \fwrite($output, $diff[$i][0]); + } + //} elseif ($diff[$i][1] === Differ::DIFF_LINE_END_WARNING) { // custom comment inserted by PHPUnit/diff package + // skip + //} else { + // unknown/invalid + //} + } + } +} diff --git a/src/Output/UnifiedDiffOutputBuilder.php b/src/Output/UnifiedDiffOutputBuilder.php index 211d64eb..13e6636a 100644 --- a/src/Output/UnifiedDiffOutputBuilder.php +++ b/src/Output/UnifiedDiffOutputBuilder.php @@ -1,4 +1,4 @@ -= 0 + */ + private $commonLineThreshold = 6; + + /** + * @var int >= 0 + */ + private $contextLines = 3; + /** * @var string */ @@ -25,13 +42,13 @@ final class UnifiedDiffOutputBuilder extends AbstractChunkOutputBuilder */ private $addLineNumbers; - public function __construct(string $header = "--- Original\n+++ New\n", bool $addLineNumbers = false) + public function __construct($header = "--- Original\n+++ New\n", $addLineNumbers = false) { $this->header = $header; $this->addLineNumbers = $addLineNumbers; } - public function getDiff(array $diff): string + public function getDiff(array $diff) { $buffer = \fopen('php://memory', 'r+b'); @@ -42,84 +59,181 @@ public function getDiff(array $diff): string } } - $this->writeDiffChunked($buffer, $diff, $this->getCommonChunks($diff)); + if (0 !== \count($diff)) { + $this->writeDiffHunks($buffer, $diff); + } $diff = \stream_get_contents($buffer, -1, 0); \fclose($buffer); - return $diff; + // If the diff is non-empty and a linebreak: add it. + // This might happen when both the `from` and `to` do not have a trailing linebreak + $last = \substr($diff, -1); + + return 0 !== \strlen($diff) && "\n" !== $last && "\r" !== $last + ? $diff . "\n" + : $diff + ; } - // `old` is an array with key => value pairs . Each pair represents a start and end index of `diff` - // of a list of elements all containing `same` (0) entries. - private function writeDiffChunked($output, array $diff, array $old) + private function writeDiffHunks($output, array $diff) { + // detect "No newline at end of file" and insert into `$diff` if needed + $upperLimit = \count($diff); - $start = 0; - $fromStart = 0; - $toStart = 0; - if (\count($old)) { // no common parts, list all diff entries - \reset($old); + if (0 === $diff[$upperLimit - 1][1]) { + $lc = \substr($diff[$upperLimit - 1][0], -1); + if ("\n" !== $lc) { + \array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); + } + } else { + // search back for the last `+` and `-` line, + // check if has trailing linebreak, else add under it warning under it + $toFind = [1 => true, 2 => true]; + for ($i = $upperLimit - 1; $i >= 0; --$i) { + if (isset($toFind[$diff[$i][1]])) { + unset($toFind[$diff[$i][1]]); + $lc = \substr($diff[$i][0], -1); + if ("\n" !== $lc) { + \array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]); + } - // iterate the diff, go from chunk to chunk skipping common chunk of lines between those - do { - $commonStart = \key($old); - $commonEnd = \current($old); + if (!\count($toFind)) { + break; + } + } + } + } + + // write hunks to output buffer - if ($commonStart !== $start) { - list($fromRange, $toRange) = $this->getChunkRange($diff, $start, $commonStart); - $this->writeChunk($output, $diff, $start, $commonStart, $fromStart, $fromRange, $toStart, $toRange); + $cutOff = \max($this->commonLineThreshold, $this->contextLines); + $hunkCapture = false; + $sameCount = $toRange = $fromRange = 0; + $toStart = $fromStart = 1; + + foreach ($diff as $i => $entry) { + if (0 === $entry[1]) { // same + if (false === $hunkCapture) { + ++$fromStart; + ++$toStart; + + continue; + } + + ++$sameCount; + ++$toRange; + ++$fromRange; + + if ($sameCount === $cutOff) { + $contextStartOffset = ($hunkCapture - $this->contextLines) < 0 + ? $hunkCapture + : $this->contextLines + ; + + // note: $contextEndOffset = $this->contextLines; + // + // because we never go beyond the end of the diff. + // with the cutoff/contextlines here the follow is never true; + // + // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) { + // $contextEndOffset = count($diff) - 1; + // } + // + // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop + + $this->writeHunk( + $diff, + $hunkCapture - $contextStartOffset, + $i - $cutOff + $this->contextLines + 1, + $fromStart - $contextStartOffset, + $fromRange - $cutOff + $contextStartOffset + $this->contextLines, + $toStart - $contextStartOffset, + $toRange - $cutOff + $contextStartOffset + $this->contextLines, + $output + ); $fromStart += $fromRange; $toStart += $toRange; + + $hunkCapture = false; + $sameCount = $toRange = $fromRange = 0; } - $start = $commonEnd + 1; - $commonLength = $commonEnd - $commonStart + 1; // calculate number of non-change lines in the common part - $fromStart += $commonLength; - $toStart += $commonLength; - } while (false !== \next($old)); - - \end($old); // short cut for finding possible last `change entry` - $tmp = \key($old); - \reset($old); - if ($old[$tmp] === $upperLimit - 1) { - $upperLimit = $tmp; + continue; } - } - if ($start < $upperLimit - 1) { // check for trailing (non) diff entries - do { - --$upperLimit; - } while (isset($diff[$upperLimit][1]) && $diff[$upperLimit][1] === 0); - ++$upperLimit; + $sameCount = 0; - list($fromRange, $toRange) = $this->getChunkRange($diff, $start, $upperLimit); - $this->writeChunk($output, $diff, $start, $upperLimit, $fromStart, $fromRange, $toStart, $toRange); + if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) { + continue; + } + + if (false === $hunkCapture) { + $hunkCapture = $i; + } + + if (Differ::ADDED === $entry[1]) { + ++$toRange; + } + + if (Differ::REMOVED === $entry[1]) { + ++$fromRange; + } + } + + if (false === $hunkCapture) { + return; } + + // we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk, + // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold + + $contextStartOffset = $hunkCapture - $this->contextLines < 0 + ? $hunkCapture + : $this->contextLines + ; + + // prevent trying to write out more common lines than there are in the diff _and_ + // do not write more than configured through the context lines + $contextEndOffset = \min($sameCount, $this->contextLines); + + $fromRange -= $sameCount; + $toRange -= $sameCount; + + $this->writeHunk( + $diff, + $hunkCapture - $contextStartOffset, + $i - $sameCount + $contextEndOffset + 1, + $fromStart - $contextStartOffset, + $fromRange + $contextStartOffset + $contextEndOffset, + $toStart - $contextStartOffset, + $toRange + $contextStartOffset + $contextEndOffset, + $output + ); } - private function writeChunk( - $output, + private function writeHunk( array $diff, - int $diffStartIndex, - int $diffEndIndex, - int $fromStart, - int $fromRange, - int $toStart, - int $toRange + $diffStartIndex, + $diffEndIndex, + $fromStart, + $fromRange, + $toStart, + $toRange, + $output ) { if ($this->addLineNumbers) { - \fwrite($output, '@@ -' . (1 + $fromStart)); + \fwrite($output, '@@ -' . $fromStart); - if ($fromRange > 1) { + if (!$this->collapseRanges || 1 !== $fromRange) { \fwrite($output, ',' . $fromRange); } - \fwrite($output, ' +' . (1 + $toStart)); - if ($toRange > 1) { + \fwrite($output, ' +' . $toStart); + if (!$this->collapseRanges || 1 !== $toRange) { \fwrite($output, ',' . $toRange); } @@ -129,37 +243,17 @@ private function writeChunk( } for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) { - if ($diff[$i][1] === 1 /* ADDED */) { + if ($diff[$i][1] === Differ::ADDED) { \fwrite($output, '+' . $diff[$i][0]); - } elseif ($diff[$i][1] === 2 /* REMOVED */) { + } elseif ($diff[$i][1] === Differ::REMOVED) { \fwrite($output, '-' . $diff[$i][0]); - } else { /* Not changed (old) 0 or Warning 3 */ + } elseif ($diff[$i][1] === Differ::OLD) { + \fwrite($output, ' ' . $diff[$i][0]); + } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) { + \fwrite($output, "\n"); // $diff[$i][0] + } else { /* Not changed (old) Differ::OLD or Warning Differ::DIFF_LINE_END_WARNING */ \fwrite($output, ' ' . $diff[$i][0]); - } - - $lc = \substr($diff[$i][0], -1); - if ($lc !== "\n" && $lc !== "\r") { - \fwrite($output, "\n"); // \No newline at end of file - } - } - } - - private function getChunkRange(array $diff, int $diffStartIndex, int $diffEndIndex): array - { - $toRange = 0; - $fromRange = 0; - - for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) { - if ($diff[$i][1] === 1) { // added - ++$toRange; - } elseif ($diff[$i][1] === 2) { // removed - ++$fromRange; - } elseif ($diff[$i][1] === 0) { // same - ++$fromRange; - ++$toRange; } } - - return [$fromRange, $toRange]; } } diff --git a/src/Parser.php b/src/Parser.php index dbbf6f4c..87afb93f 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -1,4 +1,4 @@ -chunk = new Chunk; } + public function testHasInitiallyNoLines() + { + $this->assertSame([], $this->chunk->getLines()); + } + public function testCanBeCreatedWithoutArguments() { $this->assertInstanceOf(Chunk::class, $this->chunk); @@ -59,10 +66,10 @@ public function testLinesCanBeRetrieved() public function testLinesCanBeSet() { - $this->assertSame([], $this->chunk->getLines()); + $lines = [new Line(Line::ADDED, 'added'), new Line(Line::REMOVED, 'removed')]; + + $this->chunk->setLines($lines); - $testValue = ['line0', 'line1']; - $this->chunk->setLines($testValue); - $this->assertSame($testValue, $this->chunk->getLines()); + $this->assertSame($lines, $this->chunk->getLines()); } } diff --git a/tests/DiffTest.php b/tests/DiffTest.php index fe5cb228..d7a46f28 100644 --- a/tests/DiffTest.php +++ b/tests/DiffTest.php @@ -1,4 +1,4 @@ -assertSame($expected, $this->differ->diff($from, $to, new TimeEfficientLongestCommonSubsequenceCalculator)); } /** * @param array $expected - * @param string|array $from - * @param string|array $to + * @param array|string $from + * @param array|string $to + * * @dataProvider arrayProvider */ public function testArrayRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLcsImplementation(array $expected, $from, $to) @@ -82,60 +73,14 @@ public function testArrayRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLc * @param string $expected * @param string $from * @param string $to + * * @dataProvider textProvider */ - public function testTextRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLcsImplementation(string $expected, string $from, string $to) + public function testTextRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLcsImplementation($expected, $from, $to) { $this->assertSame($expected, $this->differ->diff($from, $to, new MemoryEfficientLongestCommonSubsequenceCalculator)); } - /** - * @param string $expected - * @param string $from - * @param string $to - * @param string $header - * @dataProvider headerProvider - */ - public function testCustomHeaderCanBeUsed(string $expected, string $from, string $to, string $header) - { - $differ = new Differ(new UnifiedDiffOutputBuilder($header)); - - $this->assertSame( - $expected, - $differ->diff($from, $to) - ); - } - - public function headerProvider() - { - return [ - [ - "CUSTOM HEADER\n@@ @@\n-a\n+b\n", - 'a', - 'b', - 'CUSTOM HEADER' - ], - [ - "CUSTOM HEADER\n@@ @@\n-a\n+b\n", - 'a', - 'b', - "CUSTOM HEADER\n" - ], - [ - "CUSTOM HEADER\n\n@@ @@\n-a\n+b\n", - 'a', - 'b', - "CUSTOM HEADER\n\n" - ], - [ - "@@ @@\n-a\n+b\n", - 'a', - 'b', - '' - ], - ]; - } - public function testTypesOtherThanArrayAndStringCanBePassed() { $this->assertSame( @@ -144,137 +89,137 @@ public function testTypesOtherThanArrayAndStringCanBePassed() ); } - /** - * @param string $diff - * @param Diff[] $expected - * @dataProvider diffProvider - */ - public function testParser(string $diff, array $expected) + public function testArrayDiffs() { - $parser = new Parser; - $result = $parser->parse($diff); - - $this->assertEquals($expected, $result); + $this->assertSame( + '--- Original ++++ New +@@ @@ +-one ++two +', + $this->differ->diff(['one'], ['two']) + ); } - public function arrayProvider(): array + public function arrayProvider() { return [ [ [ - ['a', self::REMOVED], - ['b', self::ADDED] + ['a', Differ::REMOVED], + ['b', Differ::ADDED], ], 'a', - 'b' + 'b', ], [ [ - ['ba', self::REMOVED], - ['bc', self::ADDED] + ['ba', Differ::REMOVED], + ['bc', Differ::ADDED], ], 'ba', - 'bc' + 'bc', ], [ [ - ['ab', self::REMOVED], - ['cb', self::ADDED] + ['ab', Differ::REMOVED], + ['cb', Differ::ADDED], ], 'ab', - 'cb' + 'cb', ], [ [ - ['abc', self::REMOVED], - ['adc', self::ADDED] + ['abc', Differ::REMOVED], + ['adc', Differ::ADDED], ], 'abc', - 'adc' + 'adc', ], [ [ - ['ab', self::REMOVED], - ['abc', self::ADDED] + ['ab', Differ::REMOVED], + ['abc', Differ::ADDED], ], 'ab', - 'abc' + 'abc', ], [ [ - ['bc', self::REMOVED], - ['abc', self::ADDED] + ['bc', Differ::REMOVED], + ['abc', Differ::ADDED], ], 'bc', - 'abc' + 'abc', ], [ [ - ['abc', self::REMOVED], - ['abbc', self::ADDED] + ['abc', Differ::REMOVED], + ['abbc', Differ::ADDED], ], 'abc', - 'abbc' + 'abbc', ], [ [ - ['abcdde', self::REMOVED], - ['abcde', self::ADDED] + ['abcdde', Differ::REMOVED], + ['abcde', Differ::ADDED], ], 'abcdde', - 'abcde' + 'abcde', ], 'same start' => [ [ - [17, self::OLD], - ['b', self::REMOVED], - ['d', self::ADDED], + [17, Differ::OLD], + ['b', Differ::REMOVED], + ['d', Differ::ADDED], ], [30 => 17, 'a' => 'b'], [30 => 17, 'c' => 'd'], ], 'same end' => [ [ - [1, self::REMOVED], - [2, self::ADDED], - ['b', self::OLD], + [1, Differ::REMOVED], + [2, Differ::ADDED], + ['b', Differ::OLD], ], [1 => 1, 'a' => 'b'], [1 => 2, 'a' => 'b'], ], 'same start (2), same end (1)' => [ [ - [17, self::OLD], - [2, self::OLD], - [4, self::REMOVED], - ['a', self::ADDED], - [5, self::ADDED], - ['x', self::OLD], + [17, Differ::OLD], + [2, Differ::OLD], + [4, Differ::REMOVED], + ['a', Differ::ADDED], + [5, Differ::ADDED], + ['x', Differ::OLD], ], [30 => 17, 1 => 2, 2 => 4, 'z' => 'x'], [30 => 17, 1 => 2, 3 => 'a', 2 => 5, 'z' => 'x'], ], 'same' => [ [ - ['x', self::OLD], + ['x', Differ::OLD], ], ['z' => 'x'], ['z' => 'x'], ], 'diff' => [ [ - ['y', self::REMOVED], - ['x', self::ADDED], + ['y', Differ::REMOVED], + ['x', Differ::ADDED], ], ['x' => 'y'], ['z' => 'x'], ], 'diff 2' => [ [ - ['y', self::REMOVED], - ['b', self::REMOVED], - ['x', self::ADDED], - ['d', self::ADDED], + ['y', Differ::REMOVED], + ['b', Differ::REMOVED], + ['x', Differ::ADDED], + ['d', Differ::ADDED], ], ['x' => 'y', 'a' => 'b'], ['z' => 'x', 'c' => 'd'], @@ -282,16 +227,16 @@ public function arrayProvider(): array 'test line diff detection' => [ [ [ - "#Warning: Strings contain different line endings!\n", - self::WARNING, + "#Warnings contain different line endings!\n", + Differ::DIFF_LINE_END_WARNING, ], [ " [ [ [ - "#Warning: Strings contain different line endings!\n", - self::WARNING, + "#Warnings contain different line endings!\n", + Differ::DIFF_LINE_END_WARNING, ], [ "assertSame($expected, $differ->diff($from, $to)); - } - - public function textForNoNonDiffLinesProvider(): array - { - return [ - [ - " #Warning: Strings contain different line endings!\n-A\r\n+B\n", - "A\r\n", - "B\n", - ], - [ - "-A\n+B\n", - "\nA", - "\nB" - ], - [ - '', - 'a', - 'a' - ], - [ - "-A\n+C\n", - "A\n\n\nB", - "C\n\n\nB", - ], - [ - "header\n", - 'a', - 'a', - 'header' - ], - [ - "header\n", - 'a', - 'a', - "header\n" - ], - ]; - } - public function testDiffToArrayInvalidFromType() { - $this->expectException('\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessageRegExp('#^"from" must be an array or string\.$#'); $this->differ->diffToArray(null, ''); @@ -485,145 +339,19 @@ public function testDiffToArrayInvalidFromType() public function testDiffInvalidToType() { - $this->expectException('\InvalidArgumentException'); + $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessageRegExp('#^"to" must be an array or string\.$#'); $this->differ->diffToArray('', new \stdClass); } - /** - * @param array $expected - * @param string $from - * @param string $to - * @param int $lineThreshold - * @dataProvider provideGetCommonChunks - */ - public function testGetCommonChunks(array $expected, string $from, string $to, int $lineThreshold = 5) - { - $output = new class extends AbstractChunkOutputBuilder { - public function getDiff(array $diff): string - { - return ''; - } - - public function getChunks(array $diff, $lineThreshold) - { - return $this->getCommonChunks($diff, $lineThreshold); - } - }; - - $this->assertSame( - $expected, - $output->getChunks($this->differ->diffToArray($from, $to), $lineThreshold) - ); - } - - public function provideGetCommonChunks(): array - { - return[ - 'same (with default threshold)' => [ - [], - 'A', - 'A', - ], - 'same (threshold 0)' => [ - [0 => 0], - 'A', - 'A', - 0, - ], - 'empty' => [ - [], - '', - '', - ], - 'single line diff' => [ - [], - 'A', - 'B', - ], - 'below threshold I' => [ - [], - "A\nX\nC", - "A\nB\nC", - ], - 'below threshold II' => [ - [], - "A\n\n\n\nX\nC", - "A\n\n\n\nB\nC", - ], - 'below threshold III' => [ - [0 => 5], - "A\n\n\n\n\n\nB", - "A\n\n\n\n\n\nA", - ], - 'same start' => [ - [0 => 5], - "A\n\n\n\n\n\nX\nC", - "A\n\n\n\n\n\nB\nC", - ], - 'same start long' => [ - [0 => 13], - "\n\n\n\n\n\n\n\n\n\n\n\n\n\nA", - "\n\n\n\n\n\n\n\n\n\n\n\n\n\nB", - ], - 'same part in between' => [ - [2 => 8], - "A\n\n\n\n\n\n\nX\nY\nZ\n\n", - "B\n\n\n\n\n\n\nX\nA\nZ\n\n", - ], - 'same trailing' => [ - [2 => 14], - "A\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "B\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - ], - 'same part in between, same trailing' => [ - [2 => 7, 10 => 15], - "A\n\n\n\n\n\n\nA\n\n\n\n\n\n\n", - "B\n\n\n\n\n\n\nB\n\n\n\n\n\n\n", - ], - 'below custom threshold I' => [ - [], - "A\n\nB", - "A\n\nD", - 2 - ], - 'custom threshold I' => [ - [0 => 1], - "A\n\nB", - "A\n\nD", - 1 - ], - 'custom threshold II' => [ - [], - "A\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "A\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - 19 - ], - [ - [3 => 9], - "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk", - "a\np\nc\nd\ne\nf\ng\nh\ni\nw\nk", - ], - [ - [0 => 5, 8 => 13], - "A\nA\nA\nA\nA\nA\nX\nC\nC\nC\nC\nC\nC", - "A\nA\nA\nA\nA\nA\nB\nC\nC\nC\nC\nC\nC", - ], - [ - [0 => 5, 8 => 13], - "A\nA\nA\nA\nA\nA\nX\nC\nC\nC\nC\nC\nC\nX", - "A\nA\nA\nA\nA\nA\nB\nC\nC\nC\nC\nC\nC\nY", - ], - ]; - } - /** * @param array $expected * @param string $input + * * @dataProvider provideSplitStringByLinesCases */ - public function testSplitStringByLines(array $expected, string $input) + public function testSplitStringByLines(array $expected, $input) { $reflection = new \ReflectionObject($this->differ); $method = $reflection->getMethod('splitStringByLines'); @@ -632,47 +360,47 @@ public function testSplitStringByLines(array $expected, string $input) $this->assertSame($expected, $method->invoke($this->differ, $input)); } - public function provideSplitStringByLinesCases(): array + public function provideSplitStringByLinesCases() { return [ [ [], - '' + '', ], [ ['a'], - 'a' + 'a', ], [ ["a\n"], - "a\n" + "a\n", ], [ ["a\r"], - "a\r" + "a\r", ], [ ["a\r\n"], - "a\r\n" + "a\r\n", ], [ ["\n"], - "\n" + "\n", ], [ ["\r"], - "\r" + "\r", ], [ ["\r\n"], - "\r\n" + "\r\n", ], [ [ "A\n", "B\n", "\n", - "C\n" + "C\n", ], "A\nB\n\nC\n", ], @@ -681,7 +409,7 @@ public function provideSplitStringByLinesCases(): array "A\r\n", "B\n", "\n", - "C\r" + "C\r", ], "A\r\nB\n\nC\r", ], @@ -691,321 +419,13 @@ public function provideSplitStringByLinesCases(): array "A\r\n", "B\n", "\n", - 'C' + 'C', ], "\nA\r\nB\n\nC", ], ]; } - /** - * @param string $expected - * @param string $from - * @param string $to - * @dataProvider provideDiffWithLineNumbers - */ - public function testDiffWithLineNumbers($expected, $from, $to) - { - $differ = new Differ(new UnifiedDiffOutputBuilder("--- Original\n+++ New\n", true)); - $this->assertSame($expected, $differ->diff($from, $to)); - } - - public function provideDiffWithLineNumbers(): array - { - return [ - 'diff line 1 non_patch_compat' => [ - '--- Original -+++ New -@@ -1 +1 @@ --AA -+BA -', - 'AA', - 'BA', - ], - 'diff line +1 non_patch_compat' => [ - '--- Original -+++ New -@@ -1 +1,2 @@ --AZ -+ -+B -', - 'AZ', - "\nB", - ], - 'diff line -1 non_patch_compat' => [ - '--- Original -+++ New -@@ -1,2 +1 @@ -- --AF -+B -', - "\nAF", - 'B', - ], - 'II non_patch_compat' => [ - '--- Original -+++ New -@@ -1,2 +1 @@ -- -- -' - , - "\n\nA\n1", - "A\n1", - ], - 'diff last line II - no trailing linebreak non_patch_compat' => [ - '--- Original -+++ New -@@ -8 +8 @@ --E -+B -', - "A\n\n\n\n\n\n\nE", - "A\n\n\n\n\n\n\nB", - ], - [ - "--- Original\n+++ New\n@@ -1,2 +1 @@\n \n-\n", - "\n\n", - "\n", - ], - 'diff line endings non_patch_compat' => [ - "--- Original\n+++ New\n@@ -1 +1 @@\n #Warning: Strings contain different line endings!\n- [ - '--- Original -+++ New -', - "AT\n", - "AT\n", - ], - [ - '--- Original -+++ New -@@ -1 +1 @@ --b -+a -', - "b\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", - "a\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" - ], - 'diff line @1' => [ - '--- Original -+++ New -@@ -1,2 +1,2 @@ - ' . ' --AG -+B -', - "\nAG\n", - "\nB\n", - ], - 'same multiple lines' => [ - '--- Original -+++ New -@@ -1,3 +1,3 @@ - ' . ' - ' . ' --V -+B -' - - , - "\n\nV\nC213", - "\n\nB\nC213", - ], - 'diff last line I' => [ - '--- Original -+++ New -@@ -8 +8 @@ --E -+B -', - "A\n\n\n\n\n\n\nE\n", - "A\n\n\n\n\n\n\nB\n", - ], - 'diff line middle' => [ - '--- Original -+++ New -@@ -8 +8 @@ --X -+Z -', - "A\n\n\n\n\n\n\nX\n\n\n\n\n\n\nAY", - "A\n\n\n\n\n\n\nZ\n\n\n\n\n\n\nAY", - ], - 'diff last line III' => [ - '--- Original -+++ New -@@ -15 +15 @@ --A -+B -', - "A\n\n\n\n\n\n\nA\n\n\n\n\n\n\nA\n", - "A\n\n\n\n\n\n\nA\n\n\n\n\n\n\nB\n", - ], - [ - '--- Original -+++ New -@@ -1,7 +1,7 @@ - A --B -+B1 - D - E - EE - F --G -+G1 -', - "A\nB\nD\nE\nEE\nF\nG\nH", - "A\nB1\nD\nE\nEE\nF\nG1\nH", - ], - [ - '--- Original -+++ New -@@ -1 +1,2 @@ - Z -+ -@@ -10 +11 @@ --i -+x -', - 'Z -a -b -c -d -e -f -g -h -i -j', - 'Z - -a -b -c -d -e -f -g -h -x -j' - ], - [ - '--- Original -+++ New -@@ -1,5 +1,3 @@ -- --a -+b - A --a -- -+b -', - "\na\nA\na\n\n\nA", - "b\nA\nb\n\nA" - ], - [ - <<assertAttributeInstanceOf( diff --git a/tests/DifferTestTest.php b/tests/DifferTestTest.php deleted file mode 100644 index af60e26d..00000000 --- a/tests/DifferTestTest.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SebastianBergmann\Diff; - -use PHPUnit\Framework\TestCase; - -/** - * @requires OS Linux - */ -final class DifferTestTest extends TestCase -{ - private $fileFrom; - private $filePatch; - - protected function setUp() - { - $dir = \realpath(__DIR__ . '/../') . '/'; - $this->fileFrom = $dir . 'from.txt'; - $this->filePatch = $dir . 'patch.txt'; - } - - /** - * @dataProvider provideDiffWithLineNumbers - */ - public function testTheTestProvideDiffWithLineNumbers($expected, $from, $to) - { - $this->runThisTest($expected, $from, $to); - } - - public function provideDiffWithLineNumbers() - { - require_once __DIR__ . '/DifferTest.php'; - $test = new DifferTest(); - $tests = $test->provideDiffWithLineNumbers(); - - $tests = \array_filter( - $tests, - function ($key) { - return !\is_string($key) || false === \strpos($key, 'non_patch_compat'); - }, - ARRAY_FILTER_USE_KEY - ); - - return $tests; - } - - private function runThisTest(string $expected, string $from, string $to) - { - $expected = \str_replace('--- Original', '--- from.txt', $expected); - $expected = \str_replace('+++ New', '+++ from.txt', $expected); - - @\unlink($this->fileFrom); - @\unlink($this->filePatch); - - $this->assertNotFalse(\file_put_contents($this->fileFrom, $from)); - $this->assertNotFalse(\file_put_contents($this->filePatch, $expected)); - - $command = \sprintf( - 'patch -u --verbose %s < %s', // --posix - \escapeshellarg($this->fileFrom), - \escapeshellarg($this->filePatch) - ); - - \exec($command, $output, $d); - - $this->assertSame(0, $d, \sprintf('%s | %s', $command, \implode("\n", $output))); - - $patched = \file_get_contents($this->fileFrom); - $this->assertSame($patched, $to); - - @\unlink($this->fileFrom . '.orig'); - @\unlink($this->fileFrom); - @\unlink($this->filePatch); - } -} diff --git a/tests/Exception/ConfigurationExceptionTest.php b/tests/Exception/ConfigurationExceptionTest.php new file mode 100644 index 00000000..a60f01a5 --- /dev/null +++ b/tests/Exception/ConfigurationExceptionTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\ConfigurationException + */ +final class ConfigurationExceptionTest extends TestCase +{ + public function testConstructWithDefaults() + { + $e = new ConfigurationException('test', 'A', 'B'); + + $this->assertSame(0, $e->getCode()); + $this->assertNull($e->getPrevious()); + $this->assertSame('Option "test" must be A, got "string#B".', $e->getMessage()); + } + + public function testConstruct() + { + $e = new ConfigurationException( + 'test', + 'integer', + new \SplFileInfo(__FILE__), + 789, + new \BadMethodCallException(__METHOD__) + ); + + $this->assertSame('Option "test" must be integer, got "SplFileInfo".', $e->getMessage()); + } +} diff --git a/tests/Exception/InvalidArgumentExceptionTest.php b/tests/Exception/InvalidArgumentExceptionTest.php new file mode 100644 index 00000000..bb8ce7cc --- /dev/null +++ b/tests/Exception/InvalidArgumentExceptionTest.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\InvalidArgumentException + */ +final class InvalidArgumentExceptionTest extends TestCase +{ + public function testInvalidArgumentException() + { + $previousException = new \LogicException(); + $message = 'test'; + $code = 123; + + $exception = new InvalidArgumentException($message, $code, $previousException); + + $this->assertInstanceOf(Exception::class, $exception); + $this->assertSame($message, $exception->getMessage()); + $this->assertSame($code, $exception->getCode()); + $this->assertSame($previousException, $exception->getPrevious()); + } +} diff --git a/tests/LineTest.php b/tests/LineTest.php index 7239d664..3e52b85e 100644 --- a/tests/LineTest.php +++ b/tests/LineTest.php @@ -1,4 +1,4 @@ -memoryLimit = \ini_get('memory_limit'); - \ini_set('memory_limit', '256M'); + \ini_set('memory_limit', '-1'); $this->implementation = $this->createImplementation(); } - /** - * @return LongestCommonSubsequenceCalculator - */ - abstract protected function createImplementation(); - protected function tearDown() { \ini_set('memory_limit', $this->memoryLimit); @@ -63,7 +58,7 @@ public function testIsStrictComparison() { $from = [ false, 0, 0.0, '', null, [], - true, 1, 1.0, 'foo', ['foo', 'bar'], ['foo' => 'bar'] + true, 1, 1.0, 'foo', ['foo', 'bar'], ['foo' => 'bar'], ]; $to = $from; $common = $this->implementation->calculate($from, $to); @@ -72,7 +67,7 @@ public function testIsStrictComparison() $to = [ false, false, false, false, false, false, - true, true, true, true, true, true + true, true, true, true, true, true, ]; $expected = [ @@ -198,4 +193,9 @@ public function testStrictTypeCalculate() $this->assertInternalType('array', $diff); $this->assertCount(0, $diff); } + + /** + * @return LongestCommonSubsequenceCalculator + */ + abstract protected function createImplementation(); } diff --git a/tests/MemoryEfficientImplementationTest.php b/tests/MemoryEfficientImplementationTest.php index b5f7bbe4..26b18628 100644 --- a/tests/MemoryEfficientImplementationTest.php +++ b/tests/MemoryEfficientImplementationTest.php @@ -1,4 +1,4 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\Differ; + +class TestingAbstractChunkOutputBuilder extends AbstractChunkOutputBuilder { + public function getDiff(array $diff) + { + return ''; + } + + public function getChunks(array $diff, $lineThreshold) + { + return $this->getCommonChunks($diff, $lineThreshold); + } +}; + +/** + * @covers PhpCsFixer\Diff\Output\AbstractChunkOutputBuilder + * + * @uses PhpCsFixer\Diff\Differ + * @uses PhpCsFixer\Diff\Output\UnifiedDiffOutputBuilder + * @uses PhpCsFixer\Diff\TimeEfficientLongestCommonSubsequenceCalculator + */ +final class AbstractChunkOutputBuilderTest extends TestCase +{ + /** + * @param array $expected + * @param string $from + * @param string $to + * @param int $lineThreshold + * + * @dataProvider provideGetCommonChunks + */ + public function testGetCommonChunks(array $expected, $from, $to, $lineThreshold = 5) + { + $output = new TestingAbstractChunkOutputBuilder(); + + $this->assertSame( + $expected, + $output->getChunks((new Differ)->diffToArray($from, $to), $lineThreshold) + ); + } + + public function provideGetCommonChunks() + { + return[ + 'same (with default threshold)' => [ + [], + 'A', + 'A', + ], + 'same (threshold 0)' => [ + [0 => 0], + 'A', + 'A', + 0, + ], + 'empty' => [ + [], + '', + '', + ], + 'single line diff' => [ + [], + 'A', + 'B', + ], + 'below threshold I' => [ + [], + "A\nX\nC", + "A\nB\nC", + ], + 'below threshold II' => [ + [], + "A\n\n\n\nX\nC", + "A\n\n\n\nB\nC", + ], + 'below threshold III' => [ + [0 => 5], + "A\n\n\n\n\n\nB", + "A\n\n\n\n\n\nA", + ], + 'same start' => [ + [0 => 5], + "A\n\n\n\n\n\nX\nC", + "A\n\n\n\n\n\nB\nC", + ], + 'same start long' => [ + [0 => 13], + "\n\n\n\n\n\n\n\n\n\n\n\n\n\nA", + "\n\n\n\n\n\n\n\n\n\n\n\n\n\nB", + ], + 'same part in between' => [ + [2 => 8], + "A\n\n\n\n\n\n\nX\nY\nZ\n\n", + "B\n\n\n\n\n\n\nX\nA\nZ\n\n", + ], + 'same trailing' => [ + [2 => 14], + "A\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "B\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + ], + 'same part in between, same trailing' => [ + [2 => 7, 10 => 15], + "A\n\n\n\n\n\n\nA\n\n\n\n\n\n\n", + "B\n\n\n\n\n\n\nB\n\n\n\n\n\n\n", + ], + 'below custom threshold I' => [ + [], + "A\n\nB", + "A\n\nD", + 2, + ], + 'custom threshold I' => [ + [0 => 1], + "A\n\nB", + "A\n\nD", + 1, + ], + 'custom threshold II' => [ + [], + "A\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "A\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + 19, + ], + [ + [3 => 9], + "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk", + "a\np\nc\nd\ne\nf\ng\nh\ni\nw\nk", + ], + [ + [0 => 5, 8 => 13], + "A\nA\nA\nA\nA\nA\nX\nC\nC\nC\nC\nC\nC", + "A\nA\nA\nA\nA\nA\nB\nC\nC\nC\nC\nC\nC", + ], + [ + [0 => 5, 8 => 13], + "A\nA\nA\nA\nA\nA\nX\nC\nC\nC\nC\nC\nC\nX", + "A\nA\nA\nA\nA\nA\nB\nC\nC\nC\nC\nC\nC\nY", + ], + ]; + } +} diff --git a/tests/Output/DiffOnlyOutputBuilderTest.php b/tests/Output/DiffOnlyOutputBuilderTest.php new file mode 100644 index 00000000..799b8f2d --- /dev/null +++ b/tests/Output/DiffOnlyOutputBuilderTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\Differ; + +/** + * @covers PhpCsFixer\Diff\Output\DiffOnlyOutputBuilder + * + * @uses PhpCsFixer\Diff\Differ + * @uses PhpCsFixer\Diff\TimeEfficientLongestCommonSubsequenceCalculator + */ +final class DiffOnlyOutputBuilderTest extends TestCase +{ + /** + * @param string $expected + * @param string $from + * @param string $to + * @param string $header + * + * @dataProvider textForNoNonDiffLinesProvider + */ + public function testDiffDoNotShowNonDiffLines($expected, $from, $to, $header = '') + { + $differ = new Differ(new DiffOnlyOutputBuilder($header)); + + $this->assertSame($expected, $differ->diff($from, $to)); + } + + public function textForNoNonDiffLinesProvider() + { + return [ + [ + " #Warnings contain different line endings!\n-A\r\n+B\n", + "A\r\n", + "B\n", + ], + [ + "-A\n+B\n", + "\nA", + "\nB", + ], + [ + '', + 'a', + 'a', + ], + [ + "-A\n+C\n", + "A\n\n\nB", + "C\n\n\nB", + ], + [ + "header\n", + 'a', + 'a', + 'header', + ], + [ + "header\n", + 'a', + 'a', + "header\n", + ], + ]; + } +} diff --git a/tests/Output/Integration/StrictUnifiedDiffOutputBuilderIntegrationTest.php b/tests/Output/Integration/StrictUnifiedDiffOutputBuilderIntegrationTest.php new file mode 100644 index 00000000..f39d8fea --- /dev/null +++ b/tests/Output/Integration/StrictUnifiedDiffOutputBuilderIntegrationTest.php @@ -0,0 +1,300 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\Differ; +use PhpCsFixer\Diff\Utils\FileUtils; +use PhpCsFixer\Diff\Utils\UnifiedDiffAssertTrait; +use Symfony\Component\Process\Process; + +/** + * @covers PhpCsFixer\Diff\Output\StrictUnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\Differ + * @uses PhpCsFixer\Diff\TimeEfficientLongestCommonSubsequenceCalculator + * @uses PhpCsFixer\Diff\MemoryEfficientLongestCommonSubsequenceCalculator + * + * @requires OS Linux + */ +final class StrictUnifiedDiffOutputBuilderIntegrationTest extends TestCase +{ + use UnifiedDiffAssertTrait; + + private $dir; + + private $fileFrom; + + private $fileTo; + + private $filePatch; + + protected function setUp() + { + $this->dir = \realpath(__DIR__ . '/../../fixtures/out') . '/'; + $this->fileFrom = $this->dir . 'from.txt'; + $this->fileTo = $this->dir . 'to.txt'; + $this->filePatch = $this->dir . 'diff.patch'; + + if (!\is_dir($this->dir)) { + throw new \RuntimeException('Integration test working directory not found.'); + } + + $this->cleanUpTempFiles(); + } + + protected function tearDown() + { + $this->cleanUpTempFiles(); + } + + /** + * Integration test + * + * - get a file pair + * - create a `diff` between the files + * - test applying the diff using `git apply` + * - test applying the diff using `patch` + * + * @param string $fileFrom + * @param string $fileTo + * + * @dataProvider provideFilePairs + */ + public function testIntegrationUsingPHPFileInVendorGitApply($fileFrom, $fileTo) + { + $from = FileUtils::getFileContent($fileFrom); + $to = FileUtils::getFileContent($fileTo); + + $diff = (new Differ(new StrictUnifiedDiffOutputBuilder(['fromFile' => 'Original', 'toFile' => 'New'])))->diff($from, $to); + + if ('' === $diff && $from === $to) { + // odd case: test after executing as it is more efficient than to read the files and check the contents every time + $this->addToAssertionCount(1); + + return; + } + + $this->doIntegrationTestGitApply($diff, $from, $to); + } + + /** + * Integration test + * + * - get a file pair + * - create a `diff` between the files + * - test applying the diff using `git apply` + * - test applying the diff using `patch` + * + * @param string $fileFrom + * @param string $fileTo + * + * @dataProvider provideFilePairs + */ + public function testIntegrationUsingPHPFileInVendorPatch($fileFrom, $fileTo) + { + $from = FileUtils::getFileContent($fileFrom); + $to = FileUtils::getFileContent($fileTo); + + $diff = (new Differ(new StrictUnifiedDiffOutputBuilder(['fromFile' => 'Original', 'toFile' => 'New'])))->diff($from, $to); + + if ('' === $diff && $from === $to) { + // odd case: test after executing as it is more efficient than to read the files and check the contents every time + $this->addToAssertionCount(1); + + return; + } + + $this->doIntegrationTestPatch($diff, $from, $to); + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * + * @dataProvider provideOutputBuildingCases + * @dataProvider provideSample + * @dataProvider provideBasicDiffGeneration + */ + public function testIntegrationOfUnitTestCasesGitApply($expected, $from, $to) + { + $this->doIntegrationTestGitApply($expected, $from, $to); + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * + * @dataProvider provideOutputBuildingCases + * @dataProvider provideSample + * @dataProvider provideBasicDiffGeneration + */ + public function testIntegrationOfUnitTestCasesPatch($expected, $from, $to) + { + $this->doIntegrationTestPatch($expected, $from, $to); + } + + public function provideOutputBuildingCases() + { + return StrictUnifiedDiffOutputBuilderDataProvider::provideOutputBuildingCases(); + } + + public function provideSample() + { + return StrictUnifiedDiffOutputBuilderDataProvider::provideSample(); + } + + public function provideBasicDiffGeneration() + { + return StrictUnifiedDiffOutputBuilderDataProvider::provideBasicDiffGeneration(); + } + + public function provideFilePairs() + { + $cases = []; + $fromFile = __FILE__; + $vendorDir = \realpath(__DIR__ . '/../../../vendor'); + + $fileIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($vendorDir, \RecursiveDirectoryIterator::SKIP_DOTS)); + + /** @var \SplFileInfo $file */ + foreach ($fileIterator as $file) { + if ('php' !== $file->getExtension()) { + continue; + } + + $toFile = $file->getPathname(); + $cases[\sprintf("Diff file:\n\"%s\"\nvs.\n\"%s\"\n", \realpath($fromFile), \realpath($toFile))] = [$fromFile, $toFile]; + $fromFile = $toFile; + } + + return $cases; + } + + /** + * Compare diff create by builder and against one create by `diff` command. + * + * @param string $diff + * @param string $from + * @param string $to + * + * @dataProvider provideBasicDiffGeneration + */ + public function testIntegrationDiffOutputBuilderVersusDiffCommand($diff, $from, $to) + { + $this->assertNotSame('', $diff); + $this->assertValidUnifiedDiffFormat($diff); + + $this->assertNotFalse(\file_put_contents($this->fileFrom, $from)); + $this->assertNotFalse(\file_put_contents($this->fileTo, $to)); + + $p = new Process(\sprintf('diff -u %s %s', \escapeshellarg($this->fileFrom), \escapeshellarg($this->fileTo))); + $p->run(); + $this->assertSame(1, $p->getExitCode()); // note: Process assumes exit code 0 for `isSuccessful`, however `diff` uses the exit code `1` for success with diff + + $output = $p->getOutput(); + + $diffLines = \preg_split('/(.*\R)/', $diff, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $diffLines[0] = \preg_replace('#^\-\-\- .*#', '--- /' . $this->fileFrom, $diffLines[0], 1); + $diffLines[1] = \preg_replace('#^\+\+\+ .*#', '+++ /' . $this->fileFrom, $diffLines[1], 1); + $diff = \implode('', $diffLines); + + $outputLines = \preg_split('/(.*\R)/', $output, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $outputLines[0] = \preg_replace('#^\-\-\- .*#', '--- /' . $this->fileFrom, $outputLines[0], 1); + $outputLines[1] = \preg_replace('#^\+\+\+ .*#', '+++ /' . $this->fileFrom, $outputLines[1], 1); + $output = \implode('', $outputLines); + + $this->assertSame($diff, $output); + } + + private function doIntegrationTestGitApply($diff, $from, $to) + { + $this->assertNotSame('', $diff); + $this->assertValidUnifiedDiffFormat($diff); + + $diff = self::setDiffFileHeader($diff, $this->fileFrom); + + $this->assertNotFalse(\file_put_contents($this->fileFrom, $from)); + $this->assertNotFalse(\file_put_contents($this->filePatch, $diff)); + + $p = new Process(\sprintf( + 'git --git-dir %s apply --check -v --unsafe-paths --ignore-whitespace %s', + \escapeshellarg($this->dir), + \escapeshellarg($this->filePatch) + )); + + $p->run(); + + $this->assertProcessSuccessful($p); + } + + private function doIntegrationTestPatch($diff, $from, $to) + { + $this->assertNotSame('', $diff); + $this->assertValidUnifiedDiffFormat($diff); + + $diff = self::setDiffFileHeader($diff, $this->fileFrom); + + $this->assertNotFalse(\file_put_contents($this->fileFrom, $from)); + $this->assertNotFalse(\file_put_contents($this->filePatch, $diff)); + + $command = \sprintf( + 'patch -u --verbose --posix %s < %s', + \escapeshellarg($this->fileFrom), + \escapeshellarg($this->filePatch) + ); + + $p = new Process($command); + $p->run(); + + $this->assertProcessSuccessful($p); + + $this->assertStringEqualsFile( + $this->fileFrom, + $to, + \sprintf('Patch command "%s".', $command) + ); + } + + private function assertProcessSuccessful(Process $p) + { + $this->assertTrue( + $p->isSuccessful(), + \sprintf( + "Command exec. was not successful:\n\"%s\"\nOutput:\n\"%s\"\nStdErr:\n\"%s\"\nExit code %d.\n", + $p->getCommandLine(), + $p->getOutput(), + $p->getErrorOutput(), + $p->getExitCode() + ) + ); + } + + private function cleanUpTempFiles() + { + @\unlink($this->fileFrom . '.orig'); + @\unlink($this->fileFrom . '.rej'); + @\unlink($this->fileFrom); + @\unlink($this->fileTo); + @\unlink($this->filePatch); + } + + private static function setDiffFileHeader($diff, $file) + { + $diffLines = \preg_split('/(.*\R)/', $diff, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $diffLines[0] = \preg_replace('#^\-\-\- .*#', '--- /' . $file, $diffLines[0], 1); + $diffLines[1] = \preg_replace('#^\+\+\+ .*#', '+++ /' . $file, $diffLines[1], 1); + + return \implode('', $diffLines); + } +} diff --git a/tests/Output/Integration/UnifiedDiffOutputBuilderIntegrationTest.php b/tests/Output/Integration/UnifiedDiffOutputBuilderIntegrationTest.php new file mode 100644 index 00000000..f9f5a2fe --- /dev/null +++ b/tests/Output/Integration/UnifiedDiffOutputBuilderIntegrationTest.php @@ -0,0 +1,163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\Utils\UnifiedDiffAssertTrait; +use Symfony\Component\Process\Process; + +/** + * @covers PhpCsFixer\Diff\Output\UnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\Differ + * @uses PhpCsFixer\Diff\TimeEfficientLongestCommonSubsequenceCalculator + * + * @requires OS Linux + */ +final class UnifiedDiffOutputBuilderIntegrationTest extends TestCase +{ + use UnifiedDiffAssertTrait; + + private $dir; + + private $fileFrom; + + private $filePatch; + + protected function setUp() + { + $this->dir = \realpath(__DIR__ . '/../../fixtures/out/') . '/'; + $this->fileFrom = $this->dir . 'from.txt'; + $this->filePatch = $this->dir . 'patch.txt'; + + $this->cleanUpTempFiles(); + } + + protected function tearDown() + { + $this->cleanUpTempFiles(); + } + + /** + * @dataProvider provideDiffWithLineNumbers + * + * @param mixed $expected + * @param mixed $from + * @param mixed $to + */ + public function testDiffWithLineNumbersPath($expected, $from, $to) + { + $this->doIntegrationTestPatch($expected, $from, $to); + } + + /** + * @dataProvider provideDiffWithLineNumbers + * + * @param mixed $expected + * @param mixed $from + * @param mixed $to + */ + public function testDiffWithLineNumbersGitApply($expected, $from, $to) + { + $this->doIntegrationTestGitApply($expected, $from, $to); + } + + public function provideDiffWithLineNumbers() + { + return \array_filter( + UnifiedDiffOutputBuilderDataProvider::provideDiffWithLineNumbers(), + static function ($key) { + return !\is_string($key) || false === \strpos($key, 'non_patch_compat'); + }, + ARRAY_FILTER_USE_KEY + ); + } + + private function doIntegrationTestPatch($diff, $from, $to) + { + $this->assertNotSame('', $diff); + $this->assertValidUnifiedDiffFormat($diff); + + $diff = self::setDiffFileHeader($diff, $this->fileFrom); + + $this->assertNotFalse(\file_put_contents($this->fileFrom, $from)); + $this->assertNotFalse(\file_put_contents($this->filePatch, $diff)); + + $command = \sprintf( + 'patch -u --verbose --posix %s < %s', // --posix + \escapeshellarg($this->fileFrom), + \escapeshellarg($this->filePatch) + ); + + $p = new Process($command); + $p->run(); + + $this->assertProcessSuccessful($p); + + $this->assertStringEqualsFile( + $this->fileFrom, + $to, + \sprintf('Patch command "%s".', $command) + ); + } + + private function doIntegrationTestGitApply($diff, $from, $to) + { + $this->assertNotSame('', $diff); + $this->assertValidUnifiedDiffFormat($diff); + + $diff = self::setDiffFileHeader($diff, $this->fileFrom); + + $this->assertNotFalse(\file_put_contents($this->fileFrom, $from)); + $this->assertNotFalse(\file_put_contents($this->filePatch, $diff)); + + $command = \sprintf( + 'git --git-dir %s apply --check -v --unsafe-paths --ignore-whitespace %s', + \escapeshellarg($this->dir), + \escapeshellarg($this->filePatch) + ); + + $p = new Process($command); + $p->run(); + + $this->assertProcessSuccessful($p); + } + + private function assertProcessSuccessful(Process $p) + { + $this->assertTrue( + $p->isSuccessful(), + \sprintf( + "Command exec. was not successful:\n\"%s\"\nOutput:\n\"%s\"\nStdErr:\n\"%s\"\nExit code %d.\n", + $p->getCommandLine(), + $p->getOutput(), + $p->getErrorOutput(), + $p->getExitCode() + ) + ); + } + + private function cleanUpTempFiles() + { + @\unlink($this->fileFrom . '.orig'); + @\unlink($this->fileFrom); + @\unlink($this->filePatch); + } + + private static function setDiffFileHeader($diff, $file) + { + $diffLines = \preg_split('/(.*\R)/', $diff, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $diffLines[0] = \preg_replace('#^\-\-\- .*#', '--- /' . $file, $diffLines[0], 1); + $diffLines[1] = \preg_replace('#^\+\+\+ .*#', '+++ /' . $file, $diffLines[1], 1); + + return \implode('', $diffLines); + } +} diff --git a/tests/Output/StrictUnifiedDiffOutputBuilderDataProvider.php b/tests/Output/StrictUnifiedDiffOutputBuilderDataProvider.php new file mode 100644 index 00000000..e2431a5c --- /dev/null +++ b/tests/Output/StrictUnifiedDiffOutputBuilderDataProvider.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +final class StrictUnifiedDiffOutputBuilderDataProvider +{ + public static function provideOutputBuildingCases() + { + return [ + [ +'--- input.txt ++++ output.txt +@@ -1,3 +1,4 @@ ++b + ' . ' + ' . ' + ' . ' +@@ -16,5 +17,4 @@ + ' . ' + ' . ' + ' . ' +- +-B ++A +', + "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nB\n", + "b\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nA\n", + [ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ], + ], + [ +'--- ' . __FILE__ . "\t2017-10-02 17:38:11.586413675 +0100 ++++ output1.txt\t2017-10-03 12:09:43.086719482 +0100 +@@ -1,1 +1,1 @@ +-B ++X +", + "B\n", + "X\n", + [ + 'fromFile' => __FILE__, + 'fromFileDate' => '2017-10-02 17:38:11.586413675 +0100', + 'toFile' => 'output1.txt', + 'toFileDate' => '2017-10-03 12:09:43.086719482 +0100', + 'collapseRanges' => false, + ], + ], + [ +'--- input.txt ++++ output.txt +@@ -1 +1 @@ +-B ++X +', + "B\n", + "X\n", + [ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + 'collapseRanges' => true, + ], + ], + ]; + } + + public static function provideSample() + { + return [ + [ +'--- input.txt ++++ output.txt +@@ -1,6 +1,6 @@ + 1 + 2 + 3 +-4 ++X + 5 + 6 +', + "1\n2\n3\n4\n5\n6\n", + "1\n2\n3\nX\n5\n6\n", + [ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ], + ], + ]; + } + + public static function provideBasicDiffGeneration() + { + return [ + [ +"--- input.txt ++++ output.txt +@@ -1,2 +1 @@ +-A +-B ++A\rB +", + "A\nB\n", + "A\rB\n", + ], + [ +"--- input.txt ++++ output.txt +@@ -1 +1 @@ +- ++\r +\\ No newline at end of file +", + "\n", + "\r", + ], + [ +"--- input.txt ++++ output.txt +@@ -1 +1 @@ +-\r +\\ No newline at end of file ++ +", + "\r", + "\n", + ], + [ +'--- input.txt ++++ output.txt +@@ -1,3 +1,3 @@ + X + A +-A ++B +', + "X\nA\nA\n", + "X\nA\nB\n", + ], + [ +'--- input.txt ++++ output.txt +@@ -1,3 +1,3 @@ + X + A +-A +\ No newline at end of file ++B +', + "X\nA\nA", + "X\nA\nB\n", + ], + [ +'--- input.txt ++++ output.txt +@@ -1,3 +1,3 @@ + A + A +-A ++B +\ No newline at end of file +', + "A\nA\nA\n", + "A\nA\nB", + ], + [ +'--- input.txt ++++ output.txt +@@ -1 +1 @@ +-A +\ No newline at end of file ++B +\ No newline at end of file +', + 'A', + 'B', + ], + ]; + } +} diff --git a/tests/Output/StrictUnifiedDiffOutputBuilderTest.php b/tests/Output/StrictUnifiedDiffOutputBuilderTest.php new file mode 100644 index 00000000..17d76ff5 --- /dev/null +++ b/tests/Output/StrictUnifiedDiffOutputBuilderTest.php @@ -0,0 +1,715 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\ConfigurationException; +use PhpCsFixer\Diff\Differ; +use PhpCsFixer\Diff\Utils\UnifiedDiffAssertTrait; + +/** + * @covers PhpCsFixer\Diff\Output\StrictUnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\Differ + * @uses PhpCsFixer\Diff\TimeEfficientLongestCommonSubsequenceCalculator + * @uses PhpCsFixer\Diff\ConfigurationException + */ +final class StrictUnifiedDiffOutputBuilderTest extends TestCase +{ + use UnifiedDiffAssertTrait; + + /** + * @param string $expected + * @param string $from + * @param string $to + * @param array $options + * + * @dataProvider provideOutputBuildingCases + */ + public function testOutputBuilding($expected, $from, $to, array $options) + { + $diff = $this->getDiffer($options)->diff($from, $to); + + $this->assertValidDiffFormat($diff); + $this->assertSame($expected, $diff); + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * @param array $options + * + * @dataProvider provideSample + */ + public function testSample($expected, $from, $to, array $options) + { + $diff = $this->getDiffer($options)->diff($from, $to); + + $this->assertValidDiffFormat($diff); + $this->assertSame($expected, $diff); + } + + /** + * {@inheritdoc} + */ + public function assertValidDiffFormat($diff) + { + $this->assertValidUnifiedDiffFormat($diff); + } + + /** + * {@inheritdoc} + */ + public function provideOutputBuildingCases() + { + return StrictUnifiedDiffOutputBuilderDataProvider::provideOutputBuildingCases(); + } + + /** + * {@inheritdoc} + */ + public function provideSample() + { + return StrictUnifiedDiffOutputBuilderDataProvider::provideSample(); + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * + * @dataProvider provideBasicDiffGeneration + */ + public function testBasicDiffGeneration($expected, $from, $to) + { + $diff = $this->getDiffer([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ])->diff($from, $to); + + $this->assertValidDiffFormat($diff); + $this->assertSame($expected, $diff); + } + + public function provideBasicDiffGeneration() + { + return StrictUnifiedDiffOutputBuilderDataProvider::provideBasicDiffGeneration(); + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * @param array $config + * + * @dataProvider provideConfiguredDiffGeneration + */ + public function testConfiguredDiffGeneration($expected, $from, $to, array $config = []) + { + $diff = $this->getDiffer(\array_merge([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ], $config))->diff($from, $to); + + $this->assertValidDiffFormat($diff); + $this->assertSame($expected, $diff); + } + + public function provideConfiguredDiffGeneration() + { + return [ + [ + '--- input.txt ++++ output.txt +@@ -1 +1 @@ +-a +\ No newline at end of file ++b +\ No newline at end of file +', + 'a', + 'b', + ], + [ + '', + "1\n2", + "1\n2", + ], + [ + '', + "1\n", + "1\n", + ], + [ +'--- input.txt ++++ output.txt +@@ -4 +4 @@ +-X ++4 +', + "1\n2\n3\nX\n5\n6\n7\n8\n9\n0\n", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", + [ + 'contextLines' => 0, + ], + ], + [ +'--- input.txt ++++ output.txt +@@ -3,3 +3,3 @@ + 3 +-X ++4 + 5 +', + "1\n2\n3\nX\n5\n6\n7\n8\n9\n0\n", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", + [ + 'contextLines' => 1, + ], + ], + [ +'--- input.txt ++++ output.txt +@@ -1,10 +1,10 @@ + 1 + 2 + 3 +-X ++4 + 5 + 6 + 7 + 8 + 9 + 0 +', + "1\n2\n3\nX\n5\n6\n7\n8\n9\n0\n", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", + [ + 'contextLines' => 999, + ], + ], + [ +'--- input.txt ++++ output.txt +@@ -1,0 +1,2 @@ ++ ++A +', + '', + "\nA\n", + ], + [ +'--- input.txt ++++ output.txt +@@ -1,2 +1,0 @@ +- +-A +', + "\nA\n", + '', + ], + [ + '--- input.txt ++++ output.txt +@@ -1,5 +1,5 @@ + 1 +-X ++2 + 3 +-Y ++4 + 5 +@@ -8,3 +8,3 @@ + 8 +-X ++9 + 0 +', + "1\nX\n3\nY\n5\n6\n7\n8\nX\n0\n", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", + [ + 'commonLineThreshold' => 2, + 'contextLines' => 1, + ], + ], + [ + '--- input.txt ++++ output.txt +@@ -2 +2 @@ +-X ++2 +@@ -4 +4 @@ +-Y ++4 +@@ -9 +9 @@ +-X ++9 +', + "1\nX\n3\nY\n5\n6\n7\n8\nX\n0\n", + "1\n2\n3\n4\n5\n6\n7\n8\n9\n0\n", + [ + 'commonLineThreshold' => 1, + 'contextLines' => 0, + ], + ], + ]; + } + + public function testReUseBuilder() + { + $differ = $this->getDiffer([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ]); + + $diff = $differ->diff("A\nB\n", "A\nX\n"); + $this->assertSame( +'--- input.txt ++++ output.txt +@@ -1,2 +1,2 @@ + A +-B ++X +', + $diff + ); + + $diff = $differ->diff("A\n", "A\n"); + $this->assertSame( + '', + $diff + ); + } + + public function testEmptyDiff() + { + $builder = new StrictUnifiedDiffOutputBuilder([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ]); + + $this->assertSame( + '', + $builder->getDiff([]) + ); + } + + /** + * @param string $from + * @param string $to + * + * @dataProvider provideSameEmptyDiff + */ + public function testSameEmptyDiff($from, $to) + { + $builder = new StrictUnifiedDiffOutputBuilder([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ]); + + $differ = new Differ($builder); + + $this->assertSame( + '', + $differ->diff($from, $to) + ); + } + + public function provideSameEmptyDiff() + { + return [ + ['', ''], + ['a', 'a'], + ]; + } + + /** + * @param array $options + * @param string $message + * + * @dataProvider provideInvalidConfiguration + */ + public function testInvalidConfiguration(array $options, $message) + { + $this->expectException(ConfigurationException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote($message, '#'))); + + new StrictUnifiedDiffOutputBuilder($options); + } + + public function provideInvalidConfiguration() + { + $time = \time(); + + return [ + [ + ['collapseRanges' => 1], + 'Option "collapseRanges" must be a bool, got "integer#1".', + ], + [ + ['contextLines' => 'a'], + 'Option "contextLines" must be an int >= 0, got "string#a".', + ], + [ + ['commonLineThreshold' => -2], + 'Option "commonLineThreshold" must be an int > 0, got "integer#-2".', + ], + [ + ['commonLineThreshold' => 0], + 'Option "commonLineThreshold" must be an int > 0, got "integer#0".', + ], + [ + ['fromFile' => new \SplFileInfo(__FILE__)], + 'Option "fromFile" must be a string, got "SplFileInfo".', + ], + [ + ['fromFile' => null], + 'Option "fromFile" must be a string, got "".', + ], + [ + [ + 'fromFile' => __FILE__, + 'toFile' => 1, + ], + 'Option "toFile" must be a string, got "integer#1".', + ], + [ + [ + 'fromFile' => __FILE__, + 'toFile' => __FILE__, + 'toFileDate' => $time, + ], + 'Option "toFileDate" must be a string or , got "integer#' . $time . '".', + ], + [ + [], + 'Option "fromFile" must be a string, got "".', + ], + ]; + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * @param int $threshold + * + * @dataProvider provideCommonLineThresholdCases + */ + public function testCommonLineThreshold($expected, $from, $to, $threshold) + { + $diff = $this->getDiffer([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + 'commonLineThreshold' => $threshold, + 'contextLines' => 0, + ])->diff($from, $to); + + $this->assertValidDiffFormat($diff); + $this->assertSame($expected, $diff); + } + + public function provideCommonLineThresholdCases() + { + return [ + [ +'--- input.txt ++++ output.txt +@@ -2,3 +2,3 @@ +-X ++B + C12 +-Y ++D +@@ -7 +7 @@ +-X ++Z +', + "A\nX\nC12\nY\nA\nA\nX\n", + "A\nB\nC12\nD\nA\nA\nZ\n", + 2, + ], + [ +'--- input.txt ++++ output.txt +@@ -2 +2 @@ +-X ++B +@@ -4 +4 @@ +-Y ++D +', + "A\nX\nV\nY\n", + "A\nB\nV\nD\n", + 1, + ], + ]; + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * @param int $contextLines + * @param int $commonLineThreshold + * + * @dataProvider provideContextLineConfigurationCases + */ + public function testContextLineConfiguration($expected, $from, $to, $contextLines, $commonLineThreshold = 6) + { + $diff = $this->getDiffer([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + 'contextLines' => $contextLines, + 'commonLineThreshold' => $commonLineThreshold, + ])->diff($from, $to); + + $this->assertValidDiffFormat($diff); + $this->assertSame($expected, $diff); + } + + public function provideContextLineConfigurationCases() + { + $from = "A\nB\nC\nD\nE\nF\nX\nG\nH\nI\nJ\nK\nL\nM\n"; + $to = "A\nB\nC\nD\nE\nF\nY\nG\nH\nI\nJ\nK\nL\nM\n"; + + return [ + 'EOF 0' => [ + "--- input.txt\n+++ output.txt\n@@ -3 +3 @@ +-X +\\ No newline at end of file ++Y +\\ No newline at end of file +", + "A\nB\nX", + "A\nB\nY", + 0, + ], + 'EOF 1' => [ + "--- input.txt\n+++ output.txt\n@@ -2,2 +2,2 @@ + B +-X +\\ No newline at end of file ++Y +\\ No newline at end of file +", + "A\nB\nX", + "A\nB\nY", + 1, +], + 'EOF 2' => [ + "--- input.txt\n+++ output.txt\n@@ -1,3 +1,3 @@ + A + B +-X +\\ No newline at end of file ++Y +\\ No newline at end of file +", + "A\nB\nX", + "A\nB\nY", + 2, + ], + 'EOF 200' => [ + "--- input.txt\n+++ output.txt\n@@ -1,3 +1,3 @@ + A + B +-X +\\ No newline at end of file ++Y +\\ No newline at end of file +", + "A\nB\nX", + "A\nB\nY", + 200, + ], + 'n/a 0' => [ + "--- input.txt\n+++ output.txt\n@@ -7 +7 @@\n-X\n+Y\n", + $from, + $to, + 0, + ], + 'G' => [ + "--- input.txt\n+++ output.txt\n@@ -6,3 +6,3 @@\n F\n-X\n+Y\n G\n", + $from, + $to, + 1, + ], + 'H' => [ + "--- input.txt\n+++ output.txt\n@@ -5,5 +5,5 @@\n E\n F\n-X\n+Y\n G\n H\n", + $from, + $to, + 2, + ], + 'I' => [ + "--- input.txt\n+++ output.txt\n@@ -4,7 +4,7 @@\n D\n E\n F\n-X\n+Y\n G\n H\n I\n", + $from, + $to, + 3, + ], + 'J' => [ + "--- input.txt\n+++ output.txt\n@@ -3,9 +3,9 @@\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n", + $from, + $to, + 4, + ], + 'K' => [ + "--- input.txt\n+++ output.txt\n@@ -2,11 +2,11 @@\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n", + $from, + $to, + 5, + ], + 'L' => [ + "--- input.txt\n+++ output.txt\n@@ -1,13 +1,13 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n", + $from, + $to, + 6, + ], + 'M' => [ + "--- input.txt\n+++ output.txt\n@@ -1,14 +1,14 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n M\n", + $from, + $to, + 7, + ], + 'M no linebreak EOF .1' => [ + "--- input.txt\n+++ output.txt\n@@ -1,14 +1,14 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n-M\n+M\n\\ No newline at end of file\n", + $from, + \substr($to, 0, -1), + 7, + ], + 'M no linebreak EOF .2' => [ + "--- input.txt\n+++ output.txt\n@@ -1,14 +1,14 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n-M\n\\ No newline at end of file\n+M\n", + \substr($from, 0, -1), + $to, + 7, + ], + 'M no linebreak EOF .3' => [ + "--- input.txt\n+++ output.txt\n@@ -1,14 +1,14 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n M\n", + \substr($from, 0, -1), + \substr($to, 0, -1), + 7, + ], + 'M no linebreak EOF .4' => [ + "--- input.txt\n+++ output.txt\n@@ -1,14 +1,14 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n M\n\\ No newline at end of file\n", + \substr($from, 0, -1), + \substr($to, 0, -1), + 10000, + 10000, + ], + 'M+1' => [ + "--- input.txt\n+++ output.txt\n@@ -1,14 +1,14 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n M\n", + $from, + $to, + 8, + ], + 'M+100' => [ + "--- input.txt\n+++ output.txt\n@@ -1,14 +1,14 @@\n A\n B\n C\n D\n E\n F\n-X\n+Y\n G\n H\n I\n J\n K\n L\n M\n", + $from, + $to, + 107, + ], + '0 II' => [ + "--- input.txt\n+++ output.txt\n@@ -12 +12 @@\n-X\n+Y\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nX\nM\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nY\nM\n", + 0, + 999, + ], + '0\' II' => [ + "--- input.txt\n+++ output.txt\n@@ -12 +12 @@\n-X\n+Y\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nX\nM\nA\nA\nA\nA\nA\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nY\nM\nA\nA\nA\nA\nA\n", + 0, + 999, + ], + '0\'\' II' => [ + "--- input.txt\n+++ output.txt\n@@ -12,2 +12,2 @@\n-X\n-M\n\\ No newline at end of file\n+Y\n+M\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nX\nM", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nY\nM\n", + 0, + ], + '0\'\'\' II' => [ + "--- input.txt\n+++ output.txt\n@@ -12,2 +12,2 @@\n-X\n-X1\n+Y\n+Y2\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nX\nX1\nM\nA\nA\nA\nA\nA\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nY\nY2\nM\nA\nA\nA\nA\nA\n", + 0, + 999, + ], + '1 II' => [ + "--- input.txt\n+++ output.txt\n@@ -11,3 +11,3 @@\n K\n-X\n+Y\n M\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nX\nM\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nY\nM\n", + 1, + ], + '5 II' => [ + "--- input.txt\n+++ output.txt\n@@ -7,7 +7,7 @@\n G\n H\n I\n J\n K\n-X\n+Y\n M\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nX\nM\n", + "A\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nY\nM\n", + 5, + ], + [ + '--- input.txt ++++ output.txt +@@ -1,28 +1,28 @@ + A +-X ++B + V +-Y ++D + 1 + A + 2 + A + 3 + A + 4 + A + 8 + A + 9 + A + 5 + A + A + A + A + A + A + A + A + A + A + A +', + "A\nX\nV\nY\n1\nA\n2\nA\n3\nA\n4\nA\n8\nA\n9\nA\n5\nA\nA\nA\nA\nA\nA\nA\nA\nA\nA\nA\n", + "A\nB\nV\nD\n1\nA\n2\nA\n3\nA\n4\nA\n8\nA\n9\nA\n5\nA\nA\nA\nA\nA\nA\nA\nA\nA\nA\nA\n", + 9999, + 99999, + ], + ]; + } + + /** + * Returns a new instance of a Differ with a new instance of the class (DiffOutputBuilderInterface) under test. + * + * @param array $options + * + * @return Differ + */ + private function getDiffer(array $options = []) + { + return new Differ(new StrictUnifiedDiffOutputBuilder($options)); + } +} diff --git a/tests/Output/UnifiedDiffOutputBuilderDataProvider.php b/tests/Output/UnifiedDiffOutputBuilderDataProvider.php new file mode 100644 index 00000000..8feb2659 --- /dev/null +++ b/tests/Output/UnifiedDiffOutputBuilderDataProvider.php @@ -0,0 +1,396 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +final class UnifiedDiffOutputBuilderDataProvider +{ + public static function provideDiffWithLineNumbers() + { + return [ + 'diff line 1 non_patch_compat' => [ +'--- Original ++++ New +@@ -1 +1 @@ +-AA ++BA +', + 'AA', + 'BA', + ], + 'diff line +1 non_patch_compat' => [ +'--- Original ++++ New +@@ -1 +1,2 @@ +-AZ ++ ++B +', + 'AZ', + "\nB", + ], + 'diff line -1 non_patch_compat' => [ +'--- Original ++++ New +@@ -1,2 +1 @@ +- +-AF ++B +', + "\nAF", + 'B', + ], + 'II non_patch_compat' => [ +'--- Original ++++ New +@@ -1,4 +1,2 @@ +- +- + A + 1 +', + "\n\nA\n1", + "A\n1", + ], + 'diff last line II - no trailing linebreak non_patch_compat' => [ +'--- Original ++++ New +@@ -5,4 +5,4 @@ + ' . ' + ' . ' + ' . ' +-E ++B +', + "A\n\n\n\n\n\n\nE", + "A\n\n\n\n\n\n\nB", + ], + [ + "--- Original\n+++ New\n@@ -1,2 +1 @@\n \n-\n", + "\n\n", + "\n", + ], + 'diff line endings non_patch_compat' => [ + "--- Original\n+++ New\n@@ -1 +1 @@\n #Warnings contain different line endings!\n- [ +'--- Original ++++ New +', + "AT\n", + "AT\n", + ], + [ +'--- Original ++++ New +@@ -1,4 +1,4 @@ +-b ++a + ' . ' + ' . ' + ' . ' +', + "b\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + "a\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n", + ], + 'diff line @1' => [ +'--- Original ++++ New +@@ -1,2 +1,2 @@ + ' . ' +-AG ++B +', + "\nAG\n", + "\nB\n", + ], + 'same multiple lines' => [ +'--- Original ++++ New +@@ -1,4 +1,4 @@ + ' . ' + ' . ' +-V ++B + C213 +', + "\n\nV\nC213", + "\n\nB\nC213", + ], + 'diff last line I' => [ +'--- Original ++++ New +@@ -5,4 +5,4 @@ + ' . ' + ' . ' + ' . ' +-E ++B +', + "A\n\n\n\n\n\n\nE\n", + "A\n\n\n\n\n\n\nB\n", + ], + 'diff line middle' => [ +'--- Original ++++ New +@@ -5,7 +5,7 @@ + ' . ' + ' . ' + ' . ' +-X ++Z + ' . ' + ' . ' + ' . ' +', + "A\n\n\n\n\n\n\nX\n\n\n\n\n\n\nAY", + "A\n\n\n\n\n\n\nZ\n\n\n\n\n\n\nAY", + ], + 'diff last line III' => [ +'--- Original ++++ New +@@ -12,4 +12,4 @@ + ' . ' + ' . ' + ' . ' +-A ++B +', + "A\n\n\n\n\n\n\nA\n\n\n\n\n\n\nA\n", + "A\n\n\n\n\n\n\nA\n\n\n\n\n\n\nB\n", + ], + [ +'--- Original ++++ New +@@ -1,8 +1,8 @@ + A +-B ++B1 + D + E + EE + F +-G ++G1 + H +', + "A\nB\nD\nE\nEE\nF\nG\nH", + "A\nB1\nD\nE\nEE\nF\nG1\nH", + ], + [ +'--- Original ++++ New +@@ -1,4 +1,5 @@ + Z ++ + a + b + c +@@ -7,5 +8,5 @@ + f + g + h +-i ++x + j +', +'Z +a +b +c +d +e +f +g +h +i +j +', +'Z + +a +b +c +d +e +f +g +h +x +j +', + ], + [ +'--- Original ++++ New +@@ -1,7 +1,5 @@ +- +-a ++b + A +-X +- ++Y + ' . ' + A +', + "\na\nA\nX\n\n\nA\n", + "b\nA\nY\n\nA\n", + ], + [ +<< + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\Differ; + +/** + * @covers PhpCsFixer\Diff\Output\UnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\Differ + * @uses PhpCsFixer\Diff\Output\AbstractChunkOutputBuilder + * @uses PhpCsFixer\Diff\TimeEfficientLongestCommonSubsequenceCalculator + */ +final class UnifiedDiffOutputBuilderTest extends TestCase +{ + /** + * @param string $expected + * @param string $from + * @param string $to + * @param string $header + * + * @dataProvider headerProvider + */ + public function testCustomHeaderCanBeUsed($expected, $from, $to, $header) + { + $differ = new Differ(new UnifiedDiffOutputBuilder($header)); + + $this->assertSame( + $expected, + $differ->diff($from, $to) + ); + } + + public function headerProvider() + { + return [ + [ + "CUSTOM HEADER\n@@ @@\n-a\n+b\n", + 'a', + 'b', + 'CUSTOM HEADER', + ], + [ + "CUSTOM HEADER\n@@ @@\n-a\n+b\n", + 'a', + 'b', + "CUSTOM HEADER\n", + ], + [ + "CUSTOM HEADER\n\n@@ @@\n-a\n+b\n", + 'a', + 'b', + "CUSTOM HEADER\n\n", + ], + [ + "@@ @@\n-a\n+b\n", + 'a', + 'b', + '', + ], + ]; + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * + * @dataProvider provideDiffWithLineNumbers + */ + public function testDiffWithLineNumbers($expected, $from, $to) + { + $differ = new Differ(new UnifiedDiffOutputBuilder("--- Original\n+++ New\n", true)); + $this->assertSame($expected, $differ->diff($from, $to)); + } + + public function provideDiffWithLineNumbers() + { + return UnifiedDiffOutputBuilderDataProvider::provideDiffWithLineNumbers(); + } + + /** + * @param string $from + * @param string $to + * + * @dataProvider provideStringsThatAreTheSame + */ + public function testEmptyDiffProducesEmptyOutput($from, $to) + { + $differ = new Differ(new UnifiedDiffOutputBuilder('', false)); + $output = $differ->diff($from, $to); + $this->assertEmpty($output); + } + + public function provideStringsThatAreTheSame() + { + return [ + ['', ''], + ['a', 'a'], + ['these strings are the same', 'these strings are the same'], + ["\n", "\n"], + ["multi-line strings\nare the same", "multi-line strings\nare the same"] + ]; + } +} diff --git a/tests/ParserTest.php b/tests/ParserTest.php index a3667e31..1bbe5968 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1,4 +1,4 @@ -parser->parse($content); - $this->assertInternalType('array', $diffs); $this->assertContainsOnlyInstancesOf(Diff::class, $diffs); $this->assertCount(1, $diffs); $chunks = $diffs[0]->getChunks(); - $this->assertInternalType('array', $chunks); $this->assertContainsOnlyInstancesOf(Chunk::class, $chunks); $this->assertCount(1, $chunks); @@ -54,7 +53,7 @@ public function testParse() public function testParseWithMultipleChunks() { - $content = \file_get_contents(__DIR__ . '/fixtures/patch2.txt'); + $content = FileUtils::getFileContent(__DIR__ . '/fixtures/patch2.txt'); $diffs = $this->parser->parse($content); @@ -74,7 +73,7 @@ public function testParseWithMultipleChunks() public function testParseWithRemovedLines() { - $content = <<parser->parse($content); - $this->assertInternalType('array', $diffs); $this->assertContainsOnlyInstancesOf(Diff::class, $diffs); $this->assertCount(1, $diffs); $chunks = $diffs[0]->getChunks(); - $this->assertInternalType('array', $chunks); $this->assertContainsOnlyInstancesOf(Chunk::class, $chunks); $this->assertCount(1, $chunks); @@ -101,7 +98,6 @@ public function testParseWithRemovedLines() $this->assertSame(8, $chunk->getEndRange()); $lines = $chunk->getLines(); - $this->assertInternalType('array', $lines); $this->assertContainsOnlyInstancesOf(Line::class, $lines); $this->assertCount(2, $lines); @@ -117,7 +113,7 @@ public function testParseWithRemovedLines() public function testParseDiffForMulitpleFiles() { - $content = <<parser->parse($content); $this->assertCount(2, $diffs); diff --git a/tests/TimeEfficientImplementationTest.php b/tests/TimeEfficientImplementationTest.php index 713ccb88..66523122 100644 --- a/tests/TimeEfficientImplementationTest.php +++ b/tests/TimeEfficientImplementationTest.php @@ -1,4 +1,4 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Utils; + +final class FileUtils +{ + public static function getFileContent($file) + { + $content = @\file_get_contents($file); + if (false === $content) { + $error = \error_get_last(); + + throw new \RuntimeException(\sprintf( + 'Failed to read content of file "%s".%s', + $file, + $error ? ' ' . $error['message'] : '' + )); + } + + return $content; + } +} diff --git a/tests/Utils/UnifiedDiffAssertTrait.php b/tests/Utils/UnifiedDiffAssertTrait.php new file mode 100644 index 00000000..8598fe96 --- /dev/null +++ b/tests/Utils/UnifiedDiffAssertTrait.php @@ -0,0 +1,277 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Utils; + +trait UnifiedDiffAssertTrait +{ + /** + * @param string $diff + * + * @throws \UnexpectedValueException + */ + public function assertValidUnifiedDiffFormat($diff) + { + if ('' === $diff) { + $this->addToAssertionCount(1); + + return; + } + + // test diff ends with a line break + $last = \substr($diff, -1); + + if ("\n" !== $last && "\r" !== $last) { + throw new \UnexpectedValueException(\sprintf('Expected diff to end with a line break, got "%s".', $last)); + } + + $lines = \preg_split('/(.*\R)/', $diff, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $lineCount = \count($lines); + $lineNumber = $diffLineFromNumber = $diffLineToNumber = 1; + $fromStart = $fromTillOffset = $toStart = $toTillOffset = -1; + $expectHunkHeader = true; + + // check for header + if ($lineCount > 1) { + $this->unifiedDiffAssertLinePrefix($lines[0], 'Line 1.'); + $this->unifiedDiffAssertLinePrefix($lines[1], 'Line 2.'); + + if ('---' === \substr($lines[0], 0, 3)) { + if ('+++' !== \substr($lines[1], 0, 3)) { + throw new \UnexpectedValueException(\sprintf("Line 1 indicates a header, so line 2 must start with \"+++\".\nLine 1: \"%s\"\nLine 2: \"%s\".", $lines[0], $lines[1])); + } + + $this->unifiedDiffAssertHeaderLine($lines[0], '--- ', 'Line 1.'); + $this->unifiedDiffAssertHeaderLine($lines[1], '+++ ', 'Line 2.'); + + $lineNumber = 3; + } + } + + $endOfLineTypes = []; + $diffClosed = false; + + // assert format of lines, get all hunks, test the line numbers + for (; $lineNumber <= $lineCount; ++$lineNumber) { + if ($diffClosed) { + throw new \UnexpectedValueException(\sprintf('Unexpected line as 2 "No newline" markers have found, ". Line %d.', $lineNumber)); + } + + $line = $lines[$lineNumber - 1]; // line numbers start by 1, array index at 0 + $type = $this->unifiedDiffAssertLinePrefix($line, \sprintf('Line %d.', $lineNumber)); + + if ($expectHunkHeader && '@' !== $type && '\\' !== $type) { + throw new \UnexpectedValueException(\sprintf('Expected hunk start (\'@\'), got "%s". Line %d.', $type, $lineNumber)); + } + + if ('@' === $type) { + if (!$expectHunkHeader) { + throw new \UnexpectedValueException(\sprintf('Unexpected hunk start (\'@\'). Line %d.', $lineNumber)); + } + + $previousHunkFromEnd = $fromStart + $fromTillOffset; + $previousHunkTillEnd = $toStart + $toTillOffset; + + list($fromStart, $fromTillOffset, $toStart, $toTillOffset) = $this->unifiedDiffAssertHunkHeader($line, \sprintf('Line %d.', $lineNumber)); + + // detect overlapping hunks + if ($fromStart < $previousHunkFromEnd) { + throw new \UnexpectedValueException(\sprintf('Unexpected new hunk; "from" (\'-\') start overlaps previous hunk. Line %d.', $lineNumber)); + } + + if ($toStart < $previousHunkTillEnd) { + throw new \UnexpectedValueException(\sprintf('Unexpected new hunk; "to" (\'+\') start overlaps previous hunk. Line %d.', $lineNumber)); + } + + /* valid states; hunks touches against each other: + $fromStart === $previousHunkFromEnd + $toStart === $previousHunkTillEnd + */ + + $diffLineFromNumber = $fromStart; + $diffLineToNumber = $toStart; + $expectHunkHeader = false; + + continue; + } + + if ('-' === $type) { + if (isset($endOfLineTypes['-'])) { + throw new \UnexpectedValueException(\sprintf('Not expected from (\'-\'), already closed by "\\ No newline at end of file". Line %d.', $lineNumber)); + } + + ++$diffLineFromNumber; + } elseif ('+' === $type) { + if (isset($endOfLineTypes['+'])) { + throw new \UnexpectedValueException(\sprintf('Not expected to (\'+\'), already closed by "\\ No newline at end of file". Line %d.', $lineNumber)); + } + + ++$diffLineToNumber; + } elseif (' ' === $type) { + if (isset($endOfLineTypes['-'])) { + throw new \UnexpectedValueException(\sprintf('Not expected same (\' \'), \'-\' already closed by "\\ No newline at end of file". Line %d.', $lineNumber)); + } + + if (isset($endOfLineTypes['+'])) { + throw new \UnexpectedValueException(\sprintf('Not expected same (\' \'), \'+\' already closed by "\\ No newline at end of file". Line %d.', $lineNumber)); + } + + ++$diffLineFromNumber; + ++$diffLineToNumber; + } elseif ('\\' === $type) { + if (!isset($lines[$lineNumber - 2])) { + throw new \UnexpectedValueException(\sprintf('Unexpected "\\ No newline at end of file", it must be preceded by \'+\' or \'-\' line. Line %d.', $lineNumber)); + } + + $previousType = $this->unifiedDiffAssertLinePrefix($lines[$lineNumber - 2], \sprintf('Preceding line of "\\ No newline at end of file" of unexpected format. Line %d.', $lineNumber)); + + if (isset($endOfLineTypes[$previousType])) { + throw new \UnexpectedValueException(\sprintf('Unexpected "\\ No newline at end of file", "%s" was already closed. Line %d.', $type, $lineNumber)); + } + + $endOfLineTypes[$previousType] = true; + $diffClosed = \count($endOfLineTypes) > 1; + } else { + // internal state error + throw new \RuntimeException(\sprintf('Unexpected line type "%s" Line %d.', $type, $lineNumber)); + } + + $expectHunkHeader = + $diffLineFromNumber === ($fromStart + $fromTillOffset) + && $diffLineToNumber === ($toStart + $toTillOffset) + ; + } + + if ( + $diffLineFromNumber !== ($fromStart + $fromTillOffset) + && $diffLineToNumber !== ($toStart + $toTillOffset) + ) { + throw new \UnexpectedValueException(\sprintf('Unexpected EOF, number of lines in hunk "from" (\'-\')) and "to" (\'+\') mismatched. Line %d.', $lineNumber)); + } + + if ($diffLineFromNumber !== ($fromStart + $fromTillOffset)) { + throw new \UnexpectedValueException(\sprintf('Unexpected EOF, number of lines in hunk "from" (\'-\')) mismatched. Line %d.', $lineNumber)); + } + + if ($diffLineToNumber !== ($toStart + $toTillOffset)) { + throw new \UnexpectedValueException(\sprintf('Unexpected EOF, number of lines in hunk "to" (\'+\')) mismatched. Line %d.', $lineNumber)); + } + + $this->addToAssertionCount(1); + } + + /** + * @param string $line + * @param string $message + * + * @return string '+', '-', '@', ' ' or '\' + */ + private function unifiedDiffAssertLinePrefix($line, $message) + { + $this->unifiedDiffAssertStrLength($line, 2, $message); // 2: line type indicator ('+', '-', ' ' or '\') and a line break + $firstChar = $line[0]; + + if ('+' === $firstChar || '-' === $firstChar || '@' === $firstChar || ' ' === $firstChar) { + return $firstChar; + } + + if ("\\ No newline at end of file\n" === $line) { + return '\\'; + } + + throw new \UnexpectedValueException(\sprintf('Expected line to start with \'@\', \'-\' or \'+\', got "%s". %s', $line, $message)); + } + + private function unifiedDiffAssertStrLength($line, $min, $message) + { + $length = \strlen($line); + + if ($length < $min) { + throw new \UnexpectedValueException(\sprintf('Expected string length of minimal %d, got %d. %s', $min, $length, $message)); + } + } + + /** + * Assert valid unified diff header line + * + * Samples: + * - "+++ from1.txt\t2017-08-24 19:51:29.383985722 +0200" + * - "+++ from1.txt" + * + * @param string $line + * @param string $start + * @param string $message + */ + private function unifiedDiffAssertHeaderLine($line, $start, $message) + { + if (0 !== \strpos($line, $start)) { + throw new \UnexpectedValueException(\sprintf('Expected header line to start with "%s", got "%s". %s', $start . ' ', $line, $message)); + } + + // sample "+++ from1.txt\t2017-08-24 19:51:29.383985722 +0200\n" + $match = \preg_match( + "/^([^\t]*)(?:[\t]([\\S].*[\\S]))?\n$/", + \substr($line, 4), // 4 === string length of "+++ " / "--- " + $matches + ); + + if (1 !== $match) { + throw new \UnexpectedValueException(\sprintf('Header line does not match expected pattern, got "%s". %s', $line, $message)); + } + + // $file = $matches[1]; + + if (\count($matches) > 2) { + $this->unifiedDiffAssertHeaderDate($matches[2], $message); + } + } + + private function unifiedDiffAssertHeaderDate($date, $message) + { + // sample "2017-08-24 19:51:29.383985722 +0200" + $match = \preg_match( + '/^([\d]{4})-([01]?[\d])-([0123]?[\d])(:? [\d]{1,2}:[\d]{1,2}(?::[\d]{1,2}(:?\.[\d]+)?)?(?: ([\+\-][\d]{4}))?)?$/', + $date, + $matches + ); + + if (1 !== $match || ($matchesCount = \count($matches)) < 4) { + throw new \UnexpectedValueException(\sprintf('Date of header line does not match expected pattern, got "%s". %s', $date, $message)); + } + + // [$full, $year, $month, $day, $time] = $matches; + } + + /** + * @param string $line + * @param string $message + * + * @return int[] + */ + private function unifiedDiffAssertHunkHeader($line, $message) + { + if (1 !== \preg_match('#^@@ -([\d]+)((?:,[\d]+)?) \+([\d]+)((?:,[\d]+)?) @@\n$#', $line, $matches)) { + throw new \UnexpectedValueException( + \sprintf( + 'Hunk header line does not match expected pattern, got "%s". %s', + $line, + $message + ) + ); + } + + return [ + (int) $matches[1], + empty($matches[2]) ? 1 : (int) \substr($matches[2], 1), + (int) $matches[3], + empty($matches[4]) ? 1 : (int) \substr($matches[4], 1), + ]; + } +} diff --git a/tests/Utils/UnifiedDiffAssertTraitIntegrationTest.php b/tests/Utils/UnifiedDiffAssertTraitIntegrationTest.php new file mode 100644 index 00000000..b61091be --- /dev/null +++ b/tests/Utils/UnifiedDiffAssertTraitIntegrationTest.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Utils; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; + +/** + * @requires OS Linux + * + * @coversNothing + */ +final class UnifiedDiffAssertTraitIntegrationTest extends TestCase +{ + use UnifiedDiffAssertTrait; + + private $filePatch; + + protected function setUp() + { + $this->filePatch = __DIR__ . '/../fixtures/out/patch.txt'; + + $this->cleanUpTempFiles(); + } + + protected function tearDown() + { + $this->cleanUpTempFiles(); + } + + /** + * @param string $fileFrom + * @param string $fileTo + * + * @dataProvider provideFilePairsCases + */ + public function testValidPatches($fileFrom, $fileTo) + { + $command = \sprintf( + 'diff -u %s %s > %s', + \escapeshellarg(\realpath($fileFrom)), + \escapeshellarg(\realpath($fileTo)), + \escapeshellarg($this->filePatch) + ); + + $p = new Process($command); + $p->run(); + + $exitCode = $p->getExitCode(); + + if (0 === $exitCode) { + // odd case when two files have the same content. Test after executing as it is more efficient than to read the files and check the contents every time. + $this->addToAssertionCount(1); + + return; + } + + $this->assertSame( + 1, // means `diff` found a diff between the files we gave it + $exitCode, + \sprintf( + "Command exec. was not successful:\n\"%s\"\nOutput:\n\"%s\"\nStdErr:\n\"%s\"\nExit code %d.\n", + $command, + $p->getOutput(), + $p->getErrorOutput(), + $p->getExitCode() + ) + ); + + $this->assertValidUnifiedDiffFormat(FileUtils::getFileContent($this->filePatch)); + } + + /** + * @return array> + */ + public function provideFilePairsCases() + { + $cases = []; + + // created cases based on dedicated fixtures + $dir = \realpath(__DIR__ . '/../fixtures/UnifiedDiffAssertTraitIntegrationTest'); + $dirLength = \strlen($dir); + + for ($i = 1;; ++$i) { + $fromFile = \sprintf('%s/%d_a.txt', $dir, $i); + $toFile = \sprintf('%s/%d_b.txt', $dir, $i); + + if (!\file_exists($fromFile)) { + break; + } + + $this->assertFileExists($toFile); + $cases[\sprintf("Diff file:\n\"%s\"\nvs.\n\"%s\"\n", \substr(\realpath($fromFile), $dirLength), \substr(\realpath($toFile), $dirLength))] = [$fromFile, $toFile]; + } + + // create cases based on PHP files within the vendor directory for integration testing + $dir = \realpath(__DIR__ . '/../../vendor'); + $dirLength = \strlen($dir); + + $fileIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)); + $fromFile = __FILE__; + + /** @var \SplFileInfo $file */ + foreach ($fileIterator as $file) { + if ('php' !== $file->getExtension()) { + continue; + } + + $toFile = $file->getPathname(); + $cases[\sprintf("Diff file:\n\"%s\"\nvs.\n\"%s\"\n", \substr(\realpath($fromFile), $dirLength), \substr(\realpath($toFile), $dirLength))] = [$fromFile, $toFile]; + $fromFile = $toFile; + } + + return $cases; + } + + private function cleanUpTempFiles() + { + @\unlink($this->filePatch); + } +} diff --git a/tests/Utils/UnifiedDiffAssertTraitTest.php b/tests/Utils/UnifiedDiffAssertTraitTest.php new file mode 100644 index 00000000..2172838b --- /dev/null +++ b/tests/Utils/UnifiedDiffAssertTraitTest.php @@ -0,0 +1,434 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\Utils; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\Utils\UnifiedDiffAssertTrait + */ +final class UnifiedDiffAssertTraitTest extends TestCase +{ + use UnifiedDiffAssertTrait; + + /** + * @param string $diff + * + * @dataProvider provideValidCases + */ + public function testValidCases($diff) + { + $this->assertValidUnifiedDiffFormat($diff); + } + + public function provideValidCases() + { + return [ + [ +'--- Original ++++ New +@@ -8 +8 @@ +-Z ++U +', + ], + [ +'--- Original ++++ New +@@ -8 +8 @@ +-Z ++U +@@ -15 +15 @@ +-X ++V +', + ], + 'empty diff. is valid' => [ + '', + ], + ]; + } + + public function testNoLinebreakEnd() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Expected diff to end with a line break, got "C".', '#'))); + + $this->assertValidUnifiedDiffFormat("A\nB\nC"); + } + + public function testInvalidStartWithoutHeader() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote("Expected line to start with '@', '-' or '+', got \"A\n\". Line 1.", '#'))); + + $this->assertValidUnifiedDiffFormat("A\n"); + } + + public function testInvalidStartHeader1() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote("Line 1 indicates a header, so line 2 must start with \"+++\".\nLine 1: \"--- A\n\"\nLine 2: \"+ 1\n\".", '#'))); + + $this->assertValidUnifiedDiffFormat("--- A\n+ 1\n"); + } + + public function testInvalidStartHeader2() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote("Header line does not match expected pattern, got \"+++ file X\n\". Line 2.", '#'))); + + $this->assertValidUnifiedDiffFormat("--- A\n+++ file\tX\n"); + } + + public function testInvalidStartHeader3() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Date of header line does not match expected pattern, got "[invalid date]". Line 1.', '#'))); + + $this->assertValidUnifiedDiffFormat( +"--- Original\t[invalid date] ++++ New +@@ -1,2 +1,2 @@ +-A ++B + " . ' +' + ); + } + + public function testInvalidStartHeader4() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote("Expected header line to start with \"+++ \", got \"+++INVALID\n\". Line 2.", '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++INVALID +@@ -1,2 +1,2 @@ +-A ++B + ' . ' +' + ); + } + + public function testInvalidLine1() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote("Expected line to start with '@', '-' or '+', got \"1\n\". Line 5.", '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8 +8 @@ +-Z +1 ++U +' + ); + } + + public function testInvalidLine2() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Expected string length of minimal 2, got 1. Line 4.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8 +8 @@ + + +' + ); + } + + public function testHunkInvalidFormat() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote("Hunk header line does not match expected pattern, got \"@@ INVALID -1,1 +1,1 @@\n\". Line 3.", '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ INVALID -1,1 +1,1 @@ +-Z ++U +' + ); + } + + public function testHunkOverlapFrom() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected new hunk; "from" (\'-\') start overlaps previous hunk. Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8,1 +8,1 @@ +-Z ++U +@@ -7,1 +9,1 @@ +-Z ++U +' + ); + } + + public function testHunkOverlapTo() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected new hunk; "to" (\'+\') start overlaps previous hunk. Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8,1 +8,1 @@ +-Z ++U +@@ -17,1 +7,1 @@ +-Z ++U +' + ); + } + + public function testExpectHunk1() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Expected hunk start (\'@\'), got "+". Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8 +8 @@ +-Z ++U ++O +' + ); + } + + public function testExpectHunk2() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected hunk start (\'@\'). Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8,12 +8,12 @@ + ' . ' + ' . ' +@@ -38,12 +48,12 @@ +' + ); + } + + public function testMisplacedLineAfterComments1() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected line as 2 "No newline" markers have found, ". Line 8.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8 +8 @@ +-Z +\ No newline at end of file ++U +\ No newline at end of file ++A +' + ); + } + + public function testMisplacedLineAfterComments2() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected line as 2 "No newline" markers have found, ". Line 7.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8 +8 @@ ++U +\ No newline at end of file +\ No newline at end of file +\ No newline at end of file +' + ); + } + + public function testMisplacedLineAfterComments3() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected line as 2 "No newline" markers have found, ". Line 7.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8 +8 @@ ++U +\ No newline at end of file +\ No newline at end of file ++A +' + ); + } + + public function testMisplacedComment() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected "\ No newline at end of file", it must be preceded by \'+\' or \'-\' line. Line 1.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'\ No newline at end of file +' + ); + } + + public function testUnexpectedDuplicateNoNewLineEOF() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected "\\ No newline at end of file", "\\" was already closed. Line 8.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8,12 +8,12 @@ + ' . ' + ' . ' +\ No newline at end of file + ' . ' +\ No newline at end of file +' + ); + } + + public function testFromAfterClose() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Not expected from (\'-\'), already closed by "\ No newline at end of file". Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8,12 +8,12 @@ +-A +\ No newline at end of file +-A +\ No newline at end of file +' + ); + } + + public function testSameAfterFromClose() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Not expected same (\' \'), \'-\' already closed by "\ No newline at end of file". Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( + '--- Original ++++ New +@@ -8,12 +8,12 @@ +-A +\ No newline at end of file + A +\ No newline at end of file +' + ); + } + + public function testToAfterClose() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Not expected to (\'+\'), already closed by "\ No newline at end of file". Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( + '--- Original ++++ New +@@ -8,12 +8,12 @@ ++A +\ No newline at end of file ++A +\ No newline at end of file +' + ); + } + + public function testSameAfterToClose() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Not expected same (\' \'), \'+\' already closed by "\ No newline at end of file". Line 6.', '#'))); + + $this->assertValidUnifiedDiffFormat( + '--- Original ++++ New +@@ -8,12 +8,12 @@ ++A +\ No newline at end of file + A +\ No newline at end of file +' + ); + } + + public function testUnexpectedEOFFromMissingLines() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected EOF, number of lines in hunk "from" (\'-\')) mismatched. Line 7.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8,19 +7,2 @@ +-A ++B + ' . ' +' + ); + } + + public function testUnexpectedEOFToMissingLines() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected EOF, number of lines in hunk "to" (\'+\')) mismatched. Line 7.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -8,2 +7,3 @@ +-A ++B + ' . ' +' + ); + } + + public function testUnexpectedEOFBothFromAndToMissingLines() + { + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessageRegExp(\sprintf('#^%s$#', \preg_quote('Unexpected EOF, number of lines in hunk "from" (\'-\')) and "to" (\'+\') mismatched. Line 7.', '#'))); + + $this->assertValidUnifiedDiffFormat( +'--- Original ++++ New +@@ -1,12 +1,14 @@ +-A ++B + ' . ' +' + ); + } +} diff --git a/tests/fixtures/.editorconfig b/tests/fixtures/.editorconfig new file mode 100644 index 00000000..78b36ca0 --- /dev/null +++ b/tests/fixtures/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt b/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt new file mode 100644 index 00000000..2e65efe2 --- /dev/null +++ b/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_b.txt b/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_b.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt b/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt new file mode 100644 index 00000000..c7fe26e9 --- /dev/null +++ b/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt @@ -0,0 +1,35 @@ +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a +a \ No newline at end of file diff --git a/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt b/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt new file mode 100644 index 00000000..377a70f8 --- /dev/null +++ b/tests/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt @@ -0,0 +1,18 @@ +a +a +a +a +a +a +a +a +a +a +b +a +a +a +a +a +a +c \ No newline at end of file diff --git a/tests/fixtures/out/.editorconfig b/tests/fixtures/out/.editorconfig new file mode 100644 index 00000000..78b36ca0 --- /dev/null +++ b/tests/fixtures/out/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/tests/fixtures/out/.gitignore b/tests/fixtures/out/.gitignore new file mode 100644 index 00000000..f6f7a478 --- /dev/null +++ b/tests/fixtures/out/.gitignore @@ -0,0 +1,2 @@ +# reset all ignore rules to create sandbox for integration test +!/** \ No newline at end of file