From f4d2f29e5a73fa6f6e9c880ad566de2b7fe5cce2 Mon Sep 17 00:00:00 2001 From: Sergey Biryukov Date: Thu, 21 May 2026 23:20:36 +0000 Subject: [PATCH 1/7] Tests: Get rid of extra `remove_filter()`/`remove_action()` calls in Abilities API tests. These are redundant: `WP_UnitTestCase_Base::tear_down()` runs `::_restore_hooks()`, which restores `$wp_filter`/`$wp_actions` to a pre-test baseline, so hooks added during a test are removed automatically. Follow-up to [61032]. Props mohamedahamed, gziolo, westonruter, SergeyBiryukov. Fixes #65301. git-svn-id: https://develop.svn.wordpress.org/trunk@62405 602fd350-edb4-49c9-b593-d223f7449a82 --- .../phpunit/tests/abilities-api/wpAbility.php | 144 +++++++++--------- .../tests/abilities-api/wpRegisterAbility.php | 16 +- .../wpRegisterAbilityCategory.php | 16 +- .../wpRestAbilitiesV1CategoriesController.php | 1 - .../wpRestAbilitiesV1ListController.php | 2 - 5 files changed, 90 insertions(+), 89 deletions(-) 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/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() ); From 207702c84bbd73dcca3a50f1c4ec51102f54d3ef Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Thu, 21 May 2026 23:31:07 +0000 Subject: [PATCH 2/7] Build/Test Tools: Change how `BASE_TAG` version is downlaoded. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recent attempts to change the `BASE_TAG` version in the Performance Testing workflows in [60324] and [62402] have failed due to memory exhaustion errors when trying to download the version using WP-CLI in the local Docker environment. While the. performance tests are run within the Docker environment, there’s no hard requirement for the `wp core download` command to happen through the `wordpressdevelop/cli` conatiner. This adjusts the workflow to perform the failing WP-CLI call within the GitHub Action runner and outside of Docker to avoid the memory exhaustion issue. Props westonruter, swissspidy. Fixes #65289. git-svn-id: https://develop.svn.wordpress.org/trunk@62406 602fd350-edb4-49c9-b593-d223f7449a82 --- .github/workflows/reusable-performance-test-v2.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 }} From 4e28109e35a7d6008b65958cb0f01627e39cebcb Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers Date: Fri, 22 May 2026 04:20:08 +0000 Subject: [PATCH 3/7] Build/Test Tools: Add MySQL 9.7 to relevenat test matrices. This adds `9.7` (released on April 21, 2026) to the relevant testing strategies and makes it the default version in the local Docker environment. MySQL 9.7 is also an LTS release. Props chrisdavidmiles. See #64894. git-svn-id: https://develop.svn.wordpress.org/trunk@62407 602fd350-edb4-49c9-b593-d223f7449a82 --- .env.example | 4 ++-- .github/workflows/install-testing.yml | 5 +++-- .github/workflows/local-docker-environment.yml | 1 + .github/workflows/phpunit-tests.yml | 16 +++++++--------- .github/workflows/upgrade-testing.yml | 4 ++-- .version-support-mysql.json | 8 +------- 6 files changed, 16 insertions(+), 22 deletions(-) diff --git a/.env.example b/.env.example index 76a4744165505..6974d8554611a 100644 --- a/.env.example +++ b/.env.example @@ -46,12 +46,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/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", From 956081111b451bf312be89665d643387f9cd48ac Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 22 May 2026 06:59:15 +0000 Subject: [PATCH 4/7] Plugins: Improve hook performance by using `spl_object_id()` instead of `spl_object_hash()` to construct unique IDs. * Also use `spl_object_id()` similarly when registering and unregistering classic widgets. * Improve typing and phpdoc in `_wp_filter_build_unique_id()`. Return `null` for malformed callbacks. * Add tests for `_wp_filter_build_unique_id()`. * Improve type safety of `WP_Hook::add_filter()` in case an invalid callback is provided for parity with `::has_filter()` and `::remove_filter()`. Developed in https://github.com/WordPress/wordpress-develop/pull/11865 Follow-up to r46220, r46801, r60179. Props bor0, westonruter, SergeyBiryukov, schlessera, arshidkv12, knutsp, spacedmonkey, swissspidy. See #64898. Fixes #58291. git-svn-id: https://develop.svn.wordpress.org/trunk@62408 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/class-wp-hook.php | 3 + src/wp-includes/class-wp-widget-factory.php | 4 +- src/wp-includes/plugin.php | 31 ++++++----- tests/phpunit/tests/hooks/buildUniqueId.php | 62 +++++++++++++++++++++ 4 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 tests/phpunit/tests/hooks/buildUniqueId.php 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/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 ); + } +} From bc730ed53034c4cdfa8995dbb644ce555a75f1bf Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Fri, 22 May 2026 08:52:59 +0000 Subject: [PATCH 5/7] Build/Test Tools: Switch to PCOV for the coverage report runner. PCOV is a dedicated coverage reporting tool that performs significantly faster than Xdebug in coverage mode. This reduces the time that tests with coverage enabled take to run by around 50%. This also removes the HTML report generation which was producing unusable artifacts in excess of 7 GB in size. Props johnbillion, desrosj, swissspidy See #64893 git-svn-id: https://develop.svn.wordpress.org/trunk@62409 602fd350-edb4-49c9-b593-d223f7449a82 --- .../workflows/reusable-phpunit-tests-v3.yml | 42 +++++++++++++------ .github/workflows/test-coverage.yml | 2 - 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index b75a6e37f91b4..bcad042dfe559 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -82,8 +82,8 @@ 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_DB_TYPE: ${{ inputs.db-type }} LOCAL_DB_VERSION: ${{ inputs.db-version }} LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} @@ -113,7 +113,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. @@ -201,15 +200,40 @@ jobs: - name: Install WordPress run: npm run env:install + # Installs PCOV as the code coverage driver for the PHPUnit run below. + # + # The INI directives tune PCOV for WordPress's codebase: + # - `pcov.enabled` keeps the Zend hooks active (this is the default, but + # stated explicitly for clarity). + # - `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. + # - `pcov.initial.files` pre-sizes the internal file tracking array for + # the thousands of files under `src/`, avoiding reallocation churn + # during test warmup. The default of 64 is far too low here. + - name: Install PCOV coverage driver + if: ${{ inputs.coverage-report }} + run: | + docker compose exec -T -u 0 php sh -c ' + pecl install --force pcov-1.0.12 && + docker-php-ext-enable pcov && + { + echo "pcov.enabled=1" + echo "pcov.directory=/var/www/src" + echo "pcov.initial.files=10000" + } >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini && + php -m | grep -i pcov + ' + - 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 +268,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 From 4d069802771569c5f11a3021428c7e957bbb2567 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 22 May 2026 09:40:45 -0400 Subject: [PATCH 6/7] Use pre-installed PCOV within Docker environment. --- .env.example | 3 +++ .../workflows/reusable-phpunit-tests-v3.yml | 26 +------------------ docker-compose.yml | 2 ++ tools/local-env/php-config.ini | 6 +++++ tools/local-env/scripts/docker.js | 19 ++++++++++++++ 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index 6974d8554611a..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. # diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index bcad042dfe559..e06f9f24be17e 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -84,6 +84,7 @@ env: LOCAL_PHP: ${{ inputs.php }}-fpm 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 }} @@ -200,31 +201,6 @@ jobs: - name: Install WordPress run: npm run env:install - # Installs PCOV as the code coverage driver for the PHPUnit run below. - # - # The INI directives tune PCOV for WordPress's codebase: - # - `pcov.enabled` keeps the Zend hooks active (this is the default, but - # stated explicitly for clarity). - # - `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. - # - `pcov.initial.files` pre-sizes the internal file tracking array for - # the thousands of files under `src/`, avoiding reallocation churn - # during test warmup. The default of 64 is far too low here. - - name: Install PCOV coverage driver - if: ${{ inputs.coverage-report }} - run: | - docker compose exec -T -u 0 php sh -c ' - pecl install --force pcov-1.0.12 && - docker-php-ext-enable pcov && - { - echo "pcov.enabled=1" - echo "pcov.directory=/var/www/src" - echo "pcov.initial.files=10000" - } >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini && - php -m | grep -i pcov - ' - - 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: | diff --git a/docker-compose.yml b/docker-compose.yml index cc2ed8d94975e..71ecfb8f6b424 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} @@ -99,6 +100,7 @@ services: 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/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' ); From 454ef979fca3297c0bea6d7845a51033f8ac138b Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 22 May 2026 09:41:44 -0400 Subject: [PATCH 7/7] Use test images from wpdev-docker-images. See https://github.com/WordPress/wpdev-docker-images/pull/210. --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 71ecfb8f6b424..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 @@ -93,7 +93,7 @@ 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