私の知る限り、時刻の変更をエミュレートしたり、Boostで時刻を設定したりする方法はありません。この問題に対処するために使用できるいくつかの手法を詳しく説明する前に、考慮すべき点がいくつかあります。
- Boost.Asio はクロックを使用するタイマーを提供しますが、Boost.Asio の範囲外であるためクロックを提供しません。したがって、設定やエミュレートなどのクロック関連の機能は、Boost.Asio の機能の範囲内ではありません。
- モノトニック クロックは内部で使用できます。したがって、クロック (エミュレートまたは実際のクロック) を変更しても、目的の効果が得られない場合があります。たとえば、boost::asio::steady_timerはシステム時間の変更の影響を受けません
epoll
。システム クロックの変更から保護されているため、reactor の実装はシステム時間の変更を検出するまでに最大 5 分かかる場合があります。
- Boost.Asio タイマーの場合、有効期限を変更すると、WaitableTimerServiceおよびTimerServiceの要件に従って非同期待機操作が暗黙的にキャンセルされます。このキャンセルにより、未処理の非同期待機操作ができるだけ早く完了し、キャンセルされた操作のエラー コードは になり
boost::asio::error::operation_aborted
ます。
それにもかかわらず、テスト対象に基づいて、この問題に対処するための 2 つの全体的な手法があります。
スケーリング時間
スケーリング時間は、複数のタイマー間で同じ全体的な相対フローを維持します。たとえば、有効期限が 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
ます。内部ハンドラーが呼び出されるたびに、次の 4 つのケースを処理する必要があります。
- 通常のタイムアウト。
- 明示的なキャンセル。
- 時間に変更された有効期限からの暗黙のキャンセルは、未来ではありません。
- 有効期限が将来の時間に変更されることによる暗黙のキャンセル。
内部ハンドラ コードは次のようになります。
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の作成は、もう少し複雑です。io_service
ドキュメントには API と事前/事後条件が記載されていますが、実装には、実装や、多くの場合リアクタであるスケジューラ インターフェイスなど、内部の一部を理解する必要があります。サービスがユーザーのハンドラをスケジューラに渡すと、有効期限が変更されるとユーザー ハンドラに通知されます。したがって、タイマーをラップするのと同様に、ユーザー ハンドラーを内部で管理する必要があります。
これには、タイマーをラップするのと同じ欠点があります。型を変更する必要があり、事前/事後条件を一致させようとするとエラーが発生する可能性があるため、継承のリスクがあります。
例えば:
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 として、非同期呼び出しチェーン全体を 1 つの単位として扱うことが役立つ場合があります。非同期チェーンをテストするのが難しすぎる場合は、そのチェーンを理解および/または維持するのが難しすぎることに気づき、リファクタリングの候補としてマークします。さらに、多くの場合、テスト ハーネスが非同期操作を同期的に処理できるようにするヘルパー型を作成する必要があります。