diff --git a/packages/testing/composer.json b/packages/testing/composer.json index 6392fa1e1..0d906140b 100644 --- a/packages/testing/composer.json +++ b/packages/testing/composer.json @@ -23,6 +23,7 @@ "ext-json": "*", "ock/class-discovery": "self.version", "ock/helpers": "self.version", + "fisharebest/algorithm": "^1.6", "symfony/yaml": "^7.1.5" }, "require-dev": { diff --git a/packages/testing/src/Diff/ExportedArrayDiffer.php b/packages/testing/src/Diff/ExportedArrayDiffer.php index 2e4ceb23c..1adc38125 100644 --- a/packages/testing/src/Diff/ExportedArrayDiffer.php +++ b/packages/testing/src/Diff/ExportedArrayDiffer.php @@ -4,6 +4,7 @@ namespace Ock\Testing\Diff; +use Fisharebest\Algorithm\MyersDiff; use Symfony\Component\Yaml\Tag\TaggedValue; class ExportedArrayDiffer implements DifferInterface { @@ -107,76 +108,45 @@ protected function compareArrays(array $before, array $after, bool $could_be_lis * @return array|false */ protected function compareLists(array $before, array $after): array|false { - $diff = $this->doCompareLists($before, $after); - if (!$diff || count($diff) === count($before) + count($after)) { - // The two lists are completely different. + $algorithm = new MyersDiff(); + /** @var list $myers_diff_solution */ + $myers_diff_solution = $algorithm->calculate( + // The Myers diff actually works for non-strings. + // See https://github.com/fisharebest/algorithm/pull/9. + // @phpstan-ignore argument.type + $before, + // @phpstan-ignore argument.type + $after, + fn ($a, $b) => false !== $this->compareValues($a, $b), + ); + if (count($myers_diff_solution) >= count($before) + count($after) - 1) { + // The two lists are too different. return false; } - return $diff; - } - - /** - * Compares two lists recursively. - * - * @param list $before - * @param list $after - * @param int $i_before - * @param int $i_after - * - * @return array - */ - protected function doCompareLists(array $before, array $after, int $i_before = 0, int $i_after = 0): array { - $diff = []; - while (true) { - if ($i_before >= count($before)) { - // There are more items in "after" list. - for (; $i_after < count($after); ++$i_after) { - $diff[] = new TaggedValue('add', $after[$i_after]); - } - return $diff; + $result = []; + $i_before = 0; + $i_after = 0; + foreach ($myers_diff_solution as [, $operation]) { + if ($operation === -1) { + $result[] = new TaggedValue('--', $before[$i_before]); + ++$i_before; } - if ($i_after >= count($after)) { - // There are more items in "before" list. - for (; $i_before < count($before); ++$i_before) { - $diff[] = new TaggedValue('--', $before[$i_before]); - } - return $diff; + elseif ($operation === 1) { + $result[] = new TaggedValue('add', $after[$i_after]); + ++$i_after; } - $item_diff = $this->compareValues($before[$i_before], $after[$i_after]); - if ($item_diff === []) { - // The two values are the same. + else { + $item_diff = $this->compareValues($before[$i_before], $after[$i_after]); ++$i_before; ++$i_after; - continue; - } - // The two items are completely different. - $diff_minus = $this->doCompareLists($before, $after, $i_before + 1, $i_after); - $diff_plus = $this->doCompareLists($before, $after, $i_before, $i_after + 1); - if ($item_diff !== false) { - $diff_eq = $this->doCompareLists($before, $after, $i_before + 1, $i_after + 1); - if (count($diff_eq) < count($diff_minus) && count($diff_eq) < count($diff_plus)) { - return [ - ...$diff, - new TaggedValue('diff', $item_diff), - ...$diff_eq, - ]; + #assert($item_diff !== false); + if ($item_diff === []) { + continue; } - } - if (count($diff_minus) <= count($diff_plus)) { - return [ - ...$diff, - new TaggedValue('--', $before[$i_before]), - ...$diff_minus, - ]; - } - else { - return [ - ...$diff, - new TaggedValue('add', $after[$i_after]), - ...$diff_plus, - ]; + $result[] = new TaggedValue('diff', $item_diff); } } + return $result; } protected function compareAssoc(array $before, array $after): array|false { diff --git a/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.change-assoc-value.yml b/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.change-assoc-value.yml index 50a41061d..076d42fdf 100644 --- a/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.change-assoc-value.yml +++ b/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.change-assoc-value.yml @@ -17,12 +17,12 @@ after: b: 'B orig' - after diff: - - !-- - a: 'A orig' - b: 'B orig' - !add a: 'A changed' b: 'B changed' - !diff '~- a': 'A orig' '~+ a': 'A changed' + - !-- + a: 'A orig' + b: 'B orig' diff --git a/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.hello.yml b/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.hello.yml index 9e13c7a49..a72b2d493 100644 --- a/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.hello.yml +++ b/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.hello.yml @@ -5,5 +5,9 @@ after: - goodbye - world diff: - - !-- hello - - !add goodbye + '-': + - hello + - world + +: + - goodbye + - world diff --git a/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.insert-between-complex.yml b/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.insert-between-complex.yml index 92758819f..a1714b2d1 100644 --- a/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.insert-between-complex.yml +++ b/packages/testing/tests/fixtures/ExportedArrayDifferTest/list.insert-between-complex.yml @@ -40,32 +40,44 @@ after: - '@drupal.proxy_original_service.paramconverter.menu_link' - drupal.proxy_original_service.paramconverter.menu_link diff: - - !add - - addConverter + '-': - + - addConverter - - class: Symfony\Component\DependencyInjection\Definition - getClass(): Drupal\ock\UI\ParamConverter\ParamConverter_Iface - getTags(): - paramconverter: - - { } - - Drupal\ock\UI\ParamConverter\ParamConverter_Iface - - !add - - addConverter + - '@drupal.proxy_original_service.paramconverter.menu_link' + - drupal.proxy_original_service.paramconverter.menu_link + +: - + - addConverter - - class: Symfony\Component\DependencyInjection\Definition - getArguments(): - - '@Ock\Ock\Plugin\Map\PluginMapInterface' - - - class: Symfony\Component\DependencyInjection\TypedReference - $type: Psr\Log\LoggerInterface - $attributes: - - - class: Symfony\Component\DependencyInjection\Attribute\Autowire - $value: '@logger.channel.ock' - getClass(): Drupal\ock\UI\ParamConverter\ParamConverter_Plugin - getTags(): - paramconverter: - - { } - - Drupal\ock\UI\ParamConverter\ParamConverter_Plugin + - + class: Symfony\Component\DependencyInjection\Definition + getClass(): Drupal\ock\UI\ParamConverter\ParamConverter_Iface + getTags(): + paramconverter: + - { } + - Drupal\ock\UI\ParamConverter\ParamConverter_Iface + - + - addConverter + - + - + class: Symfony\Component\DependencyInjection\Definition + getArguments(): + - '@Ock\Ock\Plugin\Map\PluginMapInterface' + - + class: Symfony\Component\DependencyInjection\TypedReference + $type: Psr\Log\LoggerInterface + $attributes: + - + class: Symfony\Component\DependencyInjection\Attribute\Autowire + $value: '@logger.channel.ock' + getClass(): Drupal\ock\UI\ParamConverter\ParamConverter_Plugin + getTags(): + paramconverter: + - { } + - Drupal\ock\UI\ParamConverter\ParamConverter_Plugin + - + - addConverter + - + - '@drupal.proxy_original_service.paramconverter.menu_link' + - drupal.proxy_original_service.paramconverter.menu_link