Skip to content
Snippets Groups Projects
Verified Commit b1a87360 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3352851 by catch, Fabianx, mondrake, xjm, alexpott: Allow assertions on...

Issue #3352851 by catch, Fabianx, mondrake, xjm, alexpott: Allow assertions on the number of database queries run during tests
parent 5cd249e9
No related branches found
No related tags found
37 merge requests!12227Issue #3181946 by jonmcl, mglaman,!8528Issue #3456871 by Tim Bozeman: Support NULL services,!8323Fix source code editing and in place front page site studio editing.,!6278Issue #3187770 by godotislate, smustgrave, catch, quietone: Views Rendered...,!3878Removed unused condition head title for views,!38582585169-10.1.x,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3668Resolve #3347842 "Deprecate the trusted",!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3546refactored dialog.pcss file,!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3502Issue #3335308: Confusing behavior with FormState::setFormState and FormState::setMethod,!3478Issue #3337882: Deleted menus are not removed from content type config,!3452Issue #3332701: Refactor Claro's tablesort-indicator stylesheet,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3147Issue #3328457: Replace most substr($a, $i) where $i is negative with str_ends_with(),!3146Issue #3328456: Replace substr($a, 0, $i) with str_starts_with(),!3133core/modules/system/css/components/hidden.module.css,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2614Issue #2981326: Replace non-test usages of \Drupal::logger() with IoC injection,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!844Resolve #3036010 "Updaters",!673Issue #3214208: FinishResponseSubscriber could create duplicate headers,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493
Pipeline #60305 passed
Pipeline: drupal

