LUMIERA.clone/tests/core/steam/control/dispatcher-looper-test.cpp
Ichthyostega 806db414dd Copyright: clarify and simplify the file headers
* Lumiera source code always was copyrighted by individual contributors
 * there is no entity "Lumiera.org" which holds any copyrights
 * Lumiera source code is provided under the GPL Version 2+

== Explanations ==
Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above.
For this to become legally effective, the ''File COPYING in the root directory is sufficient.''

The licensing header in each file is not strictly necessary, yet considered good practice;
attaching a licence notice increases the likeliness that this information is retained
in case someone extracts individual code files. However, it is not by the presence of some
text, that legally binding licensing terms become effective; rather the fact matters that a
given piece of code was provably copyrighted and published under a license. Even reformatting
the code, renaming some variables or deleting parts of the code will not alter this legal
situation, but rather creates a derivative work, which is likewise covered by the GPL!

The most relevant information in the file header is the notice regarding the
time of the first individual copyright claim. By virtue of this initial copyright,
the first author is entitled to choose the terms of licensing. All further
modifications are permitted and covered by the License. The specific wording
or format of the copyright header is not legally relevant, as long as the
intention to publish under the GPL remains clear. The extended wording was
based on a recommendation by the FSF. It can be shortened, because the full terms
of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00

517 lines
18 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
DispatcherLooper(Test) - verify loop control and timing functionality of SteamDispatcher
Copyright (C)
2016, Hermann Vosseler <Ichthyostega@web.de>
  **Lumiera** is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License as published by the
  Free Software Foundation; either version 2 of the License, or (at your
  option) any later version. See the file COPYING for further details.
