diff --git a/composer.json b/composer.json index ceffd95d..e08be75b 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "php": "^5.6 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3", + "phpunit/phpunit": "^5.7.23 || ^6.4.3", "symfony/process": "^3.3" }, "autoload": { @@ -33,6 +33,7 @@ "psr-4": { "PhpCsFixer\\Diff\\v1_4\\Tests\\": "tests/v1_4", "PhpCsFixer\\Diff\\v2_0\\Tests\\": "tests/v2_0", + "PhpCsFixer\\Diff\\v3_0\\": "tests/v3_0", "PhpCsFixer\\Diff\\GeckoPackages\\DiffOutputBuilder\\Tests\\": "tests/GeckoPackages/DiffOutputBuilder/Tests", "PhpCsFixer\\Diff\\GeckoPackages\\DiffOutputBuilder\\Utils\\": "tests/GeckoPackages/DiffOutputBuilder/Utils" } diff --git a/src/v1_4/Differ.php b/src/v1_4/Differ.php index a6ef45e2..b2015abf 100644 --- a/src/v1_4/Differ.php +++ b/src/v1_4/Differ.php @@ -232,7 +232,7 @@ public function diffToArray($from, $to, LongestCommonSubsequence $lcs = null) if ($this->detectUnmatchedLineEndings($fromMatches, $toMatches)) { $diff[] = array( - '#Warning: Strings contain different line endings!', + '#Warnings contain different line endings!', 0 ); } diff --git a/src/v2_0/Differ.php b/src/v2_0/Differ.php index ed66d867..a8f7dba7 100644 --- a/src/v2_0/Differ.php +++ b/src/v2_0/Differ.php @@ -158,7 +158,7 @@ public function diffToArray($from, $to, LongestCommonSubsequenceCalculator $lcs } if ($this->detectUnmatchedLineEndings($diff)) { - \array_unshift($diff, ["#Warning: Strings contain different line endings!\n", 3]); + \array_unshift($diff, ["#Warnings contain different line endings!\n", 3]); } return $diff; diff --git a/src/v3_0/Chunk.php b/src/v3_0/Chunk.php new file mode 100644 index 00000000..6b633c17 --- /dev/null +++ b/src/v3_0/Chunk.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +final class Chunk +{ + /** + * @var int + */ + private $start; + + /** + * @var int + */ + private $startRange; + + /** + * @var int + */ + private $end; + + /** + * @var int + */ + private $endRange; + + /** + * @var array + */ + private $lines; + + public function __construct($start = 0, $startRange = 1, $end = 0, $endRange = 1, array $lines = []) + { + $this->start = $start; + $this->startRange = $startRange; + $this->end = $end; + $this->endRange = $endRange; + $this->lines = $lines; + } + + public function getStart() + { + return $this->start; + } + + public function getStartRange() + { + return $this->startRange; + } + + public function getEnd() + { + return $this->end; + } + + public function getEndRange() + { + return $this->endRange; + } + + public function getLines() + { + return $this->lines; + } + + public function setLines(array $lines) + { + $this->lines = $lines; + } +} diff --git a/src/v3_0/Diff.php b/src/v3_0/Diff.php new file mode 100644 index 00000000..9f537ec9 --- /dev/null +++ b/src/v3_0/Diff.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +final class Diff +{ + /** + * @var string + */ + private $from; + + /** + * @var string + */ + private $to; + + /** + * @var Chunk[] + */ + private $chunks; + + /** + * @param string $from + * @param string $to + * @param Chunk[] $chunks + */ + public function __construct($from, $to, array $chunks = []) + { + $this->from = $from; + $this->to = $to; + $this->chunks = $chunks; + } + + public function getFrom() + { + return $this->from; + } + + public function getTo() + { + return $this->to; + } + + /** + * @return Chunk[] + */ + public function getChunks() + { + return $this->chunks; + } + + /** + * @param Chunk[] $chunks + */ + public function setChunks(array $chunks) + { + $this->chunks = $chunks; + } +} diff --git a/src/v3_0/Differ.php b/src/v3_0/Differ.php new file mode 100644 index 00000000..42315f5a --- /dev/null +++ b/src/v3_0/Differ.php @@ -0,0 +1,329 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +use PhpCsFixer\Diff\v3_0\Output\DiffOutputBuilderInterface; +use PhpCsFixer\Diff\v3_0\Output\UnifiedDiffOutputBuilder; + +/** + * Diff implementation. + */ +final class Differ +{ + const OLD = 0; + const ADDED = 1; + const REMOVED = 2; + const DIFF_LINE_END_WARNING = 3; + const NO_LINE_END_EOF_WARNING = 4; + + /** + * @var DiffOutputBuilderInterface + */ + private $outputBuilder; + + /** + * @param DiffOutputBuilderInterface $outputBuilder + * + * @throws InvalidArgumentException + */ + public function __construct($outputBuilder = null) + { + if ($outputBuilder instanceof DiffOutputBuilderInterface) { + $this->outputBuilder = $outputBuilder; + } elseif (null === $outputBuilder) { + $this->outputBuilder = new UnifiedDiffOutputBuilder; + } elseif (\is_string($outputBuilder)) { + // PHPUnit 6.1.4, 6.2.0, 6.2.1, 6.2.2, and 6.2.3 support + // @see https://github.com/sebastianbergmann/phpunit/issues/2734#issuecomment-314514056 + // @deprecated + $this->outputBuilder = new UnifiedDiffOutputBuilder($outputBuilder); + } else { + throw new InvalidArgumentException( + \sprintf( + 'Expected builder to be an instance of DiffOutputBuilderInterface, or a string, got %s.', + \is_object($outputBuilder) ? 'instance of "' . \get_class($outputBuilder) . '"' : \gettype($outputBuilder) . ' "' . $outputBuilder . '"' + ) + ); + } + } + + /** + * Returns the diff between two arrays or strings as string. + * + * @param array|string $from + * @param array|string $to + * @param null|LongestCommonSubsequenceCalculator $lcs + * + * @return string + */ + public function diff($from, $to, LongestCommonSubsequenceCalculator $lcs = null) + { + $diff = $this->diffToArray( + $this->normalizeDiffInput($from), + $this->normalizeDiffInput($to), + $lcs + ); + + return $this->outputBuilder->getDiff($diff); + } + + /** + * Returns the diff between two arrays or strings as array. + * + * Each array element contains two elements: + * - [0] => mixed $token + * - [1] => 2|1|0 + * + * - 2: REMOVED: $token was removed from $from + * - 1: ADDED: $token was added to $from + * - 0: OLD: $token is not changed in $to + * + * @param array|string $from + * @param array|string $to + * @param LongestCommonSubsequenceCalculator $lcs + * + * @return 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.'); + } + + if (\is_string($to)) { + $to = $this->splitStringByLines($to); + } elseif (!\is_array($to)) { + throw new InvalidArgumentException('"to" must be an array or string.'); + } + + list($from, $to, $start, $end) = self::getArrayDiffParted($from, $to); + + if ($lcs === null) { + $lcs = $this->selectLcsImplementation($from, $to); + } + + $common = $lcs->calculate(\array_values($from), \array_values($to)); + $diff = []; + + foreach ($start as $token) { + $diff[] = [$token, self::OLD]; + } + + \reset($from); + \reset($to); + + foreach ($common as $token) { + while (($fromToken = \reset($from)) !== $token) { + $diff[] = [\array_shift($from), self::REMOVED]; + } + + while (($toToken = \reset($to)) !== $token) { + $diff[] = [\array_shift($to), self::ADDED]; + } + + $diff[] = [$token, self::OLD]; + + \array_shift($from); + \array_shift($to); + } + + while (($token = \array_shift($from)) !== null) { + $diff[] = [$token, self::REMOVED]; + } + + while (($token = \array_shift($to)) !== null) { + $diff[] = [$token, self::ADDED]; + } + + foreach ($end as $token) { + $diff[] = [$token, self::OLD]; + } + + if ($this->detectUnmatchedLineEndings($diff)) { + \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. + * + * @param string $input + * + * @return array + */ + private function splitStringByLines($input) + { + return \preg_split('/(.*\R)/', $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + } + + /** + * @param array $from + * @param array $to + * + * @return 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 + // calculation is only an estimation for the matrix and the LCS method + // will typically allocate a bit more memory than this. + $memoryLimit = 100 * 1024 * 1024; + + if ($this->calculateEstimatedFootprint($from, $to) > $memoryLimit) { + return new MemoryEfficientLongestCommonSubsequenceCalculator; + } + + return new TimeEfficientLongestCommonSubsequenceCalculator; + } + + /** + * Calculates the estimated memory footprint for the DP-based method. + * + * @param array $from + * @param array $to + * + * @return float|int + */ + private function calculateEstimatedFootprint(array $from, array $to) + { + $itemSize = PHP_INT_SIZE === 4 ? 76 : 144; + + return $itemSize * \min(\count($from), \count($to)) ** 2; + } + + /** + * Returns true if line ends don't match in a diff. + * + * @param array $diff + * + * @return bool + */ + private function detectUnmatchedLineEndings(array $diff) + { + $newLineBreaks = ['' => true]; + $oldLineBreaks = ['' => true]; + + foreach ($diff as $entry) { + if (self::OLD === $entry[1]) { + $ln = $this->getLinebreak($entry[0]); + $oldLineBreaks[$ln] = true; + $newLineBreaks[$ln] = true; + } elseif (self::ADDED === $entry[1]) { + $newLineBreaks[$this->getLinebreak($entry[0])] = true; + } elseif (self::REMOVED === $entry[1]) { + $oldLineBreaks[$this->getLinebreak($entry[0])] = true; + } + } + + // if either input or output is a single line without breaks than no warning should be raised + if (['' => true] === $newLineBreaks || ['' => true] === $oldLineBreaks) { + return false; + } + + // two way compare + foreach ($newLineBreaks as $break => $set) { + if (!isset($oldLineBreaks[$break])) { + return true; + } + } + + foreach ($oldLineBreaks as $break => $set) { + if (!isset($newLineBreaks[$break])) { + return true; + } + } + + return false; + } + + private function getLinebreak($line) + { + if (!\is_string($line)) { + return ''; + } + + $lc = \substr($line, -1); + if ("\r" === $lc) { + return "\r"; + } + + if ("\n" !== $lc) { + return ''; + } + + if ("\r\n" === \substr($line, -2)) { + return "\r\n"; + } + + return "\n"; + } + + private static function getArrayDiffParted(array &$from, array &$to) + { + $start = []; + $end = []; + + \reset($to); + + foreach ($from as $k => $v) { + $toK = \key($to); + + if ($toK === $k && $v === $to[$k]) { + $start[$k] = $v; + + unset($from[$k], $to[$k]); + } else { + break; + } + } + + \end($from); + \end($to); + + do { + $fromK = \key($from); + $toK = \key($to); + + if (null === $fromK || null === $toK || \current($from) !== \current($to)) { + break; + } + + \prev($from); + \prev($to); + + $end = [$fromK => $from[$fromK]] + $end; + unset($from[$fromK], $to[$toK]); + } while (true); + + return [$from, $to, $start, $end]; + } +} diff --git a/src/v3_0/Exception/ConfigurationException.php b/src/v3_0/Exception/ConfigurationException.php new file mode 100644 index 00000000..8892d8f9 --- /dev/null +++ b/src/v3_0/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\v3_0; + +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/v3_0/Exception/Exception.php b/src/v3_0/Exception/Exception.php new file mode 100644 index 00000000..7a391db7 --- /dev/null +++ b/src/v3_0/Exception/Exception.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +interface Exception +{ +} diff --git a/src/v3_0/Exception/InvalidArgumentException.php b/src/v3_0/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..50854824 --- /dev/null +++ b/src/v3_0/Exception/InvalidArgumentException.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +class InvalidArgumentException extends \InvalidArgumentException implements Exception +{ +} diff --git a/src/v3_0/Line.php b/src/v3_0/Line.php new file mode 100644 index 00000000..07be8805 --- /dev/null +++ b/src/v3_0/Line.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +final class Line +{ + const ADDED = 1; + const REMOVED = 2; + const UNCHANGED = 3; + + /** + * @var int + */ + private $type; + + /** + * @var string + */ + private $content; + + public function __construct($type = self::UNCHANGED, $content = '') + { + $this->type = $type; + $this->content = $content; + } + + public function getContent() + { + return $this->content; + } + + public function getType() + { + return $this->type; + } +} diff --git a/src/v3_0/LongestCommonSubsequenceCalculator.php b/src/v3_0/LongestCommonSubsequenceCalculator.php new file mode 100644 index 00000000..be5ed5a1 --- /dev/null +++ b/src/v3_0/LongestCommonSubsequenceCalculator.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +interface LongestCommonSubsequenceCalculator +{ + /** + * Calculates the longest common subsequence of two arrays. + * + * @param array $from + * @param array $to + * + * @return array + */ + public function calculate(array $from, array $to); +} diff --git a/src/v3_0/MemoryEfficientLongestCommonSubsequenceCalculator.php b/src/v3_0/MemoryEfficientLongestCommonSubsequenceCalculator.php new file mode 100644 index 00000000..85a1c4e6 --- /dev/null +++ b/src/v3_0/MemoryEfficientLongestCommonSubsequenceCalculator.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +final class MemoryEfficientLongestCommonSubsequenceCalculator implements LongestCommonSubsequenceCalculator +{ + /** + * {@inheritdoc} + */ + public function calculate(array $from, array $to) + { + $cFrom = \count($from); + $cTo = \count($to); + + if ($cFrom === 0) { + return []; + } + + if ($cFrom === 1) { + if (\in_array($from[0], $to, true)) { + return [$from[0]]; + } + + return []; + } + + $i = (int) ($cFrom / 2); + $fromStart = \array_slice($from, 0, $i); + $fromEnd = \array_slice($from, $i); + $llB = $this->length($fromStart, $to); + $llE = $this->length(\array_reverse($fromEnd), \array_reverse($to)); + $jMax = 0; + $max = 0; + + for ($j = 0; $j <= $cTo; $j++) { + $m = $llB[$j] + $llE[$cTo - $j]; + + if ($m >= $max) { + $max = $m; + $jMax = $j; + } + } + + $toStart = \array_slice($to, 0, $jMax); + $toEnd = \array_slice($to, $jMax); + + return \array_merge( + $this->calculate($fromStart, $toStart), + $this->calculate($fromEnd, $toEnd) + ); + } + + private function length(array $from, array $to) + { + $current = \array_fill(0, \count($to) + 1, 0); + $cFrom = \count($from); + $cTo = \count($to); + + for ($i = 0; $i < $cFrom; $i++) { + $prev = $current; + + for ($j = 0; $j < $cTo; $j++) { + if ($from[$i] === $to[$j]) { + $current[$j + 1] = $prev[$j] + 1; + } else { + $current[$j + 1] = \max($current[$j], $prev[$j + 1]); + } + } + } + + return $current; + } +} diff --git a/src/v3_0/Output/AbstractChunkOutputBuilder.php b/src/v3_0/Output/AbstractChunkOutputBuilder.php new file mode 100644 index 00000000..ecf7d321 --- /dev/null +++ b/src/v3_0/Output/AbstractChunkOutputBuilder.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +abstract class AbstractChunkOutputBuilder implements DiffOutputBuilderInterface +{ + /** + * Takes input of the diff array and returns the common parts. + * Iterates through diff line by line. + * + * @param array $diff + * @param int $lineThreshold + * + * @return array + */ + protected function getCommonChunks(array $diff, $lineThreshold = 5) + { + $diffSize = \count($diff); + $capturing = false; + $chunkStart = 0; + $chunkSize = 0; + $commonChunks = []; + + for ($i = 0; $i < $diffSize; ++$i) { + if ($diff[$i][1] === 0 /* OLD */) { + if ($capturing === false) { + $capturing = true; + $chunkStart = $i; + $chunkSize = 0; + } else { + ++$chunkSize; + } + } elseif ($capturing !== false) { + if ($chunkSize >= $lineThreshold) { + $commonChunks[$chunkStart] = $chunkStart + $chunkSize; + } + + $capturing = false; + } + } + + if ($capturing !== false && $chunkSize >= $lineThreshold) { + $commonChunks[$chunkStart] = $chunkStart + $chunkSize; + } + + return $commonChunks; + } +} diff --git a/src/v3_0/Output/DiffOnlyOutputBuilder.php b/src/v3_0/Output/DiffOnlyOutputBuilder.php new file mode 100644 index 00000000..0f3b81f5 --- /dev/null +++ b/src/v3_0/Output/DiffOnlyOutputBuilder.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +use PhpCsFixer\Diff\v3_0\Differ; + +/** + * Builds a diff string representation in a loose unified diff format + * listing only changes lines. Does not include line numbers. + */ +final class DiffOnlyOutputBuilder implements DiffOutputBuilderInterface +{ + /** + * @var string + */ + private $header; + + public function __construct($header = "--- Original\n+++ New\n") + { + $this->header = $header; + } + + public function getDiff(array $diff) + { + $buffer = \fopen('php://memory', 'r+b'); + + if ('' !== $this->header) { + \fwrite($buffer, $this->header); + if ("\n" !== \substr($this->header, -1, 1)) { + \fwrite($buffer, "\n"); + } + } + + foreach ($diff as $diffEntry) { + if ($diffEntry[1] === Differ::ADDED) { + \fwrite($buffer, '+' . $diffEntry[0]); + } elseif ($diffEntry[1] === Differ::REMOVED) { + \fwrite($buffer, '-' . $diffEntry[0]); + } 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 + } else { /* Not changed (old) 0 */ + continue; // we didn't write the non changs line, so do not add a line break either + } + + $lc = \substr($diffEntry[0], -1); + if ($lc !== "\n" && $lc !== "\r") { + \fwrite($buffer, "\n"); // \No newline at end of file + } + } + + $diff = \stream_get_contents($buffer, -1, 0); + \fclose($buffer); + + return $diff; + } +} diff --git a/src/v3_0/Output/DiffOutputBuilderInterface.php b/src/v3_0/Output/DiffOutputBuilderInterface.php new file mode 100644 index 00000000..ae690e6a --- /dev/null +++ b/src/v3_0/Output/DiffOutputBuilderInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +/** + * Defines how an output builder should take a generated + * diff array and return a string representation of that diff. + */ +interface DiffOutputBuilderInterface +{ + public function getDiff(array $diff); +} diff --git a/src/v3_0/Output/StrictUnifiedDiffOutputBuilder.php b/src/v3_0/Output/StrictUnifiedDiffOutputBuilder.php new file mode 100644 index 00000000..49faa8ad --- /dev/null +++ b/src/v3_0/Output/StrictUnifiedDiffOutputBuilder.php @@ -0,0 +1,315 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +use PhpCsFixer\Diff\v3_0\ConfigurationException; +use PhpCsFixer\Diff\v3_0\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/v3_0/Output/UnifiedDiffOutputBuilder.php b/src/v3_0/Output/UnifiedDiffOutputBuilder.php new file mode 100644 index 00000000..5e7276ab --- /dev/null +++ b/src/v3_0/Output/UnifiedDiffOutputBuilder.php @@ -0,0 +1,259 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +use PhpCsFixer\Diff\v3_0\Differ; + +/** + * Builds a diff string representation in unified diff format in chunks. + */ +final class UnifiedDiffOutputBuilder extends AbstractChunkOutputBuilder +{ + /** + * @var bool + */ + private $collapseRanges = true; + + /** + * @var int >= 0 + */ + private $commonLineThreshold = 6; + + /** + * @var int >= 0 + */ + private $contextLines = 3; + + /** + * @var string + */ + private $header; + + /** + * @var bool + */ + private $addLineNumbers; + + public function __construct($header = "--- Original\n+++ New\n", $addLineNumbers = false) + { + $this->header = $header; + $this->addLineNumbers = $addLineNumbers; + } + + public function getDiff(array $diff) + { + $buffer = \fopen('php://memory', 'r+b'); + + if ('' !== $this->header) { + \fwrite($buffer, $this->header); + if ("\n" !== \substr($this->header, -1, 1)) { + \fwrite($buffer, "\n"); + } + } + + if (0 !== \count($diff)) { + $this->writeDiffHunks($buffer, $diff); + } + + $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; + } + + 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 writeHunk( + array $diff, + $diffStartIndex, + $diffEndIndex, + $fromStart, + $fromRange, + $toStart, + $toRange, + $output + ) { + if ($this->addLineNumbers) { + \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"); + } else { + \fwrite($output, "@@ @@\n"); + } + + for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) { + if ($diff[$i][1] === Differ::ADDED) { + \fwrite($output, '+' . $diff[$i][0]); + } elseif ($diff[$i][1] === Differ::REMOVED) { + \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) { + \fwrite($output, "\n"); // $diff[$i][0] + } else { /* Not changed (old) Differ::OLD or Warning Differ::DIFF_LINE_END_WARNING */ + \fwrite($output, ' ' . $diff[$i][0]); + } + } + } +} diff --git a/src/v3_0/Parser.php b/src/v3_0/Parser.php new file mode 100644 index 00000000..aa7175a3 --- /dev/null +++ b/src/v3_0/Parser.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +/** + * Unified diff parser. + */ +final class Parser +{ + /** + * @param string $string + * + * @return Diff[] + */ + public function parse($string) + { + $lines = \preg_split('(\r\n|\r|\n)', $string); + + if (!empty($lines) && $lines[\count($lines) - 1] === '') { + \array_pop($lines); + } + + $lineCount = \count($lines); + $diffs = []; + $diff = null; + $collected = []; + + for ($i = 0; $i < $lineCount; ++$i) { + if (\preg_match('(^---\\s+(?P\\S+))', $lines[$i], $fromMatch) && + \preg_match('(^\\+\\+\\+\\s+(?P\\S+))', $lines[$i + 1], $toMatch)) { + if ($diff !== null) { + $this->parseFileDiff($diff, $collected); + + $diffs[] = $diff; + $collected = []; + } + + $diff = new Diff($fromMatch['file'], $toMatch['file']); + + ++$i; + } else { + if (\preg_match('/^(?:diff --git |index [\da-f\.]+|[+-]{3} [ab])/', $lines[$i])) { + continue; + } + + $collected[] = $lines[$i]; + } + } + + if ($diff !== null && \count($collected)) { + $this->parseFileDiff($diff, $collected); + + $diffs[] = $diff; + } + + return $diffs; + } + + private function parseFileDiff(Diff $diff, array $lines) + { + $chunks = []; + $chunk = null; + + foreach ($lines as $line) { + if (\preg_match('/^@@\s+-(?P\d+)(?:,\s*(?P\d+))?\s+\+(?P\d+)(?:,\s*(?P\d+))?\s+@@/', $line, $match)) { + $chunk = new Chunk( + (int) $match['start'], + isset($match['startrange']) ? \max(1, (int) $match['startrange']) : 1, + (int) $match['end'], + isset($match['endrange']) ? \max(1, (int) $match['endrange']) : 1 + ); + + $chunks[] = $chunk; + $diffLines = []; + + continue; + } + + if (\preg_match('/^(?P[+ -])?(?P.*)/', $line, $match)) { + $type = Line::UNCHANGED; + + if ($match['type'] === '+') { + $type = Line::ADDED; + } elseif ($match['type'] === '-') { + $type = Line::REMOVED; + } + + $diffLines[] = new Line($type, $match['line']); + + if (null !== $chunk) { + $chunk->setLines($diffLines); + } + } + } + + $diff->setChunks($chunks); + } +} diff --git a/src/v3_0/TimeEfficientLongestCommonSubsequenceCalculator.php b/src/v3_0/TimeEfficientLongestCommonSubsequenceCalculator.php new file mode 100644 index 00000000..644968ac --- /dev/null +++ b/src/v3_0/TimeEfficientLongestCommonSubsequenceCalculator.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +final class TimeEfficientLongestCommonSubsequenceCalculator implements LongestCommonSubsequenceCalculator +{ + /** + * {@inheritdoc} + */ + public function calculate(array $from, array $to) + { + $common = []; + $fromLength = \count($from); + $toLength = \count($to); + $width = $fromLength + 1; + $matrix = new \SplFixedArray($width * ($toLength + 1)); + + for ($i = 0; $i <= $fromLength; ++$i) { + $matrix[$i] = 0; + } + + for ($j = 0; $j <= $toLength; ++$j) { + $matrix[$j * $width] = 0; + } + + for ($i = 1; $i <= $fromLength; ++$i) { + for ($j = 1; $j <= $toLength; ++$j) { + $o = ($j * $width) + $i; + $matrix[$o] = \max( + $matrix[$o - 1], + $matrix[$o - $width], + $from[$i - 1] === $to[$j - 1] ? $matrix[$o - $width - 1] + 1 : 0 + ); + } + } + + $i = $fromLength; + $j = $toLength; + + while ($i > 0 && $j > 0) { + if ($from[$i - 1] === $to[$j - 1]) { + $common[] = $from[$i - 1]; + --$i; + --$j; + } else { + $o = ($j * $width) + $i; + + if ($matrix[$o - $width] > $matrix[$o - 1]) { + --$j; + } else { + --$i; + } + } + } + + return \array_reverse($common); + } +} diff --git a/tests/v1_4/DifferTest.php b/tests/v1_4/DifferTest.php index 6f9c41bd..3c382a77 100644 --- a/tests/v1_4/DifferTest.php +++ b/tests/v1_4/DifferTest.php @@ -227,7 +227,7 @@ public function arrayProvider() 'test line diff detection' => array( array( array( - '#Warning: Strings contain different line endings!', + '#Warnings contain different line endings!', self::OLD, ), array( diff --git a/tests/v2_0/DifferTest.php b/tests/v2_0/DifferTest.php index 7c4b6b22..0283897f 100644 --- a/tests/v2_0/DifferTest.php +++ b/tests/v2_0/DifferTest.php @@ -269,7 +269,7 @@ public function arrayProvider() 'test line diff detection' => [ [ [ - "#Warning: Strings contain different line endings!\n", + "#Warnings contain different line endings!\n", self::WARNING, ], [ @@ -287,7 +287,7 @@ public function arrayProvider() 'test line diff detection in array input' => [ [ [ - "#Warning: Strings contain different line endings!\n", + "#Warnings contain different line endings!\n", self::WARNING, ], [ @@ -384,12 +384,12 @@ public function textProvider() "B\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1\n1", ], [ - "--- Original\n+++ New\n@@ @@\n #Warning: Strings contain different line endings!\n- [ - "--- Original\n+++ New\n@@ -1 +1 @@\n #Warning: Strings contain different line endings!\n- + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\v3_0\Chunk + */ +final class ChunkTest extends TestCase +{ + /** + * @var Chunk + */ + private $chunk; + + protected function setUp() + { + $this->chunk = new Chunk; + } + + public function testCanBeCreatedWithoutArguments() + { + $this->assertInstanceOf(Chunk::class, $this->chunk); + } + + public function testStartCanBeRetrieved() + { + $this->assertSame(0, $this->chunk->getStart()); + } + + public function testStartRangeCanBeRetrieved() + { + $this->assertSame(1, $this->chunk->getStartRange()); + } + + public function testEndCanBeRetrieved() + { + $this->assertSame(0, $this->chunk->getEnd()); + } + + public function testEndRangeCanBeRetrieved() + { + $this->assertSame(1, $this->chunk->getEndRange()); + } + + public function testLinesCanBeRetrieved() + { + $this->assertSame([], $this->chunk->getLines()); + } + + public function testLinesCanBeSet() + { + $this->assertSame([], $this->chunk->getLines()); + + $testValue = ['line0', 'line1']; + $this->chunk->setLines($testValue); + $this->assertSame($testValue, $this->chunk->getLines()); + } +} diff --git a/tests/v3_0/DiffTest.php b/tests/v3_0/DiffTest.php new file mode 100644 index 00000000..7cc658e1 --- /dev/null +++ b/tests/v3_0/DiffTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\v3_0\Diff + * + * @uses PhpCsFixer\Diff\v3_0\Chunk + */ +final class DiffTest extends TestCase +{ + public function testGettersAfterConstructionWithDefault() + { + $from = 'line1a'; + $to = 'line2a'; + $diff = new Diff($from, $to); + + $this->assertSame($from, $diff->getFrom()); + $this->assertSame($to, $diff->getTo()); + $this->assertSame([], $diff->getChunks(), 'Expect chunks to be default value "array()".'); + } + + public function testGettersAfterConstructionWithChunks() + { + $from = 'line1b'; + $to = 'line2b'; + $chunks = [new Chunk(), new Chunk(2, 3)]; + + $diff = new Diff($from, $to, $chunks); + + $this->assertSame($from, $diff->getFrom()); + $this->assertSame($to, $diff->getTo()); + $this->assertSame($chunks, $diff->getChunks(), 'Expect chunks to be passed value.'); + } + + public function testSetChunksAfterConstruction() + { + $diff = new Diff('line1c', 'line2c'); + $this->assertSame([], $diff->getChunks(), 'Expect chunks to be default value "array()".'); + + $chunks = [new Chunk(), new Chunk(2, 3)]; + $diff->setChunks($chunks); + $this->assertSame($chunks, $diff->getChunks(), 'Expect chunks to be passed value.'); + } +} diff --git a/tests/v3_0/DifferTest.php b/tests/v3_0/DifferTest.php new file mode 100644 index 00000000..86c5103f --- /dev/null +++ b/tests/v3_0/DifferTest.php @@ -0,0 +1,462 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\Output\UnifiedDiffOutputBuilder; + +/** + * @covers PhpCsFixer\Diff\v3_0\Differ + * @covers PhpCsFixer\Diff\v3_0\Output\UnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\v3_0\MemoryEfficientLongestCommonSubsequenceCalculator + * @uses PhpCsFixer\Diff\v3_0\TimeEfficientLongestCommonSubsequenceCalculator + * @uses PhpCsFixer\Diff\v3_0\Output\AbstractChunkOutputBuilder + */ +final class DifferTest extends TestCase +{ + /** + * @var Differ + */ + private $differ; + + protected function setUp() + { + $this->differ = new Differ; + } + + /** + * @param array $expected + * @param array|string $from + * @param array|string $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($expected, $from, $to) + { + $this->assertSame($expected, $this->differ->diff($from, $to, new TimeEfficientLongestCommonSubsequenceCalculator)); + } + + /** + * @param array $expected + * @param array|string $from + * @param array|string $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($expected, $from, $to) + { + $this->assertSame($expected, $this->differ->diff($from, $to, new MemoryEfficientLongestCommonSubsequenceCalculator)); + } + + public function testTypesOtherThanArrayAndStringCanBePassed() + { + $this->assertSame( + "--- Original\n+++ New\n@@ @@\n-1\n+2\n", + $this->differ->diff(1, 2) + ); + } + + public function testArrayDiffs() + { + $this->assertSame( + '--- Original ++++ New +@@ @@ +-one ++two +', + $this->differ->diff(['one'], ['two']) + ); + } + + public function arrayProvider() + { + return [ + [ + [ + ['a', Differ::REMOVED], + ['b', Differ::ADDED], + ], + 'a', + 'b', + ], + [ + [ + ['ba', Differ::REMOVED], + ['bc', Differ::ADDED], + ], + 'ba', + 'bc', + ], + [ + [ + ['ab', Differ::REMOVED], + ['cb', Differ::ADDED], + ], + 'ab', + 'cb', + ], + [ + [ + ['abc', Differ::REMOVED], + ['adc', Differ::ADDED], + ], + 'abc', + 'adc', + ], + [ + [ + ['ab', Differ::REMOVED], + ['abc', Differ::ADDED], + ], + 'ab', + 'abc', + ], + [ + [ + ['bc', Differ::REMOVED], + ['abc', Differ::ADDED], + ], + 'bc', + 'abc', + ], + [ + [ + ['abc', Differ::REMOVED], + ['abbc', Differ::ADDED], + ], + 'abc', + 'abbc', + ], + [ + [ + ['abcdde', Differ::REMOVED], + ['abcde', Differ::ADDED], + ], + 'abcdde', + 'abcde', + ], + 'same start' => [ + [ + [17, Differ::OLD], + ['b', Differ::REMOVED], + ['d', Differ::ADDED], + ], + [30 => 17, 'a' => 'b'], + [30 => 17, 'c' => 'd'], + ], + 'same end' => [ + [ + [1, Differ::REMOVED], + [2, Differ::ADDED], + ['b', Differ::OLD], + ], + [1 => 1, 'a' => 'b'], + [1 => 2, 'a' => 'b'], + ], + 'same start (2), same end (1)' => [ + [ + [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', Differ::OLD], + ], + ['z' => 'x'], + ['z' => 'x'], + ], + 'diff' => [ + [ + ['y', Differ::REMOVED], + ['x', Differ::ADDED], + ], + ['x' => 'y'], + ['z' => 'x'], + ], + 'diff 2' => [ + [ + ['y', Differ::REMOVED], + ['b', Differ::REMOVED], + ['x', Differ::ADDED], + ['d', Differ::ADDED], + ], + ['x' => 'y', 'a' => 'b'], + ['z' => 'x', 'c' => 'd'], + ], + 'test line diff detection' => [ + [ + [ + "#Warnings contain different line endings!\n", + Differ::DIFF_LINE_END_WARNING, + ], + [ + " [ + [ + [ + "#Warnings contain different line endings!\n", + Differ::DIFF_LINE_END_WARNING, + ], + [ + "expectException(InvalidArgumentException::class); + $this->expectExceptionMessageRegExp('#^"from" must be an array or string\.$#'); + + $this->differ->diffToArray(null, ''); + } + + public function testDiffInvalidToType() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageRegExp('#^"to" must be an array or string\.$#'); + + $this->differ->diffToArray('', new \stdClass); + } + + /** + * @param array $expected + * @param string $input + * + * @dataProvider provideSplitStringByLinesCases + */ + public function testSplitStringByLines(array $expected, $input) + { + $reflection = new \ReflectionObject($this->differ); + $method = $reflection->getMethod('splitStringByLines'); + $method->setAccessible(true); + + $this->assertSame($expected, $method->invoke($this->differ, $input)); + } + + public function provideSplitStringByLinesCases() + { + 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", + ], + ]; + } + + public function testConstructorNull() + { + $this->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__)); + } +} diff --git a/tests/v3_0/Exception/ConfigurationExceptionTest.php b/tests/v3_0/Exception/ConfigurationExceptionTest.php new file mode 100644 index 00000000..63bfa572 --- /dev/null +++ b/tests/v3_0/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\v3_0; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\v3_0\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/v3_0/Exception/InvalidArgumentExceptionTest.php b/tests/v3_0/Exception/InvalidArgumentExceptionTest.php new file mode 100644 index 00000000..6bce2b63 --- /dev/null +++ b/tests/v3_0/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\v3_0; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\v3_0\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/v3_0/LineTest.php b/tests/v3_0/LineTest.php new file mode 100644 index 00000000..fe356c09 --- /dev/null +++ b/tests/v3_0/LineTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\v3_0\Line + */ +final class LineTest extends TestCase +{ + /** + * @var Line + */ + private $line; + + protected function setUp() + { + $this->line = new Line; + } + + public function testCanBeCreatedWithoutArguments() + { + $this->assertInstanceOf(Line::class, $this->line); + } + + public function testTypeCanBeRetrieved() + { + $this->assertSame(Line::UNCHANGED, $this->line->getType()); + } + + public function testContentCanBeRetrieved() + { + $this->assertSame('', $this->line->getContent()); + } +} diff --git a/tests/v3_0/LongestCommonSubsequenceTest.php b/tests/v3_0/LongestCommonSubsequenceTest.php new file mode 100644 index 00000000..2d074b51 --- /dev/null +++ b/tests/v3_0/LongestCommonSubsequenceTest.php @@ -0,0 +1,201 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +use PHPUnit\Framework\TestCase; + +/** + * @coversNothing + */ +abstract class LongestCommonSubsequenceTest extends TestCase +{ + /** + * @var LongestCommonSubsequenceCalculator + */ + private $implementation; + + /** + * @var string + */ + private $memoryLimit; + + /** + * @var int[] + */ + private $stress_sizes = [1, 2, 3, 100, 500, 1000, 2000]; + + protected function setUp() + { + $this->memoryLimit = \ini_get('memory_limit'); + \ini_set('memory_limit', '256M'); + + $this->implementation = $this->createImplementation(); + } + + protected function tearDown() + { + \ini_set('memory_limit', $this->memoryLimit); + } + + public function testBothEmpty() + { + $from = []; + $to = []; + $common = $this->implementation->calculate($from, $to); + + $this->assertSame([], $common); + } + + public function testIsStrictComparison() + { + $from = [ + false, 0, 0.0, '', null, [], + true, 1, 1.0, 'foo', ['foo', 'bar'], ['foo' => 'bar'], + ]; + $to = $from; + $common = $this->implementation->calculate($from, $to); + + $this->assertSame($from, $common); + + $to = [ + false, false, false, false, false, false, + true, true, true, true, true, true, + ]; + + $expected = [ + false, + true, + ]; + + $common = $this->implementation->calculate($from, $to); + + $this->assertSame($expected, $common); + } + + public function testEqualSequences() + { + foreach ($this->stress_sizes as $size) { + $range = \range(1, $size); + $from = $range; + $to = $range; + $common = $this->implementation->calculate($from, $to); + + $this->assertSame($range, $common); + } + } + + public function testDistinctSequences() + { + $from = ['A']; + $to = ['B']; + $common = $this->implementation->calculate($from, $to); + $this->assertSame([], $common); + + $from = ['A', 'B', 'C']; + $to = ['D', 'E', 'F']; + $common = $this->implementation->calculate($from, $to); + $this->assertSame([], $common); + + foreach ($this->stress_sizes as $size) { + $from = \range(1, $size); + $to = \range($size + 1, $size * 2); + $common = $this->implementation->calculate($from, $to); + $this->assertSame([], $common); + } + } + + public function testCommonSubsequence() + { + $from = ['A', 'C', 'E', 'F', 'G']; + $to = ['A', 'B', 'D', 'E', 'H']; + $expected = ['A', 'E']; + $common = $this->implementation->calculate($from, $to); + $this->assertSame($expected, $common); + + $from = ['A', 'C', 'E', 'F', 'G']; + $to = ['B', 'C', 'D', 'E', 'F', 'H']; + $expected = ['C', 'E', 'F']; + $common = $this->implementation->calculate($from, $to); + $this->assertSame($expected, $common); + + foreach ($this->stress_sizes as $size) { + $from = $size < 2 ? [1] : \range(1, $size + 1, 2); + $to = $size < 3 ? [1] : \range(1, $size + 1, 3); + $expected = $size < 6 ? [1] : \range(1, $size + 1, 6); + $common = $this->implementation->calculate($from, $to); + + $this->assertSame($expected, $common); + } + } + + public function testSingleElementSubsequenceAtStart() + { + foreach ($this->stress_sizes as $size) { + $from = \range(1, $size); + $to = \array_slice($from, 0, 1); + $common = $this->implementation->calculate($from, $to); + + $this->assertSame($to, $common); + } + } + + public function testSingleElementSubsequenceAtMiddle() + { + foreach ($this->stress_sizes as $size) { + $from = \range(1, $size); + $to = \array_slice($from, (int) ($size / 2), 1); + $common = $this->implementation->calculate($from, $to); + + $this->assertSame($to, $common); + } + } + + public function testSingleElementSubsequenceAtEnd() + { + foreach ($this->stress_sizes as $size) { + $from = \range(1, $size); + $to = \array_slice($from, $size - 1, 1); + $common = $this->implementation->calculate($from, $to); + + $this->assertSame($to, $common); + } + } + + public function testReversedSequences() + { + $from = ['A', 'B']; + $to = ['B', 'A']; + $expected = ['A']; + $common = $this->implementation->calculate($from, $to); + $this->assertSame($expected, $common); + + foreach ($this->stress_sizes as $size) { + $from = \range(1, $size); + $to = \array_reverse($from); + $common = $this->implementation->calculate($from, $to); + + $this->assertSame([1], $common); + } + } + + public function testStrictTypeCalculate() + { + $diff = $this->implementation->calculate(['5'], ['05']); + + $this->assertInternalType('array', $diff); + $this->assertCount(0, $diff); + } + + /** + * @return LongestCommonSubsequenceCalculator + */ + abstract protected function createImplementation(); +} diff --git a/tests/v3_0/MemoryEfficientImplementationTest.php b/tests/v3_0/MemoryEfficientImplementationTest.php new file mode 100644 index 00000000..41f1f2f5 --- /dev/null +++ b/tests/v3_0/MemoryEfficientImplementationTest.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +/** + * @covers PhpCsFixer\Diff\v3_0\MemoryEfficientLongestCommonSubsequenceCalculator + */ +final class MemoryEfficientImplementationTest extends LongestCommonSubsequenceTest +{ + protected function createImplementation() + { + return new MemoryEfficientLongestCommonSubsequenceCalculator; + } +} diff --git a/tests/v3_0/Output/AbstractChunkOutputBuilderTest.php b/tests/v3_0/Output/AbstractChunkOutputBuilderTest.php new file mode 100644 index 00000000..3f905edf --- /dev/null +++ b/tests/v3_0/Output/AbstractChunkOutputBuilderTest.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\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\v3_0\Output\AbstractChunkOutputBuilder + * + * @uses PhpCsFixer\Diff\v3_0\Differ + * @uses PhpCsFixer\Diff\v3_0\Output\UnifiedDiffOutputBuilder + * @uses PhpCsFixer\Diff\v3_0\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/v3_0/Output/DiffOnlyOutputBuilderTest.php b/tests/v3_0/Output/DiffOnlyOutputBuilderTest.php new file mode 100644 index 00000000..9fd0cbee --- /dev/null +++ b/tests/v3_0/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\v3_0\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\Differ; + +/** + * @covers PhpCsFixer\Diff\v3_0\Output\DiffOnlyOutputBuilder + * + * @uses PhpCsFixer\Diff\v3_0\Differ + * @uses PhpCsFixer\Diff\v3_0\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/v3_0/Output/Integration/StrictUnifiedDiffOutputBuilderIntegrationTest.php b/tests/v3_0/Output/Integration/StrictUnifiedDiffOutputBuilderIntegrationTest.php new file mode 100644 index 00000000..93e105f7 --- /dev/null +++ b/tests/v3_0/Output/Integration/StrictUnifiedDiffOutputBuilderIntegrationTest.php @@ -0,0 +1,299 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\Differ; +use PhpCsFixer\Diff\v3_0\Utils\FileUtils; +use PhpCsFixer\Diff\v3_0\Utils\UnifiedDiffAssertTrait; +use Symfony\Component\Process\Process; + +/** + * @covers PhpCsFixer\Diff\v3_0\Output\StrictUnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\v3_0\Differ + * @uses PhpCsFixer\Diff\v3_0\TimeEfficientLongestCommonSubsequenceCalculator + * + * @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/v3_0/Output/Integration/UnifiedDiffOutputBuilderIntegrationTest.php b/tests/v3_0/Output/Integration/UnifiedDiffOutputBuilderIntegrationTest.php new file mode 100644 index 00000000..c6b51ab0 --- /dev/null +++ b/tests/v3_0/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\v3_0\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\Utils\UnifiedDiffAssertTrait; +use Symfony\Component\Process\Process; + +/** + * @covers PhpCsFixer\Diff\v3_0\Output\UnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\v3_0\Differ + * @uses PhpCsFixer\Diff\v3_0\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/v3_0/Output/StrictUnifiedDiffOutputBuilderDataProvider.php b/tests/v3_0/Output/StrictUnifiedDiffOutputBuilderDataProvider.php new file mode 100644 index 00000000..984d7401 --- /dev/null +++ b/tests/v3_0/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\v3_0\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/v3_0/Output/StrictUnifiedDiffOutputBuilderTest.php b/tests/v3_0/Output/StrictUnifiedDiffOutputBuilderTest.php new file mode 100644 index 00000000..2131cd41 --- /dev/null +++ b/tests/v3_0/Output/StrictUnifiedDiffOutputBuilderTest.php @@ -0,0 +1,684 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\ConfigurationException; +use PhpCsFixer\Diff\v3_0\Differ; +use PhpCsFixer\Diff\v3_0\Utils\UnifiedDiffAssertTrait; + +/** + * @covers PhpCsFixer\Diff\v3_0\Output\StrictUnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\v3_0\Differ + */ +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 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/v3_0/Output/UnifiedDiffOutputBuilderDataProvider.php b/tests/v3_0/Output/UnifiedDiffOutputBuilderDataProvider.php new file mode 100644 index 00000000..da5dc271 --- /dev/null +++ b/tests/v3_0/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\v3_0\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\v3_0\Output; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\Differ; + +/** + * @covers PhpCsFixer\Diff\v3_0\Output\UnifiedDiffOutputBuilder + * + * @uses PhpCsFixer\Diff\v3_0\Differ + * @uses PhpCsFixer\Diff\v3_0\Output\AbstractChunkOutputBuilder + * @uses PhpCsFixer\Diff\v3_0\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(); + } +} diff --git a/tests/v3_0/ParserTest.php b/tests/v3_0/ParserTest.php new file mode 100644 index 00000000..53f3a3ac --- /dev/null +++ b/tests/v3_0/ParserTest.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +use PHPUnit\Framework\TestCase; +use PhpCsFixer\Diff\v3_0\Utils\FileUtils; + +/** + * @covers PhpCsFixer\Diff\v3_0\Parser + * + * @uses PhpCsFixer\Diff\v3_0\Chunk + * @uses PhpCsFixer\Diff\v3_0\Diff + * @uses PhpCsFixer\Diff\v3_0\Line + */ +final class ParserTest extends TestCase +{ + /** + * @var Parser + */ + private $parser; + + protected function setUp() + { + $this->parser = new Parser; + } + + public function testParse() + { + $content = FileUtils::getFileContent(__DIR__ . '/fixtures/patch.txt'); + + $diffs = $this->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); + + $this->assertSame(20, $chunks[0]->getStart()); + + $this->assertCount(4, $chunks[0]->getLines()); + } + + public function testParseWithMultipleChunks() + { + $content = FileUtils::getFileContent(__DIR__ . '/fixtures/patch2.txt'); + + $diffs = $this->parser->parse($content); + + $this->assertCount(1, $diffs); + + $chunks = $diffs[0]->getChunks(); + $this->assertCount(3, $chunks); + + $this->assertSame(20, $chunks[0]->getStart()); + $this->assertSame(320, $chunks[1]->getStart()); + $this->assertSame(600, $chunks[2]->getStart()); + + $this->assertCount(5, $chunks[0]->getLines()); + $this->assertCount(5, $chunks[1]->getLines()); + $this->assertCount(4, $chunks[2]->getLines()); + } + + 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); + + $chunk = $chunks[0]; + $this->assertSame(49, $chunk->getStart()); + $this->assertSame(49, $chunk->getEnd()); + $this->assertSame(9, $chunk->getStartRange()); + $this->assertSame(8, $chunk->getEndRange()); + + $lines = $chunk->getLines(); + $this->assertInternalType('array', $lines); + $this->assertContainsOnlyInstancesOf(Line::class, $lines); + $this->assertCount(2, $lines); + + /** @var Line $line */ + $line = $lines[0]; + $this->assertSame('A', $line->getContent()); + $this->assertSame(Line::UNCHANGED, $line->getType()); + + $line = $lines[1]; + $this->assertSame('B', $line->getContent()); + $this->assertSame(Line::REMOVED, $line->getType()); + } + + public function testParseDiffForMulitpleFiles() + { + $content = <<parser->parse($content); + $this->assertCount(2, $diffs); + + /** @var Diff $diff */ + $diff = $diffs[0]; + $this->assertSame('a/Test.txt', $diff->getFrom()); + $this->assertSame('b/Test.txt', $diff->getTo()); + $this->assertCount(1, $diff->getChunks()); + + $diff = $diffs[1]; + $this->assertSame('a/Test2.txt', $diff->getFrom()); + $this->assertSame('b/Test2.txt', $diff->getTo()); + $this->assertCount(1, $diff->getChunks()); + } +} diff --git a/tests/v3_0/TimeEfficientImplementationTest.php b/tests/v3_0/TimeEfficientImplementationTest.php new file mode 100644 index 00000000..a68c6cc1 --- /dev/null +++ b/tests/v3_0/TimeEfficientImplementationTest.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0; + +/** + * @covers PhpCsFixer\Diff\v3_0\TimeEfficientLongestCommonSubsequenceCalculator + */ +final class TimeEfficientImplementationTest extends LongestCommonSubsequenceTest +{ + protected function createImplementation() + { + return new TimeEfficientLongestCommonSubsequenceCalculator; + } +} diff --git a/tests/v3_0/Utils/FileUtils.php b/tests/v3_0/Utils/FileUtils.php new file mode 100644 index 00000000..4ce4edd1 --- /dev/null +++ b/tests/v3_0/Utils/FileUtils.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\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/v3_0/Utils/UnifiedDiffAssertTrait.php b/tests/v3_0/Utils/UnifiedDiffAssertTrait.php new file mode 100644 index 00000000..cc3ca641 --- /dev/null +++ b/tests/v3_0/Utils/UnifiedDiffAssertTrait.php @@ -0,0 +1,274 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace PhpCsFixer\Diff\v3_0\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/v3_0/Utils/UnifiedDiffAssertTraitIntegrationTest.php b/tests/v3_0/Utils/UnifiedDiffAssertTraitIntegrationTest.php new file mode 100644 index 00000000..d49a206b --- /dev/null +++ b/tests/v3_0/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\v3_0\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/v3_0/Utils/UnifiedDiffAssertTraitTest.php b/tests/v3_0/Utils/UnifiedDiffAssertTraitTest.php new file mode 100644 index 00000000..882e1f26 --- /dev/null +++ b/tests/v3_0/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\v3_0\Utils; + +use PHPUnit\Framework\TestCase; + +/** + * @covers PhpCsFixer\Diff\v3_0\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/v3_0/fixtures/.editorconfig b/tests/v3_0/fixtures/.editorconfig new file mode 100644 index 00000000..78b36ca0 --- /dev/null +++ b/tests/v3_0/fixtures/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt b/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt new file mode 100644 index 00000000..2e65efe2 --- /dev/null +++ b/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_a.txt @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_b.txt b/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/1_b.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt b/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_a.txt new file mode 100644 index 00000000..c7fe26e9 --- /dev/null +++ b/tests/v3_0/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/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt b/tests/v3_0/fixtures/UnifiedDiffAssertTraitIntegrationTest/2_b.txt new file mode 100644 index 00000000..377a70f8 --- /dev/null +++ b/tests/v3_0/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/v3_0/fixtures/out/.editorconfig b/tests/v3_0/fixtures/out/.editorconfig new file mode 100644 index 00000000..78b36ca0 --- /dev/null +++ b/tests/v3_0/fixtures/out/.editorconfig @@ -0,0 +1 @@ +root = true diff --git a/tests/v3_0/fixtures/out/.gitignore b/tests/v3_0/fixtures/out/.gitignore new file mode 100644 index 00000000..f6f7a478 --- /dev/null +++ b/tests/v3_0/fixtures/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/v3_0/fixtures/patch.txt b/tests/v3_0/fixtures/patch.txt new file mode 100644 index 00000000..144b61d0 --- /dev/null +++ b/tests/v3_0/fixtures/patch.txt @@ -0,0 +1,9 @@ +diff --git a/Foo.php b/Foo.php +index abcdefg..abcdefh 100644 +--- a/Foo.php ++++ b/Foo.php +@@ -20,4 +20,5 @@ class Foo + const ONE = 1; + const TWO = 2; ++ const THREE = 3; + const FOUR = 4; diff --git a/tests/v3_0/fixtures/patch2.txt b/tests/v3_0/fixtures/patch2.txt new file mode 100644 index 00000000..41fbc959 --- /dev/null +++ b/tests/v3_0/fixtures/patch2.txt @@ -0,0 +1,21 @@ +diff --git a/Foo.php b/Foo.php +index abcdefg..abcdefh 100644 +--- a/Foo.php ++++ b/Foo.php +@@ -20,4 +20,5 @@ class Foo + const ONE = 1; + const TWO = 2; ++ const THREE = 3; + const FOUR = 4; + +@@ -320,4 +320,5 @@ class Foo + const A = 'A'; + const B = 'B'; ++ const C = 'C'; + const D = 'D'; + +@@ -600,4 +600,5 @@ class Foo + public function doSomething() { + ++ return 'foo'; + }