Contents

Dezyne: Creating Glue Code and Testing

Introduction

Handwritten code is the most error prone part of a Dezyne project. Some often-seen mistakes are:

  • Throwing an exception on the Dezyne thread.
  • Mistakes with the difference between asynchronous and synchronous events.
  • Wrong reply value.
  • Multi-threading mistakes.

Many of these mistakes are related to not strictly following the Dezyne interface. Besides that, the functional behavior of the handwritten code could also contain bugs.

In this article I will introduce the handwritten code for connecting the Dezyne models to the underlying code to control a Motor and show how to rigorously test it. For testing I will use GoogleTest and gMock.

Native Hal::Motor Interface

Shown below is the C++ interface of the hardware abstraction layer (HAL) of a motor. It is a simplified example. Each method can throw an exception when something is wrong (except the destructor).

The interface is asynchronous, meaning each call will return immediately and the action is executed in the background. This makes it very suitable to use in a single threaded Dezyne model.

The Home method will start the homing of the motor. The IsHoming method can be used to check if the homing is done. The Move method will start the moving of the motor to a specific location. It will throw an exception if the motor is not homed. The IsMoving method can be used to find out if the motor is moving. The Stop method will directly stop the movement or homing of the motor.

With the HomingChanged and MovingChanged methods a callback function can be registered that will be called when the IsHoming or IsMoving state changes. It is not allowed to call methods of the motor interface in this callback.

class Motor
{
public:
    virtual ~Motor() = default;

    virtual void Home() = 0;
    virtual bool IsHoming() = 0;
    virtual void Move(double pos) = 0;
    virtual bool IsMoving() = 0;
    virtual void Stop() = 0;

    virtual void HomingChanged(std::function<void()> event) = 0;
    virtual void MovingChanged(std::function<void()> event) = 0;
};

Integrating Handwritten Code Into Dezyne

Not Using the Thread-safe Shell

When generating code for a system component, Dezyne can be instructed to generate a thread-safe shell. This generated code will then include a thread on which the Dezyne models will run. It will move events from other threads to the dezyne thread. For in-events on provided ports a call will be made using dzn::shell and out-events of required ports will be decoupled and posted on the dzn::pump. This makes using the system component thread safe and easier to use. The tread-safe shell is not covered in this article.

The Dezyne thread can also be managed manually. This can be done by creating a dzn::pump and giving it to the dzn::locator that will be used for constructing the Dezyne components. When calling in-events, these will have to be dispatched onto the dzn::pump. This can be done with the helper function dzn::shell. By doing it like this, Dezyne features like blocking and defer can still be used, while keeping the threading semantics (difference between synchronous or asynchronous events) of Dezyne. I prefer this method.

How to Write the Handwritten Code

There are three ways to integrate handwritten code:

  1. Using Component Skeletons
  2. Using Manual Components
  3. Connect Directly (no components)

In Dezyne we can declare a foreign component. This is done by omitting the behavior part in the component declaration. This tells Dezyne the component is available and can be instantiated using system components. It will also generate a skeleton that can be used to write a component by inheriting from it. The skeleton takes care of some boilerplate code.

The other way is to create a component manually. By looking at the generated code of other Dezyne components it is easy to figure out how to do this. With Dezyne 2.18 the code integration has become more trivial, making it easier to create components like this. This is not covered in this article.

Instead of writing a component it is also possible to directly connect to the events of the ports of a Dezyne component. This way we do not have to define and connect a component. It is the easiest way to connect handwritten code to Dezyne components. This will be covered in a later article.

How to Connect the Handwritten Code

When using components (skeletons or manual) to write handwritten code, there are two ways the code can be connected to the rest of the system:

  1. Connect components to the outside ports of a Dezyne component or system.
  2. Let Dezyne instantiate the component as part of a Dezyne system and connect it automatically.

