diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 0db7124a4a57efa0c882a444a03012d47b8da4b2..37a265f4ec87345a86b83e7a8be7cf5f86a2a020 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -27,6 +27,7 @@ use Drupal\Core\Render\MainContent\MainContentRenderersPass; use Drupal\Core\Site\Settings; use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -142,6 +143,12 @@ protected function registerTest(ContainerBuilder $container) { $container ->register('test.http_client.middleware', 'Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware') ->addTag('http_client_middleware'); + // Add the wait terminate middleware which acquires a lock to signal request + // termination to the test runner. + $container + ->register('test.http_middleware.wait_terminate_middleware', 'Drupal\Core\Test\StackMiddleware\TestWaitTerminateMiddleware') + ->setArguments([new Reference('state'), new Reference('lock')]) + ->addTag('http_middleware', ['priority' => -1024]); } } diff --git a/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php b/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php index 99bb449e2f07ce3fa0060ca0a5be4dce9a31ef6e..9b8d675b28f2d134fdaae940b2d3f8694a6dfef1 100644 --- a/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php +++ b/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php @@ -31,6 +31,13 @@ public function __invoke() { if (!drupal_valid_test_ua()) { return $response; } + if (!empty($response->getHeader('X-Drupal-Wait-Terminate')[0])) { + $lock = \Drupal::lock(); + if (!$lock->acquire('test_wait_terminate')) { + $lock->wait('test_wait_terminate'); + } + $lock->release('test_wait_terminate'); + } $headers = $response->getHeaders(); foreach ($headers as $header_name => $header_values) { if (preg_match('/^X-Drupal-Assertion-[0-9]+$/', $header_name, $matches)) { diff --git a/core/lib/Drupal/Core/Test/StackMiddleware/TestWaitTerminateMiddleware.php b/core/lib/Drupal/Core/Test/StackMiddleware/TestWaitTerminateMiddleware.php new file mode 100644 index 0000000000000000000000000000000000000000..58cc844024c4482c9e83edfd0ab986c399750256 --- /dev/null +++ b/core/lib/Drupal/Core/Test/StackMiddleware/TestWaitTerminateMiddleware.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\Core\Test\StackMiddleware; + +use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\State\StateInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\HttpKernelInterface; + +/** + * Acquire a lock to signal request termination to the test runner. + */ +class TestWaitTerminateMiddleware implements HttpKernelInterface { + + /** + * Constructs a test wait terminate stack middleware object. + * + * @param \Symfony\Component\HttpKernel\HttpKernelInterface $httpKernel + * The decorated kernel. + * @param \Drupal\Core\State\StateInterface $state + * The state server. + * @param \Drupal\Core\Lock\LockBackendInterface $lock + * The lock backend. + */ + public function __construct( + protected HttpKernelInterface $httpKernel, + protected StateInterface $state, + protected LockBackendInterface $lock + ) { + } + + /** + * {@inheritdoc} + */ + public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response { + $result = $this->httpKernel->handle($request, $type, $catch); + + if ($this->state->get('drupal.test_wait_terminate')) { + // Set a header on the response to instruct the test runner that it must + // await the lock. Note that the lock acquired here is automatically + // released from within a shutdown function. + $this->lock->acquire('test_wait_terminate'); + $result->headers->set('X-Drupal-Wait-Terminate', '1'); + } + + return $result; + } + +} diff --git a/core/modules/jsonapi/tests/src/Functional/NodeTest.php b/core/modules/jsonapi/tests/src/Functional/NodeTest.php index d0893201e1a875a4edcab376c89de20c2be6f05d..2496d242f23f4209cfa90701589596d92b6859f0 100644 --- a/core/modules/jsonapi/tests/src/Functional/NodeTest.php +++ b/core/modules/jsonapi/tests/src/Functional/NodeTest.php @@ -11,6 +11,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait; +use Drupal\Tests\WaitTerminateTestTrait; use Drupal\user\Entity\User; use GuzzleHttp\RequestOptions; @@ -22,6 +23,7 @@ class NodeTest extends ResourceTestBase { use CommonCollectionFilterAccessTestPatternsTrait; + use WaitTerminateTestTrait; /** * {@inheritdoc} @@ -315,6 +317,8 @@ public function testPatchPath() { * {@inheritdoc} */ public function testGetIndividual() { + $this->setWaitForTerminate(); + parent::testGetIndividual(); $this->assertCacheableNormalizations(); @@ -393,15 +397,11 @@ protected function assertCacheableNormalizations(): void { $request_options = $this->getAuthenticationRequestOptions(); $request_options[RequestOptions::QUERY] = ['fields' => ['node--camelids' => 'title']]; $this->request('GET', $url, $request_options); - // Cacheable normalizations are written after the response is flushed to - // the client; give the server a chance to complete this work. - sleep(1); // Ensure the normalization cache is being incrementally built. After // requesting the title, only the title is in the cache. $this->assertNormalizedFieldsAreCached(['title']); $request_options[RequestOptions::QUERY] = ['fields' => ['node--camelids' => 'field_rest_test']]; $this->request('GET', $url, $request_options); - sleep(1); // After requesting an additional field, then that field is in the cache and // the old one is still there. $this->assertNormalizedFieldsAreCached(['title', 'field_rest_test']); diff --git a/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php b/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php index 2c1f529f5041043bd52ae556299e3c3cfe1f3cc7..eb11a0cd5f050138ccb498d6cc48ce23380858af 100644 --- a/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php +++ b/core/modules/language/tests/src/Functional/ConfigurableLanguageManagerTest.php @@ -11,6 +11,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\WaitTerminateTestTrait; /** * Tests Language Negotiation. @@ -21,6 +22,8 @@ */ class ConfigurableLanguageManagerTest extends BrowserTestBase { + use WaitTerminateTestTrait; + /** * {@inheritdoc} */ @@ -45,6 +48,8 @@ class ConfigurableLanguageManagerTest extends BrowserTestBase { protected function setUp(): void { parent::setUp(); + $this->setWaitForTerminate(); + /** @var \Drupal\user\UserInterface $user */ $user = $this->createUser([], '', TRUE); $this->drupalLogin($user); @@ -266,16 +271,4 @@ public function testUserProfileTranslationWithPreferredAdminLanguage() { $assert_session->pageTextNotContains($field_label_es); } - /** - * {@inheritdoc} - */ - protected function drupalGet($path, array $options = [], array $headers = []) { - $response = parent::drupalGet($path, $options, $headers); - // The \Drupal\locale\LocaleTranslation service clears caches after the - // response is flushed to the client; wait for Drupal to perform its - // termination work before continuing. - sleep(1); - return $response; - } - } diff --git a/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php b/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php index 5b43ad50bf1ab7666825b85cc705682280434ebd..34e763e7ad136aa5ef1fc03a5860128a644a9707 100644 --- a/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php +++ b/core/modules/locale/tests/src/Functional/LocaleLocaleLookupTest.php @@ -5,6 +5,7 @@ use Drupal\Component\Gettext\PoItem; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\WaitTerminateTestTrait; /** * Tests LocaleLookup. @@ -13,6 +14,8 @@ */ class LocaleLocaleLookupTest extends BrowserTestBase { + use WaitTerminateTestTrait; + /** * Modules to enable. * @@ -31,6 +34,8 @@ class LocaleLocaleLookupTest extends BrowserTestBase { protected function setUp(): void { parent::setUp(); + $this->setWaitForTerminate(); + // Change the language default object to different values. ConfigurableLanguage::createFromLangcode('fr')->save(); $this->config('system.site')->set('default_langcode', 'fr')->save(); @@ -68,11 +73,6 @@ public function testLanguageFallbackDefaults() { * @dataProvider providerTestFixOldPluralStyle */ public function testFixOldPluralStyle($translation_value, $expected) { - // The \Drupal\locale\LocaleTranslation service stores localization cache - // data after the response is flushed to the client. We do not want to race - // with any string translations that may be saving from the login in - // ::setUp(). - sleep(1); $string_storage = \Drupal::service('locale.storage'); $string = $string_storage->findString(['source' => 'Member for', 'context' => '']); $lid = $string->getId(); diff --git a/core/modules/path/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php index f6ebeb9be328cdc98a55db60470b88ab0377bbe7..080725afb5e6d79b3edb03c4ce857882a2f8e81d 100644 --- a/core/modules/path/tests/src/Functional/PathAliasTest.php +++ b/core/modules/path/tests/src/Functional/PathAliasTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Database; use Drupal\Core\Url; +use Drupal\Tests\WaitTerminateTestTrait; /** * Tests modifying path aliases from the UI. @@ -13,6 +14,8 @@ */ class PathAliasTest extends PathTestBase { + use WaitTerminateTestTrait; + /** * Modules to enable. * @@ -40,6 +43,8 @@ protected function setUp(): void { 'access content overview', ]); $this->drupalLogin($web_user); + + $this->setWaitForTerminate(); } /** @@ -66,9 +71,6 @@ public function testPathCache() { \Drupal::cache('data')->deleteAll(); // Make sure the path is not converted to the alias. $this->drupalGet(trim($edit['path[0][value]'], '/'), ['alias' => TRUE]); - // The \Drupal\path_alias\AliasWhitelist service performs cache clears after - // Drupal has flushed the response to the client; wait for this to finish. - sleep(1); $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.'); // Visit the alias for the node and confirm a cache entry is created. @@ -76,7 +78,6 @@ public function testPathCache() { // @todo Remove this once https://www.drupal.org/node/2480077 lands. Cache::invalidateTags(['rendered']); $this->drupalGet(trim($edit['alias[0][value]'], '/')); - sleep(1); $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.'); } diff --git a/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php b/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php index d61b08f8f7ab360f751c42117c436ae157873d9c..6b5da5aec68c8bf8908c2f1547703d6e6e391b04 100644 --- a/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php +++ b/core/modules/workspaces/tests/src/Functional/PathWorkspacesTest.php @@ -4,6 +4,7 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\WaitTerminateTestTrait; use Drupal\workspaces\Entity\Workspace; /** @@ -15,6 +16,7 @@ class PathWorkspacesTest extends BrowserTestBase { use WorkspaceTestUtilities; + use WaitTerminateTestTrait; /** * {@inheritdoc} @@ -67,6 +69,7 @@ protected function setUp(): void { \Drupal::entityTypeManager()->clearCachedDefinitions(); $this->setupWorkspaceSwitcherBlock(); + $this->setWaitForTerminate(); } /** @@ -106,11 +109,6 @@ public function testPathAliases() { // Publish the workspace and check that the alias can be accessed in Live. $stage->publish(); $this->assertAccessiblePaths([$path]); - - // The \Drupal\path_alias\AliasWhitelist service performs cache clears after - // Drupal has flushed the response to the client; wait for this to finish. - sleep(1); - $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1')); } @@ -155,11 +153,6 @@ public function testPathAliasesUserSwitch() { $this->drupalLogout(); $this->assertAccessiblePaths([$path]); - - // The \Drupal\path_alias\AliasWhitelist service performs cache clears after - // Drupal has flushed the response to the client; wait for this to finish. - sleep(1); - $this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1')); } diff --git a/core/tests/Drupal/Tests/WaitTerminateTestTrait.php b/core/tests/Drupal/Tests/WaitTerminateTestTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..008b1555012801054f9055554a993a345d4e3ea6 --- /dev/null +++ b/core/tests/Drupal/Tests/WaitTerminateTestTrait.php @@ -0,0 +1,21 @@ +<?php + +namespace Drupal\Tests; + +/** + * Provides a method to enforce that requests will wait for the terminate event. + */ +trait WaitTerminateTestTrait { + + /** + * Specify that subsequent requests must wait for the terminate event. + * + * The terminate event is fired after a response is sent to the user agent. + * Tests with assertions which operate on data computed during the terminate + * event need to enable this. + */ + protected function setWaitForTerminate() { + $this->container->get('state')->set('drupal.test_wait_terminate', TRUE); + } + +}