Wie schreibe ich Unit-Tests für Schauspieler? SObjectizer-Ansatz

Akteure vereinfachen die Multithread-Programmierung, indem sie einen gemeinsamen, gemeinsamen veränderlichen Zustand vermeiden. Jeder Schauspieler besitzt seine eigenen Daten, die für niemanden sichtbar sind. Akteure interagieren nur über asynchrone Nachrichten. Daher sind die schrecklichsten Schrecken des Multithreading in Form von Rennen und Deadlocks bei der Verwendung von Schauspielern nicht schrecklich (obwohl Schauspieler ihre Probleme haben, aber darum geht es jetzt nicht).

Im Allgemeinen ist das Schreiben von Multithread-Anwendungen mit Akteuren einfach und unterhaltsam. Einschließlich, weil die Schauspieler selbst leicht und natürlich geschrieben sind. Man könnte sogar sagen, dass das Schreiben von Schauspielercode der einfachste Teil des Jobs ist. Aber wenn der Schauspieler geschrieben ist, stellt sich eine sehr gute Frage: "Wie kann man die Richtigkeit seiner Arbeit überprüfen?"

Die Frage ist wirklich sehr gut. Wir werden regelmäßig gefragt, wenn wir über Schauspieler im Allgemeinen und über SObjectizer im Besonderen sprechen. Und bis vor kurzem konnten wir diese Frage nur allgemein beantworten.

Es wurde jedoch die Version 5.5.24 veröffentlicht , in der die Möglichkeit des Unit-Tests von Akteuren experimentell unterstützt wurde. Und in diesem Artikel werden wir versuchen, darüber zu sprechen, was es ist, wie man es benutzt und mit was es implementiert wurde.

Wie sehen Schauspielertests aus?


Wir werden die neuen Funktionen von SObjectizer anhand einiger Beispiele betrachten und weitergeben, was was ist. Der Quellcode für die besprochenen Beispiele befindet sich in diesem Repository .

In der gesamten Geschichte werden die Begriffe "Schauspieler" und "Agent" synonym verwendet. Sie bezeichnen dasselbe, aber SObjectizer hat in der Vergangenheit den Begriff "Agent" verwendet, so dass im Folgenden "Agent" häufiger verwendet wird.

Das einfachste Beispiel mit Pinger und Ponger


Das Beispiel der Schauspieler Pinger und Ponger ist wahrscheinlich das häufigste Beispiel für Schauspieler-Frameworks. Man kann sagen, ein Klassiker. Wenn ja, dann fangen wir mit den Klassikern an.

Wir haben also einen Pinger-Agenten, der zu Beginn seiner Arbeit eine Ping-Nachricht an den Ponger-Agenten sendet. Und der Ponger-Agent sendet eine Pong-Nachricht zurück. So sieht es im C ++ - Code aus:

// 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; } }; 

Unsere Aufgabe ist es, einen Test zu schreiben, der bestätigt, dass Ponger bei der Registrierung dieser Agenten bei SObjectizer eine Ping-Nachricht und Pinger als Antwort eine Pong-Nachricht erhält.

OK Wir schreiben einen solchen Test mit dem Doctest Unit-Test-Framework und erhalten:

 #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()); } 

Es scheint einfach zu sein. Mal sehen, was hier passiert.

Zunächst laden wir Beschreibungen der Support-Tools für Agententests herunter:

 #include <so_5/experimental/testing.hpp> 

Alle diese Tools werden im so_5 :: experimentellen :: Test-Namespace beschrieben. Um einen so langen Namen nicht zu wiederholen, führen wir einen kürzeren und bequemeren Alias ​​ein:

 namespace tests = so_5::experimental::testing; 

Das Folgende ist eine Beschreibung eines einzelnen Testfalls (und wir brauchen hier nicht mehr).

Innerhalb des Testfalls gibt es mehrere wichtige Punkte.

Erstens ist dies die Erstellung und der Start einer speziellen Testumgebung für SObjectizer:

 tests::testing_env_t sobj; 

Ohne diese Umgebung kann der „Testlauf“ für Agenten nicht abgeschlossen werden, aber wir werden etwas später darüber sprechen.