To connect to the outside ports of a component, we will have to instantiate the component and then use the dzn::connect() command to connect it to the other component. We will have to connect provided ports to required ports. This is depicted in the following image:

/posts/dezyne-test-glue/integrating1.png

The System contains the MotorComponent and the MotorGlueComponent. The MotorComponent is a system also containing a foreign (light green) TimerComponent. The two components can be connected. Note that the MotorImpl has two required ports. One is for the timer and the other one is for the glue. It is not clear in this picture.

The other way is to let Dezyne instantiate the component. This is depicted in the following image:

/posts/dezyne-test-glue/integrating2.png

Here only the MotorComponent needs to be instantiated by the client. Dezyne will create an instance of the MotorGlueComponent as part of the MotorComponent and connect it. For this to work, we will need to put the definition of the component in a header file with a specific name. The name is based on the namespace and component name of the foreign component declaration.

Which Method to Choose

I do not have rules for when to use which method, but I have my preferences. Here are some of my preferences:

When using a provides port I will just directly connect to the events. This means that I will connect callback functions to the out-events, and I will call the in-events through the dzn::shell helper function.

When I want to instantiate the component by system components, I will use foreign components using the component skeletons. I do this for the timer component, configuration components and helper components that could do the data manipulation and other tasks.

For making mock classes I will manually create components. These components are only needed for the tests and to generate the skeletons for it would be too much. These mock classes can then be connected to the ports of the component under test.

For glue components, what this article is about, I use foreign components using component skeletons. I will make the instances using system components. This means that the HAL objects will need to be registered in the locator, so the glue component can access it.

Dezyne MotorGlue Interface

Based on the Motor C++ interface above a Dezyne interface has to be defined. Since multiple motors need to be supported, there must some way to decide which component is representing which motor.

We want little to no state in the Glue interface. The implementation should be as trivial as possible because the handwritten code cannot be verified. But to be able to know which motor is being represented and then to connect to the events an Initialize in-event is defined, which must always be called first. It goes together with a Terminate in-event to un-initialize the component again. This can be seen as the constructor and destructor of the component. It is good standard practice to add these events to components. But I will omit them if nothing needs to be initialized.

interface MotorGlue {
    in Result Initialize(in string id);
    in void Terminate();
    in Result Home();
    in Result IsHoming();
    in Result Move(in double pos);
    in Result IsMoving();
    in Result Stop();

    out void OnHomingChanged();
    out void OnMovingChanged();

    behavior {
        enum State {
            Uninitialized,
            Operational
        };
        State state = State.Uninitialized;

        [state.Uninitialized]
        {
            on Initialize:
            {
                [true] { reply(Result.Ok); state = State.Operational; }
                [true] reply(Result.Fail);
            }
            on Terminate: {}
        }

        [state.Operational]
        {
            on Terminate: state = State.Uninitialized;
            on Home, Move, Stop:
            {
                [true] reply(Result.Ok);
                [true] reply(Result.Fail);
            }
            on IsHoming, IsMoving:
            {
                [true] reply(Result.True);
                [true] reply(Result.False);
                [true] reply(Result.Fail);
            }
        }

        on optional: OnHomingChanged;
        on optional: OnMovingChanged;
    }
}

The Initialize takes a string parameter that identifies which HAL Motor should be used. In the initialize declaration we see the [true] guards. This is used to indicate non-determinism. It states that there are two results when calling Initialize.

Only when the MotorGlue is initialized the other methods can be called. Each method except Terminate can always return a Result.Fail. This indicates something went wrong. MotorGlue uses it to indicate an exception was thrown in the C++ code.

The two optional events indicate that at any moment the OnHomingChanged and OnMovingChanged can be fired. For glue code it is best practice to put these at the top level, even if it is tempting to put them in the operational state. The reason to put them at the top level is that we need to decouple the incoming invents from the HAL. The events are queued, and delivery can be delayed, meaning there could be race conditions. By always allowing these events to fire we will have to take care of race conditions in the components that use this interface. The verifier will make sure we do.

