From f0ef6133d674137e902fdf8a6f2e8e97e14a087b Mon Sep 17 00:00:00 2001 From: SpacePossum Date: Thu, 19 Oct 2017 11:10:07 +0200 Subject: [PATCH] Backport GeckoPackages/DiffOutputBuilder --- .gitignore | 1 + LICENSE | 36 +- LICENSE_DIFF | 33 ++ LICENSE_GECKO | 19 + README.md | 12 +- composer.json | 12 +- .../ConfigurationException.php | 36 ++ .../UnifiedDiffOutputBuilder.php | 295 ++++++++++++ .../Tests/AbstractDiffOutputBuilderTest.php | 97 ++++ .../Tests/ConfigurationExceptionTest.php | 32 ++ .../UnifiedDiffAssertTraitIntegrationTest.php | 135 ++++++ ...nifiedDiffOutputBuilderIntegrationTest.php | 247 +++++++++++ .../Tests/Integration/fixtures/.editorconfig | 1 + .../1_a.txt | 1 + .../1_b.txt | 0 .../2_a.txt | 35 ++ .../2_b.txt | 18 + .../Tests/Integration/out/.editorconfig | 1 + .../Tests/Integration/out/.gitignore | 2 + .../Tests/UnifiedDiffAssertTraitTest.php | 419 ++++++++++++++++++ .../UnifiedDiffOutputBuilderDataProvider.php | 197 ++++++++ .../Tests/UnifiedDiffOutputBuilderTest.php | 386 ++++++++++++++++ .../Utils/PHPUnitPolyfill.php | 49 ++ .../Utils/UnifiedDiffAssertTrait.php | 279 ++++++++++++ 24 files changed, 2305 insertions(+), 38 deletions(-) create mode 100644 LICENSE_DIFF create mode 100644 LICENSE_GECKO create mode 100644 src/GeckoPackages/DiffOutputBuilder/ConfigurationException.php create mode 100644 src/GeckoPackages/DiffOutputBuilder/UnifiedDiffOutputBuilder.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/AbstractDiffOutputBuilderTest.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/ConfigurationExceptionTest.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffAssertTraitIntegrationTest.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffOutputBuilderIntegrationTest.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/.editorconfig create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_b.txt create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.editorconfig create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.gitignore create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffAssertTraitTest.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderDataProvider.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderTest.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Utils/PHPUnitPolyfill.php create mode 100644 tests/GeckoPackages/DiffOutputBuilder/Utils/UnifiedDiffAssertTrait.php diff --git a/.gitignore b/.gitignore index 5cf9a2cf..bf6dd089 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /composer.lock /phpunit.xml /vendor/ +tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out diff --git a/LICENSE b/LICENSE index e1ddf136..19a2d69c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,33 +1,5 @@ -sebastian/diff +Code from `sebastian/diff` has been forked and republished by permission of Sebastian Bergmann. +Licenced with BSD-3-Clause @ see LICENSE_DIFF, copyright (c) Sebastian Bergmann -Copyright (c) 2002-2017, Sebastian Bergmann . -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - - * Neither the name of Sebastian Bergmann nor the names of his - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. +Code from `GeckoPackages/GeckoDiffOutputBuilder` has been copied and republished by permission of GeckoPackages. +Licenced with MIT @ see LICENSE_GECKO, copyright (c) GeckoPackages https://github.com/GeckoPackages diff --git a/LICENSE_DIFF b/LICENSE_DIFF new file mode 100644 index 00000000..e1ddf136 --- /dev/null +++ b/LICENSE_DIFF @@ -0,0 +1,33 @@ +sebastian/diff + +Copyright (c) 2002-2017, Sebastian Bergmann . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Sebastian Bergmann nor the names of his + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE_GECKO b/LICENSE_GECKO new file mode 100644 index 00000000..066294d5 --- /dev/null +++ b/LICENSE_GECKO @@ -0,0 +1,19 @@ +Copyright (c) https://github.com/GeckoPackages + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 1a2f48d4..ac448c55 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ # PHP-CS-Fixer/diff -Fork of sebastian/diff +This is version is for PHP CS Fixer only! Do not use it! -This is version is for PHP CS Fixer only! +Code from `sebastian/diff` has been forked a republished by permission of Sebastian Bergmann. +Licenced with BSD-3-Clause @ see LICENSE_DIFF, copyright (c) Sebastian Bergmann +https://github.com/sebastianbergmann/diff -Do not use it! +Code from `GeckoPackages/GeckoDiffOutputBuilder` has been copied and republished by permission of GeckoPackages. +Licenced with MIT @ see LICENSE_GECKO, copyright (c) GeckoPackages https://github.com/GeckoPackages +https://github.com/GeckoPackages/GeckoDiffOutputBuilder/ + +For questions visit us @ https://gitter.im/PHP-CS-Fixer/Lobby diff --git a/composer.json b/composer.json index 49b712b3..f8f2377b 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "sebastian/diff v2 backport support for PHP5.6", "keywords": ["diff"], "homepage": "https://github.com/PHP-CS-Fixer", - "license": "BSD-3-Clause", + "license": "BSD-3-Clause|MIT", "authors": [ { "name": "Sebastian Bergmann", @@ -12,13 +12,17 @@ { "name": "Kore Nordmann", "email": "mail@kore-nordmann.de" + }, + { + "name": "SpacePossum" } ], "require": { "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3" + "phpunit/phpunit": "^4.8.35 || ^5.4.3", + "symfony/process": "^3.3" }, "autoload": { "classmap": [ @@ -28,7 +32,9 @@ "autoload-dev": { "psr-4": { "PhpCsFixer\\Diff\\v1_4\\Tests\\": "tests/v1_4", - "PhpCsFixer\\Diff\\v2_0\\Tests\\": "tests/v2_0" + "PhpCsFixer\\Diff\\v2_0\\Tests\\": "tests/v2_0", + "PhpCsFixer\\Diff\\GeckoPackages\\DiffOutputBuilder\\Tests\\": "tests/GeckoPackages/DiffOutputBuilder/Tests", + "PhpCsFixer\\Diff\\GeckoPackages\\DiffOutputBuilder\\Utils\\": "tests/GeckoPackages/DiffOutputBuilder/Utils" } } } diff --git a/src/GeckoPackages/DiffOutputBuilder/ConfigurationException.php b/src/GeckoPackages/DiffOutputBuilder/ConfigurationException.php new file mode 100644 index 00000000..315ac072 --- /dev/null +++ b/src/GeckoPackages/DiffOutputBuilder/ConfigurationException.php @@ -0,0 +1,36 @@ +' : \gettype($value).'#'.$value) + ), + $code, + $previous + ); + } +} diff --git a/src/GeckoPackages/DiffOutputBuilder/UnifiedDiffOutputBuilder.php b/src/GeckoPackages/DiffOutputBuilder/UnifiedDiffOutputBuilder.php new file mode 100644 index 00000000..8a703725 --- /dev/null +++ b/src/GeckoPackages/DiffOutputBuilder/UnifiedDiffOutputBuilder.php @@ -0,0 +1,295 @@ += 0 + */ + private $commonLineThreshold; + + /** + * @var string + */ + private $header; + + /** + * @var int >= 0 + */ + private $contextLines; + + private static $default = [ + 'contextLines' => 3, // like `diff: -u, -U NUM, --unified[=NUM]`, for patch/git apply compatibility best to keep at least @ 3 + 'collapseRanges' => true, // ranges of length one are rendered with the trailing `,1` + 'fromFile' => null, + 'fromFileDate' => null, + 'toFile' => null, + 'toFileDate' => null, + 'commonLineThreshold' => 6, // number of same lines before ending a new hunk and creating a new one (if needed) + ]; + + 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'] < 1) { + 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); + + $diff = \stream_get_contents($buffer, -1, 0); + + \fclose($buffer); + + if (!$this->changed) { + return ''; + } + + return $diff; + } + + private function writeDiffHunks($output, array $diff) + { + // detect "No newline at end of file" and insert into `$diff` if needed + + $upperLimit = \count($diff); + + // append "\ No newline at end of file" if needed + 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", self::$noNewlineAtOEFid]]); + } + } 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", self::$noNewlineAtOEFid]]); + } + + 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 + ; + + $contextEndOffset = $i + $this->contextLines >= \count($diff) + ? \count($diff) - $i + : $this->contextLines + ; + + $this->writeHunk( + $diff, + $hunkCapture - $contextStartOffset, + $i - $cutOff + $contextEndOffset + 1, + $fromStart - $contextStartOffset, + $fromRange - $cutOff + $contextStartOffset + $contextEndOffset, + $toStart - $contextStartOffset, + $toRange - $cutOff + $contextStartOffset + $contextEndOffset, + $output + ); + + $fromStart += $fromRange; + $toStart += $toRange; + + $hunkCapture = false; + $sameCount = $toRange = $fromRange = 0; + } + + continue; + } + + $sameCount = 0; + + if ($entry[1] === self::$noNewlineAtOEFid) { + continue; + } + + $this->changed = true; + + if (false === $hunkCapture) { + $hunkCapture = $i; + } + + if (1 === $entry[1]) { // added + ++$toRange; + } + + if (2 === $entry[1]) { // removed + ++$fromRange; + } + } + + if (false !== $hunkCapture) { + $contextStartOffset = $hunkCapture - $this->contextLines < 0 + ? $hunkCapture + : $this->contextLines + ; + + $this->writeHunk( + $diff, + $hunkCapture - $contextStartOffset, + \count($diff), + $fromStart - $contextStartOffset, + $fromRange + $contextStartOffset, + $toStart - $contextStartOffset, + $toRange + $contextStartOffset, + $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] === 1) { // added + $this->changed = true; + \fwrite($output, '+'.$diff[$i][0]); + } elseif ($diff[$i][1] === 2) { // removed + $this->changed = true; + \fwrite($output, '-'.$diff[$i][0]); + } elseif ($diff[$i][1] === 0) { // same + \fwrite($output, ' '.$diff[$i][0]); + } elseif ($diff[$i][1] === self::$noNewlineAtOEFid) { + $this->changed = true; + \fwrite($output, $diff[$i][0]); + } + } + } +} diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/AbstractDiffOutputBuilderTest.php b/tests/GeckoPackages/DiffOutputBuilder/Tests/AbstractDiffOutputBuilderTest.php new file mode 100644 index 00000000..ad669e0c --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/AbstractDiffOutputBuilderTest.php @@ -0,0 +1,97 @@ +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); + } + + /** + * Returns a new instance of a Differ with a new instance of the class (DiffOutputBuilderInterface) under test. + * + * @param array $options + * + * @return Differ + */ + protected function getDiffer(array $options = []) + { + if (null === $this->differClass) { + // map test class name (child) back to the Differ being tested. + $childClass = \get_class($this); + $differClass = 'PhpCsFixer\\Diff\\GeckoPackages\\DiffOutputBuilder\\'.\substr($childClass, \strrpos($childClass, '\\') + 1, -4); + + // basic tests: class must exist... + $this->assertTrue(\class_exists($differClass)); + + // ...and must implement DiffOutputBuilderInterface + $implements = \class_implements($differClass); + $this->assertInternalType('array', $implements); + $this->assertArrayHasKey(DiffOutputBuilderInterface::class, $implements); + + $this->differClass = $differClass; + } + + return new Differ(new $this->differClass($options)); + } +} diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/ConfigurationExceptionTest.php b/tests/GeckoPackages/DiffOutputBuilder/Tests/ConfigurationExceptionTest.php new file mode 100644 index 00000000..1e04846b --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/ConfigurationExceptionTest.php @@ -0,0 +1,32 @@ +assertSame(0, $e->getCode()); + $this->assertNull($e->getPrevious()); + $this->assertSame('Option "test" must be A, got "string#B".', $e->getMessage()); + } +} diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffAssertTraitIntegrationTest.php b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffAssertTraitIntegrationTest.php new file mode 100644 index 00000000..a1e7aefa --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffAssertTraitIntegrationTest.php @@ -0,0 +1,135 @@ +filePatch = __DIR__.'/out/patch.txt'; + + $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(\file_get_contents($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; + } + + protected function tearDown() + { + $this->cleanUpTempFiles(); + } + + private function cleanUpTempFiles() + { + @\unlink($this->filePatch); + } +} diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffOutputBuilderIntegrationTest.php b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffOutputBuilderIntegrationTest.php new file mode 100644 index 00000000..4fe3a89c --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/UnifiedDiffOutputBuilderIntegrationTest.php @@ -0,0 +1,247 @@ +dir = __DIR__.'/out/'; + $this->fileFrom = $this->dir.'from.txt'; + $this->fileTo = $this->dir.'to.txt'; + $this->filePatch = $this->dir.'diff.patch'; + + $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 testIntegrationUsingPHPFileInVendor($fileFrom, $fileTo) + { + $from = @\file_get_contents($fileFrom); + $this->assertInternalType('string', $from, \sprintf('Failed to read file "%s".', $fileFrom)); + + $to = @\file_get_contents($fileTo); + $this->assertInternalType('string', $to, \sprintf('Failed to read file "%s".', $fileTo)); + + $diff = (new Differ(new UnifiedDiffOutputBuilder(['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->assertNotSame('', $diff); + $this->assertValidUnifiedDiffFormat($diff); + $this->doIntegrationTest($diff, $from, $to); + } + + /** + * @param string $expected + * @param string $from + * @param string $to + * + * @dataProvider provideOutputBuildingCases + * @dataProvider provideSample + * @dataProvider provideBasicDiffGeneration + */ + public function testIntegrationOfUnitTestCases($expected, $from, $to) + { + $this->doIntegrationTest($expected, $from, $to); + } + + public function provideOutputBuildingCases() + { + return UnifiedDiffOutputBuilderDataProvider::provideOutputBuildingCases(); + } + + public function provideSample() + { + return UnifiedDiffOutputBuilderDataProvider::provideSample(); + } + + public function provideBasicDiffGeneration() + { + return UnifiedDiffOutputBuilderDataProvider::provideBasicDiffGeneration(); + } + + public function provideFilePairs() + { + $cases = []; + $fromFile = __FILE__; + $vendorDir = \realpath(__DIR__.'/../../../../../vendor'); + $vendorDirLength = \strlen($vendorDir); + $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", \substr(\realpath($fromFile), $vendorDirLength), \substr(\realpath($toFile), $vendorDirLength))] = [$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->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()); + + $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 doIntegrationTest($diff, $from, $to) + { + if ('' === $diff) { + $this->addToAssertionCount(1); // Empty diff has no integration test part. + + return; + } + + $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); + + $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 %s', // --unidiff-zero --ignore-whitespace + \escapeshellarg($this->dir), + \escapeshellarg($this->filePatch) + ); + + $p = new Process($command); + $p->run(); + + $this->assertTrue( + $p->isSuccessful(), + \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() + ) + ); + + $command = \sprintf( + 'patch -u --verbose --posix %s < %s', + \escapeshellarg($this->fileFrom), + \escapeshellarg($this->filePatch) + ); + + $p = new Process($command); + $p->run(); + + $output = $p->getOutput(); + + $this->assertTrue( + $p->isSuccessful(), + \sprintf( + "Command exec. was not successful:\n\"%s\"\nOutput:\n\"%s\"\nStdErr:\n\"%s\"\nExit code %d.\n", + $command, + $output, + $p->getErrorOutput(), + $p->getExitCode() + ) + ); + + $this->assertStringEqualsFile( + $this->fileFrom, $to, + \sprintf('Patch command "%s".', $command) + ); + } + + protected function tearDown() + { + $this->cleanUpTempFiles(); + } + + private function cleanUpTempFiles() + { + @\unlink($this->fileFrom.'.orig'); + @\unlink($this->fileFrom.'.rej'); + @\unlink($this->fileFrom); + @\unlink($this->fileTo); + @\unlink($this->filePatch); + } +} diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/.editorconfig b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/.editorconfig new file mode 100644 index 00000000..78b36ca0 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt new file mode 100644 index 00000000..2e65efe2 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_b.txt b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_b.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt new file mode 100644 index 00000000..c7fe26e9 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/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/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt new file mode 100644 index 00000000..377a70f8 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/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/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.editorconfig b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.editorconfig new file mode 100644 index 00000000..78b36ca0 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.gitignore b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.gitignore new file mode 100644 index 00000000..f6f7a478 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/Integration/out/.gitignore @@ -0,0 +1,2 @@ +# reset all ignore rules to create sandbox for integration test +!/** \ No newline at end of file diff --git a/tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffAssertTraitTest.php b/tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffAssertTraitTest.php new file mode 100644 index 00000000..764b9e2c --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffAssertTraitTest.php @@ -0,0 +1,419 @@ +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/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderDataProvider.php b/tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderDataProvider.php new file mode 100644 index 00000000..b77c85c2 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderDataProvider.php @@ -0,0 +1,197 @@ + '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/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderTest.php b/tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderTest.php new file mode 100644 index 00000000..f27c69f1 --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Tests/UnifiedDiffOutputBuilderTest.php @@ -0,0 +1,386 @@ +assertValidUnifiedDiffFormat($diff); + } + + /** + * {@inheritdoc} + */ + public function provideOutputBuildingCases() + { + return UnifiedDiffOutputBuilderDataProvider::provideOutputBuildingCases(); + } + + /** + * {@inheritdoc} + */ + public function provideSample() + { + return UnifiedDiffOutputBuilderDataProvider::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 UnifiedDiffOutputBuilderDataProvider::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 [ + [ + '', + "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 UnifiedDiffOutputBuilder([ + 'fromFile' => 'input.txt', + 'toFile' => 'output.txt', + ]); + + $this->assertSame( + '', + $builder->getDiff([]) + ); + } + + /** + * @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 UnifiedDiffOutputBuilder($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, + ], + ]; + } +} diff --git a/tests/GeckoPackages/DiffOutputBuilder/Utils/PHPUnitPolyfill.php b/tests/GeckoPackages/DiffOutputBuilder/Utils/PHPUnitPolyfill.php new file mode 100644 index 00000000..8e03414e --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Utils/PHPUnitPolyfill.php @@ -0,0 +1,49 @@ +wellYeahShipIt('expectedException', $exception); + } + + public function expectExceptionMessageRegExp($messageRegExp) + { + if (\method_exists(TestCase::class, 'expectExceptionMessageRegExp')) { + return parent::expectExceptionMessageRegExp($messageRegExp); + } + + $this->wellYeahShipIt('expectedExceptionMessageRegExp', $messageRegExp); + } + + private function wellYeahShipIt($key, $value) + { + $self = new \ReflectionClass(PHPUnit_Framework_TestCase::class); + $property = $self->getProperty($key); + $property->setAccessible(true); + $property->setValue($this, $value); + } +} diff --git a/tests/GeckoPackages/DiffOutputBuilder/Utils/UnifiedDiffAssertTrait.php b/tests/GeckoPackages/DiffOutputBuilder/Utils/UnifiedDiffAssertTrait.php new file mode 100644 index 00000000..0a5d1e6f --- /dev/null +++ b/tests/GeckoPackages/DiffOutputBuilder/Utils/UnifiedDiffAssertTrait.php @@ -0,0 +1,279 @@ +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), + ]; + } +}