你如何嘲笑助推计时器的时间

How do you mock the time for boost timers?

本文关键字:计时器 时间 何嘲笑      更新时间:2023-10-16

如果可能的话,如何模拟单元测试中触发升压定时器的时间?

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

#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),但如果没有提供的帮助,我不可能做到这一点。

basic_deadline_timer模板有一个traits参数,您可以使用它来提供自己的时钟。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()

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

  • Boost.Asio提供使用时钟的定时器,但不提供时钟,因为它们不在Boost.Acio的范围内。因此,与时钟相关的功能,如设置或模拟,不在Boost的功能范围内
  • 单音钟可以在内部使用。因此,时钟的变化(模拟的或实际的)可能不会产生期望的效果。例如,boost::asio::steady_timer不会受到系统时间变化的影响,并且使用epoll的reactor实现可能需要长达5分钟的时间才能检测到系统时间的变化,因为它受到保护,不会受到系统时钟变化的影响
  • 对于Boost.Asio计时器,根据WaitableTimerService和TimerService的要求,更改过期时间将隐式取消异步等待操作。这种取消会导致未完成的异步等待操作尽快完成,并且取消的操作将具有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
  • 创建自定义WaitableTimerService
  • 创建自定义处理程序

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

包裹deadline_timer

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

自定义计时器可以:

  • 在内部存储提供给async_wait()WaitHandler(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。如果异步链太难测试,那么我经常发现该链太难理解和/或维护,并将其标记为重构的候选者。此外,我经常需要编写助手类型,使我的测试工具能够以同步的方式处理异步操作。

我不知道如何伪造时间流逝之类的东西,我认为提供自己的时间服务太过分了。但有一个想法:

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

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

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

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部件可配置来改进该解决方案。注意,[1]中描述的to_posix_duration破解需要使用比sleep更小的持续时间。

对我来说,这种方法似乎仍然有点神奇,因为time_traits没有很好的文档记录,尤其是to_posix_duration的破解有点巫毒。我想这只是对deadline_timer实现的深入了解(我没有)。