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: Better way to expose phpunit #22365

Closed
wants to merge 1 commit into from
Closed
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,15 @@
"dev:packages": "node ./bin/packages/watch.js",
"docs:build": "node ./docs/tool/index.js && node ./bin/api-docs/update-api-docs.js",
"fixtures:clean": "rimraf \"packages/e2e-tests/fixtures/blocks/*.+(json|serialized.html)\"",
"fixtures:server-registered": "packages/env/bin/wp-env run wordpress ./wp-content/plugins/gutenberg/bin/get-server-blocks.php > test/integration/full-content/server-registered.json",
"fixtures:server-registered": "packages/env/bin/wp-env run wordpress './get-server-blocks.php' ./bin > test/integration/full-content/server-registered.json",
"fixtures:generate": "npm run fixtures:server-registered && cross-env GENERATE_MISSING_FIXTURES=y npm run test-unit",
"fixtures:regenerate": "npm run fixtures:clean && npm run fixtures:generate",
"format-js": "wp-scripts format-js",
"lint": "concurrently \"npm run lint-lockfile\" \"npm run lint-js\" \"npm run lint-pkg-json\" \"npm run lint-css\"",
"lint-js": "wp-scripts lint-js",
"lint-js:fix": "npm run lint-js -- --fix",
"prelint-php": "npm run wp-env run composer install -- --no-interaction",
"lint-php": "npm run wp-env run composer run-script lint",
"prelint-php": "npm run wp-env run 'composer install --no-interaction'",
"lint-php": "npm run wp-env run 'composer run-script lint'",
"lint-pkg-json": "wp-scripts lint-pkg-json . 'packages/*/package.json'",
"lint-lockfile": "node ./bin/validate-package-lock.js",
"lint-css": "wp-scripts lint-style '**/*.scss'",
Expand Down
6 changes: 6 additions & 0 deletions packages/env/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@

### New Feature

- Add cwd option to `wp-env run` command to support running commands in Docker relative to a mapped source.
- A new service is now available for running phpunit commands.
- You may now mount local directories to any location within the WordPress install. For example, you may specify `"wp-content/mu-plugins": "./path/to/mu-plugins"` to add mu-plugins.

### Breaking Changes

- You must now wrap multi-word commands for `wp-env run` in quotation marks for them to be passed to docker. Before: `wp-env run cli wp user list`. After: `wp-env run cli "wp user list"`.

## 1.1.0 (2020-04-01)

### New Feature
Expand Down
74 changes: 68 additions & 6 deletions packages/env/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,21 +213,31 @@ Positionals:
### `wp-env run [container] [command]`

```sh
wp-env run <container> [command..]

Runs an arbitrary command in one of the underlying Docker containers, for
example it's useful for running wp cli commands.
wp-env run <container> <command> [cwd]

Runs an arbitrary command in one of the underlying Docker containers. For
example, it is useful for running wp cli commands and phpunit.

Positionals:
container The container to run the command on. [string] [required]
command The command to run. [array] [default: []]
command The command to run. Wrap it in quotation marks if there are
multiple words or options to pass into the Docker service.
[string] [required]
cwd An optional local path to a mapped directory in which to run the
command. This is useful if you want to run a command within a
Docker service relative to one of your mapped sources. [string]

Options:
--help Show help [boolean]
--version Show version number [boolean]
--debug Enable debug output. [boolean] [default: false]
```

For example:

```sh
wp-env run cli wp user list
# Get the users in the WordPress instance
wp-env run cli "wp user list"
⠏ Running `wp user list` in 'cli'.

ID user_login display_name user_email user_registered roles
Expand All @@ -236,6 +246,31 @@ ID user_login display_name user_email user_registered roles
✔ Ran `wp user list` in 'cli'. (in 2s 374ms)
```

```sh
# Run phpunit
wp-env run phpunit phpunit ./

⠏ Running `phpunit` in 'phpunit'.

Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.

............................................................... 63 / 101 ( 62%)
...................................... 101 / 101 (100%)

Time: 4.44 seconds, Memory: 40.50 MB

OK (101 tests, 1801 assertions)



✔ Ran `phpunit` in 'phpunit'. (in 5s 670ms)
```

### `docker logs -f [container_id] >/dev/null`

```sh
Expand Down Expand Up @@ -364,4 +399,31 @@ You can tell `wp-env` to use a custom port number so that your instance does not
}
```

## Phpunit

