Skip to content

Creating Your Own Test Modules

Radek Pazdera edited this page Aug 13, 2013 · 11 revisions

This page needs reviewing!


In this document I am going to guide you through the process of writing a test module for the LNST framework.

1. Basic test

1.1 Tests code location

After installation you can find test modules, distributed with LNST, in the directory /usr/share/lnst/test_modules. This is the only directory LNST searches by default. However you can change the search locations by changing the option test_module_dirs in your configuration file. You can use as many directories as you like, we recommend you keep the default system-wide directory and use at least one user-specific directory. For details on how to configure LNST see Documentation/Configuration.

For the purpose of this document let's assume that you're going to implement a test with name MyNetworkTest. LNST requires that you name the python class with the prefix Test, therefore the class will be called TestMyNetworkTest and the file TestMyNetworkTest.py. This prefix should be omitted when you are later referring it from the recipe XML file.

So, let's start with implementation. Change to one of the configured directories and create a file TestMyNetworkTest.py and open it with your favorite editor.

Every class implementing an LNST test inherits from TestGeneric class from TestsCommon module:

from lnst.Common.TestsCommon import TestGeneric

class TestMyNetworkTest(TestGeneric):
    ...

The only method you need to implement is the run() method and this is the code that will be executed whenever the test is referenced from the recipe.

1.2 Passing the parameters to the test

TestGeneric class provides set of methods to get the parameters and their values specified in the recipe.

  • get_opt()
  • get_mopt()
  • get_multi_opt()
  • get_multi_mopt()

The get_opt() and get_multi_opt() are used to get optional parameters. To make a parameter mandatory use their mopt variants, get_mopt() and get_multi_mopt().

For example, let's assume that your test requires a parameter containing an IP address to connect to. It's name is [[span(remote_ip, style=background-color:#add8e6)]]. Additionally you want to let users specify an optional parameter saying how many messages the test should send. Let's name it [[span(message_count, style=background-color:#90ee90)]].

{{{#!div style="background: #ffffff; border: 1px ridge; border-color: #d7d7d7; margin: 23px"

<pre>
class TestMyNetworkTest(TestGeneric):

    def do_some_stuff_with_parameters(self, remote_ip, count):
        s = connect(remote_ip)
        for n in range(count):
            s.send_message("data%s" % n)
        s.close()

    def run(self):
        <span style="background-color:#add8e6">rip = self.get_mopt("remote_ip")'</span>
        <span style="background-color:#90ee90">mc = self.get_opt("message_count", default=10)</span>

        do_some_stuff_with_parameters(rip, mc)
</pre>

}}}

And following is an example how to run your test from the recipe.

{{{#!div style="background: #ffffff; border: 1px ridge; border-color: #d7d7d7; margin: 23px"

<pre>
&lt;command type="test" name="MyNetworkTest" machine_id="1"&gt;
  &lt;options&gt;
    &lt;option name="<span style="background-color:#add8e6">remote_ip</span>" value="192.168.100.10" /&gt;
    &lt;option name="<span style="background-color:#90ee90">message_count</span>" value="50" /&gt;
  &lt;/options&gt;
&lt;/command&gt;
</pre>

}}}

The multi variants let you specify multi-value parameters. Let's consider the following example. You'd like to specify multiple remote targets for your test. Without the multi opt variant you would have to run the test multiple times from the recipe in the background. Using this method you can write the following command:

<command type="test" value="MyNetworkTest" machine_id="1">
  <options>
    <option name="remote_target" value="192.168.100.10" />
    <option name="remote_target" value="192.168.100.20" />
    <option name="remote_target" value="192.168.100.30" />
  </options>
</command>

And you can use following code to use all of the values:

class TestMyNetworkTest(TestGeneric):

    def do_some_stuff_with_target(self)
        s = connect(t)
        s.send_message("hello")
        s.close()

    def run(self):
        targets = self.get_multi_opt("remote_target)
        for t in targets:
            self.do_some_stuff_with_target(t)

Method get_multi_mopt() is the same but at least one value has to be specified.

1.3 Reporting test result

For what reason do we have tests if they don't tell us their result?

The TestGeneric class provides two methods related to reporting the test results.

  • set_fail ( [message] )
  • set_pass ( [message] )

If you don't call any of these methods from your test the result will always be a success (pass). Both methods take an optional parameter message that can be used to report the result in more detail, e.g. what was the transfer rate, how many connections have been established, etc.

Let us now enhance our example above a bit:

class TestMyNetworkTest(TestGeneric):

    def do_some_stuff_with_target(self)
        s = connect(t)
        if not s:
            return False
        s.send_message("hello")
        s.close()

    def run(self):
        targets = self.get_multi_opt("remote_target)
        for t in targets:
            rc = self.do_some_stuff_with_target(t)
            if not rc:
                self.set_fail("Could not connect to target %s" % t)

        # if we're not reporting anything interesting, you can omit the 
        # following line
        self.set_pass()

2. Advanced topics

2.1 Handling interrupts

There are two approaches how to do this depending on the desired behaviour.

2.1.1 Using the LNST facilities

This approach is used if you need to block the execution of a test. The TestGeneric class provides the following two methods to support interrupt handling:

  • '''set_handle_intr()
  • wait_on_interrupt()

If set_handle_intr() method is called from the test code it simply tells the framework that the test is interested in delivering the interrupt signal. The test can then be suspended until the delivery of this signal using the wait_on_interrupt() method.

So, let's assume following command sequence:

<command type="test" value="IntrExample" bg_id="1" /> <!-- (1) -->
<command type="exec" value="sleep 30" />              <!-- (2) -->
<command type="intr" bg_id="1" />                     <!-- (3) -->
<command type="wait" bg_id="1" />                     <!-- (4) -->

We're telling the framework that we want to run IntrExample test in the background (1), then wait for 30 seconds (2) and finally interrupt the test (3) and wait for it's exit (4).

The python code would look like the following:

TestIntrExample(TestGeneric):
    def run():
        self.set_handle_intr()

        ...
        # parse options
        ...
        # spawn workers or whatever that runs in background
        ...
        self.wait_on_interrupt() 
        # we're blocked until type="intr" command is executed

2.1.2 Self-managed interrupt handling

If you plan to use more complex interrupt signal handling you have to code it directly into your test code. As an example you can look at the code in TestPacketAssert.py or TCPConnect.py and TCPListen.py under /usr/share/lnst/test_modules directory.

Basically you need to register a method for the interrupt signal. The following code should do it:

TestIntrExample2(TestGeneric):
    def _interrupt_handler(self):
        self.do_whatever_needs_to_be_done_upon_signal_delivery()

    def run(self):
        signal.signal(signal.SIGINT, self._interrupt_handler)

In your recipe you will just execute the intr command with the proper bg_id parameter.