Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Env: Download WordPress PHPUnit Into Container #41780

Merged
merged 17 commits into from
Jul 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
8 changes: 8 additions & 0 deletions packages/env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
41 changes: 33 additions & 8 deletions packages/env/lib/build-docker-compose-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -208,6 +230,7 @@ module.exports = function buildDockerComposeConfig( config ) {
environment: {
...dbEnv.credentials,
...dbEnv.development,
WP_TESTS_DIR: '/wordpress-phpunit',
},
volumes: developmentMounts,
},
Expand All @@ -218,6 +241,7 @@ module.exports = function buildDockerComposeConfig( config ) {
environment: {
...dbEnv.credentials,
...dbEnv.tests,
WP_TESTS_DIR: '/wordpress-phpunit',
},
volumes: testsMounts,
},
Expand All @@ -229,6 +253,7 @@ module.exports = function buildDockerComposeConfig( config ) {
environment: {
...dbEnv.credentials,
...dbEnv.development,
WP_TESTS_DIR: '/wordpress-phpunit',
},
},
'tests-cli': {
Expand All @@ -239,6 +264,7 @@ module.exports = function buildDockerComposeConfig( config ) {
environment: {
...dbEnv.credentials,
...dbEnv.tests,
WP_TESTS_DIR: '/wordpress-phpunit',
},
},
composer: {
Expand All @@ -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,
},
Expand Down
12 changes: 6 additions & 6 deletions packages/env/lib/commands/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const { spawn } = require( 'child_process' );
const initConfig = require( '../init-config' );

/**
* @typedef {import('../config').Config} Config
* @typedef {import('../config').WPConfig} WPConfig
*/

/**
Expand Down Expand Up @@ -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 = [
Expand Down
22 changes: 21 additions & 1 deletion packages/env/lib/commands/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to run this for every wp-env install? I wonder if we should instead have a top-level config option for including phpunit. That said, if this isn't causing any meaningful performance impact, we can probably leave it as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I considered this initially but instead decided to heavily optimize the develop checkout. It only downloads the phpunit files. I've also taken care to parallelize the download, so as far as performance goes, I've found this to be incredibly fast. It only downloads something like 21MB. For what it's worth, I'm pretty sure this is lighter than the phpunit container we're currently spinning up.

For me, this was a tradeoff between standardization and performance. Everyone who is using wp-env should be writing unit tests. All of these tests will likely require the PHPUnit files, so, I didn't see any good reasons not to include them by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good reason. The easier we can make unit tests to use, the better.

config,
{ development: wpVersions[ 0 ], tests: wpVersions[ 1 ] },
spinner,
debug
);
}

spinner.text = 'Starting WordPress.';
Expand Down
6 changes: 3 additions & 3 deletions packages/env/lib/download-sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@ 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
*/

/**
* 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 ) {
Expand Down
140 changes: 140 additions & 0 deletions packages/env/lib/download-wp-phpunit.js
Original file line number Diff line number Diff line change
@@ -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 );
}
Loading