You can run phpunit in the Docker environment with the WordPress phpunit test functions with these instructions. Note that all of the files we mention here should be included in one of the sources which is mapped into the WordPress instance. This setup depends on WP PHPUnit. [Find more information about that here](https://github.com/wp-phpunit/docs).

1. Use composer to install WP PHPUnit: `wp-env run composer "composer require wp-phpunit/wp-phpunit --dev"`.
2. Specify a `phpunit.dist.xml` within one of your mapped sources. For example, if your main source is a plugin in cwd (`"plugins": [ "." ]`), you could put `phpunit.dist.xml` in the top-level directory. [Here is an example of a `phpunit.dist.xml` file.](https://github.com/WordPress/gutenberg/blob/master/phpunit.xml.dist)
3. Make sure `phpunit.dist.xml` points to a local `bootstrap.php` file. This should require composer dependencies and also attempt to load the WordPress test functions. Below is the minimal PHP to make it work:

```php
<?php

// See https://github.com/WordPress/gutenberg/blob/master/phpunit/bootstrap.php
// for more information.

// 1. Require composer dependencies.
require_once dirname( dirname( __FILE__ ) ) . '/vendor/autoload.php';

// 2. Require the WP_PHPUNIT library, which provides WP test helpers.
$_tests_dir = getenv( 'WP_PHPUNIT__DIR' );
require_once $_tests_dir . '/includes/functions.php';

// 3. Start the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php';
```

4. Call `wp-env run phpunit phpunit $path_to_config_dir`. For example, `$path_to_config_dir` can be `./` if you placed the phpunit config file in the current working directory. Looking at the peices, this command simply runs the `phpunit` command on the `phpunit` service in the specified directory. The directory should be specified relative to cwd of the wp-env command. Under the hood, wp-env locates the mapped version of that path inside the Docker container.

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
14 changes: 12 additions & 2 deletions packages/env/lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,25 @@ module.exports = function cli() {
} );
args.positional( 'command', {
type: 'string',
describe: 'The command to run.',
describe:
'The command to run. Wrap it in quotation marks if there are multiple words or options to pass into the Docker service.',
} );
args.positional( 'cwd', {
type: 'string',
describe:
'An optional local path to a mapped directory in which to run the command. This is useful if you want to run a command within a Docker service relative to one of your mapped sources.',
} );
},
withSpinner( env.run )
);
yargs.example(
'$0 run cli wp user list',
'$0 run cli "wp user list"',
'Runs `wp user list` wp-cli command which lists WordPress users.'
);
yargs.example(
'$0 run phpunit phpunit ./',
'Runs the phpunit command in the phpunit service. "./" points at the local directory containing the phpunit config file, which in this case is the current working directory.'
);

return yargs;
};
26 changes: 23 additions & 3 deletions packages/env/lib/commands/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const dockerCompose = require( 'docker-compose' );
* Internal dependencies
*/
const initConfig = require( '../init-config' );
const getDockerPath = require( '../get-docker-path' );

/**
* Runs an arbitrary command on the given Docker container.
Expand All @@ -17,17 +18,36 @@ const initConfig = require( '../init-config' );
* @param {Object} options.spinner A CLI spinner which indicates progress.
* @param {boolean} options.debug True if debug mode is enabled.
*/
module.exports = async function run( { container, command, spinner, debug } ) {
module.exports = async function run( {
container,
command,
cwd,
spinner,
debug,
} ) {
const config = await initConfig( { spinner, debug } );

command = command.join( ' ' );
command = Array.isArray( command ) ? command.join( ' ' ) : command;

spinner.text = `Running \`${ command }\` in '${ container }'.`;

const commandOptions = [ '--rm' ];
if ( cwd ) {
// If a cwd for the command was passed, turn it into an internal absolute
// path. As given, it is relative to the local filesystem, not docker.
const internalPath = getDockerPath( config, cwd );
if ( ! internalPath ) {
throw new Error(
'Could not convert the given work directory into an internal Docker path.'
);
}
commandOptions.push( [ '-w', internalPath ] );
}

const result = await dockerCompose.run( container, command, {
config: config.dockerComposeConfigPath,
commandOptions: [ '--rm' ],
log: config.debug,
commandOptions,
} );

if ( result.out ) {
Expand Down
94 changes: 94 additions & 0 deletions packages/env/lib/get-docker-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* External dependencies
*/
const { resolve } = require( 'path' );

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

/**
* @callback pathFinder
* @return {?string} If found, an absolute path which works in the docker image.
*/

/**
* Given a local path contained by one of the sources, returns the internal path.
*
* Note that the given local path must be included in the docker environment
* already via one of the sources. The local path is for your local filesystem,
* and the the internal path is for Docker filesystem.
*
* We look through plugins, themes, mappings, and the core source in that order
* until finding the source which is a parent directory of the given path. We
* then generate an absolute path based on the parent source which works in the
* docker image.
*
* @param {Config} config Parsed wp-env config object.
* @param {string} path The local path which should be converted.
* @return {?string} If found, the path to the
*/
module.exports = function getDockerPath( config, path ) {
path = resolve( path );
const pathFinders = [
getPathFinder( path, config.pluginSources, true, 'wp-content/plugins' ),
getPathFinder( path, config.themeSources, true, 'wp-content/themes' ),
getPathFinder( path, config.mappings ),
getPathFinder( path, [ config.coreSource ], false, '' ),
];
while ( pathFinders.length ) {
// Removes the next path finder and calls it.
const result = pathFinders.shift()();
if ( result ) {
return result;
}
}
return null;
};

/**
* Returns a function which computes the internal path of searchPath.
*
* Returns an absolute path which works in the docker container if the
* searchPath is a child of one of the sources. Returns undefined if none of the
* sources contain the searchPath.
*
* @param {string} searchPath The path value whose parent we are trying to find.
* @param {Source[]|Object.<string, Source>} sources A collection of sources to
* look through.
* @param {boolean} withBasename If true, include the parent source's basename
* in the returned internal docker path.
* @param {string} wpPath If set, used for the internal WordPress path.
* If not set, the key of the source in the sources
* collection is assumed to be the internal path.
*
* @return {pathFinder} A function which when called, tries to compute the path.
*/
function getPathFinder(
searchPath,
sources,
withBasename = false,
wpPath = null
) {
return () => {
// If sources is an array, maybeWpPath is an array index. So we used the
// the passed wpPath in that scenario.
for ( const [ maybeWpPath, { path, basename } ] of Object.entries(
sources
) ) {
// This works because searchPath and path are both absolute paths
// for the local filesystem, so searchPath will start with the
// entirety of the absolute path of any of its parent directories.
if ( searchPath.startsWith( path ) ) {
const internalPath = wpPath === null ? maybeWpPath : wpPath;
const relativePath = searchPath.substring( path.length );

return `/var/www/html/${ internalPath }${
withBasename ? `/${ basename }` : ''
}${ relativePath }`;
}
}
return null;
};
}