Tester ses agents avec MockTransport
Les tests habituels des agents gAgent démarrent de vrais processus, ouvrent des sockets ZeroMQ et attendent des messages réseau. Cela prend plusieurs secondes et peut échouer pour des raisons extérieures au code (port occupé, timing réseau, etc.).
MockTransport résout ce problème : c’est un transport en mémoire
qui permet de tester les protocoles FIPA sans aucune infrastructure
réseau, sans fork(), et en quelques millisecondes.
Principe
Test normal (ZMQ) Test avec MockTransport
───────────────────────── ─────────────────────────
fork() × 2 processus Un seul processus
Sockets ZeroMQ Files en mémoire (MockBus)
Attente de timing réseau Déterministe, instantané
~1-16 secondes par test < 1 milliseconde par test
La mécanique est simple : MockBus est une boîte aux lettres
partagée entre tous les agents du test. Quand Alice envoie à Bob,
le message atterrit immédiatement dans la file de Bob. Bob peut
le lire au prochain appel à receive().
Les composants
#include <gagent/messaging/MockTransport.hpp>
using namespace gagent::messaging;
// Le bus partagé — une seule instance par test
auto bus = std::make_shared<MockBus>();
// Un transport par agent, tous branchés sur le même bus
auto transport_alice = std::make_shared<MockTransport>(bus);
auto transport_bob = std::make_shared<MockTransport>(bus);
MockBus expose aussi une méthode utilitaire pour les assertions :
// Combien de messages Bob a-t-il en attente ?
int n = bus->pending("bob");
L’agent de test : StubAgent
Les agents normaux démarrent avec init(), ce qui lance un fork().
Pour les tests, on crée un agent minimal sans fork :
// À déclarer dans votre fichier de test
class StubAgent : public Agent {
public:
explicit StubAgent(std::shared_ptr<ITransport> t) {
setTransport(std::move(t));
}
void setup() override {} // pas de behaviours automatiques
};
Piloter les state machines manuellement
Sans threads ni event loop, vous appelez onStart() et action()
vous-même dans l’ordre voulu. Chaque appel à action() fait avancer
la machine à états d’un pas.
// Créer les agents de test
StubAgent alice(std::make_shared<MockTransport>(bus));
StubAgent bob (std::make_shared<MockTransport>(bus));
// Instancier les behaviours directement
MonRequester req(&alice, ...);
MonServeur srv(&bob, ...);
// Initialiser (équivalent de onStart() appelé par le framework)
req.onStart();
srv.onStart();
// Piloter pas à pas
req.action(); // Alice envoie REQUEST
srv.action(); // Bob reçoit REQUEST, envoie INFORM
req.action(); // Alice reçoit INFORM → handleInform()
// Vérifier le résultat
assert(req.done());
Exemple 1 — Protocole Request
Test complet du flux REQUEST → INFORM :
#include <gagent/messaging/MockTransport.hpp>
#include <gagent/protocols/Request.hpp>
#include <cassert>
using namespace gagent::messaging;
using namespace gagent::protocols;
class StubAgent : public Agent {
public:
explicit StubAgent(std::shared_ptr<ITransport> t) { setTransport(std::move(t)); }
void setup() override {}
};
int main() {
auto bus = std::make_shared<MockBus>();
StubAgent alice(std::make_shared<MockTransport>(bus));
StubAgent bob (std::make_shared<MockTransport>(bus));
bool inform_recu = false;
// Définir les comportements
struct Client : RequestInitiator {
bool& flag_;
Client(Agent* ag, bool& f)
: RequestInitiator(ag, "alice", "bob", "6*7", "math", 500)
, flag_(f) {}
void handleInform(const ACLMessage& msg) override {
std::cout << "Résultat : " << msg.getContent() << "\n";
flag_ = true;
}
};
struct Serveur : RequestParticipant {
Serveur(Agent* ag) : RequestParticipant(ag, "bob") {}
ACLMessage handleRequest(const ACLMessage& req) override {
auto r = req.createReply(ACLMessage::Performative::INFORM);
r.setSender(AgentIdentifier{"bob"});
r.setContent("42");
return r;
}
};
Client client(&alice, inform_recu);
Serveur serveur(&bob);
client.onStart(); // bind("alice")
serveur.onStart(); // bind("bob")
client.action(); // envoie REQUEST à bob
serveur.action(); // reçoit REQUEST, envoie INFORM à alice
client.action(); // reçoit INFORM → handleInform()
assert(inform_recu); // alice a reçu la réponse
assert(client.done()); // protocole terminé
std::cout << "Test OK\n";
}
Résultat :
Résultat : 42
Test OK
Exemple 2 — AGREE + INFORM différé
Tester le flux REQUEST → AGREE → INFORM :
struct ServeurLent : RequestParticipant {
ServeurLent(Agent* ag) : RequestParticipant(ag, "bob") {}
bool prepareAgree(const ACLMessage&) override { return true; }
ACLMessage handleRequest(const ACLMessage& req) override {
// Pas de sleep en test — le MockTransport est synchrone
auto r = req.createReply(ACLMessage::Performative::INFORM);
r.setContent("traitement terminé");
return r;
}
};
bool agree_recu = false;
bool inform_recu = false;
struct ClientAgreeable : RequestInitiator {
bool& agree_;
bool& inform_;
ClientAgreeable(Agent* ag, bool& a, bool& i)
: RequestInitiator(ag, "alice", "bob", "tâche", "", 500)
, agree_(a), inform_(i) {}
void handleAgree (const ACLMessage&) override { agree_ = true; }
void handleInform(const ACLMessage&) override { inform_ = true; }
};
ClientAgreeable client(&alice, agree_recu, inform_recu);
ServeurLent serveur(&bob);
client.onStart();
serveur.onStart();
client.action(); // envoie REQUEST
serveur.action(); // reçoit REQUEST → envoie AGREE puis INFORM
client.action(); // reçoit AGREE → handleAgree()
client.action(); // reçoit INFORM → handleInform()
assert(agree_recu && inform_recu);
Exemple 3 — Contract Net
Tester un appel d’offres complet avec deux participants :
#include <gagent/protocols/ContractNet.hpp>
using namespace gagent::protocols;
// Manager : prend l'offre la moins chère
struct Manager : ContractNetInitiator {
std::string& gagnant_;
Manager(Agent* ag, std::vector<AgentIdentifier> parts, std::string& g)
: ContractNetInitiator(ag, "manager", make_cfp(), parts, 500, 500)
, gagnant_(g) {}
std::vector<std::string> selectProposals(
const std::vector<ACLMessage>& propositions) override {
auto best = std::min_element(propositions.begin(), propositions.end(),
[](const ACLMessage& a, const ACLMessage& b) {
return std::stoi(a.getContent()) < std::stoi(b.getContent());
});
gagnant_ = best->getSender().name;
return { gagnant_ };
}
static ACLMessage make_cfp() {
ACLMessage cfp(ACLMessage::Performative::CFP);
cfp.setContent("tâche");
return cfp;
}
};
// Worker : propose un coût fixe
struct Worker : ContractNetParticipant {
int cout_;
Worker(Agent* ag, const std::string& nom, int c)
: ContractNetParticipant(ag, nom, 500), cout_(c) {}
ACLMessage prepareProposal(const ACLMessage&) override {
ACLMessage p(ACLMessage::Performative::PROPOSE);
p.setContent(std::to_string(cout_));
return p;
}
ACLMessage executeTask(const ACLMessage&) override {
ACLMessage r(ACLMessage::Performative::INFORM);
r.setContent("fait");
return r;
}
};
// ── Test ──────────────────────────────────────────────────────────────────
auto bus = std::make_shared<MockBus>();
StubAgent ag_manager(std::make_shared<MockTransport>(bus));
StubAgent ag_bob (std::make_shared<MockTransport>(bus));
StubAgent ag_carol (std::make_shared<MockTransport>(bus));
std::string gagnant;
std::vector<AgentIdentifier> parts = {
AgentIdentifier{"bob"}, AgentIdentifier{"carol"}
};
Manager manager(&ag_manager, parts, gagnant);
Worker bob (&ag_bob, "bob", 10); // coût 10
Worker carol (&ag_carol, "carol", 5); // coût 5 ← moins chère
manager.onStart(); bob.onStart(); carol.onStart();
manager.action(); // envoie CFP à bob et carol
bob.action(); // reçoit CFP → envoie PROPOSE(10)
carol.action(); // reçoit CFP → envoie PROPOSE(5)
manager.action(); // reçoit PROPOSE de bob (1/2)
manager.action(); // reçoit PROPOSE de carol (2/2)
manager.action(); // HANDLE_PROPOSALS → ACCEPT carol, REJECT bob
bob.action(); // reçoit REJECT → done
carol.action(); // reçoit ACCEPT → execute → envoie INFORM
manager.action(); // reçoit INFORM → handleInform → state=DONE
manager.action(); // DONE → done_ = true
assert(gagnant == "carol"); // carol sélectionnée (moins chère)
assert(manager.done());
assert(bob.done());
assert(carol.done());
Bonnes pratiques
Nommer les agents différemment par test
Pour éviter les collisions entre tests qui tournent en parallèle, préfixez les noms avec un identifiant de test :
// Dans test_calcul
RequestInitiator(ag, "test1-alice", "test1-bob", ...)
// Dans test_refus
RequestInitiator(ag, "test2-alice", "test2-bob", ...)
Vérifier les messages en attente
Si un test échoue, bus->pending() permet de savoir si des messages
n’ont pas été consommés :
if (bus->pending("bob") > 0) {
std::cerr << "Bob a des messages non lus !\n";
}
Pas de sleep() dans les tests MockTransport
Les sleep() ou sleep_for() n’ont aucun sens avec MockTransport
car les messages arrivent instantanément. Supprimez-les dans vos
handleRequest() lors des tests.
Résumé
Élément |
Rôle |
|---|---|
|
Bus en mémoire partagé entre les agents du test |
|
Transport sans réseau — |
|
Agent minimal sans fork ni event loop |
|
Piloter les state machines manuellement, pas à pas |
|
Vérifier les messages non consommés dans les assertions |