Как вы издеваетесь над временем для таймеров boost?

Если возможно, как вы издеваетесь над временем с целью запуска таймеров наддува в модульном тесте?

например, можно ли достичь чего-то вроде следующего:

#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 ответов


на 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() при выполнении тестов?


насколько я знаю, нет способа эмулировать изменение времени или установить время С Boost. Прежде чем расширить несколько методов, которые могут быть использованы для решения этой проблемы, необходимо рассмотреть несколько моментов:

  • импульс.Asio предоставляет таймеры, которые используют часы, но не предоставляют часы, поскольку они находятся вне области Boost.Азио. Таким образом, функции, связанные с часами, такие как настройка или эмуляция, не находятся в Boost.Асио это способности.
  • монотонные часы могут использоваться внутри. Таким образом, изменение часов (эмулированное или фактическое) может не произвести желаемого эффекта. Например, boost::asio:: steady_timer не будут затронуты изменениями системного времени, а реализация реактора с использованием epoll может занять до 5 минут до обнаружения изменений в системном времени, так как он защищен от изменений в системных часах.
  • Для Наддува.Таймеры 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 требует внутреннего управления обработчиком пользователя. Если таймер передает обработчик пользователя службе, связанной с таймером, затем обработчик пользователя будет уведомлен об изменении времени истечения срока действия.

пользовательский таймер может:

  • сохранить 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));
  }
}

хотя это довольно легко реализовать, это требует понимания интерфейса таймера достаточно, чтобы имитировать его pre/post-условия, за исключением поведения, для которого вы хотите отклониться. В тестировании также может быть фактор риска, так как поведение должно быть имитировано как можно ближе. Кроме того, это требует изменения типа таймера для тестирования.

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 и условия pre / post, реализация требует понимания некоторых внутренних компонентов, таких как io_service реализация и интерфейс планировщика, который часто является реактором. Если служба передает обработчик пользователя планировщику, то обработчик пользователя будет уведомлен об изменении времени истечения. Таким образом, подобно упаковке таймера, обработчик пользователя должен быть управляется внутренне.

это имеет те же недостатки, что и обертывание таймера: требует изменения типов и наследования риска из-за потенциальных ошибок при попытке соответствовать условиям pre/post.

например:

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. Если асинхронная цепь слишком сложна для тестирования, то я часто нахожу, что цепь слишком сложна для понимания и/или поддержания, и отмечу ее как кандидата на рефакторинг. Кроме того, мне часто приходится писать вспомогательные типы, которые позволяют моей тестовой жгуте синхронно обрабатывайте асинхронные операции.


Я не знаю о том, как подделать уходящего времени, и я считаю, что это перебор, чтобы обеспечить собственную службу времени. Но вот мысль:

инициализируя таймер с жестко закодированным 24h, вы использовали что-то, что можно считать магической константой (что означает: что вы не должны делать). Вместо этого вы можете попробовать следующее:

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

теперь, если вы заполним getDeadLineForX функция в вашем наборе тестов, вы можете пройти достаточно небольшой срок, чтобы проверить таймер, и вам не нужно ждать 24 часа для завершения набора тестов.


A 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 частично изменить. Обратите внимание, что to_posix_duration hack описано в [1] необходимо использовать меньшую продолжительность, чем sleep.

мне этот подход все еще кажется немного волшебным, так как time_traits не хорошо документированы, и в частности взлом to_posix_duration имеет запах вуду об этом. Я думаю, это просто сводится к интимному знанию deadline_timer реализация (которой у меня нет).