Aktor menyederhanakan pemrograman multi-utas dengan menghindari keadaan yang dapat dibagikan bersama. Setiap aktor memiliki data sendiri yang tidak terlihat oleh siapa pun. Aktor hanya berinteraksi melalui pesan asinkron. Oleh karena itu, kengerian multithreading yang paling menakutkan dalam bentuk ras dan kebuntuan ketika menggunakan aktor tidak mengerikan (meskipun aktor memiliki masalah mereka, tetapi ini bukan tentang itu sekarang).
Secara umum, menulis aplikasi multi-utas menggunakan aktor mudah dan menyenangkan. Termasuk karena aktornya sendiri ditulis dengan mudah dan alami. Anda bahkan bisa mengatakan bahwa menulis kode aktor adalah bagian termudah dari pekerjaan itu. Tetapi ketika aktor ditulis, pertanyaan yang sangat bagus muncul: "Bagaimana cara memeriksa kebenaran dari pekerjaannya?"
Pertanyaannya sangat bagus. Kami secara teratur ditanya ketika kami berbicara tentang aktor pada umumnya dan tentang
SObjectizer pada khususnya. Dan hingga saat ini, kami hanya dapat menjawab pertanyaan ini secara umum.
Tetapi
versi 5.5.24 keluar , di mana ada dukungan eksperimental untuk kemungkinan pengujian unit aktor. Dan pada artikel ini kita akan mencoba untuk membicarakan tentang apa itu, bagaimana menggunakannya dan dengan apa yang telah diterapkan.
Seperti apa tes aktor?
Kami akan mempertimbangkan fitur baru SObjectizer pada beberapa contoh, meneruskan apa. Kode sumber untuk contoh-contoh yang dibahas dapat ditemukan
di repositori ini .
Sepanjang cerita, istilah "aktor" dan "agen" akan digunakan secara bergantian. Mereka menunjuk hal yang sama, tetapi dalam SObjectizer istilah "agen" secara historis digunakan, oleh karena itu "agen" lebih lanjut akan digunakan lebih sering.
Contoh paling sederhana dengan Pinger dan Ponger
Contoh aktor Pinger dan Ponger mungkin adalah contoh paling umum untuk kerangka aktor. Bisa dikatakan klasik. Nah, jika demikian, maka mari kita mulai dengan klasik.
Jadi, kami memiliki agen Pinger, yang pada awal kerjanya mengirim pesan Ping ke agen Ponger. Dan agen Ponger mengirim kembali pesan Pong. Ini adalah tampilannya dalam kode C ++:
Tugas kami adalah menulis tes yang akan memverifikasi bahwa setelah mendaftarkan agen-agen ini dengan SObjectizer, Ponger akan menerima pesan Ping, dan Pinger akan menerima pesan Pong sebagai tanggapan.
Oke Kami menulis tes seperti itu menggunakan kerangka kerja unit-tes
doctest dan mendapatkan:
#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()); }
Tampaknya mudah. Mari kita lihat apa yang terjadi di sini.
Pertama-tama, kami mengunduh deskripsi alat dukungan pengujian agen:
#include <so_5/experimental/testing.hpp>
Semua alat ini dijelaskan di so_5 :: experimental :: testing namespace, tetapi agar tidak mengulangi nama yang panjang, kami memperkenalkan alias yang lebih pendek dan lebih nyaman:
namespace tests = so_5::experimental::testing;
Berikut ini adalah uraian satu kasus uji (dan kami tidak perlu lebih banyak di sini).
Di dalam test case, ada beberapa poin kunci.
Pertama, ini adalah pembuatan dan peluncuran lingkungan uji khusus untuk SObjectizer:
tests::testing_env_t sobj;
Tanpa lingkungan ini, "uji coba" untuk agen tidak dapat diselesaikan, tetapi kami akan membicarakannya nanti.
Kelas testing_env_t sangat mirip dengan kelas wrap_env_t di SObjectizer. Dengan cara yang sama, SObjectizer dimulai di konstruktor, dan berhenti di destruktor. Jadi saat menulis tes, Anda tidak perlu memikirkan memulai dan menghentikan SObjectizer.
Selanjutnya, kita perlu membuat dan mendaftarkan agen Pinger dan Ponger. Dalam hal ini, kita perlu menggunakan agen ini dalam menentukan apa yang disebut. "Uji skenario." Karena itu, kami secara terpisah menyimpan pointer ke agen:
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() ); });
Dan kemudian kita mulai bekerja dengan "skenario pengujian".
Kasing uji adalah bagian yang terdiri dari urutan langkah-langkah langsung yang harus diselesaikan dari awal hingga akhir. Ungkapan "dari urutan langsung" berarti bahwa dalam SObjectizer-5.5.24 script langkah "bekerja" secara ketat berurutan, tanpa bercabang atau loop.
Menulis tes untuk agen adalah definisi skrip uji yang perlu dijalankan. Yaitu semua langkah skenario pengujian harus bekerja, dari yang pertama hingga yang terakhir.
Oleh karena itu, dalam kasus uji kami, kami menetapkan skenario dua langkah. Langkah pertama memverifikasi bahwa agen Ponger akan menerima dan memproses pesan Ping:
sobj.scenario().define_step("ping") .when(*ponger & tests::reacts_to<ping>());
Langkah kedua memeriksa apakah agen Pinger menerima pesan Pong:
sobj.scenario().define_step("pong") .when(*pinger & tests::reacts_to<pong>());
Dua langkah ini cukup untuk uji kasus kami, oleh karena itu, setelah ditentukan, kami melanjutkan ke eksekusi skrip. Kami menjalankan skrip dan memungkinkannya bekerja tidak lebih dari 100 ms:
sobj.scenario().run_for(std::chrono::milliseconds(100));
Seratus milidetik harus lebih dari cukup bagi kedua agen untuk bertukar pesan (bahkan jika tes dijalankan di dalam mesin virtual yang sangat lambat, seperti yang terkadang terjadi pada Travis CI). Nah, jika kita membuat kesalahan dalam menulis agen atau salah menggambarkan skrip pengujian, maka menunggu selesainya skrip yang salah selama lebih dari 100 ms tidak masuk akal.
Jadi, setelah kembali dari run_for (), skrip kami dapat berhasil diselesaikan atau tidak. Karena itu, kami cukup memeriksa hasil skrip:
REQUIRE(tests::completed() == sobj.scenario().result());
Jika skrip tidak berhasil diselesaikan, maka ini akan menyebabkan kegagalan uji kasus kami.
Beberapa klarifikasi dan tambahan
Jika kita menjalankan kode ini di dalam 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() ); });
kemudian, kemungkinan besar, agen Pinger dan Ponger akan mengatur untuk bertukar pesan dan menyelesaikan pekerjaan mereka sebelum kembali dari perkenalan_coop (keajaiban multithreading begitu). Tetapi di dalam lingkungan pengujian, yang dibuat berkat testing_env_t, ini tidak terjadi, agen Pinger dan Ponger dengan sabar menunggu sampai kami menjalankan skrip pengujian kami. Bagaimana ini bisa terjadi?
Faktanya adalah bahwa di dalam lingkungan pengujian, agen tampak dalam keadaan beku. Yaitu setelah pendaftaran, mereka hadir di SObjectizer, tetapi mereka tidak dapat memproses pesan mereka. Oleh karena itu, bahkan so_evt_start () tidak dipanggil untuk agen sebelum skrip uji dijalankan.
Ketika kami menjalankan skrip uji menggunakan metode run_for (), skrip uji pertama mencairkan semua agen beku. Dan kemudian skrip mulai menerima pemberitahuan dari SObjectizer tentang apa yang terjadi pada agen. Misalnya, bahwa agen Ponger menerima pesan Ping dan bahwa agen Ponger memproses pesan tersebut, tetapi tidak menolaknya.
Ketika pemberitahuan tersebut mulai datang ke skrip pengujian, skrip mencoba untuk "mencoba" mereka ke langkah pertama. Jadi, kami memiliki pemberitahuan bahwa Ponger menerima dan memproses Ping - apakah ini menarik bagi kami atau tidak? Ternyata itu menarik, karena deskripsi langkah itu mengatakan dengan tepat bahwa: ia bekerja ketika Ponger bereaksi terhadap Ping. Apa yang kita lihat dalam kode:
.when(*ponger & tests::reacts_to<ping>())
Oke Jadi langkah pertama berhasil, lanjutkan ke langkah berikutnya.
Selanjutnya muncul pemberitahuan bahwa Agen Pinger bereaksi terhadap Pong. Dan inilah yang Anda butuhkan untuk langkah kedua untuk bekerja:
.when(*pinger & tests::reacts_to<pong>())
Oke Jadi langkah kedua berhasil, apakah kita memiliki sesuatu yang lain? Tidak. Ini berarti bahwa seluruh skrip pengujian selesai dan Anda dapat mengembalikan kontrol dari run_for ().
Di sini, pada prinsipnya, cara kerja skrip uji. Sebenarnya, semuanya agak lebih rumit, tetapi kita akan menyentuh aspek yang lebih kompleks ketika kita mempertimbangkan contoh yang lebih kompleks.
Contoh Filsuf Bersantap
Contoh yang lebih kompleks dari agen penguji dapat dilihat dalam menyelesaikan tugas terkenal "Makan filsuf." Pada aktor, masalah ini dapat diselesaikan dengan beberapa cara. Selanjutnya, kita akan mempertimbangkan solusi paling sepele: aktor dan filsuf diwakili dalam bentuk aktor, yang harus diperangi oleh para filsuf. Setiap filsuf berpikir sejenak, lalu mencoba mengambil garpu di sebelah kiri. Jika ini berhasil, ia mencoba mengambil garpu di sebelah kanan. Jika ini berhasil, maka filsuf makan untuk beberapa waktu, setelah itu ia meletakkan garpu dan mulai berpikir. Jika tidak mungkin untuk mengambil steker di sebelah kanan (mis., Itu diambil oleh filsuf lain), maka filsuf mengembalikan steker di sebelah kiri dan berpikir untuk beberapa waktu lagi. Yaitu ini bukan solusi yang baik dalam arti bahwa beberapa filsuf mungkin kelaparan terlalu lama. Tapi kemudian itu sangat sederhana. Dan memiliki ruang untuk menunjukkan kemampuan untuk menguji agen.
Kode sumber dengan implementasi agen Fork dan Filsuf dapat ditemukan di
sini , dalam artikel kami tidak akan mempertimbangkan mereka untuk menghemat ruang.
Tes untuk Fork
Tes pertama untuk agen dari Philosophers Makan adalah untuk agen Fork.
Agen ini bekerja sesuai dengan skema sederhana. Dia memiliki dua negara: Gratis dan Diambil. Saat agen berada dalam status Bebas, ia merespons pesan Ambil. Dalam hal ini, agen memasuki status Diambil dan merespons dengan pesan tanggapan Diambil.
Ketika agen berada di negara bagian Diambil, itu menanggapi pesan Ambil berbeda: keadaan agen tidak berubah, dan Sibuk dikirim sebagai pesan respon. Juga di status Diambil, agen merespons pesan Put: agen kembali ke status Bebas.
Dalam status bebas, pesan Put diabaikan.
Kami akan mencoba menguji yang ini dengan menggunakan test case berikut:
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")); }
Ada banyak kode, jadi kita akan mengatasinya di bagian-bagian, melewatkan bagian-bagian yang seharusnya sudah jelas.
Hal pertama yang kita butuhkan di sini adalah mengganti agen Filsuf yang asli. Agen Fork harus menerima pesan dari seseorang dan merespons seseorang. Tetapi kita tidak dapat menggunakan filsuf asli dalam kasus uji ini, karena agen filsuf nyata memiliki logika perilaku sendiri, ia mengirim pesan sendiri dan kemerdekaan ini akan mengganggu kita di sini.
Karena itu, kami
mengejek , mis. alih-alih filsuf asli, kami akan memperkenalkan pengganti untuk itu: agen kosong yang tidak mengirim apa pun itu sendiri, tetapi hanya menerima pesan yang dikirim, tanpa proses yang bermanfaat. Ini adalah filsuf semu yang diimplementasikan dalam kode:
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>) {}); } };
Selanjutnya, kami membuat kolaborasi dari agen Fork dan agen PseudoPhilospher dan mulai menentukan konten dari kasus pengujian kami.
Langkah pertama skrip adalah memverifikasi bahwa Fork, yang berada dalam status Bebas (dan ini adalah kondisi awalnya), tidak menanggapi pesan Put. Begini cara penulisan cek ini:
sobj.scenario().define_step("put_when_free") .impact<msg_put>(*fork) .when(*fork & tests::ignores<msg_put>());
Hal pertama yang menarik perhatian adalah dampak konstruksi.
Dia dibutuhkan karena agen kami Fork tidak melakukan apa-apa sendiri, dia hanya bereaksi terhadap pesan yang masuk. Karena itu, seseorang harus mengirim pesan kepada agen. Tapi siapa
Tetapi langkah skrip itu sendiri mengirimkan melalui dampak. Bahkan, dampak adalah analog dari fungsi kirim yang biasa (dan formatnya sama).
Nah, langkah skrip itu sendiri akan mengirim pesan melalui dampak. Tetapi kapan dia akan melakukannya?
Dan dia akan melakukannya ketika giliran datang kepadanya. Yaitu jika langkah dalam skrip adalah yang pertama, maka dampak akan dieksekusi segera setelah memasukkan run_for. Jika langkah dalam skrip bukan yang pertama, maka dampak akan dieksekusi segera setelah langkah sebelumnya berhasil dan skrip akan melanjutkan untuk memproses langkah berikutnya.
Hal kedua yang perlu kita diskusikan di sini adalah panggilan abaikan. Fungsi helper ini mengatakan bahwa langkah tersebut dipicu ketika agen gagal memproses pesan. Yaitu dalam hal ini, agen Fork harus menolak untuk memproses pesan Put.
Mari kita pertimbangkan satu langkah lagi dari skenario pengujian lebih terinci:
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>());
Pertama, di sini kita melihat when_all bukan when. Ini karena untuk memicu langkah, kita perlu memenuhi beberapa kondisi sekaligus. Agen garpu perlu menangani Take. Dan Filsuf perlu menangani tanggapan Diambil. Karena itu, kami menulis when_all, bukan kapan. Ngomong-ngomong, ada juga when_any, tetapi kita tidak akan bertemu dengannya dalam contoh-contoh yang dipertimbangkan hari ini.
Kedua, kita juga perlu memeriksa fakta bahwa setelah Mengambil pemrosesan, agen Fork akan berada dalam status Taken. Kami melakukan pemeriksaan sebagai berikut: pertama kami menunjukkan bahwa segera setelah agen Fork selesai memproses Take, nama negara saat ini harus disimpan menggunakan tag tag "fork". Konstruksi ini hanya mempertahankan nama negara agen:
& tests::store_state_name("fork")
Dan kemudian, ketika skrip selesai dengan sukses, kami memeriksa nama yang disimpan ini:
REQUIRE("taken" == sobj.scenario().stored_state_name("take_when_free", "fork"));
Yaitu kami meminta skrip: beri kami nama yang disimpan dengan tag garpu untuk langkah bernama take_when_free, lalu bandingkan nama dengan nilai yang diharapkan.
Di sini, mungkin, adalah semua yang dapat dicatat dalam kasus uji untuk agen Fork. Jika pembaca memiliki pertanyaan, maka tanyakan di komentar, kami akan menjawab dengan senang hati.
Tes Naskah yang Berhasil untuk Filsuf
Untuk agen Philosopher, kami hanya akan mempertimbangkan satu test case - untuk kasus ketika Philosopher dapat mengambil garpu dan makan.
Test case ini akan terlihat seperti ini:
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")); }
Cukup tebal, tapi sepele. Pertama, periksa apakah Filsuf telah selesai berpikir dan sudah mulai menyiapkan makanan. Lalu kami periksa apakah dia mencoba mengambil garpu kiri. Selanjutnya, ia harus mencoba mengambil garpu yang tepat. Maka dia harus makan dan menghentikan aktivitas ini. Maka dia harus meletakkan kedua garpu diambil.
Secara umum, semuanya sederhana. Tetapi Anda harus fokus pada dua hal.
Pertama, kelas testing_env_t, seperti prototipenya, envelop_env_t, memungkinkan Anda untuk menyesuaikan Lingkungan SObjectizer. Kami akan menggunakan ini untuk mengaktifkan mekanisme pelacakan pengiriman pesan:
tests::testing_env_t sobj{ [](so_5::environment_params_t & params) { params.message_delivery_tracer( so_5::msg_tracing::std_cout_tracer()); } };
Mekanisme ini memungkinkan Anda untuk "memvisualisasikan" proses pengiriman pesan, yang membantu dalam penyelidikan perilaku agen (kami sudah
membicarakan hal ini secara
lebih rinci ).
Kedua, agen Filsuf melakukan serangkaian tindakan tidak segera, tetapi setelah beberapa waktu. Jadi, mulai bekerja, agen harus mengirim sendiri pesan StopThinking yang tertunda. Jadi pesan ini harus sampai ke agen setelah beberapa milidetik. Yang kami tunjukkan dengan menetapkan batasan yang diperlukan untuk langkah tertentu:
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)) );
Yaitu di sini kita mengatakan bahwa kita tidak tertarik pada reaksi apa pun dari agen Filsuf terhadap StopThinking, tetapi hanya itu yang terjadi tidak lebih awal dari 250 ms setelah dimulainya pemrosesan langkah ini.
Pembatasan dari jenis not_before memberitahu skrip bahwa semua peristiwa yang terjadi sebelum batas waktu yang ditentukan berakhir harus diabaikan.
Ada juga pembatasan bentuk not_after, ia bekerja sebaliknya: hanya peristiwa yang terjadi sampai batas waktu yang ditentukan telah kedaluwarsa yang diperhitungkan.
Batasan not_before dan not_after dapat digabungkan, misalnya:
.constraints( tests::not_before(std::chrono::milliseconds(250)), tests::not_after(std::chrono::milliseconds(1250)))
tetapi dalam kasus ini, SObjectizer tidak memeriksa konsistensi dari nilai yang diberikan.
Bagaimana Anda bisa menerapkan ini?
Saya ingin mengatakan beberapa patah kata tentang bagaimana semuanya bekerja. Bagaimanapun, pada umumnya, kami dihadapkan pada satu pertanyaan ideologis besar: "Bagaimana cara menguji agen pada prinsipnya?" dan satu pertanyaan yang lebih kecil, sudah teknis: "Bagaimana cara menerapkan ini?"
Dan jika tentang ideologi pengujian itu mungkin untuk keluar dari pikiran Anda, maka tentang implementasi situasinya lebih rumit. Itu perlu untuk menemukan solusi yang, pertama, tidak akan memerlukan perubahan radikal dari interior SObjectizer. Dan, kedua, itu seharusnya menjadi solusi yang dapat diimplementasikan dalam waktu dekat dan, sangat diinginkan, dalam waktu singkat.
Sebagai hasil dari proses sulit merokok bambu, solusinya ditemukan. Untuk ini, diperlukan, pada kenyataannya, untuk membuat hanya satu inovasi kecil dalam perilaku reguler dari SObjectizer. Dan dasar solusinya adalah
mekanisme amplop untuk pesan, yang ditambahkan dalam versi 5.5.23 dan yang sudah kita bicarakan .
Di dalam lingkungan pengujian, setiap pesan yang dikirim terbungkus dalam amplop khusus. Ketika sebuah amplop dengan pesan diberikan kepada agen untuk diproses (atau, sebaliknya, ditolak oleh agen), skenario pengujian menjadi sadar akan hal ini. Berkat amplop, skrip uji tahu apa yang terjadi dan dapat menentukan saat-saat ketika skrip langkah "bekerja".
Tetapi bagaimana membuat SObjectizer membungkus setiap pesan dalam amplop khusus?
Itu pertanyaan yang menarik. Dia memutuskan sebagai berikut: hal seperti
event_queue_hook ditemukan. Ini adalah objek khusus dengan dua metode - on_bind dan on_unbind.
Ketika agen terikat ke dispatcher tertentu, dispatcher mengeluarkan event_queue agen ke agen. Melalui event_queue ini, permintaan untuk agen masuk ke antrian yang diperlukan dan tersedia untuk dispatcher untuk diproses. Ketika agen berjalan di dalam SObjectizer, ia memiliki pointer ke event_queue. Ketika agen dihapus dari SObjectizer, penunjuknya ke event_queue dibatalkan.
Jadi, dimulai dengan versi 5.5.24, agen, setelah menerima event_queue, harus memanggil metode on_bind dari event_queue_hook. Di mana agen harus meneruskan pointer yang diterima ke event_queue. Dan event_queue_hook dapat mengembalikan pointer yang sama atau pointer lain sebagai respons. Dan agen harus menggunakan nilai yang dikembalikan.
Ketika agen dihapus dari SObjectizer, itu harus memanggil on_unbind pada event_queue_hook. Di on_unbind, agen melewati nilai yang dikembalikan oleh metode on_bind.
Seluruh dapur ini dijalankan di dalam SObjectizer dan pengguna tidak melihat hal ini. Dan, pada prinsipnya, Anda mungkin tidak tahu tentang ini sama sekali. Tapi lingkungan pengujian SObjectizer, testing_env_t yang sama, mengeksploitasi event_queue_hook persis. Di dalam testing_env_t, implementasi khusus event_queue_hook dibuat.
Implementasi ini di on_bind membungkus setiap event_queue dalam objek proxy khusus. Dan sudah objek proxy ini menempatkan pesan yang dikirim ke agen dalam amplop khusus.Tapi itu belum semuanya. Anda mungkin ingat bahwa dalam lingkungan pengujian, agen harus dibekukan. Ini juga diimplementasikan melalui objek proxy yang disebutkan. Saat skrip uji tidak berjalan, objek proxy menyimpan pesan yang dikirim ke agen di rumah. Tetapi ketika skrip dijalankan, objek proxy mentransfer semua pesan yang terakumulasi sebelumnya ke antrian pesan agen saat ini.Kesimpulan
Kesimpulannya, saya ingin mengatakan dua hal.Pertama-tama, kami menerapkan pandangan kami tentang bagaimana agen dapat diuji dalam SObjectizer. Pendapat saya karena tidak ada banyak panutan yang baik di sekitar. Kami melihat ke arah Akka . Pengujian . Tetapi Akka dan SObjectizer terlalu berbeda untuk port pendekatan yang bekerja di Akka ke SObjectizer. Dan C ++ bukan Scala / Java, di mana beberapa hal yang berkaitan dengan introspeksi dapat dilakukan melalui refleksi. Jadi saya harus datang dengan pendekatan yang akan jatuh pada SObjectizer.Dalam versi 5.5.24, implementasi eksperimental yang pertama menjadi tersedia. Tentunya Anda bisa melakukan yang lebih baik. Tetapi bagaimana memahami apa yang akan berguna dan apa fantasi yang tidak berguna? Sayangnya, tidak ada apa-apa. Anda perlu mengambil dan mencoba, lihat apa yang terjadi dalam latihan.Jadi kami membuat versi minimal yang dapat Anda ambil dan coba. Apa yang kami usulkan untuk dilakukan bagi semua orang: coba, bereksperimen, dan bagikan kesan Anda dengan kami. Apa yang kamu suka, apa yang tidak kamu sukai? Mungkin ada yang hilang?Kedua, kata-kata yang diucapkan di awal 2017 menjadi lebih relevan :… , , , . - — . . . : , .
, , , — , .
Oleh karena itu, saran saya untuk mereka yang mencari kerangka aktor yang sudah jadi: perhatikan tidak hanya keaslian ide dan keindahan contoh. Lihat juga segala macam hal tambahan yang akan membantu Anda mengetahui apa yang terjadi dalam aplikasi Anda: misalnya, cari tahu berapa banyak aktor yang ada di dalam sekarang, apa ukuran antrian mereka, jika pesan tidak mencapai penerima, lalu ke mana ia pergi ... Jika kerangka kerjanya tidak menyediakan sesuatu seperti itu, akan lebih mudah bagi Anda. Jika tidak, maka Anda akan memiliki lebih banyak pekerjaan.
Semua hal di atas bahkan lebih penting dalam hal menguji aktor. Karena itu, ketika memilih kerangka aktor untuk diri sendiri, perhatikan apa yang ada di dalamnya dan apa yang tidak. Sebagai contoh, kita sudah memiliki alat untuk menyederhanakan pengujian :)