Die Klasse "testing_env_t" ist der Klasse "wrap_env_t" in SObjectizer sehr ähnlich. Auf die gleiche Weise startet der SObjectizer im Konstruktor und stoppt im Destruktor. Wenn Sie also Tests schreiben, müssen Sie nicht daran denken, SObjectizer zu starten und zu stoppen.

Als nächstes müssen wir Pinger- und Ponger-Agenten erstellen und registrieren. In diesem Fall müssen wir diese Agenten zur Bestimmung der sogenannten verwenden. "Testszenario." Daher speichern wir Zeiger auf Agenten separat:

 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() ); }); 

Und dann beginnen wir mit dem „Testszenario“ zu arbeiten.

Ein Testfall ist ein Teil, der aus einer direkten Abfolge von Schritten besteht, die von Anfang bis Ende ausgeführt werden müssen. Der Ausdruck "aus einer direkten Sequenz" bedeutet, dass in SObjectizer-5.5.24 die Skriptschritte streng sequentiell "arbeiten", ohne Verzweigungen oder Schleifen.

Das Schreiben eines Tests für Agenten ist die Definition eines Testskripts, das ausgeführt werden muss. Das heißt, Alle Schritte des Testszenarios sollten funktionieren, vom ersten bis zum letzten.

Daher definieren wir in unserem Testfall ein zweistufiges Szenario. Im ersten Schritt wird überprüft, ob der Ponger-Agent die Ping-Nachricht empfängt und verarbeitet:

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

Der zweite Schritt überprüft, ob der Pinger-Agent eine Pong-Nachricht empfängt:

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

Diese beiden Schritte reichen für unseren Testfall völlig aus, daher fahren wir nach ihrer Bestimmung mit der Ausführung des Skripts fort. Wir führen das Skript aus und lassen es nicht länger als 100 ms funktionieren:

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

Hundert Millisekunden sollten mehr als genug sein, damit die beiden Agenten Nachrichten austauschen können (selbst wenn der Test in einer sehr langsamen virtuellen Maschine ausgeführt wird, wie dies manchmal bei Travis CI der Fall ist). Wenn wir beim Schreiben von Agenten einen Fehler gemacht oder ein Testskript falsch beschrieben haben, macht es keinen Sinn, länger als 100 ms auf die Fertigstellung eines fehlerhaften Skripts zu warten.

Nach der Rückkehr von run_for () kann unser Skript entweder erfolgreich abgeschlossen werden oder nicht. Deshalb überprüfen wir einfach das Ergebnis des Skripts:

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

Wenn das Skript nicht erfolgreich abgeschlossen wurde, führt dies zum Fehlschlagen unseres Testfalls.

Einige Klarstellungen und Ergänzungen


Wenn wir diesen Code in einem normalen SObjectizer ausführen:

 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() ); }); 

Dann würden die Agenten von Pinger und Ponger höchstwahrscheinlich Nachrichten austauschen und ihre Arbeit abschließen, bevor sie von Introduce_coop zurückkehren (Wunder des Multithreading sind solche). In der Testumgebung, die dank testing_env_t erstellt wird, geschieht dies jedoch nicht. Die Agenten von Pinger und Ponger warten geduldig, bis wir unser Testskript ausführen. Wie passiert das?

Tatsache ist, dass sich die Agenten in der Testumgebung in einem eingefrorenen Zustand zu befinden scheinen. Das heißt, Nach der Registrierung sind sie in SObjectizer vorhanden, können jedoch keine ihrer Nachrichten verarbeiten. Daher wird auch so_evt_start () nicht für Agenten aufgerufen, bevor das Testskript ausgeführt wird.

Wenn wir das Testskript mit der Methode run_for () ausführen, taut das Testskript zuerst alle eingefrorenen Agenten auf. Anschließend empfängt das Skript vom SObjectizer Benachrichtigungen darüber, was mit den Agenten geschieht. Zum Beispiel, dass der Ponger-Agent die Ping-Nachricht empfangen hat und dass der Ponger-Agent die Nachricht verarbeitet, aber nicht abgelehnt hat.

Wenn solche Benachrichtigungen beim Testskript eingehen, versucht das Skript, sie bis zum ersten Schritt „anzuprobieren“. Wir haben also eine Benachrichtigung, dass Ponger Ping erhalten und verarbeitet hat - ist es für uns interessant oder nicht? Es stellt sich heraus, dass es interessant ist, weil die Beschreibung des Schritts genau das sagt: Es funktioniert, wenn Ponger auf Ping reagiert. Was wir im Code sehen:

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

OK Also hat der erste Schritt funktioniert, fahren Sie mit dem nächsten Schritt fort.

Als nächstes kommt eine Benachrichtigung, dass Agent Pinger auf Pong reagiert hat. Und genau das brauchen Sie, damit der zweite Schritt funktioniert:

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

OK Der zweite Schritt hat also funktioniert. Haben wir noch etwas? Nein. Dies bedeutet, dass das gesamte Testskript abgeschlossen ist und Sie die Steuerung von run_for () zurückgeben können.

Hier im Prinzip, wie das Testskript funktioniert. Tatsächlich ist alles etwas komplizierter, aber wir werden komplexere Aspekte ansprechen, wenn wir ein komplexeres Beispiel betrachten.

Beispiel Essphilosophen


Komplexere Beispiele für Testmittel lassen sich bei der Lösung der bekannten Aufgabe "Dining Philosophen" erkennen. Bei den Schauspielern kann dieses Problem auf verschiedene Arten gelöst werden. Als nächstes betrachten wir die trivialste Lösung: Sowohl Schauspieler als auch Philosophen sind in Form von Akteuren vertreten, für die Philosophen kämpfen müssen. Jeder Philosoph denkt eine Weile nach und versucht dann, die Gabelung links zu nehmen. Wenn dies gelingt, versucht er, die Gabelung rechts zu nehmen. Gelingt dies, dann isst der Philosoph einige Zeit, danach legt er die Gabeln nieder und beginnt zu denken. Wenn es nicht möglich war, den Stecker rechts zu nehmen (d. H. Er wurde von einem anderen Philosophen genommen), gibt der Philosoph den Stecker links zurück und denkt noch einige Zeit nach. Das heißt, Dies ist keine gute Lösung in dem Sinne, dass ein Philosoph möglicherweise zu lange verhungert. Aber dann ist es sehr einfach. Und hat den Umfang, die Fähigkeit zu demonstrieren, Agenten zu testen.

Quellcodes mit der Implementierung von Fork- und Philosopher-Agenten finden Sie hier . In dem Artikel werden wir sie nicht als platzsparend betrachten.

Test auf Gabel


Der erste Test für Agenten der Dining Philosophers wird für Agent Fork sein.

Dieser Agent arbeitet nach einem einfachen Schema. Er hat zwei Zustände: Frei und Genommen. Wenn sich der Agent im Status "Frei" befindet, antwortet er auf eine Take-Nachricht. In diesem Fall wechselt der Agent in den Status "Aufgenommen" und antwortet mit einer Antwortnachricht "Aufgenommen".

Wenn sich der Agent im Status "Aufgenommen" befindet, reagiert er anders auf die Nachricht "Nehmen": Der Status des Agenten ändert sich nicht, und "Besetzt" wird als Antwortnachricht gesendet. Ebenfalls im Status "Aufgenommen" antwortet der Agent auf die Nachricht "Put": Der Agent kehrt in den Status "Frei" zurück.

Im freien Zustand wird die Put-Nachricht ignoriert.

Wir werden versuchen, diesen anhand des folgenden Testfalls zu testen:

 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")); } 

Da es viel Code gibt, werden wir uns in Teilen damit befassen und die Fragmente überspringen, die bereits klar sein sollten.

Das erste, was wir hier brauchen, ist, den echten Philosophenagenten zu ersetzen. Ein Fork-Agent muss Nachrichten von jemandem empfangen und auf jemanden antworten. Aber wir können den echten Philosophen in diesem Testfall nicht verwenden, da der echte Philosoph-Agent seine eigene Verhaltenslogik hat, er selbst Nachrichten sendet und diese Unabhängigkeit uns hier stören wird.

Daher verspotten wir, d.h. Anstelle des echten Philosophen werden wir einen Ersatz dafür einführen: einen leeren Agenten, der selbst nichts sendet, sondern nur gesendete Nachrichten ohne nützliche Verarbeitung empfängt. Dies ist der im Code implementierte Pseudo-Philosoph:

 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>) {}); } }; 

Als Nächstes erstellen wir eine Zusammenarbeit zwischen dem Fork-Agenten und dem PseudoPhilospher-Agenten und beginnen, den Inhalt unseres Testfalls zu bestimmen.

Der erste Schritt des Skripts besteht darin, zu überprüfen, ob Fork im Status "Frei" (und dies ist der Ausgangszustand) nicht auf die Put-Nachricht reagiert. So wird dieser Scheck geschrieben:

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

Das erste, was Aufmerksamkeit erregt, ist die Aufprallkonstruktion.

Sie wird gebraucht, weil unser Agent Fork selbst nichts tut, er reagiert nur auf eingehende Nachrichten. Daher sollte jemand eine Nachricht an den Agenten senden. Aber wer?

Aber der Skriptschritt selbst sendet durch Wirkung. In der Tat ist Impact ein Analogon der üblichen Sendefunktion (und das Format ist das gleiche).

Nun, der Skriptschritt selbst sendet eine Nachricht durch Auswirkung. Aber wann wird er es tun?

Und er wird es tun, wenn er an der Reihe ist. Das heißt, Wenn der Schritt im Skript der erste ist, wird die Auswirkung unmittelbar nach der Eingabe von run_for ausgeführt. Wenn der Schritt im Skript nicht der erste ist, wird die Auswirkung ausgeführt, sobald der vorherige Schritt ausgeführt wurde, und das Skript fährt mit der Verarbeitung des nächsten Schritts fort.

Das zweite, was wir hier diskutieren müssen, ist das Ignorieren von Anrufen. Diese Hilfsfunktion besagt, dass der Schritt ausgelöst wird, wenn der Agent die Nachricht verarbeitet. Das heißt, In diesem Fall muss der Fork-Agent die Verarbeitung der Put-Nachricht ablehnen.

Betrachten wir einen weiteren Schritt des Testszenarios genauer:

 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>()); 

Zuerst sehen wir hier when_all statt when. Dies liegt daran, dass wir mehrere Bedingungen gleichzeitig erfüllen müssen, um einen Schritt auszulösen. Der Gabelagent muss mit Take umgehen. Und der Philosoph muss mit der Antwort umgehen. Deshalb schreiben wir when_all, nicht when. Übrigens gibt es auch wann_jeder, aber wir werden ihn in den heute betrachteten Beispielen nicht treffen.

Zweitens müssen wir auch überprüfen, ob sich der Fork-Agent nach der Take-Verarbeitung im Status Taken befindet. Wir führen die Überprüfung wie folgt durch: Zuerst geben wir an, dass der Name seines aktuellen Status mit dem Tag-Tag "fork" gespeichert werden soll, sobald der Fork-Agent die Verarbeitung von Take abgeschlossen hat. Diese Konstruktion bewahrt nur den Statusnamen des Agenten:

 & tests::store_state_name("fork") 

Wenn das Skript erfolgreich abgeschlossen wurde, überprüfen wir diesen gespeicherten Namen:
 REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork")); 

Das heißt, Wir fragen das Skript: Geben Sie uns den Namen, der mit dem Fork-Tag für den Schritt take_when_free gespeichert wurde, und vergleichen Sie den Namen mit dem erwarteten Wert.

Hier ist vielleicht alles, was im Testfall für den Fork-Agenten notiert werden könnte. Wenn Leser Fragen haben, dann fragen Sie in den Kommentaren, wir werden gerne antworten.

Erfolgreicher Skripttest für Philosophen


Für den Philosophenagenten betrachten wir nur einen Testfall - für den Fall, dass der Philosoph beide Gabeln nehmen und essen kann.

Dieser Testfall sieht folgendermaßen aus:

 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")); } 

Ziemlich umfangreich, aber trivial. Überprüfen Sie zunächst, ob der Philosoph mit dem Nachdenken fertig ist und sich auf das Essen vorbereitet. Dann überprüfen wir, ob er versucht hat, die linke Gabel zu nehmen. Als nächstes sollte er versuchen, die richtige Gabel zu nehmen. Dann sollte er essen und diese Aktivität beenden. Dann muss er beide Gabeln nehmen.

Im Allgemeinen ist alles einfach. Sie sollten sich jedoch auf zwei Dinge konzentrieren.

Erstens können Sie mit der Klasse "testing_env_t" wie mit ihrem Prototyp "wrap_env_t" die SObjectizer-Umgebung anpassen. Wir werden dies verwenden, um den Mechanismus zur Verfolgung der Nachrichtenübermittlung zu aktivieren:

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

Mit diesem Mechanismus können Sie den Nachrichtenübermittlungsprozess „visualisieren“, was bei der Untersuchung des Agentenverhaltens hilfreich ist (darüber haben wir bereits ausführlicher gesprochen ).

Zweitens führt der Agent Philosoph eine Reihe von Aktionen nicht sofort, sondern nach einiger Zeit aus. Um zu arbeiten, muss sich der Agent eine ausstehende StopThinking-Nachricht senden. Diese Nachricht sollte also nach einigen Millisekunden beim Agenten eingehen. Was wir anzeigen, indem wir die notwendige Einschränkung für einen bestimmten Schritt festlegen:

 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)) ); 

Das heißt, hier sagen wir, dass wir nicht an einer Reaktion des Philosophenagenten auf StopThinking interessiert sind, sondern nur an der, die frühestens 250 ms nach Beginn der Verarbeitung dieses Schritts auftrat.

Eine Einschränkung des Typs not_before teilt dem Skript mit, dass alle Ereignisse, die vor Ablauf des angegebenen Zeitlimits auftreten, ignoriert werden sollen.

Es gibt auch eine Einschränkung des Formulars not_after, es funktioniert umgekehrt: Es werden nur die Ereignisse berücksichtigt, die auftreten, bis das angegebene Zeitlimit abgelaufen ist.

Die Einschränkungen not_before und not_after können kombiniert werden, zum Beispiel:

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

In diesem Fall überprüft SObjectizer jedoch nicht die Konsistenz der angegebenen Werte.

Wie haben Sie das umgesetzt?


Ich möchte ein paar Worte darüber sagen, wie alles funktioniert hat. Schließlich standen wir im Großen und Ganzen vor einer großen ideologischen Frage: "Wie testet man Agenten im Prinzip?" und eine kleinere Frage, bereits technisch: "Wie implementiert man das?"

Und wenn es in Bezug auf die Testideologie möglich war, sich aus dem Kopf zu verlieren, dann war die Situation in Bezug auf die Implementierung komplizierter. Es war notwendig, eine Lösung zu finden, die erstens keine radikale Veränderung der Innenräume von SObjectizer erfordert. Und zweitens sollte es eine Lösung sein, die in absehbarer und sehr wünschenswerter kurzer Zeit implementiert werden kann.

Als Ergebnis des schwierigen Prozesses des Rauchens von Bambus wurde eine Lösung gefunden. Dafür war es tatsächlich erforderlich, nur eine kleine Neuerung im regulären Verhalten von SObjectizer vorzunehmen. Grundlage der Lösung ist der Mechanismus für den Nachrichtenumschlag, der in Version 5.5.23 hinzugefügt wurde und über den wir bereits gesprochen haben .

In der Testumgebung wird jede gesendete Nachricht in einen speziellen Umschlag verpackt. Wenn dem Agenten ein Umschlag mit einer Nachricht zur Verarbeitung übergeben wird (oder umgekehrt vom Agenten abgelehnt wird), wird das Testszenario darauf aufmerksam. Dank der Umschläge weiß das Testskript, was gerade passiert, und kann die Momente bestimmen, in denen das Skript die Schritte „Arbeit“ ausführt.

Aber wie kann SObjectizer jede Nachricht in einen speziellen Umschlag einwickeln?

Das war eine interessante Frage. Er entschied sich wie folgt: Ein Konzept wie event_queue_hook wurde erfunden. Dies ist ein spezielles Objekt mit zwei Methoden - on_bind und on_unbind.

Wenn ein Agent an einen bestimmten Dispatcher gebunden ist, gibt der Dispatcher eine Agent-Ereigniswarteschlange an den Agenten aus. Über diese event_queue gelangen Anforderungen für den Agenten in die erforderliche Warteschlange und stehen dem Dispatcher zur Verarbeitung zur Verfügung. Wenn ein Agent in einem SObjectizer ausgeführt wird, hat er einen Zeiger auf event_queue. Wenn ein Agent aus einem SObjectizer entfernt wird, wird sein Zeiger auf event_queue ungültig.

Ab Version 5.5.24 muss der Agent nach Erhalt von event_queue die on_bind-Methode von event_queue_hook aufrufen. Wo der Agent den empfangenen Zeiger an event_queue übergeben soll. Und event_queue_hook kann als Antwort entweder denselben Zeiger oder einen anderen Zeiger zurückgeben. Und der Agent muss den zurückgegebenen Wert verwenden.

Wenn ein Agent aus einem SObjectizer entfernt wird, muss er on_unbind on event_queue_hook aufrufen. In on_unbind übergibt der Agent den Wert, der von der on_bind-Methode zurückgegeben wurde.

Diese ganze Küche wird im SObjectizer ausgeführt und der Benutzer sieht nichts davon. Und im Prinzip wissen Sie vielleicht überhaupt nichts davon. Die Testumgebung von SObjectizer, dieselbe Testumgebung, nutzt jedoch genau event_queue_hook. Innerhalb von testing_env_t wird eine spezielle Implementierung von event_queue_hook erstellt.Diese Implementierung in on_bind umschließt jede event_queue mit einem speziellen Proxy-Objekt. Und bereits dieses Proxy-Objekt legt die an den Agenten gesendeten Nachrichten in einem speziellen Umschlag ab.

Das ist aber noch nicht alles.Sie können sich daran erinnern, dass in einer Testumgebung Agenten eingefroren werden müssen. Dies wird auch durch die genannten Proxy-Objekte implementiert. Während das Testskript nicht ausgeführt wird, speichert das Proxy-Objekt Nachrichten, die zu Hause an den Agenten gesendet werden. Wenn das Skript ausgeführt wird, überträgt das Proxy-Objekt alle zuvor gesammelten Nachrichten in die aktuelle Nachrichtenwarteschlange des Agenten.

Fazit


Abschließend möchte ich zwei Dinge sagen.

Zunächst haben wir unsere Ansicht umgesetzt, wie Agenten in SObjectizer getestet werden können. Meine Meinung, weil es nicht so viele gute Vorbilder gibt. Wir schauten zu Akka . Akka und SObjectizer sind jedoch zu unterschiedlich , um die in Akka funktionierenden Ansätze auf SObjectizer zu portieren. Und C ++ ist nicht Scala / Java, in dem einige Dinge im Zusammenhang mit Introspektion aufgrund von Reflexion erledigt werden können. Also musste ich mir einen Ansatz einfallen lassen, der auf SObjectizer fallen würde.

In Version 5.5.24 wurde die allererste experimentelle Implementierung verfügbar. Sicher können Sie es besser machen. Aber wie kann man verstehen, was nützlich sein wird und was nutzlose Fantasien sind? Leider nichts. Sie müssen versuchen, zu sehen, was in der Praxis passiert.

Also haben wir eine Minimalversion erstellt, die Sie ausprobieren können. Was wir für alle tun möchten: Probieren Sie es aus, experimentieren Sie und teilen Sie Ihre Eindrücke mit uns. Was hat dir gefallen, was hat dir nicht gefallen? Vielleicht fehlt etwas?

Zweitens wurden die Worte, die zu Beginn des Jahres 2017 gesagt wurden, noch relevanter :
… , , , . - — . . . : , .

, , , — , .

Daher mein Rat an diejenigen, die nach einem vorgefertigten Rahmen für Schauspieler suchen: Achten Sie nicht nur auf die Originalität der Ideen und die Schönheit der Beispiele. Schauen Sie sich auch alle möglichen Hilfsprogramme an, die Ihnen dabei helfen, herauszufinden, was in Ihrer Anwendung geschieht: Um beispielsweise herauszufinden, wie viele Akteure sich jetzt in der Warteschlange befinden, wie groß ihre Warteschlange ist, ob die Nachricht den Empfänger nicht erreicht, wohin geht sie dann ... Wenn das Framework dies tut bietet so etwas, wird es für Sie einfacher sein. Wenn dies nicht der Fall ist, haben Sie mehr Arbeit.
All dies ist noch wichtiger, wenn es darum geht, Schauspieler zu testen. Achten Sie daher bei der Auswahl eines Schauspieler-Frameworks darauf, was darin enthalten ist und was nicht. Zum Beispiel haben wir bereits in unserem Toolkit, um das Testen zu vereinfachen :)

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


All Articles