From 6e7a4dc77e93280baca541df0e4f7b3ac7d719b0 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:38:52 +0200 Subject: [PATCH 01/10] Tests: Optimize shared PHPUnit setup Cache core registration resets and expected annotations, keep common test-context hooks registered persistently, avoid repeated autocommit setup, and apply low-cost password hashing to class fixtures. Update auth fixtures and add guard coverage for cached registration snapshots. --- tests/phpunit/includes/abstract-testcase.php | 525 +++++++++++++++---- tests/phpunit/tests/auth.php | 82 ++- tests/phpunit/tests/utils.php | 99 ++++ 3 files changed, 588 insertions(+), 118 deletions(-) diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index b8e8598362ec5..8b454ce6341fe 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -24,6 +24,11 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase { protected static $hooks_saved = array(); protected static $ignore_files; + protected static $expected_annotations_cache = array(); + protected static $core_registration_snapshots = array(); + protected static $db_autocommit_is_disabled = false; + protected static $test_context_hooks_registered = false; + protected static $deprecation_tracking_test = null; /** * Fixture factory. @@ -71,12 +76,19 @@ public static function set_up_before_class() { $wpdb->suppress_errors = false; $wpdb->show_errors = true; $wpdb->db_connect(); + self::$db_autocommit_is_disabled = false; ini_set( 'display_errors', 1 ); $class = get_called_class(); if ( method_exists( $class, 'wpSetUpBeforeClass' ) ) { - call_user_func( array( $class, 'wpSetUpBeforeClass' ), static::factory() ); + add_filter( 'wp_hash_password_options', array( __CLASS__, 'wp_hash_password_options_for_tests' ), 1, 2 ); + + try { + call_user_func( array( $class, 'wpSetUpBeforeClass' ), static::factory() ); + } finally { + remove_filter( 'wp_hash_password_options', array( __CLASS__, 'wp_hash_password_options_for_tests' ), 1 ); + } } self::commit_transaction(); @@ -116,6 +128,8 @@ public function set_up() { self::$ignore_files = $this->scan_user_uploads(); } + self::register_test_context_hooks(); + if ( ! self::$hooks_saved ) { $this->_backup_hooks(); } @@ -129,9 +143,7 @@ public function set_up() { * taxonomies at 'init'. */ if ( defined( 'WP_RUN_CORE_TESTS' ) && WP_RUN_CORE_TESTS ) { - $this->reset_post_types(); - $this->reset_taxonomies(); - $this->reset_post_statuses(); + $this->reset_core_registrations(); $this->reset__SERVER(); if ( $wp_rewrite->permalink_structure ) { @@ -141,8 +153,6 @@ public function set_up() { $this->start_transaction(); $this->expectDeprecated(); - add_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) ); - add_filter( 'wp_hash_password_options', array( $this, 'wp_hash_password_options' ), 1, 2 ); } /** @@ -152,8 +162,18 @@ public function set_up() { * @param string $algorithm The algorithm to use for hashing. */ public function wp_hash_password_options( array $options, string $algorithm ): array { + return self::wp_hash_password_options_for_tests( $options, $algorithm ); + } + + /** + * Sets the bcrypt cost option for password hashing during tests. + * + * @param array $options The options for password hashing. + * @param string $algorithm The algorithm to use for hashing. + */ + public static function wp_hash_password_options_for_tests( array $options, string $algorithm ): array { if ( PASSWORD_BCRYPT === $algorithm ) { - $options['cost'] = 5; + $options['cost'] = 4; } return $options; @@ -170,64 +190,68 @@ public function wp_hash_password_options( array $options, string $algorithm ): a public function tear_down() { global $wpdb, $wp_the_query, $wp_query, $wp; - $wpdb->query( 'ROLLBACK' ); + try { + $wpdb->query( 'ROLLBACK' ); - if ( is_multisite() ) { - while ( ms_is_switched() ) { - restore_current_blog(); + if ( is_multisite() ) { + while ( ms_is_switched() ) { + restore_current_blog(); + } } - } - // Reset query, main query, and WP globals similar to wp-settings.php. - $wp_the_query = new WP_Query(); - $wp_query = $wp_the_query; - $wp = new WP(); + // Reset query, main query, and WP globals similar to wp-settings.php. + $wp_the_query = new WP_Query(); + $wp_query = $wp_the_query; + $wp = new WP(); - // Reset globals related to the post loop and `setup_postdata()`. - $post_globals = array( 'post', 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' ); - foreach ( $post_globals as $global ) { - $GLOBALS[ $global ] = null; - } + // Reset globals related to the post loop and `setup_postdata()`. + $post_globals = array( 'post', 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' ); + foreach ( $post_globals as $global ) { + $GLOBALS[ $global ] = null; + } - /* - * Reset globals related to current screen to provide a consistent global starting state - * for tests that interact with admin screens. Replaces the need for individual tests - * to invoke `set_current_screen( 'front' )` (or an alternative implementation) as a reset. - * - * The globals are from `WP_Screen::set_current_screen()`. - * - * Why not invoke `set_current_screen( 'front' )`? - * Performance (faster test runs with less memory usage). How so? For each test, - * it saves creating an instance of WP_Screen, making two method calls, - * and firing of the `current_screen` action. - */ - $current_screen_globals = array( 'current_screen', 'taxnow', 'typenow' ); - foreach ( $current_screen_globals as $global ) { - $GLOBALS[ $global ] = null; - } + /* + * Reset globals related to current screen to provide a consistent global starting state + * for tests that interact with admin screens. Replaces the need for individual tests + * to invoke `set_current_screen( 'front' )` (or an alternative implementation) as a reset. + * + * The globals are from `WP_Screen::set_current_screen()`. + * + * Why not invoke `set_current_screen( 'front' )`? + * Performance (faster test runs with less memory usage). How so? For each test, + * it saves creating an instance of WP_Screen, making two method calls, + * and firing of the `current_screen` action. + */ + $current_screen_globals = array( 'current_screen', 'taxnow', 'typenow' ); + foreach ( $current_screen_globals as $global ) { + $GLOBALS[ $global ] = null; + } + + // Reset comment globals. + $comment_globals = array( 'comment_alt', 'comment_depth', 'comment_thread_alt' ); + foreach ( $comment_globals as $global ) { + $GLOBALS[ $global ] = null; + } + + /* + * Reset $wp_sitemap global so that sitemap-related dynamic $wp->public_query_vars + * are added when the next test runs. + */ + $GLOBALS['wp_sitemaps'] = null; + + // Reset template globals. + $GLOBALS['wp_stylesheet_path'] = null; + $GLOBALS['wp_template_path'] = null; - // Reset comment globals. - $comment_globals = array( 'comment_alt', 'comment_depth', 'comment_thread_alt' ); - foreach ( $comment_globals as $global ) { - $GLOBALS[ $global ] = null; + $this->unregister_all_meta_keys(); + remove_theme_support( 'html5' ); + remove_filter( 'query', array( $this, '_create_temporary_tables' ) ); + remove_filter( 'query', array( $this, '_drop_temporary_tables' ) ); + $this->_restore_hooks(); + } finally { + self::$deprecation_tracking_test = null; } - /* - * Reset $wp_sitemap global so that sitemap-related dynamic $wp->public_query_vars - * are added when the next test runs. - */ - $GLOBALS['wp_sitemaps'] = null; - - // Reset template globals. - $GLOBALS['wp_stylesheet_path'] = null; - $GLOBALS['wp_template_path'] = null; - - $this->unregister_all_meta_keys(); - remove_theme_support( 'html5' ); - remove_filter( 'query', array( $this, '_create_temporary_tables' ) ); - remove_filter( 'query', array( $this, '_drop_temporary_tables' ) ); - remove_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) ); - $this->_restore_hooks(); wp_set_current_user( 0 ); $this->reset_lazyload_queue(); @@ -462,6 +486,216 @@ public static function flush_cache() { wp_cache_add_non_persistent_groups( array( 'counts', 'plugins', 'theme_json' ) ); } + /** + * Resets core post type, taxonomy, and post status registrations. + */ + protected function reset_core_registrations() { + if ( ! $this->can_cache_core_registration_reset() ) { + $this->reset_post_types(); + $this->reset_taxonomies(); + $this->reset_post_statuses(); + return; + } + + $cache_key = $this->get_core_registration_cache_key(); + + if ( isset( self::$core_registration_snapshots[ $cache_key ] ) ) { + $this->restore_core_registration_snapshot( self::$core_registration_snapshots[ $cache_key ] ); + return; + } + + $this->reset_post_types(); + $this->reset_taxonomies(); + $this->reset_post_statuses(); + + self::$core_registration_snapshots[ $cache_key ] = $this->capture_core_registration_snapshot(); + } + + /** + * Determines whether the registration reset can use cached snapshots. + * + * @return bool Whether caching is safe for the current global state. + */ + protected function can_cache_core_registration_reset() { + global $wp_rewrite; + + /* + * Base setup clears active permalink structures immediately after the + * registration reset. Avoid caching this transient pre-clear state, as + * rewrite-heavy tests rebuild their rules explicitly after setup. + */ + if ( $wp_rewrite->permalink_structure ) { + return false; + } + + /* + * Preserve registration-time hooks and filters for tests that explicitly + * observe them. These hooks are rare and bypassing the cache keeps their + * behavior identical to the existing reset path. + */ + if ( + has_filter( 'post_format_rewrite_base' ) || + has_action( 'registered_post_type' ) || + has_action( 'unregistered_post_type' ) || + has_action( 'registered_taxonomy' ) || + has_action( 'unregistered_taxonomy' ) || + has_action( 'registered_taxonomy_for_object_type' ) || + has_action( 'unregistered_taxonomy_for_object_type' ) + ) { + return false; + } + + foreach ( array_keys( (array) $GLOBALS['wp_post_types'] ) as $post_type ) { + if ( + has_action( "registered_post_type_{$post_type}" ) || + has_filter( "post_type_labels_{$post_type}" ) + ) { + return false; + } + + if ( ! empty( $GLOBALS['wp_post_types'][ $post_type ]->tests_no_auto_unregister ) ) { + return false; + } + } + + foreach ( array_keys( (array) $GLOBALS['wp_taxonomies'] ) as $taxonomy ) { + if ( + has_action( "registered_taxonomy_{$taxonomy}" ) || + has_filter( "taxonomy_labels_{$taxonomy}" ) + ) { + return false; + } + } + + return true; + } + + /** + * Builds a cache key for the current registration defaults. + * + * @return string Cache key. + */ + protected function get_core_registration_cache_key() { + global $wp_rewrite; + + return md5( + serialize( + array( + 'is_admin' => is_admin(), + 'did_init' => did_action( 'init' ), + 'locale' => get_locale(), + 'category_base' => get_option( 'category_base' ), + 'tag_base' => get_option( 'tag_base' ), + 'wp_attachment_pages_enabled' => get_option( 'wp_attachment_pages_enabled' ), + 'post_formats' => current_theme_supports( 'post-formats' ), + 'permalink_structure' => $wp_rewrite->permalink_structure, + 'using_index_permalinks' => $wp_rewrite->using_index_permalinks(), + 'front' => $wp_rewrite->front, + 'root' => $wp_rewrite->root, + 'index' => $wp_rewrite->index, + ) + ) + ); + } + + /** + * Captures the registration globals after the normal reset path runs. + * + * @return array Snapshot of registration-related globals. + */ + protected function capture_core_registration_snapshot() { + global $wp, $wp_rewrite, $wp_post_types, $wp_taxonomies, $wp_post_statuses, $_wp_post_type_features, $post_type_meta_caps; + + return array( + 'wp_post_types' => self::clone_core_registration_objects( $wp_post_types ), + 'wp_taxonomies' => self::clone_core_registration_objects( $wp_taxonomies ), + 'wp_post_statuses' => self::clone_core_registration_objects( $wp_post_statuses ), + 'post_type_features' => $_wp_post_type_features, + 'post_type_meta_caps' => $post_type_meta_caps, + 'public_query_vars' => $wp->public_query_vars, + 'private_query_vars' => $wp->private_query_vars, + 'extra_query_vars' => $wp->extra_query_vars, + 'rewritecode' => $wp_rewrite->rewritecode, + 'rewritereplace' => $wp_rewrite->rewritereplace, + 'queryreplace' => $wp_rewrite->queryreplace, + 'rules' => $wp_rewrite->rules, + 'matches' => $wp_rewrite->matches, + 'extra_rules' => $wp_rewrite->extra_rules, + 'extra_rules_top' => $wp_rewrite->extra_rules_top, + 'extra_permastructs' => $wp_rewrite->extra_permastructs, + 'non_wp_rules' => $wp_rewrite->non_wp_rules, + 'endpoints' => $wp_rewrite->endpoints, + 'use_verbose_page_rules' => $wp_rewrite->use_verbose_page_rules, + ); + } + + /** + * Restores a cached registration snapshot. + * + * @param array $snapshot Snapshot of registration-related globals. + */ + protected function restore_core_registration_snapshot( array $snapshot ) { + global $wp, $wp_rewrite, $wp_post_types, $wp_taxonomies, $wp_post_statuses, $_wp_post_type_features, $post_type_meta_caps; + + WP_Post_Type::reset_default_labels(); + WP_Taxonomy::reset_default_labels(); + + $wp_post_types = self::clone_core_registration_objects( $snapshot['wp_post_types'] ); + $wp_taxonomies = self::clone_core_registration_objects( $snapshot['wp_taxonomies'] ); + $wp_post_statuses = self::clone_core_registration_objects( $snapshot['wp_post_statuses'] ); + $_wp_post_type_features = $snapshot['post_type_features']; + $post_type_meta_caps = $snapshot['post_type_meta_caps']; + + $wp->public_query_vars = $snapshot['public_query_vars']; + $wp->private_query_vars = $snapshot['private_query_vars']; + $wp->extra_query_vars = $snapshot['extra_query_vars']; + + $wp_rewrite->rewritecode = $snapshot['rewritecode']; + $wp_rewrite->rewritereplace = $snapshot['rewritereplace']; + $wp_rewrite->queryreplace = $snapshot['queryreplace']; + $wp_rewrite->rules = $snapshot['rules']; + $wp_rewrite->matches = $snapshot['matches']; + $wp_rewrite->extra_rules = $snapshot['extra_rules']; + $wp_rewrite->extra_rules_top = $snapshot['extra_rules_top']; + $wp_rewrite->extra_permastructs = $snapshot['extra_permastructs']; + $wp_rewrite->non_wp_rules = $snapshot['non_wp_rules']; + $wp_rewrite->endpoints = $snapshot['endpoints']; + $wp_rewrite->use_verbose_page_rules = $snapshot['use_verbose_page_rules']; + } + + /** + * Clones registration objects so cached snapshots cannot be mutated by tests. + * + * @param array $objects Registration objects. + * @return array Cloned registration objects. + */ + protected static function clone_core_registration_objects( array $objects ) { + $clones = array(); + + foreach ( $objects as $name => $object ) { + if ( ! is_object( $object ) ) { + $clones[ $name ] = $object; + continue; + } + + $clones[ $name ] = clone $object; + + foreach ( array( 'labels', 'cap', 'label_count' ) as $property ) { + if ( isset( $clones[ $name ]->$property ) && is_object( $clones[ $name ]->$property ) ) { + $clones[ $name ]->$property = clone $clones[ $name ]->$property; + } + } + + foreach ( array( 'rest_controller', 'revisions_rest_controller', 'autosave_rest_controller' ) as $property ) { + if ( property_exists( $clones[ $name ], $property ) ) { + $clones[ $name ]->$property = null; + } + } + } + + return $clones; + } + /** * Cleans up any registered meta keys. * @@ -493,7 +727,11 @@ public function unregister_all_meta_keys() { public function start_transaction() { global $wpdb; - $wpdb->query( 'SET autocommit = 0;' ); + if ( ! self::$db_autocommit_is_disabled ) { + $wpdb->query( 'SET autocommit = 0;' ); + self::$db_autocommit_is_disabled = true; + } + $wpdb->query( 'START TRANSACTION;' ); add_filter( 'query', array( $this, '_create_temporary_tables' ) ); @@ -584,46 +822,153 @@ public function wp_die_handler( $message, $title, $args ) { * @since 3.7.0 */ public function expectDeprecated() { - if ( method_exists( $this, 'getAnnotations' ) ) { - // PHPUnit < 9.5.0. - $annotations = $this->getAnnotations(); - } else { - // PHPUnit >= 9.5.0. - $annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations( - static::class, - $this->getName( false ) - ); - } + self::$deprecation_tracking_test = $this; - foreach ( array( 'class', 'method' ) as $depth ) { - if ( ! empty( $annotations[ $depth ]['expectedDeprecated'] ) ) { - $this->expected_deprecated = array_merge( - $this->expected_deprecated, - $annotations[ $depth ]['expectedDeprecated'] + $cache_key = static::class . '::' . $this->getName( false ); + + if ( ! isset( self::$expected_annotations_cache[ $cache_key ] ) ) { + if ( method_exists( $this, 'getAnnotations' ) ) { + // PHPUnit < 9.5.0. + $annotations = $this->getAnnotations(); + } else { + // PHPUnit >= 9.5.0. + $annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations( + static::class, + $this->getName( false ) ); } - if ( ! empty( $annotations[ $depth ]['expectedIncorrectUsage'] ) ) { - $this->expected_doing_it_wrong = array_merge( - $this->expected_doing_it_wrong, - $annotations[ $depth ]['expectedIncorrectUsage'] - ); + $expected_deprecated = array(); + $expected_doing_it_wrong = array(); + + foreach ( array( 'class', 'method' ) as $depth ) { + if ( ! empty( $annotations[ $depth ]['expectedDeprecated'] ) ) { + $expected_deprecated = array_merge( + $expected_deprecated, + $annotations[ $depth ]['expectedDeprecated'] + ); + } + + if ( ! empty( $annotations[ $depth ]['expectedIncorrectUsage'] ) ) { + $expected_doing_it_wrong = array_merge( + $expected_doing_it_wrong, + $annotations[ $depth ]['expectedIncorrectUsage'] + ); + } } + + self::$expected_annotations_cache[ $cache_key ] = array( + 'expected_deprecated' => $expected_deprecated, + 'expected_doing_it_wrong' => $expected_doing_it_wrong, + ); } - add_action( 'deprecated_function_run', array( $this, 'deprecated_function_run' ), 10, 3 ); - add_action( 'deprecated_argument_run', array( $this, 'deprecated_function_run' ), 10, 3 ); - add_action( 'deprecated_class_run', array( $this, 'deprecated_function_run' ), 10, 3 ); - add_action( 'deprecated_file_included', array( $this, 'deprecated_function_run' ), 10, 4 ); - add_action( 'deprecated_hook_run', array( $this, 'deprecated_function_run' ), 10, 4 ); - add_action( 'doing_it_wrong_run', array( $this, 'doing_it_wrong_run' ), 10, 3 ); + $this->expected_deprecated = array_merge( + $this->expected_deprecated, + self::$expected_annotations_cache[ $cache_key ]['expected_deprecated'] + ); + + $this->expected_doing_it_wrong = array_merge( + $this->expected_doing_it_wrong, + self::$expected_annotations_cache[ $cache_key ]['expected_doing_it_wrong'] + ); + } + + /** + * Registers persistent hooks for detecting deprecated and incorrect usage calls during tests. + */ + protected static function register_test_context_hooks() { + if ( self::$test_context_hooks_registered ) { + return; + } + + add_action( 'deprecated_function_run', array( __CLASS__, 'deprecated_function_run_for_current_test' ), 10, 3 ); + add_action( 'deprecated_argument_run', array( __CLASS__, 'deprecated_function_run_for_current_test' ), 10, 3 ); + add_action( 'deprecated_class_run', array( __CLASS__, 'deprecated_function_run_for_current_test' ), 10, 3 ); + add_action( 'deprecated_file_included', array( __CLASS__, 'deprecated_function_run_for_current_test' ), 10, 4 ); + add_action( 'deprecated_hook_run', array( __CLASS__, 'deprecated_function_run_for_current_test' ), 10, 4 ); + add_action( 'doing_it_wrong_run', array( __CLASS__, 'doing_it_wrong_run_for_current_test' ), 10, 3 ); + + add_filter( 'deprecated_function_trigger_error', array( __CLASS__, 'deprecation_trigger_error_for_current_test' ) ); + add_filter( 'deprecated_argument_trigger_error', array( __CLASS__, 'deprecation_trigger_error_for_current_test' ) ); + add_filter( 'deprecated_class_trigger_error', array( __CLASS__, 'deprecation_trigger_error_for_current_test' ) ); + add_filter( 'deprecated_file_trigger_error', array( __CLASS__, 'deprecation_trigger_error_for_current_test' ) ); + add_filter( 'deprecated_hook_trigger_error', array( __CLASS__, 'deprecation_trigger_error_for_current_test' ) ); + add_filter( 'doing_it_wrong_trigger_error', array( __CLASS__, 'deprecation_trigger_error_for_current_test' ) ); + add_filter( 'wp_die_handler', array( __CLASS__, 'wp_die_handler_for_current_test' ) ); + add_filter( 'wp_hash_password_options', array( __CLASS__, 'wp_hash_password_options_for_current_test' ), 1, 2 ); + + self::$test_context_hooks_registered = true; + } + + /** + * Retrieves the `wp_die()` handler for the currently running test. + * + * @param callable $handler The current die handler. + * @return callable The test die handler. + */ + public static function wp_die_handler_for_current_test( $handler ) { + if ( self::$deprecation_tracking_test instanceof self ) { + return array( self::$deprecation_tracking_test, 'wp_die_handler' ); + } + + return $handler; + } + + /** + * Sets the password hashing options for the currently running test. + * + * @param array $options The options for password hashing. + * @param string $algorithm The algorithm to use for hashing. + * @return array Password hashing options. + */ + public static function wp_hash_password_options_for_current_test( array $options, string $algorithm ): array { + if ( self::$deprecation_tracking_test instanceof self ) { + return self::$deprecation_tracking_test->wp_hash_password_options( $options, $algorithm ); + } + + return $options; + } + + /** + * Records deprecated calls for the currently running test. + * + * @param string $function_name Name of the deprecated function, class, file, or hook. + * @param string $replacement Replacement. + * @param string $version Version. + * @param string $message Optional message. + */ + public static function deprecated_function_run_for_current_test( $function_name, $replacement, $version, $message = '' ) { + if ( self::$deprecation_tracking_test instanceof self ) { + self::$deprecation_tracking_test->deprecated_function_run( $function_name, $replacement, $version, $message ); + } + } + + /** + * Records incorrect usage calls for the currently running test. + * + * @param string $function_name Function name. + * @param string $message Message. + * @param string $version Version. + */ + public static function doing_it_wrong_run_for_current_test( $function_name, $message, $version ) { + if ( self::$deprecation_tracking_test instanceof self ) { + self::$deprecation_tracking_test->doing_it_wrong_run( $function_name, $message, $version ); + } + } + + /** + * Suppresses PHP notices for deprecated and incorrect usage calls only while a test is running. + * + * @param bool $trigger Whether to trigger the PHP notice. + * @return bool Whether to trigger the PHP notice. + */ + public static function deprecation_trigger_error_for_current_test( $trigger ) { + if ( self::$deprecation_tracking_test instanceof self ) { + return false; + } - add_action( 'deprecated_function_trigger_error', '__return_false' ); - add_action( 'deprecated_argument_trigger_error', '__return_false' ); - add_action( 'deprecated_class_trigger_error', '__return_false' ); - add_action( 'deprecated_file_trigger_error', '__return_false' ); - add_action( 'deprecated_hook_trigger_error', '__return_false' ); - add_action( 'doing_it_wrong_trigger_error', '__return_false' ); + return $trigger; } /** diff --git a/tests/phpunit/tests/auth.php b/tests/phpunit/tests/auth.php index 409496a1167bd..7b354f8ca38f0 100644 --- a/tests/phpunit/tests/auth.php +++ b/tests/phpunit/tests/auth.php @@ -36,6 +36,14 @@ class Tests_Auth extends WP_UnitTestCase { protected static $password_length_limit = 4096; + protected static $phpass_length_limit_hash = '$P$BuCJXibHerqQlUjDbCXINsNu5pYqWd0'; + + protected static $plain_bcrypt_hash = '$2y$04$oPhLTndsIu9lp9LwxaaZJOscaqyfc1cAChJ0ppzRgKb11wIMoPsc.'; + + protected static $argon2i_hash = '$argon2i$v=19$m=1024,t=1,p=1$NUZUeUltZk1lZk9kTnNEag$y53Ml5m/6u0sE/74+f4hAc5cqqt2SFsVCuYkEhYeG+I'; + + protected static $argon2id_hash = '$argon2id$v=19$m=1024,t=1,p=1$Wi5seTFGMjRzeE1MbW9zTg$w9+Y093vlc6CFuQ63MnU8+dbdR91Z9HSRbmfZ/gGAh4'; + /** * Action hook. */ @@ -212,8 +220,8 @@ public function test_wp_check_password_supports_phpass_hash() { /** * Ensure wp_check_password() remains compatible with an increase to the default bcrypt cost. * - * The test verifies this by reducing the cost used to generate the hash, therefore mimicing a hash - * which was generated prior to the default cost being increased. + * The test verifies this by raising the active default cost after generating the hash, therefore + * mimicing a hash which was generated prior to the default cost being increased. * * Notably the bcrypt cost was increased in PHP 8.4: https://wiki.php.net/rfc/bcrypt_cost_2023 . * @@ -222,14 +230,16 @@ public function test_wp_check_password_supports_phpass_hash() { public function test_wp_check_password_supports_hash_with_increased_bcrypt_cost() { $password = 'password'; - // Reducing the cost mimics an increase to the default cost. - add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); $hash = wp_hash_password( $password, PASSWORD_BCRYPT ); - remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); + + // Increasing the cost before validation mimics a default cost increase. + add_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); $this->assertTrue( wp_check_password( $password, $hash ) ); $this->assertSame( 1, did_filter( 'check_password' ) ); $this->assertTrue( wp_password_needs_rehash( $hash ) ); + + remove_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); } /** @@ -273,8 +283,7 @@ public function test_wp_check_password_supports_wp_hash_with_default_bcrypt_cost */ public function test_wp_check_password_supports_plain_bcrypt_hash_with_default_bcrypt_cost() { $password = 'password'; - - $hash = password_hash( $password, PASSWORD_BCRYPT ); + $hash = self::$plain_bcrypt_hash; $this->assertTrue( wp_check_password( $password, $hash ) ); $this->assertSame( 1, did_filter( 'check_password' ) ); @@ -292,7 +301,7 @@ public function test_wp_check_password_supports_argon2i_hash() { } $password = 'password'; - $hash = password_hash( trim( $password ), PASSWORD_ARGON2I ); + $hash = self::$argon2i_hash; $this->assertTrue( wp_check_password( $password, $hash ) ); $this->assertSame( 1, did_filter( 'check_password' ) ); } @@ -310,7 +319,7 @@ public function test_wp_check_password_supports_argon2id_hash() { } $password = 'password'; - $hash = password_hash( trim( $password ), PASSWORD_ARGON2ID ); + $hash = self::$argon2id_hash; $this->assertTrue( wp_check_password( $password, $hash ) ); $this->assertSame( 1, did_filter( 'check_password' ) ); } @@ -685,7 +694,7 @@ public function test_invalid_password_at_phpass_length_limit_is_rejected() { $limit = str_repeat( 'a', self::$phpass_length_limit ); // Set the user password with the old phpass algorithm. - self::set_user_password_with_phpass( $limit, self::$user_id ); + self::set_user_password_hash( self::$phpass_length_limit_hash, self::$user_id ); // Authenticate. $user = wp_authenticate( $this->user->user_login, 'aaaaaaaa' ); @@ -699,7 +708,7 @@ public function test_valid_password_at_phpass_length_limit_is_accepted() { $limit = str_repeat( 'a', self::$phpass_length_limit ); // Set the user password with the old phpass algorithm. - self::set_user_password_with_phpass( $limit, self::$user_id ); + self::set_user_password_hash( self::$phpass_length_limit_hash, self::$user_id ); // Authenticate. $user = wp_authenticate( $this->user->user_login, $limit ); @@ -714,7 +723,7 @@ public function test_too_long_password_at_phpass_length_limit_is_rejected() { $limit = str_repeat( 'a', self::$phpass_length_limit ); // Set the user password with the old phpass algorithm. - self::set_user_password_with_phpass( $limit, self::$user_id ); + self::set_user_password_hash( self::$phpass_length_limit_hash, self::$user_id ); // Authenticate with a password that is one character too long. $user = wp_authenticate( $this->user->user_login, $limit . 'a' ); @@ -1038,13 +1047,10 @@ public function check_password_needs_rehashing() { $this->assertFalse( wp_password_needs_rehash( $hash ) ); // A future upgrade from a previously lower cost. - $default = self::get_default_bcrypt_cost(); - $opts = array( - // Reducing the cost mimics an increase in the default cost. - 'cost' => $default - 1, - ); - $hash = password_hash( $password, PASSWORD_BCRYPT, $opts ); + $hash = wp_hash_password( $password ); + add_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); $this->assertTrue( wp_password_needs_rehash( $hash ) ); + remove_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); // Previous phpass algorithm. $hash = self::$wp_hasher->HashPassword( $password ); @@ -1237,10 +1243,9 @@ public function test_md5_password_is_rehashed_after_successful_user_password_aut public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_user_password_authentication( $username_or_email ) { $password = 'password'; - // Hash the user password with a lower cost than default to mimic a cost upgrade. - add_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); + // Hash the user password before increasing the default cost. wp_set_password( $password, self::$user_id ); - remove_filter( 'wp_hash_password_options', array( $this, 'reduce_hash_cost' ) ); + add_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); // Verify that the password needs rehashing. $hash = get_userdata( self::$user_id )->user_pass; @@ -1249,7 +1254,7 @@ public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_ // Authenticate. $user = wp_authenticate( $username_or_email, $password ); - // Verify that the reduced cost password hash was valid. + // Verify that the previous-cost password hash was valid. $this->assertNotWPError( $user ); $this->assertInstanceOf( 'WP_User', $user ); $this->assertSame( self::$user_id, $user->ID ); @@ -1257,7 +1262,7 @@ public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_ // Verify that the password has been rehashed with the increased cost. $hash = get_userdata( self::$user_id )->user_pass; $this->assertFalse( wp_password_needs_rehash( $hash, self::$user_id ) ); - $this->assertSame( self::get_default_bcrypt_cost(), password_get_info( substr( $hash, 3 ) )['options']['cost'] ); + $this->assertSame( self::get_default_bcrypt_cost() + 1, password_get_info( substr( $hash, 3 ) )['options']['cost'] ); // Authenticate a second time to ensure the new hash is valid. $user = wp_authenticate( $username_or_email, $password ); @@ -1266,10 +1271,12 @@ public function test_bcrypt_password_is_rehashed_with_new_cost_after_successful_ $this->assertNotWPError( $user ); $this->assertInstanceOf( 'WP_User', $user ); $this->assertSame( self::$user_id, $user->ID ); + + remove_filter( 'wp_hash_password_options', array( $this, 'increase_hash_cost' ) ); } public function reduce_hash_cost( array $options ): array { - $options['cost'] = self::get_default_bcrypt_cost() - 1; + $options['cost'] = max( 4, self::get_default_bcrypt_cost() - 1 ); return $options; } @@ -1916,12 +1923,16 @@ public function test_set_user_password_with_phpass() { } private static function set_user_password_with_phpass( string $password, int $user_id ) { + self::set_user_password_hash( self::$wp_hasher->HashPassword( $password ), $user_id ); + } + + private static function set_user_password_hash( string $hash, int $user_id ) { global $wpdb; $wpdb->update( $wpdb->users, array( - 'user_pass' => self::$wp_hasher->HashPassword( $password ), + 'user_pass' => $hash, ), array( 'ID' => $user_id, @@ -1985,7 +1996,13 @@ private static function set_user_password_with_plain_bcrypt( string $password, i $wpdb->update( $wpdb->users, array( - 'user_pass' => password_hash( 'password', PASSWORD_BCRYPT ), + 'user_pass' => password_hash( + $password, + PASSWORD_BCRYPT, + array( + 'cost' => self::get_default_bcrypt_cost(), + ) + ), ), array( 'ID' => $user_id, @@ -2022,7 +2039,16 @@ public function test_set_application_password_with_plain_bcrypt() { * @return string The UUID of the application password. */ private static function set_application_password_with_plain_bcrypt( string $password, int $user_id ) { - return self::set_application_password( password_hash( $password, PASSWORD_BCRYPT ), $user_id ); + return self::set_application_password( + password_hash( + $password, + PASSWORD_BCRYPT, + array( + 'cost' => self::get_default_bcrypt_cost(), + ) + ), + $user_id + ); } /** @@ -2089,6 +2115,6 @@ private static function set_application_password( string $hash, int $user_id ) { } private static function get_default_bcrypt_cost(): int { - return 5; + return 4; } } diff --git a/tests/phpunit/tests/utils.php b/tests/phpunit/tests/utils.php index 6a4f0d45f6dc7..09520653d582c 100644 --- a/tests/phpunit/tests/utils.php +++ b/tests/phpunit/tests/utils.php @@ -55,4 +55,103 @@ public function test_mask_input_value() { EOF; $this->assertSame( $expected, mask_input_value( $in ) ); } + + /** + * @covers WP_UnitTestCase_Base::reset_core_registrations + */ + public function test_core_registration_snapshot_restore_uses_clean_clones() { + self::$core_registration_snapshots = array(); + + $this->reset_core_registrations(); + + $GLOBALS['wp_post_types']['post']->labels->name = 'Mutated Posts'; + $GLOBALS['wp_taxonomies']['category']->labels->name = 'Mutated Categories'; + $GLOBALS['_wp_post_type_features']['post']['title'] = false; + + $this->reset_core_registrations(); + + $this->assertNotSame( 'Mutated Posts', get_post_type_object( 'post' )->labels->name ); + $this->assertNotSame( 'Mutated Categories', get_taxonomy( 'category' )->labels->name ); + $this->assertNotFalse( $GLOBALS['_wp_post_type_features']['post']['title'] ); + } +} + +/** + * Tests registration reset behavior that must happen before parent setup. + * + * @group testsuite + */ +class Tests_Utils_Core_Registration_Reset extends WP_UnitTestCase { + + /** + * Primes the registration snapshot before this class adds label filters + * ahead of parent setup. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + self::$core_registration_snapshots = array(); + + $testcase = new self( 'test_core_registration_reset_cache_is_bypassed_for_label_filters_before_parent_setup' ); + $testcase->reset_core_registrations(); + } + + /** + * Adds label filters before the base setup reset runs. + */ + public function set_up() { + if ( ! self::$hooks_saved ) { + $this->_backup_hooks(); + } + + add_filter( 'post_type_labels_post', array( $this, 'filter_post_type_labels' ) ); + add_filter( 'taxonomy_labels_category', array( $this, 'filter_taxonomy_labels' ) ); + + parent::set_up(); + + remove_filter( 'post_type_labels_post', array( $this, 'filter_post_type_labels' ) ); + remove_filter( 'taxonomy_labels_category', array( $this, 'filter_taxonomy_labels' ) ); + } + + /** + * Removes filters after parent teardown restores the saved hooks. + */ + public function tear_down() { + parent::tear_down(); + + remove_filter( 'post_type_labels_post', array( $this, 'filter_post_type_labels' ) ); + remove_filter( 'taxonomy_labels_category', array( $this, 'filter_taxonomy_labels' ) ); + } + + /** + * @covers WP_UnitTestCase_Base::reset_core_registrations + */ + public function test_core_registration_reset_cache_is_bypassed_for_label_filters_before_parent_setup() { + $this->assertSame( 'Filtered Posts', get_post_type_object( 'post' )->labels->name ); + $this->assertSame( 'Filtered Categories', get_taxonomy( 'category' )->labels->name ); + } + + /** + * Filters core post type labels. + * + * @param object $labels Post type labels. + * @return object Filtered labels. + */ + public function filter_post_type_labels( $labels ) { + $labels->name = 'Filtered Posts'; + + return $labels; + } + + /** + * Filters core taxonomy labels. + * + * @param object $labels Taxonomy labels. + * @return object Filtered labels. + */ + public function filter_taxonomy_labels( $labels ) { + $labels->name = 'Filtered Categories'; + + return $labels; + } } From fb964f6bc43ee52e2165702a48975b468b98ee82 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:39:07 +0200 Subject: [PATCH 02/10] Tests: Run export tests in process Guard export headers once output has already started and define WXR helper functions only once per PHP process so the export test class no longer needs process isolation. --- src/wp-admin/includes/export.php | 450 +++++++++++++------------ tests/phpunit/tests/admin/exportWp.php | 3 - 2 files changed, 229 insertions(+), 224 deletions(-) diff --git a/src/wp-admin/includes/export.php b/src/wp-admin/includes/export.php index a77cb804f0780..0400dcbdad20f 100644 --- a/src/wp-admin/includes/export.php +++ b/src/wp-admin/includes/export.php @@ -93,9 +93,11 @@ function export_wp( $args = array() ) { */ $filename = apply_filters( 'export_wp_filename', $wp_filename, $sitename, $date ); - header( 'Content-Description: File Transfer' ); - header( 'Content-Disposition: attachment; filename=' . $filename ); - header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true ); + if ( ! headers_sent() ) { + header( 'Content-Description: File Transfer' ); + header( 'Content-Disposition: attachment; filename=' . $filename ); + header( 'Content-Type: text/xml; charset=' . get_option( 'blog_charset' ), true ); + } if ( 'all' !== $args['content'] && post_type_exists( $args['content'] ) ) { $ptype = get_post_type_object( $args['content'] ); @@ -234,266 +236,272 @@ function export_wp( $args = array() ) { unset( $categories, $custom_taxonomies, $custom_terms ); } - /** - * Wraps given string in XML CDATA tag. - * - * @since 2.1.0 - * - * @param string|null $str String to wrap in XML CDATA tag. May be null. - * @return string - */ - function wxr_cdata( $str ) { - $str = (string) $str; + static $wxr_functions_defined = false; - if ( ! wp_is_valid_utf8( $str ) ) { - $str = utf8_encode( $str ); - } - // $str = ent2ncr(esc_html($str)); - $str = '', ']]]]>', $str ) . ']]>'; + if ( ! $wxr_functions_defined ) { + /** + * Wraps given string in XML CDATA tag. + * + * @since 2.1.0 + * + * @param string|null $str String to wrap in XML CDATA tag. May be null. + * @return string + */ + function wxr_cdata( $str ) { + $str = (string) $str; - return $str; - } + if ( ! wp_is_valid_utf8( $str ) ) { + $str = utf8_encode( $str ); + } + // $str = ent2ncr(esc_html($str)); + $str = '', ']]]]>', $str ) . ']]>'; - /** - * Returns the URL of the site. - * - * @since 2.5.0 - * - * @return string Site URL. - */ - function wxr_site_url() { - if ( is_multisite() ) { - // Multisite: the base URL. - return network_home_url(); - } else { - // WordPress (single site): the site URL. - return get_bloginfo_rss( 'url' ); + return $str; } - } - /** - * Outputs a cat_name XML tag from a given category object. - * - * @since 2.1.0 - * - * @param WP_Term $category Category Object. - */ - function wxr_cat_name( $category ) { - if ( empty( $category->name ) ) { - return; + /** + * Returns the URL of the site. + * + * @since 2.5.0 + * + * @return string Site URL. + */ + function wxr_site_url() { + if ( is_multisite() ) { + // Multisite: the base URL. + return network_home_url(); + } else { + // WordPress (single site): the site URL. + return get_bloginfo_rss( 'url' ); + } } - echo '' . wxr_cdata( $category->name ) . "\n"; - } + /** + * Outputs a cat_name XML tag from a given category object. + * + * @since 2.1.0 + * + * @param WP_Term $category Category Object. + */ + function wxr_cat_name( $category ) { + if ( empty( $category->name ) ) { + return; + } - /** - * Outputs a category_description XML tag from a given category object. - * - * @since 2.1.0 - * - * @param WP_Term $category Category Object. - */ - function wxr_category_description( $category ) { - if ( empty( $category->description ) ) { - return; + echo '' . wxr_cdata( $category->name ) . "\n"; } - echo '' . wxr_cdata( $category->description ) . "\n"; - } + /** + * Outputs a category_description XML tag from a given category object. + * + * @since 2.1.0 + * + * @param WP_Term $category Category Object. + */ + function wxr_category_description( $category ) { + if ( empty( $category->description ) ) { + return; + } - /** - * Outputs a tag_name XML tag from a given tag object. - * - * @since 2.3.0 - * - * @param WP_Term $tag Tag Object. - */ - function wxr_tag_name( $tag ) { - if ( empty( $tag->name ) ) { - return; + echo '' . wxr_cdata( $category->description ) . "\n"; } - echo '' . wxr_cdata( $tag->name ) . "\n"; - } + /** + * Outputs a tag_name XML tag from a given tag object. + * + * @since 2.3.0 + * + * @param WP_Term $tag Tag Object. + */ + function wxr_tag_name( $tag ) { + if ( empty( $tag->name ) ) { + return; + } - /** - * Outputs a tag_description XML tag from a given tag object. - * - * @since 2.3.0 - * - * @param WP_Term $tag Tag Object. - */ - function wxr_tag_description( $tag ) { - if ( empty( $tag->description ) ) { - return; + echo '' . wxr_cdata( $tag->name ) . "\n"; } - echo '' . wxr_cdata( $tag->description ) . "\n"; - } + /** + * Outputs a tag_description XML tag from a given tag object. + * + * @since 2.3.0 + * + * @param WP_Term $tag Tag Object. + */ + function wxr_tag_description( $tag ) { + if ( empty( $tag->description ) ) { + return; + } - /** - * Outputs a term_name XML tag from a given term object. - * - * @since 2.9.0 - * - * @param WP_Term $term Term Object. - */ - function wxr_term_name( $term ) { - if ( empty( $term->name ) ) { - return; + echo '' . wxr_cdata( $tag->description ) . "\n"; } - echo '' . wxr_cdata( $term->name ) . "\n"; - } + /** + * Outputs a term_name XML tag from a given term object. + * + * @since 2.9.0 + * + * @param WP_Term $term Term Object. + */ + function wxr_term_name( $term ) { + if ( empty( $term->name ) ) { + return; + } - /** - * Outputs a term_description XML tag from a given term object. - * - * @since 2.9.0 - * - * @param WP_Term $term Term Object. - */ - function wxr_term_description( $term ) { - if ( empty( $term->description ) ) { - return; + echo '' . wxr_cdata( $term->name ) . "\n"; } - echo "\t\t" . wxr_cdata( $term->description ) . "\n"; - } - - /** - * Outputs term meta XML tags for a given term object. - * - * @since 4.6.0 - * - * @global wpdb $wpdb WordPress database abstraction object. - * - * @param WP_Term $term Term object. - */ - function wxr_term_meta( $term ) { - global $wpdb; - - $termmeta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->termmeta WHERE term_id = %d", $term->term_id ) ); - - foreach ( $termmeta as $meta ) { - /** - * Filters whether to selectively skip term meta used for WXR exports. - * - * Returning a truthy value from the filter will skip the current meta - * object from being exported. - * - * @since 4.6.0 - * - * @param bool $skip Whether to skip the current piece of term meta. Default false. - * @param string $meta_key Current meta key. - * @param object $meta Current meta object. - */ - if ( ! apply_filters( 'wxr_export_skip_termmeta', false, $meta->meta_key, $meta ) ) { - printf( "\t\t\n\t\t\t%s\n\t\t\t%s\n\t\t\n", wxr_cdata( $meta->meta_key ), wxr_cdata( $meta->meta_value ) ); + /** + * Outputs a term_description XML tag from a given term object. + * + * @since 2.9.0 + * + * @param WP_Term $term Term Object. + */ + function wxr_term_description( $term ) { + if ( empty( $term->description ) ) { + return; } + + echo "\t\t" . wxr_cdata( $term->description ) . "\n"; } - } - /** - * Outputs list of authors with posts. - * - * @since 3.1.0 - * - * @global wpdb $wpdb WordPress database abstraction object. - * - * @param int[] $post_ids Optional. Array of post IDs to filter the query by. - */ - function wxr_authors_list( ?array $post_ids = null ) { - global $wpdb; - - if ( ! empty( $post_ids ) ) { - $post_ids = array_map( 'absint', $post_ids ); - $post_id_chunks = array_chunk( $post_ids, 20 ); - } else { - $post_id_chunks = array( array() ); + /** + * Outputs term meta XML tags for a given term object. + * + * @since 4.6.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param WP_Term $term Term object. + */ + function wxr_term_meta( $term ) { + global $wpdb; + + $termmeta = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->termmeta WHERE term_id = %d", $term->term_id ) ); + + foreach ( $termmeta as $meta ) { + /** + * Filters whether to selectively skip term meta used for WXR exports. + * + * Returning a truthy value from the filter will skip the current meta + * object from being exported. + * + * @since 4.6.0 + * + * @param bool $skip Whether to skip the current piece of term meta. Default false. + * @param string $meta_key Current meta key. + * @param object $meta Current meta object. + */ + if ( ! apply_filters( 'wxr_export_skip_termmeta', false, $meta->meta_key, $meta ) ) { + printf( "\t\t\n\t\t\t%s\n\t\t\t%s\n\t\t\n", wxr_cdata( $meta->meta_key ), wxr_cdata( $meta->meta_value ) ); + } + } } - $authors = array(); + /** + * Outputs list of authors with posts. + * + * @since 3.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int[] $post_ids Optional. Array of post IDs to filter the query by. + */ + function wxr_authors_list( ?array $post_ids = null ) { + global $wpdb; + + if ( ! empty( $post_ids ) ) { + $post_ids = array_map( 'absint', $post_ids ); + $post_id_chunks = array_chunk( $post_ids, 20 ); + } else { + $post_id_chunks = array( array() ); + } - foreach ( $post_id_chunks as $next_posts ) { - $and = ! empty( $next_posts ) ? 'AND ID IN (' . implode( ', ', $next_posts ) . ')' : ''; + $authors = array(); - $results = $wpdb->get_results( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_status != 'auto-draft' $and" ); + foreach ( $post_id_chunks as $next_posts ) { + $and = ! empty( $next_posts ) ? 'AND ID IN (' . implode( ', ', $next_posts ) . ')' : ''; - foreach ( (array) $results as $result ) { - $authors[] = get_userdata( $result->post_author ); + $results = $wpdb->get_results( "SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_status != 'auto-draft' $and" ); + + foreach ( (array) $results as $result ) { + $authors[] = get_userdata( $result->post_author ); + } } - } - $authors = array_filter( $authors ); - $authors = array_unique( $authors, SORT_REGULAR ); // Remove duplicate authors. - - foreach ( $authors as $author ) { - echo "\t"; - echo '' . (int) $author->ID . ''; - echo '' . wxr_cdata( $author->user_login ) . ''; - echo '' . wxr_cdata( $author->user_email ) . ''; - echo '' . wxr_cdata( $author->display_name ) . ''; - echo '' . wxr_cdata( $author->first_name ) . ''; - echo '' . wxr_cdata( $author->last_name ) . ''; - echo "\n"; + $authors = array_filter( $authors ); + $authors = array_unique( $authors, SORT_REGULAR ); // Remove duplicate authors. + + foreach ( $authors as $author ) { + echo "\t"; + echo '' . (int) $author->ID . ''; + echo '' . wxr_cdata( $author->user_login ) . ''; + echo '' . wxr_cdata( $author->user_email ) . ''; + echo '' . wxr_cdata( $author->display_name ) . ''; + echo '' . wxr_cdata( $author->first_name ) . ''; + echo '' . wxr_cdata( $author->last_name ) . ''; + echo "\n"; + } } - } - /** - * Outputs all navigation menu terms. - * - * @since 3.1.0 - */ - function wxr_nav_menu_terms() { - $nav_menus = wp_get_nav_menus(); - if ( empty( $nav_menus ) || ! is_array( $nav_menus ) ) { - return; - } + /** + * Outputs all navigation menu terms. + * + * @since 3.1.0 + */ + function wxr_nav_menu_terms() { + $nav_menus = wp_get_nav_menus(); + if ( empty( $nav_menus ) || ! is_array( $nav_menus ) ) { + return; + } - foreach ( $nav_menus as $menu ) { - echo "\t"; - echo '' . (int) $menu->term_id . ''; - echo 'nav_menu'; - echo '' . wxr_cdata( $menu->slug ) . ''; - wxr_term_name( $menu ); - echo "\n"; + foreach ( $nav_menus as $menu ) { + echo "\t"; + echo '' . (int) $menu->term_id . ''; + echo 'nav_menu'; + echo '' . wxr_cdata( $menu->slug ) . ''; + wxr_term_name( $menu ); + echo "\n"; + } } - } - /** - * Outputs list of taxonomy terms, in XML tag format, associated with a post. - * - * @since 2.3.0 - */ - function wxr_post_taxonomy() { - $post = get_post(); + /** + * Outputs list of taxonomy terms, in XML tag format, associated with a post. + * + * @since 2.3.0 + */ + function wxr_post_taxonomy() { + $post = get_post(); - $taxonomies = get_object_taxonomies( $post->post_type ); - if ( empty( $taxonomies ) ) { - return; - } - $terms = wp_get_object_terms( $post->ID, $taxonomies ); + $taxonomies = get_object_taxonomies( $post->post_type ); + if ( empty( $taxonomies ) ) { + return; + } + $terms = wp_get_object_terms( $post->ID, $taxonomies ); - foreach ( (array) $terms as $term ) { - echo "\t\ttaxonomy}\" nicename=\"{$term->slug}\">" . wxr_cdata( $term->name ) . "\n"; + foreach ( (array) $terms as $term ) { + echo "\t\ttaxonomy}\" nicename=\"{$term->slug}\">" . wxr_cdata( $term->name ) . "\n"; + } } - } - /** - * Determines whether to selectively skip post meta used for WXR exports. - * - * @since 3.3.0 - * - * @param bool $return_me Whether to skip the current post meta. Default false. - * @param string $meta_key Meta key. - * @return bool - */ - function wxr_filter_postmeta( $return_me, $meta_key ) { - if ( '_edit_lock' === $meta_key ) { - $return_me = true; + /** + * Determines whether to selectively skip post meta used for WXR exports. + * + * @since 3.3.0 + * + * @param bool $return_me Whether to skip the current post meta. Default false. + * @param string $meta_key Meta key. + * @return bool + */ + function wxr_filter_postmeta( $return_me, $meta_key ) { + if ( '_edit_lock' === $meta_key ) { + $return_me = true; + } + return $return_me; } - return $return_me; + + $wxr_functions_defined = true; } add_filter( 'wxr_export_skip_postmeta', 'wxr_filter_postmeta', 10, 2 ); diff --git a/tests/phpunit/tests/admin/exportWp.php b/tests/phpunit/tests/admin/exportWp.php index f17ef0d4ad343..6b96654339d44 100644 --- a/tests/phpunit/tests/admin/exportWp.php +++ b/tests/phpunit/tests/admin/exportWp.php @@ -6,9 +6,6 @@ * * @covers ::export_wp * - * Tests run in a separate process to prevent "headers already sent" error. - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled */ class Tests_Admin_ExportWp extends WP_UnitTestCase { /** From eadb4dab9a60f0800b97e390d22601718d212fad Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:39:15 +0200 Subject: [PATCH 03/10] Tests: Avoid process isolation in sitemap 404 tests Initialize the sitemap server explicitly for the disabled-sitemap path and remove unnecessary process isolation from the two 404-path tests. --- tests/phpunit/tests/sitemaps/sitemaps.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/sitemaps/sitemaps.php b/tests/phpunit/tests/sitemaps/sitemaps.php index 85f9965245842..501d2d6cb7cf1 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps.php +++ b/tests/phpunit/tests/sitemaps/sitemaps.php @@ -464,12 +464,12 @@ public function test_sitemaps_enabled() { /** * @ticket 50643 - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_disable_sitemap_should_return_404() { add_filter( 'wp_sitemaps_enabled', '__return_false' ); + wp_sitemaps_get_server(); + $this->go_to( home_url( '/?sitemap=index' ) ); wp_sitemaps_get_server()->render_sitemaps(); @@ -481,8 +481,6 @@ public function test_disable_sitemap_should_return_404() { /** * @ticket 50643 - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_empty_url_list_should_return_404() { wp_register_sitemap_provider( 'foo', new WP_Sitemaps_Empty_Test_Provider( 'foo' ) ); From 1dca902b0e2393ab58803595d365f7396cb3af94 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:39:26 +0200 Subject: [PATCH 04/10] Tests: Reduce isolated PHP processes Pass explicit chmod values where constants are not under test, keep a focused FS_CHMOD_DIR coverage case isolated, and remove isolation from independent ID, upgrader, and theme fixture tests. --- tests/phpunit/tests/admin/wpUpgrader.php | 53 +++++------ .../filesystem/wpFilesystemDirect/mkdir.php | 89 ++++++++----------- .../tests/functions/wpUniquePrefixedId.php | 6 -- tests/phpunit/tests/theme.php | 2 - 4 files changed, 61 insertions(+), 89 deletions(-) diff --git a/tests/phpunit/tests/admin/wpUpgrader.php b/tests/phpunit/tests/admin/wpUpgrader.php index ffea594ae87bc..a5f371cdd091f 100644 --- a/tests/phpunit/tests/admin/wpUpgrader.php +++ b/tests/phpunit/tests/admin/wpUpgrader.php @@ -999,13 +999,8 @@ public function test_install_package_should_return_wp_error_when_source_director * @ticket 61114 * * @covers WP_Upgrader::install_package - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_install_package_should_return_wp_error_when_a_filtered_source_directory_file_list_cannot_be_retrieved() { - define( 'FS_CHMOD_DIR', 0755 ); - self::$instance->generic_strings(); self::$upgrader_skin_mock @@ -1097,23 +1092,11 @@ function ( $source ) { * Tests that `WP_Upgrader::install_package()` applies * 'upgrader_clear_destination' filters with arguments. * - * This test runs in a separate process so that it can define - * constants without impacting other tests. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed." when running in a - * separate process. - * * @ticket 54245 * * @covers WP_Upgrader::install_package - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_install_package_should_clear_destination_when_clear_destination_is_true() { - define( 'FS_CHMOD_FILE', 0644 ); - self::$instance->generic_strings(); self::$upgrader_skin_mock @@ -1149,6 +1132,18 @@ public function test_install_package_should_clear_destination_when_clear_destina ->withConsecutive( ...$dirlist_args ) ->willReturn( $dirlist_results ); + self::$wp_filesystem_mock + ->expects( $this->once() ) + ->method( 'is_writable' ) + ->with( '/dest_dir/file1.php' ) + ->willReturn( true ); + + self::$wp_filesystem_mock + ->expects( $this->once() ) + ->method( 'delete' ) + ->with( '/dest_dir/', true ) + ->willReturn( true ); + add_filter( 'upgrader_clear_destination', function ( $removed, $local_destination, $remote_destination, $hook_extra ) { @@ -1191,28 +1186,16 @@ function ( $removed, $local_destination, $remote_destination, $hook_extra ) { * Tests that `WP_Upgrader::install_package()` makes the * remote destination safe when set to a protected directory. * - * This test runs in a separate process so that it can define - * constants without impacting other tests. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed." when running in a - * separate process. - * * @ticket 54245 * * @covers WP_Upgrader::install_package * * @dataProvider data_install_package_should_make_remote_destination_safe_when_set_to_a_protected_directory * - * @runInSeparateProcess - * @preserveGlobalState disabled - * * @param string $protected_directory The path to a protected directory. * @param string $expected The expected safe remote destination. */ public function test_install_package_should_make_remote_destination_safe_when_set_to_a_protected_directory( $protected_directory, $expected ) { - define( 'FS_CHMOD_FILE', 0644 ); - self::$instance->generic_strings(); self::$upgrader_skin_mock @@ -1248,6 +1231,18 @@ public function test_install_package_should_make_remote_destination_safe_when_se ->withConsecutive( ...$dirlist_args ) ->willReturn( $dirlist_results ); + self::$wp_filesystem_mock + ->expects( $this->once() ) + ->method( 'is_writable' ) + ->with( $expected . 'file1.php' ) + ->willReturn( true ); + + self::$wp_filesystem_mock + ->expects( $this->once() ) + ->method( 'delete' ) + ->with( $expected, true ) + ->willReturn( true ); + add_filter( 'upgrader_clear_destination', function ( $removed, $local_destination, $remote_destination ) use ( $expected ) { diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/mkdir.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/mkdir.php index 0d87bd536b51d..0b7586f831427 100644 --- a/tests/phpunit/tests/filesystem/wpFilesystemDirect/mkdir.php +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/mkdir.php @@ -19,27 +19,15 @@ class Tests_Filesystem_WpFilesystemDirect_Mkdir extends WP_Filesystem_Direct_Uni /** * Tests that `WP_Filesystem_Direct::mkdir()` creates a directory. * - * This test runs in a separate process so that it can define - * constants without impacting other tests. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed." when running in a - * separate process. - * * @ticket 57774 * * @dataProvider data_should_create_directory * - * @runInSeparateProcess - * @preserveGlobalState disabled - * * @param mixed $path The path to create. */ public function test_should_create_directory( $path ) { - define( 'FS_CHMOD_DIR', 0755 ); - $path = str_replace( 'TEST_DIR', self::$file_structure['test_dir']['path'], $path ); - $actual = self::$filesystem->mkdir( $path ); + $actual = self::$filesystem->mkdir( $path, 0755 ); if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { rmdir( $path ); @@ -67,27 +55,15 @@ public function data_should_create_directory() { /** * Tests that `WP_Filesystem_Direct::mkdir()` does not create a directory. * - * This test runs in a separate process so that it can define - * constants without impacting other tests. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed." when running in a - * separate process. - * * @ticket 57774 * * @dataProvider data_should_not_create_directory * - * @runInSeparateProcess - * @preserveGlobalState disabled - * * @param mixed $path The path to create. */ public function test_should_not_create_directory( $path ) { - define( 'FS_CHMOD_DIR', 0755 ); - $path = str_replace( 'TEST_DIR', self::$file_structure['test_dir']['path'], $path ); - $actual = self::$filesystem->mkdir( $path ); + $actual = self::$filesystem->mkdir( $path, 0755 ); if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { rmdir( $path ); @@ -112,6 +88,39 @@ public function data_should_not_create_directory() { ); } + /** + * Tests that `WP_Filesystem_Direct::mkdir()` uses FS_CHMOD_DIR when chmod is not passed. + * + * This test runs in a separate process so that it can define + * constants without impacting other tests. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed." when running in a + * separate process. + * + * @ticket 57774 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_should_use_fs_chmod_dir_when_chmod_not_passed() { + define( 'FS_CHMOD_DIR', 0755 ); + + $path = self::$file_structure['test_dir']['path'] . 'directory-to-create'; + + $created = self::$filesystem->mkdir( $path ); + $chmod = substr( sprintf( '%o', fileperms( $path ) ), -4 ); + + if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { + rmdir( $path ); + } + + $expected_permissions = $this->is_windows() ? '0777' : '0755'; + + $this->assertTrue( $created, 'The directory was not created.' ); + $this->assertSame( $expected_permissions, $chmod, 'The permissions are incorrect.' ); + } + /** * Tests that `WP_Filesystem_Direct::mkdir()` sets chmod. * @@ -136,25 +145,13 @@ public function test_should_set_chmod() { /** * Tests that `WP_Filesystem_Direct::mkdir()` sets the owner. * - * This test runs in a separate process so that it can define - * constants without impacting other tests. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed." when running in a - * separate process. - * * @ticket 57774 - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_should_set_owner() { - define( 'FS_CHMOD_DIR', 0755 ); - $path = self::$file_structure['test_dir']['path'] . 'directory-to-create'; // Get the default owner. - self::$filesystem->mkdir( $path ); + self::$filesystem->mkdir( $path, 0755 ); $original_owner = fileowner( $path ); rmdir( $path ); @@ -173,25 +170,13 @@ public function test_should_set_owner() { /** * Tests that `WP_Filesystem_Direct::mkdir()` sets the group. * - * This test runs in a separate process so that it can define - * constants without impacting other tests. - * - * This test does not preserve global state to prevent the exception - * "Serialization of 'Closure' is not allowed." when running in a - * separate process. - * * @ticket 57774 - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_should_set_group() { - define( 'FS_CHMOD_DIR', 0755 ); - $path = self::$file_structure['test_dir']['path'] . 'directory-to-create'; // Get the default group. - self::$filesystem->mkdir( $path ); + self::$filesystem->mkdir( $path, 0755 ); $original_group = filegroup( $path ); rmdir( $path ); diff --git a/tests/phpunit/tests/functions/wpUniquePrefixedId.php b/tests/phpunit/tests/functions/wpUniquePrefixedId.php index ed6e489a3e259..e49e48d06f24d 100644 --- a/tests/phpunit/tests/functions/wpUniquePrefixedId.php +++ b/tests/phpunit/tests/functions/wpUniquePrefixedId.php @@ -19,9 +19,6 @@ class Tests_Functions_WpUniquePrefixedId extends WP_UnitTestCase { * * @dataProvider data_should_create_unique_prefixed_ids * - * @runInSeparateProcess - * @preserveGlobalState disabled - * * @param mixed $prefix The prefix. * @param array $expected The next two expected IDs. */ @@ -76,9 +73,6 @@ public function data_should_create_unique_prefixed_ids() { * * @dataProvider data_should_raise_notice_and_use_empty_string_prefix_when_nonstring_given * - * @runInSeparateProcess - * @preserveGlobalState disabled - * * @param mixed $non_string_prefix Non-string prefix. * @param int $number_of_ids_to_generate Number of IDs to generate. * As the prefix will default to an empty string, changing the number of IDs generated within each dataset further tests ID uniqueness. diff --git a/tests/phpunit/tests/theme.php b/tests/phpunit/tests/theme.php index aa67f7189c64d..c0e5c540a6915 100644 --- a/tests/phpunit/tests/theme.php +++ b/tests/phpunit/tests/theme.php @@ -276,8 +276,6 @@ public function test_default_theme_matches_constant() { * * @coversNothing * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_default_themes_are_included_in_new_files() { require_once ABSPATH . 'wp-admin/includes/update-core.php'; From 9b32b13f63561d62ace171d0e4757df89b58b609 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:39:39 +0200 Subject: [PATCH 05/10] Compat: Decode UTF-8 hot paths directly Avoid the generic UTF-8 scanner for the common first-code-point and ISO-8859-1 decode paths while retaining exhaustive polyfill and invalid-subpart coverage. --- src/wp-includes/compat-utf8.php | 76 ++++++++++++------ src/wp-includes/compat.php | 77 +++++++++++-------- tests/phpunit/tests/compat/mbChr.php | 10 +-- tests/phpunit/tests/compat/mbOrd.php | 12 +-- .../formatting/deprecatedUtfEncodeDecode.php | 16 ++-- .../tests/unicode/wpHasNoncharacters.php | 25 +++--- 6 files changed, 126 insertions(+), 90 deletions(-) diff --git a/src/wp-includes/compat-utf8.php b/src/wp-includes/compat-utf8.php index 5fa8cde158789..cba1fe85d82e9 100644 --- a/src/wp-includes/compat-utf8.php +++ b/src/wp-includes/compat-utf8.php @@ -506,37 +506,63 @@ function _wp_utf8_decode_fallback( $utf8_text ) { continue; } - $next_at = $at; $invalid_length = 0; - $found = _wp_scan_utf8( $utf8_text, $next_at, $invalid_length, null, 1 ); - $span_length = $next_at - $at; + $span_length = 0; $next_byte = '?'; + $byte1 = ord( $utf8_text[ $at ] ); + $byte2 = ord( $utf8_text[ $at + 1 ] ?? "\xC0" ); + $byte3 = ord( $utf8_text[ $at + 2 ] ?? "\xC0" ); + $byte4 = ord( $utf8_text[ $at + 3 ] ?? "\xC0" ); + + if ( $byte1 >= 0xC2 && $byte1 <= 0xDF && $byte2 >= 0x80 && $byte2 <= 0xBF ) { + $span_length = 2; + $code_point = ( ( $byte1 & 0x1F ) << 6 ) | ( $byte2 & 0x3F ); + $next_byte = $code_point <= 0xFF ? chr( $code_point ) : '?'; + } elseif ( + $byte3 >= 0x80 && $byte3 <= 0xBF && + ( + ( 0xE0 === $byte1 && $byte2 >= 0xA0 && $byte2 <= 0xBF ) || + ( $byte1 >= 0xE1 && $byte1 <= 0xEC && $byte2 >= 0x80 && $byte2 <= 0xBF ) || + ( 0xED === $byte1 && $byte2 >= 0x80 && $byte2 <= 0x9F ) || + ( $byte1 >= 0xEE && $byte1 <= 0xEF && $byte2 >= 0x80 && $byte2 <= 0xBF ) + ) + ) { + $span_length = 3; + } elseif ( + $byte3 >= 0x80 && $byte3 <= 0xBF && + $byte4 >= 0x80 && $byte4 <= 0xBF && + ( + ( 0xF0 === $byte1 && $byte2 >= 0x90 && $byte2 <= 0xBF ) || + ( $byte1 >= 0xF1 && $byte1 <= 0xF3 && $byte2 >= 0x80 && $byte2 <= 0xBF ) || + ( 0xF4 === $byte1 && $byte2 >= 0x80 && $byte2 <= 0x8F ) + ) + ) { + $span_length = 4; + } else { + $next_byte = ''; + $invalid_length = 1; + + if ( 0xE0 === ( $byte1 & 0xF0 ) ) { + $byte2_valid = ( + ( 0xE0 === $byte1 && $byte2 >= 0xA0 && $byte2 <= 0xBF ) || + ( $byte1 >= 0xE1 && $byte1 <= 0xEC && $byte2 >= 0x80 && $byte2 <= 0xBF ) || + ( 0xED === $byte1 && $byte2 >= 0x80 && $byte2 <= 0x9F ) || + ( $byte1 >= 0xEE && $byte1 <= 0xEF && $byte2 >= 0x80 && $byte2 <= 0xBF ) + ); - if ( 1 !== $found ) { - if ( $invalid_length > 0 ) { - $next_byte = ''; - goto flush_sub_part; - } - - break; - } - - // All convertible code points are two-bytes long. - $byte1 = ord( $utf8_text[ $at ] ); - if ( 0xC0 !== ( $byte1 & 0xE0 ) ) { - goto flush_sub_part; - } + $invalid_length = min( $end - $at, $byte2_valid ? 2 : 1 ); + } elseif ( 0xF0 === ( $byte1 & 0xF8 ) ) { + $byte2_valid = ( + ( 0xF0 === $byte1 && $byte2 >= 0x90 && $byte2 <= 0xBF ) || + ( $byte1 >= 0xF1 && $byte1 <= 0xF3 && $byte2 >= 0x80 && $byte2 <= 0xBF ) || + ( 0xF4 === $byte1 && $byte2 >= 0x80 && $byte2 <= 0x8F ) + ); + $byte3_valid = $byte3 >= 0x80 && $byte3 <= 0xBF; - // All convertible code points are not greater than U+FF. - $byte2 = ord( $utf8_text[ $at + 1 ] ); - $code_point = ( ( $byte1 & 0x1F ) << 6 ) | ( ( $byte2 & 0x3F ) ); - if ( $code_point > 0xFF ) { - goto flush_sub_part; + $invalid_length = min( $end - $at, $byte2_valid ? ( $byte3_valid ? 3 : 2 ) : 1 ); + } } - $next_byte = chr( $code_point ); - - flush_sub_part: $iso_8859_1_text .= substr( $utf8_text, $was_at, $at - $was_at ); $iso_8859_1_text .= $next_byte; $at += $span_length; diff --git a/src/wp-includes/compat.php b/src/wp-includes/compat.php index 3387b1d85c935..3e8ab8020b83c 100644 --- a/src/wp-includes/compat.php +++ b/src/wp-includes/compat.php @@ -213,41 +213,56 @@ function _mb_ord( $string, $encoding = null ) { return false; } - $byte_length = 0; - $invalid_length = 0; - $found_count = _wp_scan_utf8( $string, $byte_length, $invalid_length, null, 1 ); + $b0 = ord( $string[0] ); - if ( 1 !== $found_count ) { - return false; + if ( $b0 <= 0x7F ) { + return $b0; } - // These are valid code points, so no further validation is required. - $b0 = ord( $string[0] ); + $b1 = ord( $string[1] ?? "\x00" ); - switch ( $byte_length ) { - case 1: - return $b0; - - case 2: - return ( - ( ( $b0 & 0x1F ) << 6 ) | - ( ( ord( $string[1] ) & 0x3F ) ) - ); - - case 3: - return ( - ( ( $b0 & 0x0F ) << 12 ) | - ( ( ord( $string[1] ) & 0x3F ) << 6 ) | - ( ( ord( $string[2] ) & 0x3F ) ) - ); - - case 4: - return ( - ( ( $b0 & 0x07 ) << 18 ) | - ( ( ord( $string[1] ) & 0x3F ) << 12 ) | - ( ( ord( $string[2] ) & 0x3F ) << 6 ) | - ( ( ord( $string[3] ) & 0x3F ) ) - ); + if ( $b0 >= 0xC2 && $b0 <= 0xDF && $b1 >= 0x80 && $b1 <= 0xBF ) { + return ( + ( ( $b0 & 0x1F ) << 6 ) | + ( $b1 & 0x3F ) + ); + } + + $b2 = ord( $string[2] ?? "\x00" ); + + if ( + $b2 >= 0x80 && $b2 <= 0xBF && + ( + ( 0xE0 === $b0 && $b1 >= 0xA0 && $b1 <= 0xBF ) || + ( $b0 >= 0xE1 && $b0 <= 0xEC && $b1 >= 0x80 && $b1 <= 0xBF ) || + ( 0xED === $b0 && $b1 >= 0x80 && $b1 <= 0x9F ) || + ( $b0 >= 0xEE && $b0 <= 0xEF && $b1 >= 0x80 && $b1 <= 0xBF ) + ) + ) { + return ( + ( ( $b0 & 0x0F ) << 12 ) | + ( ( $b1 & 0x3F ) << 6 ) | + ( $b2 & 0x3F ) + ); + } + + $b3 = ord( $string[3] ?? "\x00" ); + + if ( + $b2 >= 0x80 && $b2 <= 0xBF && + $b3 >= 0x80 && $b3 <= 0xBF && + ( + ( 0xF0 === $b0 && $b1 >= 0x90 && $b1 <= 0xBF ) || + ( $b0 >= 0xF1 && $b0 <= 0xF3 && $b1 >= 0x80 && $b1 <= 0xBF ) || + ( 0xF4 === $b0 && $b1 >= 0x80 && $b1 <= 0x8F ) + ) + ) { + return ( + ( ( $b0 & 0x07 ) << 18 ) | + ( ( $b1 & 0x3F ) << 12 ) | + ( ( $b2 & 0x3F ) << 6 ) | + ( $b3 & 0x3F ) + ); } return false; diff --git a/tests/phpunit/tests/compat/mbChr.php b/tests/phpunit/tests/compat/mbChr.php index 6862ed9170479..54fcd36264577 100644 --- a/tests/phpunit/tests/compat/mbChr.php +++ b/tests/phpunit/tests/compat/mbChr.php @@ -14,13 +14,13 @@ class Tests_Compat_mbChr extends WP_UnitTestCase { */ public function test_mb_chr_polyfill_matches_spec() { for ( $code_point = 0; $code_point <= 0x10FFFF; $code_point++ ) { - $this->assertSame( - mb_chr( $code_point ), - _mb_chr( $code_point ), - 'Failed to properly decode the code point from the string.' - ); + if ( mb_chr( $code_point ) !== _mb_chr( $code_point ) ) { + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + $this->fail( "Failed to properly encode U+{$hex_char}." ); + } } + $this->assertTrue( true, 'All valid code points were encoded properly.' ); $this->assertFalse( _mb_chr( ord( 'A' ), 'latin1' ), 'Should have rejected non-UTF-8 encoding.' ); $this->assertFalse( _mb_ord( ord( 'A' ), 'utf8' ), 'Should have rejected non-UTF-8 encoding.' ); $this->assertSame( 'A', _mb_chr( ord( 'A' ), 'UTF-8' ), 'Should have accepted UTF-8 encoding.' ); diff --git a/tests/phpunit/tests/compat/mbOrd.php b/tests/phpunit/tests/compat/mbOrd.php index 214547498d643..b327ab02f5d0d 100644 --- a/tests/phpunit/tests/compat/mbOrd.php +++ b/tests/phpunit/tests/compat/mbOrd.php @@ -22,15 +22,15 @@ public function test_mb_ord_polyfill_matches_spec() { * and spot-check an unpaired and incorrectly-converted surrogate * half below. */ - if ( false !== mb_chr( $code_point ) ) { - $this->assertSame( - $code_point, - _mb_ord( mb_chr( $code_point ) ), - 'Failed to properly decode the code point from the string.' - ); + $char = mb_chr( $code_point ); + + if ( false !== $char && $code_point !== _mb_ord( $char ) ) { + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + $this->fail( "Failed to properly decode U+{$hex_char} from the string." ); } } + $this->assertTrue( true, 'All valid code points were decoded properly.' ); $this->assertFalse( _mb_ord( '' ), 'Should have failed on empty string.' ); $this->assertFalse( _mb_ord( 'hi', 'latin1' ), 'Should have rejected non-UTF-8 encoding.' ); $this->assertFalse( _mb_ord( 'hi', 'utf8' ), 'Should have rejected non-UTF-8 encoding.' ); diff --git a/tests/phpunit/tests/formatting/deprecatedUtfEncodeDecode.php b/tests/phpunit/tests/formatting/deprecatedUtfEncodeDecode.php index e685df3429e15..162a54e1e438a 100644 --- a/tests/phpunit/tests/formatting/deprecatedUtfEncodeDecode.php +++ b/tests/phpunit/tests/formatting/deprecatedUtfEncodeDecode.php @@ -58,9 +58,9 @@ public static function data_utf8_strings() { * @ticket 63863. */ public function test_utf8_decode_characters() { - for ( $i = 0; $i <= 0x10FFFF; $i++ ) { - $hex_i = strtoupper( str_pad( dechex( $i ), 2, '0', STR_PAD_LEFT ) ); + $input = ''; + for ( $i = 0; $i <= 0x10FFFF; $i++ ) { if ( $i < 0xD800 || $i > 0xE000 ) { $c = mb_chr( $i ); } else { @@ -75,12 +75,14 @@ public function test_utf8_decode_characters() { $c = "{$byte1}{$byte2}{$byte3}"; } - $this->assertSame( - bin2hex( mb_convert_encoding( $c, 'ISO-8859-1', 'UTF-8' ) ), - bin2hex( _wp_utf8_decode_fallback( $c ) ), - "Failed to convert U+{$hex_i} properly." - ); + $input .= "{$c} "; } + + $this->assertSame( + bin2hex( mb_convert_encoding( $input, 'ISO-8859-1', 'UTF-8' ) ), + bin2hex( _wp_utf8_decode_fallback( $input ) ), + 'Failed to convert all Unicode code points properly.' + ); } /** diff --git a/tests/phpunit/tests/unicode/wpHasNoncharacters.php b/tests/phpunit/tests/unicode/wpHasNoncharacters.php index 073b57b65134e..0fb0b43d2a333 100644 --- a/tests/phpunit/tests/unicode/wpHasNoncharacters.php +++ b/tests/phpunit/tests/unicode/wpHasNoncharacters.php @@ -68,22 +68,13 @@ public function test_detects_non_characters_when_string_contains_invalid_utf8() * @ticket 63863 */ public function test_avoids_false_positives() { - // Get all the noncharacters in one long string, each surrounded on both sides by null bytes. - $noncharacters = implode( - "\x00", - array_map( - static function ( $c ) { - return "\x00{$c}"; - }, - array_column( array_values( iterator_to_array( self::data_noncharacters() ) ), 0 ) - ) - ) . "\x00"; - $this->assertFalse( wp_has_noncharacters( "\x00" ), 'Falsely detected noncharacter in U+0000' ); + $characters = ''; + for ( $code_point = 1; $code_point <= 0x10FFFF; $code_point++ ) { // Surrogate halves are invalid UTF-8. if ( $code_point >= 0xD800 && $code_point <= 0xDFFF ) { @@ -93,18 +84,20 @@ static function ( $c ) { $char = mb_chr( $code_point ); $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); - if ( str_contains( $noncharacters, $char ) ) { + if ( ( $code_point >= 0xFDD0 && $code_point <= 0xFDEF ) || 0xFFFE === ( $code_point & 0xFFFE ) ) { $this->assertTrue( wp_has_noncharacters( $char ), "Failed to detect noncharacter as test verification for U+{$hex_char}" ); } else { - $this->assertFalse( - wp_has_noncharacters( $char ), - "Falsely detected noncharacter in U+{$hex_char}." - ); + $characters .= $char; } } + + $this->assertFalse( + wp_has_noncharacters( $characters ), + 'Falsely detected a noncharacter in a string containing every valid Unicode character.' + ); } /** From 0f2273fdb14681e8fffbaec384fd1edb7164d09f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:39:47 +0200 Subject: [PATCH 06/10] HTML API: Use byte helpers in token maps Replace pack/unpack single-byte length handling with chr/ord in WP_Token_Map hot paths. --- src/wp-includes/class-wp-token-map.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/class-wp-token-map.php b/src/wp-includes/class-wp-token-map.php index fc223b187f8c5..507b3365852d8 100644 --- a/src/wp-includes/class-wp-token-map.php +++ b/src/wp-includes/class-wp-token-map.php @@ -352,8 +352,8 @@ static function ( array $a, array $b ): int { foreach ( $groups[ $group ] as $group_word ) { list( $word, $mapping ) = $group_word; - $word_length = pack( 'C', strlen( $word ) ); - $mapping_length = pack( 'C', strlen( $mapping ) ); + $word_length = chr( strlen( $word ) ); + $mapping_length = chr( strlen( $mapping ) ); $group_string .= "{$word_length}{$word}{$mapping_length}{$mapping}"; } @@ -472,10 +472,10 @@ public function contains( string $word, string $case_sensitivity = 'case-sensiti $at = 0; while ( $at < $group_length ) { - $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token_length = ord( $group[ $at++ ] ); $token_at = $at; $at += $token_length; - $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping_length = ord( $group[ $at++ ] ); $mapping_at = $at; if ( $token_length === $length && 0 === substr_compare( $group, $slug, $token_at, $token_length, $ignore_case ) ) { @@ -559,10 +559,10 @@ public function read_token( string $text, int $offset = 0, &$matched_token_byte_ $group_length = strlen( $group ); $at = 0; while ( $at < $group_length ) { - $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token_length = ord( $group[ $at++ ] ); $token = substr( $group, $at, $token_length ); $at += $token_length; - $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping_length = ord( $group[ $at++ ] ); $mapping_at = $at; if ( 0 === substr_compare( $text, $token, $offset + $this->key_length, $token_length, $ignore_case ) ) { @@ -666,11 +666,11 @@ public function to_array(): array { $group_length = strlen( $group ); $at = 0; while ( $at < $group_length ) { - $length = unpack( 'C', $group[ $at++ ] )[1]; + $length = ord( $group[ $at++ ] ); $key = $prefix . substr( $group, $at, $length ); $at += $length; - $length = unpack( 'C', $group[ $at++ ] )[1]; + $length = ord( $group[ $at++ ] ); $value = substr( $group, $at, $length ); $tokens[ $key ] = $value; @@ -737,10 +737,10 @@ public function precomputed_php_source_table( string $indent = "\t" ): string { $data_line = "{$i3}\""; $at = 0; while ( $at < $group_length ) { - $token_length = unpack( 'C', $group[ $at++ ] )[1]; + $token_length = ord( $group[ $at++ ] ); $token = substr( $group, $at, $token_length ); $at += $token_length; - $mapping_length = unpack( 'C', $group[ $at++ ] )[1]; + $mapping_length = ord( $group[ $at++ ] ); $mapping = substr( $group, $at, $mapping_length ); $at += $mapping_length; From f6115ff31577433521e03e54eda0bf4ef07c6c91 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:39:59 +0200 Subject: [PATCH 07/10] Tests: Reduce media fixture cost Use smaller deterministic image fixtures, avoid external missing-URL latency, and keep cross-origin rewrite coverage in-process while preserving one isolated header assertion. --- src/wp-includes/media.php | 5 +- tests/phpunit/tests/image/functions.php | 3 +- tests/phpunit/tests/media.php | 85 +++++++++++-------- .../tests/media/wpCrossOriginIsolation.php | 55 ++++++------ .../media/wpGenerateAttachmentMetadata.php | 10 ++- 5 files changed, 90 insertions(+), 68 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 4657b5872eb18..1a452890cad4f 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6593,7 +6593,9 @@ function wp_start_cross_origin_isolation_output_buffer(): void { ob_start( static function ( string $output ): string { - header( 'Document-Isolation-Policy: isolate-and-credentialless' ); + if ( ! headers_sent() ) { + header( 'Document-Isolation-Policy: isolate-and-credentialless' ); + } return wp_add_crossorigin_attributes( $output ); } @@ -6674,4 +6676,3 @@ function wp_add_crossorigin_attributes( string $html ): string { return $processor->get_updated_html(); } - diff --git a/tests/phpunit/tests/image/functions.php b/tests/phpunit/tests/image/functions.php index 94cc0111f0d4c..babf5a3a1003b 100644 --- a/tests/phpunit/tests/image/functions.php +++ b/tests/phpunit/tests/image/functions.php @@ -687,11 +687,10 @@ public function test_wp_crop_image_should_fail_with_wp_error_object_if_file_does /** * @covers ::wp_crop_image - * @requires extension openssl */ public function test_wp_crop_image_should_fail_with_wp_error_object_if_url_does_not_exist() { $file = wp_crop_image( - 'https://wordpress.org/screenshots/3.9/canoladoesnotexist.jpg', + 'http://127.0.0.1:0/canoladoesnotexist.jpg', 0, 0, 100, diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index c3e118ee718d5..b8ba1139cc2ed 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -1864,16 +1864,9 @@ public function test_wp_calculate_image_srcset() { */ public function test_wp_calculate_image_srcset_no_date_uploads() { $_wp_additional_image_sizes = wp_get_additional_image_sizes(); - - // Disable date organized uploads. - add_filter( 'upload_dir', '_upload_dir_no_subdir' ); - - // Make an image. - $filename = DIR_TESTDATA . '/images/' . self::$large_filename; - $id = self::factory()->attachment->create_upload_object( $filename ); - - $image_meta = wp_get_attachment_metadata( $id ); - $uploads_dir_url = 'http://' . WP_TESTS_DOMAIN . '/wp-content/uploads/'; + $image_meta = wp_get_attachment_metadata( self::$large_id ); + $image_meta['file'] = wp_basename( $image_meta['file'] ); + $uploads_dir_url = 'http://' . WP_TESTS_DOMAIN . '/wp-content/uploads/'; // Set up test cases for all expected size names. $intermediates = array( 'medium', 'medium_large', 'large', 'full' ); @@ -1896,13 +1889,13 @@ public function test_wp_calculate_image_srcset_no_date_uploads() { $expected = trim( $expected, ' ,' ); foreach ( $intermediates as $int_size ) { - $image_urls[ $int_size ] = wp_get_attachment_image_url( $id, $int_size ); + if ( 'full' === $int_size ) { + $image_urls[ $int_size ] = $uploads_dir_url . $image_meta['file']; + } else { + $image_urls[ $int_size ] = $uploads_dir_url . $image_meta['sizes'][ $int_size ]['file']; + } } - // Remove the attachment. - wp_delete_attachment( $id, true ); - remove_filter( 'upload_dir', '_upload_dir_no_subdir' ); - foreach ( $intermediates as $int_size ) { $size_array = $this->get_image_size_array_from_meta( $image_meta, $int_size ); $image_url = $image_urls[ $int_size ]; @@ -5650,8 +5643,8 @@ private function reset_high_priority_element_flag() { public function test_quality_with_image_conversion_file_sizes() { add_filter( 'image_editor_output_format', array( $this, 'image_editor_output_jpeg' ) ); $temp_dir = get_temp_dir(); - $file = $temp_dir . '/33772.jpg'; - copy( DIR_TESTDATA . '/images/33772.jpg', $file ); + $file = $temp_dir . '/a2-small.jpg'; + copy( DIR_TESTDATA . '/images/a2-small.jpg', $file ); // Set JPEG output quality very low and WebP quality very high, this should force all generated WebP images to // be larger than the matching generated JPEGs. @@ -5671,27 +5664,40 @@ public function test_quality_with_image_conversion_file_sizes() { ) ); - add_filter( 'big_image_size_threshold', array( $this, 'add_big_image_size_threshold' ) ); + add_image_size( 'test-size-250', 250, 250 ); + add_image_size( 'test-size-200', 200, 200 ); + + add_filter( + 'big_image_size_threshold', + static function () { + return 300; + } + ); - // Generate all sizes as JPEGs. - $jpeg_sizes = wp_generate_attachment_metadata( $attachment_id, $file ); - remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_jpeg' ) ); + try { + // Generate all sizes as JPEGs. + $jpeg_sizes = wp_generate_attachment_metadata( $attachment_id, $file ); + remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_jpeg' ) ); - // Generate all sizes as WebP. - add_filter( 'image_editor_output_format', array( $this, 'image_editor_output_webp' ) ); - $webp_sizes = wp_generate_attachment_metadata( $attachment_id, $file ); - remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_webp' ) ); + // Generate all sizes as WebP. + add_filter( 'image_editor_output_format', array( $this, 'image_editor_output_webp' ) ); + $webp_sizes = wp_generate_attachment_metadata( $attachment_id, $file ); + remove_filter( 'image_editor_output_format', array( $this, 'image_editor_output_webp' ) ); - // The main (scaled) image: the JPEG should be smaller than the WebP. - $this->assertLessThan( $webp_sizes['filesize'], $jpeg_sizes['filesize'], 'The JPEG should be smaller than the WebP.' ); + // The main (scaled) image: the JPEG should be smaller than the WebP. + $this->assertLessThan( $webp_sizes['filesize'], $jpeg_sizes['filesize'], 'The JPEG should be smaller than the WebP.' ); - // Sub-sizes: for each size, the JPEGs should be smaller than the WebP. - $sizes_to_compare = array_intersect_key( $jpeg_sizes['sizes'], $webp_sizes['sizes'] ); + // Sub-sizes: for each size, the JPEGs should be smaller than the WebP. + $sizes_to_compare = array_intersect_key( $jpeg_sizes['sizes'], $webp_sizes['sizes'] ); - $this->assertNotEmpty( $sizes_to_compare ); + $this->assertNotEmpty( $sizes_to_compare ); - foreach ( $sizes_to_compare as $size => $size_data ) { - $this->assertLessThan( $webp_sizes['sizes'][ $size ]['filesize'], $jpeg_sizes['sizes'][ $size ]['filesize'] ); + foreach ( $sizes_to_compare as $size => $size_data ) { + $this->assertLessThan( $webp_sizes['sizes'][ $size ]['filesize'], $jpeg_sizes['sizes'][ $size ]['filesize'] ); + } + } finally { + remove_image_size( 'test-size-250' ); + remove_image_size( 'test-size-200' ); } } @@ -7094,9 +7100,9 @@ public function test_heic_image_upload_is_converted_to_jpeg( bool $apply_big_ima */ public function test_jpeg_image_converts_to_webp_when_filtered( bool $apply_big_image_size_threshold ) { $temp_dir = get_temp_dir(); - $file = $temp_dir . '/33772.jpg'; + $file = $temp_dir . '/a2-small.jpg'; $scaled_suffix = $apply_big_image_size_threshold ? '-scaled' : ''; - copy( DIR_TESTDATA . '/images/33772.jpg', $file ); + copy( DIR_TESTDATA . '/images/a2-small.jpg', $file ); $editor = wp_get_image_editor( $file ); @@ -7113,7 +7119,12 @@ public function test_jpeg_image_converts_to_webp_when_filtered( bool $apply_big_ ); if ( $apply_big_image_size_threshold ) { - add_filter( 'big_image_size_threshold', array( $this, 'add_big_image_size_threshold' ) ); + add_filter( + 'big_image_size_threshold', + static function () { + return 300; + } + ); } // Generate all sizes as WebP. @@ -7122,8 +7133,8 @@ public function test_jpeg_image_converts_to_webp_when_filtered( bool $apply_big_ $image_meta = wp_generate_attachment_metadata( $attachment_id, $file ); $this->assertStringEndsNotWith( '.jpg', $image_meta['file'], 'The file extension is expected to change.' ); - $this->assertSame( "33772{$scaled_suffix}.webp", basename( $image_meta['file'] ), "The file name is expected to be 33772{$scaled_suffix}.webp." ); - $this->assertSame( '33772.jpg', $image_meta['original_image'], 'The original image name is expected to be stored in the meta data.' ); + $this->assertSame( "a2-small{$scaled_suffix}.webp", basename( $image_meta['file'] ), "The file name is expected to be a2-small{$scaled_suffix}.webp." ); + $this->assertSame( 'a2-small.jpg', $image_meta['original_image'], 'The original image name is expected to be stored in the meta data.' ); $this->assertSame( 'image/webp', wp_get_image_mime( $image_meta['file'] ), 'The image mime type is expected to be image/webp.' ); } diff --git a/tests/phpunit/tests/media/wpCrossOriginIsolation.php b/tests/phpunit/tests/media/wpCrossOriginIsolation.php index 3ec4231d5bede..b1f6125943b01 100644 --- a/tests/phpunit/tests/media/wpCrossOriginIsolation.php +++ b/tests/phpunit/tests/media/wpCrossOriginIsolation.php @@ -30,12 +30,18 @@ class Tests_Media_wpCrossOriginIsolation extends WP_UnitTestCase { */ private ?string $original_get_action; + /** + * Original output buffer level. + */ + private int $original_ob_level; + public function set_up() { parent::set_up(); $this->original_user_agent = $_SERVER['HTTP_USER_AGENT'] ?? null; $this->original_http_host = $_SERVER['HTTP_HOST'] ?? null; $this->original_https = $_SERVER['HTTPS'] ?? null; $this->original_get_action = $_GET['action'] ?? null; + $this->original_ob_level = ob_get_level(); } public function tear_down() { @@ -64,7 +70,7 @@ public function tear_down() { } // Clean up any output buffers started during tests. - while ( ob_get_level() > 1 ) { + while ( ob_get_level() > $this->original_ob_level ) { ob_end_clean(); } @@ -99,14 +105,7 @@ public function test_returns_early_when_no_screen() { } /** - * This test must run in a separate process because the output buffer - * callback sends HTTP headers via header(), which would fail in the - * main PHPUnit process where output has already started. - * * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_starts_output_buffer_for_chrome_137() { $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; @@ -120,6 +119,29 @@ public function test_starts_output_buffer_for_chrome_137() { ob_end_clean(); } + /** + * @ticket 64766 + * + * @requires function xdebug_get_headers + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_output_buffer_sends_document_isolation_policy_header() { + $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; + + ob_start(); + + wp_start_cross_origin_isolation_output_buffer(); + echo ''; + + ob_end_flush(); + ob_get_clean(); + + $headers = xdebug_get_headers(); + + $this->assertContains( 'Document-Isolation-Policy: isolate-and-credentialless', $headers ); + } + /** * @ticket 64766 */ @@ -189,10 +211,6 @@ public function test_client_side_processing_enabled_on_localhost() { * Verifies that cross-origin elements get crossorigin="anonymous" added. * * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * * @dataProvider data_elements_that_should_get_crossorigin * * @param string $html HTML input to process. @@ -244,10 +262,6 @@ public function data_elements_that_should_get_crossorigin() { * in credentialless mode without needing explicit CORS headers. * * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * * @dataProvider data_elements_that_should_not_get_crossorigin * * @param string $html HTML input to process. @@ -294,9 +308,6 @@ public function data_elements_that_should_not_get_crossorigin() { * Uses site_url() at runtime since the test domain varies by CI config. * * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_output_buffer_does_not_add_crossorigin_to_same_origin() { $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; @@ -316,9 +327,6 @@ public function test_output_buffer_does_not_add_crossorigin_to_same_origin() { * Elements that already have a crossorigin attribute should not be modified. * * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_output_buffer_does_not_override_existing_crossorigin() { $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; @@ -339,9 +347,6 @@ public function test_output_buffer_does_not_override_existing_crossorigin() { * Multiple tags in the same output should each be handled correctly. * * @ticket 64766 - * - * @runInSeparateProcess - * @preserveGlobalState disabled */ public function test_output_buffer_handles_mixed_tags() { $_SERVER['HTTP_USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'; diff --git a/tests/phpunit/tests/media/wpGenerateAttachmentMetadata.php b/tests/phpunit/tests/media/wpGenerateAttachmentMetadata.php index 625d6ff665d83..0e41aa3a2f7a4 100644 --- a/tests/phpunit/tests/media/wpGenerateAttachmentMetadata.php +++ b/tests/phpunit/tests/media/wpGenerateAttachmentMetadata.php @@ -93,8 +93,14 @@ static function ( $mimes ) { * @ticket 62900 */ public function test_wp_generate_attachment_metadata_png_thumbnail_smaller_than_original() { - // Use the test-image-large.png test file. - $attachment = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/png-tests/test-image-large.png' ); + add_filter( + 'big_image_size_threshold', + static function () { + return 25; + } + ); + + $attachment = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/test-image.png' ); $metadata = wp_get_attachment_metadata( $attachment ); From dbdc75f3a7019b36444d74b5a4e31b99a537b44f Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:40:15 +0200 Subject: [PATCH 08/10] Tests: Add reduced REST route setup helpers Let REST controller test classes opt into rest_api_init without create_initial_rest_routes and register only the post type, taxonomy, or controller routes they need. --- tests/phpunit/includes/testcase-rest-api.php | 68 +++++++++++++++++++ .../includes/testcase-rest-controller.php | 32 +++++++-- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/includes/testcase-rest-api.php b/tests/phpunit/includes/testcase-rest-api.php index 27349809306f6..45340af0a303a 100644 --- a/tests/phpunit/includes/testcase-rest-api.php +++ b/tests/phpunit/includes/testcase-rest-api.php @@ -28,4 +28,72 @@ protected function assertErrorResponse( $code, $response, $status = null, $messa $this->assertSame( $status, $data['status'], $message . ' The expected status code does not match.' ); } } + + protected function do_rest_api_init_without_initial_routes() { + $priority = has_action( 'rest_api_init', 'create_initial_rest_routes' ); + + if ( false !== $priority ) { + remove_action( 'rest_api_init', 'create_initial_rest_routes', $priority ); + } + + try { + do_action( 'rest_api_init', $GLOBALS['wp_rest_server'] ); + } finally { + if ( false !== $priority ) { + add_action( 'rest_api_init', 'create_initial_rest_routes', $priority ); + } + } + } + + protected function register_post_type_rest_routes_for_test( $post_type_names ) { + foreach ( $post_type_names as $post_type_name ) { + $post_type = get_post_type_object( $post_type_name ); + + if ( ! $post_type ) { + continue; + } + + $controller = $post_type->get_rest_controller(); + + if ( ! $controller ) { + continue; + } + + if ( ! $post_type->late_route_registration ) { + $controller->register_routes(); + } + + $revisions_controller = $post_type->get_revisions_rest_controller(); + if ( $revisions_controller ) { + $revisions_controller->register_routes(); + } + + $autosaves_controller = $post_type->get_autosave_rest_controller(); + if ( $autosaves_controller ) { + $autosaves_controller->register_routes(); + } + + if ( $post_type->late_route_registration ) { + $controller->register_routes(); + } + } + } + + protected function register_taxonomy_rest_routes_for_test( $taxonomy_names ) { + foreach ( $taxonomy_names as $taxonomy_name ) { + $taxonomy = get_taxonomy( $taxonomy_name ); + + if ( ! $taxonomy ) { + continue; + } + + $controller = $taxonomy->get_rest_controller(); + + if ( ! $controller ) { + continue; + } + + $controller->register_routes(); + } + } } diff --git a/tests/phpunit/includes/testcase-rest-controller.php b/tests/phpunit/includes/testcase-rest-controller.php index 9a4c2a0b61b2c..f71676d458858 100644 --- a/tests/phpunit/includes/testcase-rest-controller.php +++ b/tests/phpunit/includes/testcase-rest-controller.php @@ -6,21 +6,43 @@ abstract class WP_Test_REST_Controller_Testcase extends WP_Test_REST_TestCase { public function set_up() { parent::set_up(); + + if ( ! $this->should_register_rest_routes() ) { + return; + } + add_filter( 'rest_url', array( $this, 'filter_rest_url_for_leading_slash' ), 10, 2 ); /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $wp_rest_server = new Spy_REST_Server(); - do_action( 'rest_api_init', $wp_rest_server ); + if ( $this->should_create_initial_rest_routes() ) { + do_action( 'rest_api_init', $wp_rest_server ); + } else { + $this->do_rest_api_init_without_initial_routes(); + $this->register_initial_rest_routes_for_test(); + } } public function tear_down() { - remove_filter( 'rest_url', array( $this, 'test_rest_url_for_leading_slash' ), 10, 2 ); - /** @var WP_REST_Server $wp_rest_server */ - global $wp_rest_server; - $wp_rest_server = null; + if ( $this->should_register_rest_routes() ) { + remove_filter( 'rest_url', array( $this, 'filter_rest_url_for_leading_slash' ), 10, 2 ); + /** @var WP_REST_Server $wp_rest_server */ + global $wp_rest_server; + $wp_rest_server = null; + } parent::tear_down(); } + protected function should_register_rest_routes() { + return true; + } + + protected function should_create_initial_rest_routes() { + return true; + } + + protected function register_initial_rest_routes_for_test() {} + abstract public function test_register_routes(); abstract public function test_context_param(); From bbd6926c132d12cbde28617e2efa66fef04caeac Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 09:40:40 +0200 Subject: [PATCH 09/10] Tests: Reduce REST route setup in controller suites Opt REST controller-focused tests into minimal route registration while preserving related routes for dispatch, links, embedding, revisions, autosaves, widgets, menus, templates, fonts, and application-password behavior. --- .../wpRestFontCollectionsController.php | 9 +++++ .../wpRestFontFacesController.php | 8 +++++ .../wpRestFontFamiliesController.php | 8 +++++ tests/phpunit/tests/rest-api.php | 36 ++++++++++++++++++- .../rest-application-passwords-controller.php | 12 +++++++ .../rest-api/rest-attachments-controller.php | 11 ++++++ .../rest-api/rest-autosaves-controller.php | 8 +++++ .../rest-block-directory-controller.php | 9 +++++ .../rest-block-renderer-controller.php | 9 +++++ .../rest-api/rest-block-type-controller.php | 9 +++++ .../rest-api/rest-categories-controller.php | 8 +++++ .../rest-api/rest-comments-controller.php | 14 ++++++++ .../rest-global-styles-controller.php | 9 +++++ ...est-global-styles-revisions-controller.php | 8 +++++ .../rest-navigation-fallback-controller.php | 11 ++++++ .../tests/rest-api/rest-pages-controller.php | 25 +++++++++++++ .../rest-pattern-directory-controller.php | 9 +++++ .../rest-api/rest-plugins-controller.php | 9 +++++ .../tests/rest-api/rest-post-meta-fields.php | 7 +++- .../rest-post-statuses-controller.php | 9 +++++ .../rest-api/rest-post-types-controller.php | 9 +++++ .../tests/rest-api/rest-posts-controller.php | 25 +++++++++++++ .../rest-api/rest-revisions-controller.php | 8 +++++ .../tests/rest-api/rest-search-controller.php | 20 +++++++++++ .../rest-api/rest-settings-controller.php | 9 +++++ .../rest-api/rest-sidebars-controller.php | 12 +++++++ .../tests/rest-api/rest-tags-controller.php | 8 +++++ .../rest-api/rest-taxonomies-controller.php | 9 +++++ .../tests/rest-api/rest-term-meta-fields.php | 6 ++-- .../tests/rest-api/rest-themes-controller.php | 9 +++++ .../tests/rest-api/rest-users-controller.php | 9 +++++ .../rest-api/rest-widget-types-controller.php | 9 +++++ .../rest-api/rest-widgets-controller.php | 15 ++++++++ .../rest-api/wpRestMenuItemsController.php | 9 +++++ .../wpRestMenuLocationsController.php | 9 +++++ .../tests/rest-api/wpRestMenusController.php | 11 ++++++ .../wpRestTemplateAutosavesController.php | 8 +++++ .../wpRestTemplateRevisionsController.php | 8 +++++ .../rest-api/wpRestTemplatesController.php | 12 +++++++ .../rest-api/wpRestUrlDetailsController.php | 24 ++++++++++++- 40 files changed, 452 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php index 5bad5435472d4..e350e8209445b 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -53,6 +53,15 @@ public static function wpTearDownAfterClass() { wp_unregister_font_collection( 'mock-col-slug' ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Font_Collections_Controller(); + $controller->register_routes(); + } + /** * @covers WP_REST_Font_Collections_Controller::register_routes */ diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php index 19a2a6a86b8b0..9811f86251e47 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -86,6 +86,14 @@ public function tear_down() { parent::tear_down(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'wp_font_family', 'wp_font_face' ) ); + } + public static function create_font_face_post( $parent_id, $settings = array() ) { $settings = array_merge( self::$default_settings, $settings ); $title = WP_Font_Utils::get_font_face_slug( $settings ); diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php index fada1d1643890..da0457ac03874 100644 --- a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -99,6 +99,14 @@ public function tear_down() { parent::tear_down(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'wp_font_family', 'wp_font_face' ) ); + } + public static function create_font_family_post( $settings = array() ) { $settings = array_merge( self::$default_settings, $settings ); $post_id = self::factory()->post->create( diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index fcb8e3da87f4c..b409e8bbdbee2 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -19,7 +19,12 @@ public function set_up() { // Override the normal server with our spying server. $GLOBALS['wp_rest_server'] = new Spy_REST_Server(); - do_action( 'rest_api_init', $GLOBALS['wp_rest_server'] ); + + if ( $this->requires_initial_rest_routes() ) { + do_action( 'rest_api_init', $GLOBALS['wp_rest_server'] ); + } else { + $this->do_rest_api_init_without_initial_routes(); + } } public function tear_down() { @@ -31,6 +36,35 @@ public function filter_wp_rest_server_class( $class_name ) { return 'Spy_REST_Server'; } + private function do_rest_api_init_without_initial_routes() { + $priority = has_action( 'rest_api_init', 'create_initial_rest_routes' ); + + if ( false !== $priority ) { + remove_action( 'rest_api_init', 'create_initial_rest_routes', $priority ); + } + + try { + do_action( 'rest_api_init', $GLOBALS['wp_rest_server'] ); + } finally { + if ( false !== $priority ) { + add_action( 'rest_api_init', 'create_initial_rest_routes', $priority ); + } + } + } + + private function requires_initial_rest_routes() { + return in_array( + $this->getName( false ), + array( + 'test_rest_preload_api_request_with_method', + 'test_rest_preload_api_request_removes_trailing_slashes', + 'test_rest_preload_api_request_embeds_links', + 'test_rest_preload_api_request_fields', + ), + true + ); + } + public function test_rest_get_server_fails_with_undefined_method() { $this->expectException( Error::class ); rest_get_server()->does_not_exist(); diff --git a/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php b/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php index 060a5c0912a94..8830e6bd8d590 100644 --- a/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php +++ b/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php @@ -67,6 +67,18 @@ public function set_up() { add_filter( 'wp_is_application_passwords_available', '__return_true' ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Users_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Application_Passwords_Controller(); + $controller->register_routes(); + } + public function tear_down() { unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'], $GLOBALS['wp_rest_application_password_status'], $GLOBALS['wp_rest_application_password_uuid'] ); parent::tear_down(); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 79e9d23cf9dd3..26662a430246c 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -194,6 +194,17 @@ public function tear_down() { parent::tear_down(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'post', 'page', 'attachment' ) ); + + $controller = new WP_REST_Users_Controller(); + $controller->register_routes(); + } + /** * Enables client-side media processing and reinitializes the REST server * so that the sideload and finalize routes are registered. diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index 7815f8ced23c9..eafa172e757d5 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -126,6 +126,14 @@ public function set_up() { $this->post_autosave = wp_get_post_autosave( self::$post_id ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'post', 'page' ) ); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/posts/(?P[\d]+)/autosaves', $routes ); diff --git a/tests/phpunit/tests/rest-api/rest-block-directory-controller.php b/tests/phpunit/tests/rest-api/rest-block-directory-controller.php index f61b317240532..f06b6ebefcc2e 100644 --- a/tests/phpunit/tests/rest-api/rest-block-directory-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-directory-controller.php @@ -41,6 +41,15 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Block_Directory_Controller(); + $controller->register_routes(); + } + /** * @ticket 50321 */ diff --git a/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php b/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php index 3573ecb6e0bea..e2c50d6bbef9a 100644 --- a/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php @@ -148,6 +148,15 @@ public function tear_down() { parent::tear_down(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Block_Renderer_Controller(); + $controller->register_routes(); + } + /** * Register test block. * diff --git a/tests/phpunit/tests/rest-api/rest-block-type-controller.php b/tests/phpunit/tests/rest-api/rest-block-type-controller.php index 7ba693286c993..2c442e1ea2281 100644 --- a/tests/phpunit/tests/rest-api/rest-block-type-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-type-controller.php @@ -66,6 +66,15 @@ public static function wpTearDownAfterClass() { unregister_block_type( 'fake/false' ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Block_Types_Controller(); + $controller->register_routes(); + } + /** * @ticket 47620 */ diff --git a/tests/phpunit/tests/rest-api/rest-categories-controller.php b/tests/phpunit/tests/rest-api/rest-categories-controller.php index 5d1f893d87fd4..f94c28babb79b 100644 --- a/tests/phpunit/tests/rest-api/rest-categories-controller.php +++ b/tests/phpunit/tests/rest-api/rest-categories-controller.php @@ -104,6 +104,14 @@ public function set_up() { ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_taxonomy_rest_routes_for_test( array( 'category' ) ); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/categories', $routes ); diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index 547757ae6042e..21f127b09aa2b 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -177,6 +177,20 @@ public function set_up() { } } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'post', 'page', 'attachment' ) ); + + $controller = new WP_REST_Users_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Comments_Controller(); + $controller->register_routes(); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php index 08ed7f65f818b..c803645b0a43b 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -52,6 +52,15 @@ public function tear_down() { parent::tear_down(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Global_Styles_Controller(); + $controller->register_routes(); + } + /** * Create fake data before our tests run. * diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php index a715899979ad1..bc397f8abac22 100644 --- a/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-global-styles-revisions-controller.php @@ -224,6 +224,14 @@ public function set_up() { $this->revision_3_id = $this->revision_3->ID; } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'wp_global_styles' ) ); + } + /** * @ticket 58524 * @ticket 59810 diff --git a/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php b/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php index 3be0bba59f26f..a26dac32bb8ad 100644 --- a/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php +++ b/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php @@ -35,6 +35,17 @@ public function set_up() { wp_set_current_user( self::$admin_user ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Navigation_Fallback_Controller(); + $controller->register_routes(); + + $this->register_post_type_rest_routes_for_test( array( 'wp_navigation' ) ); + } + /** * @ticket 58557 * @covers WP_REST_Navigation_Fallback_Controller::register_routes diff --git a/tests/phpunit/tests/rest-api/rest-pages-controller.php b/tests/phpunit/tests/rest-api/rest-pages-controller.php index 9717a7fcda1c6..625ea43997729 100644 --- a/tests/phpunit/tests/rest-api/rest-pages-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pages-controller.php @@ -33,6 +33,31 @@ public function set_up() { $GLOBALS['wp_rest_server']->override_by_default = false; } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'post', 'page', 'attachment' ) ); + + $controller = new WP_REST_Post_Types_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Post_Statuses_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Taxonomies_Controller(); + $controller->register_routes(); + + $this->register_taxonomy_rest_routes_for_test( array( 'category', 'post_tag' ) ); + + $controller = new WP_REST_Users_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Comments_Controller(); + $controller->register_routes(); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/pages', $routes ); diff --git a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php index 6f84306dad61f..460bb9d49e2d9 100644 --- a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php @@ -65,6 +65,15 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$contributor_id ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Pattern_Directory_Controller(); + $controller->register_routes(); + } + /** * Clear the captured request URLs after each test. * diff --git a/tests/phpunit/tests/rest-api/rest-plugins-controller.php b/tests/phpunit/tests/rest-api/rest-plugins-controller.php index d6290b071bf22..ebdc651627690 100644 --- a/tests/phpunit/tests/rest-api/rest-plugins-controller.php +++ b/tests/phpunit/tests/rest-api/rest-plugins-controller.php @@ -111,6 +111,15 @@ public function tear_down() { parent::tear_down(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Plugins_Controller(); + $controller->register_routes(); + } + /** * @ticket 50321 */ diff --git a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php index 5ce72a57fa55f..858df252aa105 100644 --- a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php @@ -258,7 +258,12 @@ public function set_up() { /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $wp_rest_server = new Spy_REST_Server(); - do_action( 'rest_api_init', $wp_rest_server ); + $this->do_rest_api_init_without_initial_routes(); + $this->register_initial_rest_routes_for_test(); + } + + private function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'post', 'page', 'cpt' ) ); } protected function grant_write_permission() { diff --git a/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php b/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php index f6bb1d795a114..e6db20a139e0a 100644 --- a/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php @@ -9,6 +9,15 @@ */ class WP_Test_REST_Post_Statuses_Controller extends WP_Test_REST_Controller_Testcase { + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Post_Statuses_Controller(); + $controller->register_routes(); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/statuses', $routes ); diff --git a/tests/phpunit/tests/rest-api/rest-post-types-controller.php b/tests/phpunit/tests/rest-api/rest-post-types-controller.php index f230373ce27a4..4f3f80737f725 100644 --- a/tests/phpunit/tests/rest-api/rest-post-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-types-controller.php @@ -9,6 +9,15 @@ */ class WP_Test_REST_Post_Types_Controller extends WP_Test_REST_Controller_Testcase { + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Post_Types_Controller(); + $controller->register_routes(); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/types', $routes ); diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 212ddde70dd83..0d0b85c8c6f5f 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -139,6 +139,31 @@ public function save_posts_clauses( $orderby, $query ) { return $orderby; } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'post', 'page', 'attachment' ) ); + + $controller = new WP_REST_Post_Types_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Post_Statuses_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Taxonomies_Controller(); + $controller->register_routes(); + + $this->register_taxonomy_rest_routes_for_test( array( 'category', 'post_tag' ) ); + + $controller = new WP_REST_Users_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Comments_Controller(); + $controller->register_routes(); + } + public function assertPostsClause( $clause, $pattern ) { global $wpdb; $expected_clause = str_replace( '{posts}', $wpdb->posts, $pattern ); diff --git a/tests/phpunit/tests/rest-api/rest-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-revisions-controller.php index 52011afcb9318..904504a483ed1 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -105,6 +105,14 @@ public function set_up() { $this->revision_2_1_id = $post_2_revision->ID; } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( 'post', 'page' ) ); + } + public function _filter_map_meta_cap_remove_no_allow_revisions( $caps, $cap, $user_id, $args ) { if ( 'delete_post' !== $cap || empty( $args ) ) { return $caps; diff --git a/tests/phpunit/tests/rest-api/rest-search-controller.php b/tests/phpunit/tests/rest-api/rest-search-controller.php index e4235fd699798..09cfa2e3cc544 100644 --- a/tests/phpunit/tests/rest-api/rest-search-controller.php +++ b/tests/phpunit/tests/rest-api/rest-search-controller.php @@ -44,6 +44,26 @@ class WP_Test_REST_Search_Controller extends WP_Test_REST_Controller_Testcase { */ private static $my_tag_id; + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $search_handlers = array( + new WP_REST_Post_Search_Handler(), + new WP_REST_Term_Search_Handler(), + new WP_REST_Post_Format_Search_Handler(), + ); + + $search_handlers = apply_filters( 'wp_rest_search_handlers', $search_handlers ); + + $controller = new WP_REST_Search_Controller( $search_handlers ); + $controller->register_routes(); + + $this->register_post_type_rest_routes_for_test( array( 'post', 'page' ) ); + $this->register_taxonomy_rest_routes_for_test( array( 'category', 'post_tag' ) ); + } + /** * Create fake data before our tests run. * diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index e8f90b53f20f1..05487f3a4e6c7 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -41,6 +41,15 @@ public function set_up() { $this->endpoint = new WP_REST_Settings_Controller(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Settings_Controller(); + $controller->register_routes(); + } + public function tear_down() { $settings_to_unregister = array( 'mycustomsetting', diff --git a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php index dd01d4f2de4ee..f9cb2e903beaa 100644 --- a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php +++ b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php @@ -59,6 +59,18 @@ public function set_up() { update_option( 'sidebars_widgets', array() ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Sidebars_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Widgets_Controller(); + $controller->register_routes(); + } + public function clean_up_global_scope() { global $wp_widget_factory, $wp_registered_sidebars, $wp_registered_widgets, $wp_registered_widget_controls, $wp_registered_widget_updates; diff --git a/tests/phpunit/tests/rest-api/rest-tags-controller.php b/tests/phpunit/tests/rest-api/rest-tags-controller.php index 3b23135c93706..52cf95426fe0c 100644 --- a/tests/phpunit/tests/rest-api/rest-tags-controller.php +++ b/tests/phpunit/tests/rest-api/rest-tags-controller.php @@ -122,6 +122,14 @@ public function set_up() { ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_taxonomy_rest_routes_for_test( array( 'post_tag' ) ); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); $this->assertArrayHasKey( '/wp/v2/tags', $routes ); diff --git a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php index 4d8a57d5602b8..d0712e1a75bc3 100644 --- a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php +++ b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php @@ -23,6 +23,15 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$contributor_id ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Taxonomies_Controller(); + $controller->register_routes(); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); diff --git a/tests/phpunit/tests/rest-api/rest-term-meta-fields.php b/tests/phpunit/tests/rest-api/rest-term-meta-fields.php index 737fa90d84633..65d5a4cc13c90 100644 --- a/tests/phpunit/tests/rest-api/rest-term-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-term-meta-fields.php @@ -193,7 +193,8 @@ public function set_up() { /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $wp_rest_server = new Spy_REST_Server(); - do_action( 'rest_api_init', $wp_rest_server ); + $this->do_rest_api_init_without_initial_routes(); + $this->register_taxonomy_rest_routes_for_test( array( 'category', 'post_tag', 'customtax' ) ); } protected function grant_write_permission() { @@ -326,7 +327,8 @@ public function test_get_value_types() { /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $wp_rest_server = new Spy_REST_Server(); - do_action( 'rest_api_init', $wp_rest_server ); + $this->do_rest_api_init_without_initial_routes(); + $this->register_taxonomy_rest_routes_for_test( array( 'category', 'post_tag', 'customtax' ) ); add_term_meta( self::$category_id, 'test_string', 42 ); add_term_meta( self::$category_id, 'test_number', '42' ); diff --git a/tests/phpunit/tests/rest-api/rest-themes-controller.php b/tests/phpunit/tests/rest-api/rest-themes-controller.php index aeaf92a8a27a1..02306d2cc2389 100644 --- a/tests/phpunit/tests/rest-api/rest-themes-controller.php +++ b/tests/phpunit/tests/rest-api/rest-themes-controller.php @@ -144,6 +144,15 @@ public function set_up() { switch_theme( 'rest-api' ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Themes_Controller(); + $controller->register_routes(); + } + /** * Theme routes should be registered correctly. * diff --git a/tests/phpunit/tests/rest-api/rest-users-controller.php b/tests/phpunit/tests/rest-api/rest-users-controller.php index b78e95b95f48d..499f4f40f4058 100644 --- a/tests/phpunit/tests/rest-api/rest-users-controller.php +++ b/tests/phpunit/tests/rest-api/rest-users-controller.php @@ -165,6 +165,15 @@ public function set_up() { $this->endpoint = new WP_REST_Users_Controller(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Users_Controller(); + $controller->register_routes(); + } + public function test_register_routes() { $routes = rest_get_server()->get_routes(); diff --git a/tests/phpunit/tests/rest-api/rest-widget-types-controller.php b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php index 3003c2e9741de..7d9cc394270a5 100644 --- a/tests/phpunit/tests/rest-api/rest-widget-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php @@ -57,6 +57,15 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$subscriber_id ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Widget_Types_Controller(); + $controller->register_routes(); + } + private function setup_widget( $id_base, $number, $settings ) { global $wp_widget_factory; diff --git a/tests/phpunit/tests/rest-api/rest-widgets-controller.php b/tests/phpunit/tests/rest-api/rest-widgets-controller.php index 27a58eb638f9c..afbd4a2089039 100644 --- a/tests/phpunit/tests/rest-api/rest-widgets-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widgets-controller.php @@ -139,6 +139,21 @@ static function () { ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Widget_Types_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Sidebars_Controller(); + $controller->register_routes(); + + $controller = new WP_REST_Widgets_Controller(); + $controller->register_routes(); + } + public function clean_up_global_scope() { global $wp_widget_factory, diff --git a/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php index a5b76af3438d2..e1c8ca17932d0 100644 --- a/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php +++ b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php @@ -65,6 +65,15 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$subscriber_id ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( self::POST_TYPE ) ); + $this->register_taxonomy_rest_routes_for_test( array( 'nav_menu' ) ); + } + /** * */ diff --git a/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php b/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php index 1a0ffc4bb536b..c5ccc6459df62 100644 --- a/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php +++ b/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php @@ -42,6 +42,15 @@ public function set_up() { } } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Menu_Locations_Controller(); + $controller->register_routes(); + } + /** * Register nav menu locations. * diff --git a/tests/phpunit/tests/rest-api/wpRestMenusController.php b/tests/phpunit/tests/rest-api/wpRestMenusController.php index 864b09417d2cb..e823fd757154a 100644 --- a/tests/phpunit/tests/rest-api/wpRestMenusController.php +++ b/tests/phpunit/tests/rest-api/wpRestMenusController.php @@ -64,6 +64,17 @@ public static function wpSetUpBeforeClass( $factory ) { ); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_taxonomy_rest_routes_for_test( array( self::TAXONOMY ) ); + + $controller = new WP_REST_Menu_Locations_Controller(); + $controller->register_routes(); + } + /** * */ diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php index d3cbf91260488..ab4877f6e2639 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php @@ -39,6 +39,14 @@ class Tests_REST_wpRestTemplateAutosavesController extends WP_Test_REST_Controll */ const PARENT_POST_TYPE = 'wp_template'; + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( self::TEMPLATE_POST_TYPE, self::TEMPLATE_PART_POST_TYPE ) ); + } + /** * Admin user ID. * diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php index e8a18b275e7cd..b91e6fc0cc1a0 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php @@ -108,6 +108,14 @@ class Tests_REST_wpRestTemplateRevisionsController extends WP_Test_REST_Controll */ private static $template_part_revisions = array(); + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $this->register_post_type_rest_routes_for_test( array( self::TEMPLATE_POST_TYPE, self::TEMPLATE_PART_POST_TYPE ) ); + } + /** * Create fake data before our tests run. * diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php index 0bbd6b151c6c0..b315130f4b117 100644 --- a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -98,6 +98,18 @@ public function tear_down() { parent::tear_down(); } + protected function should_create_initial_rest_routes() { + return false; + } + + protected function register_initial_rest_routes_for_test() { + $controller = new WP_REST_Templates_Controller( 'wp_template' ); + $controller->register_routes(); + + $controller = new WP_REST_Templates_Controller( 'wp_template_part' ); + $controller->register_routes(); + } + /** * @covers WP_REST_Templates_Controller::register_routes * @ticket 54596 diff --git a/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php b/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php index 7187d1696c05a..6b04aca4b93c8 100644 --- a/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php +++ b/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php @@ -88,8 +88,11 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); - add_filter( 'pre_http_request', array( $this, 'mock_success_request_to_remote_url' ), 10, 3 ); + if ( ! $this->should_register_rest_routes() ) { + return; + } + add_filter( 'pre_http_request', array( $this, 'mock_success_request_to_remote_url' ), 10, 3 ); // Disables usage of cache during major of tests. add_filter( 'pre_site_transient_' . $this->get_transient_name(), '__return_null' ); } @@ -99,6 +102,25 @@ public function tear_down() { parent::tear_down(); } + protected function should_register_rest_routes() { + return ! in_array( + $this->getName( false ), + array( + 'test_get_title', + 'test_get_icon', + 'test_get_description', + 'test_get_image', + 'test_context_param', + 'test_get_item', + 'test_create_item', + 'test_update_item', + 'test_delete_item', + 'test_prepare_item', + ), + true + ); + } + /** * @covers WP_REST_URL_Details_Controller::register_routes * From 3fb0093f5d3222744ae524607f7a3ba99d8d2b01 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 24 Jun 2026 19:15:35 +0200 Subject: [PATCH 10/10] phpcs --- tests/phpunit/includes/abstract-testcase.php | 45 ++++++++++---------- tests/phpunit/tests/compat/mbOrd.php | 2 +- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php index 8b454ce6341fe..27f783b3fdf2f 100644 --- a/tests/phpunit/includes/abstract-testcase.php +++ b/tests/phpunit/includes/abstract-testcase.php @@ -24,11 +24,12 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase { protected static $hooks_saved = array(); protected static $ignore_files; - protected static $expected_annotations_cache = array(); - protected static $core_registration_snapshots = array(); - protected static $db_autocommit_is_disabled = false; + + protected static $expected_annotations_cache = array(); + protected static $core_registration_snapshots = array(); + protected static $db_autocommit_is_disabled = false; protected static $test_context_hooks_registered = false; - protected static $deprecation_tracking_test = null; + protected static $deprecation_tracking_test = null; /** * Fixture factory. @@ -607,24 +608,24 @@ protected function capture_core_registration_snapshot() { global $wp, $wp_rewrite, $wp_post_types, $wp_taxonomies, $wp_post_statuses, $_wp_post_type_features, $post_type_meta_caps; return array( - 'wp_post_types' => self::clone_core_registration_objects( $wp_post_types ), - 'wp_taxonomies' => self::clone_core_registration_objects( $wp_taxonomies ), - 'wp_post_statuses' => self::clone_core_registration_objects( $wp_post_statuses ), - 'post_type_features' => $_wp_post_type_features, - 'post_type_meta_caps' => $post_type_meta_caps, - 'public_query_vars' => $wp->public_query_vars, - 'private_query_vars' => $wp->private_query_vars, - 'extra_query_vars' => $wp->extra_query_vars, - 'rewritecode' => $wp_rewrite->rewritecode, - 'rewritereplace' => $wp_rewrite->rewritereplace, - 'queryreplace' => $wp_rewrite->queryreplace, - 'rules' => $wp_rewrite->rules, - 'matches' => $wp_rewrite->matches, - 'extra_rules' => $wp_rewrite->extra_rules, - 'extra_rules_top' => $wp_rewrite->extra_rules_top, - 'extra_permastructs' => $wp_rewrite->extra_permastructs, - 'non_wp_rules' => $wp_rewrite->non_wp_rules, - 'endpoints' => $wp_rewrite->endpoints, + 'wp_post_types' => self::clone_core_registration_objects( $wp_post_types ), + 'wp_taxonomies' => self::clone_core_registration_objects( $wp_taxonomies ), + 'wp_post_statuses' => self::clone_core_registration_objects( $wp_post_statuses ), + 'post_type_features' => $_wp_post_type_features, + 'post_type_meta_caps' => $post_type_meta_caps, + 'public_query_vars' => $wp->public_query_vars, + 'private_query_vars' => $wp->private_query_vars, + 'extra_query_vars' => $wp->extra_query_vars, + 'rewritecode' => $wp_rewrite->rewritecode, + 'rewritereplace' => $wp_rewrite->rewritereplace, + 'queryreplace' => $wp_rewrite->queryreplace, + 'rules' => $wp_rewrite->rules, + 'matches' => $wp_rewrite->matches, + 'extra_rules' => $wp_rewrite->extra_rules, + 'extra_rules_top' => $wp_rewrite->extra_rules_top, + 'extra_permastructs' => $wp_rewrite->extra_permastructs, + 'non_wp_rules' => $wp_rewrite->non_wp_rules, + 'endpoints' => $wp_rewrite->endpoints, 'use_verbose_page_rules' => $wp_rewrite->use_verbose_page_rules, ); } diff --git a/tests/phpunit/tests/compat/mbOrd.php b/tests/phpunit/tests/compat/mbOrd.php index b327ab02f5d0d..0f9be48d5a351 100644 --- a/tests/phpunit/tests/compat/mbOrd.php +++ b/tests/phpunit/tests/compat/mbOrd.php @@ -24,7 +24,7 @@ public function test_mb_ord_polyfill_matches_spec() { */ $char = mb_chr( $code_point ); - if ( false !== $char && $code_point !== _mb_ord( $char ) ) { + if ( false !== $char && _mb_ord( $char ) !== $code_point ) { $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); $this->fail( "Failed to properly decode U+{$hex_char} from the string." ); }