Investigating a better plugin activation in WordPress tests.
A good point
In an issue opened on the wp-browser issue tracker on GitHub Barry Huges made a good point
Just to clarify, my suggestion wasn’t to change when plugins are loaded - it was to change the hook on which they are activated. I’d also agree wp may not be the best action either (though it works) - wp_loaded is probably better. Here’s my take on the expected sequence of events when a plugin is activated:
- wp-load.php bootstraps WordPress
- wp-config.php is included by wp-load.php
- wp-settings.php is included by wp-config.php
- muplugins_loaded fires
- plugins_loaded fires
- Various key objects are now instantiated (including the $wp_rewrite global)
- wp_loaded fires (at this point, we’re right at the end of wp-settings.php)
- The next thing we are interested in is the call to activate_plugin(…) which is responsible for firing the activate_{plugin} hook
So it’s not unreasonable for a plugin to want to interact with things like $wp_rewrite when the activation hook runs - directly or indirectly (ie, by registering a post type). All I’m really saying is, yep let’s load the plugins as we’re currently doing (or on plugins_loaded) - but activation ought to move forward, as it would if we weren’t in a test environment.
What he points out is an issue for any plugin that has some significant activation operations and relies on those setup conditions to operate and run.
The flow of calls that happens during a run of the WPLoader module is the same found inWordPress automated tests and it’s an emulation of a standard WordPress request handling that packs, but, a WordPress installation.
Plugin activation flow
Plugin activation usually happens using WordPress plugins administration screen in UI terms.
It’s handled, on PHP terms, by the wp/admin/plugins.php
file.
When the user clicks the “Activate” link of an inactive plugin (e.g. “My Plugin”) this is what happens under the hood:
- WordPress bootstraps the administration UI
- presuming the user can activate the plugin WordPress calls the
activate_plugin
function activates the plugin presuming the activation will trigger errors and setting the redirect url to hit thewp-admin/plugins.php
file again with the following query arguments:error
set totrue
plugin
set tomy-plugin/my-plugin.php
in the case of the example plugin
- WordPress opens the output buffer
- the main plugin file is loaded
- the plugin activation action is triggered
- if the plugin main file inclusion and the plugin activation did not produce unexpected output then the
Location
header is once again set to the activation success message and URL - WordPress exits and the user is redirected to the plugin administration page: the plugin activation is complete.
A close reality
The flow above relies on the user hitting a specific WordPress file, the wp-admin/plugins.php
one, to deal with and manage plugins activation but the WordPress automated testing suite, and the WPLoader module wrapping it, have to abstract that concept to bootstrap a ready to run WordPress installation.
As Barry points out in his comment I’m calling the plugins activation actions at muplugins_loaded
time; that’s the earliest possible moment that can be loaded but way too early.
In an ideal emulation of the “real” WordPress that call to
do_action('activate_my-plugin/my-plugin.php')
should happen exactly when it happens, in WordPress actions terms, during the user interaction with the plugins activation screen. So when is that? An XDebug run through the code and the dump of the wp_actions
variable tells the story:
Array
(
// too early
[muplugins_loaded] => 1
[registered_taxonomy] => 10
[registered_post_type] => 10
// still too early
[plugins_loaded] => 1
[sanitize_comment_cookies] => 1
[setup_theme] => 1
[unload_textdomain] => 1
[load_textdomain] => 2
[after_setup_theme] => 1
[auth_cookie_valid] => 2
[set_current_user] => 1
[init] => 1
[widgets_init] => 1
[wp_register_sidebar_widget] => 12
// good idea
[wp_loaded] => 1
[auth_redirect] => 1
[wp_default_scripts] => 1
[_admin_menu] => 1
[admin_menu] => 1
[admin_init] => 1
[wp_default_styles] => 1
[admin_bar_init] => 1
[add_admin_bar_menus] => 1
[current_screen] => 1
[load-plugins.php] => 1
[admin_action_activate] => 1
[check_admin_referer] => 1
[activate_plugin] => 1
[activate_my-plugin/my-plugin.php] => 1
)
First: muplugins_loaded
is way too early, Barry is absolutely right; second: wp_loaded
is a good candidate for the plugin activation to happen.
To try to get as close to reality as possible I’ve run through a test case relying on the WPLoader module to have a similar dump and be able to find the latest possible intersecting hook.
I’ve set up a test case with just one test method in it
<?php
class newTest extends \WP_UnitTestCase
{
protected $backupGlobals = false;
public function setUp()
{
// before
parent::setUp();
// your set up methods here
}
/**
* pass
*/
public function test_pass()
{
xdebug_break();
$this->assertTrue(true);
}
public function tearDown()
{
// your tear down methods here
// then
parent::tearDown();
}
}
and dumped the content of the wp_actions
global variable at the time of the xdebug_break
call:
Array
(
// the usual suspects
[muplugins_loaded] => 1
[registered_taxonomy] => 10
[registered_post_type] => 10
[plugins_loaded] => 1
[sanitize_comment_cookies] => 1
[setup_theme] => 1
[unload_textdomain] => 1
[load_textdomain] => 3
[after_setup_theme] => 1
[auth_cookie_malformed] => 1
[set_current_user] => 1
[init] => 1
[widgets_init] => 1
[wp_register_sidebar_widget] => 12
// here it is: this hook is fired once WP, all plugins, and
// the theme are fully loaded and instantiated.
[wp_loaded] => 1
[populate_options] => 1
[add_option] => 9
[add_option_wp_user_roles] => 1
[added_option] => 9
[update_option] => 7
[update_option_blogname] => 1
[updated_option] => 6
[update_option_admin_email] => 1
[add_user_meta] => 13
[added_user_meta] => 13
[set_user_role] => 2
[user_register] => 1
[update_user_meta] => 2
[updated_user_meta] => 2
[add_option_widget_search] => 1
[add_option_widget_recent-posts] => 1
[add_option_widget_recent-comments] => 1
[add_option_widget_archives] => 1
[update_option_widget_categories] => 1
[add_option_widget_meta] => 1
[add_option_sidebars_widgets] => 1
[update_option_permalink_structure] => 3
[permalink_structure_changed] => 3
[generate_rewrite_rules] => 2
[add_option_rewrite_rules] => 2
[http_api_curl] => 2
[http_api_debug] => 2
[delete_option] => 2
[delete_option_rewrite_rules] => 2
[deleted_option] => 2
[phpmailer_init] => 1
// note this: fires after a site is fully installed.
[wp_install] => 1
[before_delete_post] => 2
[delete_term_relationships] => 1
[deleted_term_relationships] => 1
[edit_term_taxonomy] => 1
[edited_term_taxonomy] => 1
[clean_term_cache] => 1
[delete_comment] => 1
[deleted_comment] => 1
[clean_object_term_cache] => 3
[clean_post_cache] => 3
[wp_update_comment_count] => 1
[edit_post] => 1
[wp_set_comment_status] => 1
[transition_comment_status] => 1
[comment_approved_to_delete] => 1
[comment_delete_] => 1
[delete_post] => 2
[parse_tax_query] => 4
[parse_query] => 2
[pre_get_posts] => 2
[posts_selection] => 2
[deleted_post] => 2
[after_delete_post] => 2
[delete_post_meta] => 1
[delete_postmeta] => 1
[deleted_post_meta] => 1
[deleted_postmeta] => 1
[clean_page_cache] => 1
)
Beside some “noise” generated by the bootstrapping operation accessing and modifying meta values, options and posts it’s comforting to see that the wp_loaded
hook is still there but it begs further inquiry the call to the wp_install
hook later.
Next
I’ve got a part of the information I need to move the plugins activation in tests but I will dig a little further to move things forward.