Introduction
In the Creating Glue Code and Testing article, I describe different ways to integrate handwritten code. The third method mentioned is to directly connect the handwritten code to the provided ports of the top level system. In this article I will discuss how to do this, how to test directly connected glue and what some pros and cons are.
Approach for Directly Connected Glue
This method does not use the by Dezyne generated foreign component skeleton and also doesn’t register any foreign components for Dezyne to instantiate. It uses trivial integration to connect the handwritten code to the Dezyne components. This is done by assigning lambda functions to the out event of provided ports an in events of required ports. To make this method composable and testable it is good to put the implementation in a class and mimic the style of the foreign components. The interface to connect to and other dependency can by passed into the constructor of this class. The component that is created this way can not be instantiated and connected by Dezyne. The developer will have to instantiate and connect it.
Next I will introduce a new MotorGlue
interface and the connectable MotorGlue
component. To distinguish this glue component from the previous version I will call it MotorGlueConnectable
.
New MotorGlue
Interface
In the previous version the id was passed to the Initialize
call. This is not needed anymore since the dependency to the Hal::Motor
will be given directly to the glue component instance. As a replacement I choose to add the GetId
in-event. This can be used to query the id of the motor. For example the x-axis motor can return the id: xaxis
. The rest of the interface is unchanged.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| interface MotorGlue {
in Result Initialize();
in void Terminate();
in void GetId(out string id);
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 GetId: {}
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;
}
}
|
Connectable Motor Glue Component
The connectable glue looks very similar to the MotorGlueComponent
of the previous article. I will highlight the changes.
MotorGlueConnectable.h
All dependencies that are needed are passed to the constructor. For each in-event of the interface a method is defined. This mimics the way Dezyne generated components are structured.
pump
- the Dezyne pump;glue
- the required interface to connect to;motor
- the dependency needed for this glue component;id
- the id of this motor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| struct MotorGlueConnectable
{
MotorGlueConnectable(
dzn::pump& pump,
MotorGlue& glue,
Hal::Motor& motor,
const std::string& id
);
Result api_Initialize();
void api_Terminate();
void api_GetId(std::string& id);
Result api_Home();
Result api_IsHoming();
Result api_Move(double pos);
Result api_IsMoving();
Result api_Stop();
void LogException(const std::exception& ex);
dzn::pump& m_pump;
MotorGlue& m_glue;
Hal::Motor& m_motor;
std::string m_id;
};
|
MotorGlueConnectable.cpp
The constructor stores the arguments in member variables and then connects all in-events to the passed in interface.
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| MotorGlueConnectable::MotorGlueConnectable(
dzn::pump& pump,
MotorGlue& glue,
Hal::Motor& motor,
const std::string& id) :
m_pump(pump),
m_glue(glue),
m_motor(motor),
m_id(id)
{
glue.in.Initialize = [this] { return api_Initialize(); };
glue.in.Terminate = [this] { api_Terminate(); };
glue.in.GetId = [this](std::string& id) { api_GetId(id); };
glue.in.Home = [this] { return api_Home(); };
glue.in.IsHoming = [this] { return api_IsHoming(); };
glue.in.Move = [this](double pos) { return api_Move(pos); };
glue.in.IsMoving = [this] { return api_IsMoving(); };
glue.in.Stop = [this] { return api_Stop(); };
}
|
Initialize does not need to find the hal class and can directly subscribe to events.
23
24
25
26
27
28
29
30
31
32
33
34
| Result MotorGlueConnectable::api_Initialize()
try
{
m_motor.HomingChanged([this] { m_pump(m_glue.out.OnHomingChanged); });
m_motor.MovingChanged([this] { m_pump(m_glue.out.OnMovingChanged); });
return Result::Ok;
}
catch(const std::exception& ex)
{
LogException(ex);
return Result::Fail;
}
|
Terminate will just unregister the events.
42
43
44
45
46
47
48
49
50
51
| void MotorGlueConnectable::api_Terminate()
try
{
m_motor.HomingChanged(nullptr);
m_motor.MovingChanged(nullptr);
}
catch(const std::exception& ex)
{
LogException(ex);
}
|
GetId is a new in-event which will return the id of the hal class.
53
54
55
56
| void MotorGlueConnectable::api_GetId(std::string& id)
{
id = m_id;
}
|
The rest of the implementation of the glue is unchanged.
Testing the Connectable MotorGlue Component
By connecting the connectable glue to a Dezyne component, we can test it in the same way as normal Dezyne component. I will give an example below.
Helper Classes To Create Dezyne Components
For the testing of the glue component we use the helper classes introduced in Dezyne: Implementing and Testing Components article. This is for Dezyne 2.18 or later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| struct Component : dzn::component
{
dzn::meta dzn_meta;
dzn::runtime& dzn_runtime;
const dzn::locator& dzn_locator;
Component(const char* name, const char* type, const dzn::locator& locator) :
dzn_meta({name, type, nullptr, {}, {}, {}}),
dzn_runtime(locator.get<dzn::runtime>()),
dzn_locator(locator)
{
}
};
template <typename Interface>
struct Provides : Interface
{
Provides(const char* name, Component* component) :
Interface(dzn::port::meta{
{name, this, component, &component->dzn_meta},
{name, nullptr, nullptr, nullptr}},
component
)
{
component->dzn_meta.ports_connected.emplace_back([this]
{
this->dzn_check_bindings();
});
}
Interface& operator*()
{
return *this;
}
};
|
MotorGlueProxy
Using these helper classes we create a MotorGlueProxy
component. We will connect the MotorGlueConnectable
to this component and then test it as normal.
1
2
3
4
5
6
7
8
9
10
| struct MotorGlueProxy : Component
{
Provides<MotorGlue> api;
explicit MotorGlueProxy(dzn::locator& locator) :
Component("MotorGlueProxy", "MotorGlueProxy", locator),
api("api", this)
{
}
};
|
Test Fixtures
We create this proxy glue in the test fixture. In the constructor we connect our MotorGlueConenctable
to it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| struct MotorGlueConnectableTest : testing::Test {
MotorGlueConnectableTest() :
glue(locator
.set(runtime)
.set(pump)
)
{
glue.api.out.OnHomingChanged = [this]
{
events.OnHomingChanged();
};
glue.api.out.OnMovingChanged = [this]
{
events.OnMovingChanged();
};
motorGlueConnectable = std::make_unique<MotorGlueConnectable>(
pump,
glue.api,
motor,
motorId
);
dzn::check_bindings(glue);
}
~MotorGlueConnectableTest()
{
pump.stop();
}
const std::string motorId = "abc";
dzn::locator locator;
dzn::runtime runtime;
dzn::pump pump;
testing::StrictMock<MotorMock> motor;
testing::StrictMock<MotorGlueEventsMock> events;
MotorGlueProxy glue;
std::unique_ptr<MotorGlueConnectable> motorGlueConnectable;
};
|
The operational test fixture will call Initialize
which will register callbacks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| struct MotorGlueConnectableTest_Operational : MotorGlueConnectableTest
{
void SetUp() override
{
MotorGlueConnectableTest::SetUp();
EXPECT_CALL(motor, HomingChanged(NotNull())).WillOnce(motor.HomingChangedAction());
EXPECT_CALL(motor, MovingChanged(NotNull())).WillOnce(motor.MovingChangedAction());
dzn::shell(pump, glue.api.in.Initialize);
ASSERT_TRUE(::testing::Mock::VerifyAndClear(&motor));
}
};
|
Changed Unit Tests
The Initialize
test does not have to test the passing of the motor id. Only the registering of the events are tested.
1
2
3
4
5
6
| TEST_F(MotorGlueConnectableTest, InitializeSucceeds)
{
EXPECT_CALL(motor, HomingChanged(NotNull()));
EXPECT_CALL(motor, MovingChanged(NotNull()));
ASSERT_EQ(Result::Ok, dzn::shell(pump, glue.api.in.Initialize));
}
|
When the registering of the event throws, Initialize
should return Result::Fail
.
1
2
3
4
5
6
7
| TEST_F(MotorGlueConnectableTest, InitializeFails)
{
EXPECT_CALL(motor, HomingChanged(NotNull())).WillOnce(
Throw(std::runtime_error("error"))
);
ASSERT_EQ(Result::Fail, dzn::shell(pump, glue.api.in.Initialize));
}
|
The Terminate test makes sure the callbacks are deregistered.
1
2
3
4
5
6
| TEST_F(MotorGlueConnectableTest, Terminate)
{
EXPECT_CALL(motor, HomingChanged(IsNull()));
EXPECT_CALL(motor, MovingChanged(IsNull()));
dzn::shell(pump, glue.api.in.Terminate);
}
|
Terminate
will do exactly the same when terminating from the operational state.
1
2
3
4
5
6
| TEST_F(MotorGlueConnectableTest_Operational, Terminate)
{
EXPECT_CALL(motor, HomingChanged(IsNull()));
EXPECT_CALL(motor, MovingChanged(IsNull()));
dzn::shell(pump, glue.api.in.Terminate);
}
|
New Unit Tests
Only one additional test. It will test if the retrieved id is the same as which which the connectable was constructed.
1
2
3
4
5
6
| TEST_F(MotorGlueConnectableTest_Operational, GetId)
{
std::string id;
dzn::shell(pump, glue.api.in.GetId, id);
ASSERT_EQ(motorId, id);
}
|
All the other tests are the same.
Other Changes
To use the new glue a few changes need to be made to the models that use it.
MotorImpl.dzn
The MotorImpl
initialize will now use the new GetId
in-event to get the id that needs to be used to find te configuration values.
33
34
35
36
37
38
39
40
41
42
43
44
45
| on api.Initialize():
{
string motorId;
Result res = Result.Ok;
if (res.Ok) res = glue.Initialize();
if (res.Ok) glue.GetId(motorId);
if (res.Ok) res = config.GetHomeTimeout(motorId, homeTimeout);
if (res.Ok) res = config.GetMoveTimeout(motorId, moveTimeout);
if (res.Ok) state = State.Idle;
else glue.Terminate();
reply(res);
}
|
MotorComponent.dzn
The MotorComponent
can not instantiate the glue itself. It will require the glue.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| component MotorComponent {
provides Motor api;
requires MotorGlue glue;
system {
MotorImpl impl;
impl.api <=> api;
impl.glue <=> glue;
TimerComponent timer;
timer.api <=> impl.timer;
AsyncComponent async;
async.api <=> impl.async;
}
}
|
Top-Level System
The requiring of the MotorGlue
will trickle up, all the way to the top-level system. This is one of the main downsides. If there are a lot of connectable components, they will all be exposed all the way to the top level-system. All system components in-between will need to know about the connectable components.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| component StageComponent {
provides Stage api;
requires MotorGlue xaxisGlue;
requires MotorGlue yaxisGlue;
system {
StageImpl impl;
impl.api <=> api;
MotorComponent xaxis;
xaxis.api <=> impl.xaxis;
xaxis.glue <=> xaxisGlue;
MotorComponent yaxis;
yaxis.api <=> impl.yaxis;
yaxis.glue <=> yaxisGlue;
}
}
|
Download and Run Code
The code can be downloaded here. I have only tested it on Linux. Dezyne 2.8.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
The testing of connectable components is almost the same as for foreign components. An advantage is that the dependencies can be directly injected in the constructors. It is not needed to use specific file names for the header and source files, since Dezyne will not have to instantiate the components. Putting the dependencies in the locator when using foreign components for glue makes a loose coupling. It’s not clear which dependencies are needed by just looking at a system. By using connectable glue this is more explicit. Downside is that the systems get more cluttered with the larger amount of required ports. It’s debatable if this is worse or better than putting dependencies in the locator. Up to now I mainly use the locator to inject my dependencies.