Продолжение темы, начатой здесь. Кому не интересно, можно смело не читать.
Показанный в предыдущем посте пример претерпел изменения и теперь выглядит вот так:
TEST_CASE("fork check") { namespace tests = so_5::extra::testing; tests::wrapped_env_t sobj; auto fork = make_agent<fork_t>(sobj); auto dummy_ch = create_mchain(sobj.env()); tests::check_delivery( tests::impact<msg_take>(*fork, dummy_ch->as_mbox()), [&](auto & dc) { REQUIRE(1u == dc.handlers_count()); REQUIRE("taken" == tests::current_agent_name(*fork)); auto taken = tests::try_receive<msg_taken>(dummy_ch); REQUIRE(taken.extracted()); }); tests::check_delivery( tests::impact<msg_take>(*fork, dummy_ch->as_mbox()), [&](auto & dc) { REQUIRE(1u == dc.handlers_count()); REQUIRE("taken" == tests::current_agent_name(*fork)); auto busy = tests::try_receive<msg_busy>(dummy_ch); REQUIRE(busy.extracted()); }); tests::check_delivery( tests::impact<msg_put>(*fork), [&](auto & dc) { REQUIRE(1u == dc.handlers_count()); REQUIRE("free" == tests::current_agent_name(*fork)); }); tests::check_delivery( tests::impact<msg_put>(*fork), [&](auto & dc) { REQUIRE(0u == dc.handlers_count()); }); } |
Что здесь изменилось?
Во-первых, сделана попытка представить, как оформлять unit-тесты для SObjectizer-а с помощью уже существующих фреймворков для этих целей. В частности здесь используется API Catch2 и doctest. ИМХО, возможность делать unit-тесты для SObjectizer-а посредством готовых и популярных инструментов, вроде Catch2/doctest/GTest/Boost.Test и пр., намного предпочтительнее, чем превращение so_5::extra::testing в подобный фреймворк.
Во-вторых, первый вариант, показанный пару дней назад, требовал разработки специализированного SObjectizer Environment, который был нужен для контроля того, что происходит внутри SObjectizer-а. А так же для создания специализированных реализаций mbox-ов, чтобы эти реализации можно было использовать в тестах.
Но такой кастомный SOEnv -- это неслабый кусок работы. Как в первоначальной реализации, так и в последующем сопровождении. А создание кастомных mbox-ов неявным образом -- это хорошо для случая, когда используются только штатные MPMC и MPSC mbox-ы. Но вот со сторонними mbox-ами, в том числе и из состава so_5_extra, уже засада.
Поэтому второй пример написан так, чтобы по максимуму переиспользовать в тестах штатные возможности самого SObjectizer-а.
В частности, для того, чтобы проверить факт отсылки сигналов msg_taken и msg_busy в этом тесте вместо какого-то специального mbox-а с хитрой функциональностью, используется обычный mchain. В сообщение msg_take отсылается mchain в виде mbox-а.
tests::check_delivery( tests::impact<msg_take>(*fork, dummy_ch->as_mbox()), [&](auto & dc) { REQUIRE(1u == dc.handlers_count()); REQUIRE("taken" == tests::current_agent_name(*fork)); auto taken = tests::try_receive<msg_taken>(dummy_ch); REQUIRE(taken.extracted()); }); |
Агент fork отвечает в этот mchain. И мы можем проконтролировать, что же лежит в mchain-е. Для этого мы пытаемся извлечь из канала сигнал msg_taken, если он там есть. Выполняется это посредством вспомогательной функции:
template<typename Msg> extraction_result_t<Msg> try_receive(const so_5::mchain_t & mchain); |
template<typename Msg> class extraction_result_t { so_5::intrusive_ptr_t<Msg> m_msg; bool m_extracted; ... public: ... bool extrected() const noexcept { return m_extracted; } ... const Msg & msg() const noexcept; }; |
Т.е. try_receive пытается взять из mchain-а сообщение указанного типа. Если сообщения в mchain-е нет, то метод extraction_result_t::extracted() будет возвращать false. Что можно будет проверить обычным assertion-ом из тестового фреймворка.
Еще один момент, на который имеет смысл обратить внимание -- это наличие функции check_delivery и ее важность. Дело в том, что одним из основных инструментов для unit-тестирования агентов является отсылка сообщения в какой-то mbox с последующей проверкой того, что же произошло в результате обработки сообщения. Но фокус в том, что отсылка сообщения -- это асинхронная операция. Фактически она выполняется на контексте другой рабочей нити. Поэтому, если мы просто сделаем вызов so_5::send(), а затем сразу начнем проверять, что изменилось, то мы можем оказаться в двух ситуациях: либо сообщение уже доставлено и обработано к моменту выхода из so_5::send(), либо этого еще не произошло. В первой ситуации мы можем продолжать тест. Но вот во второй нам нужно дождаться доставки сообщения до получателя. Но как это сделать?
И вот тут и приходит на помощь функция check_delivery. Она выполняет so_5::send() и ждет, пока обработка сообщения завершится. Когда обработка завершается, то вызывается лямбда с действиями по проверке последствий доставки. В эту лямбду передается ссылка на объект некого (пока гипотетического) типа delivery_completion_t. У этого объекта, в частности, можно узнать, сколько обработчиков было вызвано при обработке сообщения.
Пока предполагается, что реализация check_delivery будет построена на использовании message envelopes. Но это все еще преждевременные детали реализации.
Еще несколько мыслей, которые пока еще не оформились в конкретные предложения.
Во-первых, для каких-то тестов нужно следить за тем, что отсылается в mbox. Чтобы mbox, с одной стороны, выполнял свою основную работу. Но, с другой стороны, чтобы было поднять информацию о том, что именно в mbox отсылают.
Возможно, для этого можно предоставить специальную обертку. Что-то вроде:
auto traced_wrapper = tests::make_traced_wrapper(env.create_mbox()); tests::check_delivery( tests::impact<msg_take>(*fork, traced_wrapper), [&](auto & dc) { REQUIRE(1u == traced_wrapper->occurences_of<msg_taken>()); }); |
Т.е. создаем обертку вокруг другого mbox-а и используем эту обертку как mbox. Но при этом можем заглянуть в историю того, что через этот mbox было отослано.
Во-вторых, следить за отсылкой отложенных и периодических сообщений можно посредством специального объекта timer_tracer (название гипотетическое). Что-то вроде:
tests::timer_tracer ttracer; so_5::wrapped_env_t sobj( [](so_5::environment_t &){}, [&](so_5::environment_params_t & params) { params.timer_thread(ttracer.make_timer_thread()); }); ... tests::check_delivery(tests::impact<some_msg>(...), [&](auto & dc) { auto info = ttracer.find_periodic<my_msg>(some_mbox); REQUIRE(info.found()); REQUIRE(info.delay() <= 250ms); }); |
Т.е. мы создаем timer_tracer из которого потом берем реализацию timer_thread для SObjectizer-а. Эта реализация timer_thread будет давать нам доступ к текущим таймерным заявкам (а может не только к текущим, но и к тем, которые уже были обработаны). Что позволяет нам в показанном выше примере найти информацию об отложенном сообщении типа my_msg, которое должно было быть отправлено в some_mbox. И если мы информацию о таком сообщении нашли, то можем проверить величину задержки.
Причем, можно сделать так, чтобы timer_tracer не нужно было создавать вручную. Можно включить в so_5::extra::testing какой-то класс wrapped_env_t, который уже будет содержать timer_tracer внутри себя. Что позволит писать вот так:
tests::wrapped_env_t sobj; ... tests::check_delivery(tests::impact<some_msg>(...), [&](auto & dc) { auto info = sobj.timer_tracer().find_periodic<my_msg>(some_mbox); REQUIRE(info.found()); REQUIRE(info.delay() <= 250ms); }); |
В-третьих, в тестах агентов, которые отсылают отложенные/периодические сообщения, может быть неудобно использовать нормальное время. Ну, скажем, агент отослал себе на 20 минут отложенное сообщение. И мы хотим проверить, как агент на него среагирует. Не ждать же ради этого 20 минут? ;)
Поэтому в unit-тестах может быть желательно управлять скоростью течения времени (как звучит, а?). Например, ускорить. Скажем так:
tests::timer_tracer ttracer; ttracer.set_speedup_factor(5.5); so_5::wrapped_env_t sobj( [](so_5::environment_t &){}, [&](so_5::environment_params_t & params) { params.timer_thread(ttracer.make_timer_thread()); }); |
И таймерные события будут происходит в 5.5 раз быстрее.
Или даже так:
tests::timer_tracer ttracer; so_5::wrapped_env_t sobj( [](so_5::environment_t &){}, [&](so_5::environment_params_t & params) { params.timer_thread(ttracer.make_timer_thread()); }); ... so_5::send_delayed<my_msg>(sobj.env(), some_mbox, 15m, ...); ttracer.execute_nearest(); |
Т.е. отослали отложенное на 15 минут сообщение, а потом дали команду таймеру перейти к обслуживанию ближайшей заявки, как будто эти 15 минут уже прошли.
В общем, не смотря на то, что какие-то вещи начинают приобретать, пусть расплывчатые, но очертания, непонятностей и белых пятен пока еще слишком много.
Комментариев нет:
Отправить комментарий