Plugins

Plugins are a comfortable way of extending Slash’s behavior. They are objects inheriting from a common base class that can be activated to modify or what happens in select point of the infrastructure.

The Plugin Interface

Plugins have several special methods that can be overriden, like get_name or configure_argument_parser. Except for these methods and the ones documented, each public method (i.e. a method not beginning with an underscore) must correspond to a slash hook by name.

The name of the plugin should be returned by get_name. This name should be unique, and not shared by any other plugin.

Plugin Discovery

Plugins can be loaded from multiple locations.

Search Paths

First, the paths in plugins.search_paths are searched for python files. For each file, a function called install_plugins is called (assuming it exists), and this gives the file a chance to install its plugins.

Plugin Installation

To install a plugin, use the slash.plugins.manager.install function, and pass it the plugin class that is being installed. Note that installed plugins are not active by default, and need to be explicitly activated (see below).

Only plugins that are PluginInterface derivative instances are accepted.

To uninstall plugins, you can use the slash.plugins.manager.uninstall.

Note

uninstalling plugins also deactivates them.

Internal Plugins

By default, plugins are considered “external”, meaning they were loaded by the user (either directly or indirectly). External plugins can be activated and deactivated through the command-line using --with-<plugin name> and --without-<plugin name>.

In some cases, though, you may want to install a plugin in a way that would not let the user disable it externally. Such plugins are considered “internal”, and cannot be deactivated through the command line.

You can install a plugin as an internal plugin by passing internal=True to the install function.

Plugin Activation

Plugins are activated via slash.plugins.manager.activate and deactivated via slash.plugins.manager.deactivate.

During the activation all hook methods get registered to their respective hooks, so any plugin containing an unknown hook will trigger an exception.

Note

by default, all method names in a plugin are assumed to belong to the slash gossip group. This means that the method session_start will register on slash.session_start. You can override this behavior by using slash.plugins.registers_on():

from slash.plugins import registers_on

class MyPlugin(PluginInterface):
    @registers_on('some_hook')
    def func(self):
        ...

registers_on(None) has a special meaning - letting Slash know that this is not a hook entry point, but a private method belonging to the plugin class itself.

See also

Hooks

Activating plugins from command-line is usually done with the --with- prefix. For example, to activate a plugin called test-plugin, you can pass --with-test-plugin when running slash run.

Also, since some plugins can be activated from other locations, you can also override and deactivate plugins using --without-X (e.g. --without-test-plugin).

Conditionally Registering Hooks

You can make the hook registration of a plugin conditional, meaning it should only happen if a boolean condition is True.

This can be used to create plugins that are compatible with multiple versions of Slash:

class MyPlugin(PluginInterface):
    ...
    @slash.plugins.register_if(int(slash.__version__.split('.')[0]) >= 1)
    def shiny_new_hook(self):
        ...

Plugin Command-Line Interaction

In many cases you would like to receive options from the command line. Plugins can implement the configure_argument_parser and the configure_parsed_args functions:

class ResultsReportingPlugin(PluginInterface):

    def configure_argument_parser(self, parser):
        parser.add_argument("--output-filename", help="File to write results to")

    def configure_from_parsed_args(self, args):
        self.output_filename = args.output_filename

Plugin Configuration

Plugins can override the config method to provide configuration to be placed under plugin_config.<plugin name>:

class LogCollectionPlugin(PluginInterface):

    def get_default_config(self):
        return {
            'log_destination': '/some/default/path'
        }

The configuration is then accessible with get_current_config property.

Plugin Examples

An example of a functioning plugin can be found in the Customizing and Extending Slash section.

Errors in Plugins

As more logic is added into plugins it becomes more likely for exceptions to occur when running their logic. As seen above, most of what plugins do is done by registering callbacks onto hooks. Any exception that escapes these registered functions will be handled the same way any exception in a hook function is handled, and this depends on the current exception swallowing configuration.

