* * 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; use SebastianBergmann\Diff\Output\AbstractChunkOutputBuilder; use SebastianBergmann\Diff\Output\DiffOnlyOutputBuilder; use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; /** * @covers SebastianBergmann\Diff\Differ * @covers SebastianBergmann\Diff\Output\AbstractChunkOutputBuilder * @covers SebastianBergmann\Diff\Output\DiffOnlyOutputBuilder * @covers SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder * * @uses SebastianBergmann\Diff\MemoryEfficientLongestCommonSubsequenceCalculator * @uses SebastianBergmann\Diff\TimeEfficientLongestCommonSubsequenceCalculator * @uses SebastianBergmann\Diff\Chunk * @uses SebastianBergmann\Diff\Diff * @uses SebastianBergmann\Diff\Line * @uses SebastianBergmann\Diff\Parser */ final class DifferTest extends TestCase { const WARNING = 3; const REMOVED = 2; const ADDED = 1; const OLD = 0; /** * @var Differ */ private $differ; protected function setUp() { $this->differ = new Differ; } /** * @param array $expected * @param string|array $from * @param string|array $to * @dataProvider arrayProvider */ public function testArrayRepresentationOfDiffCanBeRenderedUsingTimeEfficientLcsImplementation(array $expected, $from, $to) { $this->assertSame($expected, $this->differ->diffToArray($from, $to, new TimeEfficientLongestCommonSubsequenceCalculator)); } /** * @param string $expected * @param string $from * @param string $to * @dataProvider textProvider */ public function testTextRepresentationOfDiffCanBeRenderedUsingTimeEfficientLcsImplementation(string $expected, string $from, string $to) { $this->assertSame($expected, $this->differ->diff($from, $to, new TimeEfficientLongestCommonSubsequenceCalculator)); } /** * @param array $expected * @param string|array $from * @param string|array $to * @dataProvider arrayProvider */ public function testArrayRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLcsImplementation(array $expected, $from, $to) { $this->assertSame($expected, $this->differ->diffToArray($from, $to, new MemoryEfficientLongestCommonSubsequenceCalculator)); } /** * @param string $expected * @param string $from * @param string $to * @dataProvider textProvider */ public function testTextRepresentationOfDiffCanBeRenderedUsingMemoryEfficientLcsImplementation(string $expected, string $from, string $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( "--- Original\n+++ New\n@@ @@\n-1\n+2\n", $this->differ->diff(1, 2) ); } /** * @param string $diff * @param Diff[] $expected * @dataProvider diffProvider */ public function testParser(string $diff, array $expected) { $parser = new Parser; $result = $parser->parse($diff); $this->assertEquals($expected, $result); } public function arrayProvider(): array { return [ [ [ ['a', self::REMOVED], ['b', self::ADDED] ], 'a', 'b' ], [ [ ['ba', self::REMOVED], ['bc', self::ADDED] ], 'ba', 'bc' ], [ [ ['ab', self::REMOVED], ['cb', self::ADDED] ], 'ab', 'cb' ], [ [ ['abc', self::REMOVED], ['adc', self::ADDED] ], 'abc', 'adc' ], [ [ ['ab', self::REMOVED], ['abc', self::ADDED] ], 'ab', 'abc' ], [ [ ['bc', self::REMOVED], ['abc', self::ADDED] ], 'bc', 'abc' ], [ [ ['abc', self::REMOVED], ['abbc', self::ADDED] ], 'abc', 'abbc' ], [ [ ['abcdde', self::REMOVED], ['abcde', self::ADDED] ], 'abcdde', 'abcde' ], 'same start' => [ [ [17, self::OLD], ['b', self::REMOVED], ['d', self::ADDED], ], [30 => 17, 'a' => 'b'], [30 => 17, 'c' => 'd'], ], 'same end' => [ [ [1, self::REMOVED], [2, self::ADDED], ['b', self::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], ], [30 => 17, 1 => 2, 2 => 4, 'z' => 'x'], [30 => 17, 1 => 2, 3 => 'a', 2 => 5, 'z' => 'x'], ], 'same' => [ [ ['x', self::OLD], ], ['z' => 'x'], ['z' => 'x'], ], 'diff' => [ [ ['y', self::REMOVED], ['x', self::ADDED], ], ['x' => 'y'], ['z' => 'x'], ], 'diff 2' => [ [ ['y', self::REMOVED], ['b', self::REMOVED], ['x', self::ADDED], ['d', self::ADDED], ], ['x' => 'y', 'a' => 'b'], ['z' => 'x', 'c' => 'd'], ], 'test line diff detection' => [ [ [ "#Warning: Strings contain different line endings!\n", self::WARNING, ], [ " [ [ [ "#Warning: Strings contain different line endings!\n", self::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->expectExceptionMessageRegExp('#^"from" must be an array or string\.$#'); $this->differ->diffToArray(null, ''); } public function testDiffInvalidToType() { $this->expectException('\InvalidArgumentException'); $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) { $reflection = new \ReflectionObject($this->differ); $method = $reflection->getMethod('splitStringByLines'); $method->setAccessible(true); $this->assertSame($expected, $method->invoke($this->differ, $input)); } public function provideSplitStringByLinesCases(): array { return [ [ [], '' ], [ ['a'], 'a' ], [ ["a\n"], "a\n" ], [ ["a\r"], "a\r" ], [ ["a\r\n"], "a\r\n" ], [ ["\n"], "\n" ], [ ["\r"], "\r" ], [ ["\r\n"], "\r\n" ], [ [ "A\n", "B\n", "\n", "C\n" ], "A\nB\n\nC\n", ], [ [ "A\r\n", "B\n", "\n", "C\r" ], "A\r\nB\n\nC\r", ], [ [ "\n", "A\r\n", "B\n", "\n", '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( UnifiedDiffOutputBuilder::class, 'outputBuilder', new Differ(null) ); } public function testConstructorString() { $this->assertAttributeInstanceOf( UnifiedDiffOutputBuilder::class, 'outputBuilder', new Differ("--- Original\n+++ New\n") ); } public function testConstructorInvalidArgInt() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessageRegExp('/^Expected builder to be an instance of DiffOutputBuilderInterface, or a string, got integer "1"\.$/'); new Differ(1); } public function testConstructorInvalidArgObject() { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessageRegExp('/^Expected builder to be an instance of DiffOutputBuilderInterface, or a string, got instance of "SplFileInfo"\.$/'); new Differ(new \SplFileInfo(__FILE__)); } }