تبسط الجهات الفاعلة البرمجة متعددة الخيوط عن طريق تجنب حالة مشتركة قابلة للتغيير. يمتلك كل ممثل بياناته الخاصة غير المرئية لأي شخص. يتفاعل الممثلون فقط من خلال الرسائل غير المتزامنة. لذلك ، فإن أفظع الرعب من تعدد مؤشرات ترابط في شكل أعراق وموانع عند استخدام الجهات الفاعلة ليست مخيفة (على الرغم من أن الجهات الفاعلة لديها مشاكلها ، ولكن هذا لا يتعلق بذلك الآن).
بشكل عام ، تعد كتابة التطبيقات متعددة الخيوط باستخدام الجهات الفاعلة سهلة وممتعة. بما في ذلك لأن الجهات الفاعلة نفسها مكتوبة بسهولة وبشكل طبيعي. يمكنك أن تقول أن كتابة رمز الممثل هو أسهل جزء في الوظيفة. ولكن عندما يتم كتابة الفاعل ، يطرح سؤال جيد للغاية: "كيفية التحقق من صحة عملها؟"
السؤال هو حقا جيد جدا. يتم سؤالك بانتظام عندما نتحدث عن الجهات الفاعلة بشكل عام وعن
SObjectizer بشكل خاص. وحتى وقت قريب ، لم نتمكن من الإجابة على هذا السؤال إلا بعبارات عامة.
ولكن تم
إصدار الإصدار 5.5.24 ، حيث كان هناك دعم تجريبي لإمكانية اختبار وحدة من الجهات الفاعلة. وفي هذا المقال سنحاول التحدث عن ماهية هذا المحتوى وكيفية استخدامه ومع ما تم تنفيذه.
كيف تبدو اختبارات الممثل؟
سننظر في الميزات الجديدة لـ SObjectizer في بضعة أمثلة ، ونعرف ما هو. يمكن العثور على الكود المصدري للأمثلة التي تمت مناقشتها
في هذا المستودع .
خلال القصة ، سيتم استخدام مصطلحي "ممثل" و "وكيل" بالتبادل. إنهم يعينون نفس الشيء ، ولكن في SObjectizer ، يتم استخدام مصطلح "agent" تاريخياً ، وبالتالي سيتم استخدام "agent" أكثر في كثير من الأحيان.
أبسط مثال مع Pinger و Ponger
مثال الممثلين Pinger and Ponger هو المثال الأكثر شيوعًا لأطر الجهات الفاعلة. يمكن القول الكلاسيكية. حسنا ، إذا كان الأمر كذلك ، فلنبدأ مع الكلاسيكية.
لذلك ، لدينا وكيل Pinger ، والذي في بداية عمله يرسل رسالة Ping إلى وكيل Ponger. ويرسل وكيل Ponger رسالة Pong. هذه هي الطريقة التي تظهر بها في رمز C ++:
مهمتنا هي كتابة اختبار يتحقق من أنه عند تسجيل هذه العوامل مع SObjectizer ، ستتلقى Ponger رسالة Ping ، وسيتلقى Pinger رسالة Pong ردًا.
حسنا نكتب مثل هذا الاختبار باستخدام إطار عمل اختبار الوحدة ، ونحصل على:
#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()); }
يبدو أن يكون سهلا. دعونا نرى ما يحدث هنا.
بادئ ذي بدء ، نقوم بتنزيل أوصاف أدوات دعم اختبار الوكيل:
#include <so_5/experimental/testing.hpp>
يتم وصف جميع هذه الأدوات في مساحة الاسم so_5 :: experimental :: testing ، ولكن حتى لا نكرر هذا الاسم الطويل ، نقدم اسمًا مستعارًا أقصر وأكثر ملاءمة:
namespace tests = so_5::experimental::testing;
فيما يلي وصف لحالة اختبار واحدة (ونحن لسنا بحاجة إلى المزيد هنا).
داخل حالة الاختبار ، هناك العديد من النقاط الرئيسية.
أولاً ، هذا هو إنشاء وإطلاق بيئة اختبار خاصة لـ SObjectizer:
tests::testing_env_t sobj;
بدون هذه البيئة ، لا يمكن إكمال "التشغيل التجريبي" للوكلاء ، لكننا سنتحدث عن ذلك لاحقًا.
تشبه فئة testing_env_t للغاية الفئة wrapped_env_t في SObjectizer. بنفس الطريقة ، يبدأ SObjectizer في المنشئ ، ويتوقف في المدمر. لذلك عند كتابة الاختبارات ، لا يتعين عليك التفكير في بدء تشغيل برنامج SObjectizer وإيقافه.
بعد ذلك ، نحتاج إلى إنشاء وتسجيل وكلاء Pinger و Ponger. في هذه الحالة ، نحتاج إلى استخدام هذه العوامل في تحديد ما يسمى. "سيناريو الاختبار". لذلك ، نقوم بشكل منفصل بتخزين المؤشرات للوكلاء:
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() ); });
ثم نبدأ العمل مع "سيناريو الاختبار".
حالة الاختبار عبارة عن قطعة تتكون من سلسلة مباشرة من الخطوات التي يجب إكمالها من البداية إلى النهاية. تعني عبارة "من تسلسل مباشر" أن البرنامج النصي في SObjectizer-5.5.24 يخطو "العمل" بشكل متسلسل ، دون أي تفرع أو حلقات.
إن كتابة اختبار للعاملين هو تعريف البرنامج النصي للاختبار الذي يجب تنفيذه. أي يجب أن تعمل جميع خطوات سيناريو الاختبار ، من الأول إلى الأخير.
لذلك ، في حالة الاختبار الخاصة بنا ، نقوم بتحديد سيناريو من خطوتين. تتحقق الخطوة الأولى من أن وكيل Ponger سيتلقى رسالة Ping ويعالجها:
sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>());
تتحقق الخطوة الثانية من تلقي عامل Pinger رسالة Pong:
sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>());
هاتان الخطوتان تكفيان تمامًا لحالة اختبارنا ، وبالتالي ، بعد تصميمهما ، ننتقل إلى تنفيذ البرنامج النصي. نقوم بتشغيل البرنامج النصي والسماح له بالعمل لفترة أطول من 100ms:
sobj.scenario().run_for(std::chrono::milliseconds(100));
يجب أن يكون مائة مللي ثانية أكثر من كافية لكي يقوم الوكلاء بتبادل الرسائل (حتى لو تم تشغيل الاختبار داخل جهاز افتراضي بطيء للغاية ، كما هو الحال أحيانًا مع Travis CI). حسنًا ، إذا ارتكبنا خطأ في كتابة الوكلاء أو وصفنا نصًا تجريبيًا بشكل خاطئ ، فلن يكون من المنطقي انتظار اكتمال البرنامج النصي الخاطئ لأكثر من 100 مللي ثانية.
لذلك ، بعد العودة من run_for () ، يمكن إكمال البرنامج النصي الخاص بنا بنجاح أم لا. لذلك ، نتحقق ببساطة من نتيجة البرنامج النصي:
REQUIRE(tests::completed() == sobj.scenario().result());
إذا لم يتم إكمال البرنامج النصي بنجاح ، فسيؤدي ذلك إلى فشل حالة الاختبار الخاصة بنا.
بعض التوضيحات والإضافات
إذا قمنا بتشغيل هذا الرمز داخل SObjectizer العادي:
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() ); });
بعد ذلك ، على الأرجح ، سيتمكّن وكلاء Pinger و Ponger من تبادل الرسائل وإكمال عملهم قبل العودة من intro_coop (معجزات multithreading هكذا). ولكن داخل بيئة الاختبار ، التي يتم إنشاؤها بفضل testing_env_t ، لا يحدث هذا ، ينتظر عملاء Pinger و Ponger بصبر حتى نقوم بتشغيل برنامجنا للاختبار. كيف يحدث هذا؟
الحقيقة هي أنه داخل بيئة الاختبار ، تظهر العوامل في حالة متجمدة. أي بعد التسجيل ، تكون موجودة في SObjectizer ، لكن لا يمكنهم معالجة أي من رسائلهم. لذلك ، حتى so_evt_start () لا يتم استدعاء وكلاء قبل تشغيل البرنامج النصي الاختبار.
عندما نقوم بتشغيل البرنامج النصي للاختبار باستخدام طريقة run_for () ، يقوم البرنامج النصي للتجميد أولاً بإزالة جميع العوامل المجمدة. ثم يبدأ البرنامج النصي في تلقي إعلامات من SObjectizer حول ما يحدث للعملاء. على سبيل المثال ، تلقى عامل Ponger رسالة Ping وأن عامل Ponger قام بمعالجة الرسالة ، لكنه لم يرفضها.
عندما تبدأ هذه الإخطارات في الوصول إلى البرنامج النصي للاختبار ، يحاول البرنامج النصي "محاولة" عليها حتى الخطوة الأولى. لذلك ، لدينا إشعار بأن Ponger استقبلت Ping وعالجته - هل هو مثير للاهتمام بالنسبة لنا أم لا؟ اتضح أنه أمر مثير للاهتمام ، لأن وصف الخطوة يقول بالضبط: إنه يعمل عندما يتفاعل Ponger مع Ping. ما نراه في الكود:
.when(*ponger & tests::reacts_to<ping>())
حسنا لذلك عملت الخطوة الأولى ، انتقل إلى الخطوة التالية.
التالي يأتي إشعار بأن عميل Pinger قد تفاعل مع Pong. وهذا ما تحتاجه للخطوة الثانية للعمل:
.when(*pinger & tests::reacts_to<pong>())
حسنا إذن الخطوة الثانية عملت ، هل لدينا شيء آخر؟ رقم هذا يعني أنه تم إكمال البرنامج النصي للاختبار بالكامل ويمكنك إرجاع التحكم من run_for ().
هنا ، من حيث المبدأ ، كيف يعمل البرنامج النصي للاختبار. في الواقع ، كل شيء أكثر تعقيدًا إلى حد ما ، لكننا سنتطرق إلى جوانب أكثر تعقيدًا عندما ننظر إلى مثال أكثر تعقيدًا.
مثال الطعام الفلاسفة
يمكن رؤية أمثلة أكثر تعقيدًا من عوامل الاختبار في حل المهمة المعروفة "فلاسفة الطعام". بالنسبة للجهات الفاعلة ، يمكن حل هذه المشكلة بعدة طرق. بعد ذلك ، سننظر في الحل الأكثر تافهة: يتم تمثيل كل من الممثلين والفلاسفة في شكل جهات فاعلة ، والتي يتعين على الفلاسفة محاربتها. يفكر كل فيلسوف لفترة ، ثم يحاول أخذ الشوكة على اليسار. إذا نجح ذلك ، يحاول أن يأخذ الشوكة على اليمين. إذا نجح هذا ، فإن الفيلسوف يأكل لبعض الوقت ، وبعد ذلك يضع الشوك ويبدأ في التفكير. إذا لم يكن بالإمكان أخذ القابس على اليمين (أي ، أخذه فيلسوف آخر) ، فإن الفيلسوف يُرجع القابس على اليسار ويفكر لبعض الوقت. أي هذا ليس حلاً جيدًا بمعنى أن بعض الفيلسوف قد يتضور جوعًا لفترة طويلة. ولكن بعد ذلك هو بسيط جدا. ولديه مجال لإثبات القدرة على اختبار العوامل.
يمكن العثور على أكواد المصدر مع تطبيق وكلاء Fork و Philosopher
هنا ، في المقالة لن نعتبرها توفر مساحة.
اختبار لشوكة
سيكون أول اختبار لوكلاء من فلاسفة الطعام هو وكيل شوكة.
هذا الوكيل يعمل وفق مخطط بسيط. لديه دولتين: حرة وأخذت. عندما يكون الوكيل في حالة Free ، فإنه يستجيب لرسالة Take. في هذه الحالة ، يدخل الوكيل في حالة Taken ويستجيب برسالة استجابة Taken.
عندما يكون الوكيل في حالة Taken ، فإنه يستجيب لرسالة Take بشكل مختلف: حالة الوكيل لا تتغير ، ويتم إرسال Busy كرسالة استجابة. في حالة Taken أيضًا ، يستجيب الوكيل لرسالة Put: يعود العامل إلى الحالة الحرة.
في الحالة الحرة ، يتم تجاهل رسالة Put.
سنحاول اختبار هذا عن طريق حالة الاختبار التالية:
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")); }
هناك الكثير من التعليمات البرمجية ، لذلك سنتعامل معها في أجزاء ، وتخطي تلك الشظايا التي يجب أن تكون واضحة بالفعل.
أول شيء نحتاجه هنا هو استبدال وكيل الفيلسوف الحقيقي. يجب أن يتلقى وكيل 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>) {}); } };
بعد ذلك ، نقوم بإنشاء تعاون من وكيل Fork ووكيل PseudoPhilospher ونبدأ في تحديد محتويات حالة الاختبار الخاصة بنا.
تتمثل الخطوة الأولى من البرنامج النصي في التحقق من أن Fork ، كونه في الحالة الحرة (وهذه حالته الأولية) ، لا يستجيب لرسالة Put. إليك كيفية كتابة هذا التحقق:
sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>());
أول ما يجذب الانتباه هو تأثير البناء.
إنها مطلوبة لأن وكيلنا Fork لا يفعل شيئًا بنفسه ، فهو يتفاعل فقط مع الرسائل الواردة. لذلك ، يجب على شخص ما إرسال رسالة إلى الوكيل. لكن من؟
لكن خطوة البرنامج النصي نفسها ترسل عبر التأثير. في الواقع ، فإن التأثير هو تناظرية لوظيفة الإرسال المعتادة (والتنسيق هو نفسه).
حسنًا ، ستقوم خطوة البرنامج النصي نفسها بإرسال رسالة من خلال التأثير. ولكن متى سيفعل ذلك؟
وسوف يفعل ذلك عندما يحين دوره. أي إذا كانت الخطوة في البرنامج النصي هي الأولى ، فسيتم تنفيذ التأثير مباشرة بعد إدخال run_for. إذا لم تكن الخطوة في البرنامج النصي هي الأولى ، فسيتم تنفيذ التأثير بمجرد نجاح الخطوة السابقة وسيواصل البرنامج النصي معالجة الخطوة التالية.
الشيء الثاني الذي نحتاج إلى مناقشته هنا هو تجاهل المكالمات. تقول وظيفة المساعد هذه أن الخطوة يتم تشغيلها عندما يفشل العامل في معالجة الرسالة. أي في هذه الحالة ، يجب أن يرفض وكيل Fork معالجة رسالة 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>());
أولاً ، هنا نرى when_all بدلاً من متى. هذا لأنه لبدء خطوة ، نحتاج إلى تلبية العديد من الشروط في وقت واحد. يحتاج عامل الشوكة إلى التعامل مع Take. ويحتاج الفيلسوف إلى التعامل مع استجابة Taken. لذلك ، نكتب when_all ، وليس متى. بالمناسبة ، هناك أيضًا وقت ، لكننا لن نلتقي به في الأمثلة التي تم بحثها اليوم.
ثانياً ، نحتاج أيضًا إلى التحقق من حقيقة أنه بعد معالجة Take ، سيكون وكيل Fork في ولاية Taken. نقوم بالتحقق على النحو التالي: أولاً ، نشير إلى أنه بمجرد انتهاء عامل Fork من المعالجة Take ، يجب حفظ اسم حالته الحالية باستخدام علامة التمييز "fork". هذا البناء يحفظ فقط اسم الدولة للوكيل:
& tests::store_state_name("fork")
وبعد ذلك ، عند اكتمال البرنامج النصي بنجاح ، نتحقق من هذا الاسم المحفوظ:
REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork"));
أي نطلب البرنامج النصي: أعطنا الاسم الذي تم حفظه بعلامة الشوكة للخطوة المسماة take_when_free ، ثم قارن الاسم بالقيمة المتوقعة.
هنا ، ربما ، كل ما يمكن ملاحظته في حالة اختبار وكيل شوكة. إذا كان لدى القراء أي أسئلة ، ثم طرحها في التعليقات ، سوف نقوم بالرد بكل سرور.
اختبار سيناريو ناجح للفيلسوف
بالنسبة لفيلسوف الفيلسوف ، سننظر في حالة اختبار واحدة فقط - للحالة التي يمكن فيها للفيلسوف أن يأخذ كل من الشوك والأكل.
ستبدو حالة الاختبار هذه كما يلي:
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")); }
ضخمة جدا ، ولكن تافهة. أولاً ، تحقق من أن الفيلسوف قد انتهى من التفكير وبدأ التحضير للطعام. ثم نتحقق من أنه حاول أخذ الشوكة اليسرى. بعد ذلك ، يجب أن يحاول أخذ الشوكة الصحيحة. ثم يجب أن يأكل ووقف هذا النشاط. ثم يجب أن يضع كل من الشوك المتخذة.
بشكل عام ، كل شيء بسيط. ولكن يجب عليك التركيز على شيئين.
أولاً ، تتيح لك الفئة testing_env_t ، مثل النموذج الأولي ، wrapped_env_t ، تخصيص بيئة SObjectizer. سنستخدم هذا لتمكين آلية تتبع تسليم الرسائل:
tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } };
تتيح لك هذه الآلية "تصور" عملية تسليم الرسائل ، مما يساعد في التحقيق في سلوك الوكيل (
تحدثنا بالفعل عن ذلك
بمزيد من التفاصيل ).
ثانياً ، يقوم الفيلسوف وكيل سلسلة من الإجراءات ليس على الفور ، ولكن بعد مرور بعض الوقت. لذلك ، بدء العمل ، يجب على الوكيل إرسال رسالة StopThinking معلقة إلى نفسه. لذلك يجب أن تأتي هذه الرسالة إلى الوكيل بعد بضع ميلي ثانية. التي نشير إليها عن طريق وضع القيود اللازمة لخطوة معينة:
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)) );
أي نقول هنا أننا لسنا مهتمين بأي رد فعل من وكيل الفيلسوف على StopThinking ، ولكن فقط الذي حدث في وقت لا يتجاوز 250ms بعد بدء معالجة هذه الخطوة.
هناك قيود من النوع not_before تخبر البرنامج النصي بأنه يجب تجاهل كل الأحداث التي تحدث قبل انتهاء المهلة المحددة.
يوجد أيضًا تقييد للنموذج not_after ، وهو يعمل في الاتجاه المعاكس: فقط الأحداث التي تحدث حتى انتهاء المهلة المحددة يتم أخذها في الاعتبار.
يمكن دمج قيود not_before و not_after ، على سبيل المثال:
.constraints( tests::not_before(std::chrono::milliseconds(250)), tests::not_after(std::chrono::milliseconds(1250)))
ولكن في هذه الحالة ، لا يتحقق SObjectizer من تناسق القيم المعطاة.
كيف تمكنت من تنفيذ هذا؟
أود أن أقول بضع كلمات حول كيفية عمل كل شيء. بعد كل شيء ، على وجه العموم ، واجهنا سؤال أيديولوجي كبير واحد: "كيف يمكن اختبار العوامل من حيث المبدأ؟" وسؤال أصغر ، تقني بالفعل: "كيفية تنفيذ هذا؟"
وإذا كان الأمر يتعلق بفكر الاختبار ، فقد كان من الممكن الخروج من عقلك ، ثم حول التنفيذ ، كان الموقف أكثر تعقيدًا. كان من الضروري إيجاد حل لا يتطلب أولاً تغييرًا جذريًا في التصميمات الداخلية لـ SObjectizer. وثانياً ، كان من المفترض أن يكون حلاً يمكن تنفيذه في وقت قصير متوقع ومرغوب فيه للغاية.
نتيجة للعملية الصعبة المتمثلة في تدخين البامبو ، تم العثور على حل. لهذا ، كان مطلوبًا ، في الواقع ، إنشاء ابتكار صغير واحد فقط في السلوك المنتظم لـ SObjectizer. وأساس الحل هو
آلية مغلف الرسائل ، التي تمت إضافتها في الإصدار 5.5.23 والتي تحدثنا عنها بالفعل .
داخل بيئة الاختبار ، يتم تغليف كل رسالة مرسلة في مظروف خاص. عندما يتم إعطاء مغلف مع رسالة إلى الوكيل للمعالجة (أو ، على العكس ، يرفضه الوكيل) ، يصبح سيناريو الاختبار على علم بذلك. بفضل المغلفات ، يعرف البرنامج النصي للاختبار ما يحدث ويمكنه تحديد لحظات عندما يتخطى البرنامج النصي "العمل".
ولكن كيف تجعل SObjectizer يلف كل رسالة في مظروف خاص؟
كان هذا سؤالًا مثيرًا للاهتمام. قرر ما يلي: تم اختراع مفهوم مثل
event_queue_hook . هذا كائن خاص له طريقتان - on_bind و on_unbind.
عندما يكون الوكيل منضماً إلى مرسل معين ، يصدر المرسل وكيل event_queue إلى الوكيل. من خلال event_queue ، تدخل طلبات الوكيل في قائمة الانتظار اللازمة وتصبح متاحة للمرسل للمعالجة. عندما يعمل الوكيل داخل SObjectizer ، فإنه يحتوي على مؤشر إلى event_queue. عند إزالة أحد العوامل من SObjectizer ، يتم إلغاء مؤشره إلى event_queue.
لذلك ، بدءًا من الإصدار 5.5.24 ، يجب على الوكيل ، عند استلام event_queue ، استدعاء طريقة on_bind event_queue_hook. حيث يجب على الوكيل تمرير المؤشر المستلم إلى event_queue. و event_queue_hook يمكنه إرجاع إما المؤشر نفسه أو مؤشر آخر استجابة. ويجب على الوكيل استخدام القيمة التي تم إرجاعها.
عند إزالة وكيل من SObjectizer ، يجب عليه الاتصال on_unbind على event_queue_hook. في on_unbind ، يمرر العامل القيمة التي تم إرجاعها بواسطة أسلوب on_bind.
يتم تنفيذ هذا المطبخ بالكامل داخل SObjectizer ولا يرى المستخدم أي شيء من هذا. ومن حيث المبدأ ، قد لا تعرف هذا على الإطلاق. لكن بيئة اختبار SObjectizer ، وهي نفس testing_env_t ، تستغل event_queue_hook بالضبط. داخل testing_env_t ، يتم إنشاء تطبيق خاص لـ event_queue_hook.
هذا التطبيق في on_bind يلتف كل event_queue في كائن وكيل خاص. وبالفعل يضع هذا الكائن الوكيل الرسائل المرسلة إلى الوكيل في مظروف خاص.لكن هذا ليس كل شيء.
قد تتذكر أنه في بيئة الاختبار ، يجب تجميد الوكلاء. يتم تطبيق هذا أيضًا من خلال كائنات الوكيل المذكورة. أثناء عدم تشغيل البرنامج النصي للاختبار ، يقوم الكائن الوكيل بتخزين الرسائل المرسلة إلى العامل في المنزل. ولكن عند تشغيل البرنامج النصي ، ينقل الكائن الوكيل جميع الرسائل المتراكمة مسبقًا إلى قائمة انتظار الرسائل الحالية للوكيل.الخاتمة
في الختام ، أريد أن أقول شيئين.بادئ ذي بدء ، قمنا بتطبيق وجهة نظرنا حول كيفية اختبار العوامل في SObjectizer. رأيي لأنه لا يوجد الكثير من الأمثلة الجيدة حولها. نظرنا نحو عكا . لكن Akka و SObjectizer مختلفان جدًا عن توجيه المناهج التي تعمل في Akka إلى SObjectizer. و C ++ ليس Scala / Java ، حيث يمكن القيام ببعض الأشياء المتعلقة بالتأمل من خلال التفكير. لذلك اضطررت إلى الخروج بنهج من شأنه أن يقع على SObjectizer.في الإصدار 5.5.24 ، أصبح التطبيق التجريبي الأول متاحًا. بالتأكيد يمكنك أن تفعل أفضل. ولكن كيف نفهم ما الذي سيكون مفيدا وما هي التخيلات عديمة الفائدة؟ لسوء الحظ ، لا شيء. تحتاج إلى أن تأخذ وتجرب ، ترى ما يحدث في الممارسة العملية.لذلك قمنا بإعداد إصدار بسيط يمكنك إجراؤه وتجربته. ما نقترح القيام به بالنسبة للجميع: جرِّب انطباعاتك وجربها وشاركها معنا. ما الذي أعجبك ، ما لم يعجبك؟ ربما هناك شيء مفقود؟ثانياً ، أصبحت الكلمات التي قيلت في بداية عام 2017 أكثر أهمية :… , , , . - — . . . : , .
, , , — , .
لذلك ، نصيحتي لأولئك الذين يبحثون عن إطار فاعل جاهز: لا ينتبه فقط إلى أصالة الأفكار وجمال الأمثلة. انظر أيضًا إلى جميع أنواع الأشياء المساعدة التي ستساعدك على معرفة ما يحدث في التطبيق الخاص بك: على سبيل المثال ، لمعرفة عدد الممثلين الموجودين في الداخل الآن ، ما هي أحجام قائمة الانتظار الخاصة بهم ، إذا لم تصل الرسالة إلى المستلم ، ثم أين تذهب ... يوفر شيئا من هذا القبيل ، سيكون من الأسهل بالنسبة لك. إذا لم يحدث ذلك ، سيكون لديك المزيد من العمل.
كل ما سبق هو أكثر أهمية عندما يتعلق الأمر باختبار الجهات الفاعلة. لذلك ، عند اختيار إطار الفاعل لنفسك ، انتبه لما هو فيه وما هو غير موجود. على سبيل المثال ، لدينا بالفعل في مجموعة أدوات لدينا لتبسيط الاختبار :)