Plugin Dependencies

Slash supports defining dependencies between plugins, in a mechanism closely related to to gossip’s hook dependencies. The purpose of these dependencies is to make sure a certain hook registration in a specific plugin (or all such hooks for that matter) is called before or after equivalent hooks on other plugins.

Notable examples of why you might want this include, among many other cases:

  • Plugins reporting test status needing a state computed by other plugins

  • Error handling plugins wanting to be called first in certain events

  • Log collection plugins wanting to be called only after all interesting code paths are logged

Defining Plugin Dependencies

Defining dependencies is done primarily with two decorators Slash provides: @slash.plugins.needs and @slash.plugins.provides. Both of these decorators use string identifiers to denote the dependencies used. These identifiers are arbitrary, and can be basically any string, as long as it matches between the dependent plugin and the providing plugin.

Several use cases exist:

Hook-Level Dependencies

Adding the slash.plugins.needs or slash.plugins.provides decorator to a specific hook method on a plugin indicates that we would like to depend on or be the dependency accordingly. For example:

class TestIdentificationPlugin(PluginInterface):

    @slash.plugins.provides('awesome_test_id')
    def test_start(self):
        slash.context.test.awesome_test_id = awesome_id_allocation_service()

class TestIdentificationLoggingPlugin(PluginInterface):

    @slash.plugins.needs('awesome_test_id')
    def test_start(self):
        slash.logger.debug('Test has started with the awesome id of {!r}', slash.context.test.awesome_id)

In the above example, the test_start hook on TestIdentificationLoggingPlugin needs the test_start of TestIdentificationPlugin to be called first, and thus requires the 'awesome_test_id' identifier which is provided by the latter.

Plugin-Level Dependencies

Much like hook-level dependencies, you can decorate the entire plugin with the needs and provides decorators, creating a dependency on all hooks provided by the plugin:

@slash.plugins.provides('awesome_test_id')
class TestIdentificationPlugin(PluginInterface):

    def test_start(self):
        slash.context.test.awesome_test_id = awesome_id_allocation_service()

@slash.plugins.needs('awesome_test_id')
class TestIdentificationLoggingPlugin(PluginInterface):

    def test_start(self):
        slash.logger.debug('Test has started with the awesome id of {!r}', slash.context.test.awesome_id)

The above example is equivalent to the previous one, only now future hooks added to either of the plugins will automatically assume the same dependency specifications.

Note

You can use provides and needs in more complex cases, for example specifying needs on a specific hook in one plugin, where the entire other plugin is decorated with provides (at plugin-level).

Note

Plugin-level provides and needs also get transferred upon inheritence, automatically adding the dependency configuration to derived classes.

Plugin Manager

As mentioned above, the Plugin Manager provides API to activate (or deacativate) and install (or uninstall) plugins. Additionally, it provides access to instances of registered plugins by their name via slash.plugins.manager.get_plugin. This could be used to access plugin attributes whose modification (e.g. by fixtures) can alter the plugin’s behavior.

Plugins and Parallel Runs

Not all plugins can support parallel execution, and for others implementing support for it can be much harder than supporting non-parallel runs alone.

To deal with this, in addition to possible mistakes or corruption caused by plugins incorrectly used in parallel mode, Slash requires each plugin to indicate whether or not it supports parallel execution. The assumption is that by default plugins do not support parallel runs at all.

To indicate that your plugin supports parallel execution, use the plugins.parallel_mode marker:

from slash.plugins import PluginInterface, parallel_mode

@parallel_mode('enabled')
class MyPlugin(PluginInterface):
    ...

parallel_mode supports the following modes:

  • disabled - meaning the plugin does not support parallel execution at all. This is the default.

  • parent-only - meaning the plugin supports parallel execution, but should be active only on the parent process.

  • child-only - meaning the plugin should only be activated on worker/child processes executing the actual tests.

  • enabled - meaning the plugin supports parallel execution, both on parent and child.