The Result being used is an enum that is defined as follows.

enum Result {
    Ok,
    Fail,
    Error,
    True,
    False
}

This Result enum is used globally in this project. All reply-values can be put in this enum. This way the same Result can be reused. The interface will specify which reply values are allowed for which in-events and the verifier makes sure no one returns unallowed Result values.

Dezyne Foreign Component Declaration

We are going to use the component skeleton to write the C++ glue code. For this we need to declare the foreign component. This can be done by writing a component without the behavior section. This declaration will let Dezyne generate the code for the skeleton and make it possible to instantiate this handwritten component in system components.

The file MotorGlueComponentSkel.dzn:

component MotorGlueComponent {
    provides MotorGlue api;
}

The name of the foreign component cannot be the same as its filename. Since Dezyne will only generate the skeleton, I end the filename with Skel.

With this declaration, the code generator will create MotorGlueComponentSkel.hh (based on the filename) with the skeleton code inside. When a Dezyne system component wants to instantiate the foreign component, it will include MotorGlueComponent.hh to get the definition of the foreign component. This header file needs to be provided by the developer.

With the foreign component declaration above, Dezyne will generate something like the following skeleton code. I only included the interesting parts.

namespace skel
{
    struct MotorGlueComponent: public dzn::component
    {
        dzn::meta dzn_meta;
        ... snip ...
        MotorGlue api;
        ... snip ...
        MotorGlueComponent(dzn::locator const& locator);
        virtual ~MotorGlueComponent();
    private:
        virtual Result api_Initialize(std::string id) = 0;
        virtual void api_Terminate() = 0;
        virtual Result api_Home() = 0;
        virtual Result api_IsHoming() = 0;
        virtual Result api_Move(double pos) = 0;
        virtual Result api_IsMoving() = 0;
        virtual Result api_Stop() = 0;
    };
}

Implementation of the Foreign Component

Header File

The header MotorGlueComponent.hh looks as follows:

#include "MotorGlueComponentSkel.hh"

struct MotorGlueComponent : skel::MotorGlueComponent
{
    explicit MotorGlueComponent(const dzn::locator& locator);

    Result api_Initialize(std::string id) override;
    void api_Terminate() override;
    Result api_Home() override;
    Result api_IsHoming() override;
    Result api_Move(double pos) override;
    Result api_IsMoving() override;
    Result api_Stop() override;

    void LogException(const std::exception& ex);

    const dzn::locator& m_locator;
    dzn::pump& m_pump;
    Hal::Motor* m_motor;
};

The header file of the skeleton is included and the class MotorGlueComponent is declared that inherits from the skeleton overriding all abstract methods. Next sections will show the implementation of the methods.

Constructor

For Dezyne to be able to instantiate the component from a system component we will need a constructor that accepts a single argument of the type dzn::locator&. The locator is used to inject dependencies into the Dezyne environment.

We can make different constructors and inject dependencies using extra parameters, but to use these we will have to instantiate and connect the components manually.

Objects are retrieved from the locator by type. Here can be seen that a reference to the dzn::pump is retrieved and stored in the member variable m_pump.

MotorGlueComponent::MotorGlueComponent(const dzn::locator& locator) :
    skel::MotorGlueComponent(locator),
    m_locator(locator),
    m_pump(locator.get<dzn::pump>()),
    m_motor(nullptr)
{
}

Initialize

The Initialize method acquires the right instance of the motor. It does this by getting the motor from the locator by id and storing the pointer to it in the m_motor member variable.

Since the Changed events from the Motor come from another thread, it must be put on the Dezyne thread, which is managed by dzn::pump. There are two choices. One is to do a blocking call using dzn::shell, but this is seldom the right choice.

m_motor->HomingChanged([this]
{
    dzn::shell(m_pump, api.out.OnHomingChanged);
});

