`AssertConfigTrait::assertConfigDiff()` Fails on `_core.default_config_hash` when Diff Op is "Change"

Created on 3 August 2022, almost 2 years ago
Updated 8 January 2024, 5 months ago

The \Drupal\KernelTests\AssertConfigTrait::assertConfigDiff() method doesn't properly handle diffs in which _core.default_config_hash was added in the middle of a config in database storage.

<!--break-->

Problem/Motivation

Distribution maintainers are encouraged to use \Drupal\KernelTests\AssertConfigTrait::assertConfigDiff() in an install test so that they can catch when the on-disk copy of config for their installation profile needs to be updated to account for changes in database config storage (e.g., after schema upgrades, core upgrades, etc.). For example, Umami does this in \Drupal\Tests\demo_umami\Functional\DemoUmamiProfileTest::testConfig(). It appears that the method is designed to disregard diffs for the _core and uuid keys, but it only does this for Drupal\Component\Diff\Engine\DiffOpAdd -- if the config installer adds these values toward the middle of a config, the diff can flag them as a change, resulting in an assertion failure during tests.

Steps to reproduce

  1. Using Composer, pull in the Crop API contrib module β†’ into your project.
  2. Modify the Testing installation profile (testing.info.yml) so that it depends on crop as an install dependency.
  3. Add the following to web/core/profiles/testing/config/optional/crop.type.free_crop.yml:
    langcode: en
    status: true
    dependencies: {}
    label: 'Free crop'
    id: free_crop
    description: 'A crop preset that is not constrained by aspect ratio. This is used on the homepage for the hero image and section dividers.'
    aspect_ratio: ''
    soft_limit_width: null
    soft_limit_height: null
    hard_limit_width: null
    hard_limit_height: null
    
  4. Put the following code into web/core/profiles/testing/tests/src/Functional/TestingProfileTest.php:
    
    namespace Drupal\Tests\testing\Functional;
    
    use Drupal\Core\Config\FileStorage;
    use Drupal\Core\Config\InstallStorage;
    use Drupal\Core\Config\StorageInterface;
    use Drupal\KernelTests\AssertConfigTrait;
    use Drupal\Tests\BrowserTestBase;
    use Drupal\Core\Session\AccountInterface;
    use Drupal\Component\Render\FormattableMarkup;
    
    /**
     * Tests testing profile.
     *
     * @group testing
     */
    class TestingProfileTest extends BrowserTestBase {
      use AssertConfigTrait;
    
      /**
       * Tests the profile supplied configuration is the same after installation.
       */
      public function testConfig() {
        // Just connect directly to the config table so we don't need to worry about
        // the cache layer.
        $active_config_storage = $this->container->get('config.storage');
    
        $default_config_storage = new FileStorage($this->container->get('extension.list.profile')->getPath('testing') . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY, InstallStorage::DEFAULT_COLLECTION);
        $this->assertDefaultConfig($default_config_storage, $active_config_storage);
      }
    
      /**
       * Asserts that the default configuration matches active configuration.
       *
       * @param \Drupal\Core\Config\StorageInterface $default_config_storage
       *   The default configuration storage to check.
       * @param \Drupal\Core\Config\StorageInterface $active_config_storage
       *   The active configuration storage.
       */
      protected function assertDefaultConfig(StorageInterface $default_config_storage, StorageInterface $active_config_storage): void {
        /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
        $config_manager = $this->container->get('config.manager');
    
        foreach ($default_config_storage->listAll() as $config_name) {
          if ($active_config_storage->exists($config_name)) {
            $result = $config_manager->diff($default_config_storage, $active_config_storage, $config_name);
            $this->assertConfigDiff($result, $config_name, []);
          }
          else {
            $this->fail("$config_name has not been installed");
          }
        }
      }
    
    }
    
  5. Run \Drupal\Tests\testing\Functional\TestingProfileTest using the test runner (for example, with a command line like /usr/local/bin/php ./web/core/scripts/run-tests.sh --php /usr/local/bin/php --sqlite ../results/test.sqlite --dburl mysql://pantheon:pantheon@database/pantheon --url http://testserver_1 --types PHPUnit-Functional --xml ../results --verbose --color --non-html --class '\Drupal\Tests\testing\Functional\TestingProfileTest').

The test will fail with the following error:

There was 1 error:

    1) Drupal\Tests\testing\Functional\TestingProfileTest::testConfig
    Exception: crop.type.free_crop:
    Drupal\Component\Diff\Engine\DiffOpChange::__set_state(array(
       'type' => 'change',
       'orig' =>
      array (
        0 => 'label: \'Free crop\'',
      ),
       'closing' =>
      array (
        0 => '_core:',
        1 => '  default_config_hash:
    rHGTFvM-sBD6K7VXD1NgB5PX99Da68jpqRPIEqlCo6M',
      ),
    ))

    /app/web/core/tests/Drupal/KernelTests/AssertConfigTrait.php:39
    /app/web/core/profiles/testing/tests/src/Functional/TestingProfileTest.php:48
    /app/web/core/profiles/testing/tests/src/Functional/TestingProfileTest.php:30
    /app/vendor/phpunit/phpunit/src/Framework/TestResult.php:726

    ERRORS!
    Tests: 1, Assertions: 1, Errors: 1.

