diff --git a/.env.example b/.env.example index 76a4744165505..ac96332b0ae92 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,9 @@ LOCAL_PHP_XDEBUG_MODE=develop,debug # Whether or not to enable Memcached. LOCAL_PHP_MEMCACHED=false +# Whether or not to enable PCOV. +LOCAL_PHP_PCOV=false + ## # The database software to use. # @@ -46,12 +49,12 @@ LOCAL_DB_TYPE=mysql ## # The database version to use. # -# Defaults to 8.0 with the assumption that LOCAL_DB_TYPE is set to `mysql` above. +# Defaults to 9.7 with the assumption that LOCAL_DB_TYPE is set to `mysql` above. # # When using `mysql`, see https://hub.docker.com/_/mysql for valid versions. # When using `mariadb`, see https://hub.docker.com/_/mariadb for valid versions. ## -LOCAL_DB_VERSION=8.4 +LOCAL_DB_VERSION=9.7 # Whether or not to enable multisite. LOCAL_MULTISITE=false diff --git a/.github/workflows/install-testing.yml b/.github/workflows/install-testing.yml index c55e2c6669f74..440c2ee842149 100644 --- a/.github/workflows/install-testing.yml +++ b/.github/workflows/install-testing.yml @@ -94,11 +94,12 @@ jobs: - db-version: '9.3' - db-version: '9.4' - db-version: '9.5' + - db-version: '9.6' # MySQL 9.0+ will not work on PHP 7.2 & 7.3. See https://core.trac.wordpress.org/ticket/61218. - php: '7.2' - db-version: '9.6' + db-version: '9.7' - php: '7.3' - db-version: '9.6' + db-version: '9.7' services: database: diff --git a/.github/workflows/local-docker-environment.yml b/.github/workflows/local-docker-environment.yml index d42bba623ec64..b786cc2419c03 100644 --- a/.github/workflows/local-docker-environment.yml +++ b/.github/workflows/local-docker-environment.yml @@ -108,6 +108,7 @@ jobs: - db-version: '9.3' - db-version: '9.4' - db-version: '9.5' + - db-version: '9.6' # No PHP 8.5 + Memcached support yet. - php: '8.5' memcached: true diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 74dfc220c04a6..f46b419656a44 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -76,7 +76,7 @@ jobs: os: [ ubuntu-24.04 ] php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] db-type: [ 'mysql' ] - db-version: [ '5.7', '8.0', '8.4' ] + db-version: [ '5.7', '8.0', '8.4', '9.7' ] tests-domain: [ 'example.org' ] multisite: [ false, true ] memcached: [ false ] @@ -209,15 +209,13 @@ jobs: os: [ ubuntu-24.04 ] php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] db-type: [ 'mysql', 'mariadb' ] - db-version: [ '9.6', '12.1' ] + db-version: [ '12.1' ] multisite: [ false, true ] memcached: [ false ] db-innovation: [ true ] exclude: # Exclude version combinations that don't exist. - - db-type: 'mariadb' - db-version: '9.6' - db-type: 'mysql' db-version: '12.1' with: @@ -283,14 +281,14 @@ jobs: fail-fast: false matrix: php: [ '7.4', '8.4' ] - db-version: [ '8.4', '11.8' ] + db-version: [ '9.7', '11.8' ] db-type: [ 'mysql', 'mariadb' ] multisite: [ false ] include: # Include one multisite job for each database type. - php: '8.4' - db-version: '8.4' + db-version: '9.7' db-type: 'mysql' multisite: true - php: '8.4' @@ -299,13 +297,13 @@ jobs: multisite: true # Test with memcached. - php: '8.4' - db-version: '8.4' + db-version: '9.7' db-type: 'mysql' multisite: true memcached: true # Run specific test groups once. - php: '8.4' - db-version: '8.4' + db-version: '9.7' db-type: 'mysql' phpunit-test-groups: 'html-api-html5lib-tests' @@ -314,7 +312,7 @@ jobs: - db-type: 'mysql' db-version: '11.8' - db-type: 'mariadb' - db-version: '8.4' + db-version: '9.7' with: php: ${{ matrix.php }} diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index d5617a66c17d5..c0279c37fe64b 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -78,6 +78,7 @@ jobs: # - Configure environment variables. # - Checkout repository. # - Set up Node.js. + # - Set up PHP. # - Log debug information. # - Install npm dependencies. # - Install Playwright browsers. @@ -127,6 +128,14 @@ jobs: node-version-file: '.nvmrc' cache: npm + - name: Set up PHP + if: ${{ inputs.subject == 'base' }} + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + with: + php-version: ${{ inputs.php-version }} + tools: wp-cli + coverage: none + - name: Log debug information run: | npm --version @@ -183,7 +192,7 @@ jobs: if: ${{ inputs.subject == 'base' }} run: | VERSION="${BASE_TAG%.0}" - npm run env:cli -- core download --version="$VERSION" --force --path="/var/www/${LOCAL_DIR}" + wp core download --version="$VERSION" --force --path="${LOCAL_DIR}" - name: Install object cache drop-in if: ${{ inputs.memcached }} diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index b75a6e37f91b4..e06f9f24be17e 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -82,8 +82,9 @@ on: env: LOCAL_PHP: ${{ inputs.php }}-fpm - LOCAL_PHP_XDEBUG: ${{ inputs.coverage-report || false }} - LOCAL_PHP_XDEBUG_MODE: ${{ inputs.coverage-report && 'coverage' || 'develop,debug' }} + LOCAL_PHP_XDEBUG: false + LOCAL_PHP_XDEBUG_MODE: 'develop,debug' + LOCAL_PHP_PCOV: ${{ inputs.coverage-report }} LOCAL_DB_TYPE: ${{ inputs.db-type }} LOCAL_DB_VERSION: ${{ inputs.db-version }} LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} @@ -113,7 +114,6 @@ jobs: # - Install WordPress within the Docker container. # - Run the PHPUnit tests. # - Upload the code coverage report to Codecov.io. - # - Upload the HTML code coverage report as an artifact. # - Ensures version-controlled files are not modified or deleted. # - Checks out the WordPress Test reporter repository. # - Submit the test results to the WordPress.org host test results. @@ -204,12 +204,12 @@ jobs: - name: Run PHPUnit tests${{ inputs.phpunit-test-groups && format( ' ({0} groups)', inputs.phpunit-test-groups ) || '' }}${{ inputs.coverage-report && ' with coverage report' || '' }} continue-on-error: ${{ inputs.allow-errors }} run: | - node ./tools/local-env/scripts/docker.js run \ + node ./tools/local-env/scripts/docker.js ${{ inputs.coverage-report && 'exec' || 'run' }} \ php ./vendor/bin/phpunit \ --verbose \ -c "${PHPUNIT_CONFIG}" \ ${{ inputs.phpunit-test-groups && '--group "${TEST_GROUPS}"' || '' }} \ - ${{ inputs.coverage-report && '--coverage-clover "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}.xml" --coverage-html "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}"' || '' }} + ${{ inputs.coverage-report && '--coverage-clover "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}.xml"' || '' }} env: TEST_GROUPS: ${{ inputs.phpunit-test-groups }} MULTISITE_FLAG: ${{ inputs.multisite && 'multisite' || 'single' }} @@ -244,14 +244,6 @@ jobs: flags: ${{ inputs.multisite && 'multisite' || 'single' }},php fail_ci_if_error: true - - name: Upload HTML coverage report as artifact - if: ${{ inputs.coverage-report }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }} - path: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }} - overwrite: true - - name: Ensure version-controlled files are not modified or deleted run: git diff --exit-code diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index f2b0afce3256f..e1c3dd3be2f59 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -38,8 +38,6 @@ concurrency: permissions: {} env: - LOCAL_PHP_XDEBUG: true - LOCAL_PHP_XDEBUG_MODE: 'coverage' LOCAL_PHP_MEMCACHED: false PUPPETEER_SKIP_DOWNLOAD: true diff --git a/.github/workflows/upgrade-testing.yml b/.github/workflows/upgrade-testing.yml index ce80cfb6f0989..8a74e5ae614c5 100644 --- a/.github/workflows/upgrade-testing.yml +++ b/.github/workflows/upgrade-testing.yml @@ -70,7 +70,7 @@ jobs: os: [ 'ubuntu-24.04' ] php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] db-type: [ 'mysql' ] - db-version: [ '5.7', '8.0', '8.4', '9.6' ] + db-version: [ '5.7', '8.0', '8.4', '9.7' ] wp: [ '6.9', '7.0' ] multisite: [ false, true ] with: @@ -179,7 +179,7 @@ jobs: os: [ 'ubuntu-24.04' ] php: [ '7.4' ] db-type: [ 'mysql' ] - db-version: [ '5.7', '8.0', '8.4', '9.6' ] + db-version: [ '5.7', '8.0', '8.4', '9.7' ] wp: [ '4.7' ] multisite: [ false, true ] with: diff --git a/.version-support-mysql.json b/.version-support-mysql.json index 6e81f2eff0f09..7ab8aa38df392 100644 --- a/.version-support-mysql.json +++ b/.version-support-mysql.json @@ -1,12 +1,6 @@ { "7-1": [ - "9.6", - "9.5", - "9.4", - "9.3", - "9.2", - "9.1", - "9.0", + "9.7", "8.4", "8.0", "5.7", diff --git a/docker-compose.yml b/docker-compose.yml index cc2ed8d94975e..0af433a972b13 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: # The PHP container. ## php: - image: wordpressdevelop/php:${LOCAL_PHP-latest} + image: ghcr.io/wordpress/wpdev-docker-images/php:${LOCAL_PHP-latest}-210 networks: - wpdevnet @@ -40,6 +40,7 @@ services: environment: LOCAL_PHP_XDEBUG: ${LOCAL_PHP_XDEBUG-false} XDEBUG_MODE: ${LOCAL_PHP_XDEBUG_MODE-develop,debug} + LOCAL_PHP_PCOV: ${LOCAL_PHP_PCOV-false} LOCAL_PHP_MEMCACHED: ${LOCAL_PHP_MEMCACHED-false} PHP_FPM_UID: ${PHP_FPM_UID-1000} PHP_FPM_GID: ${PHP_FPM_GID-1000} @@ -92,13 +93,14 @@ services: # The WP CLI container. ## cli: - image: wordpressdevelop/cli:${LOCAL_PHP-latest} + image: ghcr.io/wordpress/wpdev-docker-images/cli:${LOCAL_PHP-latest}-210 networks: - wpdevnet environment: LOCAL_PHP_XDEBUG: ${LOCAL_PHP_XDEBUG-false} + LOCAL_PHP_PCOV: ${LOCAL_PHP_PCOV-false} LOCAL_PHP_MEMCACHED: ${LOCAL_PHP_MEMCACHED-false} PHP_FPM_UID: ${PHP_FPM_UID-1000} PHP_FPM_GID: ${PHP_FPM_GID-1000} diff --git a/src/wp-includes/class-wp-hook.php b/src/wp-includes/class-wp-hook.php index cd6860c0f81f2..2ce6ee0a17648 100644 --- a/src/wp-includes/class-wp-hook.php +++ b/src/wp-includes/class-wp-hook.php @@ -85,6 +85,9 @@ public function add_filter( $hook_name, $callback, $priority, $accepted_args ) { } $idx = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); + if ( null === $idx ) { + return; + } $priority_existed = isset( $this->callbacks[ $priority ] ); diff --git a/src/wp-includes/class-wp-widget-factory.php b/src/wp-includes/class-wp-widget-factory.php index ed719b9d4d7a1..b6233eabba002 100644 --- a/src/wp-includes/class-wp-widget-factory.php +++ b/src/wp-includes/class-wp-widget-factory.php @@ -57,7 +57,7 @@ public function WP_Widget_Factory() { */ public function register( $widget ) { if ( $widget instanceof WP_Widget ) { - $this->widgets[ spl_object_hash( $widget ) ] = $widget; + $this->widgets[ spl_object_id( $widget ) ] = $widget; } else { $this->widgets[ $widget ] = new $widget(); } @@ -74,7 +74,7 @@ public function register( $widget ) { */ public function unregister( $widget ) { if ( $widget instanceof WP_Widget ) { - unset( $this->widgets[ spl_object_hash( $widget ) ] ); + unset( $this->widgets[ spl_object_id( $widget ) ] ); } else { unset( $this->widgets[ $widget ] ); } diff --git a/src/wp-includes/plugin.php b/src/wp-includes/plugin.php index e980af5a09f66..55459c0dd96c8 100644 --- a/src/wp-includes/plugin.php +++ b/src/wp-includes/plugin.php @@ -986,33 +986,36 @@ function _wp_call_all_hook( $args ) { * @since 2.2.3 * @since 5.3.0 Removed workarounds for spl_object_hash(). * `$hook_name` and `$priority` are no longer used, - * and the function always returns a string. + * and no longer returns false, but can still return void for invalid callbacks. + * @since 6.9.0 Returns explicit null if an invalid callback is supplied. + * @since 7.1.0 Uses spl_object_id() instead of spl_object_hash() for performance. * * @access private * - * @param string $hook_name Unused. The name of the filter to build ID for. - * @param callable|string|array $callback The callback to generate ID for. The callback may - * or may not exist. - * @param int $priority Unused. The order in which the functions - * associated with a particular action are executed. - * @return string|null Unique function ID for usage as array key. - * Null if a valid `$callback` is not passed. + * @param string $hook_name Unused. The name of the filter to build ID for. + * @param callable $callback The callback to generate ID for. The callback may + * or may not exist. + * @param int $priority Unused. The order in which the functions + * associated with a particular action are executed. + * @return string|null Unique function ID for usage as array key, or null if it couldn't be determined. */ -function _wp_filter_build_unique_id( $hook_name, $callback, $priority ) { +function _wp_filter_build_unique_id( $hook_name, $callback, $priority ): ?string { if ( is_string( $callback ) ) { return $callback; } if ( is_object( $callback ) ) { - // Closures are currently implemented as objects. - $callback = array( $callback, '' ); - } else { - $callback = (array) $callback; + return (string) spl_object_id( (object) $callback ); + } + + $callback = (array) $callback; + if ( ! isset( $callback[1] ) || ! is_string( $callback[1] ) ) { + return null; } if ( is_object( $callback[0] ) ) { // Object class calling. - return spl_object_hash( $callback[0] ) . $callback[1]; + return ( (string) spl_object_id( $callback[0] ) ) . $callback[1]; } elseif ( is_string( $callback[0] ) ) { // Static calling. return $callback[0] . '::' . $callback[1]; diff --git a/tests/phpunit/tests/abilities-api/wpAbility.php b/tests/phpunit/tests/abilities-api/wpAbility.php index 926a0e56a5280..73c4f9db43ffd 100644 --- a/tests/phpunit/tests/abilities-api/wpAbility.php +++ b/tests/phpunit/tests/abilities-api/wpAbility.php @@ -568,18 +568,19 @@ public function test_before_execute_ability_action() { ) ); - $callback = static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) { - $action_ability_name = $ability_name; - $action_input = $input; - }; - - add_action( 'wp_before_execute_ability', $callback, 10, 2 ); + add_action( + 'wp_before_execute_ability', + static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) { + $action_ability_name = $ability_name; + $action_input = $input; + }, + 10, + 2 + ); $ability = new WP_Ability( self::$test_ability_name, $args ); $result = $ability->execute( 5 ); - remove_action( 'wp_before_execute_ability', $callback ); - $this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' ); $this->assertSame( 5, $action_input, 'Action should receive correct input' ); $this->assertSame( 10, $result, 'Ability should execute correctly' ); @@ -603,18 +604,19 @@ public function test_before_execute_ability_action_no_input() { ) ); - $callback = static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) { - $action_ability_name = $ability_name; - $action_input = $input; - }; - - add_action( 'wp_before_execute_ability', $callback, 10, 2 ); + add_action( + 'wp_before_execute_ability', + static function ( $ability_name, $input ) use ( &$action_ability_name, &$action_input ) { + $action_ability_name = $ability_name; + $action_input = $input; + }, + 10, + 2 + ); $ability = new WP_Ability( self::$test_ability_name, $args ); $result = $ability->execute(); - remove_action( 'wp_before_execute_ability', $callback ); - $this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' ); $this->assertNull( $action_input, 'Action should receive null input when no input provided' ); $this->assertSame( 42, $result, 'Ability should execute correctly' ); @@ -644,19 +646,20 @@ public function test_after_execute_ability_action() { ) ); - $callback = static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) { - $action_ability_name = $ability_name; - $action_input = $input; - $action_result = $result; - }; - - add_action( 'wp_after_execute_ability', $callback, 10, 3 ); + add_action( + 'wp_after_execute_ability', + static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) { + $action_ability_name = $ability_name; + $action_input = $input; + $action_result = $result; + }, + 10, + 3 + ); $ability = new WP_Ability( self::$test_ability_name, $args ); $result = $ability->execute( 7 ); - remove_action( 'wp_after_execute_ability', $callback ); - $this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' ); $this->assertSame( 7, $action_input, 'Action should receive correct input' ); $this->assertSame( 21, $action_result, 'Action should receive correct result' ); @@ -683,19 +686,20 @@ public function test_after_execute_ability_action_no_input() { ) ); - $callback = static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) { - $action_ability_name = $ability_name; - $action_input = $input; - $action_result = $result; - }; - - add_action( 'wp_after_execute_ability', $callback, 10, 3 ); + add_action( + 'wp_after_execute_ability', + static function ( $ability_name, $input, $result ) use ( &$action_ability_name, &$action_input, &$action_result ) { + $action_ability_name = $ability_name; + $action_input = $input; + $action_result = $result; + }, + 10, + 3 + ); $ability = new WP_Ability( self::$test_ability_name, $args ); $result = $ability->execute(); - remove_action( 'wp_after_execute_ability', $callback ); - $this->assertSame( self::$test_ability_name, $action_ability_name, 'Action should receive correct ability name' ); $this->assertNull( $action_input, 'Action should receive null input when no input provided' ); $this->assertSame( 'test-result', $action_result, 'Action should receive correct result' ); @@ -720,23 +724,23 @@ public function test_actions_not_fired_on_permission_failure() { ) ); - $before_callback = static function () use ( &$before_action_fired ) { - $before_action_fired = true; - }; - - $after_callback = static function () use ( &$after_action_fired ) { - $after_action_fired = true; - }; + add_action( + 'wp_before_execute_ability', + static function () use ( &$before_action_fired ) { + $before_action_fired = true; + } + ); - add_action( 'wp_before_execute_ability', $before_callback ); - add_action( 'wp_after_execute_ability', $after_callback ); + add_action( + 'wp_after_execute_ability', + static function () use ( &$after_action_fired ) { + $after_action_fired = true; + } + ); $ability = new WP_Ability( self::$test_ability_name, $args ); $result = $ability->execute(); - remove_action( 'wp_before_execute_ability', $before_callback ); - remove_action( 'wp_after_execute_ability', $after_callback ); - $this->assertFalse( $before_action_fired, 'before_execute_ability action should not be fired on permission failure' ); $this->assertFalse( $after_action_fired, 'after_execute_ability action should not be fired on permission failure' ); $this->assertInstanceOf( WP_Error::class, $result, 'Should return WP_Error on permission failure' ); @@ -760,23 +764,23 @@ public function test_after_action_not_fired_on_execution_error() { ) ); - $before_callback = static function () use ( &$before_action_fired ) { - $before_action_fired = true; - }; - - $after_callback = static function () use ( &$after_action_fired ) { - $after_action_fired = true; - }; + add_action( + 'wp_before_execute_ability', + static function () use ( &$before_action_fired ) { + $before_action_fired = true; + } + ); - add_action( 'wp_before_execute_ability', $before_callback ); - add_action( 'wp_after_execute_ability', $after_callback ); + add_action( + 'wp_after_execute_ability', + static function () use ( &$after_action_fired ) { + $after_action_fired = true; + } + ); $ability = new WP_Ability( self::$test_ability_name, $args ); $result = $ability->execute(); - remove_action( 'wp_before_execute_ability', $before_callback ); - remove_action( 'wp_after_execute_ability', $after_callback ); - $this->assertTrue( $before_action_fired, 'before_execute_ability action should be fired even if execution fails' ); $this->assertFalse( $after_action_fired, 'after_execute_ability action should not be fired when execution returns WP_Error' ); $this->assertInstanceOf( WP_Error::class, $result, 'Should return WP_Error from execution callback' ); @@ -805,23 +809,23 @@ public function test_after_action_not_fired_on_output_validation_error() { ) ); - $before_callback = static function () use ( &$before_action_fired ) { - $before_action_fired = true; - }; - - $after_callback = static function () use ( &$after_action_fired ) { - $after_action_fired = true; - }; + add_action( + 'wp_before_execute_ability', + static function () use ( &$before_action_fired ) { + $before_action_fired = true; + } + ); - add_action( 'wp_before_execute_ability', $before_callback ); - add_action( 'wp_after_execute_ability', $after_callback ); + add_action( + 'wp_after_execute_ability', + static function () use ( &$after_action_fired ) { + $after_action_fired = true; + } + ); $ability = new WP_Ability( self::$test_ability_name, $args ); $result = $ability->execute(); - remove_action( 'wp_before_execute_ability', $before_callback ); - remove_action( 'wp_after_execute_ability', $after_callback ); - $this->assertTrue( $before_action_fired, 'before_execute_ability action should be fired even if output validation fails' ); $this->assertFalse( $after_action_fired, 'after_execute_ability action should not be fired when output validation fails' ); $this->assertInstanceOf( WP_Error::class, $result, 'Should return WP_Error for output validation failure' ); diff --git a/tests/phpunit/tests/abilities-api/wpRegisterAbility.php b/tests/phpunit/tests/abilities-api/wpRegisterAbility.php index 61bf8f59dba53..133343635cf59 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterAbility.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterAbility.php @@ -521,13 +521,15 @@ public function test_get_ability_no_init_action(): void { public function test_get_existing_ability_using_callback() { $this->simulate_doing_wp_abilities_init_action(); - $name = self::$test_ability_name; - $args = self::$test_ability_args; - $callback = static function ( $instance ) use ( $name, $args ) { - wp_register_ability( $name, $args ); - }; + $name = self::$test_ability_name; + $args = self::$test_ability_args; - add_action( 'wp_abilities_api_init', $callback ); + add_action( + 'wp_abilities_api_init', + static function ( $instance ) use ( $name, $args ) { + wp_register_ability( $name, $args ); + } + ); // Reset the Registry, to ensure it's empty before the test. $registry_reflection = new ReflectionClass( WP_Abilities_Registry::class ); @@ -539,8 +541,6 @@ public function test_get_existing_ability_using_callback() { $result = wp_get_ability( $name ); - remove_action( 'wp_abilities_api_init', $callback ); - $this->assertEquals( new WP_Ability( $name, $args ), $result, diff --git a/tests/phpunit/tests/abilities-api/wpRegisterAbilityCategory.php b/tests/phpunit/tests/abilities-api/wpRegisterAbilityCategory.php index 5a59b1050bf25..9b83928e19d5c 100644 --- a/tests/phpunit/tests/abilities-api/wpRegisterAbilityCategory.php +++ b/tests/phpunit/tests/abilities-api/wpRegisterAbilityCategory.php @@ -293,13 +293,15 @@ public function test_get_nonexistent_category(): void { * @ticket 64098 */ public function test_get_existing_category_using_callback(): void { - $name = self::$test_ability_category_name; - $args = self::$test_ability_category_args; - $callback = static function ( $instance ) use ( $name, $args ) { - wp_register_ability_category( $name, $args ); - }; + $name = self::$test_ability_category_name; + $args = self::$test_ability_category_args; - add_action( 'wp_abilities_api_categories_init', $callback ); + add_action( + 'wp_abilities_api_categories_init', + static function ( $instance ) use ( $name, $args ) { + wp_register_ability_category( $name, $args ); + } + ); // Reset the Registry, to ensure it's empty before the test. $registry_reflection = new ReflectionClass( WP_Ability_Categories_Registry::class ); @@ -311,8 +313,6 @@ public function test_get_existing_category_using_callback(): void { $result = wp_get_ability_category( $name ); - remove_action( 'wp_abilities_api_categories_init', $callback ); - $this->assertInstanceOf( WP_Ability_Category::class, $result ); $this->assertSame( self::$test_ability_category_name, $result->get_slug() ); } diff --git a/tests/phpunit/tests/hooks/buildUniqueId.php b/tests/phpunit/tests/hooks/buildUniqueId.php new file mode 100644 index 0000000000000..387a24bbc2fd1 --- /dev/null +++ b/tests/phpunit/tests/hooks/buildUniqueId.php @@ -0,0 +1,62 @@ +assertIsString( $result ); + $this->assertSame( '__return_null', $result ); + } + + public function test_closure_returns_string(): void { + $cb = function (): void {}; + $result = _wp_filter_build_unique_id( '', $cb, 10 ); + $this->assertIsString( $result ); + } + + public function test_object_callback_returns_string(): void { + $a = new MockAction(); + $result = _wp_filter_build_unique_id( '', array( $a, 'action' ), 10 ); + $this->assertIsString( $result ); + } + + public function test_static_callback_returns_string(): void { + $result = _wp_filter_build_unique_id( '', array( 'MockAction', 'action' ), 10 ); + $this->assertIsString( $result ); + } + + public function test_two_different_objects_produce_different_ids(): void { + $a = new MockAction(); + $b = new MockAction(); + $this->assertNotSame( + _wp_filter_build_unique_id( '', array( $a, 'action' ), 10 ), + _wp_filter_build_unique_id( '', array( $b, 'action' ), 10 ) + ); + } + + public function test_same_object_produces_same_id(): void { + $a = new MockAction(); + $this->assertSame( + _wp_filter_build_unique_id( '', array( $a, 'action' ), 10 ), + _wp_filter_build_unique_id( '', array( $a, 'action' ), 10 ) + ); + } + + public function test_malformed_array_missing_method_returns_null(): void { + $a = new MockAction(); + $result = _wp_filter_build_unique_id( '', array( $a ), 10 ); + $this->assertNull( $result ); + } + + public function test_malformed_array_non_string_method_returns_null(): void { + $a = new MockAction(); + $result = _wp_filter_build_unique_id( '', array( $a, 123 ), 10 ); + $this->assertNull( $result ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php index 43525263ac5ba..f52fa32543d47 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php @@ -206,7 +206,6 @@ public function test_get_item_with_selected_fields(): void { $response = $this->server->dispatch( $request ); add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); - remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); $this->assertEquals( 200, $response->get_status() ); diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php index d73a2c64177fc..1465319f234de 100644 --- a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php @@ -328,7 +328,6 @@ public function test_get_item_with_selected_fields(): void { $response = $this->server->dispatch( $request ); add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); - remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); $this->assertEquals( 200, $response->get_status() ); @@ -349,7 +348,6 @@ public function test_get_item_with_embed_context(): void { $response = $this->server->dispatch( $request ); add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); - remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); $this->assertEquals( 200, $response->get_status() ); diff --git a/tools/local-env/php-config.ini b/tools/local-env/php-config.ini index 6f98802e5c68a..30ee177289f1d 100644 --- a/tools/local-env/php-config.ini +++ b/tools/local-env/php-config.ini @@ -5,3 +5,9 @@ post_max_size = 1G xdebug.start_with_request=trigger xdebug.discover_client_host=true xdebug.client_host=host.docker.internal + +# Pre-sizes the internal file tracking array for the thousands of files under +# `src/` or `build/`, avoiding reallocation churn during test warmup. +# +# The default of 64 is far too low here. +pcov.initial.files=10000 diff --git a/tools/local-env/scripts/docker.js b/tools/local-env/scripts/docker.js index c7b11f0058424..bfe2e36136739 100644 --- a/tools/local-env/scripts/docker.js +++ b/tools/local-env/scripts/docker.js @@ -20,6 +20,25 @@ if ( [ 'exec', 'run' ].includes( dockerCommand[0] ) && ! process.stdin.isTTY ) { dockerCommand.splice( 1, 0, '--no-TTY' ); } +/* + * `pcov.directory` restricts instrumentation to `src/`, so PCOV does not record hits for `vendor/`, `tests/`, or + * WordPress test fixtures that PHPUnit would discard at report time anyway. + */ +if ( process.env.LOCAL_PHP_PCOV === 'true' && dockerCommand.includes( '--coverage-clover' ) ) { + const phpunitIdx = dockerCommand.findIndex( ( arg ) => typeof arg === 'string' && arg.endsWith( 'phpunit' ) ); + if ( phpunitIdx !== -1 ) { + const localDir = process.env.LOCAL_DIR || 'src'; + dockerCommand.splice( + phpunitIdx, + 1, + 'php', + '-d', + `pcov.directory=/var/www/${ localDir }`, + dockerCommand[ phpunitIdx ] + ); + } +} + // Add a --defaults flag to any db command WP-CLI command. See https://core.trac.wordpress.org/ticket/63876. if ( dockerCommand.includes( 'cli' ) && dockerCommand.includes( 'db' ) && ! dockerCommand.includes( '--defaults' ) ) { dockerCommand.push( '--defaults' );