If possible, how do you mock the time for the purpose of triggering boost timers in a unit test?
For example, is it possible to achieve something like the following:
#include <iostream>
#include <boost/asio.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
void print(const boost::system::error_code& /*e*/)
{
std::cout << "Hello, world!\n";
}
int main()
{
boost::asio::io_service io; // Possibly another class needed here, or a way of setting the clock to be fake
boost::asio::deadline_timer t(io, boost::posix_time::hours(24));
t.async_wait(&print);
io.poll(); // Nothing should happen - no handlers ready
// PSEUDO-CODE below of what I'd like to happen, jump ahead 24 hours
io.set_time(io.get_time() + boost::posix_time::hours(24));
io.poll(); // The timer should go off
return 0;
}
Update Thank you to all the answers, they have provided excellent insight into the problem. I have provided my own answer (SSCCE), but couldn't have done that without the help provided.
The basic_deadline_timer
template has a traits parameter which you can use to provide your own clock. The author of Boost Asio has a blog post showing how to do this. Here's an example from the post:
class offset_time_traits
: public asio::deadline_timer::traits_type
{
public:
static time_type now()
{
return add(asio::deadline_timer::traits_type::now(), offset_);
}
static void set_now(time_type t)
{
offset_ =
subtract(t, asio::deadline_timer::traits_type::now());
}
private:
static duration_type offset_;
};
typedef asio::basic_deadline_timer<
boost::posix_time::ptime, offset_time_traits> offset_timer;
Maybe you can use something like offset_timer
throughout your application but only call set_now()
when running your tests?
As far as I know, there is no way to emulate time change or to set the time with Boost. Before expanding upon a few techniques that can be used to approach this problem, there are a few points to consider:
epoll
can take up to 5 minutes before detecting changes to system time, as it is protected from changes to the system clock.boost::asio::error::operation_aborted
.Nevertheless, there are two overall techniques to approach this problem based on what is being tested:
Scaling time preserves the same overall relative flow between multiple timers. For example, a timer with a 1 second expiration should trigger before a timer with a 24 hour expiration. Minimum and maximum durations can also be used for additional control. Furthermore, scaling durations works for timers that are not affected by the system clock, as as the steady_timer
.
Here is an example, where a scale of 1 hour = 1 second is applied. Thus, the 24 hour expiration will actual be a 24 second expiration. Additionally,
namespace bpt = boost::posix_time;
const bpt::time_duration max_duration = bpt::seconds(24);
const boost::chrono::seconds max_sleep(max_duration.total_seconds());
bpt::time_duration scale_time(const bpt::time_duration& duration)
{
// Scale of 1 hour = 1 seconds.
bpt::time_duration value =
bpt::seconds(duration.total_seconds() * bpt::seconds(1).total_seconds() /
bpt::hours(1).total_seconds());
return value < max_duration ? value : max_duration;
}
int main()
{
boost::asio::io_service io;
boost::asio::deadline_timer t(io, scale_time(bpt::hours(24)));
t.async_wait(&print);
io.poll();
boost::this_thread::sleep_for(max_sleep);
io.poll();
}
There are a few distinct locations where new types can be introduced to obtain some of the desired behavior.
deadline_timer
.In all of these cases, it is important to account for the behavior that changing the expiration time will implicitly cancel the asynchronous wait operation.
deadline_timer
.Wrapping the deadline_timer
requires managing the user's handler internally. If the timer passes the user's handler to the service associated with the timer, then the user handler will be notified when the expiry time changes.
A custom timer could:
WaitHandler
provided to async_wait()
internally (user_handler_
).cancel()
is invoked, an internal flag is set to indicate that cancellation has occurred (cancelled_
).async_wait
. Anytime the internal handler is called, it needs to handle the following four cases:
The internal handler code may look like the following:
void handle_async_wait(const boost::system::error_code& error)
{
// Handle normal and explicit cancellation.
if (error != boost::asio::error::operation_aborted || cancelled_)
{
user_handler_(error);
}
// Otherwise, if the new expiry time is not in the future, then invoke
// the user handler.
if (timer_.expires_from_now() <= boost::posix_time::seconds(0))
{
user_handler_(make_error_code(boost::system::errc::success));
}
// Otherwise, the new expiry time is in the future, so internally wait.
else
{
timer_.async_wait(boost::bind(&custom_timer::handle_async_wait, this,
boost::asio::placeholders::error));
}
}
While this is fairly easy to implement, it requires understanding the timer interface enough to mimic its pre/post-conditions, with the exception of the behavior for which you want to deviate. There may also be a risk factor in testing, as the behaviors need to be mimicked as close as possible. Additionally, this requires changing the type of timer for testing.
int main()
{
boost::asio::io_service io;
// Internal timer set to expire in 24 hours.
custom_timer t(io, boost::posix_time::hours(24));
// Store user handler into user_handler_.
t.async_wait(&print);
io.poll(); // Nothing should happen - no handlers ready
// Modify expiry time. The internal timer's handler will be ready to
// run with an error of operation_aborted.
t.expires_from_now(t.expires_from_now() - boost::posix_time::hours(24));
// The internal handler will be called, and handle the case where the
// expiry time changed to timeout. Thus, print will be called with
// success.
io.poll();
return 0;
}
WaitableTimerService
Creating a custom WaitableTimerService is a little bit more complex. Although the documentation states the API, and the pre/post conditions, the implementation requires an understanding some of the internals, such as the io_service
implementation and the scheduler interface, which is often a reactor. If the service passes the user's handler to the scheduler, then the user handler will be notified when the expiry time changes. Thus, similar to wrapping a timer, the user handler must be managed internally.
This has the same drawbacks as wrapping a timer: requires changing types and has inherit risk due to potential errors when trying to match the pre/post conditions.
For example:
deadline_timer timer;
is the equivalent of:
basic_deadline_timer<boost::posix_time::ptime> timer;
and would become:
basic_deadline_timer<boost::posix_time::ptime,
boost::asio::time_traits<boost::posix_time::ptime>,
CustomTimerService> timer;
Although this could be mitigated with a typedef:
typedef basic_deadline_timer<
boost::posix_time::ptime,
boost::asio::time_traits<boost::posix_time::ptime>,
CustomTimerService> customer_timer;
A handler class could be used to wrap the actual handler, and provide the same approach as above with an extra degree of freedom. While this requires changing a type, and modifying what is provided to async_wait
, it provides flexibility in that the custom handler's API has no pre-existing requirements. This reduced complexity provides a minimal risk solution.
int main()
{
boost::asio::io_service io;
// Internal timer set to expire in 24 hours.
deadline_timer t(io, boost::posix_time::hours(24));
// Create the handler.
expirable_handler handler(t, &print);
t.async_wait(&handler);
io.poll(); // Nothing should happen - no handlers ready
// Cause the handler to be ready to run.
// - Sets the timer's expiry time to negative infinity.
// - The internal handler will be ready to run with an error of
// operation_aborted.
handler.set_to_expire();
// The internal handler will be called, and handle the case where the
// expiry time changed to timeout. Thus, print will be called with
// success.
io.poll();
return 0;
}
All in all, testing asynchronous programs in a traditional manner can be very difficult. With proper encapsulation, it may even be nearly impossible to unit test without conditional builds. Sometimes it helps to shift perspectives and treat the entire asynchronous call chain as a single unit, with all external handlers being the API. If an asynchronous chain is too difficult to test, then I often find that the chain is too difficult to understand and/or maintain, and will mark it as a candidate for refactoring. Additionally, I often have to write helper types that allow my test harness to treat the asynchronous operations in a synchronous manner.
I dont' know about how to fake something like time passing, and I consider it to be overkill to provide your own time service. But here's a thought:
By initializing the timer with a hardcoded 24h, you used something that could be considered a magic constant (meaning: what you should not do). Instead, you could try this:
boost::asio::deadline_timer t(io, getDeadLineForX());
Now, if you stub out the getDeadLineForX
function in your test suite, you can pass a sufficiently small deadline to test the timer, and you don't have to wait 24 hours for your test suite to complete.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With