据我所知,没有办法模拟时间变化或使用 Boost设置时间。在扩展可用于解决此问题的一些技术之前,需要考虑以下几点:
- Boost.Asio 提供了使用时钟的计时器,但不提供时钟,因为它们超出了 Boost.Asio 的范围。因此,与时钟相关的功能,例如设置或仿真,不在 Boost.Asio 的能力范围内。
- 可以在内部使用单调时钟。因此,时钟(模拟的或实际的)的变化可能不会产生预期的效果。例如,boost::asio::steady_timer不会受到系统时间更改的影响,并且使用的反应器实现
epoll
可能需要长达 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
.
包装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。如果异步链太难测试,那么我经常发现该链太难理解和/或维护,并将其标记为重构的候选者。此外,我经常必须编写帮助程序类型,以允许我的测试工具以同步方式处理异步操作。