18

如果可能,您如何模拟时间以在单元测试中触发加速计时器?

例如,是否有可能实现以下目标:

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

更新感谢所有答案,他们对问题提供了极好的洞察力。我提供了自己的答案 (SSCCE),但如果没有提供的帮助,我无法做到这一点。

4

4 回答 4

8

basic_deadline_timer模板有一个特征参数,您可以使用它来提供您自己的时钟。Boost Asio 的作者有一篇文展示了如何做到这一点。这是帖子中的一个示例:

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;

offset_timer也许您可以在整个应用程序中使用类似的东西,但只set_now()在运行测试时调用?

于 2013-01-20T02:20:47.017 回答
4

据我所知,没有办法模拟时间变化或使用 Boost设置时间。在扩展可用于解决此问题的一些技术之前,需要考虑以下几点:

  • Boost.Asio 提供了使用时钟的计时器,但不提供时钟,因为它们超出了 Boost.Asio 的范围。因此,与时钟相关的功能,例如设置或仿真,不在 Boost.Asio 的能力范围内。
  • 可以在内部使用单调时钟。因此,时钟(模拟的或实际的)的变化可能不会产生预期的效果。例如,boost::asio::steady_timer不会受到系统时间更改的影响,并且使用的反应器实现epoll可能需要长达 5 分钟才能检测到系统时间的更改,因为它受到保护,不会更改系统时钟。
  • 对于 Boost.Asio 计时器,更改过期时间将根据WaitableTimerServiceTimerService要求隐式取消异步等待操作。此取消会导致未完成的异步等待操作尽快完成,并且取消的操作将具有错误代码boost::asio::error::operation_aborted

尽管如此,根据正在测试的内容,有两种总体技术可以解决此问题:

  • 缩放时间。
  • 包装类型。

缩放时间

缩放时间在多个计时器之间保持相同的整体相对流。例如,到期时间为 1 秒的计时器应在到期时间为 24 小时的计时器之前触发。最小和最大持续时间也可用于附加控制。此外,缩放持续时间适用于不受系统时钟影响的计时器,例如steady_timer.

这是一个示例,其中应用了 1 小时 = 1 秒的比例。因此,24 小时到期实际上是 24 秒到期。此外,

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

包装类型

有几个不同的位置可以引入新类型以获得一些所需的行为。

在所有这些情况下,重要的是要考虑更改过期时间将隐式取消异步等待操作的行为。

deadline_timer.

包装deadline_timer需要在内部管理用户的处理程序。如果计时器将用户的处理程序传递给与计时器关联的服务,则当到期时间更改时,将通知用户处理程序。

自定义计时器可以:

  • WaitHandler提供的内容存储在async_wait()内部 ( user_handler_)。
  • 调用时cancel(),会设置一个内部标志以指示已发生取消 ( cancelled_)。
  • 聚合一个计时器。设置到期时间后,内部处理程序将传递给聚合计时器的async_wait. 每当调用内部处理程序时,它都需要处理以下四种情况:
    • 正常超时。
    • 显式取消。
    • 从到期时间更改为时间的隐式取消不在未来。
    • 从到期时间更改为未来时间的隐式取消。

内部处理程序代码可能如下所示:

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

虽然这很容易实现,但它需要充分了解计时器接口以模仿其前置/后置条件,但要偏离的行为除外。测试中也可能存在风险因素,因为需要尽可能地模仿行为。此外,这需要更改用于测试的计时器类型。

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

创建一个自定义的 WaitableTimerService有点复杂。尽管文档说明了 API 和前置/后置条件,但实现需要了解一些内部结构,例如io_service实现和调度程序接口,这通常是一个反应器。如果服务将用户的处理程序传递给调度程序,则用户处理程序将在到期时间更改时收到通知。因此,类似于包装计时器,用户处理程序必须在内部进行管理。

这与包装计时器具有相同的缺点:需要更改类型并且由于尝试匹配前置/后置条件时的潜在错误而具有继承风险。

例如:

deadline_timer timer;

相当于:

basic_deadline_timer<boost::posix_time::ptime> timer;

并会变成:

basic_deadline_timer<boost::posix_time::ptime,
                     boost::asio::time_traits<boost::posix_time::ptime>,
                     CustomTimerService> timer;

虽然这可以通过 typedef 来缓解:

typedef basic_deadline_timer<
  boost::posix_time::ptime,
  boost::asio::time_traits<boost::posix_time::ptime>,
  CustomTimerService> customer_timer;

创建自定义处理程序。

处理程序类可用于包装实际处理程序,并提供与上述相同的方法,具有额外的自由度。虽然这需要更改类型并修改提供给 的内容async_wait,但它提供了灵活性,因为自定义处理程序的 API 没有预先存在的要求。这种降低的复杂性提供了最小风险的解决方案。

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

总而言之,以传统方式测试异步程序可能非常困难。通过适当的封装,如果没有条件构建,甚至几乎不可能进行单元测试。有时它有助于转变观点并将整个异步调用链视为一个单元,所有外部处理程序都是 API。如果异步链太难测试,那么我经常发现该链太难理解和/或维护,并将其标记为重构的候选者。此外,我经常必须编写帮助程序类型,以允许我的测试工具以同步方式处理异步操作。

于 2013-01-19T06:42:49.777 回答
1

我不知道如何伪造时间流逝之类的东西,而且我认为提供自己的时间服务是矫枉过正的。但这里有一个想法:

通过使用硬编码的 24 小时来初始化计时器,您使用了可以被视为魔术常数的东西(意思是:您不应该做的事情)。相反,你可以试试这个:

boost::asio::deadline_timer t(io, getDeadLineForX());

现在,如果您getDeadLineForX在测试套件中存根函数,您可以通过足够小的截止日期来测试计时器,并且您不必等待 24 小时才能完成测试套件。

于 2013-01-11T14:12:31.840 回答
1

SSCCE,基于@free_coffee发布的链接

#include <boost/asio.hpp>
#include <boost/optional.hpp>

class mock_time_traits
{       
    typedef boost::asio::deadline_timer::traits_type  source_traits;

public:

    typedef source_traits::time_type time_type;
    typedef source_traits::duration_type duration_type;

    // Note this implemenation requires set_now(...) to be called before now()
    static time_type now() { return *now_; }

    // After modifying the clock, we need to sleep the thread to give the io_service
    // the opportunity to poll and notice the change in clock time
    static void set_now(time_type t) 
    { 
        now_ = t; 
        boost::this_thread::sleep_for(boost::chrono::milliseconds(2)); 
    }

    static time_type add(time_type t, duration_type d) { return source_traits::add(t, d); }
    static duration_type subtract(time_type t1, time_type t2) { return source_traits::subtract(t1, t2); }
    static bool less_than(time_type t1, time_type t2) { return source_traits::less_than(t1, t2); }

    // This function is called by asio to determine how often to check 
    // if the timer is ready to fire. By manipulating this function, we
    // can make sure asio detects changes to now_ in a timely fashion.
    static boost::posix_time::time_duration to_posix_duration(duration_type d) 
    { 
        return d < boost::posix_time::milliseconds(1) ? d : boost::posix_time::milliseconds(1);
    }

private:

    static boost::optional<time_type> now_;
};

boost::optional<mock_time_traits::time_type> mock_time_traits::now_;



typedef boost::asio::basic_deadline_timer<
            boost::posix_time::ptime, mock_time_traits> mock_deadline_timer;

void handler(const boost::system::error_code &ec)
{
    std::cout << "Handler!" << std::endl;
}


int main()
{
    mock_time_traits::set_now(boost::posix_time::time_from_string("2013-01-20 1:44:01.000"));

    boost::asio::io_service io_service;
    mock_deadline_timer timer(io_service, boost::posix_time::seconds(5));
    timer.async_wait(handler);

    std::cout << "Poll 1" << std::endl;
    io_service.poll();

    mock_time_traits::set_now(mock_time_traits::now() + boost::posix_time::seconds(6));


    std::cout << "Poll 2" << std::endl;
    io_service.poll();

    std::cout << "Poll 3" << std::endl;
    io_service.poll();

    return 0;
}

// Output
Poll 1
Poll 2
Handler!
Poll 3

感谢@free_coffee 提供此链接到boost asio 的创建者的博客条目。以上略有修改(我相信略有改进)。通过不使用系统时钟上的偏移量,您可以完全控制计时器:它们不会触发,直到您明确将时间设置为超过计时器。

可以通过使this_thread::sleep部件可配置来改进解决方案。请注意, [ 1to_posix_duration ]中描述的 hack需要使用比.sleep

对我来说,这种方法似乎仍然有点神奇,因为time_traits没有很好的记录,特别是 hackto_posix_duration有一种巫毒教的味道。我想这只是归结为对deadline_timer实现的深入了解(我没有)。

于 2013-01-20T03:59:06.013 回答