As stated earlier, it is not allowed to call the methods of the motor in the Changed callbacks and this way we cannot guarantee that is not happening. The event will have to be decoupled. This can be done by posting the event callback to the queue of the dzn::pump. The pump will execute it as soon as the pump is finished executing its previous tasks.

Not finding the motor could result in an exception, which will have to be caught. The exceptions will be logged. Normally we would want to communicate the exception message back to the caller, but for simplicity this has been left out of these examples.

When all goes fine, we return Result::Ok, otherwise we return a Result::Fail as defined in the MotorGlue interface.

Result MotorGlueComponent::api_Initialize(std::string id)
try
{
    m_motor = &m_locator.get<Hal::Motor>(id);
    m_motor->HomingChanged([this]
    {
        m_pump(api.out.OnHomingChanged);
    });
    m_motor->MovingChanged([this]
    {
        m_pump(api.out.OnMovingChanged);
    });
    return Result::Ok;
}
catch(const std::exception& ex)
{
    LogException(ex);
    return Result::Fail;
}

Terminate

Terminate can always be called. It will have to handle the case m_motor is not initialized. When needed it will deregister the callbacks and clear the m_motor variable.

void MotorGlueComponent::api_Terminate()
{
    if (m_motor)
    {
        m_motor->HomingChanged(nullptr);
        m_motor->MovingChanged(nullptr);
        m_motor = nullptr;
    }
}

Move

This is what glue code should look like. It is straight forward. Just pass the parameters to the underlying implementation, handle exceptions and return the correct reply-values.

Result MotorGlueComponent::api_Move(double pos)
try
{
    m_motor->Move(pos);
    return Result::Ok;
}
catch(const std::exception& ex)
{
    LogException(ex);
    return Result::Fail;
}

IsMoving

Also, for IsMoving we catch exceptions.

Result MotorGlueComponent::api_IsMoving()
try
{
    return m_motor->IsMoving() ? Result::True : Result::False;
}
catch(const std::exception& ex)
{
    LogException(ex);
    return Result::Fail;
}

Remaining Methods

The implementation of the other methods follows the same pattern.

Testing the Foreign Component

Mock for the Hal::Motor

Most of the mock for the native Motor class is basic. Only for the HomingChanged and MovingChanged callback registration some extra helper functions are needed. I added two actions that will store the callback functions in the onHomingChanged and onMovingChanged member variables.

class MotorMock : public Hal::Motor {
public:
    MOCK_METHOD(void, Home, (), (override));
    MOCK_METHOD(bool, IsHoming, (), (override));
    MOCK_METHOD(void, Move, (double pos), (override));
    MOCK_METHOD(bool, IsMoving, (), (override));
    MOCK_METHOD(void, Stop, (), (override));

    MOCK_METHOD(void, HomingChanged, (std::function<void()> event), (override));
    MOCK_METHOD(void, MovingChanged, (std::function<void()> event), (override));

    auto HomingChangedAction()
    {
        return [this](auto event) { onHomingChanged = event; };
    }

    auto MovingChangedAction()
    {
        return [this](auto event) { onMovingChanged = event; };
    }

    std::function<void()> onHomingChanged;
    std::function<void()> onMovingChanged;
};

Mock for Tracking Event Calls

This mock will intercept the events emitted by the MotorGlue.

struct MotorGlueEventsMock
{
    MOCK_METHOD(void, OnHomingChanged, ());
    MOCK_METHOD(void, OnMovingChanged, ());
};

Test Fixture

Everything comes together in the test fixture. The test fixture will set up the Dezyne environment and connect everything together.

A Dezyne environment consists of a dzn::locator, dzn::runtime and optionally a dzn::pump. In our case, we use the pump for the Dezyne thread, so it must be included. Then the motor mock is injected into the locator. Note that we need to specify the exact type for which it should be registered, and, in this case, we also give it a name (motorId).

The out-events are connected to the event mocks. This way expectations can be set on the out-events.

