Moving the plugins activation in WordPress tests 01

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 the wp-admin/plugins.php file again with the following query arguments:
    • error set to true
    • plugin set to my-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.