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.
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
- Using Composer, pull in the Crop API contrib module into your project.
- Modify the Testing installation profile (
testing.info.yml
) so that it depends on crop
as an install
dependency. - 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
- 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");
}
}
}
}
- 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 #2852557: Config export key order is not predictable, use config schema to order keys for maps.
Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet