¿Cómo escribir pruebas unitarias para actores? Enfoque de SObjectizer

Los actores simplifican la programación multiproceso al evitar un estado mutable compartido. Cada actor posee sus propios datos que no son visibles para nadie. Los actores interactúan solo a través de mensajes asincrónicos. Por lo tanto, los horrores más terroríficos de los subprocesos múltiples en forma de razas y puntos muertos cuando se usan actores no son terribles (aunque los actores tienen sus problemas, pero no se trata de eso ahora).

En general, escribir aplicaciones multiproceso con actores es fácil y divertido. Incluso porque los actores mismos están escritos de manera fácil y natural. Incluso podría decir que escribir código de actor es la parte más fácil del trabajo. Pero cuando el actor está escrito, surge una muy buena pregunta: "¿Cómo verificar la exactitud de su trabajo?"

La pregunta es realmente muy buena. Se nos pregunta regularmente cuando hablamos de actores en general y de SObjectizer en particular. Y hasta hace poco, podíamos responder esta pregunta solo en términos generales.

Pero se lanzó la versión 5.5.24 , en la que había soporte experimental para la posibilidad de pruebas unitarias de actores. Y en este artículo trataremos de hablar sobre qué es, cómo usarlo y con qué se implementó.

¿Cómo son las pruebas de actor?


Consideraremos las nuevas características de SObjectizer en un par de ejemplos, pasando qué es qué. El código fuente de los ejemplos discutidos se puede encontrar en este repositorio .

A lo largo de la historia, los términos "actor" y "agente" se utilizarán indistintamente. Denotan lo mismo, pero SObjectizer ha usado históricamente el término "agente", por lo que en adelante "agente" se usará con más frecuencia.

El ejemplo más simple con Pinger y Ponger.


El ejemplo de los actores Pinger y Ponger es probablemente el ejemplo más común para los marcos de actores. Se puede decir un clásico. Bueno, si es así, entonces comencemos con los clásicos.

Entonces, tenemos un agente Pinger, que al comienzo de su trabajo envía un mensaje Ping al agente Ponger. Y el agente Ponger devuelve un mensaje Pong. Así es como se ve en el código C ++:

// Types of signals to be used. struct ping final : so_5::signal_t {}; struct pong final : so_5::signal_t {}; // Pinger agent. class pinger_t final : public so_5::agent_t { so_5::mbox_t m_target; public : pinger_t( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { so_subscribe_self().event( [this](mhood_t<pong>) { so_deregister_agent_coop_normally(); } ); } void set_target( const so_5::mbox_t & to ) { m_target = to; } void so_evt_start() override { so_5::send< ping >( m_target ); } }; // Ponger agent. class ponger_t final : public so_5::agent_t { so_5::mbox_t m_target; public : ponger_t( context_t ctx ) : so_5::agent_t{ std::move(ctx) } { so_subscribe_self().event( [this](mhood_t<ping>) { so_5::send< pong >( m_target ); } ); } void set_target( const so_5::mbox_t & to ) { m_target = to; } }; 

Nuestra tarea es escribir una prueba que verifique que al registrar estos agentes con SObjectizer, Ponger recibirá un mensaje Ping y Pinger recibirá un mensaje Pong en respuesta.

Ok Escribimos dicha prueba usando el marco de prueba de unidad doctest y obtenemos:

 #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include <doctest/doctest.h> #include <ping_pong/agents.hpp> #include <so_5/experimental/testing.hpp> namespace tests = so_5::experimental::testing; TEST_CASE( "ping_pong" ) { tests::testing_env_t sobj; pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); }); sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>()); sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>()); sobj.scenario().run_for(std::chrono::milliseconds(100)); REQUIRE(tests::completed() == sobj.scenario().result()); } 

Parece ser facil. Veamos que pasa aquí.

En primer lugar, descargamos descripciones de las herramientas de soporte de prueba de agentes:

 #include <so_5/experimental/testing.hpp> 

Todas estas herramientas se describen en el espacio de nombres so_5 :: experimental :: testing, pero para no repetir un nombre tan largo, presentamos un alias más corto y más conveniente:

 namespace tests = so_5::experimental::testing; 

La siguiente es una descripción de un solo caso de prueba (y no necesitamos más aquí).

Dentro del caso de prueba, hay varios puntos clave.

En primer lugar, esta es la creación y el lanzamiento de un entorno de prueba especial para SObjectizer:

 tests::testing_env_t sobj; 