The dzn::check_binding call verifies that all in and out-events of the glue have been connected.

In the destructor the dzn::pump is stopped. This will wait until the queue is empty and make sure no new tasks will be executed before destroying the fixture (and checking the mocks expectations).

struct MotorGlueComponentTest : testing::Test {
    MotorGlueComponentTest() :
        glue(locator
            .set(runtime)
            .set(pump)
            .set(static_cast<Hal::Motor&>(motor), motorId)
        )
    {
        glue.api.out.OnHomingChanged = [this]
        {
            events.OnHomingChanged();
        };
        glue.api.out.OnMovingChanged = [this]
        {
            events.OnMovingChanged();
        };

        dzn::check_bindings(glue);
    }

    ~MotorGlueComponentTest() override
    {
        pump.stop();
    }

    const std::string motorId = "abc";
    dzn::locator locator;
    dzn::runtime runtime;
    dzn::pump pump;
    testing::StrictMock<MotorMock> motor;
    testing::StrictMock<MotorGlueEventsMock> events;
    MotorGlueComponent glue;
};

Test Intialize

For Initialize the two paths are tested. One where it can find the motor implementation, and one where it cannot. If it can find the motor implementation the callback functions will be registered.

dzn::shell is used to send the in-events on the Dezyne thread. It will wait until the event has been processed before continuing.

TEST_F(MotorGlueComponentTest, InitializeSucceedsWhenItFindsMotor)
{
    EXPECT_CALL(motor, HomingChanged(NotNull()));
    EXPECT_CALL(motor, MovingChanged(NotNull()));
    ASSERT_EQ(Result::Ok, dzn::shell(pump, glue.api.in.Initialize, motorId));
}

TEST_F(MotorGlueComponentTest, InitializeFailsWhenItDoesNotFindMotor)
{
    ASSERT_EQ(Result::Fail, dzn::shell(pump, glue.api.in.Initialize, "wrongId"));
}

Test Terminate

Terminate should not do anything. But it should still be tested, because this is the code path where the if statement returns false.

TEST_F(MotorGlueComponentTest, Terminate)
{
    dzn::shell(pump, glue.api.in.Terminate);
}

Operational Test Fixture

We have now tested all allowed in events in the State.Uninitialized state (see MotorGlue interface). Next, we need to test all events while the component is in the State.Operational state. To do this we use a fixture to represent the State.Operational state.

We use special actions to store the callback functions.

struct MotorGlueComponentTest_Operational : MotorGlueComponentTest
{
    void SetUp() override
    {
        EXPECT_CALL(motor, HomingChanged(NotNull())).WillOnce(motor.HomingChangedAction());
        EXPECT_CALL(motor, MovingChanged(NotNull())).WillOnce(motor.MovingChangedAction());
        ASSERT_EQ(Result::Ok, dzn::shell(pump, glue.api.in.Initialize, motorId));

        ASSERT_TRUE(::testing::Mock::VerifyAndClear(&motor));
    }
};

Here I am doing something that is not allowed but works for me. To use fixtures, we have the SetUp method to setup the testing environment to the desired state. For this we need the mock objects. After that we need the mock objects for the test. But the Google Mock documentation states that you are not allowed to intermingle EXPECT_CALLs with calls to the mocks. It also states that using the mocks after a VerifyAndClear call is not allowed. Both cases are undefined behavior. How can we use the mock objects in fixtures that use the mocks to do the setup? I have not found a workable solution for this, and I am open to suggestions.

But it seems that the VerifyAndClear call makes things work as expected. The expectations and actions are cleared, and the mock objects are ready for reuse. I assume there are edge cases where this does not result in the expected behavior and that that is the reason it is defined as undefined behavior. Maybe this is related to multi-threading. Since this is part of the tests, and the tests are strict, I am willing to take this risk for now.

Test Terminate Again

Now the terminate should deregister the callbacks.

