diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml deleted file mode 100644 index d77e439b2964c..0000000000000 --- a/.github/workflows/end-to-end-tests.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: End-to-end Tests - -on: - # The end-to-end test suite was introduced in WordPress 5.3. - push: - branches: - - trunk - - '5.[3-9]' - - '[6-9].[0-9]' - tags: - - '[0-9]+.[0-9]' - - '[0-9]+.[0-9].[0-9]+' - - '![34].[0-9].[0-9]+' - - '!5.[0-2].[0-9]+' - pull_request: - branches: - - trunk - - '5.[3-9]' - - '[6-9].[0-9]' - workflow_dispatch: - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -env: - LOCAL_DIR: build - PUPPETEER_SKIP_DOWNLOAD: ${{ true }} - -jobs: - # Runs the end-to-end test suite. - e2e-tests: - name: Test with SCRIPT_DEBUG ${{ matrix.LOCAL_SCRIPT_DEBUG && 'enabled' || 'disabled' }} - uses: WordPress/wordpress-develop/.github/workflows/reusable-end-to-end-tests.yml@trunk - permissions: - contents: read - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - LOCAL_SCRIPT_DEBUG: [ true, false ] - with: - LOCAL_SCRIPT_DEBUG: ${{ matrix.LOCAL_SCRIPT_DEBUG }} - - slack-notifications: - name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk - permissions: - actions: read - contents: read - needs: [ e2e-tests ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-latest - permissions: - actions: write - needs: [ e2e-tests, slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - steps: - - name: Dispatch workflow run - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: '${{ github.run_id }}' - } - }); diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml deleted file mode 100644 index dba6091a5547a..0000000000000 --- a/.github/workflows/performance.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Performance Tests - -on: - push: - branches: - - trunk - - '6.[2-9]' - - '[7-9].[0-9]' - tags: - - '[0-9]+.[0-9]' - - '[0-9]+.[0-9].[0-9]+' - - '![45].[0-9].[0-9]+' - - '!6.[01].[0-9]+' - pull_request: - branches: - - trunk - - '6.[2-9]' - - '[7-9].[0-9]' - workflow_dispatch: - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Runs the performance test suite. - performance: - name: Performance tests ${{ matrix.memcached && '(with memcached)' || '' }} - uses: WordPress/wordpress-develop/.github/workflows/reusable-performance.yml@trunk - permissions: - contents: read - if: ${{ ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} - strategy: - fail-fast: false - matrix: - memcached: [ true, false ] - with: - memcached: ${{ matrix.memcached }} - secrets: - CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} - - slack-notifications: - name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk - permissions: - actions: read - contents: read - needs: [ performance ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-latest - permissions: - actions: write - needs: [ slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - - steps: - - name: Dispatch workflow run - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: '${{ github.run_id }}' - } - }); diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 3ea45fcdc331e..f8afb9900b5ae 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -45,7 +45,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] + php: [ '8.3' ] db-type: [ 'mysql' ] db-version: [ '5.7', '8.0', '8.4', '9.0' ] tests-domain: [ 'example.org' ] @@ -68,29 +68,6 @@ jobs: tests-domain: 'example.org' multisite: true memcached: true - # Include jobs with a port on the test domain for both single and multisite. - - os: ubuntu-latest - php: '8.4' - db-type: 'mysql' - db-version: '8.4' - tests-domain: 'example.org:8889' - multisite: false - memcached: false - - os: ubuntu-latest - php: '8.4' - db-type: 'mysql' - db-version: '8.4' - tests-domain: 'example.org:8889' - multisite: true - memcached: false - # Report test results to the Host Test Results. - - os: ubuntu-latest - db-type: 'mysql' - db-version: '8.4' - tests-domain: 'example.org' - multisite: false - memcached: false - report: true exclude: # MySQL 9.0+ will not work on PHP 7.2 & 7.3. See https://core.trac.wordpress.org/ticket/61218. @@ -124,7 +101,7 @@ jobs: fail-fast: false matrix: os: [ ubuntu-latest ] - php: [ '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4' ] + php: [ '8.3' ] db-type: [ 'mariadb' ] db-version: [ '10.4', '10.6', '10.11', '11.2' ] multisite: [ false, true ] @@ -154,74 +131,3 @@ jobs: phpunit-config: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} report: ${{ matrix.report || false }} - # - # Runs specific individual test groups. - # - specific-test-groups: - name: ${{ matrix.phpunit-test-groups }} - uses: WordPress/wordpress-develop/.github/workflows/reusable-phpunit-tests-v3.yml@trunk - permissions: - contents: read - secrets: inherit - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - php: [ '7.2', '7.4', '8.0', '8.4' ] - db-type: [ 'mysql' ] - db-version: [ '8.4' ] - phpunit-test-groups: [ 'html-api-html5lib-tests' ] - with: - php: ${{ matrix.php }} - db-type: ${{ matrix.db-type }} - db-version: ${{ matrix.db-version }} - phpunit-test-groups: ${{ matrix.phpunit-test-groups }} - - slack-notifications: - name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk - permissions: - actions: read - contents: read - needs: [ test-with-mysql, test-with-mariadb, specific-test-groups ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-latest - permissions: - actions: write - needs: [ slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - - steps: - - name: Dispatch workflow run - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: '${{ github.run_id }}' - } - }); diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml deleted file mode 100644 index a7b6e2f924650..0000000000000 --- a/.github/workflows/test-build-processes.yml +++ /dev/null @@ -1,165 +0,0 @@ -name: Test Build Processes - -on: - push: - branches: - - trunk - - '3.[7-9]' - - '[4-9].[0-9]' - tags: - - '[0-9]+.[0-9]' - - '[0-9]+.[0-9].[0-9]+' - pull_request: - branches: - - trunk - - '3.[7-9]' - - '[4-9].[0-9]' - workflow_dispatch: - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -# Disable permissions for all available scopes by default. -# Any needed permissions should be configured at the job level. -permissions: {} - -jobs: - # Tests the WordPress Core build process on multiple operating systems. - test-core-build-process: - name: Core running from ${{ matrix.directory }} - uses: WordPress/wordpress-develop/.github/workflows/reusable-test-core-build-process.yml@trunk - permissions: - contents: read - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, windows-latest ] - directory: [ 'src', 'build' ] - include: - # Only prepare artifacts for Playground once. - - os: ubuntu-latest - directory: 'build' - save-build: true - prepare-playground: ${{ github.event_name == 'pull_request' && true || '' }} - - with: - os: ${{ matrix.os }} - directory: ${{ matrix.directory }} - save-build: ${{ matrix.save-build && matrix.save-build || false }} - prepare-playground: ${{ matrix.prepare-playground && matrix.prepare-playground || false }} - - # Tests the WordPress Core build process on MacOS. - # - # This is separate from the job above in order to use stricter conditions when determining when to run. - # This avoids unintentionally consuming excessive minutes, as MacOS jobs consume minutes at a 10x rate. - # - # The `matrix` and `runner` contexts are not available for use within `if` expressions. So there is - # currently no way to determine the OS being used on a given job. - # See https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability. - test-core-build-process-macos: - name: Core running from ${{ matrix.directory }} - uses: WordPress/wordpress-develop/.github/workflows/reusable-test-core-build-process.yml@trunk - permissions: - contents: read - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - strategy: - fail-fast: false - matrix: - os: [ macos-latest ] - directory: [ 'src', 'build' ] - with: - os: ${{ matrix.os }} - directory: ${{ matrix.directory }} - - # Tests the Gutenberg plugin build process on multiple operating systems when run within a wordpress-develop checkout. - test-gutenberg-build-process: - name: Gutenberg running from ${{ matrix.directory }} - uses: WordPress/wordpress-develop/.github/workflows/reusable-test-gutenberg-build-process.yml@trunk - permissions: - contents: read - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, windows-latest ] - directory: [ 'src', 'build' ] - with: - os: ${{ matrix.os }} - directory: ${{ matrix.directory }} - - # Tests the Gutenberg plugin build process on MacOS when run within a wordpress-develop checkout. - # - # This is separate from the job above in order to use stricter conditions when determining when to run. - # This avoids unintentionally consuming excessive minutes, as MacOS jobs consume minutes at a 10x rate. - # - # The `matrix` and `runner` contexts are not available for use within `if` expressions. So there is - # currently no way to determine the OS being used on a given job. - # See https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability. - test-gutenberg-build-process-macos: - name: Gutenberg running from ${{ matrix.directory }} - uses: WordPress/wordpress-develop/.github/workflows/reusable-test-gutenberg-build-process.yml@trunk - permissions: - contents: read - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - strategy: - fail-fast: false - matrix: - os: [ macos-latest ] - directory: [ 'src', 'build' ] - with: - os: ${{ matrix.os }} - directory: ${{ matrix.directory }} - - slack-notifications: - name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@trunk - permissions: - actions: read - contents: read - needs: [ test-core-build-process, test-core-build-process-macos, test-gutenberg-build-process, test-gutenberg-build-process-macos ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} - - failed-workflow: - name: Failed workflow tasks - runs-on: ubuntu-latest - permissions: - actions: write - needs: [ slack-notifications ] - if: | - always() && - github.repository == 'WordPress/wordpress-develop' && - github.event_name != 'pull_request' && - github.run_attempt < 2 && - ( - contains( needs.*.result, 'cancelled' ) || - contains( needs.*.result, 'failure' ) - ) - - steps: - - name: Dispatch workflow run - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - retries: 2 - retry-exempt-status-codes: 418 - script: | - github.rest.actions.createWorkflowDispatch({ - owner: context.repo.owner, - repo: context.repo.repo, - workflow_id: 'failed-workflow.yml', - ref: 'trunk', - inputs: { - run_id: '${{ github.run_id }}' - } - }); diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index c84f1660f931b..beb1c9e751519 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -161,6 +161,33 @@ function get_option( $option, $default_value = false ) { $passed_default = func_num_args() > 1; if ( ! wp_installing() ) { + // Prevent non-existent options from triggering multiple queries. + $notoptions = wp_cache_get( 'notoptions', 'options' ); + + // Prevent non-existent `notoptions` key from triggering multiple key lookups. + if ( ! is_array( $notoptions ) ) { + $notoptions = array(); + wp_cache_set( 'notoptions', $notoptions, 'options' ); + } + + if ( isset( $notoptions[ $option ] ) ) { + /** + * Filters the default value for an option. + * + * The dynamic portion of the hook name, `$option`, refers to the option name. + * + * @since 3.4.0 + * @since 4.4.0 The `$option` parameter was added. + * @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value. + * + * @param mixed $default_value The default value to return if the option does not exist + * in the database. + * @param string $option Option name. + * @param bool $passed_default Was `get_option()` passed a default value? + */ + return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default ); + } + $alloptions = wp_load_alloptions(); if ( isset( $alloptions[ $option ] ) ) { @@ -169,31 +196,6 @@ function get_option( $option, $default_value = false ) { $value = wp_cache_get( $option, 'options' ); if ( false === $value ) { - // Prevent non-existent options from triggering multiple queries. - $notoptions = wp_cache_get( 'notoptions', 'options' ); - - // Prevent non-existent `notoptions` key from triggering multiple key lookups. - if ( ! is_array( $notoptions ) ) { - $notoptions = array(); - wp_cache_set( 'notoptions', $notoptions, 'options' ); - } elseif ( isset( $notoptions[ $option ] ) ) { - /** - * Filters the default value for an option. - * - * The dynamic portion of the hook name, `$option`, refers to the option name. - * - * @since 3.4.0 - * @since 4.4.0 The `$option` parameter was added. - * @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value. - * - * @param mixed $default_value The default value to return if the option does not exist - * in the database. - * @param string $option Option name. - * @param bool $passed_default Was `get_option()` passed a default value? - */ - return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default ); - } - $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) ); // Has to be get_row() instead of get_var() because of funkiness with 0, false, null values. @@ -201,6 +203,10 @@ function get_option( $option, $default_value = false ) { $value = $row->option_value; wp_cache_add( $option, $value, 'options' ); } else { // Option does not exist, so we must cache its non-existence. + if ( ! is_array( $notoptions ) ) { + $notoptions = array(); + } + $notoptions[ $option ] = true; wp_cache_set( 'notoptions', $notoptions, 'options' ); diff --git a/tests/phpunit/tests/option/option.php b/tests/phpunit/tests/option/option.php index 36a40d9a2f495..5c285f7c6e1bb 100644 --- a/tests/phpunit/tests/option/option.php +++ b/tests/phpunit/tests/option/option.php @@ -100,62 +100,6 @@ public function test_get_option_should_call_pre_option_filter() { $this->assertSame( 1, $filter->get_call_count() ); } - /** - * @ticket 58277 - * - * @covers ::get_option - */ - public function test_get_option_notoptions_cache() { - $notoptions = array( - 'invalid' => true, - ); - wp_cache_set( 'notoptions', $notoptions, 'options' ); - - $before = get_num_queries(); - $value = get_option( 'invalid' ); - $after = get_num_queries(); - - $this->assertSame( 0, $after - $before ); - } - - /** - * @ticket 58277 - * - * @covers ::get_option - */ - public function test_get_option_notoptions_set_cache() { - get_option( 'invalid' ); - - $before = get_num_queries(); - $value = get_option( 'invalid' ); - $after = get_num_queries(); - - $notoptions = wp_cache_get( 'notoptions', 'options' ); - - $this->assertSame( 0, $after - $before, 'The notoptions cache was not hit on the second call to `get_option()`.' ); - $this->assertIsArray( $notoptions, 'The notoptions cache should be set.' ); - $this->assertArrayHasKey( 'invalid', $notoptions, 'The "invalid" option should be in the notoptions cache.' ); - } - - /** - * @ticket 58277 - * - * @covers ::get_option - */ - public function test_get_option_notoptions_do_not_load_cache() { - add_option( 'foo', 'bar', '', false ); - wp_cache_delete( 'notoptions', 'options' ); - - $before = get_num_queries(); - $value = get_option( 'foo' ); - $after = get_num_queries(); - - $notoptions = wp_cache_get( 'notoptions', 'options' ); - - $this->assertSame( 0, $after - $before, 'The options cache was not hit on the second call to `get_option()`.' ); - $this->assertFalse( $notoptions, 'The notoptions cache should not be set.' ); - } - /** * @covers ::get_option * @covers ::add_option @@ -548,4 +492,57 @@ public function test_add_option_clears_the_notoptions_cache() { $updated_notoptions = wp_cache_get( 'notoptions', 'options' ); $this->assertArrayNotHasKey( $option_name, $updated_notoptions, 'The "foobar" option should not be in the notoptions cache after adding it.' ); } + + /** + * Test that get_option() does not hit the external cache multiple times for the same option. + * + * @ticket 62692 + * + * @dataProvider data_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option + * + * @param int $expected_connections Expected number of connections to the memcached server. + * @param bool $option_exists Whether the option should be set. Default true. + * @param string $autoload Whether the option should be auto loaded. Default true. + */ + public function test_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option( $expected_connections, $option_exists = true, $autoload = true ) { + if ( ! wp_using_ext_object_cache() ) { + $this->markTestSkipped( 'This test requires an external object cache.' ); + } + + if ( ! function_exists( 'wp_cache_get_stats' ) ) { + $this->markTestSkipped( 'This test requires the Memcached PECL extension.' ); + } + + if ( $option_exists ) { + add_option( 'ticket-62692', 'value', '', $autoload ); + } + + wp_cache_delete_multiple( array( 'ticket-62692', 'notoptions', 'alloptions' ), 'options' ); + + $stats = wp_cache_get_stats(); + $connections_start = array_shift( $stats )['cmd_get']; + + $call_getter = 10; + while ( $call_getter-- ) { + get_option( 'ticket-62692' ); + } + + $stats = wp_cache_get_stats(); + $connections_end = array_shift( $stats )['cmd_get']; + + $this->assertSame( $expected_connections, $connections_end - $connections_start ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option() { + return array( + 'exists, autoload' => array( 1, true, true ), // 1 on trunk. + 'exists, not autoloaded' => array( 12, true, false ), // 3 on trunk. + 'does not exist' => array( 3, false ), // 12 on trunk. + ); + } }