Sin este entorno, la "ejecución de prueba" para los agentes no puede completarse, pero hablaremos de esto un poco más adelante.

La clase testing_env_t es muy similar a la clase wrap_env_t en SObjectizer. Del mismo modo, el SObjectizer comienza en el constructor y se detiene en el destructor. Entonces, al escribir pruebas, no tiene que pensar en iniciar y detener SObjectizer.

A continuación, necesitamos crear y registrar agentes Pinger y Ponger. En este caso, necesitamos utilizar estos agentes para determinar el llamado. "Escenario de prueba". Por lo tanto, almacenamos separadamente los punteros a los agentes:

 pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); }); 

Y luego comenzamos a trabajar con el "escenario de prueba".

Un caso de prueba es una pieza que consiste en una secuencia directa de pasos que deben completarse de principio a fin. La frase "de una secuencia directa" significa que en SObjectizer-5.5.24 los pasos de guión "funcionan" estrictamente secuencialmente, sin ramificaciones ni bucles.

Escribir una prueba para agentes es la definición de un script de prueba que debe ejecutarse. Es decir Todos los pasos del escenario de prueba deberían funcionar, desde el primero hasta el último.

Por lo tanto, en nuestro caso de prueba, definimos un escenario de dos pasos. El primer paso verifica que el agente Ponger recibirá y procesará el mensaje Ping:

 sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>()); 

El segundo paso verifica que el agente Pinger reciba un mensaje Pong:

 sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>()); 

Estos dos pasos son suficientes para nuestro caso de prueba, por lo tanto, después de su determinación, procedemos a la ejecución del script. Ejecutamos el script y permitimos que funcione no más de 100 ms:

 sobj.scenario().run_for(std::chrono::milliseconds(100)); 

Cien milisegundos deberían ser más que suficientes para que los dos agentes intercambien mensajes (incluso si la prueba se ejecuta dentro de una máquina virtual muy lenta, como a veces es el caso con Travis CI). Bueno, si cometimos un error al escribir agentes o describimos incorrectamente un guión de prueba, entonces esperar a que se complete un guión erróneo durante más de 100 ms no tiene sentido.

Entonces, después de regresar de run_for (), nuestro script puede completarse con éxito o no. Por lo tanto, simplemente verificamos el resultado del script:

 REQUIRE(tests::completed() == sobj.scenario().result()); 

Si la secuencia de comandos no se completó con éxito, esto conducirá a la falla de nuestro caso de prueba.

Algunas aclaraciones y adiciones


Si ejecutamos este código dentro de un SObjectizer normal:

 pinger_t * pinger{}; ponger_t * ponger{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { pinger = coop.make_agent< pinger_t >(); ponger = coop.make_agent< ponger_t >(); pinger->set_target( ponger->so_direct_mbox() ); ponger->set_target( pinger->so_direct_mbox() ); }); 

entonces, lo más probable es que los agentes de Pinger y Ponger lograran intercambiar mensajes y completar su trabajo antes de regresar de introdu_coop (los milagros de subprocesamiento múltiple son tales). Pero dentro del entorno de prueba, que se crea gracias a testing_env_t, esto no sucede, los agentes de Pinger y Ponger esperan pacientemente hasta que ejecutemos nuestro script de prueba. ¿Cómo sucede esto?

El hecho es que dentro del entorno de prueba, los agentes parecen estar congelados. Es decir después del registro, están presentes en SObjectizer, pero no pueden procesar ninguno de sus mensajes. Por lo tanto, incluso so_evt_start () no se llama a los agentes antes de ejecutar el script de prueba.

Cuando ejecutamos el script de prueba usando el método run_for (), el script de prueba primero descongela todos los agentes congelados. Y luego el script comienza a recibir notificaciones del SObjectizer sobre lo que les sucede a los agentes. Por ejemplo, que el agente Ponger recibió el mensaje Ping y que el agente Ponger procesó el mensaje, pero no lo rechazó.

Cuando tales notificaciones comienzan a llegar al script de prueba, el script intenta "probarlas" hasta el primer paso. Entonces, tenemos una notificación de que Ponger recibió y procesó Ping. ¿Nos interesa o no? Resulta que es interesante, porque la descripción del paso dice exactamente eso: funciona cuando Ponger reacciona a Ping. Lo que vemos en el código:

 .when(*ponger & tests::reacts_to<ping>()) 

Ok Entonces, el primer paso funcionó, vaya al siguiente paso.

Luego viene una notificación de que el Agente Pinger ha reaccionado a Pong. Y esto es justo lo que necesita para que funcione el segundo paso:

 .when(*pinger & tests::reacts_to<pong>()) 

Ok Así que el segundo paso funcionó, ¿tenemos algo más? No Esto significa que se completa el script de prueba completo y puede devolver el control desde run_for ().

Aquí, en principio, cómo funciona el script de prueba. De hecho, todo es algo más complicado, pero tocaremos aspectos más complejos cuando consideremos un ejemplo más complejo.

Ejemplo de filósofos gastronómicos


Se pueden ver ejemplos más complejos de agentes de prueba para resolver la conocida tarea "Filósofos gastronómicos". En los actores, este problema se puede resolver de varias maneras. A continuación, consideraremos la solución más trivial: tanto los actores como los filósofos están representados en forma de actores, por los cuales los filósofos tienen que luchar. Cada filósofo piensa por un momento, luego trata de tomar el tenedor a la izquierda. Si esto tiene éxito, intenta tomar la bifurcación a la derecha. Si esto tiene éxito, entonces el filósofo come durante un tiempo, después de lo cual deja los tenedores y comienza a pensar. Si no fue posible tomar el tapón a la derecha (es decir, fue tomado por otro filósofo), entonces el filósofo regresa el tapón a la izquierda y piensa un poco más. Es decir Esta no es una buena solución en el sentido de que algún filósofo puede morir de hambre durante demasiado tiempo. Pero entonces es muy simple. Y tiene el alcance para demostrar la capacidad de probar agentes.

Los códigos fuente con la implementación de los agentes Fork y Philosopher se pueden encontrar aquí , en el artículo no los consideraremos para ahorrar espacio.

Prueba de horquilla


La primera prueba para agentes de Dining Philosophers será para el agente Fork.

Este agente funciona de acuerdo con un esquema simple. Tiene dos estados: Libre y Tomado. Cuando el agente está en estado Libre, responde a un mensaje Tomar. En este caso, el agente ingresa al estado Tomado y responde con un mensaje de respuesta Tomado.

Cuando el agente está en el estado Tomado, responde al mensaje Tomar de manera diferente: el estado del agente no cambia y Ocupado se envía como un mensaje de respuesta. También en el estado Tomado, el agente responde al mensaje Put: el agente vuelve al estado Libre.

En el estado Libre, el mensaje Put se ignora.

Intentaremos probar este mediante el siguiente caso de prueba:

 TEST_CASE( "fork" ) { class pseudo_philosopher_t final : public so_5::agent_t { public: pseudo_philosopher_t(context_t ctx) : so_5::agent_t{std::move(ctx)} { so_subscribe_self() .event([](mhood_t<msg_taken>) {}) .event([](mhood_t<msg_busy>) {}); } }; tests::testing_env_t sobj; so_5::agent_t * fork{}; so_5::agent_t * philosopher{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { fork = coop.make_agent<fork_t>(); philosopher = coop.make_agent<pseudo_philosopher_t>(); }); sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>()); sobj.scenario().define_step("take_when_free") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>() & tests::store_state_name("fork"), *philosopher & tests::reacts_to<msg_taken>()); sobj.scenario().define_step("take_when_taken") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>(), *philosopher & tests::reacts_to<msg_busy>()); sobj.scenario().define_step("put_when_taken") .impact<msg_put>(*fork) .when( *fork & tests::reacts_to<msg_put>() & tests::store_state_name("fork")); sobj.scenario().run_for(std::chrono::milliseconds(100)); REQUIRE(tests::completed() == sobj.scenario().result()); REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork")); REQUIRE("free" == sobj.scenario().stored_state_name("put_when_taken", "fork")); } 

Hay mucho código, por lo que lo trataremos en partes, omitiendo esos fragmentos que ya deberían estar claros.

Lo primero que necesitamos aquí es reemplazar el verdadero agente de Filósofo. Un agente de Fork debe recibir mensajes de alguien y responder a alguien. Pero no podemos usar al verdadero filósofo en este caso de prueba, porque el verdadero agente del filósofo tiene su propia lógica de comportamiento, él mismo envía mensajes y esta independencia interferirá con nosotros aquí.

Por lo tanto, nos burlamos , es decir en lugar del verdadero filósofo, presentaremos un sustituto: un agente vacío que no envía nada en sí, sino que solo recibe mensajes enviados, sin ningún procesamiento útil. Este es el pseudo-filósofo implementado en el código:

 class pseudo_philosopher_t final : public so_5::agent_t { public: pseudo_philosopher_t(context_t ctx) : so_5::agent_t{std::move(ctx)} { so_subscribe_self() .event([](mhood_t<msg_taken>) {}) .event([](mhood_t<msg_busy>) {}); } }; 

Luego, creamos una colaboración entre el agente de Fork y el agente de PseudoPhilospher y comenzamos a determinar el contenido de nuestro caso de prueba.

El primer paso del script es verificar que Fork, al estar en estado Libre (y este es su estado inicial), no responde al mensaje Put. Así es como se escribe este cheque:

 sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>()); 

Lo primero que llama la atención es la construcción de impacto.

Ella es necesaria porque nuestro agente Fork no hace nada, solo reacciona a los mensajes entrantes. Por lo tanto, alguien debe enviar un mensaje al agente. Pero quien?

Pero el paso de guión en sí mismo envía a través del impacto. De hecho, el impacto es un análogo de la función de envío habitual (y el formato es el mismo).

Bueno, el paso de guión mismo enviará un mensaje a través del impacto. ¿Pero cuándo lo hará?

Y lo hará cuando le llegue el turno. Es decir Si el paso en el script es el primero, el impacto se ejecutará inmediatamente después de ingresar run_for. Si el paso en el script no es el primero, entonces el impacto se ejecutará tan pronto como el paso anterior haya funcionado y el script procederá a procesar el siguiente paso.

La segunda cosa que debemos discutir aquí es ignorar llamadas. Esta función auxiliar dice que el paso se activa cuando el agente no puede procesar el mensaje. Es decir en este caso, el agente de Fork debe negarse a procesar el mensaje Put.

Consideremos un paso más del escenario de prueba con más detalle:

 sobj.scenario().define_step("take_when_free") .impact<msg_take>(*fork, philosopher->so_direct_mbox()) .when_all( *fork & tests::reacts_to<msg_take>() & tests::store_state_name("fork"), *philosopher & tests::reacts_to<msg_taken>()); 

Primero, aquí vemos cuándo_todos en lugar de cuándo. Esto se debe a que para activar un paso, debemos cumplir varias condiciones a la vez. El agente tenedor necesita manejar Take. Y Philosopher necesita manejar la respuesta Taken. Por lo tanto, escribimos when_all, no when. Por cierto, también hay when_any, pero no nos reuniremos con él en los ejemplos considerados hoy.

En segundo lugar, también debemos verificar el hecho de que después del procesamiento de Take, el agente de Fork estará en el estado de Taken. Hacemos la verificación de la siguiente manera: primero indicamos que tan pronto como el agente de Fork termine de procesar Take, el nombre de su estado actual debe guardarse usando la etiqueta “fork”. Esta construcción solo conserva el nombre del estado del agente:

 & tests::store_state_name("fork") 

Y luego, cuando el script se completa con éxito, verificamos este nombre guardado:
 REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork")); 

Es decir le pedimos el script: denos el nombre que se guardó con el marcador de la bifurcación para el paso llamado take_when_free, y luego compare el nombre con el valor esperado.

Aquí, quizás, es todo lo que se podría notar en el caso de prueba para el agente de Fork. Si los lectores tienen alguna pregunta, pregunte en los comentarios, le responderemos con gusto.

Prueba de guión exitosa para filósofo


Para el agente de Philosopher, consideraremos solo un caso de prueba, para el caso en que Philosopher puede tomar ambos tenedores y comer.

Este caso de prueba se verá así:

 TEST_CASE( "philosopher (takes both forks)" ) { tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } }; so_5::agent_t * philosopher{}; so_5::agent_t * left_fork{}; so_5::agent_t * right_fork{}; sobj.environment().introduce_coop([&](so_5::coop_t & coop) { left_fork = coop.make_agent<fork_t>(); right_fork = coop.make_agent<fork_t>(); philosopher = coop.make_agent<philosopher_t>( "philosopher", left_fork->so_direct_mbox(), right_fork->so_direct_mbox()); }); auto scenario = sobj.scenario(); scenario.define_step("stop_thinking") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_thinking>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); scenario.define_step("take_left") .when( *left_fork & tests::reacts_to<msg_take>() ); scenario.define_step("left_taken") .when( *philosopher & tests::reacts_to<msg_taken>() & tests::store_state_name("philosopher") ); scenario.define_step("take_right") .when( *right_fork & tests::reacts_to<msg_take>() ); scenario.define_step("right_taken") .when( *philosopher & tests::reacts_to<msg_taken>() & tests::store_state_name("philosopher") ); scenario.define_step("stop_eating") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_eating>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); scenario.define_step("return_forks") .when_all( *left_fork & tests::reacts_to<msg_put>(), *right_fork & tests::reacts_to<msg_put>() ); scenario.run_for(std::chrono::seconds(1)); REQUIRE(tests::completed() == scenario.result()); REQUIRE("wait_left" == scenario.stored_state_name("stop_thinking", "philosopher")); REQUIRE("wait_right" == scenario.stored_state_name("left_taken", "philosopher")); REQUIRE("eating" == scenario.stored_state_name("right_taken", "philosopher")); REQUIRE("thinking" == scenario.stored_state_name("stop_eating", "philosopher")); } 

Bastante voluminoso, pero trivial. Primero, verifique que Philosopher haya terminado de pensar y haya comenzado a prepararse para la comida. Luego verificamos que trató de tomar el tenedor izquierdo. A continuación, debe intentar tomar el tenedor correcto. Entonces debería comer y detener esta actividad. Luego debe poner los dos tenedores tomados.

En general, todo es simple. Pero debes concentrarte en dos cosas.

Primero, la clase testing_env_t, como su prototipo, wrap_env_t, le permite personalizar el entorno SObjectizer. Usaremos esto para habilitar el mecanismo de rastreo de entrega de mensajes:

 tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } }; 

Este mecanismo le permite "visualizar" el proceso de entrega de mensajes, lo que ayuda en la investigación del comportamiento del agente (ya hablamos de esto con más detalle ).

En segundo lugar, el agente Filósofo realiza una serie de acciones no inmediatamente, sino después de un tiempo. Entonces, comenzando a funcionar, el agente debe enviarse un mensaje StopThinking pendiente. Entonces, este mensaje debería llegar al agente después de unos pocos milisegundos. Lo que indicamos al establecer la restricción necesaria para un determinado paso:

 scenario.define_step("stop_thinking") .when( *philosopher & tests::reacts_to<philosopher_t::msg_stop_thinking>() & tests::store_state_name("philosopher") ) .constraints( tests::not_before(std::chrono::milliseconds(250)) ); 

Es decir Aquí decimos que no estamos interesados ​​en ninguna reacción del agente Filósofo a StopThinking, sino solo en lo que ocurrió no antes de 250 ms después del inicio del procesamiento de este paso.

Una restricción del tipo not_before le dice al script que todos los eventos que ocurren antes de que expire el tiempo de espera especificado deben ser ignorados.

También hay una restricción de la forma not_after, funciona al revés: solo se tienen en cuenta los eventos que ocurren hasta que el tiempo de espera especificado ha expirado.

Las restricciones not_before y not_after se pueden combinar, por ejemplo:

 .constraints( tests::not_before(std::chrono::milliseconds(250)), tests::not_after(std::chrono::milliseconds(1250))) 

pero en este caso, SObjectizer no verifica la consistencia de los valores dados.

¿Cómo lograste implementar esto?


Me gustaría decir algunas palabras sobre cómo todo funcionó. Después de todo, en general, nos enfrentamos con una gran pregunta ideológica: "¿Cómo evaluar a los agentes en principio?" y una pregunta más pequeña, ya técnica: "¿Cómo implementar esto?"

Y si sobre la ideología de prueba fue posible salir de su mente, entonces sobre la implementación, la situación fue más complicada. Era necesario encontrar una solución que, en primer lugar, no requiriera una alteración radical de los interiores de SObjectizer. Y, en segundo lugar, se suponía que era una solución que podría implementarse en el corto plazo previsible y, muy deseable.

Como resultado del difícil proceso de fumar bambú, se encontró una solución. Para esto, se requería, de hecho, hacer una pequeña innovación en el comportamiento regular de SObjectizer. Y la base de la solución es el mecanismo del sobre del mensaje, que se agregó en la versión 5.5.23 y del que ya hemos hablado .

Dentro del entorno de prueba, cada mensaje enviado está envuelto en un sobre especial. Cuando se entrega un sobre con un mensaje al agente para su procesamiento (o, por el contrario, el agente lo rechaza), el escenario de prueba se da cuenta de esto. Gracias a los sobres, el guión de prueba sabe lo que está sucediendo y puede determinar los momentos en que los pasos del guión "funcionan".

Pero, ¿cómo hacer que SObjectizer envuelva cada mensaje en un sobre especial?

Esa fue una pregunta interesante. Decidió lo siguiente: se inventó un concepto como event_queue_hook . Este es un objeto especial con dos métodos: on_bind y on_unbind.

Cuando un agente está vinculado a un despachador específico, el despachador emite un agente event_queue al agente. A través de este event_queue, las solicitudes del agente entran en la cola necesaria y están disponibles para el despachador para su procesamiento. Cuando un agente se ejecuta dentro de un SObjectizer, tiene un puntero a event_queue. Cuando un agente se elimina de un SObjectizer, su puntero a event_queue se anula.

Entonces, comenzando con la versión 5.5.24, el agente, al recibir event_queue, debe llamar al método on_bind de event_queue_hook. Donde el agente debe pasar el puntero recibido a event_queue. Y event_queue_hook puede devolver el mismo puntero u otro puntero en respuesta. Y el agente debe usar el valor devuelto.

Cuando un agente se elimina de un SObjectizer, debe llamar a on_unbind en event_queue_hook. En on_unbind, el agente pasa el valor devuelto por el método on_bind.

Toda esta cocina se ejecuta dentro del SObjectizer y el usuario no ve nada de esto. Y, en principio, es posible que no sepa sobre esto en absoluto. Pero el entorno de prueba de SObjectizer, el mismo testing_env_t, explota exactamente event_queue_hook. Dentro de testing_env_t, se crea una implementación especial de event_queue_hook.Esta implementación en on_bind envuelve cada event_queue en un objeto proxy especial. Y ya este objeto proxy coloca los mensajes enviados al agente en un sobre especial.

Pero eso no es todo.Puede recordar que en un entorno de prueba, los agentes deben congelarse. Esto también se implementa a través de los objetos proxy mencionados. Mientras el script de prueba no se está ejecutando, el objeto proxy almacena los mensajes enviados al agente en casa. Pero cuando se ejecuta el script, el objeto proxy transfiere todos los mensajes acumulados previamente a la cola de mensajes actual del agente.

Conclusión


En conclusión, quiero decir dos cosas.

En primer lugar, implementamos nuestra opinión sobre cómo se pueden probar los agentes en SObjectizer. Mi opinión porque no hay tantos buenos modelos a seguir. Miramos hacia Akka . Pruebas . Pero Akka y SObjectizer son demasiado diferentes para portar los enfoques que funcionan en Akka a SObjectizer. Y C ++ no es Scala / Java, en el que se pueden hacer algunas cosas relacionadas con la introspección debido a la reflexión. Así que tuve que idear un enfoque que recaería en SObjectizer.

En la versión 5.5.24, la primera implementación experimental estuvo disponible. Seguramente puedes hacerlo mejor. ¿Pero cómo entender qué será útil y cuáles son fantasías inútiles? Lamentablemente, nada. Tienes que tomar y probar, ver qué pasa en la práctica.

Así que creamos una versión mínima que puedes tomar y probar. Lo que proponemos hacer por todos: probar, experimentar y compartir sus impresiones con nosotros. ¿Qué te gustó, qué no te gustó? Tal vez algo falta?

En segundo lugar, las palabras que se dijeron a principios de 2017 se hicieron aún más relevantes :
… , , , . - — . . . : , .

, , , — , .

Por lo tanto, mi consejo para aquellos que buscan un marco de actor listo: preste atención no solo a la originalidad de las ideas y la belleza de los ejemplos. Observe también todo tipo de elementos auxiliares que lo ayudarán a descubrir qué está sucediendo en su aplicación: por ejemplo, averigüe cuántos actores hay dentro, cuáles son sus tamaños de cola, si el mensaje no llega al destinatario, entonces a dónde va ... Si el marco lo hace proporciona algo así, será más fácil para ti. Si no es así, tendrá más trabajo.
Todo lo anterior es aún más importante cuando se trata de probar actores. Por lo tanto, al elegir un marco de actor para usted, preste atención a lo que contiene y lo que no. Por ejemplo, ya tenemos en nuestro kit de herramientas para simplificar las pruebas :)

Source: https://habr.com/ru/post/es435606/


All Articles