Make WordPress Core

source: trunk/tests/phpunit/includes/abstract-testcase.php

Last change on this file was 61038, checked in by davidbaumwald, 7 weeks ago

Users: Revert Lazy-load user meta.

With [60915] reverted, this changeset is also being reverted to resolve test failures due to common code.

Reverts [60989].

Follow-up to [61037].

Props jorbin, ellatrix, spacedmonkey.
See #63021, #58001.

  • Property svn:eol-style set to native
File size: 54.2 KB
Line 
1<?php
2
3require_once __DIR__ . '/build-visual-html-tree.php';
4require_once __DIR__ . '/factory.php';
5require_once __DIR__ . '/trac.php';
6
7/**
8 * Defines a basic fixture to run multiple tests.
9 *
10 * Resets the state of the WordPress installation before and after every test.
11 *
12 * Includes utility functions and assertions useful for testing WordPress.
13 *
14 * All WordPress unit tests should inherit from this class.
15 */
16abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase {
17        protected static $forced_tickets   = array();
18        protected $expected_deprecated     = array();
19        protected $caught_deprecated       = array();
20        protected $expected_doing_it_wrong = array();
21        protected $caught_doing_it_wrong   = array();
22
23        protected static $hooks_saved = array();
24        protected static $ignore_files;
25
26        /**
27         * Fixture factory.
28         *
29         * @deprecated 6.1.0 Use the WP_UnitTestCase_Base::factory() method instead.
30         *
31         * @var WP_UnitTest_Factory
32         */
33        protected $factory;
34
35        /**
36         * Fetches the factory object for generating WordPress fixtures.
37         *
38         * @return WP_UnitTest_Factory The fixture factory.
39         */
40        protected static function factory() {
41                static $factory = null;
42                if ( ! $factory ) {
43                        $factory = new WP_UnitTest_Factory();
44                }
45                return $factory;
46        }
47
48        /**
49         * Retrieves the name of the class the static method is called in.
50         *
51         * @deprecated 5.3.0 Use the PHP native get_called_class() function instead.
52         *
53         * @return string The class name.
54         */
55        public static function get_called_class() {
56                return get_called_class();
57        }
58
59        /**
60         * Runs the routine before setting up all tests.
61         */
62        public static function set_up_before_class() {
63                global $wpdb;
64
65                parent::set_up_before_class();
66
67                $wpdb->suppress_errors = false;
68                $wpdb->show_errors     = true;
69                $wpdb->db_connect();
70                ini_set( 'display_errors', 1 );
71
72                $class = get_called_class();
73
74                if ( method_exists( $class, 'wpSetUpBeforeClass' ) ) {
75                        call_user_func( array( $class, 'wpSetUpBeforeClass' ), static::factory() );
76                }
77
78                self::commit_transaction();
79        }
80
81        /**
82         * Runs the routine after all tests have been run.
83         */
84        public static function tear_down_after_class() {
85                $class = get_called_class();
86
87                if ( method_exists( $class, 'wpTearDownAfterClass' ) ) {
88                        call_user_func( array( $class, 'wpTearDownAfterClass' ) );
89                }
90
91                _delete_all_data();
92                self::flush_cache();
93
94                self::commit_transaction();
95
96                parent::tear_down_after_class();
97        }
98
99        /**
100         * Runs the routine before each test is executed.
101         */
102        public function set_up() {
103                set_time_limit( 0 );
104
105                $this->factory = static::factory();
106
107                if ( ! self::$ignore_files ) {
108                        self::$ignore_files = $this->scan_user_uploads();
109                }
110
111                if ( ! self::$hooks_saved ) {
112                        $this->_backup_hooks();
113                }
114
115                global $wp_rewrite;
116
117                $this->clean_up_global_scope();
118
119                /*
120                 * When running core tests, ensure that post types and taxonomies
121                 * are reset for each test. We skip this step for non-core tests,
122                 * given the large number of plugins that register post types and
123                 * taxonomies at 'init'.
124                 */
125                if ( defined( 'WP_RUN_CORE_TESTS' ) && WP_RUN_CORE_TESTS ) {
126                        $this->reset_post_types();
127                        $this->reset_taxonomies();
128                        $this->reset_post_statuses();
129                        $this->reset__SERVER();
130
131                        if ( $wp_rewrite->permalink_structure ) {
132                                $this->set_permalink_structure( '' );
133                        }
134                }
135
136                $this->start_transaction();
137                $this->expectDeprecated();
138                add_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) );
139                add_filter( 'wp_hash_password_options', array( $this, 'wp_hash_password_options' ), 1, 2 );
140        }
141
142        /**
143         * Sets the bcrypt cost option for password hashing during tests.
144         *
145         * @param array      $options   The options for password hashing.
146         * @param string|int $algorithm The algorithm to use for hashing. This is a string in PHP 7.4+ and an integer in PHP 7.3 and earlier.
147         */
148        public function wp_hash_password_options( array $options, $algorithm ): array {
149                if ( PASSWORD_BCRYPT === $algorithm ) {
150                        $options['cost'] = 5;
151                }
152
153                return $options;
154        }
155
156        /**
157         * After a test method runs, resets any state in WordPress the test method might have changed.
158         */
159        public function tear_down() {
160                global $wpdb, $wp_the_query, $wp_query, $wp;
161                $wpdb->query( 'ROLLBACK' );
162                if ( is_multisite() ) {
163                        while ( ms_is_switched() ) {
164                                restore_current_blog();
165                        }
166                }
167
168                // Reset query, main query, and WP globals similar to wp-settings.php.
169                $wp_the_query = new WP_Query();
170                $wp_query     = $wp_the_query;
171                $wp           = new WP();
172
173                // Reset globals related to the post loop and `setup_postdata()`.
174                $post_globals = array( 'post', 'id', 'authordata', 'currentday', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages' );
175                foreach ( $post_globals as $global ) {
176                        $GLOBALS[ $global ] = null;
177                }
178
179                /*
180                 * Reset globals related to current screen to provide a consistent global starting state
181                 * for tests that interact with admin screens. Replaces the need for individual tests
182                 * to invoke `set_current_screen( 'front' )` (or an alternative implementation) as a reset.
183                 *
184                 * The globals are from `WP_Screen::set_current_screen()`.
185                 *
186                 * Why not invoke `set_current_screen( 'front' )`?
187                 * Performance (faster test runs with less memory usage). How so? For each test,
188                 * it saves creating an instance of WP_Screen, making two method calls,
189                 * and firing of the `current_screen` action.
190                 */
191                $current_screen_globals = array( 'current_screen', 'taxnow', 'typenow' );
192                foreach ( $current_screen_globals as $global ) {
193                        $GLOBALS[ $global ] = null;
194                }
195
196                // Reset comment globals.
197                $comment_globals = array( 'comment_alt', 'comment_depth', 'comment_thread_alt' );
198                foreach ( $comment_globals as $global ) {
199                        $GLOBALS[ $global ] = null;
200                }
201
202                /*
203                 * Reset $wp_sitemap global so that sitemap-related dynamic $wp->public_query_vars
204                 * are added when the next test runs.
205                 */
206                $GLOBALS['wp_sitemaps'] = null;
207
208                // Reset template globals.
209                $GLOBALS['wp_stylesheet_path'] = null;
210                $GLOBALS['wp_template_path']   = null;
211
212                $this->unregister_all_meta_keys();
213                remove_theme_support( 'html5' );
214                remove_filter( 'query', array( $this, '_create_temporary_tables' ) );
215                remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
216                remove_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) );
217                $this->_restore_hooks();
218                wp_set_current_user( 0 );
219
220                $this->reset_lazyload_queue();
221
222                WP_Style_Engine_CSS_Rules_Store::remove_all_stores();
223        }
224
225        /**
226         * Cleans the global scope (e.g `$_GET` and `$_POST`).
227         */
228        public function clean_up_global_scope() {
229                $_GET     = array();
230                $_POST    = array();
231                $_REQUEST = array();
232                self::flush_cache();
233        }
234
235        /**
236         * Allows tests to be skipped on some automated runs.
237         *
238         * For test runs on GitHub Actions for something other than trunk,
239         * we want to skip tests that only need to run for trunk.
240         */
241        public function skipOnAutomatedBranches() {
242                // https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
243                $github_event_name = getenv( 'GITHUB_EVENT_NAME' );
244                $github_ref        = getenv( 'GITHUB_REF' );
245
246                if ( $github_event_name ) {
247                        // We're on GitHub Actions.
248                        $skipped = array( 'pull_request', 'pull_request_target' );
249
250                        if ( in_array( $github_event_name, $skipped, true ) || 'refs/heads/trunk' !== $github_ref ) {
251                                $this->markTestSkipped( 'For automated test runs, this test is only run on trunk' );
252                        }
253                }
254        }
255
256        /**
257         * Allows tests to be skipped when Multisite is not in use.
258         *
259         * Use in conjunction with the ms-required group.
260         */
261        public function skipWithoutMultisite() {
262                if ( ! is_multisite() ) {
263                        $this->markTestSkipped( 'Test only runs on Multisite' );
264                }
265        }
266
267        /**
268         * Allows tests to be skipped when Multisite is in use.
269         *
270         * Use in conjunction with the ms-excluded group.
271         */
272        public function skipWithMultisite() {
273                if ( is_multisite() ) {
274                        $this->markTestSkipped( 'Test does not run on Multisite' );
275                }
276        }
277
278        /**
279         * Allows tests to be skipped if the HTTP request times out.
280         *
281         * @param array|WP_Error $response HTTP response.
282         */
283        public function skipTestOnTimeout( $response ) {
284                if ( ! is_wp_error( $response ) ) {
285                        return;
286                }
287                if ( 'connect() timed out!' === $response->get_error_message() ) {
288                        $this->markTestSkipped( 'HTTP timeout' );
289                }
290
291                if ( false !== strpos( $response->get_error_message(), 'timed out after' ) ) {
292                        $this->markTestSkipped( 'HTTP timeout' );
293                }
294
295                if ( 0 === strpos( $response->get_error_message(), 'stream_socket_client(): unable to connect to tcp://s.w.org:80' ) ) {
296                        $this->markTestSkipped( 'HTTP timeout' );
297                }
298        }
299
300        /**
301         * Reset the lazy load meta queue.
302         */
303        protected function reset_lazyload_queue() {
304                $lazyloader = wp_metadata_lazyloader();
305                $lazyloader->reset_queue( 'term' );
306                $lazyloader->reset_queue( 'comment' );
307                $lazyloader->reset_queue( 'blog' );
308        }
309
310        /**
311         * Unregisters existing post types and register defaults.
312         *
313         * Run before each test in order to clean up the global scope, in case
314         * a test forgets to unregister a post type on its own, or fails before
315         * it has a chance to do so.
316         */
317        protected function reset_post_types() {
318                foreach ( get_post_types( array(), 'objects' ) as $pt ) {
319                        if ( empty( $pt->tests_no_auto_unregister ) ) {
320                                _unregister_post_type( $pt->name );
321                        }
322                }
323                create_initial_post_types();
324        }
325
326        /**
327         * Unregisters existing taxonomies and register defaults.
328         *
329         * Run before each test in order to clean up the global scope, in case
330         * a test forgets to unregister a taxonomy on its own, or fails before
331         * it has a chance to do so.
332         */
333        protected function reset_taxonomies() {
334                foreach ( get_taxonomies() as $tax ) {
335                        _unregister_taxonomy( $tax );
336                }
337                create_initial_taxonomies();
338        }
339
340        /**
341         * Unregisters non-built-in post statuses.
342         */
343        protected function reset_post_statuses() {
344                foreach ( get_post_stati( array( '_builtin' => false ) ) as $post_status ) {
345                        _unregister_post_status( $post_status );
346                }
347        }
348
349        /**
350         * Resets `$_SERVER` variables
351         */
352        protected function reset__SERVER() {
353                tests_reset__SERVER();
354        }
355
356        /**
357         * Saves the hook-related globals so they can be restored later.
358         *
359         * Stores $wp_filter, $wp_actions, $wp_filters, and $wp_current_filter
360         * on a class variable so they can be restored on tear_down() using _restore_hooks().
361         *
362         * @global array $wp_filter
363         * @global array $wp_actions
364         * @global array $wp_filters
365         * @global array $wp_current_filter
366         */
367        protected function _backup_hooks() {
368                self::$hooks_saved['wp_filter'] = array();
369
370                foreach ( $GLOBALS['wp_filter'] as $hook_name => $hook_object ) {
371                        self::$hooks_saved['wp_filter'][ $hook_name ] = clone $hook_object;
372                }
373
374                $globals = array( 'wp_actions', 'wp_filters', 'wp_current_filter' );
375
376                foreach ( $globals as $key ) {
377                        self::$hooks_saved[ $key ] = $GLOBALS[ $key ];
378                }
379        }
380
381        /**
382         * Restores the hook-related globals to their state at set_up()
383         * so that future tests aren't affected by hooks set during this last test.
384         *
385         * @global array $wp_filter
386         * @global array $wp_actions
387         * @global array $wp_filters
388         * @global array $wp_current_filter
389         */
390        protected function _restore_hooks() {
391                if ( isset( self::$hooks_saved['wp_filter'] ) ) {
392                        $GLOBALS['wp_filter'] = array();
393
394                        foreach ( self::$hooks_saved['wp_filter'] as $hook_name => $hook_object ) {
395                                $GLOBALS['wp_filter'][ $hook_name ] = clone $hook_object;
396                        }
397                }
398
399                $globals = array( 'wp_actions', 'wp_filters', 'wp_current_filter' );
400
401                foreach ( $globals as $key ) {
402                        if ( isset( self::$hooks_saved[ $key ] ) ) {
403                                $GLOBALS[ $key ] = self::$hooks_saved[ $key ];
404                        }
405                }
406        }
407
408        /**
409         * Flushes the WordPress object cache.
410         */
411        public static function flush_cache() {
412                global $wp_object_cache;
413
414                wp_cache_flush_runtime();
415
416                if ( is_object( $wp_object_cache ) && method_exists( $wp_object_cache, '__remoteset' ) ) {
417                        $wp_object_cache->__remoteset();
418                }
419
420                wp_cache_flush();
421
422                wp_cache_add_global_groups(
423                        array(
424                                'blog-details',
425                                'blog-id-cache',
426                                'blog-lookup',
427                                'blog_meta',
428                                'global-posts',
429                                'networks',
430                                'network-queries',
431                                'sites',
432                                'site-details',
433                                'site-options',
434                                'site-queries',
435                                'site-transient',
436                                'theme_files',
437                                'rss',
438                                'users',
439                                'user-queries',
440                                'user_meta',
441                                'useremail',
442                                'userlogins',
443                                'userslugs',
444                        )
445                );
446
447                wp_cache_add_non_persistent_groups( array( 'counts', 'plugins', 'theme_json' ) );
448        }
449
450        /**
451         * Cleans up any registered meta keys.
452         *
453         * @since 5.1.0
454         *
455         * @global array $wp_meta_keys
456         */
457        public function unregister_all_meta_keys() {
458                global $wp_meta_keys;
459                if ( ! is_array( $wp_meta_keys ) ) {
460                        return;
461                }
462                foreach ( $wp_meta_keys as $object_type => $type_keys ) {
463                        foreach ( $type_keys as $object_subtype => $subtype_keys ) {
464                                foreach ( $subtype_keys as $key => $value ) {
465                                        unregister_meta_key( $object_type, $key, $object_subtype );
466                                }
467                        }
468                }
469        }
470
471        /**
472         * Starts a database transaction.
473         */
474        public function start_transaction() {
475                global $wpdb;
476                $wpdb->query( 'SET autocommit = 0;' );
477                $wpdb->query( 'START TRANSACTION;' );
478                add_filter( 'query', array( $this, '_create_temporary_tables' ) );
479                add_filter( 'query', array( $this, '_drop_temporary_tables' ) );
480        }
481
482        /**
483         * Commits the queries in a transaction.
484         *
485         * @since 4.1.0
486         */
487        public static function commit_transaction() {
488                global $wpdb;
489                $wpdb->query( 'COMMIT;' );
490        }
491
492        /**
493         * Replaces the `CREATE TABLE` statement with a `CREATE TEMPORARY TABLE` statement.
494         *
495         * @param string $query The query to replace the statement for.
496         * @return string The altered query.
497         */
498        public function _create_temporary_tables( $query ) {
499                if ( 0 === strpos( trim( $query ), 'CREATE TABLE' ) ) {
500                        return substr_replace( trim( $query ), 'CREATE TEMPORARY TABLE', 0, 12 );
501                }
502                return $query;
503        }
504
505        /**
506         * Replaces the `DROP TABLE` statement with a `DROP TEMPORARY TABLE` statement.
507         *
508         * @param string $query The query to replace the statement for.
509         * @return string The altered query.
510         */
511        public function _drop_temporary_tables( $query ) {
512                if ( 0 === strpos( trim( $query ), 'DROP TABLE' ) ) {
513                        return substr_replace( trim( $query ), 'DROP TEMPORARY TABLE', 0, 10 );
514                }
515                return $query;
516        }
517
518        /**
519         * Retrieves the `wp_die()` handler.
520         *
521         * @param callable $handler The current die handler.
522         * @return callable The test die handler.
523         */
524        public function get_wp_die_handler( $handler ) {
525                return array( $this, 'wp_die_handler' );
526        }
527
528        /**
529         * Throws an exception when called.
530         *
531         * @since UT (3.7.0)
532         * @since 5.9.0 Added the `$title` and `$args` parameters.
533         *
534         * @throws WPDieException Exception containing the message and the response code.
535         *
536         * @param string|WP_Error $message The `wp_die()` message or WP_Error object.
537         * @param string          $title   The `wp_die()` title.
538         * @param string|array    $args    The `wp_die()` arguments.
539         */
540        public function wp_die_handler( $message, $title, $args ) {
541                if ( is_wp_error( $message ) ) {
542                        $message = $message->get_error_message();
543                }
544
545                if ( ! is_scalar( $message ) ) {
546                        $message = '0';
547                }
548
549                $code = 0;
550                if ( isset( $args['response'] ) ) {
551                        $code = $args['response'];
552                }
553
554                throw new WPDieException( $message, $code );
555        }
556
557        /**
558         * Sets up the expectations for testing a deprecated call.
559         *
560         * @since 3.7.0
561         */
562        public function expectDeprecated() {
563                if ( method_exists( $this, 'getAnnotations' ) ) {
564                        // PHPUnit < 9.5.0.
565                        $annotations = $this->getAnnotations();
566                } else {
567                        // PHPUnit >= 9.5.0.
568                        $annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations(
569                                static::class,
570                                $this->getName( false )
571                        );
572                }
573
574                foreach ( array( 'class', 'method' ) as $depth ) {
575                        if ( ! empty( $annotations[ $depth ]['expectedDeprecated'] ) ) {
576                                $this->expected_deprecated = array_merge(
577                                        $this->expected_deprecated,
578                                        $annotations[ $depth ]['expectedDeprecated']
579                                );
580                        }
581
582                        if ( ! empty( $annotations[ $depth ]['expectedIncorrectUsage'] ) ) {
583                                $this->expected_doing_it_wrong = array_merge(
584                                        $this->expected_doing_it_wrong,
585                                        $annotations[ $depth ]['expectedIncorrectUsage']
586                                );
587                        }
588                }
589
590                add_action( 'deprecated_function_run', array( $this, 'deprecated_function_run' ), 10, 3 );
591                add_action( 'deprecated_argument_run', array( $this, 'deprecated_function_run' ), 10, 3 );
592                add_action( 'deprecated_class_run', array( $this, 'deprecated_function_run' ), 10, 3 );
593                add_action( 'deprecated_file_included', array( $this, 'deprecated_function_run' ), 10, 4 );
594                add_action( 'deprecated_hook_run', array( $this, 'deprecated_function_run' ), 10, 4 );
595                add_action( 'doing_it_wrong_run', array( $this, 'doing_it_wrong_run' ), 10, 3 );
596
597                add_action( 'deprecated_function_trigger_error', '__return_false' );
598                add_action( 'deprecated_argument_trigger_error', '__return_false' );
599                add_action( 'deprecated_class_trigger_error', '__return_false' );
600                add_action( 'deprecated_file_trigger_error', '__return_false' );
601                add_action( 'deprecated_hook_trigger_error', '__return_false' );
602                add_action( 'doing_it_wrong_trigger_error', '__return_false' );
603        }
604
605        /**
606         * Handles a deprecated expectation.
607         *
608         * The DocBlock should contain `@expectedDeprecated` to trigger this.
609         *
610         * @since 3.7.0
611         * @since 6.1.0 Includes the actual unexpected `_doing_it_wrong()` message
612         *              or deprecation notice in the output if one is encountered.
613         */
614        public function expectedDeprecated() {
615                $errors = array();
616
617                $not_caught_deprecated = array_diff(
618                        $this->expected_deprecated,
619                        array_keys( $this->caught_deprecated )
620                );
621
622                foreach ( $not_caught_deprecated as $not_caught ) {
623                        $errors[] = "Failed to assert that $not_caught triggered a deprecation notice.";
624                }
625
626                $unexpected_deprecated = array_diff(
627                        array_keys( $this->caught_deprecated ),
628                        $this->expected_deprecated
629                );
630
631                foreach ( $unexpected_deprecated as $unexpected ) {
632                        $errors[] = "Unexpected deprecation notice for $unexpected.";
633                        $errors[] = $this->caught_deprecated[ $unexpected ];
634                }
635
636                $not_caught_doing_it_wrong = array_diff(
637                        $this->expected_doing_it_wrong,
638                        array_keys( $this->caught_doing_it_wrong )
639                );
640
641                foreach ( $not_caught_doing_it_wrong as $not_caught ) {
642                        $errors[] = "Failed to assert that $not_caught triggered an incorrect usage notice.";
643                }
644
645                $unexpected_doing_it_wrong = array_diff(
646                        array_keys( $this->caught_doing_it_wrong ),
647                        $this->expected_doing_it_wrong
648                );
649
650                foreach ( $unexpected_doing_it_wrong as $unexpected ) {
651                        $errors[] = "Unexpected incorrect usage notice for $unexpected.";
652                        $errors[] = $this->caught_doing_it_wrong[ $unexpected ];
653                }
654
655                // Perform an assertion, but only if there are expected or unexpected deprecated calls or wrongdoings.
656                if ( ! empty( $this->expected_deprecated ) ||
657                        ! empty( $this->expected_doing_it_wrong ) ||
658                        ! empty( $this->caught_deprecated ) ||
659                        ! empty( $this->caught_doing_it_wrong ) ) {
660                        $this->assertEmpty( $errors, implode( "\n", $errors ) );
661                }
662        }
663
664        /**
665         * Detects post-test failure conditions.
666         *
667         * We use this method to detect expectedDeprecated and expectedIncorrectUsage annotations.
668         *
669         * @since 4.2.0
670         */
671        protected function assert_post_conditions() {
672                $this->expectedDeprecated();
673        }
674
675        /**
676         * Declares an expected `_deprecated_function()` or `_deprecated_argument()` call from within a test.
677         *
678         * @since 4.2.0
679         *
680         * @param string $deprecated Name of the function, method, class, or argument that is deprecated.
681         *                           Must match the first parameter of the `_deprecated_function()`
682         *                           or `_deprecated_argument()` call.
683         */
684        public function setExpectedDeprecated( $deprecated ) {
685                $this->expected_deprecated[] = $deprecated;
686        }
687
688        /**
689         * Declares an expected `_doing_it_wrong()` call from within a test.
690         *
691         * @since 4.2.0
692         *
693         * @param string $doing_it_wrong Name of the function, method, or class that appears in
694         *                               the first argument of the source `_doing_it_wrong()` call.
695         */
696        public function setExpectedIncorrectUsage( $doing_it_wrong ) {
697                $this->expected_doing_it_wrong[] = $doing_it_wrong;
698        }
699
700        /**
701         * Redundant PHPUnit 6+ compatibility shim. DO NOT USE!
702         *
703         * This method is only left in place for backward compatibility reasons.
704         *
705         * @since 4.8.0
706         * @deprecated 5.9.0 Use the PHPUnit native expectException*() methods directly.
707         *
708         * @param mixed      $exception
709         * @param string     $message
710         * @param int|string $code
711         */
712        public function setExpectedException( $exception, $message = '', $code = null ) {
713                $this->expectException( $exception );
714
715                if ( '' !== $message ) {
716                        $this->expectExceptionMessage( $message );
717                }
718
719                if ( null !== $code ) {
720                        $this->expectExceptionCode( $code );
721                }
722        }
723
724        /**
725         * Adds a deprecated function to the list of caught deprecated calls.
726         *
727         * @since 3.7.0
728         * @since 6.1.0 Added the `$replacement`, `$version`, and `$message` parameters.
729         *
730         * @param string $function_name The deprecated function.
731         * @param string $replacement   The function that should have been called.
732         * @param string $version       The version of WordPress that deprecated the function.
733         * @param string $message       Optional. A message regarding the change.
734         */
735        public function deprecated_function_run( $function_name, $replacement, $version, $message = '' ) {
736                if ( ! isset( $this->caught_deprecated[ $function_name ] ) ) {
737                        switch ( current_action() ) {
738                                case 'deprecated_function_run':
739                                        if ( $replacement ) {
740                                                $message = sprintf(
741                                                        'Function %1$s is deprecated since version %2$s! Use %3$s instead.',
742                                                        $function_name,
743                                                        $version,
744                                                        $replacement
745                                                );
746                                        } else {
747                                                $message = sprintf(
748                                                        'Function %1$s is deprecated since version %2$s with no alternative available.',
749                                                        $function_name,
750                                                        $version
751                                                );
752                                        }
753                                        break;
754
755                                case 'deprecated_argument_run':
756                                        if ( $replacement ) {
757                                                $message = sprintf(
758                                                        'Function %1$s was called with an argument that is deprecated since version %2$s! %3$s',
759                                                        $function_name,
760                                                        $version,
761                                                        $replacement
762                                                );
763                                        } else {
764                                                $message = sprintf(
765                                                        'Function %1$s was called with an argument that is deprecated since version %2$s with no alternative available.',
766                                                        $function_name,
767                                                        $version
768                                                );
769                                        }
770                                        break;
771
772                                case 'deprecated_class_run':
773                                        if ( $replacement ) {
774                                                $message = sprintf(
775                                                        'Class %1$s is deprecated since version %2$s! Use %3$s instead.',
776                                                        $function_name,
777                                                        $version,
778                                                        $replacement
779                                                );
780                                        } else {
781                                                $message = sprintf(
782                                                        'Class %1$s is deprecated since version %2$s with no alternative available.',
783                                                        $function_name,
784                                                        $version
785                                                );
786                                        }
787                                        break;
788
789                                case 'deprecated_file_included':
790                                        if ( $replacement ) {
791                                                $message = sprintf(
792                                                        'File %1$s is deprecated since version %2$s! Use %3$s instead.',
793                                                        $function_name,
794                                                        $version,
795                                                        $replacement
796                                                ) . ' ' . $message;
797                                        } else {
798                                                $message = sprintf(
799                                                        'File %1$s is deprecated since version %2$s with no alternative available.',
800                                                        $function_name,
801                                                        $version
802                                                ) . ' ' . $message;
803                                        }
804                                        break;
805
806                                case 'deprecated_hook_run':
807                                        if ( $replacement ) {
808                                                $message = sprintf(
809                                                        'Hook %1$s is deprecated since version %2$s! Use %3$s instead.',
810                                                        $function_name,
811                                                        $version,
812                                                        $replacement
813                                                ) . ' ' . $message;
814                                        } else {
815                                                $message = sprintf(
816                                                        'Hook %1$s is deprecated since version %2$s with no alternative available.',
817                                                        $function_name,
818                                                        $version
819                                                ) . ' ' . $message;
820                                        }
821                                        break;
822                        }
823
824                        $this->caught_deprecated[ $function_name ] = $message;
825                }
826        }
827
828        /**
829         * Adds a function called in a wrong way to the list of `_doing_it_wrong()` calls.
830         *
831         * @since 3.7.0
832         * @since 6.1.0 Added the `$message` and `$version` parameters.
833         *
834         * @param string $function_name The function to add.
835         * @param string $message       A message explaining what has been done incorrectly.
836         * @param string $version       The version of WordPress where the message was added.
837         */
838        public function doing_it_wrong_run( $function_name, $message, $version ) {
839                if ( ! isset( $this->caught_doing_it_wrong[ $function_name ] ) ) {
840                        if ( $version ) {
841                                $message .= ' ' . sprintf( '(This message was added in version %s.)', $version );
842                        }
843
844                        $this->caught_doing_it_wrong[ $function_name ] = $message;
845                }
846        }
847
848        /**
849         * Asserts that the given value is an instance of WP_Error.
850         *
851         * @param mixed  $actual  The value to check.
852         * @param string $message Optional. Message to display when the assertion fails.
853         */
854        public function assertWPError( $actual, $message = '' ) {
855                $this->assertInstanceOf( 'WP_Error', $actual, $message );
856        }
857
858        /**
859         * Asserts that the given value is not an instance of WP_Error.
860         *
861         * @param mixed  $actual  The value to check.
862         * @param string $message Optional. Message to display when the assertion fails.
863         */
864        public function assertNotWPError( $actual, $message = '' ) {
865                if ( is_wp_error( $actual ) ) {
866                        $message .= ' ' . $actual->get_error_message();
867                }
868
869                $this->assertNotInstanceOf( 'WP_Error', $actual, $message );
870        }
871
872        /**
873         * Asserts that the given value is an instance of IXR_Error.
874         *
875         * @param mixed  $actual  The value to check.
876         * @param string $message Optional. Message to display when the assertion fails.
877         */
878        public function assertIXRError( $actual, $message = '' ) {
879                $this->assertInstanceOf( 'IXR_Error', $actual, $message );
880        }
881
882        /**
883         * Asserts that the given value is not an instance of IXR_Error.
884         *
885         * @param mixed  $actual  The value to check.
886         * @param string $message Optional. Message to display when the assertion fails.
887         */
888        public function assertNotIXRError( $actual, $message = '' ) {
889                if ( $actual instanceof IXR_Error ) {
890                        $message .= ' ' . $actual->message;
891                }
892
893                $this->assertNotInstanceOf( 'IXR_Error', $actual, $message );
894        }
895
896        /**
897         * Asserts that the given fields are present in the given object.
898         *
899         * @since UT (3.7.0)
900         * @since 5.9.0 Added the `$message` parameter.
901         *
902         * @param object $actual  The object to check.
903         * @param array  $fields  The fields to check.
904         * @param string $message Optional. Message to display when the assertion fails.
905         */
906        public function assertEqualFields( $actual, $fields, $message = '' ) {
907                $this->assertIsObject( $actual, $message . ' Passed $actual is not an object.' );
908                $this->assertIsArray( $fields, $message . ' Passed $fields is not an array.' );
909                $this->assertNotEmpty( $fields, $message . ' Fields array is empty.' );
910
911                foreach ( $fields as $field_name => $field_value ) {
912                        $this->assertObjectHasProperty( $field_name, $actual, $message . " Property $field_name does not exist on the object." );
913                        $this->assertSame( $field_value, $actual->$field_name, $message . " Value of property $field_name is not $field_value." );
914                }
915        }
916
917        /**
918         * Asserts that two values are equal, with whitespace differences discarded.
919         *
920         * @since UT (3.7.0)
921         * @since 5.9.0 Added the `$message` parameter.
922         *
923         * @param mixed  $expected The expected value.
924         * @param mixed  $actual   The actual value.
925         * @param string $message  Optional. Message to display when the assertion fails.
926         */
927        public function assertDiscardWhitespace( $expected, $actual, $message = '' ) {
928                if ( is_string( $expected ) ) {
929                        $expected = preg_replace( '/\s*/', '', $expected );
930                }
931
932                if ( is_string( $actual ) ) {
933                        $actual = preg_replace( '/\s*/', '', $actual );
934                }
935
936                $this->assertEquals( $expected, $actual, $message );
937        }
938
939        /**
940         * Asserts that two values have the same type and value, with EOL differences discarded.
941         *
942         * @since 5.6.0
943         * @since 5.8.0 Added support for nested arrays.
944         * @since 5.9.0 Added the `$message` parameter.
945         *
946         * @param mixed  $expected The expected value.
947         * @param mixed  $actual   The actual value.
948         * @param string $message  Optional. Message to display when the assertion fails.
949         */
950        public function assertSameIgnoreEOL( $expected, $actual, $message = '' ) {
951                if ( null !== $expected ) {
952                        $expected = map_deep(
953                                $expected,
954                                static function ( $value ) {
955                                        if ( is_string( $value ) ) {
956                                                return str_replace( "\r\n", "\n", $value );
957                                        }
958
959                                        return $value;
960                                }
961                        );
962                }
963
964                if ( null !== $actual ) {
965                        $actual = map_deep(
966                                $actual,
967                                static function ( $value ) {
968                                        if ( is_string( $value ) ) {
969                                                return str_replace( "\r\n", "\n", $value );
970                                        }
971
972                                        return $value;
973                                }
974                        );
975                }
976
977                $this->assertSame( $expected, $actual, $message );
978        }
979
980        /**
981         * Asserts that two values are equal, with EOL differences discarded.
982         *
983         * @since 5.4.0
984         * @since 5.6.0 Turned into an alias for `::assertSameIgnoreEOL()`.
985         * @since 5.9.0 Added the `$message` parameter.
986         *
987         * @param mixed  $expected The expected value.
988         * @param mixed  $actual   The actual value.
989         * @param string $message  Optional. Message to display when the assertion fails.
990         */
991        public function assertEqualsIgnoreEOL( $expected, $actual, $message = '' ) {
992                $this->assertSameIgnoreEOL( $expected, $actual, $message );
993        }
994
995        /**
996         * Asserts that the contents of two un-keyed, single arrays are the same, without accounting for the order of elements.
997         *
998         * @since 5.6.0
999         * @since 5.9.0 Added the `$message` parameter.
1000         *
1001         * @param array  $expected Expected array.
1002         * @param array  $actual   Array to check.
1003         * @param string $message  Optional. Message to display when the assertion fails.
1004         */
1005        public function assertSameSets( $expected, $actual, $message = '' ) {
1006                $this->assertIsArray( $expected, $message . ' Expected value must be an array.' );
1007                $this->assertIsArray( $actual, $message . ' Value under test is not an array.' );
1008
1009                sort( $expected );
1010                sort( $actual );
1011                $this->assertSame( $expected, $actual, $message );
1012        }
1013
1014        /**
1015         * Asserts that the contents of two un-keyed, single arrays are equal, without accounting for the order of elements.
1016         *
1017         * @since 3.5.0
1018         * @since 5.9.0 Added the `$message` parameter.
1019         *
1020         * @param array  $expected Expected array.
1021         * @param array  $actual   Array to check.
1022         * @param string $message  Optional. Message to display when the assertion fails.
1023         */
1024        public function assertEqualSets( $expected, $actual, $message = '' ) {
1025                $this->assertIsArray( $expected, $message . ' Expected value must be an array.' );
1026                $this->assertIsArray( $actual, $message . ' Value under test is not an array.' );
1027
1028                sort( $expected );
1029                sort( $actual );
1030                $this->assertEquals( $expected, $actual, $message );
1031        }
1032
1033        /**
1034         * Asserts that the contents of two keyed, single arrays are the same, without accounting for the order of elements.
1035         *
1036         * @since 5.6.0
1037         * @since 5.9.0 Added the `$message` parameter.
1038         *
1039         * @param array  $expected Expected array.
1040         * @param array  $actual   Array to check.
1041         * @param string $message  Optional. Message to display when the assertion fails.
1042         */
1043        public function assertSameSetsWithIndex( $expected, $actual, $message = '' ) {
1044                $this->assertIsArray( $expected, $message . ' Expected value must be an array.' );
1045                $this->assertIsArray( $actual, $message . ' Value under test is not an array.' );
1046
1047                ksort( $expected );
1048                ksort( $actual );
1049                $this->assertSame( $expected, $actual, $message );
1050        }
1051
1052        /**
1053         * Asserts that the contents of two keyed, single arrays are equal, without accounting for the order of elements.
1054         *
1055         * @since 4.1.0
1056         * @since 5.9.0 Added the `$message` parameter.
1057         *
1058         * @param array  $expected Expected array.
1059         * @param array  $actual   Array to check.
1060         * @param string $message  Optional. Message to display when the assertion fails.
1061         */
1062        public function assertEqualSetsWithIndex( $expected, $actual, $message = '' ) {
1063                $this->assertIsArray( $expected, $message . ' Expected value must be an array.' );
1064                $this->assertIsArray( $actual, $message . ' Value under test is not an array.' );
1065
1066                ksort( $expected );
1067                ksort( $actual );
1068                $this->assertEquals( $expected, $actual, $message );
1069        }
1070
1071        /**
1072         * Asserts that the given variable is a multidimensional array, and that all arrays are non-empty.
1073         *
1074         * @since 4.8.0
1075         * @since 5.9.0 Added the `$message` parameter.
1076         *
1077         * @param array  $actual  Array to check.
1078         * @param string $message Optional. Message to display when the assertion fails.
1079         */
1080        public function assertNonEmptyMultidimensionalArray( $actual, $message = '' ) {
1081                $this->assertIsArray( $actual, $message . ' Value under test is not an array.' );
1082                $this->assertNotEmpty( $actual, $message . ' Array is empty.' );
1083
1084                foreach ( $actual as $sub_array ) {
1085                        $this->assertIsArray( $sub_array, $message . ' Subitem of the array is not an array.' );
1086                        $this->assertNotEmpty( $sub_array, $message . ' Subitem of the array is empty.' );
1087                }
1088        }
1089
1090        /**
1091         * Assert that two text strings representing file paths are the same, while ignoring
1092         * OS-specific differences in the directory separators.
1093         *
1094         * This allows for tests to be compatible for running on both *nix based as well as Windows OS.
1095         *
1096         * @since 6.7.0
1097         *
1098         * @param string $path_a File or directory path.
1099         * @param string $path_b File or directory path.
1100         */
1101        public function assertSamePathIgnoringDirectorySeparators( $path_a, $path_b ) {
1102                $path_a = $this->normalizeDirectorySeparatorsInPath( $path_a );
1103                $path_b = $this->normalizeDirectorySeparatorsInPath( $path_b );
1104
1105                $this->assertSame( $path_a, $path_b );
1106        }
1107
1108        /**
1109         * Normalize directory separators in a file path to be a forward slash.
1110         *
1111         * @since 6.7.0
1112         *
1113         * @param string $path File or directory path.
1114         * @return string The normalized file or directory path.
1115         */
1116        public function normalizeDirectorySeparatorsInPath( $path ) {
1117                if ( ! is_string( $path ) || PHP_OS_FAMILY !== 'Windows' ) {
1118                        return $path;
1119                }
1120
1121                return strtr( $path, '\\', '/' );
1122        }
1123
1124        /**
1125         * Checks each of the WP_Query is_* functions/properties against expected boolean value.
1126         *
1127         * Any properties that are listed by name as parameters will be expected to be true; all others are
1128         * expected to be false. For example, assertQueryTrue( 'is_single', 'is_feed' ) means is_single()
1129         * and is_feed() must be true and everything else must be false to pass.
1130         *
1131         * @since 2.5.0
1132         * @since 3.8.0 Moved from `Tests_Query_Conditionals` to `WP_UnitTestCase`.
1133         * @since 5.3.0 Formalized the existing `...$prop` parameter by adding it
1134         *              to the function signature.
1135         *
1136         * @param string ...$prop Any number of WP_Query properties that are expected to be true for the current request.
1137         */
1138        public function assertQueryTrue( ...$prop ) {
1139                global $wp_query;
1140
1141                $all = array(
1142                        'is_404',
1143                        'is_admin',
1144                        'is_archive',
1145                        'is_attachment',
1146                        'is_author',
1147                        'is_category',
1148                        'is_comment_feed',
1149                        'is_date',
1150                        'is_day',
1151                        'is_embed',
1152                        'is_feed',
1153                        'is_front_page',
1154                        'is_home',
1155                        'is_privacy_policy',
1156                        'is_month',
1157                        'is_page',
1158                        'is_paged',
1159                        'is_post_type_archive',
1160                        'is_posts_page',
1161                        'is_preview',
1162                        'is_robots',
1163                        'is_favicon',
1164                        'is_search',
1165                        'is_single',
1166                        'is_singular',
1167                        'is_tag',
1168                        'is_tax',
1169                        'is_time',
1170                        'is_trackback',
1171                        'is_year',
1172                );
1173
1174                foreach ( $prop as $true_thing ) {
1175                        $this->assertContains( $true_thing, $all, "Unknown conditional: {$true_thing}." );
1176                }
1177
1178                $passed  = true;
1179                $message = '';
1180
1181                foreach ( $all as $query_thing ) {
1182                        $result = is_callable( $query_thing ) ? call_user_func( $query_thing ) : $wp_query->$query_thing;
1183
1184                        if ( in_array( $query_thing, $prop, true ) ) {
1185                                if ( ! $result ) {
1186                                        $message .= $query_thing . ' is false but is expected to be true. ' . PHP_EOL;
1187                                        $passed   = false;
1188                                }
1189                        } elseif ( $result ) {
1190                                $message .= $query_thing . ' is true but is expected to be false. ' . PHP_EOL;
1191                                $passed   = false;
1192                        }
1193                }
1194
1195                if ( ! $passed ) {
1196                        $this->fail( $message );
1197                }
1198        }
1199
1200        /**
1201         * Check HTML markup (including blocks) for semantic equivalence.
1202         *
1203         * Given two markup strings, assert that they translate to the same semantic HTML tree,
1204         * normalizing tag names, attribute names, and attribute order. Furthermore, attributes
1205         * and class names are sorted and deduplicated, and whitespace in style attributes
1206         * is normalized. Finally, block delimiter comments are recognized and normalized,
1207         * applying the same principles.
1208         *
1209         * @since 6.9.0
1210         *
1211         * @param string      $expected         The expected HTML.
1212         * @param string      $actual           The actual HTML.
1213         * @param string|null $fragment_context Optional. The fragment context, for example "<td>" expected HTML
1214         *                                      must occur within "<table><tr>" fragment context. Default "<body>".
1215         *                                      Only "<body>" or `null` are supported at this time.
1216         *                                      Set to `null` to parse a full HTML document.
1217         * @param string|null $message          Optional. The assertion error message.
1218         */
1219        public function assertEqualHTML( string $expected, string $actual, ?string $fragment_context = '<body>', $message = 'HTML markup was not equivalent.' ): void {
1220                try {
1221                        $tree_expected = build_visual_html_tree( $expected, $fragment_context );
1222                        $tree_actual   = build_visual_html_tree( $actual, $fragment_context );
1223                } catch ( Exception $e ) {
1224                        // For PHP 8.4+, we can retry, using the built-in DOM\HTMLDocument parser.
1225                        if ( class_exists( 'DOM\HtmlDocument' ) ) {
1226                                $dom_expected  = DOM\HtmlDocument::createFromString( $expected, LIBXML_NOERROR );
1227                                $tree_expected = build_visual_html_tree( $dom_expected->saveHtml(), $fragment_context );
1228                                $dom_actual    = DOM\HtmlDocument::createFromString( $actual, LIBXML_NOERROR );
1229                                $tree_actual   = build_visual_html_tree( $dom_actual->saveHtml(), $fragment_context );
1230                        } else {
1231                                throw $e;
1232                        }
1233                }
1234
1235                $this->assertSame( $tree_expected, $tree_actual, $message );
1236        }
1237
1238        /**
1239         * Helper function to convert a single-level array containing text strings to a named data provider.
1240         *
1241         * The value of the data set will also be used as the name of the data set.
1242         *
1243         * Typical usage of this method:
1244         *
1245         *     public function data_provider_for_test_name() {
1246         *         $array = array(
1247         *             'value1',
1248         *             'value2',
1249         *         );
1250         *
1251         *         return $this->text_array_to_dataprovider( $array );
1252         *     }
1253         *
1254         * The returned result will look like:
1255         *
1256         *     array(
1257         *         'value1' => array( 'value1' ),
1258         *         'value2' => array( 'value2' ),
1259         *     )
1260         *
1261         * @since 6.1.0
1262         *
1263         * @param array $input Input array.
1264         * @return array Array which is usable as a test data provider with named data sets.
1265         */
1266        public static function text_array_to_dataprovider( $input ) {
1267                $data = array();
1268
1269                foreach ( $input as $value ) {
1270                        if ( ! is_string( $value ) ) {
1271                                throw new Exception(
1272                                        'All values in the input array should be text strings. Fix the input data.'
1273                                );
1274                        }
1275
1276                        if ( isset( $data[ $value ] ) ) {
1277                                throw new Exception(
1278                                        "Attempting to add a duplicate data set for value $value to the data provider. Fix the input data."
1279                                );
1280                        }
1281
1282                        $data[ $value ] = array( $value );
1283                }
1284
1285                return $data;
1286        }
1287
1288        /**
1289         * Sets the global state to as if a given URL has been requested.
1290         *
1291         * This sets:
1292         * - The super globals.
1293         * - The globals.
1294         * - The query variables.
1295         * - The main query.
1296         *
1297         * @since 3.5.0
1298         *
1299         * @param string $url The URL for the request.
1300         */
1301        public function go_to( $url ) {
1302                /*
1303                 * Note: the WP and WP_Query classes like to silently fetch parameters
1304                 * from all over the place (globals, GET, etc), which makes it tricky
1305                 * to run them more than once without very carefully clearing everything.
1306                 */
1307                $_GET  = array();
1308                $_POST = array();
1309                foreach ( array( 'query_string', 'id', 'postdata', 'authordata', 'day', 'currentmonth', 'page', 'pages', 'multipage', 'more', 'numpages', 'pagenow', 'current_screen' ) as $v ) {
1310                        if ( isset( $GLOBALS[ $v ] ) ) {
1311                                unset( $GLOBALS[ $v ] );
1312                        }
1313                }
1314                $parts = parse_url( $url );
1315                if ( isset( $parts['scheme'] ) ) {
1316                        $req = isset( $parts['path'] ) ? $parts['path'] : '';
1317                        if ( isset( $parts['query'] ) ) {
1318                                $req .= '?' . $parts['query'];
1319                                // Parse the URL query vars into $_GET.
1320                                parse_str( $parts['query'], $_GET );
1321                        }
1322                } else {
1323                        $req = $url;
1324                }
1325                if ( ! isset( $parts['query'] ) ) {
1326                        $parts['query'] = '';
1327                }
1328
1329                $_SERVER['REQUEST_URI'] = $req;
1330                unset( $_SERVER['PATH_INFO'] );
1331
1332                self::flush_cache();
1333                unset( $GLOBALS['wp_query'], $GLOBALS['wp_the_query'] );
1334                $GLOBALS['wp_the_query'] = new WP_Query();
1335                $GLOBALS['wp_query']     = $GLOBALS['wp_the_query'];
1336
1337                $public_query_vars  = $GLOBALS['wp']->public_query_vars;
1338                $private_query_vars = $GLOBALS['wp']->private_query_vars;
1339
1340                $GLOBALS['wp']                     = new WP();
1341                $GLOBALS['wp']->public_query_vars  = $public_query_vars;
1342                $GLOBALS['wp']->private_query_vars = $private_query_vars;
1343
1344                _cleanup_query_vars();
1345
1346                $GLOBALS['wp']->main( $parts['query'] );
1347        }
1348
1349        /**
1350         * Allows tests to be skipped on single or multisite installs by using @group annotations.
1351         *
1352         * This is a custom extension of the PHPUnit requirements handling.
1353         *
1354         * @since 3.5.0
1355         * @deprecated 5.9.0 This method has not been functional since PHPUnit 7.0.
1356         */
1357        protected function checkRequirements() {
1358                // For PHPUnit 5/6, as we're overloading a public PHPUnit native method in those versions.
1359                if ( is_callable( 'PHPUnit\Framework\TestCase', 'checkRequirements' ) ) {
1360                        parent::checkRequirements();
1361                }
1362        }
1363
1364        /**
1365         * Skips the current test if there is an open Trac ticket associated with it.
1366         *
1367         * @since 3.5.0
1368         *
1369         * @param int $ticket_id Ticket number.
1370         */
1371        public function knownWPBug( $ticket_id ) {
1372                if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( $ticket_id, self::$forced_tickets, true ) ) {
1373                        return;
1374                }
1375                if ( ! TracTickets::isTracTicketClosed( 'https://core.trac.wordpress.org', $ticket_id ) ) {
1376                        $this->markTestSkipped( sprintf( 'WordPress Ticket #%d is not fixed', $ticket_id ) );
1377                }
1378        }
1379
1380        /**
1381         * Skips the current test if there is an open Unit Test Trac ticket associated with it.
1382         *
1383         * @since 3.5.0
1384         * @deprecated No longer used since the Unit Test Trac was merged into the Core Trac.
1385         *
1386         * @param int $ticket_id Ticket number.
1387         */
1388        public function knownUTBug( $ticket_id ) {
1389                return;
1390        }
1391
1392        /**
1393         * Skips the current test if there is an open Plugin Trac ticket associated with it.
1394         *
1395         * @since 3.5.0
1396         *
1397         * @param int $ticket_id Ticket number.
1398         */
1399        public function knownPluginBug( $ticket_id ) {
1400                if ( WP_TESTS_FORCE_KNOWN_BUGS || in_array( 'Plugin' . $ticket_id, self::$forced_tickets, true ) ) {
1401                        return;
1402                }
1403                if ( ! TracTickets::isTracTicketClosed( 'https://plugins.trac.wordpress.org', $ticket_id ) ) {
1404                        $this->markTestSkipped( sprintf( 'WordPress Plugin Ticket #%d is not fixed', $ticket_id ) );
1405                }
1406        }
1407
1408        /**
1409         * Adds a Trac ticket number to the `$forced_tickets` property.
1410         *
1411         * @since 3.5.0
1412         *
1413         * @param int $ticket Ticket number.
1414         */
1415        public static function forceTicket( $ticket ) {
1416                self::$forced_tickets[] = $ticket;
1417        }
1418
1419        /**
1420         * Custom preparations for the PHPUnit process isolation template.
1421         *
1422         * When restoring global state between tests, PHPUnit defines all the constants that were already defined, and then
1423         * includes included files. This does not work with WordPress, as the included files define the constants.
1424         *
1425         * This method defines the constants after including files.
1426         *
1427         * @param Text_Template $template The template to prepare.
1428         */
1429        public function prepareTemplate( Text_Template $template ) {
1430                $template->setVar( array( 'constants' => '' ) );
1431                $template->setVar( array( 'wp_constants' => PHPUnit_Util_GlobalState::getConstantsAsString() ) );
1432                parent::prepareTemplate( $template );
1433        }
1434
1435        /**
1436         * Creates a unique temporary file name.
1437         *
1438         * The directory in which the file is created depends on the environment configuration.
1439         *
1440         * @since 3.5.0
1441         *
1442         * @return string|bool Path on success, else false.
1443         */
1444        public function temp_filename() {
1445                $tmp_dir = '';
1446                $dirs    = array( 'TMP', 'TMPDIR', 'TEMP' );
1447
1448                foreach ( $dirs as $dir ) {
1449                        if ( isset( $_ENV[ $dir ] ) && ! empty( $_ENV[ $dir ] ) ) {
1450                                $tmp_dir = $dir;
1451                                break;
1452                        }
1453                }
1454
1455                if ( empty( $tmp_dir ) ) {
1456                        $tmp_dir = get_temp_dir();
1457                }
1458
1459                $tmp_dir = realpath( $tmp_dir );
1460
1461                return tempnam( $tmp_dir, 'wpunit' );
1462        }
1463
1464        /**
1465         * Selectively deletes a file.
1466         *
1467         * Does not delete a file if its path is set in the `$ignore_files` property.
1468         *
1469         * @param string $file File path.
1470         */
1471        public function unlink( $file ) {
1472                $exists = is_file( $file );
1473                if ( $exists && ! in_array( $file, self::$ignore_files, true ) ) {
1474                        //error_log( $file );
1475                        unlink( $file );
1476                } elseif ( ! $exists ) {
1477                        $this->fail( "Trying to delete a file that doesn't exist: $file" );
1478                }
1479        }
1480
1481        /**
1482         * Selectively deletes files from a directory.
1483         *
1484         * Does not delete files if their paths are set in the `$ignore_files` property.
1485         *
1486         * @since 4.0.0
1487         *
1488         * @param string $path Directory path.
1489         */
1490        public function rmdir( $path ) {
1491                $files = $this->files_in_dir( $path );
1492                foreach ( $files as $file ) {
1493                        if ( ! in_array( $file, self::$ignore_files, true ) ) {
1494                                $this->unlink( $file );
1495                        }
1496                }
1497        }
1498
1499        /**
1500         * Deletes files added to the `uploads` directory during tests.
1501         *
1502         * This method works in tandem with the `set_up()` and `rmdir()` methods:
1503         * - `set_up()` scans the `uploads` directory before every test, and stores
1504         *   its contents inside of the `$ignore_files` property.
1505         * - `rmdir()` and its helper methods only delete files that are not listed
1506         *   in the `$ignore_files` property. If called during `tear_down()` in tests,
1507         *   this will only delete files added during the previously run test.
1508         */
1509        public function remove_added_uploads() {
1510                $uploads = wp_upload_dir();
1511                $this->rmdir( $uploads['basedir'] );
1512        }
1513
1514        /**
1515         * Returns a list of all files contained inside a directory.
1516         *
1517         * @since 4.0.0
1518         *
1519         * @param string $dir Path to the directory to scan.
1520         * @return string[] List of file paths.
1521         */
1522        public function files_in_dir( $dir ) {
1523                $files = array();
1524
1525                $iterator = new RecursiveDirectoryIterator( $dir );
1526                $objects  = new RecursiveIteratorIterator( $iterator );
1527                foreach ( $objects as $name => $object ) {
1528                        if ( is_file( $name ) ) {
1529                                $files[] = $name;
1530                        }
1531                }
1532
1533                return $files;
1534        }
1535
1536        /**
1537         * Returns a list of all files contained inside the `uploads` directory.
1538         *
1539         * @since 4.0.0
1540         *
1541         * @return string[] List of file paths.
1542         */
1543        public function scan_user_uploads() {
1544                static $files = array();
1545                if ( ! empty( $files ) ) {
1546                        return $files;
1547                }
1548
1549                $uploads = wp_upload_dir();
1550                $files   = $this->files_in_dir( $uploads['basedir'] );
1551                return $files;
1552        }
1553
1554        /**
1555         * Deletes all directories contained inside a directory.
1556         *
1557         * @since 4.1.0
1558         *
1559         * @param string $path Path to the directory to scan.
1560         */
1561        public function delete_folders( $path ) {
1562                if ( ! is_dir( $path ) ) {
1563                        return;
1564                }
1565
1566                $matched_dirs = $this->scandir( $path );
1567
1568                foreach ( array_reverse( $matched_dirs ) as $dir ) {
1569                        rmdir( $dir );
1570                }
1571
1572                rmdir( $path );
1573        }
1574
1575        /**
1576         * Retrieves all directories contained inside a directory.
1577         * Hidden directories are ignored.
1578         *
1579         * This is a helper for the `delete_folders()` method.
1580         *
1581         * @since 4.1.0
1582         * @since 6.1.0 No longer sets a (dynamic) property to keep track of the directories,
1583         *              but returns an array of the directories instead.
1584         *
1585         * @param string $dir Path to the directory to scan.
1586         * @return string[] List of directories.
1587         */
1588        public function scandir( $dir ) {
1589                $matched_dirs = array();
1590
1591                foreach ( scandir( $dir ) as $path ) {
1592                        if ( 0 !== strpos( $path, '.' ) && is_dir( $dir . '/' . $path ) ) {
1593                                $matched_dirs[] = array( $dir . '/' . $path );
1594                                $matched_dirs[] = $this->scandir( $dir . '/' . $path );
1595                        }
1596                }
1597
1598                /*
1599                 * Compatibility check for PHP < 7.4, where array_merge() expects at least one array.
1600                 * See: https://3v4l.org/BIQMA
1601                 */
1602                if ( array() === $matched_dirs ) {
1603                        return array();
1604                }
1605
1606                return array_merge( ...$matched_dirs );
1607        }
1608
1609        /**
1610         * Converts a microtime string into a float.
1611         *
1612         * @since 4.1.0
1613         *
1614         * @param string $microtime Time string generated by `microtime()`.
1615         * @return float `microtime()` output as a float.
1616         */
1617        protected function _microtime_to_float( $microtime ) {
1618                $time_array = explode( ' ', $microtime );
1619                return array_sum( $time_array );
1620        }
1621
1622        /**
1623         * Deletes a user from the database in a Multisite-agnostic way.
1624         *
1625         * @since 4.3.0
1626         *
1627         * @param int $user_id User ID.
1628         * @return bool True if the user was deleted.
1629         */
1630        public static function delete_user( $user_id ) {
1631                if ( is_multisite() ) {
1632                        return wpmu_delete_user( $user_id );
1633                }
1634
1635                return wp_delete_user( $user_id );
1636        }
1637
1638        /**
1639         * Resets permalinks and flushes rewrites.
1640         *
1641         * @since 4.4.0
1642         *
1643         * @global WP_Rewrite $wp_rewrite
1644         *
1645         * @param string $structure Optional. Permalink structure to set. Default empty.
1646         */
1647        public function set_permalink_structure( $structure = '' ) {
1648                global $wp_rewrite;
1649
1650                $wp_rewrite->init();
1651                $wp_rewrite->set_permalink_structure( $structure );
1652                $wp_rewrite->flush_rules();
1653        }
1654
1655        /**
1656         * Creates an attachment post from an uploaded file.
1657         *
1658         * @since 4.4.0
1659         * @since 6.2.0 Returns a WP_Error object on failure.
1660         *
1661         * @param array $upload         Array of information about the uploaded file, provided by wp_upload_bits().
1662         * @param int   $parent_post_id Optional. Parent post ID.
1663         * @return int|WP_Error The attachment ID on success, WP_Error object on failure.
1664         */
1665        public function _make_attachment( $upload, $parent_post_id = 0 ) {
1666                $type = '';
1667                if ( ! empty( $upload['type'] ) ) {
1668                        $type = $upload['type'];
1669                } else {
1670                        $mime = wp_check_filetype( $upload['file'] );
1671                        if ( $mime ) {
1672                                $type = $mime['type'];
1673                        }
1674                }
1675
1676                $attachment = array(
1677                        'post_title'     => wp_basename( $upload['file'] ),
1678                        'post_content'   => '',
1679                        'post_type'      => 'attachment',
1680                        'post_parent'    => $parent_post_id,
1681                        'post_mime_type' => $type,
1682                        'guid'           => $upload['url'],
1683                );
1684
1685                $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $parent_post_id, true );
1686
1687                if ( is_wp_error( $attachment_id ) ) {
1688                        return $attachment_id;
1689                }
1690
1691                wp_update_attachment_metadata(
1692                        $attachment_id,
1693                        wp_generate_attachment_metadata( $attachment_id, $upload['file'] )
1694                );
1695
1696                return $attachment_id;
1697        }
1698
1699        /**
1700         * Updates the modified and modified GMT date of a post in the database.
1701         *
1702         * @since 4.8.0
1703         *
1704         * @global wpdb $wpdb WordPress database abstraction object.
1705         *
1706         * @param int    $post_id Post ID.
1707         * @param string $date    Post date, in the format YYYY-MM-DD HH:MM:SS.
1708         * @return int|false 1 on success, or false on error.
1709         */
1710        protected function update_post_modified( $post_id, $date ) {
1711                global $wpdb;
1712                return $wpdb->update(
1713                        $wpdb->posts,
1714                        array(
1715                                'post_modified'     => $date,
1716                                'post_modified_gmt' => $date,
1717                        ),
1718                        array(
1719                                'ID' => $post_id,
1720                        ),
1721                        array(
1722                                '%s',
1723                                '%s',
1724                        ),
1725                        array(
1726                                '%d',
1727                        )
1728                );
1729        }
1730
1731        /**
1732         * Touches the given file and its directory if it doesn't already exist.
1733         *
1734         * This can be used to ensure a file that is implicitly relied on in a test exists
1735         * without it having to be built.
1736         *
1737         * @param string $file The file name.
1738         */
1739        public static function touch( $file ) {
1740                if ( file_exists( $file ) ) {
1741                        return;
1742                }
1743
1744                $dir = dirname( $file );
1745
1746                if ( ! file_exists( $dir ) ) {
1747                        mkdir( $dir, 0777, true );
1748                }
1749
1750                touch( $file );
1751        }
1752
1753        /**
1754         * Wrapper for `wp_safe_remote_request()` that retries on error and skips the test on timeout.
1755         *
1756         * @param string $url  URL to retrieve.
1757         * @param array  $args Optional. Request arguments. Default empty array.
1758         * @return array|WP_Error The response or WP_Error on failure.
1759         */
1760        protected function wp_safe_remote_request( $url, $args = array() ) {
1761                return self::retry_on_error( 'wp_safe_remote_request', $url, $args );
1762        }
1763
1764        /**
1765         * Wrapper for `wp_safe_remote_get()` that retries on error and skips the test on timeout.
1766         *
1767         * @param string $url  URL to retrieve.
1768         * @param array  $args Optional. Request arguments. Default empty array.
1769         * @return array|WP_Error The response or WP_Error on failure.
1770         */
1771        protected function wp_safe_remote_get( $url, $args = array() ) {
1772                return self::retry_on_error( 'wp_safe_remote_get', $url, $args );
1773        }
1774
1775        /**
1776         * Wrapper for `wp_safe_remote_post()` that retries on error and skips the test on timeout.
1777         *
1778         * @param string $url  URL to retrieve.
1779         * @param array  $args Optional. Request arguments. Default empty array.
1780         * @return array|WP_Error The response or WP_Error on failure.
1781         */
1782        protected function wp_safe_remote_post( $url, $args = array() ) {
1783                return self::retry_on_error( 'wp_safe_remote_post', $url, $args );
1784        }
1785
1786        /**
1787         * Wrapper for `wp_safe_remote_head()` that retries on error and skips the test on timeout.
1788         *
1789         * @param string $url  URL to retrieve.
1790         * @param array  $args Optional. Request arguments. Default empty array.
1791         * @return array|WP_Error The response or WP_Error on failure.
1792         */
1793        protected function wp_safe_remote_head( $url, $args = array() ) {
1794                return self::retry_on_error( 'wp_safe_remote_head', $url, $args );
1795        }
1796
1797        /**
1798         * Wrapper for `wp_remote_request()` that retries on error and skips the test on timeout.
1799         *
1800         * @param string $url  URL to retrieve.
1801         * @param array  $args Optional. Request arguments. Default empty array.
1802         * @return array|WP_Error The response or WP_Error on failure.
1803         */
1804        protected function wp_remote_request( $url, $args = array() ) {
1805                return self::retry_on_error( 'wp_remote_request', $url, $args );
1806        }
1807
1808        /**
1809         * Wrapper for `wp_remote_get()` that retries on error and skips the test on timeout.
1810         *
1811         * @param string $url  URL to retrieve.
1812         * @param array  $args Optional. Request arguments. Default empty array.
1813         * @return array|WP_Error The response or WP_Error on failure.
1814         */
1815        protected function wp_remote_get( $url, $args = array() ) {
1816                return self::retry_on_error( 'wp_remote_get', $url, $args );
1817        }
1818
1819        /**
1820         * Wrapper for `wp_remote_post()` that retries on error and skips the test on timeout.
1821         *
1822         * @param string $url  URL to retrieve.
1823         * @param array  $args Optional. Request arguments. Default empty array.
1824         * @return array|WP_Error The response or WP_Error on failure.
1825         */
1826        protected function wp_remote_post( $url, $args = array() ) {
1827                return self::retry_on_error( 'wp_remote_post', $url, $args );
1828        }
1829
1830        /**
1831         * Wrapper for `wp_remote_head()` that retries on error and skips the test on timeout.
1832         *
1833         * @param string $url  URL to retrieve.
1834         * @param array  $args Optional. Request arguments. Default empty array.
1835         * @return array|WP_Error The response or WP_Error on failure.
1836         */
1837        protected function wp_remote_head( $url, $args = array() ) {
1838                return self::retry_on_error( 'wp_remote_head', $url, $args );
1839        }
1840
1841        /**
1842         * Retries an HTTP API request up to three times and skips the test on timeout.
1843         *
1844         * @param callable $callback The HTTP API request function to call.
1845         * @param string   $url      URL to retrieve.
1846         * @param array    $args     Request arguments.
1847         * @return array|WP_Error The response or WP_Error on failure.
1848         */
1849        private function retry_on_error( callable $callback, $url, $args ) {
1850                $attempts = 0;
1851
1852                while ( $attempts < 3 ) {
1853                        $result = call_user_func( $callback, $url, $args );
1854
1855                        if ( ! is_wp_error( $result ) ) {
1856                                return $result;
1857                        }
1858
1859                        ++$attempts;
1860                        sleep( 5 );
1861                }
1862
1863                $this->skipTestOnTimeout( $result );
1864
1865                return $result;
1866        }
1867}
Note: See TracBrowser for help on using the repository browser.