Skip to content
Snippets Groups Projects
Verified Commit fdf0228f 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

(cherry picked from commit b1a87360)
parent a6554fca
No related branches found
No related tags found
18 merge requests!8376Drupal views: adding more granularity to the ‘use ajax’ functionality,!8300Issue #3443586 View area displays even when parent view has no results.,!7567Issue #3153723 by quietone, Hardik_Patel_12: Change the scaffolding...,!7565Issue #3153723 by quietone, Hardik_Patel_12: Change the scaffolding...,!7509Change label "Block description" to "Block type",!7344Issue #3292350 by O'Briat, KlemenDEV, hswong3i, smustgrave, quietone: Update...,!6922Issue #3412959 by quietone, smustgrave, longwave: Fix 12 'un' words,!6848Issue #3417553 by longwave: Remove withConsecutive() in CacheCollectorTest,!6720Revert "Issue #3358581 by pfrenssen, _tarik_, a.dmitriiev, smustgrave:...,!6560Update ClaroPreRender.php, confirming classes provided are in array format,!6528Issue #3414261 by catch: Add authenticated user umami performance tests,!6501Issue #3263668 by omkar-pd, Wim Leers, hooroomoo: Re-enable inline form errors...,!6354Draft: Issue #3380392 by phma: Updating language weight from the overview reverts label if translated,!6324Issue #3416723 by Ludo.R: Provide a "node type" views default argument,!6119Issue #3405704 by Spokje, longwave: symfony/psr-http-message-bridge major version bump,!5950Issue #3403653 by alexpott, longwave: Incorporate improvements to how contrib runs PHPStan to core,!5858Issue #3401971 by fjgarlin: Test-only job shouldn't require constant rebases...,!5716Draft: Issue #3401102 by Spokje, longwave, smustgrave: Nightwatch artifacts on GitLab not retained
Pipeline #60310 passed
Pipeline: drupal

#60311

    Showing
    with 459 additions and 87 deletions
    ......@@ -2205,7 +2205,9 @@ public static function removeDatabaseEntriesFromDebugBacktrace(array $backtrace,
    * The debug backtrace.
    */
    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 @@
    class PerformanceTestBase extends WebDriverTestBase {
    use PerformanceTestTrait;
    /**
    * {@inheritdoc}
    */
    protected static $modules = ['performance_test'];
    /**
    * {@inheritdoc}
    */
    ......
    ......@@ -19,6 +19,26 @@ class PerformanceData {
    */
    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.
    */
    ......@@ -64,6 +84,86 @@ public function getScriptCount(): int {
    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.
    *
    ......
    ......@@ -93,12 +93,63 @@ private function doGetMinkDriverArgs(): string {
    * A PerformanceData value object.
    */
    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->getDriver()->getWebDriverSession()->log('performance');
    $collection = \Drupal::keyValue('performance_test');
    $collection->deleteAll();
    $return = $callable();
    $performance_data = $this->processChromeDriverPerformanceLogs($service_name);
    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;
    ......
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment