среда, 28 ноября 2018 г.

[prog.c++] Первый набросок инструментов unit-тестирования агентов в SObjectizer

В результате первой попытки выкуривания бамбука на тему того, как можно писать unit-тесты для агентов, появился нижеследующий пример кода. Который, как мне думается, удовлетворяет двум важным критериям. Во-первых, он выглядит реализуемым. И, во-вторых, он позволяет достаточно декларативно записывать тесты для отдельных агентов.

Кому интересно посмотреть и пообсуждать -- милости прошу под кат.

В качестве поляны для экспериментов был взят штатный пример dining_philosophers (знаменитые обедающие философы, в одном из вариантов реализации).

И даже не весь пример, а один простенький класс оттуда: fork_t, который реализует понятие "вилка", т.е. ресурс, за который борются философы. Выглядит этот класс в слегка модифицированном варианте так:

struct msg_take
{
   const so_5::mbox_t m_who;
};

struct msg_busy : public so_5::signal_t {};

struct msg_taken : public so_5::signal_t {};

struct msg_put : public so_5::signal_t {};

class fork_t final : public so_5::agent_t
{
public :
   fork_t( context_t ctx ) : so_5::agent_t( ctx )
   {
      this >>= st_free;

      st_free.event( [this]( const msg_take & evt )
            {
               this >>= st_taken;
               so_5::send< msg_taken >( evt.m_who );
            } );

      st_taken.event( []( const msg_take & evt )
            {
               so_5::send< msg_busy >( evt.m_who );
            } )
         .just_switch_to< msg_put >( st_free );
   }

private :
   state_t st_free{ this"free" };
   state_t st_taken{ this"taken" };
};

Т.е. простой агент с двумя состояниями free и taken. Когда агент в состоянии free, он реагирует на сообщение msg_take: переходит в taken и отсылает подтверждение тому, кто msg_take отослал.

Когда агент в taken, то он реагирует на два сообщения. Самое простое -- это реакция на msg_put. Агент просто переходит в free.

Но если в состоянии taken приходит еще одно сообщение msg_take, то агент должен отослать msg_busy отправителю msg_take.

Вот такая тривиальная логика. И давайте попробуем посмотреть, как ее можно проверить посредством unit-теста, написанного с использованием пока еще не существующих инструментов.

Выглядеть это может вот так:

int main()
{
   namespace tests = so_5::extra::testing;

   tests::wrapped_env_t sobj;

   auto fork = make_agent<fork_t>(sobj);
   auto dummy_mbox = sobj.env().create_mbox();

   tests::inspect(*fork,
         [&](auto pre) {
            pre.send<msg_take>(*fork, dummy_mbox);
         },
         [&](auto post) {
            post.expect_state_by_name("taken");
            post.expect_message<msg_taken>(dummy_mbox);
         });

   tests::inspect(*fork,
         [&](auto pre) {
            pre.send<msg_take>(*fork, dummy_mbox);
         },
         [&](auto post) {
            post.expect_state_by_name("taken");
            post.expect_message<msg_busy>(dummy_mbox);
         });

   tests::inspect(*fork,
         [&](auto pre) {
            pre.send<msg_put>(*fork);
         },
         [&](auto post) {
            post.expect_state_by_name("free");
            post.expect_message<msg_put>(dummy_mbox);
         });

   tests::inspect(*fork
         [&](auto pre) {
            pre.send<msg_put>(*fork);
         },
         [&](auto post) {
            post.expect_nothing();
         });
}

Т.е. мы запускаем SObjectizer, затем создаем экземпляр агента fork и вспомогательный mbox, в который fork должен будет отсылать ответные сообщения. Ну и затем мы начинаем гонять тестовые случаи.

Вот, скажем:

   tests::inspect(*fork,
         [&](auto pre) {
            pre.send<msg_take>(*fork, dummy_mbox);
         },
         [&](auto post) {
            post.expect_state_by_name("taken");
            post.expect_message<msg_taken>(dummy_mbox);
         });

Здесь мы говорим, что хотим проинспектировать поведение агента fork. Это указывается с помощью первого аргумента inspect().

Далее в вызов inspect передаются две лямбды. Первая лямбда выполняет воздействия на объект inspect. В нашем случае отсылается сообщение msg_take.

Во второй лямбде выполняются проверки того, как агент среагировал на наше воздействие. Мы ждем, что агент окажется в состоянии taken и отошлет сообщение msg_taken в наш вспомогательный mbox. Если эти проверки окажутся пройденными, то будет пройден и весь тестовый случай.

Остальные проверки похожи на первую, за исключением вот этой:

   tests::inspect(*fork
         [&](auto pre) {
            pre.send<msg_put>(*fork);
         },
         [&](auto post) {
            post.expect_nothing();
         });

К этому времени у нас агент должен быть в состоянии free. В этом состоянии он должен просто проигнорировать очередное сообщение msg_put. Поэтому во второй лямбде мы указываем expect_nothing. Т.е. ничего не должно произойти, никакой реакции.

Вот такое пока самое первое приближение.

Не факт, что в вызове inspect первая лямбда вообще нужна. Скорее всего тестовое воздействие на агента всегда будет выражаться в отсылке единственного сообщения агенту или в какой-то mbox. Если это так, то можно будет писать что-то вроде:

   tests::inspect(*fork,
         tests::impact<msg_take>(*fork, dummy_mbox),
         [&](auto post) {
            post.expect_state_by_name("taken");
            post.expect_message<msg_taken>(dummy_mbox);
         });

Комментариев нет: