Features

Important

Please note that this part of the documentation is not yet finished. It will still be revised and updated.

Features implement functionality for a Device class. For example, if you have a device that could send messages, this device has to have a Feature class that implements the functionality to send these messages. A Device itself does never implement these functionality. A Device is only a container that contains different Feature classes.

The first definition of a feature is normally done in the Scenario class in a Scenario-Devices. If you remember, a scenario describes the interface, that should be implemented at least in the Setup-Devices, where the real implementation is provided. To understand this more easily, let’s take a look at the following example:

# file `scenario_my/features.py`
class SenderFeature(balder.Feature):

    def send(msg):
        raise NotImplementedError("this feature has to be implemented by feature class of setup")
# file `scenario_my/scenario_my.py`

from .features import SenderFeature

class MyScenario(balder.Scenario):

    class SenderDevice(balder.Device):

        sender = SenderFeature()

    def test_send_something():
        self.SenderDevice.sender.send("Hello World!")

Till now we have all interfaces we need in our test test_send_something, but we have no real implementation for that. So, let’s take a look to the setup implementation. For this we add a file and implement the feature for our setup there:

# file `my_setup/my_setup_features.py`
from ..my_scenario.features import SenderFeature

class PipeSenderFeature(SenderFeature):
    ...
    def send(msg):
        self.pipe.send(msg)

This feature class is not abstract anymore. It implements all the abstract methods (in our case the method send()). We can now use this feature in our setup class now:

# file `my_setup/setup_my.py`
from .my_setup_features import PipeSenderFeature

class SetupMy(balder.Scenario):

    class PipeDevice(balder.Device):
        pipe_sender = PipeSenderFeature()

Note

You should only provide non-abstract features inside an active setup

As you can see you have to implement the feature and instantiate it in the same way, like you have instantiated it in the scenario class.

Note

Often it is easier to rename these feature files in an way, that the filename says the origin of definition and where these features belongs to. This really depends on the project, so feel free to create a nice feature file structure.

Inner-Feature-Referencing

Sometimes it is necessary that you refer another feature from a feature. For example, in the real world, you would need access to the pipe from the example above. This can easily be done by using inner-feature-referencing.

Imagine, we have another feature ManagePipeFeature, that is also instantiated in the same setup-device where our PipeSenderFeature has already been instantiated:

# file `my_setup/setup_my.py`
from .my_setup_features import PipeSenderFeature

class SetupMy(balder.Scenario):

    class PipeDevice(balder.Device):
        pipe_manager = ManagePipeFeature()
        pipe_sender = PipeSenderFeature()

The implementation of the ManagePipeFeature doesn’t matter, but we want to use this feature inside our main PipeSenderFeature. For this, we have to inner-referencing it:

# file `my_setup/my_setup_features.py`
from ..my_scenario.features import SenderFeature

class PipeSenderFeature(SenderFeature):

    pipe = ManagePipeFeature()

    def send(msg):
        self.pipe.send(msg)

You simply have to instantiate it as class attribute inside your feature. This will automatically lead to the behavior, that Balder assumes that your feature only works with a device, that also provides an implementation for the ManagePipeFeature. The reference inside your feature will automatically provided by Balder on variation-level.

Autonomous-Features

Autonomous-Features are a special type of Feature classes. These features has no own methods or properties, they are only used to say:

This device has the feature IsRedFeature

So we want to filter them, because we only want a match with a device that has the same feature, but we can’t or don’t want to influence or interact with this device over the autonomous Feature.

The definition for such a autonomous feature, is really easy:

# file `scenario_my/scenario_my_features.py`

class PipeMirrorAutonomousFeature(balder.Feature):
    # an autonomous feature has no implementation
    pass

An Autonomous-Feature has the same behavior than a normal Feature. Since you can’t really use it, we highly recommend that you assign it to a property name with a beginning underscore:

# file `scenario_my/scenario_my.py`
from .features import SenderFeature, PipeMirrorAutonomousFeature

class MyScenario(balder.Scenario):

    class SenderDevice(balder.Device):

        sender = SenderFeature()

    @balder.connect(SenderDevice, over_connection=PipeConnection)
    class ReceiverDevice(balder.Device):
        # the autonomous feature
        _pipe_mirror = PipeMirrorAutonomousFeature()

This example shows a good real world example. Imagine, you want to test if the ReceiverDevice can mirror a message you have send with another SenderDevice. Picture that we can only influence the SenderDevice, but have no possibilities to interact with the ReceiverDevice. We only know, that this device can mirror the messages we sent. Here we can use a so-called Autonomous-Feature for our ReceiverDevice, because we know that the device must have it, but you can not influence it.

Your setup can use the same object. You don’t have to overwrite it, because you don’t want to add functionality to it, like we have done with the other features before. So we simply reuse this feature from scenario level in our setup:

# file `setup_my/setup_my.py`
from ..scenario_my.features import SenderFeature, PipeMirrorAutonomousFeature
from . import setup_my_features

class SetupMy(balder.Scenario):

    class PipeDevice(balder.Device):

        sender = setup_my_features.PipeSenderFeature()

    @balder.connect(PipeDevice, over_connection=PipeConnection)
    class MirrorDevice(balder.Device):
        # autonomous-device
        mirror = setup_my_features.PipeMirrorAutonomousFeature()

Bind features

A big advantage of Balder is that you are able to reuse components. This also applies to features. But mostly you will not use them under the exact same conditions. So maybe you want to use a SendFeature with a device that can only send messages over SMS while you also use this feature with a device that can only send its messages over tcp. So how we can handle this?

For this, it is time to bind features!

Features can be bind to special sub-connection-trees that are allowed to use with matching device for a assigned VDevice. A VDevice is used in features to define that you want another device that has some defined features, that interact with the current device, that implements this feature. If you want to limit the allowed connections the feature is able to use with this VDevice you can bind this feature class. This is the so called class-based-binding.

In addition to that, it is also possible to bind single methods of your feature to a subset of the allowed sub-connection tree or/and for the usage with one vDevice only. This allows it to define a method multiple times with different @for_vdevice bindings. So for example you can implement a method send that will be used if the device (that is assigned as vDevice) is connected over a TcpConnection. And additionally to that you have another method send that is bind to a UdpConnection. Depending on the current scenario/setup, Balder will use the correct method variation of send, after you call it in your testcase. This is the so called method-based-binding.

This section describes how this mechanism generally works. You can find a lot of more detailed explanations in the VDevices and method-variations section.

Class-Based-Binding

Class-Based-Binding can be defined with a Feature class decorator @for_vdevice().

# file `my_scenario/features.py`
import balder
from .connections import PipeConnection

@balder.for_vdevice("OtherPipeVDevice", with_connections=[PipeConnection])
class PipeSendReceive(balder.Feature):

    class OtherPipeVDevice(balder.VDevice):
        ...

The example describes that this feature needs a PipeConnection for every connection with the mapped device of the inner vDevice OtherPipeVDevice.

But how does you assign which device should be mapped with the vDevice?

You must provide this mapping in the constructor of the feature as a key-value pair. So for our example, you add the attribute OtherPipeVDevice="PipeDevice2" to the feature constructor to define that the scenario device PipeDevice2 should be mapped to the OtherPipeVDevice vDevice.

# file `my_scenario/my_scenario.py`
import balder
from .connections import PipeConnection

class ScenarioMy(balder.Scenario):

    @balder.connect("PipeDevice2", over_connection=PipeConnection)
    class PipeDevice1(balder.Device):
        pipe = PipeSendReceive(OtherPipeVDevice="PipeDevice2")

    class PipeDevice2(balder.Device):
        ...

    ...

Note

In Python it depends on the definition order if you can reference the device inside the @balder.connect(..) decorator. If the device is defined above your decorator, it is possible, otherwise not. For this, Balder always allows to provide the device as string too.

You can do this with different devices that could stand for different usages of this feature. So you can also add the PipeSendReceive feature of the PipeDevice2 should use a vdevice-device mapping with the PipeDevice1 too:

# file `my_scenario/my_scenario.py`
import balder
from .connections import PipeConnection

class ScenarioMy(balder.Scenario):

    @balder.connect("PipeDevice2", over_connection=PipeConnection)
    class PipeDevice1(balder.Device):
        pipe = PipeSendReceive(OtherPipeVDevice="PipeDevice2")

    class PipeDevice2(balder.Device):
        pipe = PipeSendReceive(OtherPipeVDevice="PipeDevice1")

    ...

As you can see the @balder.connect(PipeDevice2, over_connection=PipeConnection) is a required statement, because otherwise the requirement of our feature is not fulfilled. This scenario defines that the Feature requires a PipeConnection between the two devices PipeDevice1 and PipeDevice2. If we would use our fixture without the connection between these devices, it would result in an error, because the Feature is not applicable in this way.

Note

Balder checks if the requirement that is given with the Class-Based-Binding is available. If the requirement doesn’t match the class-based statement, it throws an error!

How does that influence the setup? - You are also able to define these vDevice-Device mapping in the setup. This is even often the case, because your setup normally uses the specific functionality. Your scenario should be as universal as possible. You can also use this mechanism on scenario level. If you want to find out more about this, take a look at the VDevices and method-variations section.

Method-Based-Binding

Often it is required to provide different implementations for different vDevices or different sub-connection-trees in a feature. For this you can use Method-Based-Binding.

Let’s assume, we have a feature that could send a message to another device. For this case, the connection type does not really matter, because the feature should support this requirement generally for every possible connection. So it is only important to test that the device can send a message to another device. It does not matter, how the feature send this message (at least at scenario level).

