Component Plugins

In this tutorial, we will discuss how to create and load component plugins. If you’re not familiar with the component models, please first visit Component Plugin Models for details.

Creating Plugins

Steps to creating a component plugin:

  1. With the two possible plugin models, TestSuiteBase or SystemBase, choose the model that fits for your component plugin.

  2. Subclass the base class and implement the required methods.

  3. Define the name of the plugin and extend one of two possible roast component namespaces.

Let’s start with a system component named MySystem that has two methods, configure() and build(). This will be created in my_system.py.

Create a test suite component named MyTestSuite that has four methods, configure(), build(), deploy(), and run(). This will be create in my_testsuite.py.

We will need a configuration file named conf.py to define which components will be used in the test scenario and a test module named test_scenario.py. Add these into the tests directory:

repository/
└── tests/
    ├── conf.py
    ├── my_system.py
    ├── my_testsuite.py
    └── test_scenario.py

conf.py

roast = {"system": ["my_system"], "testsuite": "my_testsuite"}
var = "hello world"

Here, we define a system component named "my_system" and a testsuite component named "my_testsuite".

Note

These names are identifiers used by each plugin when registering as an entry point and do not need to match the module filename.

Also note that the value for system is a list since a test scenario could have more than one.

my_system.py

from roast.component.system import SystemBase

class MySystem(SystemBase):
    def __init__(self, config):
        super().__init__(config)

    def configure(self):
        print("MySystem configure called")

    def build(self):
        msg = "MySystem build called"
        print(msg)
        return msg

    def custom_method(self, data):
        msg = f"MySystem custom method called with {data}"
        print(msg)
        return msg

Here, we are subclassing from the SystemBase abstract base class and implementing the required methods configure() and build(). In addition, we are going to extend the class with a method named custom_method().

The super() call in __init__() is where the configuration is stored as an attribute of the class and can accessed through self.config.

my_testsuite.py

from roast.component.testsuite import TestSuiteBase

class MyTestSuite(TestSuiteBase):
    def __init__(self, config):
        super().__init__(config)

    def configure(self):
        print("MyTestSuite configure called")

    def build(self):
        msg = "MyTestSuite build called"
        print(msg)
        return msg

    def deploy(self):
        print("MyTestSuite deploy called")

    def run(self):
        msg = "MyTestSuite run called"
        print(msg)
        return msg

    def custom_method(self, data):
        msg = f"MyTestSuite custom method called with {data}"
        print(msg)
        return msg

Similar to MySystem, subclass and implement the required methods. Also extend the class with a custom method.

Loading Plugins

In order to dynamically load component plugins, they first need to be registered in the ROAST namespace as an object that can be called through entry points. Two namespaces are available: roast.component.testsuite for a TestSuite component and roast.component.system for System components.

test_scenario.py

import inspect
from roast.utils import register_plugin
import my_system, my_testsuite

def test_my_scenario(create_scenario):
    system_name = "my_system"
    system_location = inspect.getsourcefile(my_system)
    register_plugin(
        system_location, system_name, "system", "my_system:MySystem",
    )
    testsuite_name = "my_testsuite"
    testsuite_location = inspect.getsourcefile(my_testsuite)
    register_plugin(
        testsuite_location, testsuite_name, "testsuite", "my_testsuite:MyTestSuite",
    )

    scn = create_scenario()
    my_ts = scn.ts
    my_sys = scn.sys(system_name)

    scn.configure_component()
    assert my_ts.config.var == "hello world"
    assert my_sys.config.var == "hello world"

    build_results = scn.build_component()
    assert build_results[testsuite_name] == "MyTestSuite build called"
    assert build_results[system_name] == "MySystem build called"

    scn.deploy_component()

    run_results = scn.run_component()
    assert run_results[testsuite_name] == "MyTestSuite run called"

    custom_result = my_ts.custom_method(data="hello")
    assert custom_result == "MyTestSuite custom method called with hello"
    custom_result = my_sys.custom_method(data="hello")
    assert custom_result == "MySystem custom method called with hello"

Here, we need to first register the MySystem and MyTestSuite classes. In order to register, we will need their file locations which can be hard coded or obtained through the use of inspect.getfile().

If the component objects will be packaged into a Python package, this can be defined in setup.py.

entry_points={
    "roast.component.system": ["repository.tests.my_system = my_system:MySystem",],
    "roast.component.testsuite": ["repository.test.my_testsuite = my_testsuite:MyTestSuite",],
}

Next, we call the create_scenario() fixture to load the components and also generate a configuration. The variable scn holds references to both MySystem and MyTestSuite instances. To access the specific instance, use scn.ts for test suite or scn.sys for systems. For systems, the specific name is required since there can be more than one system component. Here, we’re going to assign the instances to my_ts and my_sys.

Calling the configure_component() method will in turn call the configure() method in every loaded instance. In both MySystem and MyTestSuite, the configuration is stored as a config attribute. With my_ts.config.var, we can access the value of var in the MyTestSuite instance and similarly with my_sys.config.var for MySystem. Both should return "hello world".

Similarly, when build_component() is called, this will call build() in each instance. The difference here is that values are returned in a dictionary that can be accessed using the name as the key.

The methods deploy_component() and run_component() are essentially the same as the previous two except that these call only the MyTestSuite instance since systems do not have deploy() or run() methods.

Lastly, since we access to the instances, we can call custom methods, pass parameters, and also return values.

Let’s now execute the tests:

$ pytest -rP
=========== test session starts ===========
..
collected 1 item

tests/test_scenario .                [100%]

===========================================
---------- Captured stdout call -----------
MySystem configure called
MyTestSuite configure called
MySystem build called
MyTestSuite build called
MyTestSuite deploy called
MyTestSuite run called
MyTestSuite custom method called with hello
MySystem custom method called with hello
============ 1 passed in 0.12s ============