Make WordPress Core

Changeset 65234


Ignore:
Timestamp:
11/03/2026 04:42:10 PM (3 days ago)
Author:
lancewillett
Message:

Build/Test Tools: Skip the WordPress install in process-isolated tests.

A @runInSeparateProcess test loads the bootstrap again in a child process, which reran install.php and recreated every table the parent had already installed. Skip the install while PHPUnit sets $GLOBALS['__PHPUNIT_BOOTSTRAP'], which it does only in a child process. WP_TESTS_SKIP_INSTALL=1 works as before.

The reinstall also hid state that test classes leak. WP_UnitTestCase_Base now deletes all data before each class's fixtures and restores the options table and site_admins after each class. It also flushes the cache before the class cleanup and flushes a persistent object cache after each rollback.

Developed in: ​https://github.com/WordPress/wordpress-develop/pull/14786

Props lucatume, adrianmoldovanwp.
Fixes #66410.

Location:
trunk/tests/phpunit
Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/includes/abstract-testcase.php

    r65126 r65234  
    4848
    4949        /**
     50         * The options table rows before the first test class ran, keyed by option name.
     51         *
     52         * @var object[]|null
     53         */
     54        private static $original_options = null;
     55
     56        /**
     57         * The network's super admins before the first test class ran, or null if not multisite or unset.
     58         *
     59         * @var string[]|null
     60         */
     61        private static $original_site_admins = null;
     62
     63        /**
    5064         * The value of $GLOBALS['locale'] before each test, or null if it was unset.
    5165         *
    … …  
    110124                ini_set( 'display_errors', 1 );
    111125
     126                /*
     127                 * Options and super admins to restore after each class, see restore_options().
     128                 * An installed site always has options: no rows means the query failed, and
     129                 * restoring to an empty snapshot would delete them all.
     130                 */
     131                if ( null === self::$original_options ) {
     132                        $rows = self::get_option_rows();
     133
     134                        if ( $rows ) {
     135                                self::$original_options     = $rows;
     136                                self::$original_site_admins = is_multisite() ? get_site_option( 'site_admins', null ) : null;
     137                        }
     138                }
     139
     140                /*
     141                 * Start from the data a fresh install has. A process-isolated test runs this
     142                 * again in its own process, on top of the class fixtures the parent process
     143                 * already committed, and would collide with them, e.g. a second `User 1`.
     144                 */
     145                _delete_all_data();
     146                self::flush_cache();
     147
    112148                $class                             = get_called_class();
    113149                self::$meta_before_class[ $class ] = self::backup_meta_state();
    … …  
    135171                }
    136172
     173                /*
     174                 * The cache still holds values the tests' rollbacks undid, and the cleanup reads
     175                 * options, e.g. update_site_option() would not restore `site_admins` if a rolled
     176                 * back test left the original value in the cache.
     177                 */
     178                self::flush_cache();
     179
    137180                _delete_all_data();
     181                self::restore_options();
    138182                self::flush_cache();
    139183
    … …  
    141185
    142186                parent::tear_down_after_class();
     187        }
     188
     189        /**
     190         * Fetches the options table rows, keyed by option name.
     191         *
     192         * @return object[] The rows.
     193         */
     194        private static function get_option_rows() {
     195                global $wpdb;
     196
     197                return $wpdb->get_results( "SELECT option_name, option_value, autoload FROM {$wpdb->options}", OBJECT_K );
     198        }
     199
     200        /**
     201         * Restores the options table rows, and the network's super admins, to the ones
     202         * before the first test class ran.
     203         *
     204         * A test class commits the options it changes while setting up or tearing down,
     205         * e.g. a deleted `blogdescription` or super admins replaced with a fixture user,
     206         * and they would leak into the next classes.
     207         */
     208        private static function restore_options() {
     209                global $wpdb;
     210
     211                // No snapshot if no class ran the parent set_up_before_class(), e.g. Text_Diff_Check_Test.
     212                if ( null === self::$original_options ) {
     213                        return;
     214                }
     215
     216                $current = self::get_option_rows();
     217
     218                foreach ( array_keys( array_diff_key( $current, self::$original_options ) ) as $name ) {
     219                        $wpdb->delete( $wpdb->options, array( 'option_name' => $name ) );
     220                }
     221
     222                foreach ( self::$original_options as $name => $row ) {
     223                        if ( isset( $current[ $name ] ) && (array) $current[ $name ] === (array) $row ) {
     224                                continue;
     225                        }
     226
     227                        $wpdb->replace( $wpdb->options, (array) $row );
     228                }
     229
     230                if ( null !== self::$original_site_admins ) {
     231                        update_site_option( 'site_admins', self::$original_site_admins );
     232                }
    143233        }
    144234
    … …  
    234324
    235325                $wpdb->query( 'ROLLBACK' );
     326
     327                /*
     328                 * A persistent object cache still holds the values the rollback just undid.
     329                 * Flush it now: a process-isolated test bootstraps WordPress against it
     330                 * before its own set_up() runs, and would act on the stale values,
     331                 * e.g. a rolled back `theme_switched` option rebuilding the sidebars.
     332                 */
     333                if ( wp_using_ext_object_cache() ) {
     334                        self::flush_cache();
     335                }
    236336
    237337                if ( is_multisite() ) {
  • trunk/tests/phpunit/includes/bootstrap.php

    r65190 r65234  
    288288}
    289289
    290 if ( '1' !== getenv( 'WP_TESTS_SKIP_INSTALL' ) ) {
     290/*
     291 * Install WordPress, unless asked not to.
     292 *
     293 * Not in a process-isolated test: PHPUnit sets $GLOBALS['__PHPUNIT_BOOTSTRAP']
     294 * while it loads this file in a child process, after the parent process has
     295 * already installed WordPress. Reinstalling in every child would slow down
     296 * each isolated test and drop the tables the parent process is using.
     297 */
     298if ( '1' !== getenv( 'WP_TESTS_SKIP_INSTALL' ) && ! isset( $GLOBALS['__PHPUNIT_BOOTSTRAP'] ) ) {
    291299        $core_tests = ( defined( 'WP_RUN_CORE_TESTS' ) && WP_RUN_CORE_TESTS ) ? 'run_core_tests' : 'no_core_tests';
    292300        $ms_tests   = $multisite ? 'run_ms_tests' : 'no_ms_tests';
Note: See TracChangeset for help on using the changeset viewer.