So in this example, we want to implement the messaging feature for one or more devices that can send messages over TCP or over Serial. With this, we add one VDevice which implements the receiving side by adding the feature RecvMessengerFeature to it. Our send feature itself should get two possible methods to send the message. One method to send the message over TCP and one method to send it over Serial.

Basically our scenario level implementation looks like:

# file `my_scenario/features.py`
import balder
from balder.connections import TcpConnection
from .connections import SerialConnection

@balder.for_vdevice(with_vdevice='OtherVDevice', [TcpConnection, SerialConnection])
class SendMessengerFeature(balder.Feature):

    class OtherVDevice(balder.VDevice):
        msg = RecvMessengerFeature()

    def send(msg) -> None:
        raise NotImplementedError("this method has to be implemented in setup")

As you can see, we have defined the inner vDevice OtherVDevice here. We want to use the feature RecvMessengerFeature with this vDevice. For this we instantiate it as class property of the OtherVDevice. This allows us, to define the requirement the mapped device should implement already in this feature.

Note

The elements given in the inner vDevice class definition is a MUST HAVE, which means that the statement has to be available in the mapped device later, otherwise it would throw an error.

Till now the scenario-feature doesn’t use some Method-Based-Bindings. This will change in a few moments, when we implement the setup-level representation of this feature.

Before we take action for the setup implementation, we want to create a Scenario using this newly created feature. For this, we want to implement a example scenario with two devices that communicates with each other.

# file `my_scenario/scenario_my.py`
import balder
from balder.connections import TcpConnection
from .connections import SerialConnection
from .features import SendMessengerFeature, RecvMessengerFeature

class ScenarioMy(balder.Scenario):

    class SendDevice(balder.Device):
        send = SendMessengerFeature(RecvVDevice="RecvDevice")

    @balder.connect(SendDevice, over_connection=TcpConnection)
    class RecvDevice(balder.VDevice):
        recv = RecvMessengerFeature(SendVDevice="SendDevice")

    def test_check_communication(self):
        SEND_DATA = "Hello World"
        self.RecvDevice.recv.start_async_receiving()
        self.SendDevice.send.send(SEND_DATA)
        all_messages = self.RecvDevice.recv.get_msgs()
        assert len(all_messages) == 1, "have not received anything"
        assert all_messages[0] == SEND_DATA, "have not received the sent data

As you can see we have created a mapping for the inner VDevice to an real defined scenario Device by using the name of the inner vDevice as key and the name of the real device as value in the constructor. We also implement a basic test, that should check the communication.

So let’s start to implement the setup level. We can implement our earlier defined feature by simply inheriting from it. In this child class, we want to provide two different implementations for our abstract method send. We want to provide two different implementation, one for a Serial connection and one for the connection over tcp. For this we can use the Method-Based-Binding decorator:

# file `my_setup/features.py`
import balder
from balder.connections import TcpConnection
from ..my_scenario.features import MessengerFeature
from .connections import SerialConnection

class SetupSendMessengerFeature(MessengerFeature):

    @balder.for_vdevice(MessengerFeature.OtherVDevice, with_connection=SerialConnection)
    def send(msg) -> None:
        serial = MySerial(com=..)
        ...

    @balder.for_vdevice(MessengerFeature.OtherVDevice, with_connection=TcpConnection)
    def send(msg) -> None:
        sock = socket.socket(...)
        ...

As you can see you can provide completely different implementations for the different sub-connection types. Depending on the really used connection (the relative possible sub-connection the setup devices are connected with each other), the corresponding method variation will be called.

So take a look at the following Setup, that matches our Scenario and supports both connections.

# file `my_setup/setup_my.py`
import balder
from balder.connections import TcpConnection
from .features import SetupSendMessengerFeature, SetupRecvMessengerFeature

class MySetup(balder.Setup):

    @balder.connect(SlaveDevice, over_connection=balder.Connection.based_on(SerialConnection, TcpConnection))
    class MainDevice(balder.Device):
        msg = SetupSendMessengerFeature()

    class SlaveDevice(balder.Device):
        recv = SetupRecvMessengerFeature()

This example connects the two relevant devices over a TcpConnection with each other, because the scenario defines, that the devices should be connected over an TcpConnection. If the test now uses on of our methods MyMessengerFeature.send(..), the variation with the decorator @balder.for_vdevice(..., over_connection=[TcpConnection]) will be used.

If one would exchange the connection with the SerialConnection, Balder would select the method variation with the decorator @balder.for_vdevice(MessengerFeature.OtherVDevice, with_connection=SerialConnection).

Note

In the setup example there is no vDevice-Device mapping anymore. This isn’t needed, because we have already fixed this at scenario level.

Feature inheritance

Warning

This section is still under development.