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

feat(WPLoader) support the silentlyActivatePlugins conf param #678

Merged
merged 1 commit into from
Dec 6, 2023
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).

## [unreleased] Unreleased

### Added

- The `WPLoader::silentlyActivatePlugins` configuration parameter to activate plugins without firing the `activated_plugin` action.

## [4.0.14] 2023-12-06;

### Added
Expand Down
1 change: 1 addition & 0 deletions docs/modules/WPLoader.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ When used in this mode, the module supports the following configuration paramete
`WP_PLUGIN_DIR` constant.
* `plugins` - a list of plugins to activate and load in the WordPress installation. Each plugin must be specified in a
format like `hello.php` or `my-plugin/my-plugin.php` format.
* `silentlyActivatePlugins` - a list of plugins to activate **silently**, without firing their activation hooks. Depending on the plugin, a silent activation might cause the plugin to not work correctly. The list must be in the same format as the `plugins` parameter and plugin should be activated silently only if they are not working correctly during normal activation and are known to work correctly when activated silently.
* `bootstrapActions` - a list of actions or callables to call **after** WordPress is loaded and before the tests run.
* `theme` - the theme to activate and load in the WordPress installation. The theme must be specified in slug format
like
Expand Down
49 changes: 46 additions & 3 deletions src/Module/WPLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class WPLoader extends Module
* configFile: string|string[],
* pluginsFolder: string,
* plugins: string[],
* silentlyActivatePlugins: string[],
* bootstrapActions: string|string[],
* theme: string,
* AUTH_KEY: string,
Expand Down Expand Up @@ -141,6 +142,7 @@ class WPLoader extends Module
'configFile' => '',
'pluginsFolder' => '',
'plugins' => [],
'silentlyActivatePlugins' => [],
'bootstrapActions' => '',
'theme' => '',
'AUTH_KEY' => '',
Expand Down Expand Up @@ -193,6 +195,37 @@ protected function validateConfig(): void
$this->config['dbCharset'] = $this->config['DB_CHARSET'] ?? $this->config['dbCharset'] ?? '';
$this->config['dbCollate'] = $this->config['DB_COLLATE'] ?? $this->config['dbCollate'] ?? '';
$this->config['multisite'] = (bool)($this->config['WP_TESTS_MULTISITE'] ?? $this->config['multisite'] ?? false);

if (!(
is_array($this->config['plugins'])
&& Arr::containsOnly($this->config['plugins'], 'string'))
) {
throw new ModuleConfigException(
__CLASS__,
'The `plugins` configuration parameter must be an array of plugin names ' .
'in the my-plugin/plugin.php or plugin.php format.'
);
}

if (!(
is_array($this->config['silentlyActivatePlugins'])
&& Arr::containsOnly($this->config['silentlyActivatePlugins'], 'string'))
) {
throw new ModuleConfigException(
__CLASS__,
'The `silentlyActivatePlugins` configuration parameter must be an array of plugin names ' .
'in the my-plugin/plugin.php or plugin.php format.'
);
}

if (count(array_intersect($this->config['plugins'], $this->config['silentlyActivatePlugins']))) {
throw new ModuleConfigException(
__CLASS__,
'The `plugins` and `silentlyActivatePlugins` configuration parameters must not contain the ' .
'same plugins.'
);
}

$this->config['theme'] = $this->config['WP_TESTS_MULTISITE'] ?? $this->config['theme'] ?? '';

