Unit testing¶
Sen ships a TestKernel and a TestComponent so your packages can be driven from an ordinary unit
testing framework, without a running system. Both are declared in
libs/kernel/include/sen/kernel/test_kernel.h.
TestKernel starts in virtual time, so a test decides when time advances instead of waiting for it.
You move it forward with step(), one cycle at a time.
The double step¶
If your code reacts to property-changed callbacks, you need two steps before the callback runs -
kernel.step() twice, or kernel.step(2).
To see why, look at how a cycle works.
The properties changed during a cycle are collected in the commit phase ❶, and become visible in the drain phase ❷ of the next cycle.
When a test pushes an update, it does so between kernel steps ❸, while the kernel is waiting. The queue of changed properties for that cycle has already been built, so the change needs one more step to reach the object's getter, and another for the callback that reports it.
Example¶
The code below is the real test Sen runs against its own kernel, included from
libs/kernel/test/unit/test_kernel/test_kernel_test.cpp. It compiles and passes in CI, so it cannot
drift from the API it demonstrates.
First the setup: an object, a component that publishes it, and callbacks that count what arrives.
// to track the evolution of the test
int32_t counter = 0;
int32_t eventCount = 0;
int32_t propCount = 0;
sen::TimeStamp lastTime;
// the object that we will stimulate
auto object = std::make_shared<MyClassImpl>("myObject", sen::VarMap {});
// set up a component that will hold our object
sen::kernel::TestComponent component;
// on init we register the object and set a callback to track event counts
std::shared_ptr<sen::ObjectSource> source;
component.onInit(
[&](sen::kernel::InitApi&& api) -> sen::kernel::PassResult
{
source = api.getSource("local.test");
source->add(object);
return sen::kernel::done();
});
// on each iteration we track the count and last simulation time
bool emitEvent = true;
bool emitProp = true;
component.onRun(
[&](auto& api)
{
object->onSomethingHappened({api.getWorkQueue(), [&]() { ++eventCount; }}).keep();
object->onPropChanged({api.getWorkQueue(), [&]() { ++propCount; }}).keep();
return api.execLoop(std::chrono::seconds(1),
[&]()
{
++counter;
lastTime = api.getTime();
if (emitEvent)
{
object->somethingHappened();
}
if (emitProp)
{
object->setNextProp(object->getProp() + 1);
}
});
});
// create the kernel holding only our simple component
sen::kernel::TestKernel kernel(&component);
TestComponent lets you supply the component's phases as lambdas instead of writing a subclass.
onInit gets an InitApi and is where objects are published to a bus; onRun gets a RunApi and
usually ends in execLoop, which drives a cycle at the given period. The callback passed to
execLoop is optional, and runs once per cycle.
Note .keep() on the callback registrations. They return a ConnectionGuard, and dropping it
unregisters the callback immediately.
Now the double step, with the assertions that show it:
// iteration 1, sim time was 1s, we have executed 2s, we emit the event
emitEvent = true;
emitProp = true;
kernel.step();
EXPECT_EQ(2, counter);
EXPECT_EQ(0, eventCount);
EXPECT_EQ(1, object->getProp());
EXPECT_EQ(0, propCount);
EXPECT_EQ(std::chrono::seconds(1), lastTime.sinceEpoch().toChrono());
EXPECT_EQ(std::chrono::seconds(2), kernel.getTime().sinceEpoch().toChrono());
// iteration 2, sim time was 2s, we have executed 3s, we received the event
emitEvent = false;
emitProp = false;
kernel.step();
EXPECT_EQ(3, counter);
EXPECT_EQ(1, eventCount);
EXPECT_EQ(1, object->getProp());
EXPECT_EQ(1, propCount);
EXPECT_EQ(std::chrono::seconds(2), lastTime.sinceEpoch().toChrono());
EXPECT_EQ(std::chrono::seconds(3), kernel.getTime().sinceEpoch().toChrono());
Read the two blocks together. After the first step() the property has already changed -
get<Prop>() returns 1, but propCount is still 0, because the
callback has not run yet. It takes the second step() for the change to be reported. That is the
double step, and it is why a test that checks a callback after a single step will see nothing.