TEST_F(MotorGlueComponentTest_Operational, Terminate)
{
    EXPECT_CALL(motor, HomingChanged(IsNull()));
    EXPECT_CALL(motor, MovingChanged(IsNull()));
    dzn::shell(pump, glue.api.in.Terminate);
}

Test Move

The Move test makes sure the parameters are passed correctly. Both the normal path and the path where an exception is thrown are tested.

TEST_F(MotorGlueComponentTest_Operational, MoveReturnsOk)
{
    double value = 1234.0;
    EXPECT_CALL(motor, Move(value));
    ASSERT_EQ(Result::Ok, dzn::shell(pump, glue.api.in.Move, value));
}

TEST_F(MotorGlueComponentTest_Operational, MoveReturnsFail)
{
    double value = 5678.0;
    EXPECT_CALL(motor, Move(value)).WillOnce(Throw(std::runtime_error("Move failed")));
    ASSERT_EQ(Result::Fail, dzn::shell(pump, glue.api.in.Move, value));
}

Test IsMoving

For IsMoving all three paths are tested.

TEST_F(MotorGlueComponentTest_Operational, IsMovingReturnsTrue)
{
    EXPECT_CALL(motor, IsMoving()).WillOnce(Return(true));
    ASSERT_EQ(Result::True, dzn::shell(pump, glue.api.in.IsMoving));
}

TEST_F(MotorGlueComponentTest_Operational, IsMovingReturnsFalse)
{
    EXPECT_CALL(motor, IsMoving()).WillOnce(Return(false));
    ASSERT_EQ(Result::False, dzn::shell(pump, glue.api.in.IsMoving));
}

TEST_F(MotorGlueComponentTest_Operational, IsMovingReturnsFail)
{
    EXPECT_CALL(motor, IsMoving()).WillOnce(Throw(std::runtime_error("IsMoving failed")));
    ASSERT_EQ(Result::Fail, dzn::shell(pump, glue.api.in.IsMoving));
}

Test Events

The events are tested by triggering the callbacks and checking if they are received.

TEST_F(MotorGlueComponentTest_Operational, OnHomingChangedEvent)
{
    EXPECT_CALL(events, OnHomingChanged());
    motor.onHomingChanged();
}

TEST_F(MotorGlueComponentTest_Operational, OnMovingChangedEvent)
{
    EXPECT_CALL(events, OnMovingChanged());
    motor.onMovingChanged();
}

Test Remaining Methods

All the other events are tested is the same manner.

Running the Test

Below some output from running the test. The test output is intermingled with Dezyne trace output and the LogException output. Normally this should be redirected elsewhere.

We can see the trace through the MotorGlue and when it throws its exceptions. For example, the MotorGlueComponentTest.InitializeFailsWhenItDoesNotFindMotor gets an exception that no Hal::Motor with the name wrongId could be found, and therefore it returns a Result:Fail.

Running main() from gmock_main.cc
[==========] Running 18 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from MotorGlueComponentTest
[ RUN      ] MotorGlueComponentTest.InitializeSucceedsWhenItFindsMotor
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
[       OK ] MotorGlueComponentTest.InitializeSucceedsWhenItFindsMotor (0 ms)
[ RUN      ] MotorGlueComponentTest.InitializeFailsWhenItDoesNotFindMotor
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
MotorGlueComponent exception: <N3Hal5MotorE,"wrongId"> not available
<external>.api.Result:Fail <- MotorGlueComponent.api.Result:Fail
[       OK ] MotorGlueComponentTest.InitializeFailsWhenItDoesNotFindMotor (0 ms)
[ RUN      ] MotorGlueComponentTest.Terminate
<external>.api.Terminate -> MotorGlueComponent.api.Terminate
<external>.api.return <- MotorGlueComponent.api.return
[       OK ] MotorGlueComponentTest.Terminate (0 ms)
[----------] 3 tests from MotorGlueComponentTest (0 ms total)