#60306

    Showing
    with 459 additions and 87 deletions
    ...@@ -2205,7 +2205,9 @@ public static function removeDatabaseEntriesFromDebugBacktrace(array $backtrace, ...@@ -2205,7 +2205,9 @@ public static function removeDatabaseEntriesFromDebugBacktrace(array $backtrace,
    * The debug backtrace. * The debug backtrace.
    */ */
    protected function getDebugBacktrace(): array { protected function getDebugBacktrace(): array {
    return debug_backtrace(); // @todo: allow a backtrace including all arguments as an option.
    // See https://www.drupal.org/project/drupal/issues/3401906
    return debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
    } }
    } }
    name: 'Performance test'
    type: module
    description: 'Supports performance testing with PerformanceTestTrait'
    package: Testing
    version: VERSION
    services:
    Drupal\performance_test\PerformanceDataCollector:
    tags:
    - { name: event_subscriber }
    - { name: needs_destruction, priority: -1000 }
    Drupal\performance_test\DatabaseEventEnabler:
    arguments: ['@database']
    tags:
    - { name: http_middleware, priority: 1000, responder: true }
    <?php
    namespace Drupal\performance_test;
    use Drupal\Core\Database\Connection;
    use Symfony\Component\HttpKernel\HttpKernelInterface;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    use Drupal\Core\Database\Event\StatementExecutionEndEvent;
    use Drupal\Core\Database\Event\StatementExecutionStartEvent;
    class DatabaseEventEnabler implements HttpKernelInterface {
    public function __construct(protected readonly HttpKernelInterface $httpKernel, protected readonly Connection $connection) {}
    /**
    * {@inheritdoc}
    */
    public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response {
    if ($type === static::MAIN_REQUEST) {
    $this->connection->enableEvents([
    // StatementExecutionStartEvent must be enabled in order for
    // StatementExecutionEndEvent to be fired, even though we only subscribe
    // to the latter event.
    StatementExecutionStartEvent::class,
    StatementExecutionEndEvent::class,
    ]);
    }
    return $this->httpKernel->handle($request, $type, $catch);
    }
    }
    <?php
    namespace Drupal\performance_test;
    use Drupal\Core\Database\Event\StatementExecutionEndEvent;
    use Drupal\Core\DestructableInterface;
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    class PerformanceDataCollector implements EventSubscriberInterface, DestructableInterface {
    /**
    * Database events collected during the request.
    *
    * @var Drupal\Core\Database\Event\StatementExecutionEndEvent[]
    */
    protected array $databaseEvents = [];
    /**
    * {@inheritdoc}
    */
    public static function getSubscribedEvents(): array {
    return [
    StatementExecutionEndEvent::class => 'onStatementExecutionEnd',
    ];
    }
    /**
    * Logs database statements.
    */
    public function onStatementExecutionEnd(StatementExecutionEndEvent $event): void {
    // Use the event object as a value object.
    $this->databaseEvents[] = $event;
    }
    /**
    * {@inheritdoc}
    */
    public function destruct(): void {
    // Get the events now before issuing any more database queries so that this
    // logging does not become part of the recorded data.
    $database_events = $this->databaseEvents;
    // Deliberately do not use an injected key value service to avoid any
    // overhead up until this point.
    $collection = \Drupal::keyValue('performance_test');
    $existing_data = $collection->get('performance_test_data') ?? ['database_events' => []];
    $existing_data['database_events'] = array_merge($existing_data['database_events'], $database_events);
    $collection->set('performance_test_data', $existing_data);
    }
    }
    <?php
    namespace Drupal\Tests\standard\FunctionalJavascript;
    use Drupal\Tests\PerformanceData;
    use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
    use Drupal\node\NodeInterface;
    /**
    * Tests that anonymous users are not served any JavaScript.
    *
    * This is tested with the core modules that are enabled in the 'standard'
    * profile.
    *
    * @group Common
    */
    class NoJavaScriptAnonymousTest extends PerformanceTestBase {
    /**
    * {@inheritdoc}
    */
    protected $defaultTheme = 'stark';
    /**
    * {@inheritdoc}
    */
    protected $profile = 'standard';
    /**
    * {@inheritdoc}
    */
    protected function setUp(): void {
    parent::setUp();
    // Grant the anonymous user the permission to look at user profiles.
    user_role_grant_permissions('anonymous', ['access user profiles']);
    }
    /**
    * Tests that anonymous users are not served any JavaScript.
    */
    public function testNoJavaScript() {
    // Create a node of content type 'article' that is listed on the frontpage.
    $this->drupalCreateNode([
    'type' => 'article',
    'promote' => NodeInterface::PROMOTED,
    ]);
    // Test frontpage.
    $performance_data = $this->collectPerformanceData(function () {
    $this->drupalGet('');
    });
    $this->assertNoJavaScript($performance_data);
    // Test node page.
    $performance_data = $this->collectPerformanceData(function () {
    $this->drupalGet('node/1');
    });
    $this->assertNoJavaScript($performance_data);
    // Test user profile page.
    $user = $this->drupalCreateUser();
    $performance_data = $this->collectPerformanceData(function () use ($user) {
    $this->drupalGet('user/' . $user->id());
    });
    $this->assertNoJavaScript($performance_data);
    }
    /**
    * Passes if no JavaScript is found on the page.
    *
    * @param Drupal\Tests\PerformanceData $performance_data
    * A PerformanceData value object.
    *
    * @internal
    */
    protected function assertNoJavaScript(PerformanceData $performance_data): void {
    // Ensure drupalSettings is not set.
    $settings = $this->getDrupalSettings();
    $this->assertEmpty($settings, 'drupalSettings is not set.');
    $this->assertSession()->responseNotMatches('/\.js/');
    $this->assertSame(0, $performance_data->getScriptCount());
    }
    }
    <?php
    namespace Drupal\Tests\standard\FunctionalJavascript;
    use Drupal\FunctionalJavascriptTests\PerformanceTestBase;
    use Drupal\Tests\PerformanceData;
    use Drupal\node\NodeInterface;
    /**
    * Tests the performance of basic functionality in the standard profile.
    *
    * Stark is used as the default theme so that this test is not Olivero specific.
    *
    * @group Common
    */
    class StandardPerformanceTest extends PerformanceTestBase {
    /**
    * {@inheritdoc}
    */
    protected $defaultTheme = 'stark';
    /**
    * {@inheritdoc}
    */
    protected $profile = 'standard';
    /**
    * {@inheritdoc}
    */
    protected function setUp(): void {
    parent::setUp();
    // Grant the anonymous user the permission to look at user profiles.
    user_role_grant_permissions('anonymous', ['access user profiles']);
    }
    /**
    * Tests performance for anonymous users.
    */
    public function testAnonymous() {
    // Create two nodes to be shown on the front page.
    $this->drupalCreateNode([
    'type' => 'article',
    'promote' => NodeInterface::PROMOTED,
    ]);
    // Request a page that we're not otherwise explicitly testing to warm some
    // caches.
    $this->drupalGet('search');
    // Test frontpage.
    $performance_data = $this->collectPerformanceData(function () {
    $this->drupalGet('');
    });
    $this->assertNoJavaScript($performance_data);
    // This test observes a variable number of cache gets and sets, so to avoid
    // random test failures, assert greater than equal the highest and lowest
    // number of observed during test runs.
    // See https://www.drupal.org/project/drupal/issues/3402610
    $this->assertGreaterThanOrEqual(58, $performance_data->getQueryCount());
    $this->assertLessThanOrEqual(66, $performance_data->getQueryCount());
    $this->assertGreaterThanOrEqual(129, $performance_data->getCacheGetCount());
    $this->assertLessThanOrEqual(132, $performance_data->getCacheGetCount());
    $this->assertSame(59, $performance_data->getCacheSetCount());
    $this->assertSame(0, $performance_data->getCacheDeleteCount());
    // Test node page.
    $performance_data = $this->collectPerformanceData(function () {
    $this->drupalGet('node/1');
    });
    $this->assertNoJavaScript($performance_data);
    $this->assertSame(38, $performance_data->getQueryCount());
    // This test observes a variable number of cache gets and sets, so to avoid
    // random test failures, assert greater than equal the highest and lowest
    // number of queries observed during test runs.
    // See https://www.drupal.org/project/drupal/issues/3402610
    $this->assertGreaterThanOrEqual(87, $performance_data->getCacheGetCount());
    $this->assertLessThanOrEqual(88, $performance_data->getCacheGetCount());
    $this->assertSame(20, $performance_data->getCacheSetCount());
    $this->assertSame(0, $performance_data->getCacheDeleteCount());
    // Test user profile page.
    $user = $this->drupalCreateUser();
    $performance_data = $this->collectPerformanceData(function () use ($user) {
    $this->drupalGet('user/' . $user->id());
    });
    $this->assertNoJavaScript($performance_data);
    $this->assertSame(40, $performance_data->getQueryCount());
    // This test observes a variable number of cache gets and sets, so to avoid
    // random test failures, assert greater than equal the highest and lowest
    // number of queries observed during test runs.
    // See https://www.drupal.org/project/drupal/issues/3402610
    $this->assertGreaterThanOrEqual(74, $performance_data->getCacheGetCount());
    $this->assertLessThanOrEqual(80, $performance_data->getCacheGetCount());
    $this->assertSame(19, $performance_data->getCacheSetCount());
    $this->assertSame(0, $performance_data->getCacheDeleteCount());
    }
    /**
    * Tests the performance of logging in.
    */
    public function testLogin(): void {
    // Create a user and log them in to warm all caches. Manually submit the
    // form so that we repeat the same steps when recording performance data. Do
    // this twice so that any caches which take two requests to warm are also
    // covered.
    $account = $this->drupalCreateUser();
    foreach (range(0, 1) as $index) {
    $this->drupalGet('node');
    $this->drupalGet('user/login');
    $this->submitLoginForm($account);
    $this->drupalLogout();
    }
    $this->drupalGet('node');
    $this->drupalGet('user/login');
    $performance_data = $this->collectPerformanceData(function () use ($account) {
    $this->submitLoginForm($account);
    });
    // This test observes a variable number of database queries, so to avoid
    // random test failures, assert greater than equal the highest and lowest
    // number of queries observed during test runs.
    // See https://www.drupal.org/project/drupal/issues/3402610
    $this->assertLessThanOrEqual(40, $performance_data->getQueryCount());
    $this->assertGreaterThanOrEqual(39, $performance_data->getQueryCount());
    $this->assertSame(28, $performance_data->getCacheGetCount());
    $this->assertSame(1, $performance_data->getCacheSetCount());
    $this->assertSame(1, $performance_data->getCacheDeleteCount());
    }
    /**
    * Tests the performance of logging in via the user login block.
    */
    public function testLoginBlock(): void {
    $this->drupalPlaceBlock('user_login_block');
    // Create a user and log them in to warm all caches. Manually submit the
    // form so that we repeat the same steps when recording performance data. Do
    // this twice so that any caches which take two requests to warm are also
    // covered.
    $account = $this->drupalCreateUser();
    $this->drupalLogout();
    foreach (range(0, 1) as $index) {
    $this->drupalGet('node');
    $this->assertSession()->responseContains('Password');
    $this->submitLoginForm($account);
    $this->drupalLogout();
    }
    $this->drupalGet('node');
    $this->assertSession()->responseContains('Password');
    $performance_data = $this->collectPerformanceData(function () use ($account) {
    $this->submitLoginForm($account);
    });
    $this->assertSame(48, $performance_data->getQueryCount());
    $this->assertSame(30, $performance_data->getCacheGetCount());
    // This test observes a variable number of cache sets, so to avoid random
    // test failures, assert greater than equal the highest and lowest number
    // observed during test runs.
    // See https://www.drupal.org/project/drupal/issues/3402610
    $this->assertLessThanOrEqual(4, $performance_data->getCacheSetCount());
    $this->assertGreaterThanOrEqual(1, $performance_data->getCacheSetCount());
    $this->assertSame(1, $performance_data->getCacheDeleteCount());
    }
    /**
    * Submit the user login form.
    */
    protected function submitLoginForm($account) {
    $this->submitForm([
    'name' => $account->getAccountName(),
    'pass' => $account->passRaw,
    ], 'Log in');
    }
    /**
    * Passes if no JavaScript is found on the page.
    *
    * @param Drupal\Tests\PerformanceData $performance_data
    * A PerformanceData value object.
    *
    * @internal
    */
    protected function assertNoJavaScript(PerformanceData $performance_data): void {
    // Ensure drupalSettings is not set.
    $settings = $this->getDrupalSettings();
    $this->assertEmpty($settings, 'drupalSettings is not set.');
    $this->assertSession()->responseNotMatches('/\.js/');
    $this->assertSame(0, $performance_data->getScriptCount());
    }
    /**
    * Provides an empty implementation to prevent the resetting of caches.
    */
    protected function refreshVariables() {}
    }
    ...@@ -15,6 +15,11 @@ ...@@ -15,6 +15,11 @@
    class PerformanceTestBase extends WebDriverTestBase { class PerformanceTestBase extends WebDriverTestBase {
    use PerformanceTestTrait; use PerformanceTestTrait;
    /**
    * {@inheritdoc}
    */
    protected static $modules = ['performance_test'];
    /** /**
    * {@inheritdoc} * {@inheritdoc}
    */ */
    ......
    ...@@ -19,6 +19,26 @@ class PerformanceData { ...@@ -19,6 +19,26 @@ class PerformanceData {
    */ */
    protected int $scriptCount = 0; protected int $scriptCount = 0;
    /**
    * The number of database queries recorded.
    */
    protected int $queryCount = 0;
    /**
    * The number of cache gets recorded.
    */
    protected int $cacheGetCount = 0;
    /**
    * The number of cache sets recorded.
    */
    protected int $cacheSetCount = 0;
    /**
    * The number of cache deletes recorded.
    */
    protected int $cacheDeleteCount = 0;
    /** /**
    * The original return value. * The original return value.
    */ */
    ...@@ -64,6 +84,86 @@ public function getScriptCount(): int { ...@@ -64,6 +84,86 @@ public function getScriptCount(): int {
    return $this->scriptCount; return $this->scriptCount;
    } }
    /**
    * Sets the query count.
    *
    * @param int $count
    * The number of database queries recorded.
    */
    public function setQueryCount(int $count): void {
    $this->queryCount = $count;
    }
    /**
    * Gets the query count.
    *
    * @return int
    * The number of database queries recorded.
    */
    public function getQueryCount(): int {
    return $this->queryCount;
    }
    /**
    * Sets the cache get count.
    *
    * @param int $count
    * The number of cache gets recorded.
    */
    public function setCacheGetCount(int $count): void {
    $this->cacheGetCount = $count;
    }
    /**
    * Gets the cache get count.
    *
    * @return int
    * The number of cache gets recorded.
    */
    public function getCacheGetCount(): int {
    return $this->cacheGetCount;
    }
    /**
    * Sets the cache set count.
    *
    * @param int $count
    * The number of cache sets recorded.
    */
    public function setCacheSetCount(int $count): void {
    $this->cacheSetCount = $count;
    }
    /**
    * Gets the cache set count.
    *
    * @return int
    * The number of cache sets recorded.
    */
    public function getCacheSetCount(): int {
    return $this->cacheSetCount;
    }
    /**
    * Sets the cache delete count.
    *
    * @param int $count
    * The number of cache deletes recorded.
    */
    public function setCacheDeleteCount(int $count): void {
    $this->cacheDeleteCount = $count;
    }
    /**
    * Gets the cache delete count.
    *
    * @return int
    * The number of cache deletes recorded.
    */
    public function getCacheDeleteCount(): int {
    return $this->cacheDeleteCount;
    }
    /** /**
    * Sets the original return value. * Sets the original return value.
    * *
    ......
    ...@@ -93,12 +93,63 @@ private function doGetMinkDriverArgs(): string { ...@@ -93,12 +93,63 @@ private function doGetMinkDriverArgs(): string {
    * A PerformanceData value object. * A PerformanceData value object.
    */ */
    public function collectPerformanceData(callable $callable, ?string $service_name = NULL): PerformanceData { public function collectPerformanceData(callable $callable, ?string $service_name = NULL): PerformanceData {
    // Clear all existing performance logs before collecting new data. This is
    // necessary because responses are returned back to tests prior to image
    // and asset responses are returning to the browser, and before
    // post-response tasks are guaranteed to have run. Assume that if there is
    // no performance data logged by the child request within one second, that
    // this means everything has finished.
    $collection = \Drupal::keyValue('performance_test');
    while ($collection->get('performance_test_data')) {
    $collection->deleteAll();
    sleep(1);
    }
    $session = $this->getSession(); $session = $this->getSession();
    $session->getDriver()->getWebDriverSession()->log('performance'); $session->getDriver()->getWebDriverSession()->log('performance');
    $collection = \Drupal::keyValue('performance_test');
    $collection->deleteAll();
    $return = $callable(); $return = $callable();
    $performance_data = $this->processChromeDriverPerformanceLogs($service_name); $performance_data = $this->processChromeDriverPerformanceLogs($service_name);
    if (isset($return)) { if (isset($return)) {
    $performance_data->setReturnValue($performance_data); $performance_data->setReturnValue($return);
    }
    $performance_test_data = $collection->get('performance_test_data');
    if ($performance_test_data) {
    // Separate queries into two buckets, one for queries from the cache
    // backend, and one for everything else (including those for cache tags).
    $query_count = 0;
    $cache_get_count = 0;
    $cache_set_count = 0;
    $cache_delete_count = 0;
    foreach ($performance_test_data['database_events'] as $event) {
    if (isset($event->caller['class']) && is_a(str_replace('\\\\', '\\', $event->caller['class']), '\Drupal\Core\Cache\DatabaseBackend', TRUE)) {
    $method = strtolower($event->caller['function']);
    if (str_contains($method, 'get')) {
    $cache_get_count++;
    }
    elseif (str_contains($method, 'set')) {
    $cache_set_count++;
    }
    elseif (str_contains($method, 'delete')) {
    $cache_delete_count++;
    }
    elseif ($event->caller['function'] === 'ensureBinExists') {
    // Don't record anything for ensureBinExists().
    }
    else {
    throw new \Exception("Tried to record a cache operation but did not recognize {$event->caller['function']}");
    }
    }
    else {
    $query_count++;
    }
    }
    $performance_data->setQueryCount($query_count);
    $performance_data->setCacheGetCount($cache_get_count);
    $performance_data->setCacheSetCount($cache_set_count);
    $performance_data->setCacheDeleteCount($cache_delete_count);
    } }
    return $performance_data; return $performance_data;
    ......
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment