Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
42.59% |
23 / 54 |
|
0.00% |
0 / 5 |
CRAP | |
0.00% |
0 / 1 |
| Test_Environment | |
42.59% |
23 / 54 |
|
0.00% |
0 / 5 |
113.57 | |
0.00% |
0 / 1 |
| find_autoloader | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
4.02 | |||
| make_temp_dir | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| rmrf | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
| init | |
63.64% |
14 / 22 |
|
0.00% |
0 / 1 |
12.89 | |||
| reduce_password_cost | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Provides a shared WordPress test environment for Jetpack packages |
| 4 | * |
| 5 | * @package automattic/jetpack-test-environment |
| 6 | */ |
| 7 | |
| 8 | namespace Automattic\Jetpack; |
| 9 | |
| 10 | /** |
| 11 | * Provides a shared WordPress test environment for Jetpack packages. |
| 12 | * This ensures WordPress is only loaded once across all packages using this environment. |
| 13 | */ |
| 14 | class Test_Environment { |
| 15 | |
| 16 | const PACKAGE_VERSION = '0.3.1'; |
| 17 | |
| 18 | /** |
| 19 | * Whether the environment has been initialized. |
| 20 | * |
| 21 | * @var bool |
| 22 | */ |
| 23 | private static $initialized = false; |
| 24 | |
| 25 | /** |
| 26 | * Find the test environment autoloader. |
| 27 | * |
| 28 | * @throws \RuntimeException If the autoloader cannot be found. |
| 29 | * @return string Path to the test environment autoloader. |
| 30 | */ |
| 31 | public static function find_autoloader() { |
| 32 | // Try the simple path first (works for symlinked/development case). |
| 33 | $simple_path = dirname( __DIR__, 4 ) . '/tools/php-test-env/vendor/autoload.php'; |
| 34 | if ( file_exists( $simple_path ) ) { |
| 35 | return $simple_path; |
| 36 | } |
| 37 | |
| 38 | // If simple path fails, search recursively for the autoloader. |
| 39 | $dir = __DIR__; |
| 40 | while ( ! file_exists( $dir . '/tools/php-test-env/vendor/autoload.php' ) ) { |
| 41 | $parent = dirname( $dir ); |
| 42 | if ( $parent === $dir ) { |
| 43 | throw new \RuntimeException( 'Could not locate test environment autoloader' ); |
| 44 | } |
| 45 | $dir = $parent; |
| 46 | } |
| 47 | return $dir . '/tools/php-test-env/vendor/autoload.php'; |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Create a temporary directory. |
| 52 | * |
| 53 | * @param string $prefix Directory name prefix. The created directory will be under `sys_get_temp_dir()` with a random suffix added to this prefix. |
| 54 | * @return string Created path. |
| 55 | * @throws \RuntimeException On failure to create any directory. |
| 56 | */ |
| 57 | private static function make_temp_dir( $prefix ) { |
| 58 | // phpcs:ignore WordPress.WP.AlternativeFunctions.rand_rand -- Not security sensitive, and `wp_rand` isn't loaded yet. |
| 59 | $mask = rand( 0, 0xffffff ); |
| 60 | for ( $i = 0; $i < 0xffffff; $i++ ) { |
| 61 | $tmpdir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . $prefix . sprintf( '%06x', $i ^ $mask ); |
| 62 | // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- WP isn't loaded yet, and we're handling errors here. |
| 63 | if ( @mkdir( $tmpdir, 0700 ) ) { |
| 64 | return $tmpdir; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | throw new \RuntimeException( 'Failed to create temporary directory' ); |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Remove a directory, recursively. |
| 73 | * |
| 74 | * @private |
| 75 | * @param string $dir Directory to remove. |
| 76 | */ |
| 77 | public static function rmrf( $dir ) { |
| 78 | if ( ! file_exists( $dir ) ) { |
| 79 | return; |
| 80 | } |
| 81 | if ( ! is_dir( $dir ) ) { |
| 82 | // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- WP isn't loaded yet. |
| 83 | unlink( $dir ); |
| 84 | return; |
| 85 | } |
| 86 | |
| 87 | $iter = new \RecursiveIteratorIterator( |
| 88 | new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS ), |
| 89 | \RecursiveIteratorIterator::CHILD_FIRST |
| 90 | ); |
| 91 | foreach ( $iter as $path ) { |
| 92 | if ( is_dir( $path ) ) { |
| 93 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP isn't loaded yet. |
| 94 | rmdir( $path ); |
| 95 | } else { |
| 96 | // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- WP isn't loaded yet. |
| 97 | unlink( $path ); |
| 98 | } |
| 99 | } |
| 100 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- WP isn't loaded yet. |
| 101 | rmdir( $dir ); |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Initialize the shared WordPress test environment. |
| 106 | * |
| 107 | * This ensures we only load WordPress once across all packages. |
| 108 | * |
| 109 | * @param string|null $package_slug Optional package slug for custom upload directory. |
| 110 | * @param string $db_engine Optional database engine to use. Current options are 'dbless' and 'sqlite'. |
| 111 | * |
| 112 | * @throws \RuntimeException If WordPress test environment fails to initialize. |
| 113 | */ |
| 114 | public static function init( $package_slug = null, $db_engine = 'dbless' ) { |
| 115 | if ( self::$initialized ) { |
| 116 | return; |
| 117 | } |
| 118 | |
| 119 | // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath |
| 120 | require_once self::find_autoloader(); |
| 121 | |
| 122 | $tmpdir = null; |
| 123 | |
| 124 | try { |
| 125 | if ( ! class_exists( '\WorDBless\Load' ) ) { |
| 126 | throw new \RuntimeException( 'WorDBless not found. Please ensure automattic/wordbless is installed in tools/php-test-env/composer.json' ); |
| 127 | } |
| 128 | |
| 129 | // If a package slug is provided, use it for a custom upload dir. |
| 130 | if ( $package_slug && ! defined( 'dbless_UPLOADS' ) ) { |
| 131 | define( 'dbless_UPLOADS', 'uploads-' . $package_slug ); // phpcs:ignore Generic.NamingConventions.UpperCaseConstantName.ConstantNotUpperCase |
| 132 | } |
| 133 | |
| 134 | // If using sqlite, point FQDBDIR at a unique directory so concurrent runs don't interfere with each other (sqlite does poorly with multiple writers). |
| 135 | $persist = false; |
| 136 | if ( $db_engine === 'sqlite' ) { |
| 137 | $tmpdir = self::make_temp_dir( 'test-sqlite-' . ( $package_slug ?? 'null' ) . '-' ); |
| 138 | define( 'FQDBDIR', "$tmpdir/" ); |
| 139 | $persist = true; |
| 140 | } |
| 141 | |
| 142 | \WorDBless\Load::load( $db_engine, $persist ); |
| 143 | |
| 144 | // WorDBless's normal cleanup won't handle cleanup when the above is redefined. |
| 145 | // Note this must be registered after `WorDBless\Load::load()` registers the exit handler for `shutdown_action_hook`. |
| 146 | if ( $db_engine === 'sqlite' ) { |
| 147 | register_shutdown_function( array( self::class, 'rmrf' ), $tmpdir ); |
| 148 | } |
| 149 | } catch ( \Exception $e ) { |
| 150 | if ( $tmpdir ) { |
| 151 | self::rmrf( $tmpdir ); |
| 152 | } |
| 153 | throw new \RuntimeException( 'Failed to initialize WordPress test environment: ' . $e->getMessage() ); |
| 154 | } |
| 155 | |
| 156 | // For various tests using WorDBless, speed things up by reducing the password hashing cost. |
| 157 | add_filter( 'wp_hash_password_options', array( __CLASS__, 'reduce_password_cost' ) ); |
| 158 | |
| 159 | self::$initialized = true; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Hook for `wp_hash_password_options` to reduce the password hashing cost. |
| 164 | * |
| 165 | * Since WordPress 6.8, `wp_hash_password` uses PHP's `password_hash`, which has settings that use a lot |
| 166 | * of CPU power to hash passwords to make them harder to crack. We don't care about password security for |
| 167 | * the dummy users created in tests using WorDBless, so turn it way down to speed up the test runs. |
| 168 | * |
| 169 | * @param array $options Password options. |
| 170 | * @return array Modified options. |
| 171 | */ |
| 172 | public static function reduce_password_cost( $options ) { |
| 173 | $options['cost'] = 4; |
| 174 | return $options; |
| 175 | } |
| 176 | } |