[----------] 15 tests from MotorGlueComponentTest_Operational
[ RUN      ] MotorGlueComponentTest_Operational.Terminate
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.Terminate -> MotorGlueComponent.api.Terminate
<external>.api.return <- MotorGlueComponent.api.return
[       OK ] MotorGlueComponentTest_Operational.Terminate (0 ms)
... snip ...
[ RUN      ] MotorGlueComponentTest_Operational.MoveReturnsOk
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.Move -> MotorGlueComponent.api.Move
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
[       OK ] MotorGlueComponentTest_Operational.MoveReturnsOk (0 ms)
[ RUN      ] MotorGlueComponentTest_Operational.MoveReturnsFail
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.Move -> MotorGlueComponent.api.Move
MotorGlueComponent exception: Move failed
<external>.api.Result:Fail <- MotorGlueComponent.api.Result:Fail
[       OK ] MotorGlueComponentTest_Operational.MoveReturnsFail (0 ms)
[ RUN      ] MotorGlueComponentTest_Operational.IsMovingReturnsTrue
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.IsMoving -> MotorGlueComponent.api.IsMoving
<external>.api.Result:True <- MotorGlueComponent.api.Result:True
[       OK ] MotorGlueComponentTest_Operational.IsMovingReturnsTrue (0 ms)
[ RUN      ] MotorGlueComponentTest_Operational.IsMovingReturnsFalse
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.IsMoving -> MotorGlueComponent.api.IsMoving
<external>.api.Result:False <- MotorGlueComponent.api.Result:False
[       OK ] MotorGlueComponentTest_Operational.IsMovingReturnsFalse (0 ms)
[ RUN      ] MotorGlueComponentTest_Operational.IsMovingReturnsFail
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.IsMoving -> MotorGlueComponent.api.IsMoving
MotorGlueComponent exception: IsMoving failed
<external>.api.Result:Fail <- MotorGlueComponent.api.Result:Fail
[       OK ] MotorGlueComponentTest_Operational.IsMovingReturnsFail (0 ms)
... snip ...
[ RUN      ] MotorGlueComponentTest_Operational.OnHomingChangedEvent
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.OnHomingChanged <- MotorGlueComponent.api.OnHomingChanged
[       OK ] MotorGlueComponentTest_Operational.OnHomingChangedEvent (0 ms)
[ RUN      ] MotorGlueComponentTest_Operational.OnMovingChangedEvent
<external>.api.Initialize -> MotorGlueComponent.api.Initialize
<external>.api.Result:Ok <- MotorGlueComponent.api.Result:Ok
<external>.api.OnMovingChanged <- MotorGlueComponent.api.OnMovingChanged
[       OK ] MotorGlueComponentTest_Operational.OnMovingChangedEvent (0 ms)
[----------] 15 tests from MotorGlueComponentTest_Operational (3 ms total)

[----------] Global test environment tear-down
[==========] 18 tests from 2 test suites ran. (4 ms total)
[  PASSED  ] 18 tests.

Download and Run Code

The code can be downloaded here. I have only tested it on Linux. Dezyne 2.18.1 or higher must be installed and in the search path.

Use the following commands to run the unit tests:

unzip code.zip
cd unittesting
mkdir build
cd build
cmake .. -DDEZYNE_RUNTIME_PATH=/opt/dezyne/runtime/c++
make
./DezyneTest

Visual Studio Code settings have been included. Update the settings.json to point to the dezyne runtime before using it.

Conclusion

Due to the inability to verify hand-written code, meticulous care must be exercised throughout the implementation process. Even if the C++ integration code appears straightforward, thorough testing remains crucial. This article demonstrates how to write tests for glue code, and it even found a mistake in the original code where the Initialize method failed to check for the initialization of m_motor. Without rigorous testing, there’s a significant likelihood that such a bug would be encountered in production, potentially leading to significant user-facing issues.

In the next article I will continue with implementing Dezyne components and writing tests for them.