Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you mock the time for boost timers?

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.

like image 916
Zero Avatar asked Jan 07 '13 07:01

Zero


3 Answers

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?

like image 86
free_coffee Avatar answered Nov 12 '22 09:11

free_coffee


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:

  • Boost.Asio provides timers that use clocks, but does not provide clocks as they are outside of the scope of Boost.Asio. Thus, clock related features, such as setting or emulating, are not within Boost.Asio's capabilities.
  • Monotonic clocks may be used internally. Thus, a change in a clock (emulated or actual) may not produce the desired effect. For example, boost::asio::steady_timer will not be affected by changes to system time, and the reactor implementation using epoll can take up to 5 minutes before detecting changes to system time, as it is protected from changes to the system clock.
  • For Boost.Asio timers, changing the expiration time will implicitly cancel asynchronous wait operations per the WaitableTimerService and TimerService requirements. This cancellation causes outstanding asynchronous wait operations to complete as soon as possible, and cancelled operations will have an error code of boost::asio::error::operation_aborted.

Nevertheless, there are two overall techniques to approach this problem based on what is being tested:

  • Scaling time.
  • Wrapping types.

Scaling Time

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();
}

Wrapping types

There are a few distinct locations where new types can be introduced to obtain some of the desired behavior.

  • Wrap the deadline_timer.
  • Create a custom WaitableTimerService.
  • Create a custom handler.

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.

Wrap the 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:

  • Store the WaitHandler provided to async_wait() internally (user_handler_).
  • When cancel() is invoked, an internal flag is set to indicate that cancellation has occurred (cancelled_).
  • Aggregate a timer. When an expiry time is set, an internal handler is passed to the aggregated timer's async_wait. Anytime the internal handler is called, it needs to handle the following four cases:
    • A normal timeout.
    • An explicit cancellation.
    • An implicit cancellation from expiry time being changed to a time is not in the future.
    • An implicit cancellation from expiry time being changed to a time that is in the future.

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;
}

Create a custom 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;

Create a custom handler.

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.

like image 24
Tanner Sansbury Avatar answered Nov 12 '22 08:11

Tanner Sansbury


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.

like image 1
Arne Mertz Avatar answered Nov 12 '22 10:11

Arne Mertz