* *****************************************************************/
#include "lib/test/run.hpp"
#include "steam/control/looper.hpp"
#include <chrono>
#include <thread>
namespace steam {
namespace control {
namespace test {
using std::this_thread::sleep_for;
using namespace std::chrono_literals;
namespace { // test fixture...
using Dur = std::chrono::duration<double, std::milli>;
/**
* @todo this value should be retrieved from configuration ////////////////////////////////TICKET #1052 : access application configuration
* @see Looper::establishWakeTimeout()
*/
const uint EXPECTED_BUILDER_DELAY_ms = 50;
bool
isFast (milliseconds timeoutDelay)
{
return timeoutDelay < Dur{1.2 * EXPECTED_BUILDER_DELAY_ms}
and 0ms < timeoutDelay;
}
bool
isSlow (milliseconds timeoutDelay)
{
return timeoutDelay >= Dur{1.2 * EXPECTED_BUILDER_DELAY_ms};
}
bool
isDisabled (milliseconds timeoutDelay)
{
return 0ms == timeoutDelay;
}
/**
* Setup for testing the Looper.
* In operation, the Looper receives its input by invoking closures.
* With the help of this adapter, unit tests may just set values
* on the Setup record and verify the result on the wired Looper
*/
struct Setup
{
bool has_commands_in_queue = false;
Looper
install()
{
return Looper([&](){ return has_commands_in_queue; });
}
};
}//(End) test fixture
/******************************************************************************//**
* @test verify encapsulated control logic of SteamDispatcher.
* - fusing of conditions for the pthread waiting condition
* - detection and handling of work states
* - management of builder run triggers
*
* @see steam::control::Looper
* @see DispatcherLoop
* @see CommandQueue_test
*/
class DispatcherLooper_test : public Test
{
virtual void
run (Arg)
{
verifyBasics();
verifyShutdown();
verifyWakeupActivity();
verifyShutdown_stops_processing();
verifyDisabling_stops_processing();
verifyBuilderStart();
}
void
verifyBasics()
{
Setup setup;
Looper looper = setup.install();
CHECK (not looper.isDying());
CHECK (looper.shallLoop());
CHECK (not looper.runBuild());
CHECK (isDisabled (looper.getTimeout()));
setup.has_commands_in_queue = true;
CHECK (looper.requireAction());
milliseconds timeout = looper.getTimeout();
CHECK (10ms < timeout, "configured idle timeout %2l to short", timeout.count());
CHECK (timeout < 800ms, "configured idle timeout %3l to long", timeout.count());
}
void
verifyShutdown()
{
Setup setup;
Looper looper = setup.install();
CHECK (not looper.isDying());
CHECK (looper.shallLoop());
looper.triggerShutdown();
CHECK (looper.isDying());
CHECK (not looper.shallLoop());
}
void
verifyWakeupActivity()
{
Setup setup;
Looper looper = setup.install();
CHECK (not looper.isDying());
CHECK (looper.shallLoop());
CHECK (not looper.requireAction());
CHECK (not looper.isWorking());
CHECK ( looper.isIdle());
setup.has_commands_in_queue = true;
CHECK ( looper.requireAction());
CHECK ( looper.isWorking());
CHECK (not looper.isIdle());
CHECK (looper.shallLoop());
setup.has_commands_in_queue = false;
looper.markStateProcessed(); // after command processing
CHECK (not looper.requireAction()); // stops immediate work state
CHECK ( looper.useTimeout()); // but still performs timeout
CHECK (not looper.isWorking());
CHECK (not looper.isIdle()); // still need to run the builder
looper.markStateProcessed(); // second round-trip, after builder run
CHECK (not looper.requireAction());
CHECK (not looper.isWorking());
CHECK ( looper.isIdle());
CHECK (looper.shallLoop());
looper.triggerShutdown();
CHECK (not looper.shallLoop());
CHECK ( looper.requireAction());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
}
void
verifyShutdown_stops_processing()
{
Setup setup;
Looper looper = setup.install();
CHECK (not looper.isDying());
CHECK (looper.shallLoop());
CHECK (not looper.requireAction());
CHECK (not looper.isWorking());
CHECK ( looper.isIdle());
setup.has_commands_in_queue = true;
CHECK ( looper.requireAction());
CHECK ( looper.isWorking());
CHECK (not looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
looper.triggerShutdown();
CHECK ( looper.requireAction());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK (not looper.shallLoop());
CHECK ( looper.isDying());
setup.has_commands_in_queue = false;
CHECK ( looper.requireAction());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK (not looper.shallLoop());
CHECK ( looper.isDying());
setup.has_commands_in_queue = true;
CHECK ( looper.requireAction());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK (not looper.shallLoop());
CHECK ( looper.isDying());
}
void
verifyDisabling_stops_processing()
{
Setup setup;
Looper looper = setup.install();
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK ( looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
setup.has_commands_in_queue = true; // normal operation: pending commands will be processed
CHECK ( looper.requireAction()); // ..causes wake-up
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
looper.enableProcessing(false); // disable processing
CHECK (not looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
setup.has_commands_in_queue = false; // while disabled, state of the command queue has no effect
CHECK (not looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
setup.has_commands_in_queue = true;
CHECK (not looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
looper.enableProcessing(); // resume normal operation
CHECK ( looper.requireAction());
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
looper.enableProcessing(false); // disable again
CHECK (not looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK ( looper.shallLoop());
CHECK (not looper.isDying());
looper.triggerShutdown(); // wake-up for shutdown even from disabled state
CHECK ( looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.isIdle());
CHECK (not looper.shallLoop());
CHECK ( looper.isDying());
}
/** @test logic to trigger the builder over a complete simulated lifecycle.
* - when Looper::requireAction is true, the thread does not go into wait state
* - in the loop body
* * either when `runBuild()` is true, the builder run is triggered
* * or when `isWorking()` is true, the next command is processed
* - after that, Looper::markStateProcessed proceeds the state machine
* @note this test actually has to sleep in order to verify triggering a timeout
*
*/
void
verifyBuilderStart()
{
Setup setup;
Looper looper = setup.install();
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.runBuild());
CHECK ( looper.isIdle());
setup.has_commands_in_queue = true; // regular command processing
CHECK ( looper.requireAction());
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.runBuild());
CHECK (not looper.isIdle());
looper.markStateProcessed(); // at least one command has been handled
CHECK ( looper.requireAction());
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.runBuild()); // ...note: build not yet triggered
CHECK (not looper.isIdle());
CHECK (isSlow (looper.getTimeout()));
looper.markStateProcessed(); // next processing round: further command(s) processed,
// yet still more commands pending...
CHECK ( looper.requireAction());
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.runBuild()); // ...build still postponed
CHECK (not looper.isIdle());
sleep_for (800ms); // let's assume we did command processing for a long time...
CHECK ( looper.requireAction());
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK ( looper.runBuild()); // ...after some time of command processing, a build run is forced
CHECK (not looper.isIdle());
looper.markStateProcessed(); // and when the builder run is confirmed...
CHECK ( looper.requireAction());
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.runBuild()); // ...we are back to normal working state (build postponed)
CHECK (not looper.isIdle());
setup.has_commands_in_queue = false; // now emptied our queue
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK ( looper.runBuild()); // ...note: now build will be triggered
CHECK (not looper.isIdle());
CHECK (isFast (looper.getTimeout())); // ...but only after a short wait period,
// since not looper.requireAction()
looper.markStateProcessed(); // next processing round: invoked builder,
// and no more commands commands pending...
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.runBuild()); // ...note: now done with building
CHECK ( looper.isIdle());
CHECK (isDisabled(looper.getTimeout())); // ...now Dispatcher is idle and goes to sleep
setup.has_commands_in_queue = true; // next command pending
CHECK ( looper.requireAction()); // return to work mode
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.runBuild());
CHECK (not looper.isIdle());
setup.has_commands_in_queue = false; // now let's assume command has been processed
looper.markStateProcessed(); // and queue is empty again
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK ( looper.runBuild());
CHECK (not looper.isIdle());
CHECK (isFast (looper.getTimeout())); // now build *would* be triggered after short timeout, but..
looper.enableProcessing(false); // disable processing
CHECK (not looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.runBuild()); // ...note: dirty state hidden by disabled state
CHECK (not looper.isIdle());
CHECK (isDisabled (looper.getTimeout()));
looper.enableProcessing(true); // enable back
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK ( looper.runBuild()); // ...note: dirty state revealed again
CHECK (not looper.isIdle());
CHECK (isFast (looper.getTimeout()));
looper.enableProcessing(false); // disable processing
looper.markStateProcessed(); // let's assume builder was running and is now finished
CHECK (not looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.runBuild()); // ...note: dirty state not obvious
CHECK (not looper.isIdle());
CHECK (isDisabled (looper.getTimeout()));
looper.enableProcessing(true); // enable back
// NOTE special twist: it's unclear, if builder was triggered before the disabled state...
CHECK (isFast (looper.getTimeout())); // ...and thus we remain in dirty state
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK ( looper.runBuild()); // so the builder will be triggered (possibly a second time) after a short timeout
CHECK (not looper.isIdle());
looper.markStateProcessed(); // and after one round-trip the builder was running and is now finished
CHECK (not looper.requireAction());
CHECK (not looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.runBuild());
CHECK ( looper.isIdle()); // ...system is in idle state now and waits until triggered externally
CHECK (isDisabled (looper.getTimeout()));
setup.has_commands_in_queue = true; // more commands again -> wake up
looper.markStateProcessed(); // ...and let's assume one command has already been processed
CHECK ( looper.requireAction());
CHECK (not looper.isDisabled());
CHECK ( looper.isWorking());
CHECK (not looper.runBuild());
CHECK (not looper.isIdle());
looper.triggerShutdown(); // request shutdown...
CHECK ( looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.runBuild());
CHECK (not looper.isIdle());
CHECK (isDisabled (looper.getTimeout()));
setup.has_commands_in_queue = false; // and even when done with all commands...
looper.markStateProcessed();
CHECK (isDisabled (looper.getTimeout()));
CHECK (not looper.shallLoop()); // we remain disabled and break out of the loop
CHECK ( looper.requireAction());
CHECK ( looper.isDisabled());
CHECK (not looper.isWorking());
CHECK (not looper.runBuild()); // ...note: still no need for builder run, since in shutdown
CHECK (not looper.isIdle());
}
};
/** Register this test class... */
LAUNCHER (DispatcherLooper_test, "unit controller");
}}} // namespace steam::control::test