Skip to content

Commit

Permalink
Merge pull request #147 from woocommerce/24-03/local-test-env-docs
Browse files Browse the repository at this point in the history
Local test env tweaks
  • Loading branch information
Luc45 authored Mar 15, 2024
2 parents 091c0b0 + c389172 commit 6d3934e
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 61 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,36 @@ Official tool to streamline the testing of plugins and themes, ensuring they mee

You can use these parameters individually or in combination to create different scenarios for your tests. Run `qit run:<test-type> --help` to see all the available options. Different test types will have different options to choose from.

## Local Test Environment

QIT ships with a powerful, flexible testing environment for WordPress plugin and theme developers.

It provides an easy-to-use, ephemeral local testing setup, allowing for rapid testing and debugging without affecting your main development environment. This feature is available to all users, regardless of whether you are a Partner Developer of the Woo.com Marketplace.

#### Usage

1. **Basic Setup**: To quickly start an environment, run `qit env:up` in your terminal.
2. **Configurable Environment**: `qit env:up --wordpress_version=rc --php_version=8.3 --plugins=woocommerce --themes=storefront`
2. **Custom Configuration**: Use command-line options or create a JSON/YAML configuration file in your project directory to set default environment options. Example configurations are provided in the help section.

Or just place a `qit-env.yml` file in your directory and do `qit env:up` to start the environment.

```yaml
wordpress_version: rc
php_version: 8.3
plugins:
- woocommerce
themes:
- storefront
```
More information: [Local Test Environment Documentation](https://woocommerce.github.io/qit-documentation/#/environment/getting-started)
## Can I use QIT?
QIT is currently only available for plugins and themes in the Woo.com Marketplace, but we have plans to open it to all developers in the future.
Most features of QIT requires you to log-in as a Partner Developer of the Woo.com Marketplace, but we have plans to open it to all developers in the future.
The QIT Local Test Environment does not require you to be connected to Woo.com, although to install Woo.com Premium plugins and themes on your test environment you will need to be connected as a Partner Developer of the Woo.com Marketplace (and have access to the extensions you want to test).
## Support
Expand Down
Binary file modified qit
Binary file not shown.
2 changes: 1 addition & 1 deletion src/src/Commands/DynamicCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ protected function validate_required_options( InputInterface $input ): void {
/**
* @param InputInterface $input
*
* @return array<mixed> The options to send.
* @return array<string,scalar|array<scalar>> The keys are option names, the values are the option values. It can be boolean if option is boolean, scalar if it's a value, or array if option is an array.
*/
protected function parse_options( InputInterface $input ): array {
$this->validate_required_options( $input );
Expand Down
101 changes: 47 additions & 54 deletions src/src/Commands/Environment/UpEnvironmentCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,18 @@ protected function configure() {

DynamicCommandCreator::add_schema_to_command( $this, $schemas['e2e'], [], [
'wordpress_version',
'woocommerce_version',
'php_version',
] );

$this
->setDescription( 'Creates a temporary local test environment that is completely ephemeral — no data is persisted. Every time you stop and restart the environment, it\'s like starting fresh.' )
->addOption( 'plugins', 'p', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, '(Optional) Plugin to activate in the environment. Accepts paths, Woo.com slugs/product IDs, WordPress.org slugs or GitHub URLs.', [] )
->addOption( 'themes', 't', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, '(Optional) Theme install, if multiple provided activates the last. Accepts paths, Woo.com slugs/product IDs, WordPress.org slugs or GitHub URLs.', [] )
->addOption( 'volumes', 'm', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, '(Optional) Additional volume mappings, eg: /home/mycomputer/my-plugin:/var/www/html/wp-content/plugins/my-plugin.', [] )
->addOption( 'volumes', 'l', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, '(Optional) Additional volume mappings, eg: /home/mycomputer/my-plugin:/var/www/html/wp-content/plugins/my-plugin.', [] )
->addOption( 'php_extensions', 'x', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'PHP extensions to install in the environment.', [] )
->addOption( 'requires', 'r', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Load PHP file before running the command (may be used more than once).' )
->addOption( 'object_cache', 'o', InputOption::VALUE_NONE, '(Optional) Whether to enable Object Cache (Redis) in the environment.' )
->addOption( 'skip_activating_plugins', 's', InputOption::VALUE_NONE, 'Skip activating plugins in the environment.' )
->addOption( 'require', 'r', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Load PHP file before running the command (may be used more than once).' )
->addOption( 'json', 'j', InputOption::VALUE_NEGATABLE, 'Whether to return raw JSON format.', false )
// ->addOption( 'attached', 'a', InputOption::VALUE_NONE, 'Whether to attach to the environment after starting it.' )
->setAliases( [ 'env:start' ]
Expand All @@ -63,6 +62,7 @@ protected function configure() {
$this->add_option_to_send( 'plugins' );
$this->add_option_to_send( 'themes' );
$this->add_option_to_send( 'volumes' );
$this->add_option_to_send( 'requires' );
$this->add_option_to_send( 'php_extensions' );
$this->add_option_to_send( 'object_cache' );

Expand Down Expand Up @@ -96,7 +96,6 @@ protected function configure() {
];
break;
case 'wordpress_version':
case 'woocommerce_version':
$options_example[ $opt->getName() ] = 'rc';
break;
default:
Expand Down Expand Up @@ -148,10 +147,6 @@ protected function configure() {
To set the WordPress version, use the --wordpress_version flag, e.g.:
<info>qit env:up --wordpress_version=rc</info>
<comment>WooCommerce Version</comment>
To set the WooCommerce version, use the --woocommerce_version flag, e.g.:
<info>qit env:up --woocommerce_version=rc</info>
<comment>Object Cache</comment>
To enable Object Cache (Redis) in the environment, use the --object_cache flag, e.g.:
<info>qit env:up --object_cache</info>
Expand Down Expand Up @@ -180,9 +175,9 @@ protected function configure() {
- URL provided at command completion. Default: "http://localhost:<RANDOM_PORT>"
<comment>Example:</comment>
<info>qit env:up --wordpress_version=rc --woocommerce_version=rc --php_version=8.3 --php_extensions=gd --object_cache --plugins gutenberg --plugins automatewoo --themes storefront</info>
<info>qit env:up --wordpress_version=rc --php_version=8.3 --php_extensions=gd --object_cache --plugins gutenberg --plugins automatewoo --themes storefront</info>
This will create a disposable test environment with the latest release candidate versions of WordPress and WooCommerce, PHP 8.3, the GD extension, Object Cache enabled, Gutenberg from WordPress.org Plugin Repository and AutomateWoo from the Woo.com Marketplace installed and active, and Storefront installed.
This will create a disposable test environment with the latest release candidate versions of WordPress, PHP 8.3, the GD extension, Object Cache enabled, Gutenberg from WordPress.org Plugin Repository and AutomateWoo from the Woo.com Marketplace installed and active, and Storefront installed.
HELP
);
}
Expand All @@ -192,44 +187,51 @@ protected function execute( InputInterface $input, OutputInterface $output ): in
$output->writeln( '<comment>Warning: It is highly recommended to run this script from Windows Subsystem for Linux (WSL) when using Windows.</comment>' );
}

// Load custom PHP scripts, if any.
if ( ! empty( $input->getOption( 'require' ) ) ) {
foreach ( $input->getOption( 'require' ) as $file ) {
if ( file_exists( $file ) ) {
if ( $output->isVerbose() ) {
$output->writeln( sprintf( 'Loading file %s', $file ) );
}
require $file;
} else {
$output->writeln( sprintf( '<error>File %s does not exist.</error>', $file ) );

return Command::FAILURE;
}
}
}

try {
$options = $this->parse_options( $input );
$options_to_env_info = $this->parse_options( $input );
} catch ( \Exception $e ) {
$output->writeln( sprintf( '<error>%s</error>', $e->getMessage() ) );

return Command::FAILURE;
}

if ( $input->getOption( 'skip_activating_plugins' ) ) {
$this->e2e_environment->set_skip_activating_plugins( true );
}

$env_info = App::make( EnvConfigLoader::class )->init_env_info( $options_to_env_info );

if ( $output->isVeryVerbose() ) {
// Print the current options being used.
$output->writeln( sprintf( 'Starting environment with options: %s', json_encode( $options ) ) );
$this->output->writeln( 'Environment info: ' . json_encode( $env_info, JSON_PRETTY_PRINT ) );
}

if ( $input->getOption( 'skip_activating_plugins' ) ) {
$this->e2e_environment->set_skip_activating_plugins( true );
$this->e2e_environment->init( $env_info );
$this->e2e_environment->up();

if ( $input->getOption( 'json' ) ) {
$output->write( json_encode( $env_info ) );
}

// Print the site URL as the last information for easy programmatic integrations.
$output->writeln( $env_info->site_url );

return Command::SUCCESS;
}

protected function parse_options( InputInterface $input ): array {
$options = parent::parse_options( $input );

$options_to_env_info = [
'defaults' => [],
'overrides' => [],
];

$shortcuts = [];

foreach ( $this->getDefinition()->getOptions() as $o ) {
$shortcuts[ $o->getShortcut() ] = $o->getName();
}

/*
* Options can be explicitly set by the user or be a default value.
*
Expand All @@ -239,33 +241,24 @@ protected function execute( InputInterface $input, OutputInterface $output ): in
* 2: Option in config file (will be in .?qit-env.(json|yml))
* 3. Default value
*/
foreach ( $options as $k => &$v ) {
// Todo: Add support for shortcuts as well.
foreach ( $GLOBALS['argv'] as $a ) {
if ( $a === "--$k" ) {
$options_to_env_info['overrides'][ $k ] = $v;
continue 2;
}
}
$options_to_env_info['defaults'][ $k ] = $v;
}

$env_info = App::make( EnvConfigLoader::class )->init_env_info( $options_to_env_info );
foreach ( $options as $key => $value ) {
$found_override = false;

if ( $output->isVeryVerbose() ) {
$this->output->writeln( 'Environment info: ' . json_encode( $env_info, JSON_PRETTY_PRINT ) );
}
foreach ( $GLOBALS['argv'] as $arg ) {
$normalized_arg = ltrim( $arg, '-' );

$this->e2e_environment->init( $env_info );
$this->e2e_environment->up();
if ( $normalized_arg === $key || ( isset( $shortcuts[ $normalized_arg ] ) && $shortcuts[ $normalized_arg ] === $key ) ) {
$options_to_env_info['overrides'][ $key ] = $value;
$found_override = true;
break;
}
}

if ( $input->getOption( 'json' ) ) {
$output->write( json_encode( $env_info ) );
if ( ! $found_override ) {
$options_to_env_info['defaults'][ $key ] = $value;
}
}

// Print the site URL as the last information for easy programmatic integrations.
$output->writeln( $env_info->site_url );

return Command::SUCCESS;
return $options_to_env_info;
}
}
66 changes: 62 additions & 4 deletions src/src/Environment/EnvConfigLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,79 @@ public function init_env_info( array $options = [
}

// Plugins.
foreach ( $env_config['plugins'] ?? [] as &$plugin ) {
foreach ( $env_config['plugins'] ?? [] as $plugin ) {
// If it doesn't exist, we download it.
if ( file_exists( $plugin ) ) {
$env_config['volumes'][] = $plugin . ':/var/www/html/wp-content/plugins/' . basename( $plugin );
unset( $plugin );
}
}

// Themes.
foreach ( $env_config['themes'] ?? [] as &$theme ) {
foreach ( $env_config['themes'] ?? [] as $theme ) {
// If it doesn't exist, we download it.
if ( file_exists( $theme ) ) {
$env_config['volumes'][] = $theme . ':/var/www/html/wp-content/themes/' . basename( $theme );
unset( $theme );
}
}

// Requires.
foreach ( $env_config['requires'] ?? [] as $file ) {
if ( file_exists( $file ) ) {
if ( $this->output->isVerbose() ) {
$this->output->writeln( sprintf( 'Loading file %s', $file ) );
}

$prefix = null;

/**
* Since the phar is scoped with php-scoper, we need to prefix the handler as well.
*
* This essentially means, at runtime, replacing
* - use QIT_CLI\
* - use _HumbugBoxc7c7e1250ee1\QIT_CLI\
*
* Where the first part is completely random by php-scoper.
* "_HumbubBox" is the default prefix of php-scoper.
*
* @see https://github.com/humbug/php-scoper
*/
foreach ( explode( '\\', static::class ) as $namespace ) {
if ( strpos( $namespace, 'HumbugBox' ) !== false ) {
$prefix = $namespace;
break;
}
}

if ( ! is_null( $prefix ) ) {
// Prefixed phar.
if ( $this->output->isVeryVerbose() ) {
$this->output->writeln( sprintf( 'Converting handler to use prefix %s', $prefix ) );
}

$tmp_file = sys_get_temp_dir() . '/' . pathinfo( $file, PATHINFO_FILENAME ) . uniqid( 'prefixed' ) . '.php';

if ( file_put_contents( $tmp_file, str_replace( 'use QIT_CLI\\', "use $prefix\\QIT_CLI\\", file_get_contents( $file ) ) ) === false ) {
throw new \RuntimeException( 'Failed to write to the temporary file' );
}

if ( $this->output->isVeryVerbose() ) {
$this->output->writeln( sprintf( 'Loading file %s', $tmp_file ) );
}

require_once $tmp_file;
} else {
// If running outside of Phar context, just require it.
require_once $file;
}
} else {
$this->output->writeln( sprintf( '<error>File %s does not exist.</error>', $file ) );
throw new \RuntimeException( sprintf( 'File %s does not exist.', $file ) );
}
}

// No more need for this from now on.
unset( $env_config['requires'] );

// Volumes can be mapped automatically depending on the working directory.
$env_config['volumes'] = App::make( EnvVolumeParser::class )->parse_volumes( $env_config['volumes'] ?? [] );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ public function categorize_extensions( array $plugins, array $themes ): array {
*/
foreach ( get_declared_classes() as $class ) {
if ( is_subclass_of( $class, CustomHandler::class ) ) {
$handler = new $class();
$handler = App::make( $class );
if ( $handler->should_handle( $ext ) ) {
$this->output->writeln( "Custom handler '$class' is handling '{$ext->extension_identifier}'." );
$ext->handler = $class;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public function assign_handler_to_extension( string $extension_input, Extension

public function maybe_download_extensions( array $extensions, string $cache_dir ): void {
foreach ( $extensions as $e ) {
if ( ! empty( $e->path ) ) {
// Extension already handled (possibly by a custom handler).
continue;
}

if ( ! file_exists( $e->extension_identifier ) ) {
throw new \RuntimeException( 'File not found: ' . $e->path );
}
Expand Down
8 changes: 8 additions & 0 deletions src/src/Environment/ExtensionDownload/Handlers/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@
use QIT_CLI\Config;
use QIT_CLI\Environment\ExtensionDownload\Extension;
use QIT_CLI\Environment\ExtensionDownload\ExtensionDownloader;
use Symfony\Component\Console\Output\OutputInterface;
use function QIT_CLI\normalize_path;

abstract class Handler {
/** @var OutputInterface */
protected $output;

public function __construct( OutputInterface $output ) {
$this->output = $output;
}

/**
* This function should set the $version property of the given extensions.
*
Expand Down
5 changes: 5 additions & 0 deletions src/src/Environment/ExtensionDownload/Handlers/QITHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ public function maybe_download_extensions( array $extensions, string $cache_dir
$output = App::make( Output::class );

foreach ( $extensions as $e ) {
if ( ! empty( $e->path ) ) {
// Extension already handled (possibly by a custom handler).
continue;
}

$cache_file = $this->make_cache_path( $cache_dir, $e->type, $e->extension_identifier, $e->version );

// Cache hit?
Expand Down
5 changes: 5 additions & 0 deletions src/src/Environment/ExtensionDownload/Handlers/URLHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public function maybe_download_extensions( array $extensions, string $cache_dir
$output = App::make( Output::class );

foreach ( $extensions as $e ) {
if ( ! empty( $e->path ) ) {
// Extension already handled (possibly by a custom handler).
continue;
}

// As version is "undefined", cache burst is shorter: Hour of the day (0-24).
$cache_burst = gmdate( 'G' );

Expand Down

0 comments on commit 6d3934e

Please sign in to comment.