If you modify \Drupal\Core\Config\ConfigManager::diff() to print out both of the files, the file in the database will look something like this:

uuid: 225e9acd-9970-4171-8baf-b637b452cdd9
langcode: en
status: true
dependencies: {  }
_core:
  default_config_hash: rHGTFvM-sBD6K7VXD1NgB5PX99Da68jpqRPIEqlCo6M
id: free_crop
label: 'Free crop'
description: 'A crop preset that is not constrained by aspect ratio. This
is used on the homepage for the hero image and section dividers.'
aspect_ratio: ''
soft_limit_width: null
soft_limit_height: null
hard_limit_width: null
hard_limit_height: null

If you diff that with the original config on the command-line, you get:

--- ./before.yaml       2022-08-03 00:43:49.881393000 -0400
+++ ./after.yaml        2022-08-03 00:44:03.802892000 -0400
@@ -1,8 +1,11 @@
+uuid: 225e9acd-9970-4171-8baf-b637b452cdd9
 langcode: en
 status: true
 dependencies: {  }
-label: 'Free crop'
+_core:
+  default_config_hash: rHGTFvM-sBD6K7VXD1NgB5PX99Da68jpqRPIEqlCo6M
 id: free_crop
+label: 'Free crop'
 description: 'A crop preset that is not constrained by aspect ratio. This
 is used on the homepage for the hero image and section dividers.'
 aspect_ratio: ''

The issue is that diff algorithms like the one in \Drupal\Component\Diff\Engine\DiffEngine flag the label key as having changed to _core.default_config_hash. The switch statement in \Drupal\KernelTests\AssertConfigTrait::assertConfigDiff() only takes these keys into account when it encounters a
Drupal\Component\Diff\Engine\DiffOpAdd result, but we can't just make the code apply to Drupal\Component\Diff\Engine\DiffOpChange in the switch because then we'd be skipping over other parts of the config that might also have changed in the same diff hunk.

Proposed resolution

Both of the configs being diffed should be sorted before being diffed, especially after πŸ“Œ Config export key order is not predictable, use config schema to order keys for maps Fixed .

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

πŸ› Bug report
Status

Postponed: needs info

Version

9.5

Component
Configuration entityΒ  β†’

Last updated 4 days ago

Created by

πŸ‡ΊπŸ‡ΈUnited States GuyPaddock

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10

    I wonder if there's something hard coded that expects ID to be before label

    I wonder if changing the order in crop module's schema resolves this and if that should be the resolution

    Can you test that and report back?

Production build 0.69.0 2024