summaryrefslogtreecommitdiffstatshomepage
path: root/core/tests
diff options
context:
space:
mode:
Diffstat (limited to 'core/tests')
-rw-r--r--core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php8
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php14
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php14
-rw-r--r--core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php4
-rw-r--r--core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php9
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php676
-rw-r--r--core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php10
-rw-r--r--core/tests/Drupal/BuildTests/Framework/BuildTestBase.php4
-rw-r--r--core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php17
-rw-r--r--core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php13
-rw-r--r--core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php6
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php2
-rw-r--r--core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php1
-rw-r--r--core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php8
-rw-r--r--core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php36
-rw-r--r--core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php37
-rw-r--r--core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php8
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php51
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php2
-rw-r--r--core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php10
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php155
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php1
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php36
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php38
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php32
-rw-r--r--core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php1
-rw-r--r--core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php22
-rw-r--r--core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php46
-rw-r--r--core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php31
-rw-r--r--core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php4
-rw-r--r--core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php7
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php12
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php10
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php12
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php8
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php22
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php1278
-rw-r--r--core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php109
-rw-r--r--core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php4
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php85
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php1036
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php980
-rw-r--r--core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php2
-rw-r--r--core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php86
-rw-r--r--core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php75
-rw-r--r--core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php8
-rw-r--r--core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php9
-rw-r--r--core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php30
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php6
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php27
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php25
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php85
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php3
-rw-r--r--core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php71
-rw-r--r--core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php108
-rw-r--r--core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php46
-rw-r--r--core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php5
-rw-r--r--core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php74
-rw-r--r--core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php44
-rw-r--r--core/tests/Drupal/KernelTests/KernelTestBase.php2
-rw-r--r--core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php2
-rw-r--r--core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php2
-rw-r--r--core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js77
-rw-r--r--core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php8
-rw-r--r--core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php2
-rw-r--r--core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php34
-rw-r--r--core/tests/Drupal/TestTools/Extension/DeprecationBridge/DeprecationHandler.php5
-rw-r--r--core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php6
-rw-r--r--core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit11/TestCompatibilityTrait.php13
-rw-r--r--core/tests/Drupal/Tests/BrowserTestBase.php16
-rw-r--r--core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php8
-rw-r--r--core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php85
-rw-r--r--core/tests/Drupal/Tests/Component/Utility/HtmlTest.php3
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/ExecTrait.php (renamed from core/tests/Drupal/Tests/Composer/Plugin/Scaffold/ExecTrait.php)7
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/FixturesBase.php272
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Fixtures.php257
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php2
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php2
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php2
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/Fixtures.php38
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/SemVerTest.php51
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/composer-root/composer.json.tmpl97
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-a/composer.json6
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-b/composer.json6
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/composer.json9
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/recipe.yml3
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/composer.json9
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/recipe.yml3
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/composer.json9
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/recipe.yml3
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/composer.json5
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/recipe.yml3
-rw-r--r--core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/themes/composer-theme-a/composer.json5
-rw-r--r--core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php57
-rw-r--r--core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php38
-rw-r--r--core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php161
-rw-r--r--core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php92
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php36
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php130
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php3
-rw-r--r--core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css1
-rw-r--r--core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php35
-rw-r--r--core/tests/Drupal/Tests/Core/Database/ConnectionTest.php6
-rw-r--r--core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php4
-rw-r--r--core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php28
-rw-r--r--core/tests/Drupal/Tests/Core/Datetime/DrupalDateTimeTest.php8
-rw-r--r--core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php10
-rw-r--r--core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php88
-rw-r--r--core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php4
-rw-r--r--core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php80
-rw-r--r--core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php25
-rw-r--r--core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php5
-rw-r--r--core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php104
-rw-r--r--core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php51
-rw-r--r--core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc5
-rw-r--r--core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php7
-rw-r--r--core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php47
-rw-r--r--core/tests/Drupal/Tests/Core/PageCache/DenyNoCacheRoutesTest.php92
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php47
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php205
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php24
-rw-r--r--core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php26
-rw-r--r--core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php3
-rw-r--r--core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php60
-rw-r--r--core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php5
-rw-r--r--core/tests/Drupal/Tests/Core/Render/Placeholder/ChainedPlaceholderStrategyTest.php14
-rw-r--r--core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php2
-rw-r--r--core/tests/Drupal/Tests/Core/Render/RendererTest.php64
-rw-r--r--core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php43
-rw-r--r--core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php6
-rw-r--r--core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php18
-rw-r--r--core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php40
-rw-r--r--core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php2
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php92
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php371
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php80
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php3
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php3
-rw-r--r--core/tests/Drupal/Tests/RequirementsPageTrait.php10
-rw-r--r--core/tests/Drupal/Tests/SchemaCheckTestTrait.php4
-rw-r--r--core/tests/Drupal/Tests/UnitTestCase.php29
-rw-r--r--core/tests/Drupal/Tests/UpdatePathTestTrait.php6
-rw-r--r--core/tests/PHPStan/composer.json4
-rw-r--r--core/tests/fixtures/config_install/multilingual/views.view.content.yml1
-rw-r--r--core/tests/fixtures/config_install/multilingual/views.view.files.yml2
-rw-r--r--core/tests/fixtures/config_install/multilingual/views.view.glossary.yml1
-rw-r--r--core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml1
-rw-r--r--core/tests/fixtures/config_install/multilingual/views.view.watchdog.yml1
-rw-r--r--core/tests/fixtures/default_content_broken/no-uuid.yml23
154 files changed, 7153 insertions, 1779 deletions
diff --git a/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php b/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php
index ee21d5bedf52..e943a257ee26 100644
--- a/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php
+++ b/core/tests/Drupal/BuildTests/Command/GenerateThemeTest.php
@@ -8,6 +8,8 @@ use Drupal\BuildTests\QuickStart\QuickStartTestBase;
use Drupal\Core\Command\GenerateTheme;
use Drupal\Core\Serialization\Yaml;
use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful;
use Symfony\Component\Process\PhpExecutableFinder;
@@ -15,11 +17,9 @@ use Symfony\Component\Process\Process;
/**
* Tests the generate-theme commands.
- *
- * @requires extension pdo_sqlite
- *
- * @group Command
*/
+#[Group('Command')]
+#[RequiresPhpExtension('pdo_sqlite')]
class GenerateThemeTest extends QuickStartTestBase {
/**
diff --git a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php
index 0042c55e1604..8be768ed98c8 100644
--- a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsIsolatedBuildTest.php
@@ -6,16 +6,17 @@ namespace Drupal\BuildTests\Composer\Component;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
+use PHPUnit\Framework\Attributes\CoversNothing;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Finder\Finder;
/**
* Try to install dependencies per component, using Composer.
- *
- * @group Composer
- * @group Component
- *
- * @coversNothing
*/
+#[CoversNothing]
+#[Group('Composer')]
+#[Group('Component')]
class ComponentsIsolatedBuildTest extends ComposerBuildTestBase {
/**
@@ -41,9 +42,8 @@ class ComponentsIsolatedBuildTest extends ComposerBuildTestBase {
/**
* Test whether components' composer.json can be installed in isolation.
- *
- * @dataProvider provideComponentPaths
*/
+ #[DataProvider('provideComponentPaths')]
public function testComponentComposerJson(string $component_path): void {
// Only copy the components. Copy all of them because some of them depend on
// each other.
diff --git a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php
index 269edf126bef..b50aa973c59e 100644
--- a/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Component/ComponentsTaggedReleaseTest.php
@@ -6,15 +6,16 @@ namespace Drupal\BuildTests\Composer\Component;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
+use PHPUnit\Framework\Attributes\CoversNothing;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
* Demonstrate that the Component generator responds to release tagging.
- *
- * @group Composer
- * @group Component
- *
- * @coversNothing
*/
+#[CoversNothing]
+#[Group('Composer')]
+#[Group('Component')]
class ComponentsTaggedReleaseTest extends ComposerBuildTestBase {
/**
@@ -37,9 +38,8 @@ class ComponentsTaggedReleaseTest extends ComposerBuildTestBase {
/**
* Validate release tagging and regeneration of dependencies.
- *
- * @dataProvider providerVersionConstraint
*/
+ #[DataProvider('providerVersionConstraint')]
public function testReleaseTagging(string $tag, string $constraint): void {
$this->copyCodebase();
$drupal_root = $this->getWorkspaceDirectory();
diff --git a/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php b/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php
index 7bdba7e4f4f2..5c563016f52d 100644
--- a/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php
+++ b/core/tests/Drupal/BuildTests/Composer/ComposerBuildTestBase.php
@@ -5,13 +5,13 @@ declare(strict_types=1);
namespace Drupal\BuildTests\Composer;
use Drupal\BuildTests\Framework\BuildTestBase;
+use PHPUnit\Framework\Attributes\CoversNothing;
use Symfony\Component\Finder\Finder;
/**
* Base class for Composer build tests.
- *
- * @coversNothing
*/
+#[CoversNothing]
abstract class ComposerBuildTestBase extends BuildTestBase {
/**
diff --git a/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php b/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php
index 661d159bf806..50c343354e18 100644
--- a/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/ComposerValidateTest.php
@@ -6,10 +6,13 @@ namespace Drupal\BuildTests\Composer;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Tests\Composer\ComposerIntegrationTrait;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
- * @group Composer
+ * Tests.
*/
+#[Group('Composer')]
class ComposerValidateTest extends BuildTestBase {
use ComposerIntegrationTrait;
@@ -23,9 +26,7 @@ class ComposerValidateTest extends BuildTestBase {
return $data;
}
- /**
- * @dataProvider provideComposerJson
- */
+ #[DataProvider('provideComposerJson')]
public function testValidateComposer($path): void {
$this->executeCommand('composer validate --strict --no-check-all ' . $path);
$this->assertCommandSuccessful();
diff --git a/core/tests/Drupal/BuildTests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php b/core/tests/Drupal/BuildTests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php
new file mode 100644
index 000000000000..1397a78cf694
--- /dev/null
+++ b/core/tests/Drupal/BuildTests/Composer/Plugin/Unpack/Functional/UnpackRecipeTest.php
@@ -0,0 +1,676 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\BuildTests\Composer\Plugin\Unpack\Functional;
+
+use Composer\InstalledVersions;
+use Composer\Util\Filesystem;
+use Drupal\Tests\Composer\Plugin\Unpack\Fixtures;
+use Drupal\BuildTests\Framework\BuildTestBase;
+use Drupal\Tests\Composer\Plugin\ExecTrait;
+
+/**
+ * Tests recipe unpacking.
+ *
+ * @group Unpack
+ */
+class UnpackRecipeTest extends BuildTestBase {
+
+ use ExecTrait;
+
+ /**
+ * Directory to perform the tests in.
+ */
+ protected string $fixturesDir;
+
+ /**
+ * The Symfony FileSystem component.
+ *
+ * @var \Composer\Util\Filesystem
+ */
+ protected Filesystem $fileSystem;
+
+ /**
+ * The Fixtures object.
+ *
+ * @var \Drupal\Tests\Composer\Plugin\Unpack\Fixtures
+ */
+ protected Fixtures $fixtures;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->fileSystem = new Filesystem();
+ $this->fixtures = new Fixtures();
+ $this->fixtures->createIsolatedComposerCacheDir();
+ $this->fixturesDir = $this->fixtures->tmpDir($this->name());
+ $replacements = [
+ 'PROJECT_ROOT' => $this->fixtures->projectRoot(),
+ 'COMPOSER_INSTALLERS' => InstalledVersions::getInstallPath('composer/installers'),
+ ];
+ $this->fixtures->cloneFixtureProjects($this->fixturesDir, $replacements);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown(): void {
+ // Remove any temporary directories that were created.
+ $this->fixtures->tearDown();
+ parent::tearDown();
+ }
+
+ /**
+ * Tests the dependencies unpack on install.
+ */
+ public function testAutomaticUnpack(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ copy($root_project_path . '/composer.json', $root_project_path . '/composer.json.original');
+
+ // Run composer install and confirm the composer.lock was created.
+ $this->runComposer('install');
+
+ // Install a module in require-dev that should be moved to require
+ // by the unpacker.
+ $this->runComposer('require --dev fixtures/module-a:^1.0');
+ // Ensure we have added the dependency to require-dev.
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
+
+ // Install a recipe and unpack it.
+ $stdout = $this->runComposer('require fixtures/recipe-a');
+ $this->doTestRecipeAUnpacked($root_project_path, $stdout);
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ // The more specific constraint should have been used.
+ $this->assertSame("^1.0", $root_composer_json['require']['fixtures/module-a']);
+
+ // Copy old composer.json back over and require recipe again to ensure it
+ // is still unpacked. This tests that unpacking does not rely on composer
+ // package events.
+ unlink($root_project_path . '/composer.json');
+ copy($root_project_path . '/composer.json.original', $root_project_path . '/composer.json');
+ $stdout = $this->runComposer('require fixtures/recipe-a');
+ $this->doTestRecipeAUnpacked($root_project_path, $stdout);
+ }
+
+ /**
+ * Tests recursive unpacking.
+ */
+ public function testRecursiveUnpacking(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Run composer install and confirm the composer.lock was created.
+ $this->runComposer('config --merge --json sort-packages true');
+ $this->runComposer('install');
+ $stdOut = $this->runComposer('require fixtures/recipe-c fixtures/recipe-a');
+ $this->assertSame("fixtures/recipe-c unpacked.\nfixtures/recipe-a unpacked.\nfixtures/recipe-b unpacked.\n", $stdOut);
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/module-a',
+ 'fixtures/module-b',
+ 'fixtures/theme-a',
+ ], array_keys($root_composer_json['require']));
+ // Ensure the resulting composer files are valid.
+ $this->runComposer('validate');
+ // Ensure the recipes exist.
+ $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
+ $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
+ $this->assertFileExists($root_project_path . '/recipes/recipe-c/recipe.yml');
+
+ // Ensure the complex constraint has been written correctly.
+ $this->assertSame('>=2.0.1.0-dev, <3.0.0.0-dev', $root_composer_json['require']['fixtures/module-b']);
+
+ // Ensure composer.lock is ordered correctly.
+ $root_composer_lock = $this->getFileContents($root_project_path . '/composer.lock');
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/module-a',
+ 'fixtures/module-b',
+ 'fixtures/theme-a',
+ ], array_column($root_composer_lock['packages'], 'name'));
+ }
+
+ /**
+ * Tests the dev dependencies do not unpack on install.
+ */
+ public function testNoAutomaticDevUnpack(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Run composer install and confirm the composer.lock was created.
+ $this->runComposer('install');
+
+ // Install a module in require.
+ $this->runComposer('require fixtures/module-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
+
+ // Install a recipe as a dev dependency.
+ $stdout = $this->runComposer('require --dev fixtures/recipe-a');
+ $this->assertStringContainsString("Recipes required as a development dependency are not automatically unpacked.", $stdout);
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ // Assert the state of the root composer.json as no unpacking has occurred.
+ $this->assertSame(['fixtures/recipe-a'], array_keys($root_composer_json['require-dev']));
+ $this->assertSame(['composer/installers', 'drupal/core-recipe-unpack', 'fixtures/module-a'], array_keys($root_composer_json['require']));
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+ }
+
+ /**
+ * Tests dependency unpacking using drupal:recipe-unpack.
+ */
+ public function testUnpackCommand(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Run composer install and confirm the composer.lock was created.
+ $this->runComposer('install');
+
+ // Disable automatic unpacking as it is the default behavior,
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
+
+ // Install a module in require-dev.
+ $this->runComposer('require --dev fixtures/module-a');
+ // Install a module in require.
+ $this->runComposer('require fixtures/module-b:*');
+
+ // Ensure we have added the dependencies.
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertArrayHasKey('fixtures/module-b', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
+
+ // Install a recipe and check it is not unpacked.
+ $stdout = $this->runComposer('require fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ // When the package is unpacked, the unpacked dependencies should be logged
+ // in the stdout.
+ $this->assertStringNotContainsString("unpacked.", $stdout);
+
+ $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+
+ // The package dependencies should not be in the root composer.json.
+ $this->assertArrayNotHasKey('fixtures/recipe-b', $root_composer_json['require']);
+
+ // Try unpacking a recipe that in not in the root composer.json.
+ try {
+ $this->runComposer('drupal:recipe-unpack fixtures/recipe-b');
+ $this->fail('Unpacking a non-existent dependency should fail');
+ }
+ catch (\RuntimeException $e) {
+ $this->assertStringContainsString('fixtures/recipe-b not found in the root composer.json.', $e->getMessage());
+ }
+
+ // The dev dependency has not moved.
+ $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require-dev']);
+
+ $stdout = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
+ $this->doTestRecipeAUnpacked($root_project_path, $stdout);
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ // The more specific constraints has been used.
+ $this->assertSame("^2.0", $root_composer_json['require']['fixtures/module-b']);
+
+ // Try unpacking something that is not a recipe.
+ try {
+ $this->runComposer('drupal:recipe-unpack fixtures/module-a');
+ $this->fail('Unpacking a module should fail');
+ }
+ catch (\RuntimeException $e) {
+ $this->assertStringContainsString('fixtures/module-a is not a recipe.', $e->getMessage());
+ }
+
+ // Try unpacking something that in not in the root composer.json.
+ try {
+ $this->runComposer('drupal:recipe-unpack fixtures/module-c');
+ $this->fail('Unpacking a non-existent dependency should fail');
+ }
+ catch (\RuntimeException $e) {
+ $this->assertStringContainsString('fixtures/module-c not found in the root composer.json.', $e->getMessage());
+ }
+ }
+
+ /**
+ * Tests dependency unpacking using drupal:recipe-unpack with multiple args.
+ */
+ public function testUnpackCommandWithMultipleRecipes(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+ $this->runComposer('install');
+
+ // Disable automatic unpacking as it is the default behavior,
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
+
+ // Install a recipe and check it is not unpacked.
+ $stdOut = $this->runComposer('require fixtures/recipe-a fixtures/recipe-d');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ // When the package is unpacked, the unpacked dependencies should be logged
+ // in the stdout.
+ $this->assertStringNotContainsString("unpacked.", $stdOut);
+
+ $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/recipe-d', $root_composer_json['require']);
+
+ $stdOut = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a fixtures/recipe-d');
+ $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
+ $this->assertStringContainsString("fixtures/recipe-d unpacked.", $stdOut);
+
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
+ $this->assertArrayNotHasKey('fixtures/recipe-d', $root_composer_json['require']);
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+ }
+
+ /**
+ * Tests dependency unpacking using drupal:recipe-unpack with no arguments.
+ */
+ public function testUnpackCommandWithoutRecipesArgument(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+ $this->runComposer('install');
+
+ // Tests unpack command with no arguments and no recipes in the root
+ // composer package.
+ $stdOut = $this->runComposer('drupal:recipe-unpack');
+ $this->assertSame("No recipes to unpack.\n", $stdOut);
+
+ // Disable automatic unpacking as it is the default behavior,
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
+
+ // Install a recipe and check it is not unpacked.
+ $stdOut = $this->runComposer('require fixtures/recipe-a fixtures/recipe-d');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ // When the package is unpacked, the unpacked dependencies should be logged
+ // in the stdout.
+ $this->assertStringNotContainsString("unpacked.", $stdOut);
+
+ $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/recipe-d', $root_composer_json['require']);
+
+ $stdOut = $this->runComposer('drupal:recipe-unpack');
+ $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
+ $this->assertStringContainsString("fixtures/recipe-d unpacked.", $stdOut);
+
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
+ $this->assertArrayNotHasKey('fixtures/recipe-d', $root_composer_json['require']);
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+ }
+
+ /**
+ * Tests unpacking a recipe in require-dev using drupal:recipe-unpack.
+ */
+ public function testUnpackCommandOnDevRecipe(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Run composer install and confirm the composer.lock was created.
+ $this->runComposer('install');
+
+ // Disable automatic unpacking, which is the default behavior.
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
+
+ $this->runComposer('require fixtures/recipe-b');
+
+ // Install a recipe and check it is not unpacked.
+ $this->runComposer('require --dev fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require-dev']);
+ $this->assertArrayHasKey('fixtures/recipe-b', $root_composer_json['require']);
+
+ $error_output = '';
+ $stdout = $this->runComposer('drupal:recipe-unpack fixtures/recipe-a', error_output: $error_output);
+ $this->assertStringContainsString("fixtures/recipe-a is present in the require-dev key. Unpacking will move the recipe's dependencies to the require key.", $error_output);
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ // Ensure recipe A's dependencies are moved to require.
+ $this->doTestRecipeAUnpacked($root_project_path, $stdout);
+
+ // Ensure recipe B's dependencies are in require and the recipe has been
+ // unpacked.
+ $this->assertArrayNotHasKey('fixtures/recipe-b', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/theme-a', $root_composer_json['require']);
+
+ // Ensure installed.json and installed.php are correct.
+ $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
+ $installed_packages = array_column($installed_json['packages'], 'name');
+ $this->assertContains('fixtures/module-b', $installed_packages);
+ $this->assertNotContains('fixtures/recipe-a', $installed_packages);
+ $this->assertSame([], $installed_json['dev-package-names']);
+ $installed_php = include_once $root_project_path . '/vendor/composer/installed.php';
+ $this->assertArrayHasKey('fixtures/module-b', $installed_php['versions']);
+ $this->assertFalse($installed_php['versions']['fixtures/module-b']['dev_requirement']);
+ $this->assertArrayNotHasKey('fixtures/recipe-a', $installed_php['versions']);
+ }
+
+ /**
+ * Tests the unpacking a recipe that is an indirect dev dependency.
+ */
+ public function testUnpackCommandOnIndirectDevDependencyRecipe(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Run composer install and confirm the composer.lock was created.
+ $this->runComposer('install');
+ // Disable automatic unpacking as it is the default behavior,
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
+
+ $this->runComposer('require --dev fixtures/recipe-b');
+
+ // Install a recipe and ensure it is not unpacked.
+ $this->runComposer('require fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ $this->assertArrayHasKey('fixtures/recipe-a', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/recipe-b', $root_composer_json['require-dev']);
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+
+ $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ // Ensure recipe A's dependencies are in require.
+ $this->assertArrayNotHasKey('fixtures/recipe-a', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/module-b', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/module-a', $root_composer_json['require']);
+ $this->assertArrayHasKey('fixtures/theme-a', $root_composer_json['require']);
+
+ // Ensure recipe B is still in require-dev even though all it's dependencies
+ // have been unpacked to require due to unpacking recipe A.
+ $this->assertSame(['fixtures/recipe-b'], array_keys($root_composer_json['require-dev']));
+
+ // Ensure recipe B is still list in installed.json.
+ $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
+ $installed_packages = array_column($installed_json['packages'], 'name');
+ $this->assertContains('fixtures/recipe-b', $installed_packages);
+ $this->assertContains('fixtures/recipe-b', $installed_json['dev-package-names']);
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+ }
+
+ /**
+ * Tests a recipe can be removed and the unpack plugin does not interfere.
+ */
+ public function testRemoveRecipe(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Disable automatic unpacking, which is the default behavior,
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.on-require false');
+
+ $this->runComposer('install');
+
+ // Install a recipe and ensure it is not unpacked.
+ $this->runComposer('require fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/recipe-a',
+ ], array_keys($root_composer_json['require']));
+
+ // Removing the recipe should work as normal.
+ $this->runComposer('remove fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ ], array_keys($root_composer_json['require']));
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+ }
+
+ /**
+ * Tests a recipe can be ignored and not unpacked.
+ */
+ public function testIgnoreRecipe(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Disable automatic unpacking as it is the default behavior,
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.ignore \'["fixtures/recipe-a"]\'');
+
+ $this->runComposer('install');
+
+ // Install a recipe and ensure it does not get unpacked.
+ $stdOut = $this->runComposer('require --verbose fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertSame("fixtures/recipe-a not unpacked because it is ignored.", trim($stdOut));
+
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/recipe-a',
+ ], array_keys($root_composer_json['require']));
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+
+ // Try using the unpack command on an ignored recipe.
+ try {
+ $this->runComposer('drupal:recipe-unpack fixtures/recipe-a');
+ $this->fail('Ignored recipes should not be unpacked.');
+ }
+ catch (\RuntimeException $e) {
+ $this->assertStringContainsString('fixtures/recipe-a is in the extra.drupal-recipe-unpack.ignore list.', $e->getMessage());
+ }
+ }
+
+ /**
+ * Tests a dependent recipe can be ignored and not unpacked.
+ */
+ public function testIgnoreDependentRecipe(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ // Disable automatic unpacking, which is the default behavior,
+ $this->runComposer('config --merge --json extra.drupal-recipe-unpack.ignore \'["fixtures/recipe-b"]\'');
+ $this->runComposer('config sort-packages true');
+
+ $this->runComposer('install');
+
+ // Install a recipe and check it is not packed but not removed.
+ $stdOut = $this->runComposer('require --verbose fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertStringContainsString("fixtures/recipe-b not unpacked because it is ignored.", $stdOut);
+ $this->assertStringContainsString("fixtures/recipe-a unpacked.", $stdOut);
+
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/module-b',
+ 'fixtures/recipe-b',
+ ], array_keys($root_composer_json['require']));
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+ }
+
+ /**
+ * Tests that recipes stick around after being unpacked.
+ */
+ public function testRecipeIsPhysicallyPresentAfterUnpack(): void {
+ $root_project_dir = 'composer-root';
+ $root_project_path = $this->fixturesDir . '/' . $root_project_dir;
+
+ $this->runComposer('install');
+
+ // Install a recipe, which should unpack it.
+ $stdOut = $this->runComposer('require --verbose fixtures/recipe-b');
+ $this->assertStringContainsString("fixtures/recipe-b unpacked.", $stdOut);
+ $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
+
+ // Require another dependency.
+ $this->runComposer('require --verbose fixtures/module-b');
+
+ // The recipe should still be physically installed...
+ $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
+
+ // ...but it should NOT be in installed.json or installed.php.
+ $installed_json = $this->getFileContents($root_project_path . '/vendor/composer/installed.json');
+ $installed_packages = array_column($installed_json['packages'], 'name');
+ $this->assertContains('fixtures/module-b', $installed_packages);
+ $this->assertNotContains('fixtures/recipe-b', $installed_packages);
+ $installed_php = include_once $root_project_path . '/vendor/composer/installed.php';
+ $this->assertArrayHasKey('fixtures/module-b', $installed_php['versions']);
+ $this->assertArrayNotHasKey('fixtures/recipe-b', $installed_php['versions']);
+ }
+
+ /**
+ * Tests a recipe can be required using --no-install and installed later.
+ */
+ public function testRecipeNotUnpackedIfInstallIsDeferred(): void {
+ $root_project_path = $this->fixturesDir . '/composer-root';
+
+ $this->runComposer('install');
+
+ // Install a recipe and check it is in `composer.json` but not unpacked or
+ // physically installed.
+ $stdOut = $this->runComposer('require --verbose --no-install fixtures/recipe-a');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertSame("Recipes are not unpacked when the --no-install option is used.", trim($stdOut));
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/recipe-a',
+ ], array_keys($root_composer_json['require']));
+ $this->assertFileDoesNotExist($root_project_path . '/recipes/recipe-a/recipe.yml');
+
+ // After installing dependencies, the recipe should be installed, but still
+ // not unpacked.
+ $this->runComposer('install');
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/recipe-a',
+ ], array_keys($root_composer_json['require']));
+ $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate');
+ }
+
+ /**
+ * Tests that recipes are unpacked when using `composer create-project`.
+ */
+ public function testComposerCreateProject(): void {
+ // Prepare the project to use for create-project.
+ $root_project_path = $this->fixturesDir . '/composer-root';
+ $this->runComposer('require --verbose --no-install fixtures/recipe-a');
+
+ $stdOut = $this->runComposer('create-project --repository=\'{"type": "path","url": "' . $root_project_path . '","options": {"symlink": false}}\' fixtures/root composer-root2 -s dev', $this->fixturesDir);
+ // The recipes depended upon by the project, even indirectly, should all
+ // have been unpacked.
+ $this->assertSame("fixtures/recipe-b unpacked.\nfixtures/recipe-a unpacked.\n", $stdOut);
+ $this->doTestRecipeAUnpacked($this->fixturesDir . '/composer-root2', $stdOut);
+ }
+
+ /**
+ * Tests Recipe A is unpacked correctly.
+ *
+ * @param string $root_project_path
+ * Path to the composer project under test.
+ * @param string $stdout
+ * The standard out from the composer command unpacks the recipe.
+ */
+ private function doTestRecipeAUnpacked(string $root_project_path, string $stdout): void {
+ $root_composer_json = $this->getFileContents($root_project_path . '/composer.json');
+
+ // @see core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/composer.json
+ // @see core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/composer.json
+ $expected_unpacked = [
+ 'fixtures/recipe-a' => [
+ 'fixtures/module-b',
+ ],
+ 'fixtures/recipe-b' => [
+ 'fixtures/module-a',
+ 'fixtures/theme-a',
+ ],
+ ];
+ foreach ($expected_unpacked as $package => $dependencies) {
+ // When the package is unpacked, the unpacked dependencies should be logged
+ // in the stdout.
+ $this->assertStringContainsString("$package unpacked.", $stdout);
+
+ // After being unpacked, the package should be removed from the root
+ // composer.json and composer.lock.
+ $this->assertArrayNotHasKey($package, $root_composer_json['require']);
+
+ foreach ($dependencies as $dependency) {
+ // The package dependencies should be in the root composer.json.
+ $this->assertArrayHasKey($dependency, $root_composer_json['require']);
+ }
+ }
+
+ // Ensure the resulting Composer files are valid.
+ $this->runComposer('validate', $root_project_path);
+
+ // The dev dependency has moved.
+ $this->assertArrayNotHasKey('require-dev', $root_composer_json);
+
+ // Ensure recipe files exist.
+ $this->assertFileExists($root_project_path . '/recipes/recipe-a/recipe.yml');
+ $this->assertFileExists($root_project_path . '/recipes/recipe-b/recipe.yml');
+
+ // Ensure composer.lock is ordered correctly.
+ $root_composer_lock = $this->getFileContents($root_project_path . '/composer.lock');
+ $this->assertSame([
+ 'composer/installers',
+ 'drupal/core-recipe-unpack',
+ 'fixtures/module-a',
+ 'fixtures/module-b',
+ 'fixtures/theme-a',
+ ], array_column($root_composer_lock['packages'], 'name'));
+ }
+
+ /**
+ * Executes a Composer command with standard options.
+ *
+ * @param string $command
+ * The composer command to execute.
+ * @param string $cwd
+ * The current working directory to run the command from.
+ * @param string $error_output
+ * Passed by reference to allow error output to be tested.
+ *
+ * @return string
+ * Standard output from the command.
+ */
+ private function runComposer(string $command, ?string $cwd = NULL, string &$error_output = ''): string {
+ $cwd ??= $this->fixturesDir . '/composer-root';
+
+ // Always add --no-interaction and --no-ansi to Composer commands.
+ $output = $this->mustExec("composer $command --no-interaction --no-ansi", $cwd, [], $error_output);
+ if ($command === 'install') {
+ $this->assertFileExists($cwd . '/composer.lock');
+ }
+ return $output;
+ }
+
+ /**
+ * Gets the contents of a file as an array.
+ *
+ * @param string $path
+ * The path to the file.
+ *
+ * @return array
+ * The contents of the file as an array.
+ */
+ private function getFileContents(string $path): array {
+ $file = file_get_contents($path);
+ return json_decode($file, TRUE, flags: JSON_THROW_ON_ERROR);
+ }
+
+}
diff --git a/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php b/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
index 806fd530a31c..f7155bbac018 100644
--- a/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
+++ b/core/tests/Drupal/BuildTests/Composer/Template/ComposerProjectTemplatesTest.php
@@ -8,6 +8,8 @@ use Composer\Json\JsonFile;
use Composer\Semver\VersionParser;
use Drupal\BuildTests\Composer\ComposerBuildTestBase;
use Drupal\Composer\Composer;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
/**
* Demonstrate that Composer project templates can be built as patched.
@@ -21,9 +23,8 @@ use Drupal\Composer\Composer;
*
* This is because Composer only uses the packages.json file to resolve the
* project template and not any other dependencies.
- *
- * @group Template
*/
+#[Group('Template')]
class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
/**
@@ -109,6 +110,7 @@ class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
$exclude = [
'drupal/core',
+ 'drupal/core-recipe-unpack',
'drupal/core-project-message',
'drupal/core-vendor-hardening',
];
@@ -170,9 +172,7 @@ class ComposerProjectTemplatesTest extends ComposerBuildTestBase {
}
}
- /**
- * @dataProvider provideTemplateCreateProject
- */
+ #[DataProvider('provideTemplateCreateProject')]
public function testTemplateCreateProject($project, $package_dir, $docroot_dir): void {
// Make a working COMPOSER_HOME directory for setting global composer config
$composer_home = $this->getWorkspaceDirectory() . '/composer-home';
diff --git a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
index e92c75468843..0e0912109e83 100644
--- a/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
+++ b/core/tests/Drupal/BuildTests/Framework/BuildTestBase.php
@@ -333,8 +333,8 @@ abstract class BuildTestBase extends TestCase {
public function executeCommand($command_line, $working_dir = NULL) {
$this->commandProcess = Process::fromShellCommandline($command_line);
$this->commandProcess->setWorkingDirectory($this->getWorkingPath($working_dir))
- ->setTimeout(300)
- ->setIdleTimeout(300);
+ ->setTimeout(360)
+ ->setIdleTimeout(360);
$this->commandProcess->run();
return $this->commandProcess;
}
diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
index 6e29abe18ee0..7f97eb530274 100644
--- a/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
+++ b/core/tests/Drupal/BuildTests/Framework/Tests/BuildTestTest.php
@@ -6,13 +6,16 @@ namespace Drupal\BuildTests\Framework\Tests;
use Drupal\BuildTests\Framework\BuildTestBase;
use org\bovigo\vfs\vfsStream;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
/**
- * @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
- * @group Build
+ * Tests Drupal\BuildTests\Framework\BuildTestBase.
*/
+#[CoversClass(BuildTestBase::class)]
+#[Group('Build')]
class BuildTestTest extends BuildTestBase {
/**
@@ -34,7 +37,7 @@ class BuildTestTest extends BuildTestBase {
}
/**
- * @covers ::copyCodebase
+ * @legacy-covers ::copyCodebase
*/
public function testCopyCodebase(): void {
$test_directory = 'copied_codebase';
@@ -56,7 +59,7 @@ class BuildTestTest extends BuildTestBase {
/**
* Ensure we're not copying directories we wish to exclude.
*
- * @covers ::copyCodebase
+ * @legacy-covers ::copyCodebase
*/
public function testCopyCodebaseExclude(): void {
// Create a virtual file system containing items that should be
@@ -129,7 +132,7 @@ class BuildTestTest extends BuildTestBase {
/**
* Tests copying codebase when Drupal and Composer roots are different.
*
- * @covers ::copyCodebase
+ * @legacy-covers ::copyCodebase
*/
public function testCopyCodebaseDocRoot(): void {
// Create a virtual file system containing items that should be
@@ -206,7 +209,7 @@ class BuildTestTest extends BuildTestBase {
}
/**
- * @covers ::findAvailablePort
+ * @legacy-covers ::findAvailablePort
*/
public function testPortMany(): void {
$iterator = (new Finder())->in($this->getDrupalRoot())
@@ -234,7 +237,7 @@ class BuildTestTest extends BuildTestBase {
}
/**
- * @covers ::standUpServer
+ * @legacy-covers ::standUpServer
*/
public function testStandUpServer(): void {
// Stand up a server with working directory 'first'.
diff --git a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
index f34f5e7f896c..825cc6eaa468 100644
--- a/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
+++ b/core/tests/Drupal/BuildTests/Framework/Tests/HtRouterTest.php
@@ -4,18 +4,23 @@ declare(strict_types=1);
namespace Drupal\BuildTests\Framework\Tests;
+use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\BuildTests\QuickStart\QuickStartTestBase;
use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RequiresPhpExtension;
/**
- * @coversDefaultClass \Drupal\BuildTests\Framework\BuildTestBase
- * @group Build
- * @requires extension pdo_sqlite
+ * Tests Drupal\BuildTests\Framework\BuildTestBase.
*/
+#[CoversClass(BuildTestBase::class)]
+#[Group('Build')]
+#[RequiresPhpExtension('pdo_sqlite')]
class HtRouterTest extends QuickStartTestBase {
/**
- * @covers ::instantiateServer
+ * @legacy-covers ::instantiateServer
*/
public function testHtRouter(): void {
$sqlite = (new \PDO('sqlite::memory:'))->query('select sqlite_version()')->fetch()[0];
diff --git a/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php b/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php
index bbcce3d2ca2a..59622ab504f9 100644
--- a/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php
+++ b/core/tests/Drupal/BuildTests/TestSiteApplication/InstallTest.php
@@ -6,13 +6,15 @@ namespace Drupal\BuildTests\TestSiteApplication;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\sqlite\Driver\Database\sqlite\Install\Tasks;
+use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
/**
- * @group Build
- * @group TestSiteApplication
+ * Tests.
*/
+#[Group('Build')]
+#[Group('TestSiteApplication')]
class InstallTest extends BuildTestBase {
public function testInstall(): void {
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
index 31f044175860..3df154e90500 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php
@@ -196,7 +196,7 @@ class MachineNameTest extends WebDriverTestBase {
$assert->fieldExists('Name')->setValue('test 1');
$machine_name_value = $page->find('css', '#edit-name-machine-name-suffix .machine-name-value');
$this->assertNotEmpty($machine_name_value, 'Machine name field must be initialized');
- $this->assertJsCondition('jQuery("#edit-name-machine-name-suffix .machine-name-value").html() == "' . 'test_1' . '"');
+ $this->assertJsCondition('jQuery("#edit-name-machine-name-suffix .machine-name-value").html() == "test_1"');
// Ensure that machine name generation still occurs after a non-HTML 5
// validation failure.
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php b/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
index d624e03c00d9..60f7b44fe0f4 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/PerformanceTestBase.php
@@ -13,7 +13,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @ingroup testing
*/
-class PerformanceTestBase extends WebDriverTestBase {
+abstract class PerformanceTestBase extends WebDriverTestBase {
use PerformanceTestTrait;
/**
diff --git a/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php b/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
index 80fd287751c5..aac21dcff875 100644
--- a/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
+++ b/core/tests/Drupal/FunctionalTests/Bootstrap/UncaughtExceptionTest.php
@@ -203,6 +203,7 @@ class UncaughtExceptionTest extends BrowserTestBase {
switch ($this->container->get('database')->driver()) {
case 'pgsql':
case 'mysql':
+ case 'mysqli':
$this->expectedExceptionMessage = $incorrect_username;
break;
diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php
index e7509aa85281..04e771f8a654 100644
--- a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php
+++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeInstallTest.php
@@ -46,7 +46,7 @@ class StandardRecipeInstallTest extends InstallerTestBase {
*/
protected function visitInstaller(): void {
// Use a URL to install from a recipe.
- $this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?profile=&recipe=core/recipes/standard');
+ $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?profile=&recipe=core/recipes/standard');
}
/**
diff --git a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
index 4bf46e8d4911..68a793f8771f 100644
--- a/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
+++ b/core/tests/Drupal/FunctionalTests/Core/Recipe/StandardRecipeTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\FunctionalTests\Core\Recipe;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\shortcut\Entity\Shortcut;
use Drupal\Tests\standard\Functional\StandardTest;
use Drupal\user\RoleInterface;
@@ -35,7 +36,12 @@ class StandardRecipeTest extends StandardTest {
$theme_installer->uninstall(['claro', 'olivero']);
// Determine which modules to uninstall.
- $uninstall = array_diff(array_keys(\Drupal::moduleHandler()->getModuleList()), ['user', 'system', 'path_alias', \Drupal::database()->getProvider()]);
+ // If the database module has dependencies, they are expected too.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get(\Drupal::database()->getProvider());
+ $database_modules = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
+ $database_modules[] = \Drupal::database()->getProvider();
+ $keep = array_merge(['user', 'system', 'path_alias'], $database_modules);
+ $uninstall = array_diff(array_keys(\Drupal::moduleHandler()->getModuleList()), $keep);
foreach (['shortcut', 'field_config', 'filter_format', 'field_storage_config'] as $entity_type) {
$storage = \Drupal::entityTypeManager()->getStorage($entity_type);
$storage->delete($storage->loadMultiple());
diff --git a/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php b/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php
index e775b358550a..e5901e52fa41 100644
--- a/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php
+++ b/core/tests/Drupal/FunctionalTests/Entity/RevisionDeleteFormTest.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Drupal\FunctionalTests\Entity;
-use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\entity_test\Entity\EntityTestRevPub;
@@ -247,7 +246,7 @@ class RevisionDeleteFormTest extends BrowserTestBase {
*
* @covers ::submitForm
*/
- protected function doTestSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, int $totalRevisions, string $expectedLog, string $expectedMessage, $expectedDestination): void {
+ protected function doTestSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, int $totalRevisions, array $expectedLog, string $expectedMessage, $expectedDestination): void {
if (count($permissions) > 0) {
$this->drupalLogin($this->createUser($permissions));
}
@@ -297,7 +296,9 @@ class RevisionDeleteFormTest extends BrowserTestBase {
// Logger log.
$logs = $this->getLogs($entity->getEntityType()->getProvider());
- $this->assertEquals([0 => $expectedLog], $logs);
+ $this->assertCount(1, $logs);
+ $this->assertEquals("@type: deleted %title revision %revision.", $logs[0]->message);
+ $this->assertEquals($expectedLog, unserialize($logs[0]->variables));
// Messenger message.
$this->assertSession()->pageTextContains($expectedMessage);
\Drupal::database()->delete('watchdog')->execute();
@@ -314,7 +315,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_rev',
'view all revisions, delete revision',
2,
- 'entity_test_rev: deleted <em class="placeholder">view all revisions, delete revision</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view all revisions, delete revision',
+ '%revision' => '1',
+ ],
'Revision of Entity Test Bundle view all revisions, delete revision has been deleted.',
'/entity_test_rev/1/revisions',
];
@@ -324,7 +329,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_rev',
'view, view all revisions, delete revision',
2,
- 'entity_test_rev: deleted <em class="placeholder">view, view all revisions, delete revision</em> revision <em class="placeholder">3</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view, view all revisions, delete revision',
+ '%revision' => '3',
+ ],
'Revision of Entity Test Bundle view, view all revisions, delete revision has been deleted.',
'/entity_test_rev/2/revisions',
];
@@ -334,7 +343,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_revlog',
'view all revisions, delete revision',
2,
- 'entity_test_revlog: deleted <em class="placeholder">view all revisions, delete revision</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view all revisions, delete revision',
+ '%revision' => '1',
+ ],
'Revision from Sun, 11 Jan 2009 - 16:00 of Test entity - revisions log view all revisions, delete revision has been deleted.',
'/entity_test_revlog/1/revisions',
];
@@ -344,7 +357,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
'entity_test_revlog',
'view, view all revisions, delete revision',
2,
- 'entity_test_revlog: deleted <em class="placeholder">view, view all revisions, delete revision</em> revision <em class="placeholder">3</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view, view all revisions, delete revision',
+ '%revision' => '3',
+ ],
'Revision from Sun, 11 Jan 2009 - 16:00 of Test entity - revisions log view, view all revisions, delete revision has been deleted.',
'/entity_test_revlog/2/revisions',
];
@@ -362,14 +379,11 @@ class RevisionDeleteFormTest extends BrowserTestBase {
* Watchdog entries.
*/
protected function getLogs(string $channel): array {
- $logs = \Drupal::database()->select('watchdog')
+ return \Drupal::database()->select('watchdog')
->fields('watchdog')
->condition('type', $channel)
->execute()
->fetchAll();
- return array_map(function (object $log) {
- return (string) new FormattableMarkup($log->message, unserialize($log->variables));
- }, $logs);
}
}
diff --git a/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php b/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php
index c6ff4629fd9b..8ca59b6100e4 100644
--- a/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php
+++ b/core/tests/Drupal/FunctionalTests/Entity/RevisionRevertFormTest.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Drupal\FunctionalTests\Entity;
-use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Entity\RevisionLogInterface;
use Drupal\entity_test\Entity\EntityTestRev;
use Drupal\entity_test\Entity\EntityTestRevPub;
@@ -192,7 +191,7 @@ class RevisionRevertFormTest extends BrowserTestBase {
* @covers ::submitForm
* @dataProvider providerSubmitForm
*/
- public function testSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, string $expectedLog, string $expectedMessage, string $expectedDestination): void {
+ public function testSubmitForm(array $permissions, string $entityTypeId, string $entityLabel, array $expectedLog, string $expectedMessage, string $expectedDestination): void {
if (count($permissions) > 0) {
$this->drupalLogin($this->createUser($permissions));
}
@@ -231,7 +230,10 @@ class RevisionRevertFormTest extends BrowserTestBase {
// Logger log.
$logs = $this->getLogs($entity->getEntityType()->getProvider());
- $this->assertEquals([0 => $expectedLog], $logs);
+ $this->assertCount(1, $logs);
+ $this->assertEquals('@type: reverted %title revision %revision.', $logs[0]->message);
+ $this->assertEquals($expectedLog, unserialize($logs[0]->variables));
+
// Messenger message.
$this->assertSession()->pageTextContains($expectedMessage);
}
@@ -246,7 +248,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
['view test entity'],
'entity_test_rev',
'view, revert',
- 'entity_test_rev: reverted <em class="placeholder">view, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view, revert',
+ '%revision' => '1',
+ ],
'Entity Test Bundle view, revert has been reverted.',
'/entity_test_rev/manage/1',
];
@@ -255,7 +261,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
['view test entity'],
'entity_test_rev',
'view, view all revisions, revert',
- 'entity_test_rev: reverted <em class="placeholder">view, view all revisions, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_rev',
+ '%title' => 'view, view all revisions, revert',
+ '%revision' => '1',
+ ],
'Entity Test Bundle view, view all revisions, revert has been reverted.',
'/entity_test_rev/1/revisions',
];
@@ -264,7 +274,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
[],
'entity_test_revlog',
'view, revert',
- 'entity_test_revlog: reverted <em class="placeholder">view, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view, revert',
+ '%revision' => '1',
+ ],
'Test entity - revisions log view, revert has been reverted to the revision from Sun, 11 Jan 2009 - 16:00.',
'/entity_test_revlog/manage/1',
];
@@ -273,7 +287,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
[],
'entity_test_revlog',
'view, view all revisions, revert',
- 'entity_test_revlog: reverted <em class="placeholder">view, view all revisions, revert</em> revision <em class="placeholder">1</em>.',
+ [
+ '@type' => 'entity_test_revlog',
+ '%title' => 'view, view all revisions, revert',
+ '%revision' => '1',
+ ],
'Test entity - revisions log view, view all revisions, revert has been reverted to the revision from Sun, 11 Jan 2009 - 16:00.',
'/entity_test_revlog/1/revisions',
];
@@ -347,14 +365,11 @@ class RevisionRevertFormTest extends BrowserTestBase {
* Watchdog entries.
*/
protected function getLogs(string $channel): array {
- $logs = \Drupal::database()->select('watchdog')
+ return \Drupal::database()->select('watchdog')
->fields('watchdog')
->condition('type', $channel)
->execute()
->fetchAll();
- return array_map(function (object $log) {
- return (string) new FormattableMarkup($log->message, unserialize($log->variables));
- }, $logs);
}
/**
diff --git a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
index 01c694b4ef4b..fd7bfeecd38c 100644
--- a/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
+++ b/core/tests/Drupal/FunctionalTests/ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest.php
@@ -26,7 +26,7 @@ class ExistingDrupal8StyleDatabaseConnectionInSettingsPhpTest extends BrowserTes
$driver = Database::getConnection()->driver();
if (!in_array($driver, ['mysql', 'pgsql', 'sqlite'])) {
- $this->markTestSkipped("This test does not support the {$driver} database driver.");
+ $this->markTestSkipped("This test is only relevant for database drivers that were available in Drupal prior to database drivers becoming part of modules. The {$driver} database driver is not qualifying.");
}
$filename = $this->siteDirectory . '/settings.php';
diff --git a/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php b/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php
index 764e9d15d65d..67bdc2bb70a1 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/DistributionProfileTranslationQueryTest.php
@@ -67,7 +67,7 @@ class DistributionProfileTranslationQueryTest extends InstallerTestBase {
// profile. This distribution language should still be used.
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
- $this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?langcode=fr');
+ $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=fr');
}
/**
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
index 2ac3ae778a4b..f7c949637547 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerExistingConfigTestBase.php
@@ -7,6 +7,7 @@ namespace Drupal\FunctionalTests\Installer;
use Drupal\Component\Serialization\Yaml;
use Drupal\Core\Archiver\ArchiveTar;
use Drupal\Core\Database\Database;
+use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Installer\Form\SelectProfileForm;
/**
@@ -99,8 +100,11 @@ abstract class InstallerExistingConfigTestBase extends InstallerTestBase {
// modules that can not be uninstalled in the core.extension configuration.
if (file_exists($config_sync_directory . '/core.extension.yml')) {
$core_extension = Yaml::decode(file_get_contents($config_sync_directory . '/core.extension.yml'));
- $module = Database::getConnection()->getProvider();
- if ($module !== 'core') {
+ // If the database module has dependencies, they are expected too.
+ $database_module_extension = \Drupal::service(ModuleExtensionList::class)->get(Database::getConnection()->getProvider());
+ $database_modules = $database_module_extension->requires ? array_keys($database_module_extension->requires) : [];
+ $database_modules[] = Database::getConnection()->getProvider();
+ foreach ($database_modules as $module) {
$core_extension['module'][$module] = 0;
$core_extension['module'] = module_config_sort($core_extension['module']);
}
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
index 3bd0fe48af73..d33d7c4942ab 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerNonDefaultDatabaseDriverTest.php
@@ -63,25 +63,20 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
// Assert that in the settings.php the database connection array has the
// correct values set.
- $contents = file_get_contents($this->container->getParameter('app.root') . '/' . $this->siteDirectory . '/settings.php');
- $this->assertStringContainsString("'namespace' => 'Drupal\\\\driver_test\\\\Driver\\\\Database\\\\{$this->testDriverName}',", $contents);
- $this->assertStringContainsString("'driver' => '{$this->testDriverName}',", $contents);
- $this->assertStringContainsString("'autoload' => 'core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/',", $contents);
-
- $dependencies = "'dependencies' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'mysql' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'namespace' => 'Drupal\\\\mysql'," . PHP_EOL .
- " 'autoload' => 'core/modules/mysql/src/'," . PHP_EOL .
- " )," . PHP_EOL .
- " 'pgsql' => " . PHP_EOL .
- " array (" . PHP_EOL .
- " 'namespace' => 'Drupal\\\\pgsql'," . PHP_EOL .
- " 'autoload' => 'core/modules/pgsql/src/'," . PHP_EOL .
- " )," . PHP_EOL .
- " )," . PHP_EOL;
- $this->assertStringContainsString($dependencies, $contents);
+ $installedDatabaseSettings = $this->getInstalledDatabaseSettings();
+ $this->assertSame("Drupal\\driver_test\\Driver\\Database\\{$this->testDriverName}", $installedDatabaseSettings['default']['default']['namespace']);
+ $this->assertSame($this->testDriverName, $installedDatabaseSettings['default']['default']['driver']);
+ $this->assertSame("core/modules/system/tests/modules/driver_test/src/Driver/Database/{$this->testDriverName}/", $installedDatabaseSettings['default']['default']['autoload']);
+ $this->assertEquals([
+ 'mysql' => [
+ 'namespace' => 'Drupal\\mysql',
+ 'autoload' => 'core/modules/mysql/src/',
+ ],
+ 'pgsql' => [
+ 'namespace' => 'Drupal\\pgsql',
+ 'autoload' => 'core/modules/pgsql/src/',
+ ],
+ ], $installedDatabaseSettings['default']['default']['dependencies']);
// Assert that the module "driver_test" and its dependencies have been
// installed.
@@ -99,4 +94,22 @@ class InstallerNonDefaultDatabaseDriverTest extends InstallerTestBase {
$this->assertSession()->elementTextContains('xpath', '//tr[@data-drupal-selector="edit-pgsql"]', "The following reason prevents PostgreSQL from being uninstalled: Required by: driver_test");
}
+ /**
+ * Returns the databases setup from the SUT's settings.php.
+ *
+ * @return array<string,mixed>
+ * The value of the $databases variable.
+ */
+ protected function getInstalledDatabaseSettings(): array {
+ // The $app_root and $site_path variables are required by the settings.php
+ // file to be parsed correctly. The $databases variable is set in the
+ // included file, we need to inform PHPStan about that since PHPStan itself
+ // is unable to determine it.
+ $app_root = $this->container->getParameter('app.root');
+ $site_path = $this->siteDirectory;
+ include $app_root . '/' . $site_path . '/settings.php';
+ assert(isset($databases));
+ return $databases;
+ }
+
}
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
index c1694932a0f8..f1d2d507eeed 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTestBase.php
@@ -271,7 +271,7 @@ abstract class InstallerTestBase extends BrowserTestBase {
* Override this method to test specific requirements warnings or errors
* during the installer.
*
- * @see system_requirements()
+ * @see \Drupal\system\Install\SystemRequirements
*/
protected function setUpRequirementsProblem() {
if (version_compare(phpversion(), PhpRequirements::getMinimumSupportedPhp()) < 0) {
diff --git a/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php b/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php
index 532c23a814ee..97976121601d 100644
--- a/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php
+++ b/core/tests/Drupal/FunctionalTests/Installer/InstallerTranslationQueryTest.php
@@ -35,7 +35,7 @@ class InstallerTranslationQueryTest extends InstallerTestBase {
// The unrouted URL assembler does not exist at this point, so we build the
// URL ourselves.
- $this->drupalGet($GLOBALS['base_url'] . '/core/install.php' . '?langcode=' . $this->langcode);
+ $this->drupalGet($GLOBALS['base_url'] . '/core/install.php?langcode=' . $this->langcode);
// The language should have been automatically detected, all following
// screens should be translated already.
diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
index 8b2c4b493e4b..3ee78e553654 100644
--- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
+++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php
@@ -137,6 +137,16 @@ abstract class UpdatePathTestBase extends BrowserTestBase {
// Load the database(s).
foreach ($this->databaseDumpFiles as $file) {
+ // Determine the version of the database dump if specified.
+ $matches = [];
+ $dumpVersion = preg_match('/drupal-(\d+\.\d+\.\d+)\./', $file, $matches) === 1 ? $matches[1] : NULL;
+
+ // If the db driver is mysqli, we do not need to run the update tests for
+ // db dumps prior to 11.2 when the module was introduced.
+ if (Database::getConnection()->getProvider() === 'mysqli' && $dumpVersion && version_compare($dumpVersion, '11.2.0', '<')) {
+ $this->markTestSkipped("The mysqli driver was introduced in Drupal 11.2, skip update tests from database at version {$dumpVersion}");
+ }
+
if (str_ends_with($file, '.gz')) {
$file = "compress.zlib://$file";
}
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php b/core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php
new file mode 100644
index 000000000000..147b647d2da4
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Components;
+
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Tests the correct rendering of components in form.
+ *
+ * @group sdc
+ */
+class ComponentInFormTest extends ComponentKernelTestBase implements FormInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'system',
+ 'sdc_test',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $themes = ['sdc_theme_test'];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId(): string {
+ return 'component_in_form_test';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state): array {
+ $form['normal'] = [
+ '#type' => 'textfield',
+ '#title' => 'Normal form element',
+ '#default_value' => 'fake 1',
+ ];
+
+ // We want to test form elements inside a component, itself inside a
+ // component.
+ $form['banner'] = [
+ '#type' => 'component',
+ '#component' => 'sdc_test:my-banner',
+ '#props' => [
+ 'ctaText' => 'Click me!',
+ 'ctaHref' => 'https://www.example.org',
+ 'ctaTarget' => '',
+ ],
+ 'banner_body' => [
+ '#type' => 'component',
+ '#component' => 'sdc_theme_test:my-card',
+ '#props' => [
+ 'header' => 'Card header',
+ ],
+ 'card_body' => [
+ 'foo' => [
+ '#type' => 'textfield',
+ '#title' => 'Textfield in component',
+ '#default_value' => 'fake 2',
+ ],
+ 'bar' => [
+ '#type' => 'select',
+ '#title' => 'Select in component',
+ '#options' => [
+ 'option_1' => 'Option 1',
+ 'option_2' => 'Option 2',
+ ],
+ '#empty_option' => 'Empty option',
+ '#default_value' => 'option_1',
+ ],
+ ],
+ ],
+ ];
+
+ $form['actions'] = [
+ '#type' => 'actions',
+ 'submit' => [
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ ],
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state): void {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state): void {
+ // Check that submitted data are present (set with #default_value).
+ $data = [
+ 'normal' => 'fake 1',
+ 'foo' => 'fake 2',
+ 'bar' => 'option_1',
+ ];
+ foreach ($data as $key => $value) {
+ $this->assertSame($value, $form_state->getValue($key));
+ }
+ }
+
+ /**
+ * Tests that fields validation messages are sorted in the fields order.
+ */
+ public function testFormRenderingAndSubmission(): void {
+ /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */
+ $form_builder = \Drupal::service('form_builder');
+ /** @var \Drupal\Core\Render\RendererInterface $renderer */
+ $renderer = \Drupal::service('renderer');
+ $form = $form_builder->getForm($this);
+
+ // Test form structure after being processed.
+ $this->assertTrue($form['normal']['#processed'], 'The normal textfield should have been processed.');
+ $this->assertTrue($form['banner']['banner_body']['card_body']['bar']['#processed'], 'The textfield inside component should have been processed.');
+ $this->assertTrue($form['banner']['banner_body']['card_body']['foo']['#processed'], 'The select inside component should have been processed.');
+ $this->assertTrue($form['actions']['submit']['#processed'], 'The submit button should have been processed.');
+
+ // Test form rendering.
+ $markup = $renderer->renderRoot($form);
+ $this->setRawContent($markup);
+
+ // Ensure form elements are rendered once.
+ $this->assertCount(1, $this->cssSelect('input[name="normal"]'), 'The normal textfield should have been rendered once.');
+ $this->assertCount(1, $this->cssSelect('input[name="foo"]'), 'The foo textfield should have been rendered once.');
+ $this->assertCount(1, $this->cssSelect('select[name="bar"]'), 'The bar select should have been rendered once.');
+
+ // Check the position of the form elements in the DOM.
+ $paths = [
+ '//form/div[1]/input[@name="normal"]',
+ '//form/div[2][@data-component-id="sdc_test:my-banner"]/div[2][@class="component--my-banner--body"]/div[1][@data-component-id="sdc_theme_test:my-card"]/div[1][@class="component--my-card__body"]/div[1]/input[@name="foo"]',
+ '//form/div[2][@data-component-id="sdc_test:my-banner"]/div[2][@class="component--my-banner--body"]/div[1][@data-component-id="sdc_theme_test:my-card"]/div[1][@class="component--my-card__body"]/div[2]/select[@name="bar"]',
+ ];
+ foreach ($paths as $path) {
+ $this->assertNotEmpty($this->xpath($path), 'There should be a result with the path: ' . $path . '.');
+ }
+
+ // Test form submission. Assertions are in submitForm().
+ $form_state = new FormState();
+ $form_builder->submitForm($this, $form_state);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php b/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php
index eb38e858f0d2..ffdf5e96bf4c 100644
--- a/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php
+++ b/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php
@@ -72,6 +72,7 @@ class ComponentNegotiatorTest extends ComponentKernelTestBase {
'#type' => 'inline_template',
'#template' => "{{ include('sdc_theme_test:my-card') }}",
'#context' => ['header' => 'Foo bar'],
+ '#variant' => 'horizontal',
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper .component--my-card--replaced__body'));
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php b/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php
index 6b72fa745ea5..d48694330727 100644
--- a/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php
+++ b/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php
@@ -22,6 +22,9 @@ class ComponentNodeVisitorTest extends ComponentKernelTestBase {
*/
protected static $themes = ['sdc_theme_test'];
+ const DEBUG_COMPONENT_ID_PATTERN = '/<!-- ([\n\s\S]*) Component start: ([\SA-Za-z+-:]+) -->/';
+ const DEBUG_VARIANT_ID_PATTERN = '/<!-- [\n\s\S]* with variant: "([\SA-Za-z+-]+)" -->/';
+
/**
* Test that other visitors can modify Twig nodes.
*/
@@ -36,4 +39,37 @@ class ComponentNodeVisitorTest extends ComponentKernelTestBase {
$this->assertTrue(TRUE);
}
+ /**
+ * Test debug output for sdc components with component id and variant.
+ */
+ public function testDebugRendersComponentStartWithVariant(): void {
+ // Enable twig theme debug to ensure that any
+ // changes to theme debugging format force checking
+ // that the auto paragraph filter continues to be applied
+ // correctly.
+ $twig = \Drupal::service('twig');
+ $twig->enableDebug();
+
+ $build = [
+ '#type' => 'component',
+ '#component' => 'sdc_theme_test:my-card',
+ '#variant' => 'vertical',
+ '#props' => [
+ 'header' => 'My header',
+ ],
+ '#slots' => [
+ 'card_body' => 'Foo bar',
+ ],
+ ];
+ $crawler = $this->renderComponentRenderArray($build);
+ $content = $crawler->html();
+
+ $matches = [];
+ \preg_match_all(self::DEBUG_COMPONENT_ID_PATTERN, $content, $matches);
+ $this->assertSame($matches[2][0], 'sdc_theme_test:my-card');
+
+ \preg_match_all(self::DEBUG_VARIANT_ID_PATTERN, $content, $matches);
+ $this->assertSame($matches[1][0], 'vertical');
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php b/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php
new file mode 100644
index 000000000000..1b012c9f7262
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Components;
+
+/**
+ * Tests discovery of components in a theme being installed or uninstalled.
+ *
+ * @group sdc
+ */
+class ComponentPluginManagerCachedDiscoveryTest extends ComponentKernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $themes = ['stark'];
+
+ /**
+ * Tests cached component plugin discovery on theme install and uninstall.
+ */
+ public function testComponentDiscoveryOnThemeInstall(): void {
+ // Component in sdc_theme should not be found without sdc_theme installed.
+ $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
+ $this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions);
+
+ // Component in sdc_theme should be found once sdc_theme is installed.
+ \Drupal::service('theme_installer')->install(['sdc_theme_test']);
+ $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
+ $this->assertArrayHasKey('sdc_theme_test:bar', $definitions);
+
+ // Component in sdc_theme should not be found once sdc_theme is uninstalled.
+ \Drupal::service('theme_installer')->uninstall(['sdc_theme_test']);
+ $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions();
+ $this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php b/core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php
index 70a5e804af78..944d6cb145bc 100644
--- a/core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php
+++ b/core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php
@@ -96,7 +96,7 @@ class ComponentRenderTest extends ComponentKernelTestBase {
$build = [
'#type' => 'inline_template',
'#context' => ['content' => $content],
- '#template' => "{% embed 'sdc_theme_test:my-card' with { header: 'Card header', content: content } only %}{% block card_body %}This is a card with a CTA {{ include('sdc_test:my-cta', { text: content.heading, href: 'https://www.example.org', target: '_blank' }, with_context = false) }}{% endblock %}{% endembed %}",
+ '#template' => "{% embed 'sdc_theme_test:my-card' with { variant: 'horizontal', header: 'Card header', content: content } only %}{% block card_body %}This is a card with a CTA {{ include('sdc_test:my-cta', { text: content.heading, href: 'https://www.example.org', target: '_blank' }, with_context = false) }}{% endblock %}{% endembed %}",
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] h2.component--my-card__header:contains("Card header")'));
@@ -353,6 +353,36 @@ class ComponentRenderTest extends ComponentKernelTestBase {
}
/**
+ * Ensure that components variants render.
+ */
+ public function testVariants(): void {
+ $build = [
+ '#type' => 'component',
+ '#component' => 'sdc_test:my-cta',
+ '#variant' => 'primary',
+ '#props' => [
+ 'text' => 'Test link',
+ ],
+ ];
+ $crawler = $this->renderComponentRenderArray($build);
+ $this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"][data-component-variant="primary"][class*="my-cta-primary"]'));
+
+ // If there were an existing prop named variant, we don't override that for BC reasons.
+ $build = [
+ '#type' => 'component',
+ '#component' => 'sdc_test:my-cta-with-variant-prop',
+ '#variant' => 'tertiary',
+ '#props' => [
+ 'text' => 'Test link',
+ 'variant' => 'secondary',
+ ],
+ ];
+ $crawler = $this->renderComponentRenderArray($build);
+ $this->assertEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="tertiary"]'));
+ $this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="secondary"]'));
+ }
+
+ /**
* Ensures some key aspects of the plugin definition are correctly computed.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
diff --git a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
index 3ca62dcced59..f375f6f3dd69 100644
--- a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
+++ b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
@@ -377,6 +377,7 @@ class MappingTest extends KernelTestBase {
'field.value.decimal' => ['value'],
'field.value.float' => ['value'],
'field.value.timestamp' => ['value'],
+ 'field.value.language' => ['value'],
'field.value.comment' => [
'status',
'cid',
diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
index 6dd535e88f95..809e54737b7c 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php
@@ -467,4 +467,26 @@ class AttachedAssetsTest extends KernelTestBase {
$this->assertStringContainsString('<script src="' . str_replace('&', '&amp;', $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2')) . '&amp;' . $query_string . '"></script>', $rendered_js, 'JavaScript file with query string gets version query string correctly appended.');
}
+ /**
+ * Test settings can be loaded even when libraries are not.
+ */
+ public function testAttachedSettingsWithoutLibraries(): void {
+ $assets = new AttachedAssets();
+
+ // First test with no libraries will return no settings.
+ $assets->setSettings(['test' => 'foo']);
+ $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
+ $this->assertArrayNotHasKey('drupalSettings', $js);
+
+ // Second test with a warm cache.
+ $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
+ $this->assertArrayNotHasKey('drupalSettings', $js);
+
+ // Now test with different settings when drupalSettings is already loaded.
+ $assets->setSettings(['test' => 'bar']);
+ $assets->setAlreadyLoadedLibraries(['core/drupalSettings']);
+ $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1];
+ $this->assertSame('bar', $js['drupalSettings']['data']['test']);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
index 340dcc286494..15c97bea71f4 100644
--- a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
@@ -99,6 +99,20 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase {
protected function setUp(): void {
parent::setUp();
+ // Install all core themes.
+ sort($this->allThemes);
+ $this->container->get('theme_installer')->install($this->allThemes);
+
+ $this->themeHandler = $this->container->get('theme_handler');
+ $this->themeInitialization = $this->container->get('theme.initialization');
+ $this->themeManager = $this->container->get('theme.manager');
+ $this->libraryDiscovery = $this->container->get('library.discovery');
+ }
+
+ /**
+ * Ensures that all core module and theme library files exist.
+ */
+ public function testCoreLibraryCompleteness(): void {
// Enable all core modules.
$all_modules = $this->container->get('extension.list.module')->getList();
$all_modules = array_filter($all_modules, function ($module) {
@@ -141,21 +155,37 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase {
}
sort($this->allModules);
$this->container->get('module_installer')->install($this->allModules);
+ // Get a library discovery from the new container.
+ $this->libraryDiscovery = $this->container->get('library.discovery');
- // Install all core themes.
- sort($this->allThemes);
- $this->container->get('theme_installer')->install($this->allThemes);
+ $this->assertLibraries();
+ }
- $this->themeHandler = $this->container->get('theme_handler');
- $this->themeInitialization = $this->container->get('theme.initialization');
- $this->themeManager = $this->container->get('theme.manager');
+ /**
+ * Ensures that module and theme library files exist for a deprecated modules.
+ *
+ * @group legacy
+ */
+ public function testCoreLibraryCompletenessDeprecated(): void {
+ // Find and install deprecated modules to test.
+ $all_modules = $this->container->get('extension.list.module')->getList();
+ $deprecated_modules_to_test = array_filter($all_modules, function ($module) {
+ if ($module->origin == 'core'
+ && $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) {
+ return TRUE;
+ }
+ });
+ $this->container->get('module_installer')->install(array_keys($deprecated_modules_to_test));
$this->libraryDiscovery = $this->container->get('library.discovery');
+ $this->allModules = array_keys(\Drupal::moduleHandler()->getModuleList());
+
+ $this->assertLibraries();
}
/**
- * Ensures that all core module and theme library files exist.
+ * Asserts the libraries for modules and themes exist.
*/
- public function testCoreLibraryCompleteness(): void {
+ public function assertLibraries(): void {
// First verify all libraries with no active theme.
$this->verifyLibraryFilesExist($this->getAllLibraries());
diff --git a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php
index c2bff1232364..c1ff7e24406a 100644
--- a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Cache;
use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheTagsPurgeInterface;
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
@@ -62,4 +63,34 @@ class DatabaseBackendTagTest extends KernelTestBase {
$this->assertEquals($invalidations_before + 1, $invalidations_after, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.');
}
+ /**
+ * Test cache tag purging.
+ */
+ public function testTagsPurge(): void {
+ $tags = ['test_tag:1', 'test_tag:2', 'test_tag:3'];
+ /** @var \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_invalidator */
+ $checksum_invalidator = \Drupal::service('cache_tags.invalidator.checksum');
+ // Assert that initial current tag checksum is 0. This also ensures that the
+ // 'cachetags' table is created, which at this point does not exist yet.
+ $this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags));
+
+ /** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */
+ $invalidator = \Drupal::service('cache_tags.invalidator');
+ $invalidator->invalidateTags($tags);
+ // Checksum should be incremented by 1 by the invalidation for each tag.
+ $this->assertEquals(3, $checksum_invalidator->getCurrentChecksum($tags));
+
+ // After purging, confirm checksum is 0 and the 'cachetags' table is empty.
+ $this->assertInstanceOf(CacheTagsPurgeInterface::class, $invalidator);
+ $invalidator->purge();
+ $this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags));
+
+ $rows = Database::getConnection()->select('cachetags')
+ ->fields('cachetags')
+ ->countQuery()
+ ->execute()
+ ->fetchField();
+ $this->assertEmpty($rows, 'cachetags table is empty.');
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
index ad556d07e0ac..cb0b3086b141 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
@@ -260,7 +260,7 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
],
[
'dependencies.module.0' => [
- 'This value is not valid.',
+ 'This value is not a valid extension name.',
"Module 'invalid-module-name' is not installed.",
],
],
@@ -290,7 +290,7 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
],
[
'dependencies.theme.0' => [
- 'This value is not valid.',
+ 'This value is not a valid extension name.',
"Theme 'invalid-theme-name' is not installed.",
],
],
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
index bcdc76cc71d7..3d3ee862cef6 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php
@@ -28,6 +28,7 @@ class ConfigExistsConstraintValidatorTest extends KernelTestBase {
*
* @testWith [{}, "system.site", "system.site"]
* [{"prefix": "system."}, "site", "system.site"]
+ * [{"prefix": "system.[%parent.reference]."}, "admin", "system.menu.admin"]
*/
public function testValidation(array $constraint_options, string $value, string $expected_config_name): void {
// Create a data definition that specifies the value must be a string with
@@ -37,7 +38,11 @@ class ConfigExistsConstraintValidatorTest extends KernelTestBase {
/** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */
$typed_data = $this->container->get('typed_data_manager');
- $data = $typed_data->create($definition, $value);
+
+ // Create a data definition for the parent data.
+ $parent_data_definition = $typed_data->createDataDefinition('map');
+ $parent_data = $typed_data->create($parent_data_definition, ['reference' => 'menu']);
+ $data = $typed_data->create($definition, $value, 'data_name', $parent_data);
$violations = $data->validate();
$this->assertCount(1, $violations);
diff --git a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
index 079009dc7a56..8725f647e8d1 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php
@@ -63,18 +63,6 @@ class BasicSyntaxTest extends DatabaseTestBase {
}
/**
- * Tests string concatenation with separator, with field values.
- */
- public function testConcatWsFields(): void {
- $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [
- ':a1' => 'name',
- ':a2' => 'age',
- ':age' => 25,
- ]);
- $this->assertSame('name-John-age-25', $result->fetchField());
- }
-
- /**
* Tests escaping of LIKE wildcards.
*/
public function testLikeEscape(): void {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php
index ca5fb32936b5..a3b46ab67e12 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php
@@ -272,6 +272,16 @@ abstract class DriverSpecificSchemaTestBase extends DriverSpecificKernelTestBase
// Test the primary key columns.
$this->assertSame(['test_serial', 'test_composite_primary_key'], $method->invoke($this->schema, 'test_table'));
+ // Test adding and removing JSON column.
+ $this->schema->addField('test_table', 'test_json', [
+ 'description' => 'I heard you liked JSON.',
+ 'type' => 'json',
+ 'pgsql_type' => 'jsonb',
+ 'mysql_type' => 'json',
+ 'sqlite_type' => 'json',
+ ]);
+ $this->schema->dropField('test_table', 'test_json');
+
// Test renaming of keys and constraints.
$this->schema->dropTable('test_table');
$table_specification = [
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
index 7723d872cc12..e18336205398 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php
@@ -43,4 +43,16 @@ abstract class DriverSpecificSyntaxTestBase extends DriverSpecificDatabaseTestBa
$this->assertSame('[square]', $result->fetchField());
}
+ /**
+ * Tests string concatenation with separator, with field values.
+ */
+ public function testConcatWsFields(): void {
+ $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [
+ ':a1' => 'name',
+ ':a2' => 'age',
+ ':age' => 25,
+ ]);
+ $this->assertSame('name-John-age-25', $result->fetchField());
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
index c361c7af9596..b7e31e06a59a 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
@@ -38,7 +38,7 @@ use Drupal\Core\Database\TransactionOutOfOrderException;
* is active, and mysqli does not fail when rolling back and no transaction
* active.
*/
-class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
+abstract class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
/**
* Keeps track of the post-transaction callback action executed.
@@ -432,9 +432,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
/**
* Tests rollback after a DDL statement when no transactional DDL supported.
- *
- * @todo In drupal:12.0.0, rollBack will throw a
- * TransactionOutOfOrderException. Adjust the test accordingly.
*/
public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void {
if ($this->connection->supportsTransactionalDDL()) {
@@ -919,9 +916,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase {
* transaction including DDL statements is not possible, since a commit
* happened already. We cannot decide what should be the status of the
* callback, an exception is thrown.
- *
- * @todo In drupal:12.0.0, rollBack will throw a
- * TransactionOutOfOrderException. Adjust the test accordingly.
*/
public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void {
if ($this->connection->supportsTransactionalDDL()) {
diff --git a/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
index 6e28787f764c..3bfaa18889a9 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php
@@ -20,9 +20,9 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchObject(): void {
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$records = [];
$result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_OBJ]);
foreach ($result as $record) {
@@ -39,9 +39,9 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchArray(): void {
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$records = [];
$result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_ASSOC]);
foreach ($result as $record) {
@@ -59,9 +59,9 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchNum(): void {
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
- $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$records = [];
$result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_NUM]);
foreach ($result as $record) {
@@ -79,7 +79,7 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchAllColumn(): void {
- $this->expectDeprecation("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$query = $this->connection->select('test');
$query->addField('test', 'name');
$query->orderBy('name');
@@ -94,7 +94,7 @@ class FetchLegacyTest extends DatabaseTestBase {
*/
#[IgnoreDeprecations]
public function testQueryFetchAllAssoc(): void {
- $this->expectDeprecation("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$expected_result = [
"Singer" => [
"id" => "2",
diff --git a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
new file mode 100644
index 000000000000..f40a977f6f49
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php
@@ -0,0 +1,1278 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Database;
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\Transaction;
+use Drupal\Core\Database\Transaction\ClientConnectionTransactionState;
+use Drupal\Core\Database\Transaction\StackItem;
+use Drupal\Core\Database\Transaction\StackItemType;
+use Drupal\Core\Database\Transaction\TransactionManagerBase;
+use Drupal\Core\Database\TransactionNameNonUniqueException;
+use Drupal\Core\Database\TransactionOutOfOrderException;
+
+// cspell:ignore Tinky Winky Dipsy
+
+/**
+ * Tests the transactions, using the explicit ::commitOrRelease method.
+ *
+ * We test nesting by having two transaction layers, an outer and inner. The
+ * outer layer encapsulates the inner layer. Our transaction nesting abstraction
+ * should allow the outer layer function to call any function it wants,
+ * especially the inner layer that starts its own transaction, and be
+ * confident that, when the function it calls returns, its own transaction
+ * is still "alive."
+ *
+ * Call structure:
+ * transactionOuterLayer()
+ * Start transaction "A"
+ * transactionInnerLayer()
+ * Start transaction "B" (does nothing in database)
+ * [Maybe decide to roll back "B"]
+ * Do more stuff
+ * Should still be in transaction "A"
+ *
+ * These method can be overridden by non-core database driver if their
+ * transaction behavior is different from core. For example, both oci8 (Oracle)
+ * and mysqli (MySql) clients do not have a solution to check if a transaction
+ * is active, and mysqli does not fail when rolling back and no transaction
+ * active.
+ *
+ * @group Database
+ */
+class TransactionTest extends DatabaseTestBase {
+
+ /**
+ * Keeps track of the post-transaction callback action executed.
+ */
+ protected ?string $postTransactionCallbackAction = NULL;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ // Set the transaction manager to trigger warnings when appropriate.
+ $this->connection->transactionManager()->triggerWarningWhenUnpilingOnVoidTransaction = TRUE;
+ }
+
+ /**
+ * Create a root Drupal transaction.
+ */
+ protected function createRootTransaction(string $name = '', bool $insertRow = TRUE): Transaction {
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+
+ // Start root transaction. Corresponds to 'BEGIN TRANSACTION' on the
+ // database.
+ $transaction = $this->connection->startTransaction($name);
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a single row into the testing table.
+ if ($insertRow) {
+ $this->insertRow('David');
+ $this->assertRowPresent('David');
+ }
+
+ return $transaction;
+ }
+
+ /**
+ * Create a Drupal savepoint transaction after root.
+ */
+ protected function createFirstSavepointTransaction(string $name = '', bool $insertRow = TRUE): Transaction {
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_1'
+ // on the database. The name can be changed by the $name argument.
+ $savepoint = $this->connection->startTransaction($name);
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a single row into the testing table.
+ if ($insertRow) {
+ $this->insertRow('Roger');
+ $this->assertRowPresent('Roger');
+ }
+
+ return $savepoint;
+ }
+
+ /**
+ * Encapsulates a transaction's "inner layer" with an "outer layer".
+ *
+ * This "outer layer" transaction starts and then encapsulates the "inner
+ * layer" transaction. This nesting is used to evaluate whether the database
+ * transaction API properly supports nesting. By "properly supports," we mean
+ * the outer transaction continues to exist regardless of what functions are
+ * called and whether those functions start their own transactions.
+ *
+ * In contrast, a typical database would commit the outer transaction, start
+ * a new transaction for the inner layer, commit the inner layer transaction,
+ * and then be confused when the outer layer transaction tries to commit its
+ * transaction (which was already committed when the inner transaction
+ * started).
+ *
+ * @param string $suffix
+ * Suffix to add to field values to differentiate tests.
+ */
+ protected function transactionOuterLayer(string $suffix): void {
+ $txn = $this->connection->startTransaction();
+
+ // Insert a single row into the testing table.
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David' . $suffix,
+ 'age' => '24',
+ ])
+ ->execute();
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction before calling nested transaction.');
+
+ // We're already in a transaction, but we call ->transactionInnerLayer
+ // to nest another transaction inside the current one.
+ $this->transactionInnerLayer($suffix);
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction after calling nested transaction.');
+
+ $txn->commitOrRelease();
+ }
+
+ /**
+ * Creates an "inner layer" transaction.
+ *
+ * This "inner layer" transaction is either used alone or nested inside of the
+ * "outer layer" transaction.
+ *
+ * @param string $suffix
+ * Suffix to add to field values to differentiate tests.
+ */
+ protected function transactionInnerLayer(string $suffix): void {
+ $depth = $this->connection->transactionManager()->stackDepth();
+ // Start a transaction. If we're being called from ->transactionOuterLayer,
+ // then we're already in a transaction. Normally, that would make starting
+ // a transaction here dangerous, but the database API handles this problem
+ // for us by tracking the nesting and avoiding the danger.
+ $txn = $this->connection->startTransaction();
+
+ $depth2 = $this->connection->transactionManager()->stackDepth();
+ $this->assertSame($depth + 1, $depth2, 'Transaction depth has increased with new transaction.');
+
+ // Insert a single row into the testing table.
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'Daniel' . $suffix,
+ 'age' => '19',
+ ])
+ ->execute();
+
+ $this->assertTrue($this->connection->inTransaction(), 'In transaction inside nested transaction.');
+
+ $txn->commitOrRelease();
+ }
+
+ /**
+ * Tests root transaction rollback.
+ */
+ public function testRollbackRoot(): void {
+ $transaction = $this->createRootTransaction();
+
+ // Rollback. Since we are at the root, the transaction is closed.
+ // Corresponds to 'ROLLBACK' on the database.
+ $transaction->rollBack();
+ $this->assertRowAbsent('David');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests root transaction rollback after savepoint rollback.
+ */
+ public function testRollbackRootAfterSavepointRollback(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Try to rollback root. No savepoint is active, this should succeed.
+ $transaction->rollBack();
+ $this->assertRowAbsent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests root transaction rollback failure when savepoint is open.
+ */
+ public function testRollbackRootWithActiveSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Try to rollback root. Since a savepoint is active, this should fail.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\drupal_transaction\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1/");
+ $transaction->rollBack();
+ }
+
+ /**
+ * Tests savepoint transaction rollback.
+ */
+ public function testRollbackSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction commit after rollback.
+ */
+ public function testCommitAfterRollbackSameSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Try releasing savepoint. Should fail since it was released already.
+ try {
+ $savepoint->commitOrRelease();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting commit of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction rollback after commit.
+ */
+ public function testRollbackAfterCommitSameSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Release savepoint. Corresponds to 'RELEASE savepoint_1' on the database.
+ $savepoint->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Try rolling back savepoint. Should fail since it was released already.
+ try {
+ $savepoint->rollback();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Commit root.
+ $transaction->commitOrRelease();
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction duplicated rollback.
+ */
+ public function testRollbackTwiceSameSavepoint(): void {
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK
+ // TO savepoint_1' plus 'RELEASE savepoint_1' on the database.
+ $savepoint->rollBack();
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+
+ // Rollback savepoint again. Should fail since it was released already.
+ try {
+ $savepoint->rollBack();
+ $this->fail('Expected TransactionOutOfOrderException was not thrown');
+ }
+ catch (\Exception $e) {
+ $this->assertInstanceOf(TransactionOutOfOrderException::class, $e);
+ $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage());
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowAbsent('Roger');
+ $this->assertRowPresent('Syd');
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Tests savepoint transaction rollback failure when later savepoints exist.
+ */
+ public function testRollbackSavepointWithLaterSavepoint(): void {
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $transaction = $this->createRootTransaction();
+ $savepoint1 = $this->createFirstSavepointTransaction();
+
+ // Starts another savepoint transaction. Corresponds to 'SAVEPOINT
+ // savepoint_2' on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+
+ // Insert a row.
+ $this->insertRow('Syd');
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertRowPresent('Syd');
+
+ // Try to rollback to savepoint 1. Out of order.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1 > .*\\\\savepoint_2/");
+ $savepoint1->rollBack();
+ }
+
+ /**
+ * Tests commit does not fail when committing after DDL.
+ *
+ * In core, SQLite and PostgreSql databases support transactional DDL, MySql
+ * does not.
+ */
+ public function testCommitAfterDdl(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint = $this->createFirstSavepointTransaction();
+
+ $this->executeDDLStatement();
+
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+ }
+ else {
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $savepoint->commitOrRelease();
+ $this->assertTrue($this->connection->inTransaction());
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $savepoint->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('David');
+ $this->assertRowPresent('Roger');
+ $this->assertFalse($this->connection->inTransaction());
+ }
+
+ /**
+ * Tests a committed transaction.
+ *
+ * The behavior of this test should be identical for connections that support
+ * transactions and those that do not.
+ */
+ public function testCommittedTransaction(): void {
+ // Create two nested transactions. The changes should be committed.
+ $this->transactionOuterLayer('A');
+
+ // Because we committed, both of the inserted rows should be present.
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DavidA'])->fetchField();
+ $this->assertSame('24', $saved_age, 'Can retrieve DavidA row after commit.');
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DanielA'])->fetchField();
+ $this->assertSame('19', $saved_age, 'Can retrieve DanielA row after commit.');
+ }
+
+ /**
+ * Tests the compatibility of transactions with DDL statements.
+ */
+ public function testTransactionWithDdlStatement(): void {
+ // First, test that a commit works normally, even with DDL statements.
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // Even in different order.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->executeDDLStatement();
+ $this->insertRow('row');
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // Even with stacking.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction2->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction2->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->commitOrRelease();
+
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, this is out or order since $transaction3 started a
+ // new root.
+ }
+ }
+ $this->assertRowPresent('row');
+
+ // A transaction after a DDL statement should still work the same.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction2->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction2->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->rollBack();
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, this is out or order since $transaction3 started a
+ // new root.
+ }
+ }
+ $this->assertRowAbsent('row');
+
+ // The behavior of a rollback depends on the type of database server.
+ if ($this->connection->supportsTransactionalDDL()) {
+ // For database servers that support transactional DDL, a rollback
+ // of a transaction including DDL statements should be possible.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ $transaction->rollBack();
+ $this->assertRowAbsent('row');
+
+ // Including with stacking.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->executeDDLStatement();
+ $transaction2->commitOrRelease();
+ $transaction3 = $this->connection->startTransaction();
+ $this->insertRow('row');
+ $transaction3->commitOrRelease();
+ $this->assertRowPresent('row');
+ $transaction->rollBack();
+ $this->assertRowAbsent('row');
+ }
+ }
+
+ /**
+ * Tests rollback after a DDL statement when no transactional DDL supported.
+ */
+ public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void {
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database that do not support transactional DDL.');
+ }
+
+ // For database servers that do not support transactional DDL,
+ // the DDL statement should commit the transaction stack.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $reflectionMethod = new \ReflectionMethod(get_class($this->connection->transactionManager()), 'getConnectionTransactionState');
+ $this->assertSame(1, $this->connection->transactionManager()->stackDepth());
+ $this->assertEquals(ClientConnectionTransactionState::Active, $reflectionMethod->invoke($this->connection->transactionManager()));
+ $this->insertRow('row');
+ $this->executeDDLStatement();
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ $this->assertEquals(ClientConnectionTransactionState::Voided, $reflectionMethod->invoke($this->connection->transactionManager()));
+
+ // Try to rollback the root transaction. Since the DDL already committed
+ // it, it should fail.
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->rollBack();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+
+ try {
+ $transaction->commitOrRelease();
+ $this->fail('TransactionOutOfOrderException was expected, but did not throw.');
+ }
+ catch (TransactionOutOfOrderException) {
+ // Just continue, the attempted rollback made the overall state to
+ // ClientConnectionTransactionState::RollbackFailed.
+ }
+
+ $manager = $this->connection->transactionManager();
+ $this->assertSame(0, $manager->stackDepth());
+ $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState');
+ $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager));
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Inserts a single row into the testing table.
+ */
+ protected function insertRow(string $name): void {
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => $name,
+ ])
+ ->execute();
+ }
+
+ /**
+ * Executes a DDL statement.
+ */
+ protected function executeDDLStatement(): void {
+ static $count = 0;
+ $table = [
+ 'fields' => [
+ 'id' => [
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ],
+ ],
+ 'primary key' => ['id'],
+ ];
+ $this->connection->schema()->createTable('database_test_' . ++$count, $table);
+ }
+
+ /**
+ * Starts over for a new test.
+ */
+ protected function cleanUp(): void {
+ $this->connection->truncate('test')
+ ->execute();
+ $this->postTransactionCallbackAction = NULL;
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ }
+
+ /**
+ * Asserts that a given row is present in the test table.
+ *
+ * @param string $name
+ * The name of the row.
+ * @param string $message
+ * The message to log for the assertion.
+ *
+ * @internal
+ */
+ public function assertRowPresent(string $name, ?string $message = NULL): void {
+ $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
+ $this->assertTrue($present, $message ?? "Row '{$name}' should be present, but it actually does not exist.");
+ }
+
+ /**
+ * Asserts that a given row is absent from the test table.
+ *
+ * @param string $name
+ * The name of the row.
+ * @param string $message
+ * The message to log for the assertion.
+ *
+ * @internal
+ */
+ public function assertRowAbsent(string $name, ?string $message = NULL): void {
+ $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField();
+ $this->assertFalse($present, $message ?? "Row '{$name}' should be absent, but it actually exists.");
+ }
+
+ /**
+ * Tests transaction stacking, commit, and rollback.
+ */
+ public function testTransactionStacking(): void {
+ // Standard case: pop the inner transaction before the outer transaction.
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('outer');
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->insertRow('inner');
+ // Pop the inner transaction.
+ $transaction2->commitOrRelease();
+ $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the inner transaction');
+ // Pop the outer transaction.
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the outer transaction');
+ $this->assertRowPresent('outer');
+ $this->assertRowPresent('inner');
+
+ // Rollback the inner transaction.
+ $this->cleanUp();
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->insertRow('outer');
+ $transaction2 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->insertRow('inner');
+ // Now rollback the inner transaction.
+ $transaction2->rollBack();
+ $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the outer transaction');
+ // Pop the outer transaction, it should commit.
+ $this->insertRow('outer-after-inner-rollback');
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the inner transaction');
+ $this->assertRowPresent('outer');
+ $this->assertRowAbsent('inner');
+ $this->assertRowPresent('outer-after-inner-rollback');
+ }
+
+ /**
+ * Tests that transactions can continue to be used if a query fails.
+ */
+ public function testQueryFailureInTransaction(): void {
+ $transaction = $this->createRootTransaction('test_transaction', FALSE);
+ $this->connection->schema()->dropTable('test');
+
+ // Test a failed query using the query() method.
+ try {
+ $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField();
+ $this->fail('Using the query method should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed select query.
+ try {
+ $this->connection->select('test')
+ ->fields('test', ['name'])
+ ->execute();
+
+ $this->fail('Select query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed insert query.
+ try {
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David',
+ 'age' => '24',
+ ])
+ ->execute();
+
+ $this->fail('Insert query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed update query.
+ try {
+ $this->connection->update('test')
+ ->fields(['name' => 'Tiffany'])
+ ->condition('id', 1)
+ ->execute();
+
+ $this->fail('Update query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed delete query.
+ try {
+ $this->connection->delete('test')
+ ->condition('id', 1)
+ ->execute();
+
+ $this->fail('Delete query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed merge query.
+ try {
+ $this->connection->merge('test')
+ ->key('job', 'Presenter')
+ ->fields([
+ 'age' => '31',
+ 'name' => 'Tiffany',
+ ])
+ ->execute();
+
+ $this->fail('Merge query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Test a failed upsert query.
+ try {
+ $this->connection->upsert('test')
+ ->key('job')
+ ->fields(['job', 'age', 'name'])
+ ->values([
+ 'job' => 'Presenter',
+ 'age' => 31,
+ 'name' => 'Tiffany',
+ ])
+ ->execute();
+
+ $this->fail('Upsert query should have failed.');
+ }
+ catch (\Exception) {
+ // Just continue testing.
+ }
+
+ // Create the missing schema and insert a row.
+ $this->installSchema('database_test', ['test']);
+ $this->connection->insert('test')
+ ->fields([
+ 'name' => 'David',
+ 'age' => '24',
+ ])
+ ->execute();
+
+ // Commit the transaction.
+ if ($this->connection->supportsTransactionalDDL()) {
+ $transaction->commitOrRelease();
+ }
+ else {
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->commitOrRelease();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+ }
+
+ $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField();
+ $this->assertEquals('24', $saved_age);
+ }
+
+ /**
+ * Tests releasing a savepoint before last is safe.
+ */
+ public function testReleaseIntermediateSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2'
+ // on the database.
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_3'
+ // on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint3 = $this->connection->startTransaction();
+ $this->assertSame(4, $this->connection->transactionManager()->stackDepth());
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_4'
+ // on the database.
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint4 = $this->connection->startTransaction();
+ $this->assertSame(5, $this->connection->transactionManager()->stackDepth());
+
+ $this->insertRow('row');
+
+ // Release savepoint transaction. Corresponds to 'RELEASE SAVEPOINT
+ // savepoint_2' on the database.
+ $savepoint2->commitOrRelease();
+ // Since we have committed an intermediate savepoint Transaction object,
+ // the savepoints created later have been dropped by the database already.
+ $this->assertSame(2, $this->connection->transactionManager()->stackDepth());
+ $this->assertRowPresent('row');
+
+ // Commit the remaining Transaction objects. The client transaction is
+ // eventually committed.
+ $savepoint1->commitOrRelease();
+ $transaction->commitOrRelease();
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Tests committing a transaction while savepoints are active.
+ */
+ public function testCommitWithActiveSavepoint(): void {
+ $transaction = $this->createRootTransaction();
+ // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+
+ // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2'
+ // on the database.
+ $savepoint2 = $this->connection->startTransaction();
+ $this->assertSame(3, $this->connection->transactionManager()->stackDepth());
+
+ $this->insertRow('row');
+
+ // Commit the root transaction.
+ $transaction->commitOrRelease();
+ // Since we have committed the outer (root) Transaction object, the inner
+ // (savepoint) ones have been dropped by the database already, and we are
+ // no longer in an active transaction state.
+ $this->assertSame(0, $this->connection->transactionManager()->stackDepth());
+ $this->assertFalse($this->connection->inTransaction());
+ $this->assertRowPresent('row');
+ // Trying to release the inner (savepoint) Transaction object, throws an
+ // exception since it was dropped by the database already, and removed from
+ // our transaction stack.
+ $this->expectException(TransactionOutOfOrderException::class);
+ $this->expectExceptionMessageMatches("/^Error attempting commit of .*\\\\savepoint_2\\. Active stack: .* empty/");
+ $savepoint2->commitOrRelease();
+ }
+
+ /**
+ * Tests for transaction names.
+ */
+ public function testTransactionName(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->assertSame('drupal_transaction', $transaction->name());
+
+ $savepoint1 = $this->createFirstSavepointTransaction('', FALSE);
+ $this->assertSame('savepoint_1', $savepoint1->name());
+
+ $this->expectException(TransactionNameNonUniqueException::class);
+ $this->expectExceptionMessage("savepoint_1 is already in use.");
+ $this->connection->startTransaction('savepoint_1');
+ }
+
+ /**
+ * Tests for arbitrary transaction names.
+ */
+ public function testArbitraryTransactionNames(): void {
+ $transaction = $this->createRootTransaction('TinkyWinky', FALSE);
+ // Despite setting a name, the root transaction is always named
+ // 'drupal_transaction'.
+ $this->assertSame('drupal_transaction', $transaction->name());
+
+ $savepoint1 = $this->createFirstSavepointTransaction('Dipsy', FALSE);
+ $this->assertSame('Dipsy', $savepoint1->name());
+
+ $this->expectException(TransactionNameNonUniqueException::class);
+ $this->expectExceptionMessage("Dipsy is already in use.");
+ $this->connection->startTransaction('Dipsy');
+ }
+
+ /**
+ * Tests that adding a post-transaction callback fails with no transaction.
+ */
+ public function testRootTransactionEndCallbackAddedWithoutTransaction(): void {
+ $this->expectException(\LogicException::class);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ }
+
+ /**
+ * Tests post-transaction callback executes after transaction commit.
+ */
+ public function testRootTransactionEndCallbackCalledOnCommit(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a commit is not sufficient by itself.
+ $transaction->commitOrRelease();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowPresent('row');
+ $this->assertRowAbsent('rtcCommit');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcCommit'
+ // row.
+ $this->assertSame('rtcCommit', $this->postTransactionCallbackAction);
+ $this->assertRowPresent('row');
+ $this->assertRowPresent('rtcCommit');
+ }
+
+ /**
+ * Tests post-transaction callback executes after transaction rollback.
+ */
+ public function testRootTransactionEndCallbackCalledAfterRollbackAndDestruction(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a rollback is not sufficient by itself.
+ $transaction->rollBack();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowAbsent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $this->assertRowAbsent('row');
+ }
+
+ /**
+ * Tests post-transaction callback executes after a DDL statement.
+ */
+ public function testRootTransactionEndCallbackCalledAfterDdlAndDestruction(): void {
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcCommit'
+ // row.
+ $this->assertSame('rtcCommit', $this->postTransactionCallbackAction);
+ $this->assertRowPresent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * Tests post-transaction rollback executes after a DDL statement.
+ *
+ * For database servers that support transactional DDL, a rollback of a
+ * transaction including DDL statements is possible.
+ */
+ public function testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransactionalDdlDatabase(): void {
+ if (!$this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database supporting transactional DDL.');
+ }
+
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing the rollback is not sufficient by itself.
+ $transaction->rollBack();
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowAbsent('row');
+
+ // Destruct the transaction.
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $this->assertRowAbsent('row');
+ }
+
+ /**
+ * Tests post-transaction rollback failure after a DDL statement.
+ *
+ * For database servers that support transactional DDL, a rollback of a
+ * transaction including DDL statements is not possible, since a commit
+ * happened already. We cannot decide what should be the status of the
+ * callback, an exception is thrown.
+ */
+ public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void {
+ if ($this->connection->supportsTransactionalDDL()) {
+ $this->markTestSkipped('This test only works for database that do not support transactional DDL.');
+ }
+
+ $transaction = $this->createRootTransaction('', FALSE);
+ $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']);
+ $this->insertRow('row');
+ $this->assertNull($this->postTransactionCallbackAction);
+
+ // Callbacks are processed only when destructing the transaction.
+ // Executing a DDL statement is not sufficient itself.
+ // We cannot use truncate here, since it has protective code to fall back
+ // to a transactional delete when in transaction. We drop an unrelated
+ // table instead.
+ $this->connection->schema()->dropTable('test_people');
+ $this->assertNull($this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowAbsent('rtcRollback');
+ $this->assertRowPresent('row');
+
+ set_error_handler(static function (int $errno, string $errstr): bool {
+ throw new \ErrorException($errstr);
+ });
+ try {
+ $transaction->rollBack();
+ }
+ catch (\ErrorException $e) {
+ $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage());
+ }
+ finally {
+ restore_error_handler();
+ }
+
+ unset($transaction);
+
+ // The post-transaction callback should now have inserted a 'rtcRollback'
+ // row.
+ $this->assertSame('rtcRollback', $this->postTransactionCallbackAction);
+ $this->assertRowAbsent('rtcCommit');
+ $this->assertRowPresent('rtcRollback');
+ $manager = $this->connection->transactionManager();
+ $this->assertSame(0, $manager->stackDepth());
+ $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState');
+ $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager));
+ $this->assertRowPresent('row');
+ }
+
+ /**
+ * A post-transaction callback for testing purposes.
+ */
+ public function rootTransactionCallback(bool $success): void {
+ $this->postTransactionCallbackAction = $success ? 'rtcCommit' : 'rtcRollback';
+ $this->insertRow($this->postTransactionCallbackAction);
+ }
+
+ /**
+ * Tests TransactionManager failure.
+ */
+ public function testTransactionManagerFailureOnPendingStackItems(): void {
+ $connectionInfo = Database::getConnectionInfo();
+ Database::addConnectionInfo('default', 'test_fail', $connectionInfo['default']);
+ $testConnection = Database::getConnection('test_fail');
+
+ // Add a fake item to the stack.
+ $manager = $testConnection->transactionManager();
+ $reflectionMethod = new \ReflectionMethod($manager, 'addStackItem');
+ $reflectionMethod->invoke($manager, 'bar', new StackItem('qux', StackItemType::Root));
+ // Ensure transaction state can be determined during object destruction.
+ // This is necessary for the test to pass when xdebug.mode has the 'develop'
+ // option enabled.
+ $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'connectionTransactionState');
+ $reflectionProperty->setValue($manager, ClientConnectionTransactionState::Active);
+
+ // Ensure that __destruct() results in an assertion error. Note that this
+ // will normally be called by PHP during the object's destruction but Drupal
+ // will commit all transactions when a database is closed thereby making
+ // this impossible to test unless it is called directly.
+ try {
+ $manager->__destruct();
+ $this->fail("Expected AssertionError error not thrown");
+ }
+ catch (\AssertionError $e) {
+ $this->assertStringStartsWith('Transaction $stack was not empty. Active stack: bar\\qux', $e->getMessage());
+ }
+
+ // Clean up.
+ $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'stack');
+ $reflectionProperty->setValue($manager, []);
+ unset($testConnection);
+ Database::closeConnection('test_fail');
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php b/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php
new file mode 100644
index 000000000000..e1542a30c381
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php
@@ -0,0 +1,109 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Datetime;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+
+/**
+ * Tests DrupalDateTime functionality.
+ *
+ * @group Datetime
+ */
+class DrupalDateTimeTest extends KernelTestBase {
+
+ use UserCreationTrait;
+
+ /**
+ * Set up required modules.
+ *
+ * @var string[]
+ */
+ protected static $modules = [
+ 'system',
+ 'user',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->installConfig(['system']);
+ $this->installEntitySchema('user');
+ }
+
+ /**
+ * Tests that DrupalDateTime can detect the right timezone to use.
+ *
+ * Test with a variety of less commonly used timezone names to
+ * help ensure that the system timezone will be different than the
+ * stated timezones.
+ */
+ public function testDateTimezone(): void {
+ $date_string = '2007-01-31 21:00:00';
+
+ // Make sure no site timezone has been set.
+ $this->config('system.date')
+ ->set('timezone.user.configurable', 0)
+ ->set('timezone.default', NULL)
+ ->save();
+
+ // Detect the system timezone.
+ $system_timezone = date_default_timezone_get();
+
+ // Create a date object with an unspecified timezone, which should
+ // end up using the system timezone.
+ $date = new DrupalDateTime($date_string);
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame($system_timezone, $timezone, 'DrupalDateTime uses the system timezone when there is no site timezone.');
+
+ // Create a date object with a specified timezone.
+ $date = new DrupalDateTime($date_string, 'America/Yellowknife');
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame('America/Yellowknife', $timezone, 'DrupalDateTime uses the specified timezone if provided.');
+
+ // Set a site timezone.
+ $this->config('system.date')->set('timezone.default', 'Europe/Warsaw')->save();
+
+ // Create a date object with an unspecified timezone, which should
+ // end up using the site timezone.
+ $date = new DrupalDateTime($date_string);
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame('Europe/Warsaw', $timezone, 'DrupalDateTime uses the site timezone if provided.');
+
+ // Create user.
+ $this->config('system.date')->set('timezone.user.configurable', 1)->save();
+ $this->setUpCurrentUser([
+ 'timezone' => 'Asia/Manila',
+ ]);
+
+ // Create a date object with an unspecified timezone, which should
+ // end up using the user timezone.
+ $date = new DrupalDateTime($date_string);
+ $timezone = $date->getTimezone()->getName();
+ $this->assertSame('Asia/Manila', $timezone, 'DrupalDateTime uses the user timezone, if configurable timezones are used and it is set.');
+ }
+
+ /**
+ * Tests the ability to override the time zone in the format method.
+ */
+ public function testTimezoneFormat(): void {
+ // Create a date in UTC
+ $date = DrupalDateTime::createFromTimestamp(87654321, 'UTC');
+
+ // Verify that the date format method displays the default time zone.
+ $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date has default UTC time zone and correct date/time.');
+
+ // Verify that the format method can override the time zone.
+ $this->assertEquals('1972/10/11 08:25:21 America/New_York', $date->format('Y/m/d H:i:s e', ['timezone' => 'America/New_York']), 'Date displayed overridden time zone and correct date/time');
+
+ // Verify that the date format method still displays the default time zone
+ // for the date object.
+ $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date still has default UTC time zone and correct date/time');
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php
index 3be9b023b2f7..54d72e9fbff5 100644
--- a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php
+++ b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php
@@ -7,7 +7,6 @@ namespace Drupal\KernelTests\Core\DrupalKernel;
use Composer\Autoload\ClassLoader;
use Drupal\Core\DrupalKernel;
use Drupal\Core\DrupalKernelInterface;
-use Drupal\Core\Utility\Error;
use Drupal\KernelTests\KernelTestBase;
use org\bovigo\vfs\vfsStream;
use Prophecy\Argument;
@@ -27,8 +26,7 @@ class DrupalKernelTest extends KernelTestBase {
* {@inheritdoc}
*/
protected function tearDown(): void {
- $currentErrorHandler = Error::currentErrorHandler();
- if (is_string($currentErrorHandler) && $currentErrorHandler === '_drupal_error_handler') {
+ if (get_error_handler() === '_drupal_error_handler') {
restore_error_handler();
}
parent::tearDown();
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php
new file mode 100644
index 000000000000..419d8bff4d43
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\entity_test\Entity\EntityTestBundle;
+use Drupal\entity_test\Entity\EntityTestNoBundleWithLabel;
+use Drupal\entity_test\Entity\EntityTestWithBundle;
+
+/**
+ * Tests the getBundleEntity() method.
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase
+ *
+ * @group Entity
+ */
+class EntityBundleEntityTest extends EntityKernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['entity_test'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->installEntitySchema('entity_test');
+ $this->installEntitySchema('entity_test_with_bundle');
+ $this->installEntitySchema('entity_test_no_bundle_with_label');
+ }
+
+ /**
+ * Tests an entity type with config entities for bundles.
+ *
+ * @covers ::getBundleEntity
+ */
+ public function testWithConfigBundleEntity(): void {
+ $bundleEntity = EntityTestBundle::create([
+ 'id' => 'bundle_alpha',
+ 'label' => 'Alpha',
+ ]);
+ $bundleEntity->save();
+
+ $entity = EntityTestWithBundle::create([
+ 'type' => 'bundle_alpha',
+ 'name' => 'foo',
+ ]);
+ $entity->save();
+ $this->assertEquals($bundleEntity->id(), $entity->getBundleEntity()->id());
+ }
+
+ /**
+ * Tests an entity type without config entities for bundles.
+ *
+ * EntityTest doesn't have bundles, but does have the bundle entity key.
+ *
+ * @covers ::getBundleEntity
+ */
+ public function testWithoutBundleEntity(): void {
+ $entity = EntityTest::create([
+ 'name' => 'foo',
+ ]);
+ $entity->save();
+ $this->assertNull($entity->getBundleEntity());
+ }
+
+ /**
+ * Tests an entity type without the bundle entity key.
+ *
+ * @covers ::getBundleEntity
+ */
+ public function testWithBundleKeyEntity(): void {
+ $entity = EntityTestNoBundleWithLabel::create([
+ 'name' => 'foo',
+ ]);
+ $entity->save();
+ $this->assertNull($entity->getBundleEntity());
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
new file mode 100644
index 000000000000..a914752e9f17
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
@@ -0,0 +1,1036 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\IntegrityConstraintViolationException;
+use Drupal\Core\Entity\ContentEntityType;
+use Drupal\Core\Entity\EntityStorageException;
+use Drupal\Core\Entity\EntityTypeEvents;
+use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Field\FieldException;
+use Drupal\Core\Field\FieldStorageDefinitionEvents;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\entity_test\EntityTestHelper;
+use Drupal\entity_test\FieldStorageDefinition;
+use Drupal\entity_test_update\Entity\EntityTestUpdate;
+use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
+
+/**
+ * Tests EntityDefinitionUpdateManager functionality.
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\EntityDefinitionUpdateManager
+ *
+ * @group Entity
+ * @group #slow
+ */
+class EntityDefinitionUpdateMultipleTypesTest extends EntityKernelTestBase {
+
+ use EntityDefinitionTestTrait;
+
+ /**
+ * The entity definition update manager.
+ *
+ * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
+ */
+ protected $entityDefinitionUpdateManager;
+
+ /**
+ * The entity field manager.
+ *
+ * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+ */
+ protected $entityFieldManager;
+
+ /**
+ * The database connection.
+ *
+ * @var \Drupal\Core\Database\Connection
+ */
+ protected $database;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['entity_test_update', 'language'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
+ $this->entityFieldManager = $this->container->get('entity_field.manager');
+ $this->database = $this->container->get('database');
+
+ // Install every entity type's schema that wasn't installed in the parent
+ // method.
+ foreach (array_diff_key($this->entityTypeManager->getDefinitions(), array_flip(['user', 'entity_test'])) as $entity_type_id => $entity_type) {
+ $this->installEntitySchema($entity_type_id);
+ }
+ }
+
+ /**
+ * Tests when no definition update is needed.
+ */
+ public function testNoUpdates(): void {
+ // Ensure that the definition update manager reports no updates.
+ $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.');
+ $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports an empty change summary.');
+ $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeList(), 'EntityDefinitionUpdateManager reports an empty change list.');
+ }
+
+ /**
+ * Tests updating entity schema when there are no existing entities.
+ */
+ public function testEntityTypeUpdateWithoutData(): void {
+ // The 'entity_test_update' entity type starts out non-revisionable, so
+ // ensure the revision table hasn't been created during setUp().
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.');
+
+ // Update it to be revisionable and ensure the definition update manager
+ // reports that an update is needed.
+ $this->updateEntityTypeToRevisionable();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
+ $expected = [
+ 'entity_test_update' => [
+ "The $entity_type entity type needs to be updated.",
+ // The revision key is now defined, so the revision field needs to be
+ // created.
+ 'The Revision ID field needs to be installed.',
+ 'The Default revision field needs to be installed.',
+ ],
+ ];
+ $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+ // Run the update and ensure the revision table is created.
+ $this->updateEntityTypeToRevisionable(TRUE);
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.');
+ }
+
+ /**
+ * Tests updating entity schema when there are entity storage changes.
+ */
+ public function testEntityTypeUpdateWithEntityStorageChange(): void {
+ // Update the entity type to be revisionable and try to apply the update.
+ // It's expected to throw an exception.
+ $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE);
+ try {
+ $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
+ $this->fail('EntityStorageException thrown when trying to apply an update that requires shared table schema changes.');
+ }
+ catch (EntityStorageException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Tests creating a fieldable entity type that doesn't exist in code anymore.
+ *
+ * @covers ::installFieldableEntityType
+ */
+ public function testInstallFieldableEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+ $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Install the entity type and check that its tables have been created.
+ $this->entityDefinitionUpdateManager->installFieldableEntityType($entity_type, $field_storage_definitions);
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'The base table of the entity type has been created.');
+ }
+
+ /**
+ * Tests updating an entity type that doesn't exist in code anymore.
+ *
+ * @covers ::updateEntityType
+ */
+ public function testUpdateEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Add an entity index, update the entity type and check that the index has
+ // been created.
+ $this->addEntityIndex();
+ $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
+
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
+ }
+
+ /**
+ * Tests updating a fieldable entity type that doesn't exist in code anymore.
+ *
+ * @covers ::updateFieldableEntityType
+ */
+ public function testUpdateFieldableEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+ $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Rename the base table, update the fieldable entity type and check that
+ // the table has been renamed.
+ $entity_type->set('base_table', 'entity_test_update_new');
+ $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions);
+
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update_new'), 'The base table has been renamed.');
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'The old base table does not exist anymore.');
+ }
+
+ /**
+ * Tests uninstalling an entity type that doesn't exist in code anymore.
+ *
+ * @covers ::uninstallEntityType
+ */
+ public function testUninstallEntityTypeWithoutInCodeDefinition(): void {
+ $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Now uninstall it and check that the tables have been removed.
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
+ $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
+ }
+
+ /**
+ * Tests uninstalling a revisionable entity type that doesn't exist in code.
+ *
+ * @covers ::uninstallEntityType
+ */
+ public function testUninstallRevisionableEntityTypeWithoutInCodeDefinition(): void {
+ $this->updateEntityTypeToRevisionable(TRUE);
+ $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
+
+ // Remove the entity type definition. This is the same thing as removing the
+ // code that defines it.
+ $this->deleteEntityType();
+
+ // Now uninstall it and check that the tables have been removed.
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
+ $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
+ }
+
+ /**
+ * Tests creating, updating, and deleting a base field if no entities exist.
+ */
+ public function testBaseFieldCreateUpdateDeleteWithoutData(): void {
+ // Add a base field, ensure the update manager reports it, and the update
+ // creates its schema.
+ $this->addBaseField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
+
+ // Add an index on the base field, ensure the update manager reports it,
+ // and the update creates it.
+ $this->addBaseFieldIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.');
+
+ // Remove the above index, ensure the update manager reports it, and the
+ // update deletes it.
+ $this->removeBaseFieldIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.');
+
+ // Update the type of the base field from 'string' to 'text', ensure the
+ // update manager reports it, and the update adjusts the schema
+ // accordingly.
+ $this->modifyBaseField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.');
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.');
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.');
+
+ // Remove the base field, ensure the update manager reports it, and the
+ // update deletes the schema.
+ $this->removeBaseField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new base field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.');
+ $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.');
+ }
+
+ /**
+ * Tests creating, updating, and deleting a base field with no label set.
+ *
+ * See testBaseFieldCreateUpdateDeleteWithoutData() for more details
+ */
+ public function testBaseFieldWithoutLabelCreateUpdateDelete(): void {
+ // Add a base field, ensure the update manager reports it with the
+ // field id.
+ $this->addBaseField('string', 'entity_test_update', FALSE, FALSE);
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The new_base_field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+
+ // Add an index on the base field, ensure the update manager reports it with
+ // the field id.
+ $this->addBaseFieldIndex();
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The new_base_field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+
+ // Remove the base field, ensure the update manager reports it with the
+ // field id.
+ $this->removeBaseField();
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The new_base_field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
+ }
+
+ /**
+ * Tests creating, updating, and deleting a bundle field if no entities exist.
+ */
+ public function testBundleFieldCreateUpdateDeleteWithoutData(): void {
+ // Add a bundle field, ensure the update manager reports it, and the update
+ // creates its schema.
+ $this->addBundleField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new bundle field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
+
+ // Update the type of the base field from 'string' to 'text', ensure the
+ // update manager reports it, and the update adjusts the schema
+ // accordingly.
+ $this->modifyBundleField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new bundle field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.');
+
+ // Remove the bundle field, ensure the update manager reports it, and the
+ // update deletes the schema.
+ $this->removeBundleField();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
+ $this->assertCount(1, $changes['entity_test_update']);
+ $this->assertEquals('The A new bundle field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
+ $this->applyEntityUpdates();
+ $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
+ }
+
+ /**
+ * Tests creating and deleting a base field if entities exist.
+ *
+ * This tests deletion when there are existing entities, but non-existent data
+ * for the field being deleted.
+ *
+ * @see testBaseFieldDeleteWithExistingData()
+ */
+ public function testBaseFieldCreateDeleteWithExistingEntities(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+
+ // Add a base field and run the update. Ensure the base field's column is
+ // created and the prior saved entity data is still there.
+ $this->addBaseField();
+ $this->applyEntityUpdates();
+ $schema_handler = $this->database->schema();
+ $this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
+
+ // Remove the base field and run the update. Ensure the base field's column
+ // is deleted and the prior saved entity data is still there.
+ $this->removeBaseField();
+ $this->applyEntityUpdates();
+ $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
+
+ // Add a base field with a required property and run the update. Ensure
+ // 'not null' is not applied and thus no exception is thrown.
+ $this->addBaseField('shape_required');
+ $this->applyEntityUpdates();
+ $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
+ $this->assertTrue($assert, 'Columns created in shared table for new_base_field.');
+
+ // Recreate the field after emptying the base table and check that its
+ // columns are not 'not null'.
+ // @todo Revisit this test when allowing for required storage field
+ // definitions. See https://www.drupal.org/node/2390495.
+ $entity->delete();
+ $this->removeBaseField();
+ $this->applyEntityUpdates();
+ $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__shape'), 'Shape column should be removed from the shared table for new_base_field.');
+ $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__color'), 'Color column should be removed from the shared table for new_base_field.');
+ $this->addBaseField('shape_required');
+ $this->applyEntityUpdates();
+ $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
+ $this->assertTrue($assert, 'Columns created again in shared table for new_base_field.');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+ }
+
+ /**
+ * Tests creating and deleting a bundle field if entities exist.
+ *
+ * This tests deletion when there are existing entities, but non-existent data
+ * for the field being deleted.
+ *
+ * @see testBundleFieldDeleteWithExistingData()
+ */
+ public function testBundleFieldCreateDeleteWithExistingEntities(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+
+ // Add a bundle field and run the update. Ensure the bundle field's table
+ // is created and the prior saved entity data is still there.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+ $schema_handler = $this->database->schema();
+ $this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
+
+ // Remove the base field and run the update. Ensure the bundle field's
+ // table is deleted and the prior saved entity data is still there.
+ $this->removeBundleField();
+ $this->applyEntityUpdates();
+ $this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
+ $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
+
+ // Test that required columns are created as 'not null'.
+ $this->addBundleField('shape_required');
+ $this->applyEntityUpdates();
+ $message = 'The new_bundle_field_shape column is not nullable.';
+ $values = [
+ 'bundle' => $entity->bundle(),
+ 'deleted' => 0,
+ 'entity_id' => $entity->id(),
+ 'revision_id' => $entity->id(),
+ 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ 'delta' => 0,
+ 'new_bundle_field_color' => $this->randomString(),
+ ];
+ try {
+ // Try to insert a record without providing a value for the 'not null'
+ // column. This should fail.
+ $this->database->insert('entity_test_update__new_bundle_field')
+ ->fields($values)
+ ->execute();
+ $this->fail($message);
+ }
+ catch (IntegrityConstraintViolationException) {
+ // Now provide a value for the 'not null' column. This is expected to
+ // succeed.
+ $values['new_bundle_field_shape'] = $this->randomString();
+ $this->database->insert('entity_test_update__new_bundle_field')
+ ->fields($values)
+ ->execute();
+ }
+ }
+
+ /**
+ * Tests deleting a bundle field when it has existing data.
+ */
+ public function testBundleFieldDeleteWithExistingData(): void {
+ /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $schema_handler = $this->database->schema();
+
+ // Add the bundle field and run the update.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+
+ /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+ $table_mapping = $storage->getTableMapping();
+ $storage_definition = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field'];
+
+ // Check that the bundle field has a dedicated table.
+ $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
+ $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.');
+
+ // Save an entity with the bundle field populated.
+ EntityTestHelper::createBundle('custom');
+ $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']);
+ $entity->save();
+
+ // Remove the bundle field and apply updates.
+ $this->removeBundleField();
+ $this->applyEntityUpdates();
+
+ // Check that the table of the bundle field has been renamed to use a
+ // 'deleted' table name.
+ $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.');
+
+ $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
+ $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.');
+
+ // Check that the deleted field's data is preserved in the dedicated
+ // 'deleted' table.
+ $result = $this->database->select($dedicated_deleted_table_name, 't')
+ ->fields('t')
+ ->execute()
+ ->fetchAll();
+ $this->assertCount(1, $result);
+
+ $expected = [
+ 'bundle' => $entity->bundle(),
+ 'deleted' => '1',
+ 'entity_id' => $entity->id(),
+ 'revision_id' => $entity->id(),
+ 'langcode' => $entity->language()->getId(),
+ 'delta' => '0',
+ 'new_bundle_field_value' => $entity->new_bundle_field->value,
+ ];
+ // Use assertEquals and not assertSame here to prevent that a different
+ // sequence of the columns in the table will affect the check.
+ $this->assertEquals($expected, (array) $result[0]);
+
+ // Check that the field definition is marked for purging.
+ $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
+ $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_field_definitions, 'The bundle field is marked for purging.');
+
+ // Check that the field storage definition is marked for purging.
+ $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+ $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The bundle field storage is marked for purging.');
+
+ // Purge field data, and check that the storage definition has been
+ // completely removed once the data is purged.
+ field_purge_batch(10);
+ $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
+ $this->assertEmpty($deleted_field_definitions, 'The bundle field has been deleted.');
+ $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
+ $this->assertEmpty($deleted_storage_definitions, 'The bundle field storage has been deleted.');
+ $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle field has been removed.');
+ }
+
+ /**
+ * Tests updating a base field when it has existing data.
+ */
+ public function testBaseFieldUpdateWithExistingData(): void {
+ // Add the base field and run the update.
+ $this->addBaseField();
+ $this->applyEntityUpdates();
+
+ // Save an entity with the base field populated.
+ $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save();
+
+ // Change the field's field type and apply updates. It's expected to
+ // throw an exception.
+ $this->modifyBaseField();
+ try {
+ $this->applyEntityUpdates();
+ $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+ }
+ catch (FieldStorageDefinitionUpdateForbiddenException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Tests updating a bundle field when it has existing data.
+ */
+ public function testBundleFieldUpdateWithExistingData(): void {
+ // Add the bundle field and run the update.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+
+ // Save an entity with the bundle field populated.
+ EntityTestHelper::createBundle('custom');
+ $this->entityTypeManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save();
+
+ // Change the field's field type and apply updates. It's expected to
+ // throw an exception.
+ $this->modifyBundleField();
+ try {
+ $this->applyEntityUpdates();
+ $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
+ }
+ catch (FieldStorageDefinitionUpdateForbiddenException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Tests updating a bundle field when the entity type schema has changed.
+ */
+ public function testBundleFieldUpdateWithEntityTypeSchemaUpdate(): void {
+ // Add the bundle field and run the update.
+ $this->addBundleField();
+ $this->applyEntityUpdates();
+
+ // Update the entity type schema to revisionable but don't run the updates
+ // yet.
+ $this->updateEntityTypeToRevisionable();
+
+ // Perform a no-op update on the bundle field, which should work because
+ // both the storage and the storage schema are using the last installed
+ // entity type definition.
+ $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
+ $entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('new_bundle_field', 'entity_test_update'));
+ }
+
+ /**
+ * Tests creating and deleting a multi-field index when there are no existing entities.
+ */
+ public function testEntityIndexCreateDeleteWithoutData(): void {
+ // Add an entity index and ensure the update manager reports that as an
+ // update to the entity type.
+ $this->addEntityIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
+ $expected = [
+ 'entity_test_update' => [
+ "The $entity_type entity type needs to be updated.",
+ ],
+ ];
+ $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+ // Run the update and ensure the new index is created.
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
+
+ // Remove the index and ensure the update manager reports that as an
+ // update to the entity type.
+ $this->removeEntityIndex();
+ $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
+ $expected = [
+ 'entity_test_update' => [
+ "The $entity_type entity type needs to be updated.",
+ ],
+ ];
+ $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
+
+ // Run the update and ensure the index is deleted.
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+ $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
+
+ // Test that composite indexes are handled correctly when dropping and
+ // re-creating one of their columns.
+ $this->addEntityIndex();
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+
+ $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update');
+ $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
+ $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
+ $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.');
+ }
+
+ /**
+ * Tests creating a multi-field index when there are existing entities.
+ */
+ public function testEntityIndexCreateWithData(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(['name' => $name]);
+ $entity->save();
+
+ // Add an entity index, run the update. Ensure that the index is created
+ // despite having data.
+ $this->addEntityIndex();
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index added.');
+ }
+
+ /**
+ * Tests applying single updates.
+ */
+ public function testSingleActionCalls(): void {
+ $db_schema = $this->database->schema();
+
+ // Ensure that a non-existing entity type cannot be installed.
+ $message = 'A non-existing entity type cannot be installed';
+ try {
+ $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo']));
+ $this->fail($message);
+ }
+ catch (PluginNotFoundException) {
+ // Expected exception; just continue testing.
+ }
+
+ // Ensure that a field cannot be installed on non-existing entity type.
+ $message = 'A field cannot be installed on a non-existing entity type';
+ try {
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new revisionable base field')
+ ->setRevisionable(TRUE);
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition);
+ $this->fail($message);
+ }
+ catch (PluginNotFoundException) {
+ // Expected exception; just continue testing.
+ }
+
+ // Ensure that installing an existing entity type is a no-op.
+ $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
+ $this->entityDefinitionUpdateManager->installEntityType($entity_type);
+ $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op');
+
+ // Create a new base field.
+ $this->addRevisionableBaseField();
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new revisionable base field')
+ ->setRevisionable(TRUE);
+ $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
+
+ // Ensure that installing an existing field is a no-op.
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing field is a no-op');
+
+ // Update an existing field schema.
+ $this->modifyBaseField();
+ $storage_definition = BaseFieldDefinition::create('text')
+ ->setName('new_base_field')
+ ->setTargetEntityTypeId('entity_test_update')
+ ->setLabel('A new revisionable base field')
+ ->setRevisionable(TRUE);
+ $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
+ $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists.");
+ $this->assertTrue(
+ $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
+ "New schema for 'new_base_field' has been created."
+ );
+
+ // Drop an existing field schema.
+ $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
+ $this->assertFalse(
+ $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
+ "The schema for 'new_base_field' has been dropped."
+ );
+
+ // Make the entity type revisionable.
+ $this->updateEntityTypeToRevisionable();
+ $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update.");
+
+ $this->updateEntityTypeToRevisionable(TRUE);
+ $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created.");
+ }
+
+ /**
+ * Ensures that a new field and index on a shared table are created.
+ *
+ * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema
+ */
+ public function testCreateFieldAndIndexOnSharedTable(): void {
+ $this->addBaseField();
+ $this->addBaseFieldIndex();
+ $this->applyEntityUpdates();
+ $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table.");
+ // Check index size in for MySQL.
+ if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) {
+ $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject();
+ $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
+ }
+ }
+
+ /**
+ * Ensures that a new entity level index is created when data exists.
+ *
+ * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate
+ */
+ public function testCreateIndexUsingEntityStorageSchemaWithData(): void {
+ // Save an entity.
+ $name = $this->randomString();
+ $storage = $this->entityTypeManager->getStorage('entity_test_update');
+ $entity = $storage->create(['name' => $name]);
+ $entity->save();
+
+ // Create an index.
+ $indexes = [
+ 'entity_test_update__type_index' => ['type'],
+ ];
+ $this->state->set('entity_test_update.additional_entity_indexes', $indexes);
+ $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
+ $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
+
+ $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table.");
+ // Check index size in for MySQL.
+ if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) {
+ $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject();
+ $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
+ }
+ }
+
+ /**
+ * Tests updating a base field when it has existing data.
+ */
+ public function testBaseFieldEntityKeyUpdateWithExistingData(): void {
+ // Add the base field and run the update.
+ $this->addBaseField();
+ $this->applyEntityUpdates();
+
+ // Save an entity with the base field populated.
+ $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save();
+
+ // Save an entity with the base field not populated.
+ /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
+ $entity = $this->entityTypeManager->getStorage('entity_test_update')->create();
+ $entity->save();
+
+ // Promote the base field to an entity key. This will trigger the addition
+ // of a NOT NULL constraint.
+ $this->makeBaseFieldEntityKey();
+
+ // Field storage CRUD operations use the last installed entity type
+ // definition so we need to update it before doing any other field storage
+ // updates.
+ $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type'));
+
+ // Try to apply the update and verify they fail since we have a NULL value.
+ $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.';
+ try {
+ $this->applyEntityUpdates();
+ $this->fail($message);
+ }
+ catch (EntityStorageException) {
+ // Expected exception; just continue testing.
+ }
+
+ // Check that the update is correctly applied when no NULL data is left.
+ $entity->set('new_base_field', $this->randomString());
+ $entity->save();
+ $this->applyEntityUpdates();
+
+ // Check that the update actually applied a NOT NULL constraint.
+ $entity->set('new_base_field', NULL);
+ $message = 'The NOT NULL constraint was correctly applied.';
+ try {
+ $entity->save();
+ $this->fail($message);
+ }
+ catch (EntityStorageException) {
+ // Expected exception; just continue testing.
+ }
+ }
+
+ /**
+ * Check that field schema is correctly handled with long-named fields.
+ */
+ public function testLongNameFieldIndexes(): void {
+ $this->addLongNameBaseField();
+ $entity_type_id = 'entity_test_update';
+ $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
+ $definitions = EntityTestUpdate::baseFieldDefinitions($entity_type);
+ $name = 'new_long_named_entity_reference_base_field';
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition($name, $entity_type_id, 'entity_test', $definitions[$name]);
+ $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.');
+ }
+
+ /**
+ * Tests adding a base field with initial values.
+ */
+ public function testInitialValue(): void {
+ $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
+ $db_schema = $this->database->schema();
+
+ // Create two entities before adding the base field.
+ $storage->create()->save();
+ $storage->create()->save();
+
+ // Add a base field with an initial value.
+ $this->addBaseField();
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new base field')
+ ->setInitialValue('test value');
+
+ $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
+
+ // Check that the initial values have been applied.
+ $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
+ $entities = $storage->loadMultiple();
+ $this->assertEquals('test value', $entities[1]->get('new_base_field')->value);
+ $this->assertEquals('test value', $entities[2]->get('new_base_field')->value);
+ }
+
+ /**
+ * Tests entity type and field storage definition events.
+ */
+ public function testDefinitionEvents(): void {
+ /** @var \Drupal\entity_test\EntityTestDefinitionSubscriber $event_subscriber */
+ $event_subscriber = $this->container->get('entity_test.definition.subscriber');
+ $event_subscriber->enableEventTracking();
+ $event_subscriber->enableLiveDefinitionUpdates();
+
+ // Test field storage definition events.
+ $storage_definition = FieldStorageDefinition::create('string')
+ ->setName('field_storage_test')
+ ->setLabel(new TranslatableMarkup('Field storage test'))
+ ->setTargetEntityTypeId('entity_test_rev');
+
+ $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.');
+ \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition);
+ $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::CREATE), 'Last installed field storage definition was created before the event was fired.');
+
+ // Check that the newly added field can be retrieved from the live field
+ // storage definitions.
+ $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
+ $this->assertArrayHasKey('field_storage_test', $field_storage_definitions);
+
+ $updated_storage_definition = clone $storage_definition;
+ $updated_storage_definition->setLabel(new TranslatableMarkup('Updated field storage test'));
+ $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.');
+ \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($updated_storage_definition, $storage_definition);
+ $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::UPDATE), 'Last installed field storage definition was updated before the event was fired.');
+
+ // Check that the updated field can be retrieved from the live field storage
+ // definitions.
+ $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
+ $this->assertEquals(new TranslatableMarkup('Updated field storage test'), $field_storage_definitions['field_storage_test']->getLabel());
+
+ $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.');
+ \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition);
+ $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::DELETE), 'Last installed field storage definition was deleted before the event was fired.');
+
+ // Check that the deleted field can no longer be retrieved from the live
+ // field storage definitions.
+ $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
+ $this->assertArrayNotHasKey('field_storage_test', $field_storage_definitions);
+
+ // Test entity type events.
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
+
+ $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.');
+ \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
+ $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::CREATE), 'Last installed entity type definition was created before the event was fired.');
+
+ $updated_entity_type = clone $entity_type;
+ $updated_entity_type->set('label', new TranslatableMarkup('Updated entity test rev'));
+ $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.');
+ \Drupal::service('entity_type.listener')->onEntityTypeUpdate($updated_entity_type, $entity_type);
+ $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::UPDATE), 'Last installed entity type definition was updated before the event was fired.');
+
+ // Check that the updated definition can be retrieved from the live entity
+ // type definitions.
+ $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
+ $this->assertEquals(new TranslatableMarkup('Updated entity test rev'), $entity_type->getLabel());
+
+ $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.');
+ \Drupal::service('entity_type.listener')->onEntityTypeDelete($entity_type);
+ $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.');
+ $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::DELETE), 'Last installed entity type definition was deleted before the event was fired.');
+
+ // Check that the deleted entity type can no longer be retrieved from the
+ // live entity type definitions.
+ $this->assertNull($this->entityTypeManager->getDefinition('entity_test_rev', FALSE));
+ }
+
+ /**
+ * Tests the error handling when using initial values from another field.
+ */
+ public function testInitialValueFromFieldErrorHandling(): void {
+ // Check that setting invalid values for 'initial value from field' doesn't
+ // work.
+ try {
+ $this->addBaseField();
+ $storage_definition = BaseFieldDefinition::create('string')
+ ->setLabel('A new base field')
+ ->setInitialValueFromField('field_that_does_not_exist');
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->fail('Using a non-existent field as initial value does not work.');
+ }
+ catch (FieldException $e) {
+ $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage());
+ }
+
+ try {
+ $this->addBaseField();
+ $storage_definition = BaseFieldDefinition::create('integer')
+ ->setLabel('A new base field')
+ ->setInitialValueFromField('name');
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
+ $this->fail('Using a field of a different type as initial value does not work.');
+ }
+ catch (FieldException $e) {
+ $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage());
+ }
+
+ try {
+ // Add a base field that will not be stored in the shared tables.
+ $initial_field = BaseFieldDefinition::create('string')
+ ->setName('initial_field')
+ ->setLabel('An initial field')
+ ->setCardinality(2);
+ $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]);
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field);
+
+ // Now add the base field which will try to use the previously added field
+ // as the source of its initial values.
+ $new_base_field = BaseFieldDefinition::create('string')
+ ->setName('new_base_field')
+ ->setLabel('A new base field')
+ ->setInitialValueFromField('initial_field');
+ $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]);
+ $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field);
+ $this->fail('Using a field that is not stored in the shared tables as initial value does not work.');
+ }
+ catch (FieldException $e) {
+ $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage());
+ }
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
index 6437e594ee8b..d184892a976a 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php
@@ -4,21 +4,6 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Entity;
-use Drupal\Component\Plugin\Exception\PluginNotFoundException;
-use Drupal\Core\Database\Database;
-use Drupal\Core\Database\IntegrityConstraintViolationException;
-use Drupal\Core\Entity\ContentEntityType;
-use Drupal\Core\Entity\EntityStorageException;
-use Drupal\Core\Entity\EntityTypeEvents;
-use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
-use Drupal\Core\Field\BaseFieldDefinition;
-use Drupal\Core\Field\FieldException;
-use Drupal\Core\Field\FieldStorageDefinitionEvents;
-use Drupal\Core\Language\LanguageInterface;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
-use Drupal\entity_test\EntityTestHelper;
-use Drupal\entity_test\FieldStorageDefinition;
-use Drupal\entity_test_update\Entity\EntityTestUpdate;
use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
/**
@@ -27,7 +12,6 @@ use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
* @coversDefaultClass \Drupal\Core\Entity\EntityDefinitionUpdateManager
*
* @group Entity
- * @group #slow
*/
class EntityDefinitionUpdateTest extends EntityKernelTestBase {
@@ -67,12 +51,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase {
$this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager');
$this->entityFieldManager = $this->container->get('entity_field.manager');
$this->database = $this->container->get('database');
-
- // Install every entity type's schema that wasn't installed in the parent
- // method.
- foreach (array_diff_key($this->entityTypeManager->getDefinitions(), array_flip(['user', 'entity_test'])) as $entity_type_id => $entity_type) {
- $this->installEntitySchema($entity_type_id);
- }
}
/**
@@ -96,61 +74,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase {
}
/**
- * Tests when no definition update is needed.
- */
- public function testNoUpdates(): void {
- // Ensure that the definition update manager reports no updates.
- $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.');
- $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports an empty change summary.');
- $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeList(), 'EntityDefinitionUpdateManager reports an empty change list.');
- }
-
- /**
- * Tests updating entity schema when there are no existing entities.
- */
- public function testEntityTypeUpdateWithoutData(): void {
- // The 'entity_test_update' entity type starts out non-revisionable, so
- // ensure the revision table hasn't been created during setUp().
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.');
-
- // Update it to be revisionable and ensure the definition update manager
- // reports that an update is needed.
- $this->updateEntityTypeToRevisionable();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
- $expected = [
- 'entity_test_update' => [
- "The $entity_type entity type needs to be updated.",
- // The revision key is now defined, so the revision field needs to be
- // created.
- 'The Revision ID field needs to be installed.',
- 'The Default revision field needs to be installed.',
- ],
- ];
- $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
-
- // Run the update and ensure the revision table is created.
- $this->updateEntityTypeToRevisionable(TRUE);
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.');
- }
-
- /**
- * Tests updating entity schema when there are entity storage changes.
- */
- public function testEntityTypeUpdateWithEntityStorageChange(): void {
- // Update the entity type to be revisionable and try to apply the update.
- // It's expected to throw an exception.
- $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE);
- try {
- $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
- $this->fail('EntityStorageException thrown when trying to apply an update that requires shared table schema changes.');
- }
- catch (EntityStorageException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
* Tests installing an additional base field while installing an entity type.
*
* @covers ::installFieldableEntityType
@@ -173,909 +96,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase {
}
/**
- * Tests creating a fieldable entity type that doesn't exist in code anymore.
- *
- * @covers ::installFieldableEntityType
- */
- public function testInstallFieldableEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
- $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Install the entity type and check that its tables have been created.
- $this->entityDefinitionUpdateManager->installFieldableEntityType($entity_type, $field_storage_definitions);
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'The base table of the entity type has been created.');
- }
-
- /**
- * Tests updating an entity type that doesn't exist in code anymore.
- *
- * @covers ::updateEntityType
- */
- public function testUpdateEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Add an entity index, update the entity type and check that the index has
- // been created.
- $this->addEntityIndex();
- $this->entityDefinitionUpdateManager->updateEntityType($entity_type);
-
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
- }
-
- /**
- * Tests updating a fieldable entity type that doesn't exist in code anymore.
- *
- * @covers ::updateFieldableEntityType
- */
- public function testUpdateFieldableEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
- $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Rename the base table, update the fieldable entity type and check that
- // the table has been renamed.
- $entity_type->set('base_table', 'entity_test_update_new');
- $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions);
-
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update_new'), 'The base table has been renamed.');
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'The old base table does not exist anymore.');
- }
-
- /**
- * Tests uninstalling an entity type that doesn't exist in code anymore.
- *
- * @covers ::uninstallEntityType
- */
- public function testUninstallEntityTypeWithoutInCodeDefinition(): void {
- $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Now uninstall it and check that the tables have been removed.
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
- $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
- }
-
- /**
- * Tests uninstalling a revisionable entity type that doesn't exist in code.
- *
- * @covers ::uninstallEntityType
- */
- public function testUninstallRevisionableEntityTypeWithoutInCodeDefinition(): void {
- $this->updateEntityTypeToRevisionable(TRUE);
- $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
-
- // Remove the entity type definition. This is the same thing as removing the
- // code that defines it.
- $this->deleteEntityType();
-
- // Now uninstall it and check that the tables have been removed.
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.');
- $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type);
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.');
- }
-
- /**
- * Tests creating, updating, and deleting a base field if no entities exist.
- */
- public function testBaseFieldCreateUpdateDeleteWithoutData(): void {
- // Add a base field, ensure the update manager reports it, and the update
- // creates its schema.
- $this->addBaseField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
-
- // Add an index on the base field, ensure the update manager reports it,
- // and the update creates it.
- $this->addBaseFieldIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.');
-
- // Remove the above index, ensure the update manager reports it, and the
- // update deletes it.
- $this->removeBaseFieldIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.');
-
- // Update the type of the base field from 'string' to 'text', ensure the
- // update manager reports it, and the update adjusts the schema
- // accordingly.
- $this->modifyBaseField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.');
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.');
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.');
-
- // Remove the base field, ensure the update manager reports it, and the
- // update deletes the schema.
- $this->removeBaseField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new base field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.');
- $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.');
- }
-
- /**
- * Tests creating, updating, and deleting a base field with no label set.
- *
- * See testBaseFieldCreateUpdateDeleteWithoutData() for more details
- */
- public function testBaseFieldWithoutLabelCreateUpdateDelete(): void {
- // Add a base field, ensure the update manager reports it with the
- // field id.
- $this->addBaseField('string', 'entity_test_update', FALSE, FALSE);
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The new_base_field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
-
- // Add an index on the base field, ensure the update manager reports it with
- // the field id.
- $this->addBaseFieldIndex();
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The new_base_field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
-
- // Remove the base field, ensure the update manager reports it with the
- // field id.
- $this->removeBaseField();
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The new_base_field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
- }
-
- /**
- * Tests creating, updating, and deleting a bundle field if no entities exist.
- */
- public function testBundleFieldCreateUpdateDeleteWithoutData(): void {
- // Add a bundle field, ensure the update manager reports it, and the update
- // creates its schema.
- $this->addBundleField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new bundle field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
-
- // Update the type of the base field from 'string' to 'text', ensure the
- // update manager reports it, and the update adjusts the schema
- // accordingly.
- $this->modifyBundleField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new bundle field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.');
-
- // Remove the bundle field, ensure the update manager reports it, and the
- // update deletes the schema.
- $this->removeBundleField();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $changes = $this->entityDefinitionUpdateManager->getChangeSummary();
- $this->assertCount(1, $changes['entity_test_update']);
- $this->assertEquals('The A new bundle field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0]));
- $this->applyEntityUpdates();
- $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
- }
-
- /**
- * Tests creating and deleting a base field if entities exist.
- *
- * This tests deletion when there are existing entities, but non-existent data
- * for the field being deleted.
- *
- * @see testBaseFieldDeleteWithExistingData()
- */
- public function testBaseFieldCreateDeleteWithExistingEntities(): void {
- // Save an entity.
- $name = $this->randomString();
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
-
- // Add a base field and run the update. Ensure the base field's column is
- // created and the prior saved entity data is still there.
- $this->addBaseField();
- $this->applyEntityUpdates();
- $schema_handler = $this->database->schema();
- $this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
-
- // Remove the base field and run the update. Ensure the base field's column
- // is deleted and the prior saved entity data is still there.
- $this->removeBaseField();
- $this->applyEntityUpdates();
- $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
-
- // Add a base field with a required property and run the update. Ensure
- // 'not null' is not applied and thus no exception is thrown.
- $this->addBaseField('shape_required');
- $this->applyEntityUpdates();
- $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
- $this->assertTrue($assert, 'Columns created in shared table for new_base_field.');
-
- // Recreate the field after emptying the base table and check that its
- // columns are not 'not null'.
- // @todo Revisit this test when allowing for required storage field
- // definitions. See https://www.drupal.org/node/2390495.
- $entity->delete();
- $this->removeBaseField();
- $this->applyEntityUpdates();
- $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__shape'), 'Shape column should be removed from the shared table for new_base_field.');
- $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__color'), 'Color column should be removed from the shared table for new_base_field.');
- $this->addBaseField('shape_required');
- $this->applyEntityUpdates();
- $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color');
- $this->assertTrue($assert, 'Columns created again in shared table for new_base_field.');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
- }
-
- /**
- * Tests creating and deleting a bundle field if entities exist.
- *
- * This tests deletion when there are existing entities, but non-existent data
- * for the field being deleted.
- *
- * @see testBundleFieldDeleteWithExistingData()
- */
- public function testBundleFieldCreateDeleteWithExistingEntities(): void {
- // Save an entity.
- $name = $this->randomString();
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
-
- // Add a bundle field and run the update. Ensure the bundle field's table
- // is created and the prior saved entity data is still there.
- $this->addBundleField();
- $this->applyEntityUpdates();
- $schema_handler = $this->database->schema();
- $this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.');
-
- // Remove the base field and run the update. Ensure the bundle field's
- // table is deleted and the prior saved entity data is still there.
- $this->removeBundleField();
- $this->applyEntityUpdates();
- $this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.');
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id());
- $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.');
-
- // Test that required columns are created as 'not null'.
- $this->addBundleField('shape_required');
- $this->applyEntityUpdates();
- $message = 'The new_bundle_field_shape column is not nullable.';
- $values = [
- 'bundle' => $entity->bundle(),
- 'deleted' => 0,
- 'entity_id' => $entity->id(),
- 'revision_id' => $entity->id(),
- 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
- 'delta' => 0,
- 'new_bundle_field_color' => $this->randomString(),
- ];
- try {
- // Try to insert a record without providing a value for the 'not null'
- // column. This should fail.
- $this->database->insert('entity_test_update__new_bundle_field')
- ->fields($values)
- ->execute();
- $this->fail($message);
- }
- catch (IntegrityConstraintViolationException) {
- // Now provide a value for the 'not null' column. This is expected to
- // succeed.
- $values['new_bundle_field_shape'] = $this->randomString();
- $this->database->insert('entity_test_update__new_bundle_field')
- ->fields($values)
- ->execute();
- }
- }
-
- /**
- * Tests deleting a bundle field when it has existing data.
- */
- public function testBundleFieldDeleteWithExistingData(): void {
- /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $schema_handler = $this->database->schema();
-
- // Add the bundle field and run the update.
- $this->addBundleField();
- $this->applyEntityUpdates();
-
- /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
- $table_mapping = $storage->getTableMapping();
- $storage_definition = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field'];
-
- // Check that the bundle field has a dedicated table.
- $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition);
- $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.');
-
- // Save an entity with the bundle field populated.
- EntityTestHelper::createBundle('custom');
- $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']);
- $entity->save();
-
- // Remove the bundle field and apply updates.
- $this->removeBundleField();
- $this->applyEntityUpdates();
-
- // Check that the table of the bundle field has been renamed to use a
- // 'deleted' table name.
- $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.');
-
- $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE);
- $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.');
-
- // Check that the deleted field's data is preserved in the dedicated
- // 'deleted' table.
- $result = $this->database->select($dedicated_deleted_table_name, 't')
- ->fields('t')
- ->execute()
- ->fetchAll();
- $this->assertCount(1, $result);
-
- $expected = [
- 'bundle' => $entity->bundle(),
- 'deleted' => '1',
- 'entity_id' => $entity->id(),
- 'revision_id' => $entity->id(),
- 'langcode' => $entity->language()->getId(),
- 'delta' => '0',
- 'new_bundle_field_value' => $entity->new_bundle_field->value,
- ];
- // Use assertEquals and not assertSame here to prevent that a different
- // sequence of the columns in the table will affect the check.
- $this->assertEquals($expected, (array) $result[0]);
-
- // Check that the field definition is marked for purging.
- $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
- $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_field_definitions, 'The bundle field is marked for purging.');
-
- // Check that the field storage definition is marked for purging.
- $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
- $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The bundle field storage is marked for purging.');
-
- // Purge field data, and check that the storage definition has been
- // completely removed once the data is purged.
- field_purge_batch(10);
- $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions();
- $this->assertEmpty($deleted_field_definitions, 'The bundle field has been deleted.');
- $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions();
- $this->assertEmpty($deleted_storage_definitions, 'The bundle field storage has been deleted.');
- $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle field has been removed.');
- }
-
- /**
- * Tests updating a base field when it has existing data.
- */
- public function testBaseFieldUpdateWithExistingData(): void {
- // Add the base field and run the update.
- $this->addBaseField();
- $this->applyEntityUpdates();
-
- // Save an entity with the base field populated.
- $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save();
-
- // Change the field's field type and apply updates. It's expected to
- // throw an exception.
- $this->modifyBaseField();
- try {
- $this->applyEntityUpdates();
- $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
- }
- catch (FieldStorageDefinitionUpdateForbiddenException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
- * Tests updating a bundle field when it has existing data.
- */
- public function testBundleFieldUpdateWithExistingData(): void {
- // Add the bundle field and run the update.
- $this->addBundleField();
- $this->applyEntityUpdates();
-
- // Save an entity with the bundle field populated.
- EntityTestHelper::createBundle('custom');
- $this->entityTypeManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save();
-
- // Change the field's field type and apply updates. It's expected to
- // throw an exception.
- $this->modifyBundleField();
- try {
- $this->applyEntityUpdates();
- $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.');
- }
- catch (FieldStorageDefinitionUpdateForbiddenException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
- * Tests updating a bundle field when the entity type schema has changed.
- */
- public function testBundleFieldUpdateWithEntityTypeSchemaUpdate(): void {
- // Add the bundle field and run the update.
- $this->addBundleField();
- $this->applyEntityUpdates();
-
- // Update the entity type schema to revisionable but don't run the updates
- // yet.
- $this->updateEntityTypeToRevisionable();
-
- // Perform a no-op update on the bundle field, which should work because
- // both the storage and the storage schema are using the last installed
- // entity type definition.
- $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
- $entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('new_bundle_field', 'entity_test_update'));
- }
-
- /**
- * Tests creating and deleting a multi-field index when there are no existing entities.
- */
- public function testEntityIndexCreateDeleteWithoutData(): void {
- // Add an entity index and ensure the update manager reports that as an
- // update to the entity type.
- $this->addEntityIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
- $expected = [
- 'entity_test_update' => [
- "The $entity_type entity type needs to be updated.",
- ],
- ];
- $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
-
- // Run the update and ensure the new index is created.
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
-
- // Remove the index and ensure the update manager reports that as an
- // update to the entity type.
- $this->removeEntityIndex();
- $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.');
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel();
- $expected = [
- 'entity_test_update' => [
- "The $entity_type entity type needs to be updated.",
- ],
- ];
- $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.');
-
- // Run the update and ensure the index is deleted.
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
- $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
-
- // Test that composite indexes are handled correctly when dropping and
- // re-creating one of their columns.
- $this->addEntityIndex();
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
-
- $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update');
- $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.');
- $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
- $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.');
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.');
- }
-
- /**
- * Tests creating a multi-field index when there are existing entities.
- */
- public function testEntityIndexCreateWithData(): void {
- // Save an entity.
- $name = $this->randomString();
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(['name' => $name]);
- $entity->save();
-
- // Add an entity index, run the update. Ensure that the index is created
- // despite having data.
- $this->addEntityIndex();
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index added.');
- }
-
- /**
- * Tests entity type and field storage definition events.
- */
- public function testDefinitionEvents(): void {
- /** @var \Drupal\entity_test\EntityTestDefinitionSubscriber $event_subscriber */
- $event_subscriber = $this->container->get('entity_test.definition.subscriber');
- $event_subscriber->enableEventTracking();
- $event_subscriber->enableLiveDefinitionUpdates();
-
- // Test field storage definition events.
- $storage_definition = FieldStorageDefinition::create('string')
- ->setName('field_storage_test')
- ->setLabel(new TranslatableMarkup('Field storage test'))
- ->setTargetEntityTypeId('entity_test_rev');
-
- $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.');
- \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition);
- $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::CREATE), 'Last installed field storage definition was created before the event was fired.');
-
- // Check that the newly added field can be retrieved from the live field
- // storage definitions.
- $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
- $this->assertArrayHasKey('field_storage_test', $field_storage_definitions);
-
- $updated_storage_definition = clone $storage_definition;
- $updated_storage_definition->setLabel(new TranslatableMarkup('Updated field storage test'));
- $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.');
- \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($updated_storage_definition, $storage_definition);
- $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::UPDATE), 'Last installed field storage definition was updated before the event was fired.');
-
- // Check that the updated field can be retrieved from the live field storage
- // definitions.
- $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
- $this->assertEquals(new TranslatableMarkup('Updated field storage test'), $field_storage_definitions['field_storage_test']->getLabel());
-
- $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.');
- \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition);
- $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::DELETE), 'Last installed field storage definition was deleted before the event was fired.');
-
- // Check that the deleted field can no longer be retrieved from the live
- // field storage definitions.
- $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev');
- $this->assertArrayNotHasKey('field_storage_test', $field_storage_definitions);
-
- // Test entity type events.
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
-
- $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.');
- \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type);
- $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::CREATE), 'Last installed entity type definition was created before the event was fired.');
-
- $updated_entity_type = clone $entity_type;
- $updated_entity_type->set('label', new TranslatableMarkup('Updated entity test rev'));
- $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($updated_entity_type, $entity_type);
- $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::UPDATE), 'Last installed entity type definition was updated before the event was fired.');
-
- // Check that the updated definition can be retrieved from the live entity
- // type definitions.
- $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev');
- $this->assertEquals(new TranslatableMarkup('Updated entity test rev'), $entity_type->getLabel());
-
- $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.');
- \Drupal::service('entity_type.listener')->onEntityTypeDelete($entity_type);
- $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.');
- $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::DELETE), 'Last installed entity type definition was deleted before the event was fired.');
-
- // Check that the deleted entity type can no longer be retrieved from the
- // live entity type definitions.
- $this->assertNull($this->entityTypeManager->getDefinition('entity_test_rev', FALSE));
- }
-
- /**
- * Tests applying single updates.
- */
- public function testSingleActionCalls(): void {
- $db_schema = $this->database->schema();
-
- // Ensure that a non-existing entity type cannot be installed.
- $message = 'A non-existing entity type cannot be installed';
- try {
- $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo']));
- $this->fail($message);
- }
- catch (PluginNotFoundException) {
- // Expected exception; just continue testing.
- }
-
- // Ensure that a field cannot be installed on non-existing entity type.
- $message = 'A field cannot be installed on a non-existing entity type';
- try {
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new revisionable base field')
- ->setRevisionable(TRUE);
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition);
- $this->fail($message);
- }
- catch (PluginNotFoundException) {
- // Expected exception; just continue testing.
- }
-
- // Ensure that installing an existing entity type is a no-op.
- $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update');
- $this->entityDefinitionUpdateManager->installEntityType($entity_type);
- $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op');
-
- // Create a new base field.
- $this->addRevisionableBaseField();
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new revisionable base field')
- ->setRevisionable(TRUE);
- $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
-
- // Ensure that installing an existing field is a no-op.
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing field is a no-op');
-
- // Update an existing field schema.
- $this->modifyBaseField();
- $storage_definition = BaseFieldDefinition::create('text')
- ->setName('new_base_field')
- ->setTargetEntityTypeId('entity_test_update')
- ->setLabel('A new revisionable base field')
- ->setRevisionable(TRUE);
- $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition);
- $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists.");
- $this->assertTrue(
- $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
- "New schema for 'new_base_field' has been created."
- );
-
- // Drop an existing field schema.
- $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition);
- $this->assertFalse(
- $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'),
- "The schema for 'new_base_field' has been dropped."
- );
-
- // Make the entity type revisionable.
- $this->updateEntityTypeToRevisionable();
- $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update.");
-
- $this->updateEntityTypeToRevisionable(TRUE);
- $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created.");
- }
-
- /**
- * Ensures that a new field and index on a shared table are created.
- *
- * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema
- */
- public function testCreateFieldAndIndexOnSharedTable(): void {
- $this->addBaseField();
- $this->addBaseFieldIndex();
- $this->applyEntityUpdates();
- $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table.");
- // Check index size in for MySQL.
- if (Database::getConnection()->driver() == 'mysql') {
- $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject();
- $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
- }
- }
-
- /**
- * Ensures that a new entity level index is created when data exists.
- *
- * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate
- */
- public function testCreateIndexUsingEntityStorageSchemaWithData(): void {
- // Save an entity.
- $name = $this->randomString();
- $storage = $this->entityTypeManager->getStorage('entity_test_update');
- $entity = $storage->create(['name' => $name]);
- $entity->save();
-
- // Create an index.
- $indexes = [
- 'entity_test_update__type_index' => ['type'],
- ];
- $this->state->set('entity_test_update.additional_entity_indexes', $indexes);
- $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update');
- $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update');
- \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original);
-
- $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table.");
- // Check index size in for MySQL.
- if (Database::getConnection()->driver() == 'mysql') {
- $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject();
- $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.');
- }
- }
-
- /**
- * Tests updating a base field when it has existing data.
- */
- public function testBaseFieldEntityKeyUpdateWithExistingData(): void {
- // Add the base field and run the update.
- $this->addBaseField();
- $this->applyEntityUpdates();
-
- // Save an entity with the base field populated.
- $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save();
-
- // Save an entity with the base field not populated.
- /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */
- $entity = $this->entityTypeManager->getStorage('entity_test_update')->create();
- $entity->save();
-
- // Promote the base field to an entity key. This will trigger the addition
- // of a NOT NULL constraint.
- $this->makeBaseFieldEntityKey();
-
- // Field storage CRUD operations use the last installed entity type
- // definition so we need to update it before doing any other field storage
- // updates.
- $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type'));
-
- // Try to apply the update and verify they fail since we have a NULL value.
- $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.';
- try {
- $this->applyEntityUpdates();
- $this->fail($message);
- }
- catch (EntityStorageException) {
- // Expected exception; just continue testing.
- }
-
- // Check that the update is correctly applied when no NULL data is left.
- $entity->set('new_base_field', $this->randomString());
- $entity->save();
- $this->applyEntityUpdates();
-
- // Check that the update actually applied a NOT NULL constraint.
- $entity->set('new_base_field', NULL);
- $message = 'The NOT NULL constraint was correctly applied.';
- try {
- $entity->save();
- $this->fail($message);
- }
- catch (EntityStorageException) {
- // Expected exception; just continue testing.
- }
- }
-
- /**
- * Check that field schema is correctly handled with long-named fields.
- */
- public function testLongNameFieldIndexes(): void {
- $this->addLongNameBaseField();
- $entity_type_id = 'entity_test_update';
- $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
- $definitions = EntityTestUpdate::baseFieldDefinitions($entity_type);
- $name = 'new_long_named_entity_reference_base_field';
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition($name, $entity_type_id, 'entity_test', $definitions[$name]);
- $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.');
- }
-
- /**
- * Tests adding a base field with initial values.
- */
- public function testInitialValue(): void {
- $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
- $db_schema = $this->database->schema();
-
- // Create two entities before adding the base field.
- $storage->create()->save();
- $storage->create()->save();
-
- // Add a base field with an initial value.
- $this->addBaseField();
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new base field')
- ->setInitialValue('test value');
-
- $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update.");
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table.");
-
- // Check that the initial values have been applied.
- $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update');
- $entities = $storage->loadMultiple();
- $this->assertEquals('test value', $entities[1]->get('new_base_field')->value);
- $this->assertEquals('test value', $entities[2]->get('new_base_field')->value);
- }
-
- /**
- * Tests the error handling when using initial values from another field.
- */
- public function testInitialValueFromFieldErrorHandling(): void {
- // Check that setting invalid values for 'initial value from field' doesn't
- // work.
- try {
- $this->addBaseField();
- $storage_definition = BaseFieldDefinition::create('string')
- ->setLabel('A new base field')
- ->setInitialValueFromField('field_that_does_not_exist');
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->fail('Using a non-existent field as initial value does not work.');
- }
- catch (FieldException $e) {
- $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage());
- }
-
- try {
- $this->addBaseField();
- $storage_definition = BaseFieldDefinition::create('integer')
- ->setLabel('A new base field')
- ->setInitialValueFromField('name');
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition);
- $this->fail('Using a field of a different type as initial value does not work.');
- }
- catch (FieldException $e) {
- $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage());
- }
-
- try {
- // Add a base field that will not be stored in the shared tables.
- $initial_field = BaseFieldDefinition::create('string')
- ->setName('initial_field')
- ->setLabel('An initial field')
- ->setCardinality(2);
- $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]);
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field);
-
- // Now add the base field which will try to use the previously added field
- // as the source of its initial values.
- $new_base_field = BaseFieldDefinition::create('string')
- ->setName('new_base_field')
- ->setLabel('A new base field')
- ->setInitialValueFromField('initial_field');
- $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]);
- $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field);
- $this->fail('Using a field that is not stored in the shared tables as initial value does not work.');
- }
- catch (FieldException $e) {
- $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage());
- }
- }
-
- /**
* @covers ::getEntityTypes
*/
public function testGetEntityTypes(): void {
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
index 568a3d3ca4f5..4aaff77dbaa2 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
@@ -39,7 +39,7 @@ class ExtensionNameConstraintTest extends KernelTestBase {
$data->setValue('invalid-name');
$violations = $data->validate();
$this->assertCount(1, $violations);
- $this->assertSame('This value is not valid.', (string) $violations->get(0)->getMessage());
+ $this->assertSame('This value is not a valid extension name.', (string) $violations->get(0)->getMessage());
}
}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php b/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php
new file mode 100644
index 000000000000..020ef21c2273
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Extension;
+
+use Drupal\KernelTests\KernelTestBase;
+
+include_once \DRUPAL_ROOT . '/core/includes/install.inc';
+
+/**
+ * Tests the legacy requirements severity deprecations.
+ *
+ * @coversDefaultClass \Drupal\Core\Extension\Requirement\RequirementSeverity
+ * @group extension
+ * @group legacy
+ */
+class LegacyRequirementSeverityTest extends KernelTestBase {
+
+ /**
+ * @covers \drupal_requirements_severity
+ * @dataProvider requirementProvider
+ */
+ public function testGetMaxSeverity(array $requirements, int $expectedSeverity): void {
+ $this->expectDeprecation(
+ 'drupal_requirements_severity() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use Drupal\Core\Extension\Requirement\RequirementSeverity::maxSeverityFromRequirements() instead. See https://www.drupal.org/node/3410939'
+ );
+ $this->expectDeprecation(
+ 'Calling Drupal\Core\Extension\Requirement\RequirementSeverity::maxSeverityFromRequirements() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939'
+ );
+ $severity = drupal_requirements_severity($requirements);
+ $this->assertEquals($expectedSeverity, $severity);
+ }
+
+ /**
+ * Data provider for requirement helper test.
+ *
+ * @return array
+ * Test data.
+ */
+ public static function requirementProvider(): array {
+ $info = [
+ 'title' => 'Foo',
+ 'severity' => \REQUIREMENT_INFO,
+ ];
+ $warning = [
+ 'title' => 'Baz',
+ 'severity' => \REQUIREMENT_WARNING,
+ ];
+ $error = [
+ 'title' => 'Wiz',
+ 'severity' => \REQUIREMENT_ERROR,
+ ];
+ $ok = [
+ 'title' => 'Bar',
+ 'severity' => \REQUIREMENT_OK,
+ ];
+
+ return [
+ 'error is most severe' => [
+ [
+ $info,
+ $error,
+ $ok,
+ ],
+ \REQUIREMENT_ERROR,
+ ],
+ 'ok is most severe' => [
+ [
+ $info,
+ $ok,
+ ],
+ \REQUIREMENT_OK,
+ ],
+ 'warning is most severe' => [
+ [
+ $warning,
+ $info,
+ $ok,
+ ],
+ \REQUIREMENT_WARNING,
+ ],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php
index 8ee28a968fd5..c90cb5e0aad7 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Extension;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
+use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\KernelTests\KernelTestBase;
/**
@@ -36,4 +37,78 @@ class ModuleHandlerTest extends KernelTestBase {
$this->assertNotNull(\Drupal::service('module_handler')->getName('module_test'));
}
+ /**
+ * Tests that resetImplementations() clears the hook memory cache.
+ *
+ * @covers ::resetImplementations
+ */
+ public function testResetImplementationsClearsHooks(): void {
+ $oldModuleHandler = \Drupal::moduleHandler();
+ $this->assertHasResetHookImplementations(FALSE, $oldModuleHandler);
+
+ // Installing a module does not trigger ->resetImplementations().
+ /** @var \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller */
+ $moduleInstaller = \Drupal::service('module_installer');
+ $moduleInstaller->install(['module_test']);
+ $this->assertHasResetHookImplementations(FALSE, $oldModuleHandler);
+ // Only the new ModuleHandler instance has the updated implementations.
+ $moduleHandler = \Drupal::moduleHandler();
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+ $backupModuleList = $moduleHandler->getModuleList();
+ $moduleListWithout = array_diff_key($backupModuleList, ['module_test' => TRUE]);
+ $this->assertArrayHasKey('module_test', $backupModuleList);
+
+ // Silently setting the property does not clear the hooks cache.
+ $moduleListProperty = (new \ReflectionProperty($moduleHandler, 'moduleList'));
+ $this->assertSame($backupModuleList, $moduleListProperty->getValue($moduleHandler));
+ $moduleListProperty->setValue($moduleHandler, $moduleListWithout);
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+
+ // Directly calling ->resetImplementations() clears the hook caches.
+ $moduleHandler->resetImplementations();
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ $moduleListProperty->setValue($moduleHandler, $backupModuleList);
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ $moduleHandler->resetImplementations();
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+
+ // Calling ->setModuleList() triggers ->resetImplementations().
+ $moduleHandler->setModuleList(['system']);
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ $moduleHandler->setModuleList($backupModuleList);
+ $this->assertHasResetHookImplementations(TRUE, $moduleHandler);
+
+ // Uninstalling a module triggers ->resetImplementations().
+ /** @var \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller */
+ $moduleInstaller = \Drupal::service('module_installer');
+ $moduleInstaller->uninstall(['module_test']);
+ $this->assertSame($moduleListWithout, $moduleHandler->getModuleList());
+ $this->assertHasResetHookImplementations(FALSE, $moduleHandler);
+ }
+
+ /**
+ * Asserts whether certain hook implementations exist.
+ *
+ * This is used to verify that all internal hook cache properties have been
+ * reset and updated.
+ *
+ * @param bool $exists
+ * TRUE if the implementations are expected to exist, FALSE if not.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+ * The module handler.
+ *
+ * @see \module_test_test_reset_implementations_hook()
+ * @see \module_test_test_reset_implementations_alter()
+ */
+ protected function assertHasResetHookImplementations(bool $exists, ModuleHandlerInterface $moduleHandler): void {
+ $this->assertSame($exists, $moduleHandler->hasImplementations('test_reset_implementations_hook'));
+ $this->assertSame($exists, $moduleHandler->hasImplementations('test_reset_implementations_alter'));
+ $expected_list = $exists ? ['module_test_test_reset_implementations_hook'] : [];
+ $this->assertSame($expected_list, $moduleHandler->invokeAll('test_reset_implementations_hook'));
+ $expected_alter_list = $exists ? ['module_test_test_reset_implementations_alter'] : [];
+ $alter_list = [];
+ $moduleHandler->alter('test_reset_implementations', $alter_list);
+ $this->assertSame($expected_alter_list, $alter_list);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php
index 0cb098bfccab..ca1fab258f3c 100644
--- a/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php
@@ -45,8 +45,14 @@ class FileSystemRequirementsTest extends KernelTestBase {
* An array of system requirements.
*/
protected function checkSystemRequirements() {
+ // This loadInclude() is to ensure that the install API is available.
+ // Since we're loading an include of type 'install', this will also
+ // include core/includes/install.inc for us, which is where
+ // drupal_verify_install_file() is currently defined.
+ // @todo Remove this once the function lives in a better place.
+ // @see https://www.drupal.org/project/drupal/issues/3526388
$this->container->get('module_handler')->loadInclude('system', 'install');
- return system_requirements('runtime');
+ return \Drupal::moduleHandler()->invoke('system', 'runtime_requirements');
}
}
diff --git a/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
index dfe537e4d1fa..7d1e641f485a 100644
--- a/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
+++ b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php
@@ -92,6 +92,15 @@ class HtaccessTest extends KernelTestBase {
}
/**
+ * @covers ::write
+ */
+ public function testHtaccessSaveDisabled(): void {
+ $this->setSetting('auto_create_htaccess', FALSE);
+ $this->assertTrue($this->htaccessWriter->write($this->public, FALSE));
+ $this->assertFileDoesNotExist($this->public . '/.htaccess');
+ }
+
+ /**
* Asserts expected file permissions for a given file.
*
* @param string $uri
diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php
index ffc5a85fdb0b..fdfa189b880b 100644
--- a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php
@@ -17,7 +17,9 @@ use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\NodeType;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\StyleInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Validator\Exception\ValidationFailedException;
/**
@@ -289,4 +291,32 @@ YAML
RecipeRunner::processRecipe($recipe);
}
+ /**
+ * Tests that the askHidden prompt forwards arguments correctly.
+ */
+ public function testAskHiddenPromptArgumentsForwarded(): void {
+ $input = $this->createMock(InputInterface::class);
+ $output = $this->createMock(OutputInterface::class);
+ $io = new SymfonyStyle($input, $output);
+
+ $recipe = $this->createRecipe(<<<YAML
+name: 'Prompt askHidden Test'
+input:
+ foo:
+ data_type: string
+ description: Foo
+ prompt:
+ method: askHidden
+ default:
+ source: value
+ value: bar
+YAML
+ );
+ $collector = new ConsoleInputCollector($input, $io);
+ // askHidden prompt should have an ArgumentCountError rather than a general
+ // error.
+ $this->expectException(\ArgumentCountError::class);
+ $recipe->input->collectAll($collector);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php
index 09931b10f3a3..16c0a7fe12bf 100644
--- a/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php
@@ -43,7 +43,8 @@ class DeprecatedElementTest extends KernelTestBase {
], $info_manager->getInfo('deprecated_extends_form'));
// Ensure the constructor is triggering a deprecation error.
- $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
+ $previous_error_handler = get_error_handler();
+ set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
// Convert deprecation error into a catchable exception.
if ($severity === E_USER_DEPRECATED) {
throw new \ErrorException($message, 0, $severity, $file, $line);
@@ -84,7 +85,8 @@ class DeprecatedElementTest extends KernelTestBase {
* Test use of static methods trigger deprecations.
*/
public function testDeprecatedStaticMethods(): void {
- $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
+ $previous_error_handler = get_error_handler();
+ set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
// Convert deprecation error into a catchable exception.
if ($severity === E_USER_DEPRECATED) {
throw new \ErrorException($message, 0, $severity, $file, $line);
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php
new file mode 100644
index 000000000000..93470153f326
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Render\Element;
+
+use Drupal\Core\Render\Element\StatusReport;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the status report element legacy methods.
+ *
+ * @group Render
+ * @group legacy
+ */
+class LegacyStatusReportTest extends KernelTestBase {
+
+ /**
+ * Tests the getSeverities() method deprecation.
+ */
+ public function testGetSeveritiesDeprecation(): void {
+ $this->expectDeprecation('Calling Drupal\Core\Render\Element\StatusReport::getSeverities() is deprecated in drupal:11.2.0 and is removed from in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3410939');
+ $severities = StatusReport::getSeverities();
+ $this->assertIsArray($severities);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php
index 7ad8afa75be6..c300cd4c019f 100644
--- a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php
@@ -23,7 +23,7 @@ class PluginAlterTest extends KernelTestBase {
$info_manager = $this->container->get('plugin.manager.element_info');
$this->assertArrayHasKey('weight', $info_manager->getDefinitions());
- // @see element_info_test_element_plugin_alter()
+ // @see ElementInfoTestHooks::elementPluginAlter().
$this->container->get('state')->set('hook_element_plugin_alter:remove_weight', TRUE);
// The definition will be cached.
$this->assertArrayHasKey('weight', $info_manager->getDefinitions());
@@ -33,4 +33,27 @@ class PluginAlterTest extends KernelTestBase {
$this->assertArrayNotHasKey('weight', $info_manager->getDefinitions());
}
+ /**
+ * Tests hook_element_plugin_alter().
+ */
+ public function testPluginClassSwap(): void {
+ $info_manager = $this->container->get('plugin.manager.element_info');
+ $test_details = [
+ '#type' => 'details',
+ '#title' => 'Title',
+ '#description' => 'Description',
+ '#open' => TRUE,
+ ];
+
+ // @see ElementInfoTestHooks::elementPluginAlter().
+ $expected = [
+ 'class' => 'Drupal\element_info_test\Render\Element\Details',
+ 'provider' => 'element_info_test',
+ 'id' => 'details',
+ ];
+ $this->assertEquals($expected, $info_manager->getDefinitions()['details']);
+ \Drupal::service('renderer')->renderRoot($test_details);
+ $this->assertArrayHasKey('#custom', $test_details);
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php
new file mode 100644
index 000000000000..90e4a9c36be7
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Render\Element;
+
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
+use Drupal\Core\Render\Element\StatusReport;
+use Drupal\KernelTests\KernelTestBase;
+
+include_once \DRUPAL_ROOT . '/core/includes/install.inc';
+
+/**
+ * Tests the status report element.
+ *
+ * @group Render
+ * @group legacy
+ */
+class StatusReportTest extends KernelTestBase {
+
+ /**
+ * Tests the status report element.
+ */
+ public function testPreRenderGroupRequirements(): void {
+ $element = [
+ '#priorities' => [
+ 'error',
+ 'warning',
+ 'checked',
+ 'ok',
+ ],
+ '#requirements' => [
+ 'foo' => [
+ 'title' => 'Foo',
+ 'severity' => RequirementSeverity::Info,
+ ],
+ 'baz' => [
+ 'title' => 'Baz',
+ 'severity' => RequirementSeverity::Warning,
+ ],
+ 'wiz' => [
+ 'title' => 'Wiz',
+ 'severity' => RequirementSeverity::Error,
+ ],
+ 'bar' => [
+ 'title' => 'Bar',
+ 'severity' => RequirementSeverity::OK,
+ ],
+ 'legacy' => [
+ 'title' => 'Legacy',
+ 'severity' => \REQUIREMENT_OK,
+ ],
+ ],
+ ];
+
+ $this->expectDeprecation('Calling Drupal\Core\Render\Element\StatusReport::preRenderGroupRequirements() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939');
+
+ $element = StatusReport::preRenderGroupRequirements($element);
+ $groups = $element['#grouped_requirements'];
+
+ $errors = $groups['error'];
+ $this->assertEquals('Errors found', (string) $errors['title']);
+ $this->assertEquals('error', $errors['type']);
+ $errorItems = $errors['items'];
+ $this->assertCount(1, $errorItems);
+ $this->assertArrayHasKey('wiz', $errorItems);
+
+ $warnings = $groups['warning'];
+ $this->assertEquals('Warnings found', (string) $warnings['title']);
+ $this->assertEquals('warning', $warnings['type']);
+ $warningItems = $warnings['items'];
+ $this->assertCount(1, $warningItems);
+ $this->assertArrayHasKey('baz', $warningItems);
+
+ $checked = $groups['checked'];
+ $this->assertEquals('Checked', (string) $checked['title']);
+ $this->assertEquals('checked', $checked['type']);
+ $checkedItems = $checked['items'];
+ $this->assertCount(3, $checkedItems);
+ $this->assertArrayHasKey('foo', $checkedItems);
+ $this->assertArrayHasKey('bar', $checkedItems);
+ $this->assertArrayHasKey('legacy', $checkedItems);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php
index 00b7948f2a81..e36da16d9028 100644
--- a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php
@@ -8,6 +8,7 @@ use Drupal\Core\Form\FormState;
use Drupal\Core\Render\Element\Number;
use Drupal\Core\Render\Element\Select;
use Drupal\Core\Render\Element\Weight;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\element_info_test\ElementInfoTestNumberBuilder;
use Drupal\KernelTests\KernelTestBase;
@@ -40,7 +41,7 @@ class WeightTest extends KernelTestBase {
$form_state = new FormState();
$complete_form = [];
- $element_object = new Weight([], 'weight', []);
+ $element_object = new Weight([], 'weight', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $element_object->getInfo();
$element += $info;
diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php
new file mode 100644
index 000000000000..a8697cc84968
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Test;
+
+use Drupal\Core\Test\PhpUnitTestDiscovery;
+use Drupal\Core\Test\TestDiscovery;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+
+/**
+ * Tests ::findAllClassFiles() between TestDiscovery and PhpUnitTestDiscovery.
+ *
+ * PhpUnitTestDiscovery uses PHPUnit API to build the list of test classes,
+ * while TestDiscovery uses Drupal legacy code.
+ */
+#[CoversClass(PhpUnitTestDiscovery::class)]
+#[Group('TestSuites')]
+#[Group('Test')]
+#[Group('#slow')]
+class PhpUnitApiFindAllClassFilesTest extends KernelTestBase {
+
+ /**
+ * Checks that Drupal legacy and PHPUnit API based discoveries are equal.
+ */
+ #[DataProvider('argumentsProvider')]
+ #[IgnoreDeprecations]
+ public function testEquality(?string $extension = NULL, ?string $directory = NULL): void {
+ // PHPUnit discovery.
+ $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core';
+ // @todo once PHPUnit 10 is no longer used, remove the condition.
+ // @see https://www.drupal.org/project/drupal/issues/3497116
+ if (RunnerVersion::getMajor() >= 11) {
+ $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml';
+ }
+ $phpUnitTestDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath($configurationFilePath);
+ $phpUnitList = $phpUnitTestDiscovery->findAllClassFiles($extension, $directory);
+
+ // Legacy TestDiscovery.
+ $testDiscovery = new TestDiscovery(
+ $this->container->getParameter('app.root'),
+ $this->container->get('class_loader')
+ );
+ $internalList = $testDiscovery->findAllClassFiles($extension, $directory);
+
+ // Downgrade results to make them comparable, working around bugs and
+ // additions.
+ // 1. TestDiscovery discovers non-test classes that PHPUnit does not.
+ $internalList = array_intersect_key($internalList, $phpUnitList);
+
+ $this->assertEquals($internalList, $phpUnitList);
+ }
+
+ /**
+ * Provides test data to ::testEquality.
+ */
+ public static function argumentsProvider(): \Generator {
+ yield 'All tests' => [];
+ yield 'Extension: system' => ['extension' => 'system'];
+ yield 'Extension: system, directory' => [
+ 'extension' => 'system',
+ 'directory' => 'core/modules/system/tests/src',
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php
new file mode 100644
index 000000000000..caedbc0d2b62
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php
@@ -0,0 +1,108 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Test;
+
+use Drupal\Core\Test\PhpUnitTestDiscovery;
+use Drupal\Core\Test\TestDiscovery;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
+
+/**
+ * Tests ::getTestClasses() between TestDiscovery and PhpPUnitTestDiscovery.
+ *
+ * PhpPUnitTestDiscovery uses PHPUnit API to build the list of test classes,
+ * while TestDiscovery uses Drupal legacy code.
+ */
+#[CoversClass(PhpUnitTestDiscovery::class)]
+#[Group('TestSuites')]
+#[Group('Test')]
+#[Group('#slow')]
+class PhpUnitApiGetTestClassesTest extends KernelTestBase {
+
+ /**
+ * Checks that Drupal legacy and PHPUnit API based discoveries are equal.
+ */
+ #[DataProvider('argumentsProvider')]
+ #[IgnoreDeprecations]
+ public function testEquality(array $suites, ?string $extension = NULL, ?string $directory = NULL): void {
+ // PHPUnit discovery.
+ $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core';
+ // @todo once PHPUnit 10 is no longer used, remove the condition.
+ // @see https://www.drupal.org/project/drupal/issues/3497116
+ if (RunnerVersion::getMajor() >= 11) {
+ $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml';
+ }
+ $phpUnitTestDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath($configurationFilePath);
+ $phpUnitList = $phpUnitTestDiscovery->getTestClasses($extension, $suites, $directory);
+
+ // Legacy TestDiscovery.
+ $testDiscovery = new TestDiscovery(
+ $this->container->getParameter('app.root'),
+ $this->container->get('class_loader')
+ );
+ $internalList = $testDiscovery->getTestClasses($extension, $suites, $directory);
+
+ // Downgrade results to make them comparable, working around bugs and
+ // additions.
+ // 1. Remove TestDiscovery empty groups.
+ $internalList = array_filter($internalList);
+ // 2. Remove TestDiscovery '##no-group-annotations' group.
+ unset($internalList['##no-group-annotations']);
+ // 3. Remove 'file' and 'tests_count' keys from PHPUnit results.
+ foreach ($phpUnitList as &$group) {
+ foreach ($group as &$testClass) {
+ unset($testClass['file']);
+ unset($testClass['tests_count']);
+ }
+ }
+ // 4. Remove from PHPUnit results groups not found by TestDiscovery.
+ $phpUnitList = array_intersect_key($phpUnitList, $internalList);
+ // 5. Remove from PHPUnit groups classes not found by TestDiscovery.
+ foreach ($phpUnitList as $groupName => &$group) {
+ $group = array_intersect_key($group, $internalList[$groupName]);
+ }
+ // 6. Remove from PHPUnit test classes groups not found by TestDiscovery.
+ foreach ($phpUnitList as $groupName => &$group) {
+ foreach ($group as $testClassName => &$testClass) {
+ $testClass['groups'] = array_intersect_key($testClass['groups'], $internalList[$groupName][$testClassName]['groups']);
+ }
+ }
+
+ $this->assertEquals($internalList, $phpUnitList);
+ }
+
+ /**
+ * Provides test data to ::testEquality.
+ */
+ public static function argumentsProvider(): \Generator {
+ yield 'All tests' => ['suites' => []];
+ yield 'Testsuite: functional-javascript' => ['suites' => ['PHPUnit-FunctionalJavascript']];
+ yield 'Testsuite: functional' => ['suites' => ['PHPUnit-Functional']];
+ yield 'Testsuite: kernel' => ['suites' => ['PHPUnit-Kernel']];
+ yield 'Testsuite: unit' => ['suites' => ['PHPUnit-Unit']];
+ yield 'Testsuite: unit-component' => ['suites' => ['PHPUnit-Unit-Component']];
+ yield 'Testsuite: build' => ['suites' => ['PHPUnit-Build']];
+ yield 'Extension: system' => ['suites' => [], 'extension' => 'system'];
+ yield 'Extension: system, testsuite: unit' => [
+ 'suites' => ['PHPUnit-Unit'],
+ 'extension' => 'system',
+ ];
+ yield 'Extension: system, directory' => [
+ 'suites' => [],
+ 'extension' => 'system',
+ 'directory' => 'core/modules/system/tests/src',
+ ];
+ yield 'Extension: system, testsuite: unit, directory' => [
+ 'suites' => ['PHPUnit-Unit'],
+ 'extension' => 'system',
+ 'directory' => 'core/modules/system/tests/src',
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
index 6d7fcaeed9ae..345cc9282d2e 100644
--- a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
@@ -6,8 +6,9 @@ namespace Drupal\KernelTests\Core\Test;
use Drupal\Core\Test\TestDiscovery;
use Drupal\KernelTests\KernelTestBase;
-use PHPUnit\TextUI\Configuration\Builder;
-use PHPUnit\TextUI\Configuration\TestSuiteBuilder;
+use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
use Symfony\Component\Process\Process;
/**
@@ -21,11 +22,11 @@ use Symfony\Component\Process\Process;
* list thus generated, with the list generated by
* \Drupal\Core\Test\TestDiscovery, which is used by run-tests.sh, to ensure
* both methods will run the same tests,
- *
- * @group TestSuites
- * @group Test
- * @group #slow
*/
+#[Group('TestSuites')]
+#[Group('Test')]
+#[Group('#slow')]
+#[IgnoreDeprecations]
class PhpUnitTestDiscoveryTest extends KernelTestBase {
private const TEST_LIST_MISMATCH_MESSAGE =
@@ -75,11 +76,19 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase {
$internalList = array_unique($internalList);
asort($internalList);
+ // Location of PHPUnit configuration file.
+ $configurationFilePath = $this->root . \DIRECTORY_SEPARATOR . 'core';
+ // @todo once PHPUnit 10 is no longer used, remove the condition.
+ // @see https://www.drupal.org/project/drupal/issues/3497116
+ if (RunnerVersion::getMajor() >= 11) {
+ $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml';
+ }
+
// PHPUnit's test discovery - via CLI execution.
$process = new Process([
'vendor/bin/phpunit',
'--configuration',
- 'core',
+ $configurationFilePath,
'--list-tests-xml',
$this->xmlOutputFile,
], $this->root);
@@ -96,28 +105,21 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase {
$phpUnitXmlList = new \DOMDocument();
$phpUnitXmlList->loadXML(file_get_contents($this->xmlOutputFile));
$phpUnitClientList = [];
+ // Try PHPUnit 10 format first.
+ // @todo remove once PHPUnit 10 is no longer used.
foreach ($phpUnitXmlList->getElementsByTagName('testCaseClass') as $node) {
$phpUnitClientList[] = $node->getAttribute('name');
}
- asort($phpUnitClientList);
-
- // Check against Drupal's discovery.
- $this->assertEquals(implode("\n", $phpUnitClientList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE);
-
- // PHPUnit's test discovery - via API.
- $phpUnitConfiguration = (new Builder())->build(['--configuration', 'core']);
- $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration);
- $phpUnitApiList = [];
- foreach ($phpUnitTestSuite->tests() as $testSuite) {
- foreach ($testSuite->tests() as $test) {
- $phpUnitApiList[] = $test->name();
+ // If empty, try PHPUnit 11+ format.
+ if (empty($phpUnitClientList)) {
+ foreach ($phpUnitXmlList->getElementsByTagName('testClass') as $node) {
+ $phpUnitClientList[] = $node->getAttribute('name');
}
}
- asort($phpUnitApiList);
+ asort($phpUnitClientList);
// Check against Drupal's discovery.
- $this->assertEquals(implode("\n", $phpUnitApiList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE);
-
+ $this->assertEquals(implode("\n", $phpUnitClientList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE);
}
}
diff --git a/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php
index f2f9bd16a04e..cd4fdc1258c8 100644
--- a/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Drupal\KernelTests\Core\Updater;
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\KernelTests\KernelTestBase;
@@ -27,7 +28,7 @@ class UpdateRequirementsTest extends KernelTestBase {
'title' => 'UpdateError',
'value' => 'None',
'description' => 'Update Error.',
- 'severity' => REQUIREMENT_ERROR,
+ 'severity' => RequirementSeverity::Error,
];
$requirements = update_check_requirements()['test.update.error'];
$this->assertEquals($testRequirements, $requirements);
@@ -36,7 +37,7 @@ class UpdateRequirementsTest extends KernelTestBase {
'title' => 'UpdateWarning',
'value' => 'None',
'description' => 'Update Warning.',
- 'severity' => REQUIREMENT_WARNING,
+ 'severity' => RequirementSeverity::Warning,
];
$alterRequirements = update_check_requirements()['test.update.error.alter'];
$this->assertEquals($testAlterRequirements, $alterRequirements);
diff --git a/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php
new file mode 100644
index 000000000000..aa845c50b1da
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php
@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Core\Validation;
+
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests the UriHost validator.
+ *
+ * @group Validation
+ */
+class UriHostValidatorTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = ['config_test'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->installConfig('config_test');
+ }
+
+ /**
+ * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UriHostConstraint
+ */
+ public function testUriHost(): void {
+ $typed_config_manager = \Drupal::service('config.typed');
+ /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
+ $typed_config = $typed_config_manager->get('config_test.validation');
+
+ // Test valid names.
+ $typed_config->get('host')->setValue('example.com');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('example.com.');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('default');
+ $this->assertCount(0, $typed_config->validate());
+
+ // Test invalid names.
+ $typed_config->get('host')->setValue('.example.com');
+ $this->assertCount(1, $typed_config->validate());
+
+ // Test valid IPv6 literals.
+ $typed_config->get('host')->setValue('[::1]');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('[2001:DB8::]');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('[2001:db8:dd54:4473:bd6e:52db:10b3:4abe]');
+ $this->assertCount(0, $typed_config->validate());
+
+ // Test invalid IPv6 literals.
+ $typed_config->get('host')->setValue('::1');
+ $this->assertCount(1, $typed_config->validate());
+
+ // Test valid IPv4 addresses.
+ $typed_config->get('host')->setValue('127.0.0.1');
+ $this->assertCount(0, $typed_config->validate());
+
+ $typed_config->get('host')->setValue('192.0.2.254');
+ $this->assertCount(0, $typed_config->validate());
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php
index 7c7d6aa0e39b..5889553a1491 100644
--- a/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php
@@ -44,48 +44,4 @@ class UuidValidatorTest extends KernelTestBase {
$this->assertCount(1, $typed_config->validate());
}
- /**
- * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UriHostConstraint
- */
- public function testUriHost(): void {
- $typed_config_manager = \Drupal::service('config.typed');
- /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */
- $typed_config = $typed_config_manager->get('config_test.validation');
-
- // Test valid names.
- $typed_config->get('host')->setValue('example.com');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('example.com.');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('default');
- $this->assertCount(0, $typed_config->validate());
-
- // Test invalid names.
- $typed_config->get('host')->setValue('.example.com');
- $this->assertCount(1, $typed_config->validate());
-
- // Test valid IPv6 literals.
- $typed_config->get('host')->setValue('[::1]');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('[2001:DB8::]');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('[2001:db8:dd54:4473:bd6e:52db:10b3:4abe]');
- $this->assertCount(0, $typed_config->validate());
-
- // Test invalid IPv6 literals.
- $typed_config->get('host')->setValue('::1');
- $this->assertCount(1, $typed_config->validate());
-
- // Test valid IPv4 addresses.
- $typed_config->get('host')->setValue('127.0.0.1');
- $this->assertCount(0, $typed_config->validate());
-
- $typed_config->get('host')->setValue('192.0.2.254');
- $this->assertCount(0, $typed_config->validate());
- }
-
}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index 34aca08f15a9..c45e937d371e 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -439,7 +439,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa
throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.');
}
else {
- $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root, TRUE);
+ $database = Database::convertDbUrlToConnectionInfo($db_url, TRUE);
Database::addConnectionInfo('default', 'default', $database);
}
diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
index e8dfc3be6f45..828b4ab54ec0 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php
@@ -25,7 +25,7 @@ class KernelTestBaseDatabaseDriverModuleTest extends KernelTestBase {
throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.');
}
else {
- $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root);
+ $database = Database::convertDbUrlToConnectionInfo($db_url);
if (in_array($database['driver'], ['mysql', 'pgsql'])) {
// Change the used database driver to the one provided by the module
diff --git a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
index 02521bdda2f8..2f8b7309b520 100644
--- a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
+++ b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php
@@ -286,7 +286,7 @@ class TestSiteApplicationTest extends KernelTestBase {
* The database key of the added connection.
*/
protected function addTestDatabase($db_prefix): string {
- $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB'), $this->root);
+ $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB'));
$database['prefix'] = $db_prefix;
$target = __CLASS__ . $db_prefix;
Database::addConnectionInfo($target, 'default', $database);
diff --git a/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js b/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js
new file mode 100644
index 000000000000..98916702a88f
--- /dev/null
+++ b/core/tests/Drupal/Nightwatch/Tests/htmx/htmxTest.js
@@ -0,0 +1,77 @@
+// The javascript that creates dropbuttons is not present on the /page at
+// initial load. If the once data property is added then the JS was loaded
+// and triggered on the inserted content.
+// @see \Drupal\test_htmx\Controller\HtmxTestAttachmentsController
+// @see core/modules/system/tests/modules/test_htmx/js/reveal-merged-settings.js
+
+const scriptSelector = 'script[src*="test_htmx/js/behavior.js"]';
+const cssSelector = 'link[rel="stylesheet"][href*="test_htmx/css/style.css"]';
+const elementSelector = '.ajax-content';
+const elementInitSelector = `${elementSelector}[data-once="htmx-init"]`;
+
+module.exports = {
+ '@tags': ['core', 'htmx'],
+ before(browser) {
+ browser.drupalInstall({
+ setupFile: 'core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php',
+ installProfile: 'minimal',
+ });
+ },
+ afterEach(browser) {
+ browser.drupalLogAndEnd({ onlyOnError: true });
+ },
+ after(browser) {
+ browser.drupalUninstall();
+ },
+
+ 'Asset Load': (browser) => {
+ // Load the route htmx will use for the request on click and confirm the
+ // markup we will be looking for is present in the source markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/replace')
+ .waitForElementVisible('body', 1000)
+ .assert.elementPresent(elementInitSelector);
+ // Now load the page with the htmx enhanced button and verify the absence
+ // of the markup to be inserted. Click the button
+ // and check for inserted javascript and markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/page')
+ .waitForElementVisible('body', 1000)
+ .assert.not.elementPresent(scriptSelector)
+ .assert.not.elementPresent(cssSelector)
+ .waitForElementVisible('[name="replace"]', 1000)
+ .click('[name="replace"]')
+ .waitForElementVisible(elementSelector, 6000)
+ .waitForElementVisible(elementInitSelector, 6000)
+ .assert.elementPresent(scriptSelector)
+ .assert.elementPresent(cssSelector);
+ },
+
+ 'Ajax Load HTMX Element': (browser) => {
+ // Load the route htmx will use for the request on click and confirm the
+ // markup we will be looking for is present in the source markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/replace')
+ .waitForElementVisible('body', 1000)
+ .assert.elementPresent(scriptSelector);
+ // Now load the page with the ajax powered button. Click the button
+ // to insert an htmx enhanced button and verify the absence
+ // of the markup to be inserted. Click the button
+ // and check for inserted javascript and markup.
+ browser
+ .drupalRelativeURL('/htmx-test-attachments/ajax')
+ .waitForElementVisible('body', 1000)
+ .assert.not.elementPresent(scriptSelector)
+ .assert.not.elementPresent(cssSelector)
+ .waitForElementVisible('[data-drupal-selector="edit-ajax-button"]', 1000)
+ .pause(1000)
+ .click('[data-drupal-selector="edit-ajax-button"]')
+ .waitForElementVisible('[name="replace"]', 1000)
+ .pause(1000)
+ .click('[name="replace"]')
+ .waitForElementVisible(elementSelector, 6000)
+ .waitForElementVisible(elementInitSelector, 6000)
+ .assert.elementPresent(scriptSelector)
+ .assert.elementPresent(cssSelector);
+ },
+};
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
index 1459f0cdfee3..470d3ef92e22 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteInstallCommand.php
@@ -280,18 +280,18 @@ class TestSiteInstallCommand extends Command {
}
/**
- * {@inheritdoc}
+ * Changes the database connection to the prefixed one.
*/
- protected function changeDatabasePrefix() {
+ protected function changeDatabasePrefix(): void {
// Ensure that we use the database from SIMPLETEST_DB environment variable.
Database::removeConnection('default');
$this->changeDatabasePrefixTrait();
}
/**
- * {@inheritdoc}
+ * Generates a database prefix for the site installation.
*/
- protected function prepareDatabasePrefix() {
+ protected function prepareDatabasePrefix(): void {
// Override this method so that we can force a lock to be created.
$test_db = new TestDatabase(NULL, TRUE);
$this->siteDirectory = $test_db->getTestSitePath();
diff --git a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
index 3d80f3a1c178..7e601981d4c6 100644
--- a/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
+++ b/core/tests/Drupal/TestSite/Commands/TestSiteTearDownCommand.php
@@ -84,7 +84,7 @@ class TestSiteTearDownCommand extends Command {
protected function tearDown(TestDatabase $test_database, $db_url): void {
// Connect to the test database.
$root = dirname(__DIR__, 5);
- $database = Database::convertDbUrlToConnectionInfo($db_url, $root);
+ $database = Database::convertDbUrlToConnectionInfo($db_url);
$database['prefix'] = $test_database->getDatabasePrefix();
Database::addConnectionInfo(__CLASS__, 'default', $database);
diff --git a/core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php b/core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php
new file mode 100644
index 000000000000..03bb3fbdb113
--- /dev/null
+++ b/core/tests/Drupal/TestSite/HtmxAssetLoadTestSetup.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\TestSite;
+
+use Drupal\Core\Extension\ModuleInstallerInterface;
+use Drupal\Core\Extension\ThemeInstallerInterface;
+
+/**
+ * Setup file used by tests/src/Nightwatch/Tests/htmxAssetLoadTest.js.
+ *
+ * @see \Drupal\Tests\Scripts\TestSiteApplicationTest
+ */
+class HtmxAssetLoadTestSetup implements TestSetupInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setup(): void {
+ // Install Olivero and set it as the default theme.
+ $theme_installer = \Drupal::service('theme_installer');
+ assert($theme_installer instanceof ThemeInstallerInterface);
+ $theme_installer->install(['olivero'], TRUE);
+ $system_theme_config = \Drupal::configFactory()->getEditable('system.theme');
+ $system_theme_config->set('default', 'olivero')->save();
+
+ // Install required modules.
+ $module_installer = \Drupal::service('module_installer');
+ assert($module_installer instanceof ModuleInstallerInterface);
+ $module_installer->install(['test_htmx']);
+ }
+
+}
diff --git a/core/tests/Drupal/TestTools/Extension/DeprecationBridge/DeprecationHandler.php b/core/tests/Drupal/TestTools/Extension/DeprecationBridge/DeprecationHandler.php
index c176980bae27..f66ef7c8c947 100644
--- a/core/tests/Drupal/TestTools/Extension/DeprecationBridge/DeprecationHandler.php
+++ b/core/tests/Drupal/TestTools/Extension/DeprecationBridge/DeprecationHandler.php
@@ -76,6 +76,11 @@ final class DeprecationHandler {
$environmentVariable = "ignoreFile=$deprecationIgnoreFilename";
}
parse_str($environmentVariable, $configuration);
+
+ $environmentVariable = getenv('PHPUNIT_FAIL_ON_PHPUNIT_DEPRECATION');
+ $phpUnitDeprecationVariable = $environmentVariable !== FALSE ? $environmentVariable : TRUE;
+ $configuration['failOnPhpunitDeprecation'] = filter_var($phpUnitDeprecationVariable, \FILTER_VALIDATE_BOOLEAN);
+
return $configuration;
}
diff --git a/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php b/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php
index d60a4f3062c1..ed73ca8fd933 100644
--- a/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php
+++ b/core/tests/Drupal/TestTools/Extension/DeprecationBridge/ExpectDeprecationTrait.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Drupal\TestTools\Extension\DeprecationBridge;
-use Drupal\Core\Utility\Error;
use Drupal\TestTools\ErrorHandler\TestErrorHandler;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
@@ -41,7 +40,7 @@ trait ExpectDeprecationTrait {
}
DeprecationHandler::reset();
- set_error_handler(new TestErrorHandler(Error::currentErrorHandler(), $this));
+ set_error_handler(new TestErrorHandler(get_error_handler(), $this));
}
/**
@@ -61,8 +60,7 @@ trait ExpectDeprecationTrait {
// ::setUpErrorHandler() prior to the start of the test execution. If not,
// the error handler was changed during the test execution but not properly
// restored during ::tearDown().
- $handler = Error::currentErrorHandler();
- if (!$handler instanceof TestErrorHandler) {
+ if (!get_error_handler() instanceof TestErrorHandler) {
throw new \RuntimeException(sprintf('%s registered its own error handler without restoring the previous one before or during tear down. This can cause unpredictable test results. Ensure the test cleans up after itself.', $this->name()));
}
restore_error_handler();
diff --git a/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit11/TestCompatibilityTrait.php b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit11/TestCompatibilityTrait.php
new file mode 100644
index 000000000000..84638f9f0f57
--- /dev/null
+++ b/core/tests/Drupal/TestTools/PhpUnitCompatibility/PhpUnit11/TestCompatibilityTrait.php
@@ -0,0 +1,13 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\TestTools\PhpUnitCompatibility\PhpUnit11;
+
+/**
+ * Drupal's forward compatibility layer with multiple versions of PHPUnit.
+ *
+ * @internal
+ */
+trait TestCompatibilityTrait {
+}
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index 12228dbea721..98aafad7df13 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -173,16 +173,6 @@ abstract class BrowserTestBase extends TestCase {
protected $originalShutdownCallbacks = [];
/**
- * The original container.
- *
- * Move this to \Drupal\Core\Test\FunctionalTestSetupTrait once TestBase no
- * longer provides the same value.
- *
- * @var \Symfony\Component\DependencyInjection\ContainerInterface
- */
- protected $originalContainer;
-
- /**
* {@inheritdoc}
*/
public function __construct(string $name) {
@@ -512,8 +502,14 @@ abstract class BrowserTestBase extends TestCase {
*
* @return array
* Associative array of option keys and values.
+ *
+ * @deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is
+ * no direct replacement.
+ *
+ * @see https://www.drupal.org/node/3523039
*/
protected function getOptions($select, ?Element $container = NULL) {
+ @trigger_error(__METHOD__ . 'is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. There is no direct replacement. See https://www.drupal.org/node/3523039', E_DEPRECATED);
if (is_string($select)) {
$select = $this->assertSession()->selectExists($select, $container);
}
diff --git a/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php b/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php
index 8dd4d8fb5948..ae46c3cc5a56 100644
--- a/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php
+++ b/core/tests/Drupal/Tests/Component/Datetime/DateTimePlusTest.php
@@ -671,16 +671,16 @@ class DateTimePlusTest extends TestCase {
// There should be a 19 hour time interval between
// new years in Sydney and new years in LA in year 2000.
[
- 'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
- 'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
+ 'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
+ 'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
'absolute' => FALSE,
'expected' => $positive_19_hours,
],
// In 1970 Sydney did not observe daylight savings time
// So there is only an 18 hour time interval.
[
- 'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
- 'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
+ 'input1' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney')),
+ 'input2' => DateTimePlus::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles')),
'absolute' => FALSE,
'expected' => $positive_18_hours,
],
diff --git a/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php b/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php
new file mode 100644
index 000000000000..7026a2ceac5a
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/Gettext/PoItemTest.php
@@ -0,0 +1,85 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Component\Gettext;
+
+use Drupal\Component\Gettext\PoItem;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Component\Gettext\PoItem
+ * @group Gettext
+ */
+class PoItemTest extends TestCase {
+
+ /**
+ * @return array
+ * - Source string
+ * - Context (optional)
+ * - Translated string (optional)
+ * - Expected value
+ */
+ public static function providerStrings(): array {
+ // cSpell:disable
+ return [
+ [
+ '',
+ NULL,
+ NULL,
+ 'msgid ""' . "\n" . 'msgstr ""' . "\n\n",
+ ],
+ // Translated String without contesxt.
+ [
+ 'Next',
+ NULL,
+ 'Suivant',
+ 'msgid "Next"' . "\n" . 'msgstr "Suivant"' . "\n\n",
+ ],
+ // Translated string with context.
+ [
+ 'Apr',
+ 'Abbreviated month name',
+ 'Avr',
+ 'msgctxt "Abbreviated month name"' . "\n" . 'msgid "Apr"' . "\n" . 'msgstr "Avr"' . "\n\n",
+ ],
+ // Translated string with placeholder.
+ [
+ '%email is not a valid email address.',
+ NULL,
+ '%email n\'est pas une adresse de courriel valide.',
+ 'msgid "%email is not a valid email address."' . "\n" . 'msgstr "%email n\'est pas une adresse de courriel valide."' . "\n\n",
+ ],
+ // Translated Plural String without context.
+ [
+ ['Installed theme', 'Installed themes'],
+ NULL,
+ ['Thème installé', 'Thèmes installés'],
+ 'msgid "Installed theme"' . "\n" . 'msgid_plural "Installed themes"' . "\n" . 'msgstr[0] "Thème installé"' . "\n" . 'msgstr[1] "Thèmes installés"' . "\n\n",
+ ],
+ ];
+ // cSpell:enable
+ }
+
+ /**
+ * @dataProvider providerStrings
+ */
+ public function testFormat($source, $context, $translation, $expected): void {
+ $item = new PoItem();
+
+ $item->setSource($source);
+
+ if (is_array($source)) {
+ $item->setPlural(TRUE);
+ }
+ if (!empty($context)) {
+ $item->setContext($context);
+ }
+ if (!empty($translation)) {
+ $item->setTranslation($translation);
+ }
+
+ $this->assertEquals($expected, (string) $item);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
index b2c459e06a78..800483fab501 100644
--- a/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/HtmlTest.php
@@ -65,6 +65,7 @@ class HtmlTest extends TestCase {
$id1 = 'abcdefghijklmnopqrstuvwxyz_ABCDEFGHIJKLMNOPQRSTUVWXYZ-0123456789';
$id2 = '¡¢£¤¥';
$id3 = 'css__identifier__with__double__underscores';
+ $id4 = "\x80\x81";
return [
// Verify that no valid ASCII characters are stripped from the identifier.
[$id1, $id1, []],
@@ -73,6 +74,8 @@ class HtmlTest extends TestCase {
[$id2, $id2, []],
// Verify that double underscores are not stripped from the identifier.
[$id3, $id3],
+ // Confirm that NULL identifier does not trigger PHP 8.1 deprecation message.
+ ['', $id4],
// Verify that invalid characters (including non-breaking space) are
// stripped from the identifier.
['invalid_identifier', 'invalid_ !"#$%&\'()*+,./:;<=>?@[\\]^`{|}~ identifier', []],
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/ExecTrait.php b/core/tests/Drupal/Tests/Composer/Plugin/ExecTrait.php
index 142ebbf52f2b..6a234499dac2 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/ExecTrait.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/ExecTrait.php
@@ -2,7 +2,7 @@
declare(strict_types=1);
-namespace Drupal\Tests\Composer\Plugin\Scaffold;
+namespace Drupal\Tests\Composer\Plugin;
use Symfony\Component\Process\Process;
@@ -20,17 +20,20 @@ trait ExecTrait {
* The current working directory to run the command from.
* @param array $env
* Environment variables to define for the subprocess.
+ * @param string $error_output
+ * (optional) Passed by reference to allow error output to be tested.
*
* @return string
* Standard output from the command
*/
- protected function mustExec($cmd, $cwd, array $env = []): string {
+ protected function mustExec($cmd, $cwd, array $env = [], string &$error_output = ''): string {
$process = Process::fromShellCommandline($cmd, $cwd, $env + ['PATH' => getenv('PATH'), 'HOME' => getenv('HOME')]);
$process->setTimeout(300)->setIdleTimeout(300)->run();
$exitCode = $process->getExitCode();
if (0 != $exitCode) {
throw new \RuntimeException("Exit code: {$exitCode}\n\n" . $process->getErrorOutput() . "\n\n" . $process->getOutput());
}
+ $error_output = $process->getErrorOutput();
return $process->getOutput();
}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/FixturesBase.php b/core/tests/Drupal/Tests/Composer/Plugin/FixturesBase.php
new file mode 100644
index 000000000000..0c8ea9226679
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/FixturesBase.php
@@ -0,0 +1,272 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Composer\Plugin;
+
+use Composer\Composer;
+use Composer\Console\Application;
+use Composer\Factory;
+use Composer\IO\BufferIO;
+use Composer\IO\IOInterface;
+use Composer\Util\Filesystem;
+use Drupal\Composer\Plugin\Scaffold\Interpolator;
+use Symfony\Component\Console\Input\StringInput;
+use Symfony\Component\Console\Output\BufferedOutput;
+
+/**
+ * Base class for fixtures to test composer plugins.
+ */
+abstract class FixturesBase {
+
+ /**
+ * Keep a persistent prefix to help group our tmp directories together.
+ *
+ * @var string
+ */
+ protected static string $randomPrefix = '';
+
+ /**
+ * Directories to delete when we are done.
+ *
+ * @var string[]
+ */
+ protected array $tmpDirs = [];
+
+ /**
+ * A Composer IOInterface to write to.
+ *
+ * @var \Composer\IO\IOInterface|null
+ */
+ protected ?IOInterface $io;
+
+ /**
+ * The composer object.
+ *
+ * @var \Composer\Composer
+ */
+ protected Composer $composer;
+
+ /**
+ * Gets an IO fixture.
+ *
+ * @return \Composer\IO\IOInterface
+ * A Composer IOInterface to write to; output may be retrieved via
+ * Fixtures::getOutput().
+ */
+ public function io(): IOInterface {
+ if (!isset($this->io)) {
+ $this->io = new BufferIO();
+ }
+ return $this->io;
+ }
+
+ /**
+ * Gets the Composer object.
+ *
+ * @return \Composer\Composer
+ * The main Composer object, needed by the scaffold Handler, etc.
+ */
+ public function getComposer(): Composer {
+ if (!isset($this->composer)) {
+ $this->composer = Factory::create($this->io(), NULL, TRUE);
+ }
+ return $this->composer;
+ }
+
+ /**
+ * Gets the output from the io() fixture.
+ *
+ * @return string
+ * Output captured from tests that write to Fixtures::io().
+ */
+ public function getOutput(): string {
+ return $this->io()->getOutput();
+ }
+
+ /**
+ * Gets the path to Scaffold component.
+ *
+ * Used to inject the component into composer.json files.
+ *
+ * @return string
+ * Path to the root of this project.
+ */
+ abstract public function projectRoot(): string;
+
+ /**
+ * Gets the path to the project fixtures.
+ *
+ * @return string
+ * Path to project fixtures
+ */
+ abstract public function allFixturesDir(): string;
+
+ /**
+ * Gets the path to one particular project fixture.
+ *
+ * @param string $project_name
+ * The project name to get the path for.
+ *
+ * @return string
+ * Path to project fixture.
+ */
+ public function projectFixtureDir(string $project_name): string {
+ $dir = $this->allFixturesDir() . '/' . $project_name;
+ if (!is_dir($dir)) {
+ throw new \RuntimeException("Requested fixture project {$project_name} that does not exist.");
+ }
+ return $dir;
+ }
+
+ /**
+ * Gets the path to one particular bin path.
+ *
+ * @param string $bin_name
+ * The bin name to get the path for.
+ *
+ * @return string
+ * Path to project fixture.
+ */
+ public function binFixtureDir(string $bin_name): string {
+ $dir = $this->allFixturesDir() . '/scripts/' . $bin_name;
+ if (!is_dir($dir)) {
+ throw new \RuntimeException("Requested fixture bin dir {$bin_name} that does not exist.");
+ }
+ return $dir;
+ }
+
+ /**
+ * Generates a path to a temporary location, but do not create the directory.
+ *
+ * @param string $prefix
+ * A prefix for the temporary directory name.
+ *
+ * @return string
+ * Path to temporary directory
+ */
+ public function tmpDir(string $prefix): string {
+ $prefix .= static::persistentPrefix();
+ $tmpDir = sys_get_temp_dir() . '/scaffold-' . $prefix . uniqid(md5($prefix . microtime()), TRUE);
+ $this->tmpDirs[] = $tmpDir;
+ return $tmpDir;
+ }
+
+ /**
+ * Generates a persistent prefix to use with all of our temporary directories.
+ *
+ * The presumption is that this should reduce collisions in highly-parallel
+ * tests. We prepend the process id to play nicely with phpunit process
+ * isolation.
+ *
+ * @return string
+ * A random string that will remain the same for the entire process run.
+ */
+ protected static function persistentPrefix(): string {
+ if (empty(static::$randomPrefix)) {
+ static::$randomPrefix = getmypid() . md5(microtime());
+ }
+ return static::$randomPrefix;
+ }
+
+ /**
+ * Creates a temporary directory.
+ *
+ * @param string $prefix
+ * A prefix for the temporary directory name.
+ *
+ * @return string
+ * Path to temporary directory
+ */
+ public function mkTmpDir(string $prefix): string {
+ $tmpDir = $this->tmpDir($prefix);
+ $filesystem = new Filesystem();
+ $filesystem->ensureDirectoryExists($tmpDir);
+ return $tmpDir;
+ }
+
+ /**
+ * Creates an isolated cache directory for Composer.
+ */
+ public function createIsolatedComposerCacheDir(): void {
+ $cacheDir = $this->mkTmpDir('composer-cache');
+ putenv("COMPOSER_CACHE_DIR=$cacheDir");
+ }
+
+ /**
+ * Calls 'tearDown' in any test that copies fixtures to transient locations.
+ */
+ public function tearDown(): void {
+ // Remove any temporary directories that were created.
+ $filesystem = new Filesystem();
+ foreach ($this->tmpDirs as $dir) {
+ $filesystem->remove($dir);
+ }
+ // Clear out variables from the previous pass.
+ $this->tmpDirs = [];
+ $this->io = NULL;
+ // Clear the composer cache dir, if it was set
+ putenv('COMPOSER_CACHE_DIR=');
+ }
+
+ /**
+ * Creates a temporary copy of all of the fixtures projects into a temp dir.
+ *
+ * The fixtures remain dirty if they already exist. Individual tests should
+ * first delete any fixture directory that needs to remain pristine. Since all
+ * temporary directories are removed in tearDown, this is only an issue when
+ * a) the FIXTURE_DIR environment variable has been set, or b) tests are
+ * calling cloneFixtureProjects more than once per test method.
+ *
+ * @param string $fixturesDir
+ * The directory to place fixtures in.
+ * @param array $replacements
+ * Key : value mappings for placeholders to replace in composer.json
+ * templates.
+ */
+ public function cloneFixtureProjects(string $fixturesDir, array $replacements = []): void {
+ $filesystem = new Filesystem();
+ // We will replace 'SYMLINK' with the string 'true' in our composer.json
+ // fixture.
+ $replacements += ['SYMLINK' => 'true'];
+ $interpolator = new Interpolator('__', '__');
+ $interpolator->setData($replacements);
+ $filesystem->copy($this->allFixturesDir(), $fixturesDir);
+ $composer_json_templates = glob($fixturesDir . "/*/composer.json.tmpl");
+ foreach ($composer_json_templates as $composer_json_tmpl) {
+ // Inject replacements into composer.json.
+ if (file_exists($composer_json_tmpl)) {
+ $composer_json_contents = file_get_contents($composer_json_tmpl);
+ $composer_json_contents = $interpolator->interpolate($composer_json_contents, [], FALSE);
+ file_put_contents(dirname($composer_json_tmpl) . "/composer.json", $composer_json_contents);
+ @unlink($composer_json_tmpl);
+ }
+ }
+ }
+
+ /**
+ * Runs a `composer` command.
+ *
+ * @param string $cmd
+ * The Composer command to execute (escaped as required)
+ * @param string $cwd
+ * The current working directory to run the command from.
+ *
+ * @return string
+ * Standard output and standard error from the command.
+ */
+ public function runComposer(string $cmd, string $cwd): string {
+ chdir($cwd);
+ $input = new StringInput($cmd);
+ $output = new BufferedOutput();
+ $application = new Application();
+ $application->setAutoExit(FALSE);
+ $exitCode = $application->run($input, $output);
+ $output = $output->fetch();
+ if ($exitCode != 0) {
+ throw new \Exception("Fixtures::runComposer failed to set up fixtures.\n\nCommand: '{$cmd}'\nExit code: {$exitCode}\nOutput: \n\n$output");
+ }
+ return $output;
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Fixtures.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Fixtures.php
index 624aaa59f6cd..d92f54da5619 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Fixtures.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Fixtures.php
@@ -4,145 +4,33 @@ declare(strict_types=1);
namespace Drupal\Tests\Composer\Plugin\Scaffold;
-use Composer\Console\Application;
-use Composer\Factory;
-use Composer\IO\BufferIO;
-use Composer\Util\Filesystem;
+use Drupal\Tests\Composer\Plugin\FixturesBase;
use Drupal\Composer\Plugin\Scaffold\Handler;
use Drupal\Composer\Plugin\Scaffold\Interpolator;
use Drupal\Composer\Plugin\Scaffold\Operations\AppendOp;
use Drupal\Composer\Plugin\Scaffold\Operations\ReplaceOp;
use Drupal\Composer\Plugin\Scaffold\ScaffoldFilePath;
-use Symfony\Component\Console\Input\StringInput;
-use Symfony\Component\Console\Output\BufferedOutput;
/**
* Convenience class for creating fixtures.
*/
-class Fixtures {
+class Fixtures extends FixturesBase {
/**
- * Keep a persistent prefix to help group our tmp directories together.
- *
- * @var string
- */
- protected static $randomPrefix = '';
-
- /**
- * Directories to delete when we are done.
- *
- * @var string[]
- */
- protected $tmpDirs = [];
-
- /**
- * A Composer IOInterface to write to.
- *
- * @var \Composer\IO\IOInterface
- */
- protected $io;
-
- /**
- * The composer object.
- *
- * @var \Composer\Composer
+ * {@inheritdoc}
*/
- protected $composer;
-
- /**
- * Gets an IO fixture.
- *
- * @return \Composer\IO\IOInterface
- * A Composer IOInterface to write to; output may be retrieved via
- * Fixtures::getOutput().
- */
- public function io() {
- if (!$this->io) {
- $this->io = new BufferIO();
- }
- return $this->io;
- }
-
- /**
- * Gets the Composer object.
- *
- * @return \Composer\Composer
- * The main Composer object, needed by the scaffold Handler, etc.
- */
- public function getComposer() {
- if (!$this->composer) {
- $this->composer = Factory::create($this->io(), NULL, TRUE);
- }
- return $this->composer;
- }
-
- /**
- * Gets the output from the io() fixture.
- *
- * @return string
- * Output captured from tests that write to Fixtures::io().
- */
- public function getOutput() {
- return $this->io()->getOutput();
- }
-
- /**
- * Gets the path to Scaffold component.
- *
- * Used to inject the component into composer.json files.
- *
- * @return string
- * Path to the root of this project.
- */
- public function projectRoot() {
+ public function projectRoot(): string {
return realpath(__DIR__) . '/../../../../../../../composer/Plugin/Scaffold';
}
/**
- * Gets the path to the project fixtures.
- *
- * @return string
- * Path to project fixtures
+ * {@inheritdoc}
*/
- public function allFixturesDir() {
+ public function allFixturesDir(): string {
return realpath(__DIR__ . '/fixtures');
}
/**
- * Gets the path to one particular project fixture.
- *
- * @param string $project_name
- * The project name to get the path for.
- *
- * @return string
- * Path to project fixture.
- */
- public function projectFixtureDir($project_name) {
- $dir = $this->allFixturesDir() . '/' . $project_name;
- if (!is_dir($dir)) {
- throw new \RuntimeException("Requested fixture project {$project_name} that does not exist.");
- }
- return $dir;
- }
-
- /**
- * Gets the path to one particular bin path.
- *
- * @param string $bin_name
- * The bin name to get the path for.
- *
- * @return string
- * Path to project fixture.
- */
- public function binFixtureDir($bin_name) {
- $dir = $this->allFixturesDir() . '/scripts/' . $bin_name;
- if (!is_dir($dir)) {
- throw new \RuntimeException("Requested fixture bin dir {$bin_name} that does not exist.");
- }
- return $dir;
- }
-
- /**
* Gets a path to a source scaffold fixture.
*
* Use in place of ScaffoldFilePath::sourcePath().
@@ -244,114 +132,6 @@ class Fixtures {
}
/**
- * Generates a path to a temporary location, but do not create the directory.
- *
- * @param string $prefix
- * A prefix for the temporary directory name.
- *
- * @return string
- * Path to temporary directory
- */
- public function tmpDir($prefix) {
- $prefix .= static::persistentPrefix();
- $tmpDir = sys_get_temp_dir() . '/scaffold-' . $prefix . uniqid(md5($prefix . microtime()), TRUE);
- $this->tmpDirs[] = $tmpDir;
- return $tmpDir;
- }
-
- /**
- * Generates a persistent prefix to use with all of our temporary directories.
- *
- * The presumption is that this should reduce collisions in highly-parallel
- * tests. We prepend the process id to play nicely with phpunit process
- * isolation.
- *
- * @return string
- * A random string that will remain the same for the entire process run.
- */
- protected static function persistentPrefix() {
- if (empty(static::$randomPrefix)) {
- static::$randomPrefix = getmypid() . md5(microtime());
- }
- return static::$randomPrefix;
- }
-
- /**
- * Creates a temporary directory.
- *
- * @param string $prefix
- * A prefix for the temporary directory name.
- *
- * @return string
- * Path to temporary directory
- */
- public function mkTmpDir($prefix) {
- $tmpDir = $this->tmpDir($prefix);
- $filesystem = new Filesystem();
- $filesystem->ensureDirectoryExists($tmpDir);
- return $tmpDir;
- }
-
- /**
- * Create an isolated cache directory for Composer.
- */
- public function createIsolatedComposerCacheDir() {
- $cacheDir = $this->mkTmpDir('composer-cache');
- putenv("COMPOSER_CACHE_DIR=$cacheDir");
- }
-
- /**
- * Calls 'tearDown' in any test that copies fixtures to transient locations.
- */
- public function tearDown() {
- // Remove any temporary directories that were created.
- $filesystem = new Filesystem();
- foreach ($this->tmpDirs as $dir) {
- $filesystem->remove($dir);
- }
- // Clear out variables from the previous pass.
- $this->tmpDirs = [];
- $this->io = NULL;
- // Clear the composer cache dir, if it was set
- putenv('COMPOSER_CACHE_DIR=');
- }
-
- /**
- * Creates a temporary copy of all of the fixtures projects into a temp dir.
- *
- * The fixtures remain dirty if they already exist. Individual tests should
- * first delete any fixture directory that needs to remain pristine. Since all
- * temporary directories are removed in tearDown, this is only an issue when
- * a) the FIXTURE_DIR environment variable has been set, or b) tests are
- * calling cloneFixtureProjects more than once per test method.
- *
- * @param string $fixturesDir
- * The directory to place fixtures in.
- * @param array $replacements
- * Key : value mappings for placeholders to replace in composer.json
- * templates.
- */
- public function cloneFixtureProjects($fixturesDir, array $replacements = []) {
- $filesystem = new Filesystem();
- // We will replace 'SYMLINK' with the string 'true' in our composer.json
- // fixture.
- $replacements += ['SYMLINK' => 'true'];
- $interpolator = new Interpolator('__', '__');
- $interpolator->setData($replacements);
- $filesystem->copy($this->allFixturesDir(), $fixturesDir);
- $composer_json_templates = glob($fixturesDir . "/*/composer.json.tmpl");
- foreach ($composer_json_templates as $composer_json_tmpl) {
- // Inject replacements into composer.json.
- if (file_exists($composer_json_tmpl)) {
- $composer_json_contents = file_get_contents($composer_json_tmpl);
- $composer_json_contents = $interpolator->interpolate($composer_json_contents, [], FALSE);
- file_put_contents(dirname($composer_json_tmpl) . "/composer.json", $composer_json_contents);
- @unlink($composer_json_tmpl);
- }
- }
- }
-
- /**
* Runs the scaffold operation.
*
* This is equivalent to running `composer composer-scaffold`, but we do the
@@ -372,29 +152,4 @@ class Fixtures {
return $this->getOutput();
}
- /**
- * Runs a `composer` command.
- *
- * @param string $cmd
- * The Composer command to execute (escaped as required)
- * @param string $cwd
- * The current working directory to run the command from.
- *
- * @return string
- * Standard output and standard error from the command.
- */
- public function runComposer($cmd, $cwd) {
- chdir($cwd);
- $input = new StringInput($cmd);
- $output = new BufferedOutput();
- $application = new Application();
- $application->setAutoExit(FALSE);
- $exitCode = $application->run($input, $output);
- $output = $output->fetch();
- if ($exitCode != 0) {
- throw new \Exception("Fixtures::runComposer failed to set up fixtures.\n\nCommand: '{$cmd}'\nExit code: {$exitCode}\nOutput: \n\n$output");
- }
- return $output;
- }
-
}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php
index 14d896ad1823..b87861671e8e 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ComposerHookTest.php
@@ -7,7 +7,7 @@ namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem;
use Drupal\BuildTests\Framework\BuildTestBase;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
-use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait;
+use Drupal\Tests\Composer\Plugin\ExecTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
/**
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php
index c7fd1d4d7797..8c02991aa82c 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ManageGitIgnoreTest.php
@@ -7,7 +7,7 @@ namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
-use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait;
+use Drupal\Tests\Composer\Plugin\ExecTrait;
use PHPUnit\Framework\TestCase;
/**
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php
index accd108bfec6..f85732afb605 100644
--- a/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Scaffold/Functional/ScaffoldUpgradeTest.php
@@ -6,7 +6,7 @@ namespace Drupal\Tests\Composer\Plugin\Scaffold\Functional;
use Composer\Util\Filesystem;
use Drupal\Tests\Composer\Plugin\Scaffold\AssertUtilsTrait;
-use Drupal\Tests\Composer\Plugin\Scaffold\ExecTrait;
+use Drupal\Tests\Composer\Plugin\ExecTrait;
use Drupal\Tests\Composer\Plugin\Scaffold\Fixtures;
use PHPUnit\Framework\TestCase;
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/Fixtures.php b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/Fixtures.php
new file mode 100644
index 000000000000..a9acd97a14d5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/Fixtures.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Composer\Plugin\Unpack;
+
+use Drupal\Tests\Composer\Plugin\FixturesBase;
+
+/**
+ * Fixture for testing the unpack composer plugin.
+ */
+class Fixtures extends FixturesBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function projectRoot(): string {
+ return realpath(__DIR__) . '/../../../../../../../composer/Plugin/RecipeUnpack';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function allFixturesDir(): string {
+ return realpath(__DIR__ . '/fixtures');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function tmpDir(string $prefix): string {
+ $prefix .= static::persistentPrefix();
+ $tmpDir = sys_get_temp_dir() . '/unpack-' . $prefix . uniqid(md5($prefix . microtime()), TRUE);
+ $this->tmpDirs[] = $tmpDir;
+ return $tmpDir;
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/SemVerTest.php b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/SemVerTest.php
new file mode 100644
index 000000000000..acaec43ae5f7
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/SemVerTest.php
@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Composer\Plugin\Unpack;
+
+use Composer\Semver\VersionParser;
+use Drupal\Composer\Plugin\RecipeUnpack\SemVer;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Composer\Plugin\RecipeUnpack\SemVer
+ *
+ * @group Unpack
+ */
+class SemVerTest extends TestCase {
+
+ /**
+ * @testWith ["^6.1", "^6.3", "^6.3"]
+ * ["*", "^6.3", "^6.3"]
+ * ["^6@dev", "^6.3", "^6.3"]
+ *
+ * @covers ::minimizeConstraints
+ */
+ public function testMinimizeConstraints(string $constraint_a, string $constraint_b, string $expected): void {
+ $version_parser = new VersionParser();
+ $this->assertSame($expected, SemVer::minimizeConstraints($version_parser, $constraint_a, $constraint_b));
+ $this->assertSame($expected, SemVer::minimizeConstraints($version_parser, $constraint_b, $constraint_a));
+ }
+
+ /**
+ * @testWith ["^6.1 || ^4.0", "^6.3 || ^7.4", ">=6.3.0.0-dev, <7.0.0.0-dev"]
+ *
+ * @covers ::minimizeConstraints
+ */
+ public function testMinimizeConstraintsWhichAreNotSubsets(string $constraint_a, string $constraint_b, string $expected): void {
+ $this->assertSame($expected, SemVer::minimizeConstraints(new VersionParser(), $constraint_a, $constraint_b));
+ }
+
+ /**
+ * @testWith ["^6.1", "^5.1", ">=6.3.0.0-dev, <7.0.0.0-dev"]
+ *
+ * @covers ::minimizeConstraints
+ */
+ public function testMinimizeConstraintsWhichDoNotIntersect(string $constraint_a, string $constraint_b, string $expected): void {
+ $this->expectException(\LogicException::class);
+ $this->expectExceptionMessage('The constraints "^6.1" and "^5.1" do not intersect and cannot be minimized.');
+ $this->assertSame($expected, SemVer::minimizeConstraints(new VersionParser(), $constraint_a, $constraint_b));
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/composer-root/composer.json.tmpl b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/composer-root/composer.json.tmpl
new file mode 100644
index 000000000000..a5b81535f77b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/composer-root/composer.json.tmpl
@@ -0,0 +1,97 @@
+{
+ "name": "fixtures/root",
+ "description": "Test recipe unpacking",
+ "type": "project",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "repositories": {
+ "packagist.org": false,
+ "core-recipe-unpack": {
+ "type": "path",
+ "url": "__PROJECT_ROOT__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "composer/installers": {
+ "type": "path",
+ "url": "__COMPOSER_INSTALLERS__",
+ "options": {
+ "symlink": true
+ }
+ },
+ "recipes/recipe-a": {
+ "type": "path",
+ "url": "../recipes/composer-recipe-a",
+ "options": {
+ "symlink": true
+ }
+ },
+ "recipes/recipe-b": {
+ "type": "path",
+ "url": "../recipes/composer-recipe-b",
+ "options": {
+ "symlink": true
+ }
+ },
+ "recipes/recipe-c": {
+ "type": "path",
+ "url": "../recipes/composer-recipe-c",
+ "options": {
+ "symlink": true
+ }
+ },
+ "recipes/recipe-d": {
+ "type": "path",
+ "url": "../recipes/composer-recipe-d",
+ "options": {
+ "symlink": true
+ }
+ },
+ "modules/module-a": {
+ "type": "path",
+ "url": "../modules/composer-module-a",
+ "options": {
+ "symlink": true
+ }
+ },
+ "modules/module-b": {
+ "type": "path",
+ "url": "../modules/composer-module-b",
+ "options": {
+ "symlink": true
+ }
+ },
+ "themes/theme-a": {
+ "type": "path",
+ "url": "../themes/composer-theme-a",
+ "options": {
+ "symlink": true
+ }
+ }
+ },
+ "require": {
+ "composer/installers": "*",
+ "drupal/core-recipe-unpack": "*"
+ },
+ "extra": {
+ "installer-paths": {
+ "core": ["type:drupal-core"],
+ "modules/contrib/{$name}": ["type:drupal-module"],
+ "modules/custom/{$name}": ["type:drupal-custom-module"],
+ "themes/contrib/{$name}": ["type:drupal-theme"],
+ "themes/custom/{$name}": ["type:drupal-custom-theme"],
+ "recipes/{$name}": ["type:drupal-recipe"],
+ "profiles/contrib/{$name}": ["type:drupal-profile"],
+ "profiles/custom/{$name}": ["type:drupal-custom-profile"],
+ "libraries/{$name}": ["type:drupal-library"],
+ "drush/Commands/contrib/{$name}": ["type:drupal-drush"]
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "composer/installers": true,
+ "drupal/core-recipe-unpack": true
+ }
+ }
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-a/composer.json b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-a/composer.json
new file mode 100644
index 000000000000..0bc34f28fdc2
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-a/composer.json
@@ -0,0 +1,6 @@
+{
+ "name": "fixtures/module-a",
+ "version": "1.0.4",
+ "type": "drupal-module",
+ "description": "A Drupal module's composer for testing."
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-b/composer.json b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-b/composer.json
new file mode 100644
index 000000000000..1085192e4a84
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/modules/composer-module-b/composer.json
@@ -0,0 +1,6 @@
+{
+ "name": "fixtures/module-b",
+ "version": "2.0.1",
+ "type": "drupal-module",
+ "description": "A Drupal module's composer for testing."
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/composer.json b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/composer.json
new file mode 100644
index 000000000000..240f7848638d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/composer.json
@@ -0,0 +1,9 @@
+{
+ "name": "fixtures/recipe-a",
+ "type": "drupal-recipe",
+ "description": "A Drupal recipe's composer for testing.",
+ "require": {
+ "fixtures/recipe-b": "*",
+ "fixtures/module-b": "^2.0"
+ }
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/recipe.yml b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/recipe.yml
new file mode 100644
index 000000000000..94a93c054ef1
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-a/recipe.yml
@@ -0,0 +1,3 @@
+name: 'RecipeA'
+description: 'Recipe A.'
+type: 'test recipe'
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/composer.json b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/composer.json
new file mode 100644
index 000000000000..673d8ea86c46
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/composer.json
@@ -0,0 +1,9 @@
+{
+ "name": "fixtures/recipe-b",
+ "type": "drupal-recipe",
+ "description": "A Drupal recipe's composer for testing.",
+ "require": {
+ "fixtures/module-a": "*",
+ "fixtures/theme-a": "*"
+ }
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/recipe.yml b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/recipe.yml
new file mode 100644
index 000000000000..8eb3f822c3fd
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-b/recipe.yml
@@ -0,0 +1,3 @@
+name: 'RecipeB'
+description: 'Recipe B.'
+type: 'test recipe'
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/composer.json b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/composer.json
new file mode 100644
index 000000000000..692a54431643
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/composer.json
@@ -0,0 +1,9 @@
+{
+ "name": "fixtures/recipe-c",
+ "type": "drupal-recipe",
+ "description": "A Drupal recipe's composer for testing.",
+ "require": {
+ "fixtures/recipe-b": "*",
+ "fixtures/module-b": ">=2.0.1 || 1.1.1"
+ }
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/recipe.yml b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/recipe.yml
new file mode 100644
index 000000000000..d4ce9e245f88
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-c/recipe.yml
@@ -0,0 +1,3 @@
+name: 'RecipeC'
+description: 'Recipe C.'
+type: 'test recipe'
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/composer.json b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/composer.json
new file mode 100644
index 000000000000..9c349b32cb1b
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/composer.json
@@ -0,0 +1,5 @@
+{
+ "name": "fixtures/recipe-d",
+ "type": "drupal-recipe",
+ "description": "A Drupal recipe's composer for testing."
+}
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/recipe.yml b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/recipe.yml
new file mode 100644
index 000000000000..bd7037d00c10
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/recipes/composer-recipe-d/recipe.yml
@@ -0,0 +1,3 @@
+name: 'RecipeD'
+description: 'Recipe D.'
+type: 'test recipe'
diff --git a/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/themes/composer-theme-a/composer.json b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/themes/composer-theme-a/composer.json
new file mode 100644
index 000000000000..ad45c1227cc5
--- /dev/null
+++ b/core/tests/Drupal/Tests/Composer/Plugin/Unpack/fixtures/themes/composer-theme-a/composer.json
@@ -0,0 +1,5 @@
+{
+ "name": "fixtures/theme-a",
+ "type": "drupal-theme",
+ "description": "A Drupal theme's composer for testing."
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php b/core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php
new file mode 100644
index 000000000000..c3116c6f4988
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/AccessGroupAndTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Access;
+
+use Drupal\Core\Access\AccessGroupAnd;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests accessible groups.
+ *
+ * @group Access
+ */
+class AccessGroupAndTest extends UnitTestCase {
+
+ use AccessibleTestingTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->account = $this->prophesize(AccountInterface::class)->reveal();
+ }
+
+ /**
+ * @covers \Drupal\Core\Access\AccessGroupAnd
+ */
+ public function testGroups(): void {
+ $allowedAccessible = $this->createAccessibleDouble(AccessResult::allowed());
+ $forbiddenAccessible = $this->createAccessibleDouble(AccessResult::forbidden());
+ $neutralAccessible = $this->createAccessibleDouble(AccessResult::neutral());
+
+ // Ensure that groups with no dependencies return a neutral access result.
+ $this->assertTrue((new AccessGroupAnd())->access('view', $this->account, TRUE)->isNeutral());
+
+ $andNeutral = new AccessGroupAnd();
+ $andNeutral->addDependency($allowedAccessible)->addDependency($neutralAccessible);
+ $this->assertTrue($andNeutral->access('view', $this->account, TRUE)->isNeutral());
+
+ $andForbidden = $andNeutral;
+ $andForbidden->addDependency($forbiddenAccessible);
+ $this->assertTrue($andForbidden->access('view', $this->account, TRUE)->isForbidden());
+
+ // Ensure that groups added to other groups works.
+ $andGroupsForbidden = new AccessGroupAnd();
+ $andGroupsForbidden->addDependency($andNeutral)->addDependency($andForbidden);
+ $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden());
+ // Ensure you can add a non-group accessible object.
+ $andGroupsForbidden->addDependency($allowedAccessible);
+ $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden());
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php b/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php
new file mode 100644
index 000000000000..cf6d663a91a7
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/AccessibleTestingTrait.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Access;
+
+use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Access\AccessResultInterface;
+
+/**
+ * Helper methods testing accessible interfaces.
+ */
+trait AccessibleTestingTrait {
+
+ /**
+ * The test account.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $account;
+
+ /**
+ * Creates AccessibleInterface object from access result object for testing.
+ *
+ * @param \Drupal\Core\Access\AccessResultInterface $accessResult
+ * The accessible result to return.
+ *
+ * @return \Drupal\Core\Access\AccessibleInterface
+ * The AccessibleInterface object.
+ */
+ private function createAccessibleDouble(AccessResultInterface $accessResult) {
+ $accessible = $this->prophesize(AccessibleInterface::class);
+ $accessible->access('view', $this->account, TRUE)
+ ->willReturn($accessResult);
+ return $accessible->reveal();
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php b/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php
new file mode 100644
index 000000000000..43e924087ab3
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Access/DependentAccessTest.php
@@ -0,0 +1,161 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Access;
+
+use Drupal\Core\Access\AccessGroupAnd;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\RefinableDependentAccessInterface;
+use Drupal\Core\Access\RefinableDependentAccessTrait;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Access\RefinableDependentAccessTrait
+ *
+ * @group Access
+ */
+class DependentAccessTest extends UnitTestCase {
+ use AccessibleTestingTrait;
+
+ /**
+ * An accessible object that results in forbidden access result.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $forbidden;
+
+ /**
+ * An accessible object that results in neutral access result.
+ *
+ * @var \Drupal\Core\Access\AccessibleInterface
+ */
+ protected $neutral;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->account = $this->prophesize(AccountInterface::class)->reveal();
+ $this->forbidden = $this->createAccessibleDouble(AccessResult::forbidden('Because I said so'));
+ $this->neutral = $this->createAccessibleDouble(AccessResult::neutral('I have no opinion'));
+ }
+
+ /**
+ * Tests that the previous dependency is replaced when using set.
+ *
+ * @covers ::setAccessDependency
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testSetAccessDependency($use_set_first): void {
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($this->forbidden);
+ }
+ else {
+ $testRefinable->addAccessDependency($this->forbidden);
+ }
+ $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isForbidden());
+ $this->assertEquals('Because I said so', $accessResult->getReason());
+
+ // Calling setAccessDependency() replaces the existing dependency.
+ $testRefinable->setAccessDependency($this->neutral);
+ $dependency = $testRefinable->getAccessDependency();
+ $this->assertNotInstanceOf(AccessGroupAnd::class, $dependency);
+ $accessResult = $dependency->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResult->getReason());
+ }
+
+ /**
+ * Tests merging a new dependency with existing non-group access dependency.
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testMergeNonGroup($use_set_first): void {
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($this->forbidden);
+ }
+ else {
+ $testRefinable->addAccessDependency($this->forbidden);
+ }
+
+ $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResult->isForbidden());
+ $this->assertEquals('Because I said so', $accessResult->getReason());
+
+ $testRefinable->addAccessDependency($this->neutral);
+ /** @var \Drupal\Core\Access\AccessGroupAnd $dependency */
+ $dependency = $testRefinable->getAccessDependency();
+ // Ensure the new dependency create a new AND group when merged.
+ $this->assertInstanceOf(AccessGroupAnd::class, $dependency);
+ $dependencies = $dependency->getDependencies();
+ $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultForbidden->isForbidden());
+ $this->assertEquals('Because I said so', $accessResultForbidden->getReason());
+ $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultNeutral->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResultNeutral->getReason());
+ }
+
+ /**
+ * Tests merging a new dependency with an existing access group dependency.
+ *
+ * @dataProvider providerTestSetFirst
+ */
+ public function testMergeGroup($use_set_first): void {
+ $andGroup = new AccessGroupAnd();
+ $andGroup->addDependency($this->forbidden);
+ $testRefinable = new RefinableDependentAccessTraitTestClass();
+ if ($use_set_first) {
+ $testRefinable->setAccessDependency($andGroup);
+ }
+ else {
+ $testRefinable->addAccessDependency($andGroup);
+ }
+
+ $testRefinable->addAccessDependency($this->neutral);
+ /** @var \Drupal\Core\Access\AccessGroupAnd $dependency */
+ $dependency = $testRefinable->getAccessDependency();
+
+ // Ensure the new dependency is merged with the existing group.
+ $this->assertInstanceOf(AccessGroupAnd::class, $dependency);
+ $dependencies = $dependency->getDependencies();
+ $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultForbidden->isForbidden());
+ $this->assertEquals('Because I said so', $accessResultForbidden->getReason());
+ $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE);
+ $this->assertTrue($accessResultNeutral->isNeutral());
+ $this->assertEquals('I have no opinion', $accessResultNeutral->getReason());
+ }
+
+ /**
+ * Data provider for all test methods.
+ *
+ * Provides test cases for calling setAccessDependency() or
+ * mergeAccessDependency() first. A call to either should behave the same on a
+ * new RefinableDependentAccessInterface object.
+ */
+ public static function providerTestSetFirst(): array {
+ return [
+ [TRUE],
+ [FALSE],
+ ];
+ }
+
+}
+
+/**
+ * Test class that implements RefinableDependentAccessInterface.
+ */
+class RefinableDependentAccessTraitTestClass implements RefinableDependentAccessInterface {
+
+ use RefinableDependentAccessTrait;
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
index ab5fce6d191c..9dd5727f2a4b 100644
--- a/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
+++ b/core/tests/Drupal/Tests/Core/Ajax/AjaxResponseTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Ajax;
use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CommandInterface;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\HttpFoundation\Request;
@@ -98,4 +99,95 @@ class AjaxResponseTest extends UnitTestCase {
$this->assertEquals('<textarea>[]</textarea>', $response->getContent());
}
+ /**
+ * Tests the mergeWith() method.
+ *
+ * @see \Drupal\Core\Ajax\AjaxResponse::mergeWith()
+ *
+ * @throws \PHPUnit\Framework\MockObject\Exception
+ */
+ public function testMergeWithOtherAjaxResponse(): void {
+ $response = new AjaxResponse([]);
+
+ $command_one = $this->createCommandMock('one');
+
+ $command_two = $this->createCommandMockWithSettingsAndLibrariesAttachments(
+ 'Drupal\Core\Ajax\HtmlCommand', [
+ 'setting1' => 'value1',
+ 'setting2' => 'value2',
+ ], ['jquery', 'drupal'], 'two');
+ $command_three = $this->createCommandMockWithSettingsAndLibrariesAttachments(
+ 'Drupal\Core\Ajax\InsertCommand', [
+ 'setting1' => 'overridden',
+ 'setting3' => 'value3',
+ ], ['jquery', 'ajax'], 'three');
+
+ $response->addCommand($command_one);
+ $response->addCommand($command_two);
+
+ $response2 = new AjaxResponse([]);
+ $response2->addCommand($command_three);
+
+ $response->mergeWith($response2);
+ self::assertEquals([
+ 'library' => ['jquery', 'drupal', 'jquery', 'ajax'],
+ 'drupalSettings' => [
+ 'setting1' => 'overridden',
+ 'setting2' => 'value2',
+ 'setting3' => 'value3',
+ ],
+ ], $response->getAttachments());
+ self::assertEquals([['command' => 'one'], ['command' => 'two'], ['command' => 'three']], $response->getCommands());
+ }
+
+ /**
+ * Creates a mock of a provided subclass of CommandInterface.
+ *
+ * Adds given settings and libraries to assets mock
+ * that is attached to the command mock.
+ *
+ * @param string $command_class_name
+ * The command class name to create the mock for.
+ * @param array|null $settings
+ * The settings to attach.
+ * @param array|null $libraries
+ * The libraries to attach.
+ * @param string $command_name
+ * The command name to pass to the mock.
+ */
+ private function createCommandMockWithSettingsAndLibrariesAttachments(
+ string $command_class_name,
+ array|null $settings,
+ array|null $libraries,
+ string $command_name,
+ ): CommandInterface {
+ $command = $this->createMock($command_class_name);
+ $command->expects($this->once())
+ ->method('render')
+ ->willReturn(['command' => $command_name]);
+
+ $assets = $this->createMock('Drupal\Core\Asset\AttachedAssetsInterface');
+ $assets->expects($this->once())->method('getLibraries')->willReturn($libraries);
+ $assets->expects($this->once())->method('getSettings')->willReturn($settings);
+
+ $command->expects($this->once())->method('getAttachedAssets')->willReturn($assets);
+
+ return $command;
+ }
+
+ /**
+ * Creates a mock of the Drupal\Core\Ajax\CommandInterface.
+ *
+ * @param string $command_name
+ * The command name to pass to the mock.
+ */
+ private function createCommandMock(string $command_name): CommandInterface {
+ $command = $this->createMock('Drupal\Core\Ajax\CommandInterface');
+ $command->expects($this->once())
+ ->method('render')
+ ->willReturn(['command' => $command_name]);
+
+ return $command;
+ }
+
}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
index 798735a2c8a1..9dc1a0d113ee 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionOptimizerLazyUnitTest.php
@@ -148,4 +148,40 @@ class CssCollectionOptimizerLazyUnitTest extends UnitTestCase {
self::assertStringEqualsFile(__DIR__ . '/css_test_files/css_license.css.optimized.aggregated.css', $aggregate);
}
+ /**
+ * Test that external minified CSS assets do not trigger optimization.
+ *
+ * This ensures that fully external asset groups do not result in a
+ * CssOptimizer exception and are safely ignored.
+ */
+ public function testExternalMinifiedCssAssetOptimizationIsSkipped(): void {
+ $mock_grouper = $this->createMock(AssetCollectionGrouperInterface::class);
+ $mock_optimizer = $this->createMock(AssetOptimizerInterface::class);
+ $mock_optimizer->expects($this->never())->method('optimize');
+
+ $optimizer = new CssCollectionOptimizerLazy(
+ $mock_grouper,
+ $mock_optimizer,
+ $this->createMock(ThemeManagerInterface::class),
+ $this->createMock(LibraryDependencyResolverInterface::class),
+ new RequestStack(),
+ $this->createMock(FileSystemInterface::class),
+ $this->createMock(ConfigFactoryInterface::class),
+ $this->createMock(FileUrlGeneratorInterface::class),
+ $this->createMock(TimeInterface::class),
+ $this->createMock(LanguageManagerInterface::class)
+ );
+ $optimizer->optimizeGroup([
+ 'items' => [
+ [
+ 'type' => 'external',
+ 'data' => __DIR__ . '/css_test_files/css_external.optimized.aggregated.css',
+ 'license' => FALSE,
+ 'preprocess' => TRUE,
+ 'minified' => TRUE,
+ ],
+ ],
+ ]);
+ }
+
}
diff --git a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
index 7a75824bef44..0b26b97aae5f 100644
--- a/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/CssCollectionRendererUnitTest.php
@@ -117,7 +117,7 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
0 => ['group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all'],
],
[
- 0 => $create_link_element('generated-relative-url:public://css/file-all' . '?', 'all'),
+ 0 => $create_link_element('generated-relative-url:public://css/file-all?', 'all'),
],
],
// Single file CSS asset with custom attributes.
@@ -126,7 +126,7 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
0 => ['group' => 0, 'type' => 'file', 'media' => 'all', 'preprocess' => TRUE, 'data' => 'public://css/file-all', 'attributes' => $custom_attributes],
],
[
- 0 => $create_link_element('generated-relative-url:public://css/file-all' . '?', 'all', $custom_attributes),
+ 0 => $create_link_element('generated-relative-url:public://css/file-all?', 'all', $custom_attributes),
],
],
// 31 file CSS assets: expect 31 link elements.
@@ -165,37 +165,37 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
30 => $create_file_css_asset('public://css/31.css'),
],
[
- 0 => $create_link_element('generated-relative-url:public://css/1.css' . '?'),
- 1 => $create_link_element('generated-relative-url:public://css/2.css' . '?'),
- 2 => $create_link_element('generated-relative-url:public://css/3.css' . '?'),
- 3 => $create_link_element('generated-relative-url:public://css/4.css' . '?'),
- 4 => $create_link_element('generated-relative-url:public://css/5.css' . '?'),
- 5 => $create_link_element('generated-relative-url:public://css/6.css' . '?'),
- 6 => $create_link_element('generated-relative-url:public://css/7.css' . '?'),
- 7 => $create_link_element('generated-relative-url:public://css/8.css' . '?'),
- 8 => $create_link_element('generated-relative-url:public://css/9.css' . '?'),
- 9 => $create_link_element('generated-relative-url:public://css/10.css' . '?'),
- 10 => $create_link_element('generated-relative-url:public://css/11.css' . '?'),
- 11 => $create_link_element('generated-relative-url:public://css/12.css' . '?'),
- 12 => $create_link_element('generated-relative-url:public://css/13.css' . '?'),
- 13 => $create_link_element('generated-relative-url:public://css/14.css' . '?'),
- 14 => $create_link_element('generated-relative-url:public://css/15.css' . '?'),
- 15 => $create_link_element('generated-relative-url:public://css/16.css' . '?'),
- 16 => $create_link_element('generated-relative-url:public://css/17.css' . '?'),
- 17 => $create_link_element('generated-relative-url:public://css/18.css' . '?'),
- 18 => $create_link_element('generated-relative-url:public://css/19.css' . '?'),
- 19 => $create_link_element('generated-relative-url:public://css/20.css' . '?'),
- 20 => $create_link_element('generated-relative-url:public://css/21.css' . '?'),
- 21 => $create_link_element('generated-relative-url:public://css/22.css' . '?'),
- 22 => $create_link_element('generated-relative-url:public://css/23.css' . '?'),
- 23 => $create_link_element('generated-relative-url:public://css/24.css' . '?'),
- 24 => $create_link_element('generated-relative-url:public://css/25.css' . '?'),
- 25 => $create_link_element('generated-relative-url:public://css/26.css' . '?'),
- 26 => $create_link_element('generated-relative-url:public://css/27.css' . '?'),
- 27 => $create_link_element('generated-relative-url:public://css/28.css' . '?'),
- 28 => $create_link_element('generated-relative-url:public://css/29.css' . '?'),
- 29 => $create_link_element('generated-relative-url:public://css/30.css' . '?'),
- 30 => $create_link_element('generated-relative-url:public://css/31.css' . '?'),
+ 0 => $create_link_element('generated-relative-url:public://css/1.css?'),
+ 1 => $create_link_element('generated-relative-url:public://css/2.css?'),
+ 2 => $create_link_element('generated-relative-url:public://css/3.css?'),
+ 3 => $create_link_element('generated-relative-url:public://css/4.css?'),
+ 4 => $create_link_element('generated-relative-url:public://css/5.css?'),
+ 5 => $create_link_element('generated-relative-url:public://css/6.css?'),
+ 6 => $create_link_element('generated-relative-url:public://css/7.css?'),
+ 7 => $create_link_element('generated-relative-url:public://css/8.css?'),
+ 8 => $create_link_element('generated-relative-url:public://css/9.css?'),
+ 9 => $create_link_element('generated-relative-url:public://css/10.css?'),
+ 10 => $create_link_element('generated-relative-url:public://css/11.css?'),
+ 11 => $create_link_element('generated-relative-url:public://css/12.css?'),
+ 12 => $create_link_element('generated-relative-url:public://css/13.css?'),
+ 13 => $create_link_element('generated-relative-url:public://css/14.css?'),
+ 14 => $create_link_element('generated-relative-url:public://css/15.css?'),
+ 15 => $create_link_element('generated-relative-url:public://css/16.css?'),
+ 16 => $create_link_element('generated-relative-url:public://css/17.css?'),
+ 17 => $create_link_element('generated-relative-url:public://css/18.css?'),
+ 18 => $create_link_element('generated-relative-url:public://css/19.css?'),
+ 19 => $create_link_element('generated-relative-url:public://css/20.css?'),
+ 20 => $create_link_element('generated-relative-url:public://css/21.css?'),
+ 21 => $create_link_element('generated-relative-url:public://css/22.css?'),
+ 22 => $create_link_element('generated-relative-url:public://css/23.css?'),
+ 23 => $create_link_element('generated-relative-url:public://css/24.css?'),
+ 24 => $create_link_element('generated-relative-url:public://css/25.css?'),
+ 25 => $create_link_element('generated-relative-url:public://css/26.css?'),
+ 26 => $create_link_element('generated-relative-url:public://css/27.css?'),
+ 27 => $create_link_element('generated-relative-url:public://css/28.css?'),
+ 28 => $create_link_element('generated-relative-url:public://css/29.css?'),
+ 29 => $create_link_element('generated-relative-url:public://css/30.css?'),
+ 30 => $create_link_element('generated-relative-url:public://css/31.css?'),
],
],
// 32 file CSS assets with the same properties, except for the 10th and
@@ -236,38 +236,38 @@ class CssCollectionRendererUnitTest extends UnitTestCase {
31 => $create_file_css_asset('public://css/32.css'),
],
[
- 0 => $create_link_element('generated-relative-url:public://css/1.css' . '?'),
- 1 => $create_link_element('generated-relative-url:public://css/2.css' . '?'),
- 2 => $create_link_element('generated-relative-url:public://css/3.css' . '?'),
- 3 => $create_link_element('generated-relative-url:public://css/4.css' . '?'),
- 4 => $create_link_element('generated-relative-url:public://css/5.css' . '?'),
- 5 => $create_link_element('generated-relative-url:public://css/6.css' . '?'),
- 6 => $create_link_element('generated-relative-url:public://css/7.css' . '?'),
- 7 => $create_link_element('generated-relative-url:public://css/8.css' . '?'),
- 8 => $create_link_element('generated-relative-url:public://css/9.css' . '?'),
- 9 => $create_link_element('generated-relative-url:public://css/10.css' . '?', 'screen'),
- 10 => $create_link_element('generated-relative-url:public://css/11.css' . '?'),
- 11 => $create_link_element('generated-relative-url:public://css/12.css' . '?'),
- 12 => $create_link_element('generated-relative-url:public://css/13.css' . '?'),
- 13 => $create_link_element('generated-relative-url:public://css/14.css' . '?'),
- 14 => $create_link_element('generated-relative-url:public://css/15.css' . '?'),
- 15 => $create_link_element('generated-relative-url:public://css/16.css' . '?'),
- 16 => $create_link_element('generated-relative-url:public://css/17.css' . '?'),
- 17 => $create_link_element('generated-relative-url:public://css/18.css' . '?'),
- 18 => $create_link_element('generated-relative-url:public://css/19.css' . '?'),
- 19 => $create_link_element('generated-relative-url:public://css/20.css' . '?', 'print'),
- 20 => $create_link_element('generated-relative-url:public://css/21.css' . '?'),
- 21 => $create_link_element('generated-relative-url:public://css/22.css' . '?'),
- 22 => $create_link_element('generated-relative-url:public://css/23.css' . '?'),
- 23 => $create_link_element('generated-relative-url:public://css/24.css' . '?'),
- 24 => $create_link_element('generated-relative-url:public://css/25.css' . '?'),
- 25 => $create_link_element('generated-relative-url:public://css/26.css' . '?'),
- 26 => $create_link_element('generated-relative-url:public://css/27.css' . '?'),
- 27 => $create_link_element('generated-relative-url:public://css/28.css' . '?'),
- 28 => $create_link_element('generated-relative-url:public://css/29.css' . '?'),
- 29 => $create_link_element('generated-relative-url:public://css/30.css' . '?'),
- 30 => $create_link_element('generated-relative-url:public://css/31.css' . '?'),
- 31 => $create_link_element('generated-relative-url:public://css/32.css' . '?'),
+ 0 => $create_link_element('generated-relative-url:public://css/1.css?'),
+ 1 => $create_link_element('generated-relative-url:public://css/2.css?'),
+ 2 => $create_link_element('generated-relative-url:public://css/3.css?'),
+ 3 => $create_link_element('generated-relative-url:public://css/4.css?'),
+ 4 => $create_link_element('generated-relative-url:public://css/5.css?'),
+ 5 => $create_link_element('generated-relative-url:public://css/6.css?'),
+ 6 => $create_link_element('generated-relative-url:public://css/7.css?'),
+ 7 => $create_link_element('generated-relative-url:public://css/8.css?'),
+ 8 => $create_link_element('generated-relative-url:public://css/9.css?'),
+ 9 => $create_link_element('generated-relative-url:public://css/10.css?', 'screen'),
+ 10 => $create_link_element('generated-relative-url:public://css/11.css?'),
+ 11 => $create_link_element('generated-relative-url:public://css/12.css?'),
+ 12 => $create_link_element('generated-relative-url:public://css/13.css?'),
+ 13 => $create_link_element('generated-relative-url:public://css/14.css?'),
+ 14 => $create_link_element('generated-relative-url:public://css/15.css?'),
+ 15 => $create_link_element('generated-relative-url:public://css/16.css?'),
+ 16 => $create_link_element('generated-relative-url:public://css/17.css?'),
+ 17 => $create_link_element('generated-relative-url:public://css/18.css?'),
+ 18 => $create_link_element('generated-relative-url:public://css/19.css?'),
+ 19 => $create_link_element('generated-relative-url:public://css/20.css?', 'print'),
+ 20 => $create_link_element('generated-relative-url:public://css/21.css?'),
+ 21 => $create_link_element('generated-relative-url:public://css/22.css?'),
+ 22 => $create_link_element('generated-relative-url:public://css/23.css?'),
+ 23 => $create_link_element('generated-relative-url:public://css/24.css?'),
+ 24 => $create_link_element('generated-relative-url:public://css/25.css?'),
+ 25 => $create_link_element('generated-relative-url:public://css/26.css?'),
+ 26 => $create_link_element('generated-relative-url:public://css/27.css?'),
+ 27 => $create_link_element('generated-relative-url:public://css/28.css?'),
+ 28 => $create_link_element('generated-relative-url:public://css/29.css?'),
+ 29 => $create_link_element('generated-relative-url:public://css/30.css?'),
+ 30 => $create_link_element('generated-relative-url:public://css/31.css?'),
+ 31 => $create_link_element('generated-relative-url:public://css/32.css?'),
],
],
];
diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
index 6b8df5044f27..9db6abea52c1 100644
--- a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
+++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php
@@ -87,7 +87,8 @@ class LibraryDiscoveryTest extends UnitTestCase {
* Tests getting a deprecated library.
*/
public function testAssetLibraryDeprecation(): void {
- $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
+ $previous_error_handler = get_error_handler();
+ set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) {
// Convert deprecation error into a catchable exception.
if ($severity === E_USER_DEPRECATED) {
throw new \ErrorException($message, 0, $severity, $file, $line);
diff --git a/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css b/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css
new file mode 100644
index 000000000000..dac82b6b80f6
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Asset/css_test_files/css_external.optimized.aggregated.css
@@ -0,0 +1 @@
+/* Placeholder external CSS file. */
diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
index 2b4d2990d52e..76f5cc118ae6 100644
--- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
@@ -172,25 +172,17 @@ class ConfigEntityStorageTest extends UnitTestCase {
*/
public function testCreateWithPredefinedUuid(): void {
$this->cacheTagsInvalidator->invalidateTags(Argument::cetera())->shouldNotBeCalled();
-
- $entity = $this->getMockEntity();
- $entity->set('id', 'foo');
- $entity->set('langcode', 'hu');
- $entity->set('uuid', 'baz');
- $entity->setOriginalId('foo');
- $entity->enforceIsNew();
-
- $this->moduleHandler->invokeAll('test_entity_type_create', [$entity])
- ->shouldBeCalled();
- $this->moduleHandler->invokeAll('entity_create', [$entity, 'test_entity_type'])
- ->shouldBeCalled();
-
$this->uuidService->generate()->shouldNotBeCalled();
$entity = $this->entityStorage->create(['id' => 'foo', 'uuid' => 'baz']);
$this->assertInstanceOf(EntityInterface::class, $entity);
$this->assertSame('foo', $entity->id());
$this->assertSame('baz', $entity->uuid());
+
+ $this->moduleHandler->invokeAll('test_entity_type_create', [$entity])
+ ->shouldBeCalled();
+ $this->moduleHandler->invokeAll('entity_create', [$entity, 'test_entity_type'])
+ ->shouldBeCalled();
}
/**
@@ -202,25 +194,18 @@ class ConfigEntityStorageTest extends UnitTestCase {
*/
public function testCreate() {
$this->cacheTagsInvalidator->invalidateTags(Argument::cetera())->shouldNotBeCalled();
+ $this->uuidService->generate()->willReturn('bar');
- $entity = $this->getMockEntity();
- $entity->set('id', 'foo');
- $entity->set('langcode', 'hu');
- $entity->set('uuid', 'bar');
- $entity->setOriginalId('foo');
- $entity->enforceIsNew();
+ $entity = $this->entityStorage->create(['id' => 'foo']);
+ $this->assertInstanceOf(EntityInterface::class, $entity);
+ $this->assertSame('foo', $entity->id());
+ $this->assertSame('bar', $entity->uuid());
$this->moduleHandler->invokeAll('test_entity_type_create', [$entity])
->shouldBeCalled();
$this->moduleHandler->invokeAll('entity_create', [$entity, 'test_entity_type'])
->shouldBeCalled();
- $this->uuidService->generate()->willReturn('bar');
-
- $entity = $this->entityStorage->create(['id' => 'foo']);
- $this->assertInstanceOf(EntityInterface::class, $entity);
- $this->assertSame('foo', $entity->id());
- $this->assertSame('bar', $entity->uuid());
return $entity;
}
diff --git a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
index f9b25e8e7072..16fde5781255 100644
--- a/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/ConnectionTest.php
@@ -910,7 +910,7 @@ class ConnectionTest extends UnitTestCase {
#[IgnoreDeprecations]
#[DataProvider('providerSupportedLegacyFetchModes')]
public function testSupportedLegacyFetchModes(int $mode): void {
- $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$mockPdo = $this->createMock(StubPDO::class);
$mockConnection = new StubConnection($mockPdo, []);
$statement = new StatementPrefetchIterator($mockPdo, $mockConnection, '');
@@ -921,7 +921,7 @@ class ConnectionTest extends UnitTestCase {
/**
* Provides data for testSupportedFetchModes.
*
- * @return array<string,array<\Drupal\Core\Database\FetchAs>>
+ * @return array<string,array<\Drupal\Core\Database\Statement\FetchAs>>
* The FetchAs cases.
*/
public static function providerSupportedFetchModes(): array {
@@ -975,7 +975,7 @@ class ConnectionTest extends UnitTestCase {
#[IgnoreDeprecations]
#[DataProvider('providerUnsupportedFetchModes')]
public function testUnsupportedFetchModes(int $mode): void {
- $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338");
+ $this->expectDeprecation("Passing the \$mode argument as an integer to setFetchMode() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338");
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessageMatches("/^Fetch mode FETCH_.* is not supported\\. Use supported modes only/");
$mockPdo = $this->createMock(StubPDO::class);
diff --git a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
index 7ecc4518d714..569a6e3fd18e 100644
--- a/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/RowCountExceptionTest.php
@@ -27,7 +27,7 @@ class RowCountExceptionTest extends UnitTestCase {
*/
public static function providerTestExceptionMessage() {
return [
- [static::DEFAULT_EXCEPTION_MESSAGE, ''],
+ [self::DEFAULT_EXCEPTION_MESSAGE, ''],
['test', 'test'],
];
}
@@ -47,7 +47,7 @@ class RowCountExceptionTest extends UnitTestCase {
*/
public function testExceptionMessageNull(): void {
$e = new RowCountException(NULL);
- $this->assertSame(static::DEFAULT_EXCEPTION_MESSAGE, $e->getMessage());
+ $this->assertSame(self::DEFAULT_EXCEPTION_MESSAGE, $e->getMessage());
}
}
diff --git a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
index e36767d2f5a4..c74090fb599b 100644
--- a/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
+++ b/core/tests/Drupal/Tests/Core/Database/UrlConversionTest.php
@@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Database;
use Drupal\Core\Database\Database;
use Drupal\Core\Extension\Exception\UnknownExtensionException;
use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
// cspell:ignore dummydb
@@ -31,7 +32,7 @@ class UrlConversionTest extends UnitTestCase {
* @dataProvider providerConvertDbUrlToConnectionInfo
*/
public function testDbUrlToConnectionConversion($url, $database_array, $include_test_drivers): void {
- $result = Database::convertDbUrlToConnectionInfo($url, $this->root, $include_test_drivers);
+ $result = Database::convertDbUrlToConnectionInfo($url, $include_test_drivers);
$this->assertEquals($database_array, $result);
}
@@ -279,10 +280,10 @@ class UrlConversionTest extends UnitTestCase {
*
* @dataProvider providerInvalidArgumentsUrlConversion
*/
- public function testGetInvalidArgumentExceptionInUrlConversion($url, $root, $expected_exception_message): void {
+ public function testGetInvalidArgumentExceptionInUrlConversion($url, $expected_exception_message): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage($expected_exception_message);
- Database::convertDbUrlToConnectionInfo($url, $root);
+ Database::convertDbUrlToConnectionInfo($url);
}
/**
@@ -291,14 +292,12 @@ class UrlConversionTest extends UnitTestCase {
* @return array
* Array of arrays with the following elements:
* - An invalid URL string.
- * - Drupal root string.
* - The expected exception message.
*/
public static function providerInvalidArgumentsUrlConversion() {
return [
- ['foo', '', "Missing scheme in URL 'foo'"],
- ['foo', 'bar', "Missing scheme in URL 'foo'"],
- ['foo/bar/baz', 'bar2', "Missing scheme in URL 'foo/bar/baz'"],
+ ['foo', "Missing scheme in URL 'foo'"],
+ ['foo/bar/baz', "Missing scheme in URL 'foo/bar/baz'"],
];
}
@@ -307,7 +306,7 @@ class UrlConversionTest extends UnitTestCase {
*/
public function testNoModuleSpecifiedDefaultsToDriverName(): void {
$url = 'dummydb://test_user:test_pass@test_host/test_database';
- $connection_info = Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ $connection_info = Database::convertDbUrlToConnectionInfo($url, TRUE);
$expected = [
'driver' => 'dummydb',
'username' => 'test_user',
@@ -518,7 +517,7 @@ class UrlConversionTest extends UnitTestCase {
$url = 'foo_bar_mysql://test_user:test_pass@test_host:3306/test_database?module=foo_bar';
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage("The database_driver Drupal\\foo_bar\\Driver\\Database\\foo_bar_mysql does not exist.");
- Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ Database::convertDbUrlToConnectionInfo($url, TRUE);
}
/**
@@ -528,7 +527,16 @@ class UrlConversionTest extends UnitTestCase {
$url = 'driver_test_mysql://test_user:test_pass@test_host:3306/test_database?module=driver_test';
$this->expectException(UnknownExtensionException::class);
$this->expectExceptionMessage("The database_driver Drupal\\driver_test\\Driver\\Database\\driver_test_mysql does not exist.");
- Database::convertDbUrlToConnectionInfo($url, $this->root, TRUE);
+ Database::convertDbUrlToConnectionInfo($url, TRUE);
+ }
+
+ /**
+ * @covers ::convertDbUrlToConnectionInfo
+ */
+ #[IgnoreDeprecations]
+ public function testDeprecationOfRootParameter(): void {
+ $this->expectDeprecation('Passing a string $root value to Drupal\\Core\\Database\\Database::convertDbUrlToConnectionInfo() is deprecated in drupal:11.3.0 and will be removed in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3511287');
+ Database::convertDbUrlToConnectionInfo('sqlite://localhost/test_database', $this->root, TRUE);
}
}
diff --git a/core/tests/Drupal/Tests/Core/Datetime/DrupalDateTimeTest.php b/core/tests/Drupal/Tests/Core/Datetime/DrupalDateTimeTest.php
index e1ec84395f9d..40b94bd046bf 100644
--- a/core/tests/Drupal/Tests/Core/Datetime/DrupalDateTimeTest.php
+++ b/core/tests/Drupal/Tests/Core/Datetime/DrupalDateTimeTest.php
@@ -84,16 +84,16 @@ class DrupalDateTimeTest extends UnitTestCase {
// There should be a 19 hour time interval between
// new years in Sydney and new years in LA in year 2000.
[
- 'input2' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney'), $settings),
- 'input1' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles'), $settings),
+ 'input1' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('Australia/Sydney'), $settings),
+ 'input2' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '2000-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles'), $settings),
'absolute' => FALSE,
'expected' => $positive_19_hours,
],
// In 1970 Sydney did not observe daylight savings time
// So there is only an 18 hour time interval.
[
- 'input2' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney'), $settings),
- 'input1' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles'), $settings),
+ 'input1' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('Australia/Sydney'), $settings),
+ 'input2' => DrupalDateTime::createFromFormat('Y-m-d H:i:s', '1970-01-01 00:00:00', new \DateTimeZone('America/Los_Angeles'), $settings),
'absolute' => FALSE,
'expected' => $positive_18_hours,
],
diff --git a/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php b/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php
index 6abfb5b67331..ad6f98c3cf78 100644
--- a/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php
+++ b/core/tests/Drupal/Tests/Core/DefaultContent/FinderTest.php
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\DefaultContent;
-use Drupal\Component\FileSystem\FileSystem;
use Drupal\Core\DefaultContent\Finder;
use Drupal\Core\DefaultContent\ImportException;
use Drupal\Tests\UnitTestCase;
@@ -38,14 +37,9 @@ class FinderTest extends UnitTestCase {
* Tests that files without UUIDs will raise an exception.
*/
public function testExceptionIfNoUuid(): void {
- $dir = FileSystem::getOsTemporaryDirectory();
- $this->assertIsString($dir);
- /** @var string $dir */
- file_put_contents($dir . '/no-uuid.yml', '_meta: {}');
-
$this->expectException(ImportException::class);
- $this->expectExceptionMessage("$dir/no-uuid.yml does not have a UUID.");
- new Finder($dir);
+ $this->expectExceptionMessageMatches("#/no-uuid\.yml does not have a UUID\.$#");
+ new Finder(__DIR__ . '/../../../../fixtures/default_content_broken');
}
}
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
index c910a3c99c77..7af0c490d074 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/BackendCompilerPassTest.php
@@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\DependencyInjection\Compiler;
+use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass;
use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
@@ -92,6 +94,16 @@ class BackendCompilerPassTest extends UnitTestCase {
$container->setDefinition('DriverTestMysql.service', new Definition(__NAMESPACE__ . '\\ServiceClassDriverTestMysql'));
$this->backendPass->process($container);
$this->assertEquals($prefix . 'DriverTestMysql', get_class($container->get('service')));
+
+ // Verify that if the container has a default_backend parameter,
+ // and there is a service named ".my-service", the right alias is created.
+ $container = $this->getMockDriverContainerWithDefaultBackendParameterArgumentAndDotPrefixedService();
+ $this->backendPass->process($container);
+
+ // Verify that if the db service returns no driver, no invalid aliases are
+ // created.
+ $container = $this->getMockDriverContainerWithNullDriverBackend();
+ $this->backendPass->process($container);
}
/**
@@ -154,6 +166,82 @@ class BackendCompilerPassTest extends UnitTestCase {
return $container;
}
+ /**
+ * Creates a container with a database mock definition in it.
+ *
+ * This mock won't declare a driver nor databaseType to ensure no invalid
+ * aliases are set.
+ *
+ * @return \Symfony\Component\DependencyInjection\ContainerBuilder
+ * The container with a mock database service in it.
+ */
+ protected function getMockDriverContainerWithNullDriverBackend(): ContainerBuilder&MockObject {
+ $container = $this->getMockBuilder(ContainerBuilder::class)->getMock();
+ $mock = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
+ $mock->expects($this->once())
+ ->method('driver')
+ ->willReturn(NULL);
+ $mock->expects($this->once())
+ ->method('databaseType')
+ ->willReturn(NULL);
+ $container->expects($this->any())
+ ->method('get')
+ ->with('database')
+ ->willReturn($mock);
+ $container->expects($this->once())
+ ->method('findTaggedServiceIds')
+ ->willReturn(['fakeService' => ['class' => 'fakeServiceClass']]);
+ $container->expects($this->never())
+ ->method('hasDefinition')
+ ->with('.fakeService')
+ ->willReturn(TRUE);
+ $container->expects($this->never())
+ ->method('setAlias');
+ return $container;
+ }
+
+ /**
+ * Creates a container with a database mock definition in it.
+ *
+ * This mock container has a default_backend parameter and a dot-prefixed
+ * service to verify the right aliases are set.
+ *
+ * @return \Symfony\Component\DependencyInjection\ContainerBuilder
+ * The container with a mock database service in it.
+ */
+ protected function getMockDriverContainerWithDefaultBackendParameterArgumentAndDotPrefixedService(): ContainerBuilder&MockObject {
+ $container = $this->getMockBuilder(ContainerBuilder::class)->getMock();
+ $container->expects($this->once())
+ ->method('hasParameter')
+ ->with('default_backend')
+ ->willReturn(TRUE);
+ $container->expects($this->once())
+ ->method('getParameter')
+ ->with('default_backend')
+ ->willReturn('a_valid_default_backend');
+
+ $mock = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
+ $mock->expects($this->never())
+ ->method('driver');
+ $mock->expects($this->never())
+ ->method('databaseType');
+ $container->expects($this->any())
+ ->method('get')
+ ->with('database')
+ ->willReturn($mock);
+ $container->expects($this->once())
+ ->method('findTaggedServiceIds')
+ ->willReturn(['fakeService' => ['class' => 'fakeServiceClass']]);
+ $container->expects($this->once())
+ ->method('hasDefinition')
+ ->with('a_valid_default_backend.fakeService')
+ ->willReturn(TRUE);
+ $container->expects($this->once())
+ ->method('setAlias')
+ ->with('fakeService', new Alias('a_valid_default_backend.fakeService'));
+ return $container;
+ }
+
}
/**
diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php
index 717026fe3f46..cbc46da7860f 100644
--- a/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php
+++ b/core/tests/Drupal/Tests/Core/DependencyInjection/YamlFileLoaderTest.php
@@ -40,10 +40,10 @@ services:
Drupal\Core\ExampleClass: ~
example_tagged_iterator:
class: \Drupal\Core\ExampleClass
- arguments: [!tagged_iterator foo.bar]"
+ arguments: [!tagged_iterator foo.bar]
example_service_closure:
class: \Drupal\Core\ExampleClass
- arguments: [!service_closure '@example_service_1']"
+ arguments: [!service_closure '@example_service_1']
YAML;
vfsStream::setup('drupal', NULL, [
diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php
new file mode 100644
index 000000000000..29d3d17dba0f
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Entity/EntityViewBuilderTest.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Entity;
+
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityViewBuilder;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Entity\EntityViewBuilder
+ * @group Entity
+ */
+class EntityViewBuilderTest extends UnitTestCase {
+
+ const string ENTITY_TYPE_ID = 'test_entity_type';
+
+ /**
+ * The entity view builder under test.
+ *
+ * @var \Drupal\Core\Entity\EntityViewBuilder
+ */
+ protected EntityViewBuilder $viewBuilder;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->viewBuilder = new class() extends EntityViewBuilder {
+
+ public function __construct() {
+ $this->entityTypeId = EntityViewBuilderTest::ENTITY_TYPE_ID;
+ }
+
+ };
+ }
+
+ /**
+ * Tests build components using a mocked Iterator.
+ */
+ public function testBuildComponents(): void {
+ $field_name = $this->randomMachineName();
+ $bundle = $this->randomMachineName();
+ $entity_id = mt_rand(20, 30);
+ $field_item_list = $this->createStub(FieldItemListInterface::class);
+ $item = new \stdClass();
+ $this->setupMockIterator($field_item_list, [$item]);
+ $entity = $this->createConfiguredStub(FieldableEntityInterface::class, [
+ 'bundle' => $bundle,
+ 'hasField' => TRUE,
+ 'get' => $field_item_list,
+ ]);
+ $formatter_result = [
+ $entity_id => ['#' . $this->randomMachineName() => $this->randomString()],
+ ];
+ $display = $this->createConfiguredStub(EntityViewDisplayInterface::class, [
+ 'getComponents' => [$field_name => []],
+ 'buildMultiple' => $formatter_result,
+ ]);
+ $entities = [$entity_id => $entity];
+ $displays = [$bundle => $display];
+ $build = [$entity_id => []];
+ $view_mode = $this->randomMachineName();
+ // Assert the hook is invoked.
+ $module_handler = $this->createMock(ModuleHandlerInterface::class);
+ $module_handler->expects($this->once())
+ ->method('invokeAll')
+ ->with('entity_prepare_view', [self::ENTITY_TYPE_ID, $entities, $displays, $view_mode]);
+ $this->viewBuilder->setModuleHandler($module_handler);
+ $this->viewBuilder->buildComponents($build, $entities, $displays, $view_mode);
+ $this->assertSame([], $item->_attributes);
+ $this->assertSame($formatter_result, $build);
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php
index f57a80d1393c..2381b64b83a5 100644
--- a/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/KeyValueStore/KeyValueEntityStorageTest.php
@@ -206,11 +206,8 @@ class KeyValueEntityStorageTest extends UnitTestCase {
/**
* @covers ::create
* @covers ::doCreate
- *
- * @return \Drupal\Core\Entity\EntityInterface
- * The newly created entity instance with the specified ID and generated UUID.
*/
- public function testCreate() {
+ public function testCreate(): void {
$entity = $this->getMockEntity(EntityBaseTest::class, [], ['toArray']);
$this->entityType->expects($this->once())
->method('getClass')
@@ -231,24 +228,18 @@ class KeyValueEntityStorageTest extends UnitTestCase {
$this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity);
$this->assertSame('foo', $entity->id());
$this->assertSame('bar', $entity->uuid());
- return $entity;
}
/**
* @covers ::save
* @covers ::doSave
- *
- * @param \Drupal\Core\Entity\EntityInterface $entity
- * The entity.
- *
- * @return \Drupal\Core\Entity\EntityInterface
- * The saved entity instance after insertion.
- *
- * @depends testCreate
*/
- public function testSaveInsert(EntityInterface $entity) {
+ public function testSaveInsert(): EntityInterface&MockObject {
$this->setUpKeyValueEntityStorage();
+ $entity = $this->getMockEntity(EntityBaseTest::class, [['id' => 'foo']], ['toArray']);
+ $entity->enforceIsNew();
+
$expected = ['id' => 'foo'];
$this->keyValueStore->expects($this->exactly(2))
->method('has')
@@ -285,12 +276,9 @@ class KeyValueEntityStorageTest extends UnitTestCase {
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
- * @return \Drupal\Core\Entity\EntityInterface
- * The updated entity instance after saving.
- *
* @depends testSaveInsert
*/
- public function testSaveUpdate(EntityInterface $entity) {
+ public function testSaveUpdate(EntityInterface $entity): void {
$this->entityType->expects($this->once())
->method('getClass')
->willReturn(get_class($entity));
@@ -320,7 +308,6 @@ class KeyValueEntityStorageTest extends UnitTestCase {
->with('foo', $expected);
$return = $this->entityStorage->save($entity);
$this->assertSame(SAVED_UPDATED, $return);
- return $entity;
}
/**
diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
index 37cede409b88..5653e8a356b0 100644
--- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php
@@ -1105,12 +1105,7 @@ class SqlContentEntityStorageTest extends UnitTestCase {
$this->setUpEntityStorage();
$entity = $this->entityStorage->create();
- $entity->expects($this->atLeastOnce())
- ->method('id')
- ->willReturn('foo');
-
$this->assertInstanceOf(EntityInterface::class, $entity);
- $this->assertSame('foo', $entity->id());
$this->assertTrue($entity->isNew());
}
diff --git a/core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php b/core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php
new file mode 100644
index 000000000000..f91af07ddcbc
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Extension/Requirement/RequirementSeverityTest.php
@@ -0,0 +1,104 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Extension\Requirement;
+
+include_once \DRUPAL_ROOT . '/core/includes/install.inc';
+
+use Drupal\Core\Extension\Requirement\RequirementSeverity;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Extension\Requirement\RequirementSeverity
+ *
+ * @group Extension
+ */
+class RequirementSeverityTest extends UnitTestCase {
+
+ /**
+ * @covers ::convertLegacyIntSeveritiesToEnums
+ * @group legacy
+ */
+ public function testConvertLegacySeverities(): void {
+ $requirements['foo'] = [
+ 'title' => new TranslatableMarkup('Foo'),
+ 'severity' => \REQUIREMENT_INFO,
+ ];
+ $requirements['bar'] = [
+ 'title' => new TranslatableMarkup('Bar'),
+ 'severity' => \REQUIREMENT_ERROR,
+ ];
+ $this->expectDeprecation(
+ 'Calling ' . __METHOD__ . '() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939'
+ );
+ RequirementSeverity::convertLegacyIntSeveritiesToEnums($requirements, __METHOD__);
+ $this->assertEquals(
+ RequirementSeverity::Info,
+ $requirements['foo']['severity']
+ );
+ $this->assertEquals(
+ RequirementSeverity::Error,
+ $requirements['bar']['severity']
+ );
+ }
+
+ /**
+ * @covers ::maxSeverityFromRequirements
+ * @dataProvider requirementProvider
+ */
+ public function testGetMaxSeverity(array $requirements, RequirementSeverity $expectedSeverity): void {
+ $severity = RequirementSeverity::maxSeverityFromRequirements($requirements);
+ $this->assertEquals($expectedSeverity, $severity);
+ }
+
+ /**
+ * Data provider for requirement helper test.
+ */
+ public static function requirementProvider(): array {
+ $info = [
+ 'title' => 'Foo',
+ 'severity' => RequirementSeverity::Info,
+ ];
+ $warning = [
+ 'title' => 'Baz',
+ 'severity' => RequirementSeverity::Warning,
+ ];
+ $error = [
+ 'title' => 'Wiz',
+ 'severity' => RequirementSeverity::Error,
+ ];
+ $ok = [
+ 'title' => 'Bar',
+ 'severity' => RequirementSeverity::OK,
+ ];
+
+ return [
+ 'error is most severe' => [
+ [
+ $info,
+ $error,
+ $ok,
+ ],
+ RequirementSeverity::Error,
+ ],
+ 'ok is most severe' => [
+ [
+ $info,
+ $ok,
+ ],
+ RequirementSeverity::OK,
+ ],
+ 'warning is most severe' => [
+ [
+ $warning,
+ $info,
+ $ok,
+ ],
+ RequirementSeverity::Warning,
+ ],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index d7a364170f04..f2afab3f8a9c 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -1002,6 +1002,57 @@ class FormBuilderTest extends FormTestBase {
];
}
+ /**
+ * Tests the detection of the triggering element.
+ */
+ public function testTriggeringElement(): void {
+ $form_arg = 'Drupal\Tests\Core\Form\TestForm';
+
+ // No triggering element.
+ $form_state = new FormState();
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $this->assertNull($form_state->getTriggeringElement());
+
+ // When no op is provided, default to the first button element.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('op', $triggeringElement['#name']);
+ $this->assertSame('Submit', $triggeringElement['#value']);
+
+ // A single triggering element.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form', 'op' => 'Submit']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('op', $triggeringElement['#name']);
+
+ // A different triggering element.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form', 'other_action' => 'Other action']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('other_action', $triggeringElement['#name']);
+
+ // Two triggering elements.
+ $form_state = new FormState();
+ $form_state->setMethod('GET');
+ $form_state->setUserInput(['form_id' => 'test_form', 'op' => 'Submit', 'other_action' => 'Other action']);
+ $this->formBuilder->buildForm($form_arg, $form_state);
+
+ // Verify that only the first triggering element is respected.
+ $triggeringElement = $form_state->getTriggeringElement();
+ $this->assertIsArray($triggeringElement);
+ $this->assertSame('op', $triggeringElement['#name']);
+ }
+
}
/**
diff --git a/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc b/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc
index 0e34dd9110d5..0e633183d331 100644
--- a/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc
+++ b/core/tests/Drupal/Tests/Core/Form/fixtures/form_base_test.inc
@@ -36,5 +36,10 @@ function test_form_id() {
'#type' => 'submit',
'#value' => 'Submit',
];
+ $form['actions']['other_action'] = [
+ '#type' => 'submit',
+ '#name' => 'other_action',
+ '#value' => 'Other action',
+ ];
return $form;
}
diff --git a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php
index 08fa2eceaf54..d0759a4bf082 100644
--- a/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php
@@ -203,6 +203,8 @@ class LocalActionManagerTest extends UnitTestCase {
}
public static function getActionsForRouteProvider() {
+ $originalContainer = \Drupal::hasContainer() ? \Drupal::getContainer() : NULL;
+
$cache_contexts_manager = (new Prophet())->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens(Argument::any())
->willReturn(TRUE);
@@ -384,6 +386,11 @@ class LocalActionManagerTest extends UnitTestCase {
],
];
+ // Restore the original container if needed.
+ if ($originalContainer) {
+ \Drupal::setContainer($originalContainer);
+ }
+
return $data;
}
diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
index 1e75a2e3f4ba..5b1a76fbf7e7 100644
--- a/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
+++ b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php
@@ -9,6 +9,7 @@ use Drupal\Core\Menu\MenuActiveTrail;
use Drupal\Core\Routing\CurrentRouteMatch;
use Drupal\Tests\UnitTestCase;
use Drupal\TestTools\Random;
+use Drupal\Core\Path\PathMatcherInterface;
use Drupal\Core\Routing\RouteObjectInterface;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Component\DependencyInjection\Container;
@@ -68,6 +69,12 @@ class MenuActiveTrailTest extends UnitTestCase {
*/
protected $lock;
+
+ /**
+ * The mocked path matcher.
+ */
+ protected PathMatcherInterface $pathMatcher;
+
/**
* The mocked cache tags invalidator.
*
@@ -86,9 +93,10 @@ class MenuActiveTrailTest extends UnitTestCase {
$this->menuLinkManager = $this->createMock('Drupal\Core\Menu\MenuLinkManagerInterface');
$this->cache = $this->createMock('\Drupal\Core\Cache\CacheBackendInterface');
$this->lock = $this->createMock('\Drupal\Core\Lock\LockBackendInterface');
+ $this->pathMatcher = $this->createMock('\Drupal\Core\Path\PathMatcherInterface');
$this->cacheTagsInvalidator = $this->createMock('\Drupal\Core\Cache\CacheTagsInvalidatorInterface');
- $this->menuActiveTrail = new MenuActiveTrail($this->menuLinkManager, $this->currentRouteMatch, $this->cache, $this->lock);
+ $this->menuActiveTrail = new MenuActiveTrail($this->menuLinkManager, $this->currentRouteMatch, $this->cache, $this->lock, $this->pathMatcher);
$container = new Container();
$container->set('cache_tags.invalidator', $this->cacheTagsInvalidator);
@@ -105,6 +113,7 @@ class MenuActiveTrailTest extends UnitTestCase {
* - links: An array of menu links keyed by ID.
* - menu_name: The active menu name.
* - expected_link: The expected active link for the given menu.
+ * - expected_trail: The expected active trail for the given menu.
*/
public static function provider() {
$data = [];
@@ -167,6 +176,42 @@ class MenuActiveTrailTest extends UnitTestCase {
}
/**
+ * Tests that getActiveLink() returns a <front> route link for a route that is the front page and has no other links.
+ *
+ * @covers ::getActiveLink
+ */
+ public function testGetActiveLinkReturnsFrontPageLinkAtTheFrontPage(): void {
+
+ // Mock the request.
+ $mock_route = new Route('');
+ $request = new Request();
+ $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'link_1');
+ $request->attributes->set(RouteObjectInterface::ROUTE_OBJECT, $mock_route);
+ $request->attributes->set('_raw_variables', new InputBag([]));
+ $this->requestStack->push($request);
+
+ // Pretend that the current path is the front page.
+ $this->pathMatcher
+ ->method('isFrontPage')
+ ->willReturn(TRUE);
+
+ // Make 'link_1' route to have no links and the '<front>' route to have a link.
+ $home_link = MenuLinkMock::create(['id' => 'home_link', 'route_name' => 'home_link', 'title' => 'Home', 'parent' => NULL]);
+ $this->menuLinkManager
+ ->method('loadLinksByRoute')
+ ->willReturnCallback(function ($route_name) use ($home_link) {
+ return match ($route_name) {
+ 'link_1' => [],
+ '<front>' => [$home_link],
+ };
+ });
+
+ // Test.
+ $this->assertSame($home_link, $this->menuActiveTrail->getActiveLink());
+
+ }
+
+ /**
* Tests getActiveTrailIds().
*
* @covers ::getActiveTrailIds
diff --git a/core/tests/Drupal/Tests/Core/PageCache/DenyNoCacheRoutesTest.php b/core/tests/Drupal/Tests/Core/PageCache/DenyNoCacheRoutesTest.php
new file mode 100644
index 000000000000..4cc4dafcd531
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/PageCache/DenyNoCacheRoutesTest.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\PageCache;
+
+use Drupal\Core\PageCache\ResponsePolicyInterface;
+use Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Route;
+
+/**
+ * @coversDefaultClass \Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes
+ * @group PageCache
+ * @group Route
+ */
+class DenyNoCacheRoutesTest extends UnitTestCase {
+
+ /**
+ * The response policy under test.
+ *
+ * @var \Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes
+ */
+ protected $policy;
+
+ /**
+ * A request object.
+ *
+ * @var \Symfony\Component\HttpFoundation\Request
+ */
+ protected $request;
+
+ /**
+ * A response object.
+ *
+ * @var \Symfony\Component\HttpFoundation\Response
+ */
+ protected $response;
+
+ /**
+ * The current route match.
+ *
+ * @var \Drupal\Core\Routing\RouteMatch|\PHPUnit\Framework\MockObject\MockObject
+ */
+ protected $routeMatch;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->routeMatch = $this->createMock(RouteMatchInterface::class);
+ $this->policy = new DenyNoCacheRoutes($this->routeMatch);
+ $this->response = new Response();
+ $this->request = new Request();
+ }
+
+ /**
+ * Asserts that caching is denied on the node preview route.
+ *
+ * @dataProvider providerDenyNoCacheRoutesPolicy
+ * @covers ::check
+ */
+ public function testDenyNoCacheRoutesPolicy($expected_result, ?Route $route): void {
+ $this->routeMatch->expects($this->once())
+ ->method('getRouteObject')
+ ->willReturn($route);
+
+ $actual_result = $this->policy->check($this->response, $this->request);
+ $this->assertSame($expected_result, $actual_result);
+ }
+
+ /**
+ * Provides data and expected results for the test method.
+ *
+ * @return array
+ * Data and expected results.
+ */
+ public static function providerDenyNoCacheRoutesPolicy(): array {
+ $no_cache_route = new Route('', [], [], ['no_cache' => TRUE]);
+ return [
+ [ResponsePolicyInterface::DENY, $no_cache_route],
+ [NULL, new Route('')],
+ [NULL, NULL],
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php b/core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php
new file mode 100644
index 000000000000..d05413eb444e
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Plugin/ConfigurablePluginBaseTest.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Plugin;
+
+use Drupal\Core\Plugin\ConfigurablePluginBase;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests ConfigurablePluginBase.
+ *
+ * @group Plugin
+ *
+ * @coversDefaultClass \Drupal\Core\Plugin\ConfigurablePluginBase
+ */
+class ConfigurablePluginBaseTest extends TestCase {
+
+ /**
+ * Tests the Constructor.
+ */
+ public function testConstructor(): void {
+ $provided_configuration = [
+ 'foo' => 'bar',
+ ];
+ $merged_configuration = ['default' => 'default'] + $provided_configuration;
+ $plugin = new ConfigurablePluginBaseTestClass($provided_configuration, '', []);
+ $this->assertSame($merged_configuration, $plugin->getConfiguration());
+ }
+
+}
+
+/**
+ * Test class for ConfigurablePluginBase.
+ */
+class ConfigurablePluginBaseTestClass extends ConfigurablePluginBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration(): array {
+ return [
+ 'default' => 'default',
+ ];
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php b/core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php
new file mode 100644
index 000000000000..21543658769c
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Plugin/ConfigurableTraitTest.php
@@ -0,0 +1,205 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Plugin;
+
+use Drupal\Component\Plugin\ConfigurableInterface;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ConfigurableTrait;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * Tests for ConfigurableTrait.
+ *
+ * @group Plugin
+ *
+ * @coversDefaultClass \Drupal\Core\Plugin\ConfigurableTrait
+ */
+class ConfigurableTraitTest extends TestCase {
+
+ /**
+ * Tests ConfigurableTrait::defaultConfiguration.
+ *
+ * @covers ::defaultConfiguration
+ */
+ public function testDefaultConfiguration(): void {
+ /** @var \Drupal\Component\Plugin\ConfigurableInterface $configurable_plugin */
+ $configurable_plugin = new ConfigurableTestClass();
+ $this->assertSame([], $configurable_plugin->defaultConfiguration());
+ }
+
+ /**
+ * Tests ConfigurableTrait::getConfiguration.
+ *
+ * @covers ::getConfiguration
+ */
+ public function testGetConfiguration(): void {
+ $test_configuration = [
+ 'config_key_1' => 'config_value_1',
+ 'config_key_2' => [
+ 'nested_key_1' => 'nested_value_1',
+ 'nested_key_2' => 'nested_value_2',
+ ],
+ ];
+ $configurable_plugin = new ConfigurableTestClass($test_configuration);
+ $this->assertSame($test_configuration, $configurable_plugin->getConfiguration());
+ }
+
+ /**
+ * Tests configurableTrait::setConfiguration.
+ *
+ * Specifically test the way default and provided configurations are merged.
+ *
+ * @param array $default_configuration
+ * The default configuration to use for the trait.
+ * @param array $test_configuration
+ * The configuration to test.
+ * @param array $final_configuration
+ * The expected final plugin configuration.
+ *
+ * @covers ::setConfiguration
+ *
+ * @dataProvider setConfigurationDataProvider
+ */
+ public function testSetConfiguration(array $default_configuration, array $test_configuration, array $final_configuration): void {
+ $test_object = new ConfigurableTestClass();
+ $test_object->setDefaultConfiguration($default_configuration);
+ $test_object->setConfiguration($test_configuration);
+ $this->assertSame($final_configuration, $test_object->getConfiguration());
+ }
+
+ /**
+ * Provides data for testSetConfiguration.
+ *
+ * @return array
+ * The data.
+ */
+ public static function setConfigurationDataProvider(): array {
+ return [
+ 'Direct Override' => [
+ 'default_configuration' => [
+ 'default_key_1' => 'default_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'default_nested_value_1',
+ 'default_nested_key_2' => 'default_nested_value_2',
+ ],
+ ],
+ 'test_configuration' => [
+ 'default_key_1' => 'override_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_nested_value_1',
+ 'default_nested_key_2' => 'override_nested_value_2',
+ ],
+ ],
+ 'final_configuration' => [
+ 'default_key_1' => 'override_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_nested_value_1',
+ 'default_nested_key_2' => 'override_nested_value_2',
+ ],
+ ],
+ ],
+ 'Mixed Override' => [
+ 'default_configuration' => [
+ 'default_key_1' => 'default_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'default_nested_value_1',
+ 'default_nested_key_2' => 'default_nested_value_2',
+ ],
+ ],
+ 'test_configuration' => [
+ 'override_key_1' => 'config_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_value_1',
+ 'override_nested_key' => 'override_value',
+ ],
+ ],
+ 'final_configuration' => [
+ 'default_key_1' => 'default_value_1',
+ 'default_key_2' => [
+ 'default_nested_key_1' => 'override_value_1',
+ 'default_nested_key_2' => 'default_nested_value_2',
+ 'override_nested_key' => 'override_value',
+ ],
+ 'override_key_1' => 'config_value_1',
+ ],
+ ],
+ 'indexed_override' => [
+ 'default_configuration' => [
+ 'config_value_1',
+ 'config_value_2',
+ 'config_value_3',
+ ],
+ 'test_configuration' => [
+ 'override_value_1',
+ 'override_value_2',
+ ],
+ 'final_configuration' => [
+ 'override_value_1',
+ 'override_value_2',
+ 'config_value_3',
+ ],
+ ],
+ 'indexed_override_complex' => [
+ 'default_configuration' => [
+ 'config_value_1',
+ 'config_value_2',
+ 'config_value_3',
+ ],
+ 'test_configuration' => [
+ 0 => 'override_value_1',
+ 2 => 'override_value_3',
+ ],
+ 'final_configuration' => [
+ 'override_value_1',
+ 'config_value_2',
+ 'override_value_3',
+ ],
+ ],
+ ];
+ }
+
+}
+
+/**
+ * A test class using ConfigurablePluginTrait that can modify the de.
+ */
+class ConfigurableTestClass extends PluginBase implements ConfigurableInterface {
+ use ConfigurableTrait {
+ defaultConfiguration as traitDefaultConfiguration;
+ }
+
+ /**
+ * A default configuration for the test class to return.
+ *
+ * @var array|null
+ */
+ protected ?array $defaultConfiguration = NULL;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __construct(array $configuration = [], string $plugin_id = '', array $plugin_definition = []) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->setConfiguration($configuration);
+ }
+
+ /**
+ * Sets the default configuration this test will return.
+ *
+ * @param array $default_configuration
+ * The default configuration to use.
+ */
+ public function setDefaultConfiguration(array $default_configuration): void {
+ $this->defaultConfiguration = $default_configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration(): array {
+ return $this->defaultConfiguration ?? $this->traitDefaultConfiguration();
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php b/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php
index 99da7a2f9741..60e147f434a5 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/DefaultSingleLazyPluginCollectionTest.php
@@ -4,8 +4,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Plugin;
-use Drupal\Component\Plugin\ConfigurableInterface;
-use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ConfigurablePluginBase;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
@@ -98,24 +97,5 @@ class DefaultSingleLazyPluginCollectionTest extends LazyPluginCollectionTestBase
/**
* Stub configurable plugin class for testing.
*/
-class ConfigurablePlugin extends PluginBase implements ConfigurableInterface {
-
- public function __construct(array $configuration, $plugin_id, $plugin_definition) {
- parent::__construct($configuration, $plugin_id, $plugin_definition);
-
- $this->configuration = $configuration + $this->defaultConfiguration();
- }
-
- public function defaultConfiguration() {
- return [];
- }
-
- public function getConfiguration() {
- return $this->configuration;
- }
-
- public function setConfiguration(array $configuration): void {
- $this->configuration = $configuration;
- }
-
+class ConfigurablePlugin extends ConfigurablePluginBase {
}
diff --git a/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php b/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php
index 55695ace6de7..7b72a3df4530 100644
--- a/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php
+++ b/core/tests/Drupal/Tests/Core/Plugin/Fixtures/TestConfigurablePlugin.php
@@ -4,35 +4,13 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Plugin\Fixtures;
-use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
-use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Plugin\ConfigurablePluginBase;
/**
* A configurable plugin implementation used for testing.
*/
-class TestConfigurablePlugin extends PluginBase implements ConfigurableInterface, DependentPluginInterface {
-
- /**
- * {@inheritdoc}
- */
- public function getConfiguration() {
- return $this->configuration;
- }
-
- /**
- * {@inheritdoc}
- */
- public function setConfiguration(array $configuration) {
- $this->configuration = $configuration;
- }
-
- /**
- * {@inheritdoc}
- */
- public function defaultConfiguration() {
- return [];
- }
+class TestConfigurablePlugin extends ConfigurablePluginBase implements DependentPluginInterface {
/**
* {@inheritdoc}
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
index d0c09e97fc53..8490a5c0876e 100644
--- a/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/HtmlTagTest.php
@@ -6,6 +6,7 @@ namespace Drupal\Tests\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Tests\Core\Render\RendererTestBase;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\Element\HtmlTag;
/**
@@ -18,7 +19,7 @@ class HtmlTagTest extends RendererTestBase {
* @covers ::getInfo
*/
public function testGetInfo(): void {
- $htmlTag = new HtmlTag([], 'test', 'test');
+ $htmlTag = new HtmlTag([], 'test', 'test', elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $htmlTag->getInfo();
$this->assertArrayHasKey('#pre_render', $info);
$this->assertArrayHasKey('#attributes', $info);
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php b/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php
new file mode 100644
index 000000000000..c91b74f8c0a4
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/Element/ModernRenderElementTest.php
@@ -0,0 +1,60 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Render\Element;
+
+use Drupal\Component\Plugin\Factory\FactoryInterface;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\Element\Textfield;
+use Drupal\Core\Render\ElementInfoManager;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Render\Element\RenderElementBase
+ * @group Render
+ */
+class ModernRenderElementTest extends UnitTestCase {
+
+ public function testChildren(): void {
+ $factory = $this->createMock(FactoryInterface::class);
+ $elementInfoManager = new class ($factory) extends ElementInfoManager {
+
+ public function __construct(protected $factory) {}
+
+ };
+ $factory->expects($this->any())
+ ->method('createInstance')
+ ->willReturnCallback(fn () => new Textfield([], '', NULL, $elementInfoManager));
+ // If the type is not given ::fromRenderable presumes "form" and uses the
+ // plugin discovery to find which class provides the form element. This
+ // test does not set up discovery so some type must be provided.
+ $element = ['#type' => 'ignored by the mock factory'];
+ $elementObject = $elementInfoManager->fromRenderable($element);
+ for ($i = 0; $i <= 2; $i++) {
+ $child = [
+ '#type' => 'ignored by the mock factory',
+ '#test' => $i,
+ ];
+ $elementObject->addChild("test$i", $child);
+ // addChild() takes the $child render array by reference and stores a
+ // reference to it in the render object. To avoid modifying the
+ // previously created render object when reusing the $child variable,
+ // unset() it to break the reference before reassigning.
+ unset($child);
+ }
+ foreach ([1 => ['test0', 'test1', 'test2'], 2 => ['test0', 'test2']] as $delta => $expectedChildrenKeys) {
+ $i = 0;
+ foreach ($elementObject->getChildren() as $name => $child) {
+ $this->assertSame($name, "test$i");
+ $this->assertSame($i, $child->test);
+ $i += $delta;
+ }
+ $this->assertSame(Element::children($elementObject->toRenderable()), $expectedChildrenKeys);
+ // The first iteration tests removing an existing child. The second
+ // iteration tests removing a nonexistent child.
+ $elementObject->removeChild('test1');
+ }
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php b/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php
index fc58c1db4efe..7acfef4ca509 100644
--- a/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Element/TableSelectTest.php
@@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Render\Element;
use Drupal\Core\Form\FormState;
use Drupal\Core\Link;
use Drupal\Core\Render\Element\Tableselect;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\Tests\UnitTestCase;
@@ -25,7 +26,7 @@ class TableSelectTest extends UnitTestCase {
$form_state = new FormState();
$complete_form = [];
- $element_object = new Tableselect([], 'table_select', []);
+ $element_object = new Tableselect([], 'table_select', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $element_object->getInfo();
$element += $info;
@@ -50,7 +51,7 @@ class TableSelectTest extends UnitTestCase {
$form_state = new FormState();
$complete_form = [];
- $element_object = new Tableselect([], 'table_select', []);
+ $element_object = new Tableselect([], 'table_select', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $element_object->getInfo();
$element += $info;
diff --git a/core/tests/Drupal/Tests/Core/Render/Placeholder/ChainedPlaceholderStrategyTest.php b/core/tests/Drupal/Tests/Core/Render/Placeholder/ChainedPlaceholderStrategyTest.php
index f1344b0ce8e2..396d0d5db8fb 100644
--- a/core/tests/Drupal/Tests/Core/Render/Placeholder/ChainedPlaceholderStrategyTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/Placeholder/ChainedPlaceholderStrategyTest.php
@@ -40,20 +40,6 @@ class ChainedPlaceholderStrategyTest extends UnitTestCase {
$prophet = new Prophet();
$data = [];
- // Empty placeholders.
- $data['empty placeholders'] = [[], [], []];
-
- // Placeholder removing strategy.
- $placeholders = [
- 'remove-me' => ['#markup' => 'I-am-a-llama-that-will-be-removed-sad-face.'],
- ];
-
- $prophecy = $prophet->prophesize('\Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface');
- $prophecy->processPlaceholders($placeholders)->willReturn([]);
- $dev_null_strategy = $prophecy->reveal();
-
- $data['placeholder removing strategy'] = [[$dev_null_strategy], $placeholders, []];
-
// Fake Single Flush strategy.
$placeholders = [
'67890' => ['#markup' => 'special-placeholder'],
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
index 2b897fe40a1c..78f2f60e62a6 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
@@ -78,7 +78,7 @@ class RendererPlaceholdersTest extends RendererTestBase {
// \Drupal\Core\Render\Markup::create() is necessary as the render
// system would mangle this markup. As this is exactly what happens at
// runtime this is a valid use-case.
- return Markup::create('<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
+ return Markup::create('<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
};
$extract_placeholder_render_array = function ($placeholder_render_array) {
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 69301eae9bb8..e4fbfa60caf7 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -1097,7 +1097,7 @@ class RendererTest extends RendererTestBase {
'max-age' => 600,
],
],
- new \stdClass(),
+ (new CacheableMetadata())->setCacheMaxAge(0),
[
'#cache' => [
'contexts' => ['theme'],
@@ -1126,6 +1126,68 @@ class RendererTest extends RendererTestBase {
$this->assertFalse($this->renderer->hasRenderContext());
}
+ /**
+ * @covers ::executeInRenderContext
+ */
+ public function testExecuteInRenderContext(): void {
+ $return = $this->renderer->executeInRenderContext(new RenderContext(), function () {
+ $fiber_callback = function () {
+
+ // Create a #pre_render callback that renders a render array in
+ // isolation. This has its own #pre_render callback that calls
+ // Fiber::suspend(). This ensures that suspending a Fiber within
+ // multiple nested calls to ::executeInRenderContext() doesn't
+ // allow render context to get out of sync. This simulates similar
+ // conditions to BigPipe placeholder rendering.
+ $fiber_suspend_pre_render = function ($elements) {
+ $fiber_suspend = function ($elements) {
+ \Fiber::suspend();
+ return $elements;
+ };
+ $build = [
+ 'foo' => [
+ '#markup' => 'foo',
+ '#pre_render' => [$fiber_suspend],
+ ],
+ ];
+ $markup = $this->renderer->renderInIsolation($build);
+ $elements['#markup'] = $markup;
+ return $elements;
+ };
+ $build = [
+ 'foo' => [
+ '#pre_render' => [$fiber_suspend_pre_render],
+ ],
+ ];
+ return $this->renderer->render($build);
+ };
+
+ // Build an array of two fibers that executes the code defined above. This
+ // ensures that Fiber::suspend() is called from within two
+ // ::renderInIsolation() calls without either having been completed.
+ $fibers = [];
+ foreach ([0, 1] as $key) {
+ $fibers[] = new \Fiber(static fn () => $fiber_callback());
+ }
+ while ($fibers) {
+ foreach ($fibers as $key => $fiber) {
+ if ($fiber->isTerminated()) {
+ unset($fibers[$key]);
+ continue;
+ }
+ if ($fiber->isSuspended()) {
+ $fiber->resume();
+ }
+ else {
+ $fiber->start();
+ }
+ }
+ }
+ return $fiber->getReturn();
+ });
+ $this->assertEquals(Markup::create('foo'), $return);
+ }
+
}
/**
diff --git a/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php b/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php
deleted file mode 100644
index 6ab42a6c41a4..000000000000
--- a/core/tests/Drupal/Tests/Core/Template/StubTwigTemplate.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Drupal\Tests\Core\Template;
-
-use Twig\Source;
-use Twig\Template;
-
-/**
- * A stub of the Twig Template class for testing.
- */
-class StubTwigTemplate extends Template {
-
- /**
- * {@inheritdoc}
- */
- public function getTemplateName(): string {
- return '';
- }
-
- /**
- * {@inheritdoc}
- */
- public function getDebugInfo(): array {
- return [];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getSourceContext(): Source {
- throw new \LogicException(__METHOD__ . '() not implemented.');
- }
-
- /**
- * {@inheritdoc}
- */
- protected function doDisplay(array $context, array $blocks = []): iterable {
- throw new \LogicException(__METHOD__ . '() not implemented.');
- }
-
-}
diff --git a/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php b/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php
index 43ff47406ecc..d1a043446f8a 100644
--- a/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php
@@ -25,7 +25,7 @@ class BrowserTestBaseTest extends UnitTestCase {
->method('getDriver')
->willReturn($driver);
- $btb = $this->getMockBuilder(BrowserTestBaseMockableClass::class)
+ $btb = $this->getMockBuilder(BrowserTestBaseMockableClassTest::class)
->disableOriginalConstructor()
->onlyMethods(['getSession'])
->getMock();
@@ -82,7 +82,7 @@ class BrowserTestBaseTest extends UnitTestCase {
public function testTearDownWithoutSetUp(): void {
$method = 'cleanupEnvironment';
$this->assertTrue(method_exists(BrowserTestBase::class, $method));
- $btb = $this->getMockBuilder(BrowserTestBaseMockableClass::class)
+ $btb = $this->getMockBuilder(BrowserTestBaseMockableClassTest::class)
->disableOriginalConstructor()
->onlyMethods([$method])
->getMock();
@@ -96,6 +96,6 @@ class BrowserTestBaseTest extends UnitTestCase {
/**
* A class extending BrowserTestBase for testing purposes.
*/
-class BrowserTestBaseMockableClass extends BrowserTestBase {
+class BrowserTestBaseMockableClassTest extends BrowserTestBase {
}
diff --git a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
index 957b2f61f97b..9138d54523d6 100644
--- a/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/RunTests/TestFileParserTest.php
@@ -6,12 +6,18 @@ namespace Drupal\Tests\Core\Test\RunTests;
use Drupal\Core\Test\RunTests\TestFileParser;
use Drupal\Tests\UnitTestCase;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
/**
- * @coversDefaultClass \Drupal\Core\Test\RunTests\TestFileParser
- * @group Test
- * @group RunTests
+ * Tests for the deprecated TestFileParser class.
*/
+#[CoversClass(TestFileParser::class)]
+#[Group('Test')]
+#[Group('RunTest')]
+#[IgnoreDeprecations]
class TestFileParserTest extends UnitTestCase {
public static function provideTestFileContents() {
@@ -66,9 +72,9 @@ COMPOUND
}
/**
- * @covers ::parseContents
- * @dataProvider provideTestFileContents
+ * @legacy-covers ::parseContents
*/
+ #[DataProvider('provideTestFileContents')]
public function testParseContents($expected, $contents): void {
$parser = new TestFileParser();
@@ -78,7 +84,7 @@ COMPOUND
}
/**
- * @covers ::getTestListFromFile
+ * @legacy-covers ::getTestListFromFile
*/
public function testGetTestListFromFile(): void {
$parser = new TestFileParser();
diff --git a/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php
index bfbb4ca2e40d..0fb55e6c7f8a 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestDiscoveryTest.php
@@ -13,17 +13,23 @@ use Drupal\Core\Test\Exception\MissingGroupException;
use Drupal\Core\Test\TestDiscovery;
use Drupal\Tests\UnitTestCase;
use org\bovigo\vfs\vfsStream;
+use PHPUnit\Framework\Attributes\CoversClass;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\IgnoreDeprecations;
/**
- * @coversDefaultClass \Drupal\Core\Test\TestDiscovery
- * @group Test
+ * Unit tests for TestDiscovery.
*/
+#[CoversClass(TestDiscovery::class)]
+#[Group('Test')]
+#[IgnoreDeprecations]
class TestDiscoveryTest extends UnitTestCase {
/**
- * @covers ::getTestInfo
- * @dataProvider infoParserProvider
+ * @legacy-covers ::getTestInfo
*/
+ #[DataProvider('infoParserProvider')]
public function testTestInfoParser($expected, $classname, $doc_comment = NULL): void {
$info = TestDiscovery::getTestInfo($classname, $doc_comment);
$this->assertEquals($expected, $info);
@@ -34,14 +40,14 @@ class TestDiscoveryTest extends UnitTestCase {
$tests[] = [
// Expected result.
[
- 'name' => static::class,
+ 'name' => TestDatabaseTest::class,
'group' => 'Test',
- 'groups' => ['Test'],
- 'description' => 'Tests \Drupal\Core\Test\TestDiscovery.',
+ 'groups' => ['Test', 'simpletest', 'Template'],
+ 'description' => 'Tests \Drupal\Core\Test\TestDatabase.',
'type' => 'PHPUnit-Unit',
],
// Classname.
- static::class,
+ TestDatabaseTest::class,
];
// A core unit test.
@@ -217,7 +223,7 @@ class TestDiscoveryTest extends UnitTestCase {
}
/**
- * @covers ::getTestInfo
+ * @legacy-covers ::getTestInfo
*/
public function testTestInfoParserMissingGroup(): void {
$classname = 'Drupal\KernelTests\field\BulkDeleteTest';
@@ -232,7 +238,7 @@ EOT;
}
/**
- * @covers ::getTestInfo
+ * @legacy-covers ::getTestInfo
*/
public function testTestInfoParserMissingSummary(): void {
$classname = 'Drupal\KernelTests\field\BulkDeleteTest';
@@ -311,7 +317,7 @@ EOF;
}
/**
- * @covers ::getTestClasses
+ * @legacy-covers ::getTestClasses
*/
public function testGetTestClasses(): void {
$this->setupVfsWithTestClasses();
@@ -380,7 +386,7 @@ EOF;
}
/**
- * @covers ::getTestClasses
+ * @legacy-covers ::getTestClasses
*/
public function testGetTestClassesWithSelectedTypes(): void {
$this->setupVfsWithTestClasses();
@@ -425,7 +431,7 @@ EOF;
}
/**
- * @covers ::getTestClasses
+ * @legacy-covers ::getTestClasses
*/
public function testGetTestsInProfiles(): void {
$this->setupVfsWithTestClasses();
@@ -454,9 +460,9 @@ EOF;
}
/**
- * @covers ::getPhpunitTestSuite
- * @dataProvider providerTestGetPhpunitTestSuite
+ * @legacy-covers ::getPhpunitTestSuite
*/
+ #[DataProvider('providerTestGetPhpunitTestSuite')]
public function testGetPhpunitTestSuite($classname, $expected): void {
$this->assertEquals($expected, TestDiscovery::getPhpunitTestSuite($classname));
}
@@ -482,7 +488,7 @@ EOF;
/**
* Ensure that classes are not reflected when the docblock is empty.
*
- * @covers ::getTestInfo
+ * @legacy-covers ::getTestInfo
*/
public function testGetTestInfoEmptyDocblock(): void {
// If getTestInfo() performed reflection, it won't be able to find the
@@ -497,7 +503,7 @@ EOF;
/**
* Ensure TestDiscovery::scanDirectory() ignores certain abstract file types.
*
- * @covers ::scanDirectory
+ * @legacy-covers ::scanDirectory
*/
public function testScanDirectoryNoAbstract(): void {
$this->setupVfsWithTestClasses();
diff --git a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
index ba1d28a50c6d..5627d068aa77 100644
--- a/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
+++ b/core/tests/Drupal/Tests/Core/Test/TestSetupTraitTest.php
@@ -29,7 +29,7 @@ class TestSetupTraitTest extends UnitTestCase {
public function testChangeDatabasePrefix(): void {
$root = dirname(__FILE__, 7);
putenv('SIMPLETEST_DB=pgsql://user:pass@127.0.0.1/db');
- $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db', $root);
+ $connection_info = Database::convertDbUrlToConnectionInfo('mysql://user:pass@localhost/db');
Database::addConnectionInfo('default', 'default', $connection_info);
$this->assertEquals('mysql', Database::getConnectionInfo()['default']['driver']);
$this->assertEquals('localhost', Database::getConnectionInfo()['default']['host']);
diff --git a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php
new file mode 100644
index 000000000000..c39827ca7098
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentLoaderTest.php
@@ -0,0 +1,92 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\Core\Theme\Component;
+
+use Drupal\Core\Plugin\Component;
+use Drupal\Core\Template\Loader\ComponentLoader;
+use Drupal\Core\Theme\ComponentPluginManager;
+use Drupal\Tests\UnitTestCaseTest;
+use org\bovigo\vfs\vfsStream;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Unit tests for the component loader class.
+ *
+ * @coversDefaultClass \Drupal\Core\Template\Loader\ComponentLoader
+ * @group sdc
+ */
+class ComponentLoaderTest extends UnitTestCaseTest {
+
+ /**
+ * Tests the is fresh function for component loader.
+ */
+ public function testIsFresh(): void {
+ $vfs_root = vfsStream::setup();
+ $component_directory = vfsStream::newDirectory('loader-test')->at($vfs_root);
+ $current_time = time();
+ $component_twig_file = vfsStream::newFile('loader-test.twig')
+ ->at($component_directory)
+ ->setContent('twig')
+ // Mark files as changed before the current time.
+ ->lastModified($current_time - 1000);
+ $component_yml_file = vfsStream::newFile('loader-test.component.yml')
+ ->at($component_directory)
+ ->setContent('')
+ // Mark file as changed before the current time.
+ ->lastModified($current_time - 1000);
+
+ $component = new Component(
+ ['app_root' => '/fake/root'],
+ 'sdc_test:loader-test',
+ [
+ 'machineName' => 'loader-test',
+ 'extension_type' => 'module',
+ 'id' => 'sdc_test:loader-test',
+ 'path' => 'vfs://' . $component_directory->path(),
+ 'provider' => 'sdc_test',
+ 'template' => 'loader-test.twig',
+ 'group' => 'my-group',
+ 'description' => 'My description',
+ '_discovered_file_path' => 'vfs://' . $component_yml_file->path(),
+ ]
+ );
+
+ $component_manager = $this->prophesize(ComponentPluginManager::class);
+ $component_manager->find('sdc_test:loader-test')->willReturn($component);
+ $component_loader = new ComponentLoader(
+ $component_manager->reveal(),
+ $this->createMock(LoggerInterface::class),
+ );
+
+ // Assert the component is fresh, as it changed before the current time.
+ $this->assertTrue($component_loader->isFresh('sdc_test:loader-test', $current_time), 'Twig and YAML files were supposed to be fresh');
+ // Pretend that we changed the twig file.
+ // It shouldn't matter that the time is in "future".
+ $component_twig_file->lastModified($current_time + 1000);
+ // Clear stat cache, to make sure component loader gets updated time.
+ clearstatcache();
+ // Component shouldn't be "fresh" anymore.
+ $this->assertFalse($component_loader->isFresh('sdc_test:loader-test', $current_time), 'Twig file was supposed to be outdated');
+
+ // Pretend that we changed the YAML file.
+ // It shouldn't matter that the time is in "future".
+ $component_twig_file->lastModified($current_time);
+ $component_yml_file->lastModified($current_time + 1000);
+ // Clear stat cache, to make sure component loader gets updated time.
+ clearstatcache();
+ // Component shouldn't be "fresh" anymore.
+ $this->assertFalse($component_loader->isFresh('sdc_test:loader-test', $current_time), 'YAML file was supposed to be outdated');
+
+ // Pretend that we changed both files.
+ // It shouldn't matter that the time is in "future".
+ $component_twig_file->lastModified($current_time + 1000);
+ $component_yml_file->lastModified($current_time + 1000);
+ // Clear stat cache, to make sure component loader gets updated time.
+ clearstatcache();
+ // Component shouldn't be "fresh" anymore.
+ $this->assertFalse($component_loader->isFresh('sdc_test:loader-test', $current_time), 'Twig and YAML files were supposed to be outdated');
+ }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php
index 4e0555eb2ae8..71f0098a902d 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentMetadataTest.php
@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Component;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Theme\Component\ComponentMetadata;
use Drupal\Core\Render\Component\Exception\InvalidComponentException;
use Drupal\Tests\UnitTestCaseTest;
+use PHPUnit\Framework\Attributes\DataProvider;
/**
* Unit tests for the component metadata class.
@@ -18,9 +20,8 @@ class ComponentMetadataTest extends UnitTestCaseTest {
/**
* Tests that the correct data is returned for each property.
- *
- * @dataProvider dataProviderMetadata
*/
+ #[DataProvider('dataProviderMetadata')]
public function testMetadata(array $metadata_info, array $expectations): void {
$metadata = new ComponentMetadata($metadata_info, 'foo/', FALSE);
$this->assertSame($expectations['path'], $metadata->path);
@@ -31,9 +32,8 @@ class ComponentMetadataTest extends UnitTestCaseTest {
/**
* Tests the correct checks when enforcing schemas or not.
- *
- * @dataProvider dataProviderMetadata
*/
+ #[DataProvider('dataProviderMetadata')]
public function testMetadataEnforceSchema(array $metadata_info, array $expectations, bool $missing_schema): void {
if ($missing_schema) {
$this->expectException(InvalidComponentException::class);
@@ -71,7 +71,283 @@ class ComponentMetadataTest extends UnitTestCaseTest {
],
TRUE,
],
- 'complete example with schema' => [
+ 'complete example with schema, but no meta:enum' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('power', [], ['context' => '']),
+ 'like' => new TranslatableMarkup('like', [], ['context' => '']),
+ 'external' => new TranslatableMarkup('external', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema, but no matching meta:enum' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => 'Power',
+ 'fav' => 'Favorite',
+ 'external' => 'External',
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('Power', [], ['context' => '']),
+ 'like' => new TranslatableMarkup('like', [], ['context' => '']),
+ 'external' => new TranslatableMarkup('External', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema, but no meta:enum, prop value not as string' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'col' => [
+ 'type' => 'string',
+ 'title' => 'Column',
+ 'enum' => [
+ 1,
+ 2,
+ 3,
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'col' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Column',
+ 'enum' => [
+ 1,
+ 2,
+ 3,
+ ],
+ 'meta:enum' => [
+ 1 => new TranslatableMarkup('1', [], ['context' => '']),
+ 2 => new TranslatableMarkup('2', [], ['context' => '']),
+ 3 => new TranslatableMarkup('3', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema (including meta:enum)' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => 'Power',
+ 'like' => 'Like',
+ 'external' => 'External',
+ ],
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'iconType' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ 'power',
+ 'like',
+ 'external',
+ ],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('Power', [], ['context' => '']),
+ 'like' => new TranslatableMarkup('Like', [], ['context' => '']),
+ 'external' => new TranslatableMarkup('External', [], ['context' => '']),
+ ],
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema (including meta:enum and x-translation-context)' => [
[
'$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
'id' => 'core:my-button',
@@ -100,6 +376,12 @@ class ComponentMetadataTest extends UnitTestCaseTest {
'like',
'external',
],
+ 'meta:enum' => [
+ 'power' => 'Power',
+ 'like' => 'Like',
+ 'external' => 'External',
+ ],
+ 'x-translation-context' => 'Icon Type',
],
],
],
@@ -130,6 +412,85 @@ class ComponentMetadataTest extends UnitTestCaseTest {
'like',
'external',
],
+ 'meta:enum' => [
+ 'power' => new TranslatableMarkup('Power', [], ['context' => 'Icon Type']),
+ 'like' => new TranslatableMarkup('Like', [], ['context' => 'Icon Type']),
+ 'external' => new TranslatableMarkup('External', [], ['context' => 'Icon Type']),
+ ],
+ 'x-translation-context' => 'Icon Type',
+ ],
+ ],
+ ],
+ ],
+ FALSE,
+ ],
+ 'complete example with schema (including meta:enum and x-translation-context and an empty value)' => [
+ [
+ '$schema' => 'https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json',
+ 'id' => 'core:my-button',
+ 'machineName' => 'my-button',
+ 'path' => 'foo/my-other/path',
+ 'name' => 'Button',
+ 'description' => 'JavaScript enhanced button that tracks the number of times a user clicked it.',
+ 'libraryOverrides' => ['dependencies' => ['core/drupal']],
+ 'group' => 'my-group',
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'properties' => [
+ 'text' => [
+ 'type' => 'string',
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'target' => [
+ 'type' => 'string',
+ 'title' => 'Icon Type',
+ 'enum' => [
+ '',
+ '_blank',
+ ],
+ 'meta:enum' => [
+ '' => 'Opens in same window',
+ '_blank' => 'Opens in new window',
+ ],
+ 'x-translation-context' => 'Link target',
+ ],
+ ],
+ ],
+ ],
+ [
+ 'path' => 'my-other/path',
+ 'status' => 'stable',
+ 'thumbnail' => '',
+ 'group' => 'my-group',
+ 'additionalProperties' => FALSE,
+ 'props' => [
+ 'type' => 'object',
+ 'required' => ['text'],
+ 'additionalProperties' => FALSE,
+ 'properties' => [
+ 'text' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Title',
+ 'description' => 'The title for the button',
+ 'minLength' => 2,
+ 'examples' => ['Press', 'Submit now'],
+ ],
+ 'target' => [
+ 'type' => ['string', 'object'],
+ 'title' => 'Icon Type',
+ 'enum' => [
+ '',
+ '_blank',
+ ],
+ 'meta:enum' => [
+ '' => new TranslatableMarkup('Opens in same window', [], ['context' => 'Link target']),
+ '_blank' => new TranslatableMarkup('Opens in new window', [], ['context' => 'Link target']),
+ ],
+ 'x-translation-context' => 'Link target',
],
],
],
diff --git a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php
index df03332cf4b1..a126e4a81d96 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Component/ComponentValidatorTest.php
@@ -5,15 +5,17 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Component;
use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\Component\ComponentValidator;
use Drupal\Core\Render\Component\Exception\InvalidComponentException;
use Drupal\Core\Plugin\Component;
+use Drupal\Tests\UnitTestCaseTest;
+use JsonSchema\ConstraintError;
use JsonSchema\Constraints\Factory;
use JsonSchema\Constraints\FormatConstraint;
use JsonSchema\Entity\JsonPointer;
use JsonSchema\Validator;
-use PHPUnit\Framework\TestCase;
use Symfony\Component\Yaml\Yaml;
/**
@@ -22,7 +24,7 @@ use Symfony\Component\Yaml\Yaml;
* @coversDefaultClass \Drupal\Core\Theme\Component\ComponentValidator
* @group sdc
*/
-class ComponentValidatorTest extends TestCase {
+class ComponentValidatorTest extends UnitTestCaseTest {
/**
* Tests that valid component definitions don't cause errors.
@@ -123,6 +125,63 @@ class ComponentValidatorTest extends TestCase {
],
];
yield 'invalid slot (type)' => [$cta_with_invalid_slot_type];
+
+ $cta_with_invalid_slot_name = $valid_cta;
+ $cta_with_invalid_slot_name['slots'] = [
+ 'valid_slot' => [
+ 'title' => 'Valid slot',
+ 'description' => 'Valid slot description',
+ ],
+ 'invalid slot' => [
+ 'title' => 'Invalid slot',
+ 'description' => 'Slot name cannot have spaces',
+ ],
+ ];
+ yield 'invalid slot (name with spaces)' => [$cta_with_invalid_slot_name];
+
+ $cta_with_invalid_variant_title_type = $valid_cta;
+ $cta_with_invalid_variant_title_type['variants'] = [
+ 'valid_variant' => [
+ 'title' => 'Valid variant',
+ 'description' => 'Valid variant description',
+ ],
+ 'invalid_variant' => [
+ 'title' => [
+ 'hello' => 'Invalid variant',
+ 'world' => 'Invalid variant',
+ ],
+ 'description' => 'Title must be string',
+ ],
+ ];
+ yield 'invalid variant title (type)' => [$cta_with_invalid_variant_title_type];
+
+ $cta_with_missing_variant_title_type = $valid_cta;
+ $cta_with_missing_variant_title_type['variants'] = [
+ 'valid_variant' => [
+ 'title' => 'Valid variant',
+ 'description' => 'Valid variant description',
+ ],
+ 'invalid_variant' => [
+ 'description' => 'Title is required',
+ ],
+ ];
+ yield 'invalid variant title (missing title)' => [$cta_with_missing_variant_title_type];
+
+ $cta_with_invalid_variant_description_type = $valid_cta;
+ $cta_with_invalid_variant_description_type['variants'] = [
+ 'valid_variant' => [
+ 'title' => 'Valid variant',
+ 'description' => 'Valid variant description',
+ ],
+ 'invalid_variant' => [
+ 'title' => 'Invalid variant',
+ 'description' => [
+ 'this' => 'Description must be',
+ 'that' => 'a string',
+ ],
+ ],
+ ];
+ yield 'invalid variant description (type)' => [$cta_with_invalid_variant_description_type];
}
/**
@@ -133,6 +192,11 @@ class ComponentValidatorTest extends TestCase {
* @throws \Drupal\Core\Render\Component\Exception\InvalidComponentException
*/
public function testValidatePropsValid(array $context, string $component_id, array $definition): void {
+ $translation = $this->getStringTranslationStub();
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $translation);
+ \Drupal::setContainer($container);
+
$component = new Component(
['app_root' => '/fake/path/root'],
'sdc_test:' . $component_id,
@@ -177,6 +241,11 @@ class ComponentValidatorTest extends TestCase {
* Tests we can use a custom validator to validate props.
*/
public function testCustomValidator(): void {
+ $translation = $this->getStringTranslationStub();
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $translation);
+ \Drupal::setContainer($container);
+
$component = new Component(
['app_root' => '/fake/path/root'],
'sdc_test:my-cta',
@@ -208,6 +277,11 @@ class ComponentValidatorTest extends TestCase {
* @throws \Drupal\Core\Render\Component\Exception\InvalidComponentException
*/
public function testValidatePropsInvalid(array $context, string $component_id, array $definition, string $expected_exception_message): void {
+ $translation = $this->getStringTranslationStub();
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $translation);
+ \Drupal::setContainer($container);
+
$component = new Component(
['app_root' => '/fake/path/root'],
'sdc_test:' . $component_id,
@@ -302,7 +376,7 @@ class UrlHelperFormatConstraint extends FormatConstraint {
}
if ($schema->format === 'uri') {
if (\is_string($element) && !UrlHelper::isValid($element)) {
- $this->addError($path, 'Invalid URL format', 'format', ['format' => $schema->format]);
+ $this->addError(ConstraintError::FORMAT_URL, $path, ['format' => $schema->format]);
}
return;
}
diff --git a/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php b/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php
index 2c67cf9193fb..d1a6e6433407 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Icon/IconTest.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Icon;
use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\Element\Icon;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\Icon\IconDefinition;
@@ -41,7 +42,7 @@ class IconTest extends UnitTestCase {
* Test the Icon::getInfo method.
*/
public function testGetInfo(): void {
- $icon = new Icon([], 'test', 'test');
+ $icon = new Icon([], 'test', 'test', elementInfoManager: $this->createStub(ElementInfoManagerInterface::class));
$info = $icon->getInfo();
$this->assertArrayHasKey('#pre_render', $info);
diff --git a/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php b/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php
index 0f1146f84d89..59bce7791113 100644
--- a/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php
+++ b/core/tests/Drupal/Tests/Core/Theme/Icon/Plugin/SvgExtractorTest.php
@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Drupal\Tests\Core\Theme\Icon\Plugin;
// cspell:ignore corge
-use Drupal\Component\Render\FormattableMarkup;
+
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\Icon\IconDefinition;
use Drupal\Core\Theme\Icon\IconDefinitionInterface;
@@ -288,7 +288,6 @@ class SvgExtractorTest extends UnitTestCase {
$this->assertInstanceOf(IconDefinitionInterface::class, $icon_loaded);
$data_loaded = $icon_loaded->getAllData();
- $expected_content[$index] = new FormattableMarkup($expected_content[$index], []);
$this->assertEquals($expected_content[$index], $data_loaded['content']);
$expected_attributes[$index] = new Attribute($expected_attributes[$index] ?? []);
diff --git a/core/tests/Drupal/Tests/RequirementsPageTrait.php b/core/tests/Drupal/Tests/RequirementsPageTrait.php
index c10e5676079d..25f39e49ed80 100644
--- a/core/tests/Drupal/Tests/RequirementsPageTrait.php
+++ b/core/tests/Drupal/Tests/RequirementsPageTrait.php
@@ -14,7 +14,7 @@ trait RequirementsPageTrait {
/**
* Handles the update requirements page.
*/
- protected function updateRequirementsProblem() {
+ protected function updateRequirementsProblem(): void {
// Assert a warning is shown on older test environments.
$links = $this->getSession()->getPage()->findAll('named', ['link', 'try again']);
@@ -37,7 +37,7 @@ trait RequirementsPageTrait {
* next screen of the installer. If an expected warning is not found, or if
* a warning not in the list is present, a fail is raised.
*/
- protected function continueOnExpectedWarnings($expected_warnings = []) {
+ protected function continueOnExpectedWarnings($expected_warnings = []): void {
$this->assertSession()->pageTextNotContains('Errors found');
$this->assertWarningSummaries($expected_warnings);
$this->clickLink('continue anyway');
@@ -54,7 +54,7 @@ trait RequirementsPageTrait {
* A list of warning summaries to expect on the requirements screen (e.g.
* 'PHP', 'PHP OPcode caching', etc.).
*/
- protected function assertWarningSummaries(array $summaries) {
+ protected function assertWarningSummaries(array $summaries): void {
$this->assertRequirementSummaries($summaries, 'warning');
}
@@ -68,7 +68,7 @@ trait RequirementsPageTrait {
* A list of error summaries to expect on the requirements screen (e.g.
* 'PHP', 'PHP OPcode caching', etc.).
*/
- protected function assertErrorSummaries(array $summaries) {
+ protected function assertErrorSummaries(array $summaries): void {
$this->assertRequirementSummaries($summaries, 'error');
}
@@ -84,7 +84,7 @@ trait RequirementsPageTrait {
* @param string $type
* The type of requirement, either 'warning' or 'error'.
*/
- protected function assertRequirementSummaries(array $summaries, string $type) {
+ protected function assertRequirementSummaries(array $summaries, string $type): void {
// The selectors are different for Claro.
$is_claro = stripos($this->getSession()->getPage()->getContent(), 'claro/css/theme/maintenance-page.css') !== FALSE;
diff --git a/core/tests/Drupal/Tests/SchemaCheckTestTrait.php b/core/tests/Drupal/Tests/SchemaCheckTestTrait.php
index 26f97c482b4c..177744e9a2de 100644
--- a/core/tests/Drupal/Tests/SchemaCheckTestTrait.php
+++ b/core/tests/Drupal/Tests/SchemaCheckTestTrait.php
@@ -24,7 +24,7 @@ trait SchemaCheckTestTrait {
* @param array $config_data
* The configuration data.
*/
- public function assertConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data) {
+ public function assertConfigSchema(TypedConfigManagerInterface $typed_config, $config_name, $config_data): void {
$check = $this->checkConfigSchema($typed_config, $config_name, $config_data);
$message = '';
if ($check === FALSE) {
@@ -46,7 +46,7 @@ trait SchemaCheckTestTrait {
* @param string $config_name
* The configuration name.
*/
- public function assertConfigSchemaByName($config_name) {
+ public function assertConfigSchemaByName($config_name): void {
$config = $this->config($config_name);
$this->assertConfigSchema(\Drupal::service('config.typed'), $config->getName(), $config->get());
}
diff --git a/core/tests/Drupal/Tests/UnitTestCase.php b/core/tests/Drupal/Tests/UnitTestCase.php
index 2257148d0698..b279bd1ed250 100644
--- a/core/tests/Drupal/Tests/UnitTestCase.php
+++ b/core/tests/Drupal/Tests/UnitTestCase.php
@@ -13,6 +13,7 @@ use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait;
use Drupal\TestTools\Extension\Dump\DebugDump;
use PHPUnit\Framework\Attributes\BeforeClass;
+use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\VarDumper\VarDumper;
@@ -210,4 +211,32 @@ abstract class UnitTestCase extends TestCase {
return $class_resolver;
}
+ /**
+ * Set up a traversable class mock to return specific items when iterated.
+ *
+ * Test doubles for types extending \Traversable are required to implement
+ * \Iterator which requires setting up five methods. Instead, this helper
+ * can be used.
+ *
+ * @param \PHPUnit\Framework\MockObject\MockObject&\Iterator $mock
+ * A mock object mocking a traversable class.
+ * @param array $items
+ * The items to return when this mock is iterated.
+ *
+ * @return \PHPUnit\Framework\MockObject\MockObject&\Iterator
+ * The same mock object ready to be iterated.
+ *
+ * @template T of \PHPUnit\Framework\MockObject\MockObject&\Iterator
+ * @phpstan-param T $mock
+ * @phpstan-return T
+ * @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103
+ */
+ protected function setupMockIterator(MockObject&\Iterator $mock, array $items): MockObject&\Iterator {
+ $iterator = new \ArrayIterator($items);
+ foreach (get_class_methods(\Iterator::class) as $method) {
+ $mock->method($method)->willReturnCallback([$iterator, $method]);
+ }
+ return $mock;
+ }
+
}
diff --git a/core/tests/Drupal/Tests/UpdatePathTestTrait.php b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
index c79916fca289..deaa20997fb8 100644
--- a/core/tests/Drupal/Tests/UpdatePathTestTrait.php
+++ b/core/tests/Drupal/Tests/UpdatePathTestTrait.php
@@ -36,7 +36,7 @@ trait UpdatePathTestTrait {
* @param string|null $update_url
* The update URL.
*/
- protected function runUpdates($update_url = NULL) {
+ protected function runUpdates($update_url = NULL): void {
if (!$update_url) {
$update_url = Url::fromRoute('system.db_update');
}
@@ -167,7 +167,7 @@ trait UpdatePathTestTrait {
/**
* Tests the selection page.
*/
- protected function doSelectionTest() {
+ protected function doSelectionTest(): void {
// No-op. Tests wishing to do test the selection page or the general
// update.php environment before running update.php can override this method
// and implement their required tests.
@@ -176,7 +176,7 @@ trait UpdatePathTestTrait {
/**
* Installs the update_script_test module and makes an update available.
*/
- protected function ensureUpdatesToRun() {
+ protected function ensureUpdatesToRun(): void {
\Drupal::service('module_installer')->install(['update_script_test']);
// Reset the schema so there is an update to run.
\Drupal::service('update.update_hook_registry')->setInstalledVersion('update_script_test', 8000);
diff --git a/core/tests/PHPStan/composer.json b/core/tests/PHPStan/composer.json
index ab10409fcbff..5d5e51651688 100644
--- a/core/tests/PHPStan/composer.json
+++ b/core/tests/PHPStan/composer.json
@@ -2,8 +2,8 @@
"name": "drupal/phpstan-testing",
"description": "Tests Drupal core's PHPStan rules",
"require-dev": {
- "phpunit/phpunit": "^10",
- "phpstan/phpstan": "2.1.12"
+ "phpunit/phpunit": "^11",
+ "phpstan/phpstan": "2.1.17"
},
"license": "GPL-2.0-or-later",
"autoload": {
diff --git a/core/tests/fixtures/config_install/multilingual/views.view.content.yml b/core/tests/fixtures/config_install/multilingual/views.view.content.yml
index 20afac251c06..5f7a02fde704 100644
--- a/core/tests/fixtures/config_install/multilingual/views.view.content.yml
+++ b/core/tests/fixtures/config_install/multilingual/views.view.content.yml
@@ -562,6 +562,7 @@ display:
empty_table: true
caption: ''
description: ''
+ class: ''
row:
type: fields
query:
diff --git a/core/tests/fixtures/config_install/multilingual/views.view.files.yml b/core/tests/fixtures/config_install/multilingual/views.view.files.yml
index d7261ea7b7cf..b74df66290c1 100644
--- a/core/tests/fixtures/config_install/multilingual/views.view.files.yml
+++ b/core/tests/fixtures/config_install/multilingual/views.view.files.yml
@@ -713,6 +713,7 @@ display:
empty_table: true
caption: ''
description: ''
+ class: ''
row:
type: fields
query:
@@ -1104,6 +1105,7 @@ display:
empty_table: true
caption: ''
description: ''
+ class: ''
row:
type: fields
options: { }
diff --git a/core/tests/fixtures/config_install/multilingual/views.view.glossary.yml b/core/tests/fixtures/config_install/multilingual/views.view.glossary.yml
index cc63d8642feb..37af1f766bbd 100644
--- a/core/tests/fixtures/config_install/multilingual/views.view.glossary.yml
+++ b/core/tests/fixtures/config_install/multilingual/views.view.glossary.yml
@@ -346,6 +346,7 @@ display:
summary: ''
order: asc
empty_table: false
+ class: ''
row:
type: fields
options:
diff --git a/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml b/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml
index 4c48d1332245..8c0aa01004ee 100644
--- a/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml
+++ b/core/tests/fixtures/config_install/multilingual/views.view.user_admin_people.yml
@@ -854,6 +854,7 @@ display:
sticky: false
summary: ''
empty_table: true
+ class: ''
row:
type: fields
query:
diff --git a/core/tests/fixtures/config_install/multilingual/views.view.watchdog.yml b/core/tests/fixtures/config_install/multilingual/views.view.watchdog.yml
index 00ef2451f3aa..7776386d9907 100644
--- a/core/tests/fixtures/config_install/multilingual/views.view.watchdog.yml
+++ b/core/tests/fixtures/config_install/multilingual/views.view.watchdog.yml
@@ -657,6 +657,7 @@ display:
empty_table: false
caption: ''
description: ''
+ class: ''
row:
type: fields
query:
diff --git a/core/tests/fixtures/default_content_broken/no-uuid.yml b/core/tests/fixtures/default_content_broken/no-uuid.yml
new file mode 100644
index 000000000000..f95a15c6463a
--- /dev/null
+++ b/core/tests/fixtures/default_content_broken/no-uuid.yml
@@ -0,0 +1,23 @@
+_meta:
+ version: '1.0'
+ entity_type: block_content
+ bundle: basic
+ default_langcode: en
+default:
+ status:
+ -
+ value: true
+ info:
+ -
+ value: 'Useful Info'
+ reusable:
+ -
+ value: true
+ revision_translation_affected:
+ -
+ value: true
+ body:
+ -
+ value: "I'd love to put some useful info here."
+ format: plain_text
+ summary: ''