diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 2043425127d67..082171ee01133 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Breaking Changes +- Removed the `WP_PHPUNIT__TESTS_CONFIG` environment variable from the `phpunit` container. **This removes automatic support for the `wp-phpunit/wp-phpunit` Composer package. To continue using the package, set the following two environment variables in your `phpunit.xml` file or similar: `WP_TESTS_DIR=""` and `WP_PHPUNIT__TESTS_CONFIG="/wordpress-phpunit/wp-tests-config.php"`.** +- Removed the generated `/var/www/html/phpunit-wp-config.php` file from the environment. + +### Enhancement +- Read WordPress' version and include the corresponding PHPUnit test files in the environment. +- Set the `WP_TESTS_DIR` environment variable in all containers to point at the PHPUnit test files. + ## 4.8.0 (2022-06-01) ### Enhancement - Removed the need for quotation marks when passing options to `wp-env run`. diff --git a/packages/env/README.md b/packages/env/README.md index 311db067fed9f..cc071ca2a07f6 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -188,6 +188,14 @@ wp-env start --debug ... ``` +## Using included WordPress PHPUnit test files + +Out of the box `wp-env` includes the [WordPress' PHPUnit test files](https://develop.svn.wordpress.org/trunk/tests/phpunit/) corresponding to the version of WordPress installed. There is an environment variable, `WP_TESTS_DIR`, which points to the location of these files within each container. By including these files in the environment, we remove the need for you to use a package or install and mount them yourself. If you do not want to use these files, you should ignore the `WP_TESTS_DIR` environment variable and load them from the location of your choosing. + +### Customizing the `wp-tests-config.php` file + +While we do provide a default `wp-tests-config.php` file within the environment, there may be cases where you want to use your own. WordPress provides a `WP_TESTS_CONFIG_FILE_PATH` constant that you can use to change the `wp-config.php` file used for testing. Set this to a desired path in your `bootstrap.php` file and the file you've chosen will be used instead of the one included in the environment. + ## Using Xdebug Xdebug is installed in the wp-env environment, but it is turned off by default. To enable Xdebug, you can use the `--xdebug` flag with the `wp-env start` command. Here is a reference to how the flag works: diff --git a/packages/env/lib/build-docker-compose-config.js b/packages/env/lib/build-docker-compose-config.js index 808d3b5dea294..1efe0e6035e8e 100644 --- a/packages/env/lib/build-docker-compose-config.js +++ b/packages/env/lib/build-docker-compose-config.js @@ -19,13 +19,18 @@ const { dbEnv } = require( './config' ); /** * Gets the volume mounts for an individual service. * - * @param {WPServiceConfig} config The service config to get the mounts from. - * @param {string} wordpressDefault The default internal path for the WordPress - * source code (such as tests-wordpress). + * @param {string} workDirectoryPath The working directory for wp-env. + * @param {WPServiceConfig} config The service config to get the mounts from. + * @param {string} wordpressDefault The default internal path for the WordPress + * source code (such as tests-wordpress). * * @return {string[]} An array of volumes to mount in string format. */ -function getMounts( config, wordpressDefault = 'wordpress' ) { +function getMounts( + workDirectoryPath, + config, + wordpressDefault = 'wordpress' +) { // Top-level WordPress directory mounts (like wp-content/themes) const directoryMounts = Object.entries( config.mappings ).map( ( [ wpDir, source ] ) => `${ source.path }:/var/www/html/${ wpDir }` @@ -45,9 +50,19 @@ function getMounts( config, wordpressDefault = 'wordpress' ) { config.coreSource ? config.coreSource.path : wordpressDefault }:/var/www/html`; + const corePHPUnitMount = `${ path.join( + workDirectoryPath, + wordpressDefault === 'wordpress' + ? 'WordPress-PHPUnit' + : 'tests-WordPress-PHPUnit', + 'tests', + 'phpunit' + ) }:/wordpress-phpunit`; + return [ ...new Set( [ coreMount, + corePHPUnitMount, ...directoryMounts, ...pluginMounts, ...themeMounts, @@ -64,8 +79,15 @@ function getMounts( config, wordpressDefault = 'wordpress' ) { * @return {Object} A docker-compose config object, ready to serialize into YAML. */ module.exports = function buildDockerComposeConfig( config ) { - const developmentMounts = getMounts( config.env.development ); - const testsMounts = getMounts( config.env.tests, 'tests-wordpress' ); + const developmentMounts = getMounts( + config.workDirectoryPath, + config.env.development + ); + const testsMounts = getMounts( + config.workDirectoryPath, + config.env.tests, + 'tests-wordpress' + ); // When both tests and development reference the same WP source, we need to // ensure that tests pulls from a copy of the files so that it maintains @@ -208,6 +230,7 @@ module.exports = function buildDockerComposeConfig( config ) { environment: { ...dbEnv.credentials, ...dbEnv.development, + WP_TESTS_DIR: '/wordpress-phpunit', }, volumes: developmentMounts, }, @@ -218,6 +241,7 @@ module.exports = function buildDockerComposeConfig( config ) { environment: { ...dbEnv.credentials, ...dbEnv.tests, + WP_TESTS_DIR: '/wordpress-phpunit', }, volumes: testsMounts, }, @@ -229,6 +253,7 @@ module.exports = function buildDockerComposeConfig( config ) { environment: { ...dbEnv.credentials, ...dbEnv.development, + WP_TESTS_DIR: '/wordpress-phpunit', }, }, 'tests-cli': { @@ -239,6 +264,7 @@ module.exports = function buildDockerComposeConfig( config ) { environment: { ...dbEnv.credentials, ...dbEnv.tests, + WP_TESTS_DIR: '/wordpress-phpunit', }, }, composer: { @@ -256,8 +282,7 @@ module.exports = function buildDockerComposeConfig( config ) { ], environment: { LOCAL_DIR: 'html', - WP_PHPUNIT__TESTS_CONFIG: - '/var/www/html/phpunit-wp-config.php', + WP_TESTS_DIR: '/wordpress-phpunit', ...dbEnv.credentials, ...dbEnv.tests, }, diff --git a/packages/env/lib/commands/run.js b/packages/env/lib/commands/run.js index 3179a388e8540..f944d8de84fa5 100644 --- a/packages/env/lib/commands/run.js +++ b/packages/env/lib/commands/run.js @@ -9,7 +9,7 @@ const { spawn } = require( 'child_process' ); const initConfig = require( '../init-config' ); /** - * @typedef {import('../config').Config} Config + * @typedef {import('../config').WPConfig} WPConfig */ /** @@ -42,11 +42,11 @@ module.exports = async function run( { container, command, spinner, debug } ) { /** * Runs an arbitrary command on the given Docker container. * - * @param {Object} options - * @param {string} options.container The Docker container to run the command on. - * @param {string} options.command The command to run. - * @param {Config} options.config The wp-env configuration. - * @param {Object} options.spinner A CLI spinner which indicates progress. + * @param {Object} options + * @param {string} options.container The Docker container to run the command on. + * @param {string} options.command The command to run. + * @param {WPConfig} options.config The wp-env configuration. + * @param {Object} options.spinner A CLI spinner which indicates progress. */ function spawnCommandDirectly( { container, command, config, spinner } ) { const composeCommand = [ diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 0cca813c1c7d3..066754c6e5114 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -21,16 +21,18 @@ const retry = require( '../retry' ); const stop = require( './stop' ); const initConfig = require( '../init-config' ); const downloadSources = require( '../download-sources' ); +const downloadWPPHPUnit = require( '../download-wp-phpunit' ); const { checkDatabaseConnection, configureWordPress, setupWordPressDirectories, + readWordPressVersion, } = require( '../wordpress' ); const { didCacheChange, setCache } = require( '../cache' ); const md5 = require( '../md5' ); /** - * @typedef {import('../config').Config} Config + * @typedef {import('../config').WPConfig} WPConfig */ const CONFIG_CACHE_KEY = 'config_checksum'; @@ -127,7 +129,25 @@ module.exports = async function start( { spinner, debug, update, xdebug } ) { ] ); if ( shouldConfigureWp ) { + spinner.text = 'Setting up WordPress directories'; + await setupWordPressDirectories( config ); + + // Use the WordPress versions to download the PHPUnit suite. + const wpVersions = await Promise.all( [ + readWordPressVersion( + config.env.development.coreSource, + spinner, + debug + ), + readWordPressVersion( config.env.tests.coreSource, spinner, debug ), + ] ); + await downloadWPPHPUnit( + config, + { development: wpVersions[ 0 ], tests: wpVersions[ 1 ] }, + spinner, + debug + ); } spinner.text = 'Starting WordPress.'; diff --git a/packages/env/lib/download-sources.js b/packages/env/lib/download-sources.js index 537389317b058..aec47f23203d1 100644 --- a/packages/env/lib/download-sources.js +++ b/packages/env/lib/download-sources.js @@ -16,7 +16,7 @@ const extractZip = util.promisify( require( 'extract-zip' ) ); const rimraf = util.promisify( require( 'rimraf' ) ); /** - * @typedef {import('./config').Config} Config + * @typedef {import('./config').WPConfig} WPConfig * @typedef {import('./config').WPSource} WPSource */ @@ -24,8 +24,8 @@ const rimraf = util.promisify( require( 'rimraf' ) ); * Download each source for each environment. If the same source is used in * multiple environments, it will only be downloaded once. * - * @param {Config} config The wp-env configuration object. - * @param {Object} spinner The spinner object to show progress. + * @param {WPConfig} config The wp-env configuration object. + * @param {Object} spinner The spinner object to show progress. * @return {Promise} Returns a promise which resolves when the downloads finish. */ module.exports = function downloadSources( config, spinner ) { diff --git a/packages/env/lib/download-wp-phpunit.js b/packages/env/lib/download-wp-phpunit.js new file mode 100644 index 0000000000000..923750d58b1d7 --- /dev/null +++ b/packages/env/lib/download-wp-phpunit.js @@ -0,0 +1,140 @@ +'use strict'; +/** + * External dependencies + */ +const SimpleGit = require( 'simple-git' ); +const fs = require( 'fs' ); +const path = require( 'path' ); + +/** + * @typedef {import('./config').WPConfig} WPConfig + */ + +/** + * Downloads the WordPress PHPUnit files for each environment into the appropriate directories. + * + * @param {WPConfig} config The wp-env config object. + * @param {Object} wpVersions The WordPress versions for each environment. + * @param {Object} spinner The spinner object to show progress. + * @param {boolean} debug Indicates whether or not debug mode is active. + * @return {Promise} Returns a promise which resolves when the downloads finish. + */ +module.exports = function downloadWPPHPUnit( + config, + wpVersions, + spinner, + debug +) { + const progresses = {}; + const getProgressSetter = ( id ) => ( progress ) => { + progresses[ id ] = progress; + spinner.text = + 'Downloading WordPress PHPUnit Suite.\n' + + Object.entries( progresses ) + .map( + ( [ key, value ] ) => + ` - ${ key }: ${ ( value * 100 ).toFixed( 0 ) }/100%` + ) + .join( '\n' ); + }; + + const promises = []; + for ( const env in config.env ) { + const wpVersion = wpVersions[ env ] ? wpVersions[ env ] : null; + const directory = path.join( + config.workDirectoryPath, + env === 'development' + ? 'WordPress-PHPUnit' + : 'tests-WordPress-PHPUnit' + ); + promises.push( + downloadTestSuite( directory, wpVersion, { + onProgress: getProgressSetter, + spinner, + debug, + } ) + ); + } + + return Promise.all( promises ); +}; + +/** + * Downloads the PHPUnit tests for a given WordPress version into the appropriate directory. + * + * @param {string} directory The directory to place the PHPUnit tests in. + * @param {string} wpVersion The version of WordPress to install PHPUnit tests for. Trunk when empty. + * @param {Object} options + * @param {Function} options.onProgress A function called with download progress. Will be invoked with one argument: a number that ranges from 0 to 1 which indicates current download progress for this source. + * @param {Object} options.spinner A CLI spinner which indicates progress. + * @param {boolean} options.debug True if debug mode is enabled. + */ +async function downloadTestSuite( + directory, + wpVersion, + { onProgress, spinner, debug } +) { + const log = debug + ? ( message ) => { + spinner.info( `SimpleGit: ${ message }` ); + spinner.start(); + } + : () => {}; + onProgress( 0 ); + + const progressHandler = ( { progress } ) => { + onProgress( progress / 100 ); + }; + + // Make sure that the version is in X.X.X format. This is required + // because WordPress/wordpress-develop uses X.X.X tags but + // WordPress uses X.X for non-patch releases. + if ( wpVersion && wpVersion.match( /^[0-9]+.[0-9]+$/ ) ) { + wpVersion += '.0'; + } + + log( 'Cloning or getting the PHPUnit suite from GitHub.' ); + const git = SimpleGit( { progress: progressHandler } ); + + const isRepo = + fs.existsSync( directory ) && + ( await git.cwd( directory ).checkIsRepo( 'root' ) ); + + if ( isRepo ) { + log( 'Repo already exists, using it.' ); + } else { + await git.clone( + 'https://github.com/WordPress/wordpress-develop.git', + directory, + { + '--depth': '1', + '--no-checkout': null, + } + ); + await git.cwd( directory ); + + // We use a sparse checkout to minimize the amount of data we need to download. + log( 'Enabling sparse checkout.' ); + await git.raw( 'sparse-checkout', 'set', '--cone', 'tests/phpunit' ); + } + + // Figure out the ref that we need to checkout to get the correct version of the PHPUnit library. + // Alpha, Beta, and RC versions are bleeding edge and should pull from trunk. + let ref; + const fetchRaw = []; + if ( ! wpVersion || wpVersion.match( /-(?:alpha|beta|rc)/ ) ) { + ref = 'trunk'; + fetchRaw.push( 'fetch', 'origin', ref, '--depth', '1' ); + } else { + ref = `tags/${ wpVersion }`; + fetchRaw.push( 'fetch', 'origin', 'tag', wpVersion, '--depth', '1' ); + } + + log( `Fetching ${ ref }.` ); + await git.raw( fetchRaw ); + + log( `Checking out ${ ref }.` ); + await git.checkout( ref ); + + onProgress( 1 ); +} diff --git a/packages/env/lib/wordpress.js b/packages/env/lib/wordpress.js index 133524bd72bfe..62360b4c182e7 100644 --- a/packages/env/lib/wordpress.js +++ b/packages/env/lib/wordpress.js @@ -14,6 +14,7 @@ const copyDir = util.promisify( require( 'copy-dir' ) ); /** * @typedef {import('./config').WPConfig} WPConfig * @typedef {import('./config').WPServiceConfig} WPServiceConfig + * @typedef {import('./config').WPSource} WPSource * @typedef {'development'|'tests'} WPEnvironment * @typedef {'development'|'tests'|'all'} WPEnvironmentSelection */ @@ -101,25 +102,15 @@ async function configureWordPress( environment, config, spinner ) { } ); - /** - * Since wp-phpunit loads wp-settings.php at the end of its wp-config.php - * file, we need to avoid loading it too early in our own wp-config.php. If - * we load it too early, then some things (like MULTISITE) will be defined - * before wp-phpunit has a chance to configure them. To avoid this, create a - * copy of wp-config.php for phpunit which doesn't require wp-settings.php. - * - * Note that This needs to be executed using `exec` on the wordpress service - * so that file permissions work properly. - * - * This will be removed in the future. @see https://github.com/WordPress/gutenberg/issues/23171 - * - */ + // WordPress' PHPUnit suite expects a `wp-tests-config.php` in + // the directory that the test suite is contained within. + // Make sure ABSPATH points to the WordPress install. await dockerCompose.exec( environment === 'development' ? 'wordpress' : 'tests-wordpress', [ 'sh', '-c', - 'sed "/^require.*wp-settings.php/d" /var/www/html/wp-config.php > /var/www/html/phpunit-wp-config.php && chmod 777 /var/www/html/phpunit-wp-config.php', + `sed -e "/^require.*wp-settings.php/d" -e "s/define( 'ABSPATH', __DIR__ . '\\/' );/define( 'ABSPATH', '\\/var\\/www\\/html\\/' );\\n\\tdefine( 'WP_DEFAULT_THEME', 'default' );/" /var/www/html/wp-config.php > /wordpress-phpunit/wp-tests-config.php`, ], { config: config.dockerComposeConfigPath, @@ -246,10 +237,49 @@ async function copyCoreFiles( fromPath, toPath ) { } ); } +/** + * Scans through a WordPress source to find the version of WordPress it contains. + * + * @param {WPSource} coreSource The WordPress source. + * @param {Object} spinner A CLI spinner which indicates progress. + * @param {boolean} debug Indicates whether or not the CLI is in debug mode. + * @return {string} The version of WordPress the source is for. + */ +async function readWordPressVersion( coreSource, spinner, debug ) { + // No source means they're using the bleeding edge. + if ( coreSource === null ) { + return null; + } + + const versionFilePath = path.join( + coreSource.path, + 'wp-includes', + 'version.php' + ); + const versionFile = await fs.readFile( versionFilePath, { + encoding: 'utf-8', + } ); + const versionMatch = versionFile.match( + /\$wp_version = '([A-Za-z\-0-9.]+)'/ + ); + if ( ! versionMatch ) { + throw new Error( `Failed to find version in ${ versionFilePath }` ); + } + + if ( debug ) { + spinner.info( + `Found WordPress ${ versionMatch[ 1 ] } in ${ versionFilePath }.` + ); + } + + return versionMatch[ 1 ]; +} + module.exports = { hasSameCoreSource, checkDatabaseConnection, configureWordPress, resetDatabase, setupWordPressDirectories, + readWordPressVersion, }; diff --git a/packages/env/test/build-docker-compose-config.js b/packages/env/test/build-docker-compose-config.js index 3b4ce2f5067cd..5927e188c9df0 100644 --- a/packages/env/test/build-docker-compose-config.js +++ b/packages/env/test/build-docker-compose-config.js @@ -26,11 +26,13 @@ describe( 'buildDockerComposeConfig', () => { ], }; const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', env: { development: envConfig, tests: envConfig }, } ); const { volumes } = dockerConfig.services.wordpress; expect( volumes ).toEqual( [ 'wordpress:/var/www/html', // WordPress root. + '/path/WordPress-PHPUnit/tests/phpunit:/wordpress-phpunit', // WordPress test library, '/path/to/wp-plugins:/var/www/html/wp-content/plugins', // Mapped plugins root. '/path/to/local/plugin:/var/www/html/wp-content/plugins/test-name', // Mapped plugin. ] ); @@ -52,6 +54,7 @@ describe( 'buildDockerComposeConfig', () => { ], }; const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', env: { development: envConfig, tests: envConfig }, } ); const devVolumes = dockerConfig.services.wordpress.volumes; @@ -62,13 +65,20 @@ describe( 'buildDockerComposeConfig', () => { const testsCliVolumes = dockerConfig.services[ 'tests-cli' ].volumes; expect( testsVolumes ).toEqual( testsCliVolumes ); - const localSources = [ + let localSources = [ '/path/to/wp-plugins:/var/www/html/wp-content/plugins', + '/path/WordPress-PHPUnit/tests/phpunit:/wordpress-phpunit', '/path/to/local/plugin:/var/www/html/wp-content/plugins/test-name', '/path/to/local/theme:/var/www/html/wp-content/themes/test-theme', ]; - expect( devVolumes ).toEqual( expect.arrayContaining( localSources ) ); + + localSources = [ + '/path/to/wp-plugins:/var/www/html/wp-content/plugins', + '/path/tests-WordPress-PHPUnit/tests/phpunit:/wordpress-phpunit', + '/path/to/local/plugin:/var/www/html/wp-content/plugins/test-name', + '/path/to/local/theme:/var/www/html/wp-content/themes/test-theme', + ]; expect( testsVolumes ).toEqual( expect.arrayContaining( localSources ) ); @@ -84,10 +94,12 @@ describe( 'buildDockerComposeConfig', () => { }, }; const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', env: { development: envConfig, tests: envConfig }, } ); const expectedVolumes = [ 'tests-wordpress:/var/www/html', + '/path/tests-WordPress-PHPUnit/tests/phpunit:/wordpress-phpunit', '/path/to/wp-uploads:/var/www/html/wp-content/uploads', ]; expect( dockerConfig.services.phpunit.volumes ).toEqual( @@ -105,10 +117,12 @@ describe( 'buildDockerComposeConfig', () => { }, }; const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', env: { development: envConfig, tests: CONFIG }, } ); const expectedVolumes = [ 'tests-wordpress:/var/www/html', + '/path/tests-WordPress-PHPUnit/tests/phpunit:/wordpress-phpunit', 'phpunit-uploads:/var/www/html/wp-content/uploads', ]; expect( dockerConfig.services.phpunit.volumes ).toEqual( @@ -121,6 +135,7 @@ describe( 'buildDockerComposeConfig', () => { // local filesystem, so a volume should be created to contain core // sources. const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', env: { development: CONFIG, tests: CONFIG }, } ); @@ -140,6 +155,7 @@ describe( 'buildDockerComposeConfig', () => { }; const dockerConfig = buildDockerComposeConfig( { + workDirectoryPath: '/path', env: { development: envConfig, tests: envConfig }, } ); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 220581708552d..4f822fe342b24 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,4 +19,8 @@ ms-required + + + + diff --git a/phpunit/multisite.xml b/phpunit/multisite.xml index 168d4cb9e1136..e4b78091c1324 100644 --- a/phpunit/multisite.xml +++ b/phpunit/multisite.xml @@ -16,4 +16,8 @@ ms-excluded + + + +