differ = new ConfigDiffer(Grav::instance()); } // ---------- diff() ---------- #[Test] public function diff_returns_empty_when_current_matches_parent(): void { $parent = ['force_ssl' => false, 'languages' => ['supported' => ['en', 'fr']]]; $current = ['force_ssl' => false, 'languages' => ['supported' => ['en', 'fr']]]; $this->assertSame([], $this->differ->diff($current, $parent)); } #[Test] public function diff_includes_scalar_overrides(): void { $parent = ['force_ssl' => false, 'timezone' => null]; $current = ['force_ssl' => true, 'timezone' => null]; $this->assertSame(['force_ssl' => true], $this->differ->diff($current, $parent)); } #[Test] public function diff_recurses_into_associative_arrays(): void { $parent = [ 'pages' => ['theme' => 'quark', 'markdown' => ['extra' => false]], ]; $current = [ 'pages' => ['theme' => 'quark2', 'markdown' => ['extra' => false]], ]; $this->assertSame( ['pages' => ['theme' => 'quark2']], $this->differ->diff($current, $parent), ); } #[Test] public function diff_treats_sequential_arrays_as_atomic(): void { // The classic "shortened list" scenario: user removes one language from // the list. We must emit the whole shortened list, not a key-diff that // would silently merge the removed entry back in when Grav re-loads. $parent = ['languages' => ['supported' => ['en', 'fr', 'de']]]; $current = ['languages' => ['supported' => ['en', 'fr']]]; $this->assertSame( ['languages' => ['supported' => ['en', 'fr']]], $this->differ->diff($current, $parent), ); } #[Test] public function diff_treats_reordered_sequential_arrays_as_different(): void { $parent = ['types' => ['htm', 'html']]; $current = ['types' => ['html', 'htm']]; $this->assertSame( ['types' => ['html', 'htm']], $this->differ->diff($current, $parent), ); } #[Test] public function diff_null_override_is_retained(): void { // Setting a field explicitly to null should override a non-null default. $parent = ['timezone' => 'UTC']; $current = ['timezone' => null]; $this->assertSame(['timezone' => null], $this->differ->diff($current, $parent)); } #[Test] public function diff_key_absent_from_parent_is_always_kept(): void { $parent = ['a' => 1]; $current = ['a' => 1, 'b' => 2]; $this->assertSame(['b' => 2], $this->differ->diff($current, $parent)); } #[Test] public function diff_type_change_from_assoc_to_list_replaces_whole_value(): void { $parent = ['http_x_forwarded' => ['protocol' => true]]; $current = ['http_x_forwarded' => ['a', 'b']]; $this->assertSame( ['http_x_forwarded' => ['a', 'b']], $this->differ->diff($current, $parent), ); } #[Test] public function diff_ignores_key_order_differences(): void { // Yaml parsers or API clients can legitimately reorder keys. Parent has // the same content, different insertion order — should diff to empty. $parent = ['pages' => ['theme' => 'quark', 'markdown' => ['extra' => false]]]; $current = ['pages' => ['markdown' => ['extra' => false], 'theme' => 'quark']]; $this->assertSame([], $this->differ->diff($current, $parent)); } #[Test] public function diff_drops_subtree_when_no_inner_differences(): void { $parent = ['a' => ['b' => 1, 'c' => 2]]; $current = ['a' => ['b' => 1, 'c' => 2], 'd' => 3]; $this->assertSame(['d' => 3], $this->differ->diff($current, $parent)); } #[Test] public function diff_deeply_nested_override(): void { $parent = ['a' => ['b' => ['c' => ['d' => 1, 'e' => 2]]]]; $current = ['a' => ['b' => ['c' => ['d' => 1, 'e' => 99]]]]; $this->assertSame( ['a' => ['b' => ['c' => ['e' => 99]]]], $this->differ->diff($current, $parent), ); } // ---------- deepMergeAssoc() ---------- #[Test] public function deep_merge_overrides_scalar(): void { $this->assertSame( ['a' => 2, 'b' => 3], $this->differ->deepMergeAssoc(['a' => 1, 'b' => 3], ['a' => 2]), ); } #[Test] public function deep_merge_recurses_into_assoc(): void { $result = $this->differ->deepMergeAssoc( ['x' => ['a' => 1, 'b' => 2]], ['x' => ['b' => 20, 'c' => 30]], ); $this->assertSame(['x' => ['a' => 1, 'b' => 20, 'c' => 30]], $result); } #[Test] public function deep_merge_replaces_sequential_arrays(): void { $result = $this->differ->deepMergeAssoc( ['tags' => ['a', 'b', 'c']], ['tags' => ['x']], ); $this->assertSame(['tags' => ['x']], $result); } // ---------- valuesEqual() / isAssoc() ---------- #[Test] public function values_equal_treats_assoc_key_order_as_irrelevant(): void { $this->assertTrue(ConfigDiffer::valuesEqual( ['a' => 1, 'b' => 2], ['b' => 2, 'a' => 1], )); } #[Test] public function values_equal_respects_sequential_order(): void { $this->assertFalse(ConfigDiffer::valuesEqual([1, 2], [2, 1])); } #[Test] public function is_assoc_is_false_for_empty_and_lists(): void { $this->assertFalse(ConfigDiffer::isAssoc([])); $this->assertFalse(ConfigDiffer::isAssoc(['a', 'b'])); $this->assertTrue(ConfigDiffer::isAssoc(['x' => 1])); } }