if (!is_string($this->config['theme'])) {
Expand Down Expand Up @@ -314,6 +347,7 @@ public function _initialize(): void
* configFile: string|string[],
* pluginsFolder: string,
* plugins: string[],
* silentlyActivatePlugins: string[],
* bootstrapActions: string|string[],
* theme: string,
* AUTH_KEY: string,
Expand Down Expand Up @@ -645,14 +679,23 @@ private function activatePluginsSwitchThemeInSeparateProcess(): void
{
/** @var array<string> $plugins */
$plugins = (array)($this->config['plugins'] ?: []);
$silentlyActivatePlugins = (array)($this->config['silentlyActivatePlugins'] ?: []);
$allPlugins = array_merge($plugins, $silentlyActivatePlugins);
$multisite = (bool)($this->config['multisite'] ?? false);
$closuresFactory = $this->getCodeExecutionFactory();
$silentFlags = array_merge(
array_fill(0, count($plugins), false),
array_fill(0, count($silentlyActivatePlugins), true)
);

$jobs = array_combine(
array_map(static fn(string $plugin): string => 'plugin::' . $plugin, $plugins),
array_map(static fn(string $plugin): string => 'plugin::' . $plugin, $allPlugins),
array_map(
static fn(string $plugin): Closure => $closuresFactory->toActivatePlugin($plugin, $multisite),
$plugins
static function (string $plugin, bool $silent) use ($closuresFactory, $multisite): Closure {
return $closuresFactory->toActivatePlugin($plugin, $multisite, $silent);
},
$allPlugins,
$silentFlags
)
);

Expand Down
9 changes: 5 additions & 4 deletions src/WordPress/CodeExecution/ActivatePluginAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,23 @@ public function __construct(
FileRequest $request,
string $wpRootDir,
string $plugin,
bool $multisite
bool $multisite,
bool $silent = false
) {
$request->setTargetFile($wpRootDir . '/wp-load.php')
->runInFastMode($wpRootDir)
->defineConstant('MULTISITE', $multisite)
->addAfterLoadClosure(fn() => $this->activatePlugin($plugin, $multisite));
->addAfterLoadClosure(fn() => $this->activatePlugin($plugin, $multisite, $silent));
$this->request = $request;
}

/**
* @throws InstallationException
*/
private function activatePlugin(string $plugin, bool $multisite): void
private function activatePlugin(string $plugin, bool $multisite, bool $silent = false): void
{
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$activated = activate_plugin($plugin, '', $multisite);
$activated = activate_plugin($plugin, '', $multisite, $silent);
$activatedString = $multisite ? 'network activated' : 'activated';
$message = "Plugin $plugin could not be $activatedString.";

Expand Down
4 changes: 2 additions & 2 deletions src/WordPress/CodeExecution/CodeExecutionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ public function toCheckIfWpIsInstalled(bool $multisite): Closure
->getClosure();
}

public function toActivatePlugin(string $plugin, bool $multisite): Closure
public function toActivatePlugin(string $plugin, bool $multisite, bool $silent = false): Closure
{
$request = $this->requestFactory->buildGetRequest()
->blockHttpRequests()
->setRedirectFiles($this->redirectFiles)
->addPresetGlobalVars($this->presetGlobalVars);

return (new ActivatePluginAction($request, $this->wpRootDir, $plugin, $multisite))
return (new ActivatePluginAction($request, $this->wpRootDir, $plugin, $multisite, $silent))
->getClosure();
}

Expand Down
177 changes: 163 additions & 14 deletions tests/unit/lucatume/WPBrowser/Events/Module/WPLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use lucatume\WPBrowser\WordPress\InstallationException;
use lucatume\WPBrowser\WordPress\InstallationState\InstallationStateInterface;
use lucatume\WPBrowser\WordPress\InstallationState\Scaffolded;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestResult;
use stdClass;
use tad\Codeception\SnapshotAssertions\SnapshotAssertions;
Expand Down Expand Up @@ -2396,16 +2397,7 @@ public function should_skip_installation_when_skip_install_is_true(): void
'woocommerce'
]
]);
if (!mkdir($wpRootDir . '/tests/some_test_suite', 0777, true)) {
throw new \RuntimeException('Failed to create the test suite folder.');
}
$moduleContainerConfig = [
'config' => [
'suite' => 'some_test_suite',
'path' => $wpRootDir . '/tests/some_test_suite'
]
];
$moduleConfig = [
$this->config = [
'wpRootFolder' => $wpRootDir,
'dbUrl' => $installationDb->getDbUrl(),
'tablePrefix' => 'test_',
Expand All @@ -2415,7 +2407,7 @@ public function should_skip_installation_when_skip_install_is_true(): void
];

// Run the module a first time: it should create the flag file indicating the database was installed.
$wpLoader = $this->module($moduleContainerConfig, $moduleConfig);
$wpLoader = $this->module();
$moduleSplObjectHash = spl_object_hash($wpLoader);
$this->assertInIsolation(
static function () use ($wpLoader, $moduleSplObjectHash) {
Expand Down Expand Up @@ -2463,7 +2455,7 @@ static function () use ($wpLoader, $moduleSplObjectHash) {
$this->assertEquals('twentytwenty', $checkDb->getOption('stylesheet'));

// Run a second time, this time the installation should be skipped.
$wpLoader = $this->module($moduleContainerConfig, $moduleConfig);
$wpLoader = $this->module();
$this->assertInIsolation(
static function () use ($moduleSplObjectHash, $wpLoader) {
$beforeInstallCalled = false;
Expand Down Expand Up @@ -2499,9 +2491,9 @@ static function () use ($moduleSplObjectHash, $wpLoader) {
);

// Now run in --debug mode, the installation should run again.
$wpLoader = $this->module($moduleContainerConfig, $moduleConfig);
$wpLoader = $this->module();
$this->assertInIsolation(
static function () use ($moduleSplObjectHash, $wpLoader) {
static function () use ($wpLoader) {
$beforeInstallCalled = false;
$afterInstallCalled = false;
$afterBootstrapCalled = false;
Expand Down Expand Up @@ -2539,4 +2531,161 @@ static function () use ($moduleSplObjectHash, $wpLoader) {
}
);
}

/**
* It should throw if silentlyActivatePlugins config parameter is not a list of strings
*
* @test
* @dataProvider notArrayOfStringsProvider
*/
public function should_throw_if_silently_activate_plugins_config_parameter_is_not_a_list_of_strings($input): void
{
$wpRootDir = Env::get('WORDPRESS_ROOT_DIR');
$db = (new Installation($wpRootDir))->getDb();
$this->config = [
'wpRootFolder' => $wpRootDir,
'dbUrl' => $db->getDbUrl(),
'silentlyActivatePlugins' => $input,
];

$this->expectException(ModuleConfigException::class);

$this->module();
}

/**
* It should throw if plugin appears in both plugins and silentlyActivatePlugins config parameters
*
* @test
*/
public function should_throw_if_plugin_appears_in_both_plugins_and_silently_activate_plugins_config_parameters(
): void
{
$wpRootDir = Env::get('WORDPRESS_ROOT_DIR');
$db = (new Installation($wpRootDir))->getDb();
$this->config = [
'wpRootFolder' => $wpRootDir,
'dbUrl' => $db->getDbUrl(),
'plugins' => ['woocommerce/woocommerce.php', 'my-plugin/plugin.php'],
'silentlyActivatePlugins' => ['foo-plugin/plugin.php', 'woocommerce/woocommerce.php'],
];

$this->expectException(ModuleConfigException::class);

$this->module();
}

/**
* It should fail to activate when plugins generate unexpected output
*
* @test
*/
public function should_fail_to_activate_when_plugins_generate_unexpected_output(): void
{
$wpRootDir = FS::tmpDir('wploader_');
$installation = Installation::scaffold($wpRootDir);
$dbName = Random::dbName();
$dbHost = Env::get('WORDPRESS_DB_HOST');
$dbUser = Env::get('WORDPRESS_DB_USER');
$dbPassword = Env::get('WORDPRESS_DB_PASSWORD');
$installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_');
$installation->configure($installationDb);
$this->copyOverContentFromTheMainInstallation($installation, [
'plugins' => [
'woocommerce'
]
]);
// Create a plugin that will raise a doing_it_wrong error on activation.
FS::mkdirp($wpRootDir . '/wp-content/plugins', [
'my-plugin' => [
'plugin.php' => <<< PHP
<?php
/** Plugin Name: DIW Plugin */

function activate_my_plugin(){
echo 'Something went wrong';
}

register_activation_hook( __FILE__, 'activate_my_plugin' );
PHP
]
]);

$this->config = [
'wpRootFolder' => $wpRootDir,
'dbUrl' => $installationDb->getDbUrl(),
'tablePrefix' => 'test_',
'plugins' => ['woocommerce/woocommerce.php', 'my-plugin/plugin.php'],
];

// Run a first initialization that should fail due to the doing_it_wrong error.
$wpLoader = $this->module();

$this->expectException(ModuleException::class);

$this->assertInIsolation(
static function () use ($wpLoader) {
$wpLoader->_initialize();
}
);
}

/**
* It should allow activating plugins silently
*
* @test
*/
public function should_allow_activating_plugins_silently(): void
{
$wpRootDir = FS::tmpDir('wploader_');
$installation = Installation::scaffold($wpRootDir);
$dbName = Random::dbName();
$dbHost = Env::get('WORDPRESS_DB_HOST');
$dbUser = Env::get('WORDPRESS_DB_USER');
$dbPassword = Env::get('WORDPRESS_DB_PASSWORD');
$installationDb = new MysqlDatabase($dbName, $dbUser, $dbPassword, $dbHost, 'wp_');
$installation->configure($installationDb);
$this->copyOverContentFromTheMainInstallation($installation, [
'plugins' => [
'woocommerce'
]
]);
// Create a plugin that will raise a doing_it_wrong error on activation.
FS::mkdirp($wpRootDir . '/wp-content/plugins', [
'my-plugin' => [
'plugin.php' => <<< PHP
<?php
/** Plugin Name: DIW Plugin */

function activate_my_plugin(){
echo 'Something went wrong';
update_option('my_plugin_activated', '__activated__');
}

register_activation_hook( __FILE__, 'activate_my_plugin' );
update_option('my_plugin_loaded', '__loaded__');
PHP
]
]);

$this->config = [
'wpRootFolder' => $wpRootDir,
'dbUrl' => $installationDb->getDbUrl(),
'tablePrefix' => 'test_',
'plugins' => ['woocommerce/woocommerce.php'],
'silentlyActivatePlugins' => ['my-plugin/plugin.php'],
];

// Run a first initialization that should fail due to the doing_it_wrong error.
$wpLoader = $this->module();

$this->assertInIsolation(
static function () use ($wpLoader) {
$wpLoader->_initialize();

assertEquals('', get_option('my_plugin_activated'));
assertEquals('__loaded__', get_option('my_plugin_loaded'));
}
);
}
}