mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 13:01:37 +00:00
Preserve ACK ownership across contact relocation and retirement
This commit is contained in:
1 parent
d29cba046b
commit
ea938e8e84
15 files changed
+907
-50
No files matched your search
@@ -131,6 +131,7 @@ jobs:
|
||||
python3 -B test/test_companion_primary_radio_persistence.py -v
|
||||
python3 -B test/test_companion_response_bounds.py -v
|
||||
python3 -B test/test_companion_ack_delivery.py -v
|
||||
python3 -B test/test_companion_contact_lifetime.py -v
|
||||
python3 -B test/test_companion_identity_cli.py -v
|
||||
python3 -B test/test_companion_uncached_storage.py -v
|
||||
python3 -B test/test_companion_ota_config.py -v
|
||||
|
||||
@@ -1060,7 +1060,15 @@ MyMesh::AckTableEntry* MyMesh::findPendingTextMessage(
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ContactInfo* MyMesh::processAck(const uint8_t *data) {
|
||||
void MyMesh::onContactReferenceChanged(const ContactInfo* previous, ContactInfo* replacement) {
|
||||
for (AckTableEntry* entry = expected_ack_table;
|
||||
entry != expected_ack_table + EXPECTED_ACK_TABLE_SIZE; ++entry) {
|
||||
if (entry->ack != 0 && entry->contact == previous) entry->contact = replacement;
|
||||
}
|
||||
}
|
||||
|
||||
bool MyMesh::processAck(const uint8_t *data, ContactInfo*& peer) {
|
||||
peer = NULL;
|
||||
expireExpectedAcks();
|
||||
uint32_t incoming;
|
||||
memcpy(&incoming, data, sizeof(incoming));
|
||||
@@ -1090,10 +1098,11 @@ ContactInfo* MyMesh::processAck(const uint8_t *data) {
|
||||
if (contact != NULL && contact->type == ADV_TYPE_CHAT) rememberOneKeyAck(*contact);
|
||||
#endif
|
||||
expireExpectedAcks();
|
||||
return contact;
|
||||
peer = contact;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return checkConnectionsAck(data);
|
||||
return checkConnectionsAck(data, peer);
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_ONE_KEY_DM
|
||||
@@ -1260,7 +1269,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret,
|
||||
// A prior anonymous service request may occupy a transient slot for this
|
||||
// key. Remove it so regular text lookup finds the durable contact first.
|
||||
ContactInfo* transient = lookupTransientContactByPubKey(sender.pub_key, PUB_KEY_SIZE);
|
||||
if (transient != NULL) clearTransientContact(*transient);
|
||||
if (transient != NULL) clearTransientContact(*transient, added);
|
||||
onDiscoveredContact(*added, true, OUT_PATH_UNKNOWN, NULL);
|
||||
}
|
||||
|
||||
@@ -5583,7 +5592,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (scheduleContactWrite(*recipient)) {
|
||||
ContactInfo* transient = lookupTransientContactByPubKey(
|
||||
pub_key, PUB_KEY_SIZE);
|
||||
if (transient != NULL) clearTransientContact(*transient);
|
||||
if (transient != NULL) clearTransientContact(*transient, recipient);
|
||||
writeOKFrame();
|
||||
} else {
|
||||
recipient->out_path_len = previous_out_path_len;
|
||||
@@ -5613,8 +5622,6 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
|
||||
ContactInfo* transient = lookupTransientContactByPubKey(
|
||||
pub_key, PUB_KEY_SIZE);
|
||||
ContactInfo previous_transient = {};
|
||||
if (transient != NULL) previous_transient = *transient;
|
||||
|
||||
if (recipient != NULL) {
|
||||
const ContactInfo previous = *recipient;
|
||||
@@ -5622,7 +5629,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
if (scheduleContactWrite(*recipient)) {
|
||||
// A prior anonymous request can leave a same-key entry in the
|
||||
// reserved prefix. Retire it in place; never compact that prefix.
|
||||
if (transient != NULL) clearTransientContact(*transient);
|
||||
if (transient != NULL) clearTransientContact(*transient, recipient);
|
||||
updateGpsTelemetryPolicy();
|
||||
writeOKFrame();
|
||||
} else {
|
||||
@@ -5630,22 +5637,20 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
|
||||
}
|
||||
} else {
|
||||
// Promotion must free the exact transient prefix slot before lookup
|
||||
// of the newly-added persistent record. Restore it on every failure.
|
||||
if (transient != NULL) clearTransientContact(*transient);
|
||||
// Preserve the transient and its pending ACK references until the
|
||||
// persistent destination is accepted. Failure preserves the transient.
|
||||
if (addContact(candidate)) {
|
||||
ContactInfo* added = lookupPersistentContactByPubKey(
|
||||
candidate.id.pub_key, PUB_KEY_SIZE);
|
||||
if (added != NULL && scheduleContactWrite(*added)) {
|
||||
if (transient != NULL) clearTransientContact(*transient, added);
|
||||
updateGpsTelemetryPolicy();
|
||||
writeOKFrame();
|
||||
} else {
|
||||
if (added != NULL) removeContact(*added);
|
||||
if (transient != NULL) *transient = previous_transient;
|
||||
writeErrFrame(ERR_CODE_FILE_IO_ERROR);
|
||||
}
|
||||
} else {
|
||||
if (transient != NULL) *transient = previous_transient;
|
||||
writeErrFrame(ERR_CODE_TABLE_FULL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -370,13 +370,14 @@ protected:
|
||||
bool canMutateContacts() const override;
|
||||
void onContactsFull() override;
|
||||
bool onContactOverwrite(const ContactInfo& contact) override;
|
||||
void onContactReferenceChanged(const ContactInfo* previous, ContactInfo* replacement) override;
|
||||
bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override;
|
||||
void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) override;
|
||||
void onContactPathUpdated(const ContactInfo &contact) override;
|
||||
#if COMPANION_FEATURE_TEXT_TERMINAL
|
||||
void onContactVisit(const ContactInfo& contact) override;
|
||||
#endif
|
||||
ContactInfo* processAck(const uint8_t *data) override;
|
||||
bool processAck(const uint8_t *data, ContactInfo*& peer) override;
|
||||
#if MESH_ENABLE_ONE_KEY_DM
|
||||
void onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret,
|
||||
const mesh::Identity& sender, uint8_t* data,
|
||||
|
||||
@@ -240,18 +240,19 @@ protected:
|
||||
saveContacts();
|
||||
}
|
||||
|
||||
ContactInfo* processAck(const uint8_t *data) override {
|
||||
if (memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
|
||||
bool processAck(const uint8_t *data, ContactInfo*& peer) override {
|
||||
peer = NULL;
|
||||
if (expected_ack_crc != 0 && memcmp(data, &expected_ack_crc, 4) == 0) { // got an ACK from recipient
|
||||
Serial.printf(" Got ACK! (round trip: %d millis)\n", _ms->getMillis() - last_msg_sent);
|
||||
// NOTE: the same ACK can be received multiple times!
|
||||
expected_ack_crc = 0; // reset our expected hash, now that we have received ACK
|
||||
return NULL; // TODO: really should return ContactInfo pointer
|
||||
return true; // owned, even without a retained contact pointer
|
||||
}
|
||||
|
||||
//uint32_t crc;
|
||||
//memcpy(&crc, data, 4);
|
||||
//MESH_DEBUG_PRINTLN("unknown ACK received: %08X (expected: %08X)", crc, expected_ack_crc);
|
||||
return NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
void onMessageRecv(const ContactInfo& from, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) override {
|
||||
|
||||
@@ -24,6 +24,12 @@ bool BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Pack
|
||||
return sendFlood(pkt, delay_millis);
|
||||
}
|
||||
|
||||
// Share the value reset without replacing ContactInfo's assignment/RAII rules.
|
||||
__attribute__((noinline))
|
||||
void BaseChatMesh::resetContactValue(ContactInfo& contact) {
|
||||
contact = ContactInfo();
|
||||
}
|
||||
|
||||
bool BaseChatMesh::initializeContactStorage() {
|
||||
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
|
||||
const int requested_capacity = MAX_CONTACTS + MAX_ANON_CONTACTS;
|
||||
@@ -48,10 +54,14 @@ bool BaseChatMesh::initializeContactStorage() {
|
||||
for (int i = 0; i < requested_capacity; ++i) new (&expanded_contacts[i]) ContactInfo();
|
||||
for (int i = 0; i < num_contacts; ++i) {
|
||||
expanded_contacts[i] = contacts[i];
|
||||
contacts[i] = ContactInfo();
|
||||
onContactReferenceChanged(&contacts[i], &expanded_contacts[i]);
|
||||
resetContactValue(contacts[i]);
|
||||
}
|
||||
#else
|
||||
memcpy(expanded_contacts, contacts, sizeof(ContactInfo) * num_contacts);
|
||||
for (int i = 0; i < num_contacts; ++i) {
|
||||
onContactReferenceChanged(&contacts[i], &expanded_contacts[i]);
|
||||
}
|
||||
#endif
|
||||
contacts = expanded_contacts;
|
||||
sort_array = expanded_sort_array;
|
||||
@@ -145,6 +155,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
|
||||
}
|
||||
if (oldest_idx >= 0) {
|
||||
// NOTE: do NOT call onContactOverwrite()
|
||||
onContactReferenceChanged(&contacts[oldest_idx], NULL);
|
||||
return &contacts[oldest_idx];
|
||||
}
|
||||
} else {
|
||||
@@ -169,6 +180,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
|
||||
if (!onContactOverwrite(contacts[oldest_idx])) {
|
||||
return NULL;
|
||||
}
|
||||
onContactReferenceChanged(&contacts[oldest_idx], NULL);
|
||||
contact_table_revision++;
|
||||
return &contacts[oldest_idx];
|
||||
}
|
||||
@@ -178,7 +190,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot(bool transient_only) {
|
||||
}
|
||||
|
||||
void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) {
|
||||
ci = ContactInfo();
|
||||
resetContactValue(ci);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
ci.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
|
||||
#endif
|
||||
@@ -228,11 +240,9 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
|
||||
packet->header = save;
|
||||
}
|
||||
|
||||
ContactInfo* transient = NULL;
|
||||
if (from && from->type == ADV_TYPE_NONE) { // already in contacts, but from a temporary ANON_REQ ?
|
||||
*from = ContactInfo(); // release the anon/temp path as well
|
||||
#if defined(NRF52_PLATFORM)
|
||||
from->storage_slot = mesh::storage::CONTACT_SLOT_NONE;
|
||||
#endif
|
||||
transient = from; // preserve its references if filters/allocation refuse promotion
|
||||
from = NULL; // do normal 'add' flow
|
||||
}
|
||||
|
||||
@@ -283,6 +293,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id,
|
||||
}
|
||||
from->last_advert_timestamp = timestamp;
|
||||
from->lastmod = getRTCClock()->getCurrentTime();
|
||||
if (transient != NULL) clearTransientContact(*transient, from);
|
||||
|
||||
onDiscoveredContact(*from, is_new, packet->path_len, packet->path); // let UI know
|
||||
}
|
||||
@@ -470,7 +481,8 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
|
||||
|
||||
if (extra_type == PAYLOAD_TYPE_ACK && extra_len >= 4) {
|
||||
// also got an encoded ACK!
|
||||
if (processAck(extra) != NULL) {
|
||||
ContactInfo* peer;
|
||||
if (processAck(extra, peer)) {
|
||||
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
|
||||
}
|
||||
} else if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 0) {
|
||||
@@ -481,11 +493,11 @@ bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_
|
||||
|
||||
void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
||||
ContactInfo* from;
|
||||
if ((from = processAck((uint8_t *)&ack_crc)) != NULL) {
|
||||
if (processAck((uint8_t *)&ack_crc, from)) {
|
||||
txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer
|
||||
packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit
|
||||
|
||||
if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) {
|
||||
if (from != NULL && packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) {
|
||||
// we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?)
|
||||
handleReturnPathRetry(*from, packet->path, packet->path_len);
|
||||
}
|
||||
@@ -932,9 +944,11 @@ void BaseChatMesh::markConnectionActive(const ContactInfo& contact) {
|
||||
}
|
||||
}
|
||||
|
||||
ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
|
||||
bool BaseChatMesh::checkConnectionsAck(const uint8_t* data, ContactInfo*& peer) {
|
||||
peer = NULL;
|
||||
for (int i = 0; i < MAX_CONNECTIONS; i++) {
|
||||
if (connections[i].keep_alive_millis > 0 && memcmp(&connections[i].expected_ack, data, 4) == 0) {
|
||||
if (connections[i].keep_alive_millis > 0 && connections[i].expected_ack != 0
|
||||
&& memcmp(&connections[i].expected_ack, data, 4) == 0) {
|
||||
// yes, got an ack for our keep_alive request!
|
||||
connections[i].expected_ack = 0;
|
||||
connections[i].last_activity = getRTCClock()->getCurrentTime();
|
||||
@@ -943,10 +957,11 @@ ContactInfo* BaseChatMesh::checkConnectionsAck(const uint8_t* data) {
|
||||
connections[i].next_ping = futureMillis(connections[i].keep_alive_millis);
|
||||
|
||||
auto id = &connections[i].server_id;
|
||||
return lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE); // yes, a match
|
||||
peer = lookupContactByPubKey(id->pub_key, PUB_KEY_SIZE);
|
||||
return true; // owned even if its contact was removed
|
||||
}
|
||||
}
|
||||
return NULL; /// no match
|
||||
return false; /// no match
|
||||
}
|
||||
|
||||
void BaseChatMesh::checkConnections() {
|
||||
@@ -1069,9 +1084,10 @@ bool BaseChatMesh::isTransientContact(const ContactInfo& contact) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BaseChatMesh::clearTransientContact(ContactInfo& contact) {
|
||||
bool BaseChatMesh::clearTransientContact(ContactInfo& contact, ContactInfo* replacement) {
|
||||
if (!isTransientContact(contact)) return false;
|
||||
contact = ContactInfo();
|
||||
onContactReferenceChanged(&contact, replacement);
|
||||
resetContactValue(contact);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
contact.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
|
||||
#endif
|
||||
@@ -1098,12 +1114,14 @@ bool BaseChatMesh::removeContact(ContactInfo& contact) {
|
||||
if (idx >= num_contacts) return false; // not found
|
||||
|
||||
// remove from contacts array
|
||||
onContactReferenceChanged(&contacts[idx], NULL);
|
||||
num_contacts--;
|
||||
while (idx < num_contacts) {
|
||||
contacts[idx] = contacts[idx + 1];
|
||||
onContactReferenceChanged(&contacts[idx + 1], &contacts[idx]);
|
||||
idx++;
|
||||
}
|
||||
contacts[num_contacts] = ContactInfo(); // release the retired tail path
|
||||
resetContactValue(contacts[num_contacts]); // release the retired tail path
|
||||
contact_table_revision++;
|
||||
return true; // Success
|
||||
}
|
||||
|
||||
@@ -108,8 +108,15 @@ protected:
|
||||
sort_array = sort_array_fallback;
|
||||
contact_capacity = CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS;
|
||||
#endif
|
||||
contact_table_revision = 0;
|
||||
resetContacts();
|
||||
// No live references exist during base construction. Seed the initially
|
||||
// empty prefix directly; runtime resets also notify reference owners.
|
||||
#if defined(NRF52_PLATFORM)
|
||||
for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
|
||||
contacts[i].storage_slot = mesh::storage::CONTACT_SLOT_NONE;
|
||||
}
|
||||
#endif
|
||||
num_contacts = MAX_ANON_CONTACTS;
|
||||
contact_table_revision = 1;
|
||||
|
||||
#ifdef MAX_GROUP_CHANNELS
|
||||
memset(channels, 0, sizeof(channels));
|
||||
@@ -122,9 +129,13 @@ protected:
|
||||
|
||||
void bootstrapRTCfromContacts();
|
||||
bool initializeContactStorage();
|
||||
static void resetContactValue(ContactInfo& contact);
|
||||
|
||||
void resetContacts() {
|
||||
for (int i = 0; i < num_contacts; ++i) contacts[i] = ContactInfo();
|
||||
for (int i = 0; i < num_contacts; ++i) {
|
||||
onContactReferenceChanged(&contacts[i], NULL);
|
||||
resetContactValue(contacts[i]);
|
||||
}
|
||||
#if defined(NRF52_PLATFORM)
|
||||
for (int i = 0; i < MAX_ANON_CONTACTS; i++) {
|
||||
contacts[i].storage_slot = mesh::storage::CONTACT_SLOT_NONE;
|
||||
@@ -147,8 +158,14 @@ protected:
|
||||
// safely retire its associated persistent state. A refusal must not mutate
|
||||
// the contact or its backing state.
|
||||
virtual bool onContactOverwrite(const ContactInfo&) { return true; };
|
||||
// A replacement is already initialized with the same identity. Notify after
|
||||
// copying it, before the old slot is cleared/reused; NULL retires an identity.
|
||||
// Subclasses that retain no contact pointers need no relocation work.
|
||||
virtual void onContactReferenceChanged(const ContactInfo*, ContactInfo*) {}
|
||||
virtual void onDiscoveredContact(ContactInfo& contact, bool is_new, uint8_t path_len, const uint8_t* path) = 0;
|
||||
virtual ContactInfo* processAck(const uint8_t *data) = 0;
|
||||
// Ownership does not require a surviving contact. Set peer to NULL when the
|
||||
// expected ACK belongs to us but its recipient was removed or overwritten.
|
||||
virtual bool processAck(const uint8_t *data, ContactInfo*& peer) = 0;
|
||||
virtual void onContactPathUpdated(const ContactInfo& contact) = 0;
|
||||
virtual bool onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len);
|
||||
virtual void onMessageRecv(const ContactInfo& contact, mesh::Packet* pkt, uint32_t sender_timestamp, const char *text) = 0;
|
||||
@@ -190,7 +207,7 @@ protected:
|
||||
void stopConnection(const uint8_t* pub_key);
|
||||
bool hasConnectionTo(const uint8_t* pub_key);
|
||||
void markConnectionActive(const ContactInfo& contact);
|
||||
ContactInfo* checkConnectionsAck(const uint8_t* data);
|
||||
bool checkConnectionsAck(const uint8_t* data, ContactInfo*& peer);
|
||||
void checkConnections();
|
||||
|
||||
public:
|
||||
@@ -221,7 +238,8 @@ public:
|
||||
ContactInfo* lookupTransientContactByPubKey(const uint8_t* pub_key, int prefix_len);
|
||||
ContactInfo* lookupPersistentContactByPubKey(const uint8_t* pub_key, int prefix_len);
|
||||
bool isTransientContact(const ContactInfo& contact) const;
|
||||
bool clearTransientContact(ContactInfo& contact);
|
||||
// Supply a live same-identity replacement when promoting a transient peer.
|
||||
bool clearTransientContact(ContactInfo& contact, ContactInfo* replacement = NULL);
|
||||
bool removeContact(ContactInfo& contact);
|
||||
bool addContact(const ContactInfo& contact);
|
||||
int getTotalContactSlots() const { return num_contacts; }
|
||||
|
||||
+11
-3
@@ -72,11 +72,12 @@ public:
|
||||
return retry_active[key[0]];
|
||||
}
|
||||
void rememberOneKeyAck(ContactInfo&) { ++one_key_notices; }
|
||||
ContactInfo* checkConnectionsAck(const uint8_t* input) {
|
||||
bool checkConnectionsAck(const uint8_t* input, ContactInfo*& peer) {
|
||||
++connection_ack_calls;
|
||||
last_connection_ack_input = input;
|
||||
memcpy(&last_connection_ack, input, sizeof(last_connection_ack));
|
||||
return last_connection_ack == connection_ack ? connection_ack_contact : nullptr;
|
||||
peer = last_connection_ack == connection_ack ? connection_ack_contact : nullptr;
|
||||
return peer != nullptr;
|
||||
}
|
||||
bool hasTerminalOutput() const { return true; }
|
||||
Terminal& terminalOutput() { return terminal; }
|
||||
@@ -89,7 +90,14 @@ public:
|
||||
void clearExpectedAck(AckTableEntry&, bool = true);
|
||||
void expireExpectedAcks();
|
||||
AckTableEntry* findPendingTextMessage(const uint8_t*, uint32_t);
|
||||
ContactInfo* processAck(const uint8_t*);
|
||||
bool processAck(const uint8_t*, ContactInfo*&);
|
||||
// Existing delivery scenarios inspect the optional peer; the lifetime suite
|
||||
// separately exercises ownership with a NULL peer through BaseChatMesh.
|
||||
ContactInfo* processAck(const uint8_t* input) {
|
||||
ContactInfo* peer = nullptr;
|
||||
processAck(input, peer);
|
||||
return peer;
|
||||
}
|
||||
void cancelSerialOperationsForRoute(BaseSerialInterface*);
|
||||
bool hasFiniteDelayedReplyForRoute(BaseSerialInterface*) const;
|
||||
void service();
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "../../../../src/Identity.h"
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// Keep the real identities, packets and contact/path-cache types while the
|
||||
// source-linked fixture supplies only radio/clock/storage hardware boundaries.
|
||||
#include <Identity.h>
|
||||
#include <Packet.h>
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "../../../../src/Utils.h"
|
||||
+650
@@ -0,0 +1,650 @@
|
||||
#include <Arduino.h>
|
||||
#include <helpers/ArduinoSerialInterface.h>
|
||||
#include <helpers/MultiSerialInterface.h>
|
||||
#include <helpers/ContactInfo.h>
|
||||
#include <helpers/ContactListOrder.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/PersistentStoreFormat.h>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <new>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#define MAX_ANON_CONTACTS 8
|
||||
#define MAX_CONTACTS 20
|
||||
#define MAX_CONNECTIONS 16
|
||||
#define CONTACT_PSRAM_FALLBACK_TOTAL_SLOTS (MAX_ANON_CONTACTS + 16)
|
||||
#define ADV_TYPE_NONE 0
|
||||
#define ADV_TYPE_CHAT 1
|
||||
|
||||
#include "production_constants.inc"
|
||||
#include "production_types.inc"
|
||||
#include "production_stream.inc"
|
||||
#include "production_constructors.inc"
|
||||
|
||||
#if MESH_CONTACT_CACHE
|
||||
namespace mesh {
|
||||
ContactPathStorage& contactPathStorage() {
|
||||
static ContactPathPool<128, 16> pool;
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static unsigned allocations = 0, fail_allocation = 0;
|
||||
static std::set<void*> outstanding_allocations;
|
||||
static constexpr int MALLOC_CAP_SPIRAM = 1, MALLOC_CAP_8BIT = 2;
|
||||
static void* heap_caps_calloc(size_t count, size_t size, int) {
|
||||
++allocations;
|
||||
if (allocations == fail_allocation) return nullptr;
|
||||
void* result = calloc(count, size);
|
||||
if (result != nullptr) outstanding_allocations.insert(result);
|
||||
return result;
|
||||
}
|
||||
static void heap_caps_free(void* pointer) {
|
||||
assert(outstanding_allocations.erase(pointer) == 1);
|
||||
free(pointer);
|
||||
}
|
||||
|
||||
struct Clock { unsigned long getMillis() const { return millis(); } };
|
||||
struct RTC { uint32_t getCurrentTime() const { return 100; } };
|
||||
struct Store {
|
||||
bool delete_ok = true, incomplete = false;
|
||||
unsigned deletions = 0;
|
||||
bool deleteBlobByKey(const uint8_t*, size_t) { ++deletions; return delete_ok; }
|
||||
bool hasIncompleteContactLoad() const { return incomplete; }
|
||||
};
|
||||
struct Terminal {
|
||||
std::string text;
|
||||
void print(const char* value) { text += value; }
|
||||
void printf(const char* format, ...) {
|
||||
char bytes[160];
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf(bytes, sizeof(bytes), format, args);
|
||||
va_end(args);
|
||||
text += bytes;
|
||||
}
|
||||
};
|
||||
|
||||
static ContactInfo makeContact(uint8_t key, uint8_t type = ADV_TYPE_CHAT) {
|
||||
ContactInfo result;
|
||||
memset(result.id.pub_key, key, sizeof(result.id.pub_key));
|
||||
result.type = type;
|
||||
result.lastmod = key + 100;
|
||||
result.out_path_len = OUT_PATH_UNKNOWN;
|
||||
snprintf(result.name, sizeof(result.name), "peer-%u", key);
|
||||
if (key <= 3) {
|
||||
const uint8_t path[] = {key};
|
||||
assert(result.setPath(path, 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class BaseChatMesh {
|
||||
public:
|
||||
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
|
||||
ContactInfo contacts_fallback[MAX_ANON_CONTACTS + 16];
|
||||
int sort_array_fallback[MAX_ANON_CONTACTS + 16] = {};
|
||||
ContactInfo* contacts = contacts_fallback;
|
||||
int* sort_array = sort_array_fallback;
|
||||
int contact_capacity = MAX_ANON_CONTACTS + 16;
|
||||
#else
|
||||
ContactInfo contacts[MAX_ANON_CONTACTS + MAX_CONTACTS];
|
||||
#endif
|
||||
int num_contacts = 0;
|
||||
uint32_t contact_table_revision = 0;
|
||||
uint32_t txt_send_timeout = 0;
|
||||
Clock clock;
|
||||
Clock* _ms = &clock;
|
||||
RTC rtc;
|
||||
ConnectionInfo connections[MAX_CONNECTIONS] = {};
|
||||
#ifdef MAX_GROUP_CHANNELS
|
||||
ChannelDetails channels[MAX_GROUP_CHANNELS];
|
||||
int num_channels;
|
||||
#endif
|
||||
mesh::Packet* _pendingLoopback = nullptr;
|
||||
uint8_t temp_buf[MAX_TRANS_UNIT] = {};
|
||||
bool auto_add = true, overwrite = true, can_mutate = true;
|
||||
uint8_t max_hops = 0;
|
||||
unsigned discoveries = 0, full_notices = 0, path_updates = 0;
|
||||
std::vector<uint8_t> path_notices;
|
||||
|
||||
BaseChatMesh() {
|
||||
#include "production_base_constructor.inc"
|
||||
}
|
||||
virtual ~BaseChatMesh() {
|
||||
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
|
||||
if (contacts != contacts_fallback) {
|
||||
for (int i = 0; i < contact_capacity; ++i) contacts[i].~ContactInfo();
|
||||
heap_caps_free(contacts);
|
||||
heap_caps_free(sort_array);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
virtual void onContactReferenceChanged(const ContactInfo*, ContactInfo*) {}
|
||||
virtual bool onContactOverwrite(const ContactInfo&) = 0;
|
||||
virtual bool processAck(const uint8_t*, ContactInfo*&) = 0;
|
||||
bool shouldOverwriteWhenFull() const { return overwrite; }
|
||||
bool shouldAutoAddContactType(uint8_t) const { return auto_add; }
|
||||
bool canMutateContacts() const { return can_mutate; }
|
||||
uint8_t getAutoAddMaxHops() const { return max_hops; }
|
||||
RTC* getRTCClock() { return &rtc; }
|
||||
bool putBlobByKey(const uint8_t*, int, const uint8_t*, int) { return true; }
|
||||
void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) { ++discoveries; }
|
||||
void onContactsFull() { ++full_notices; }
|
||||
void onContactPathUpdated(const ContactInfo&) { ++path_updates; }
|
||||
void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) {}
|
||||
void handleReturnPathRetry(const ContactInfo& peer, const uint8_t*, uint8_t) {
|
||||
path_notices.push_back(peer.id.pub_key[0]);
|
||||
}
|
||||
void resetContacts();
|
||||
static void resetContactValue(ContactInfo&);
|
||||
bool initializeContactStorage();
|
||||
ContactInfo* allocateContactSlot(bool = false);
|
||||
void populateContactFromAdvert(ContactInfo&, const mesh::Identity&, const AdvertDataParser&, uint32_t);
|
||||
void onAdvertRecv(mesh::Packet*, const mesh::Identity&, uint32_t, const uint8_t*, size_t);
|
||||
ContactInfo* lookupContactByPubKey(const uint8_t*, int);
|
||||
ContactInfo* lookupTransientContactByPubKey(const uint8_t*, int);
|
||||
ContactInfo* lookupPersistentContactByPubKey(const uint8_t*, int);
|
||||
bool isTransientContact(const ContactInfo&) const;
|
||||
bool clearTransientContact(ContactInfo&, ContactInfo* = nullptr);
|
||||
bool addContact(const ContactInfo&);
|
||||
bool removeContact(ContactInfo&);
|
||||
bool checkConnectionsAck(const uint8_t*, ContactInfo*&);
|
||||
bool onContactPathRecv(ContactInfo&, uint8_t*, uint8_t, uint8_t*, uint8_t, uint8_t, uint8_t*, uint8_t);
|
||||
void onAckRecv(mesh::Packet*, uint32_t);
|
||||
bool millisHasNowPassed(unsigned long) const;
|
||||
unsigned long futureMillis(int) const;
|
||||
};
|
||||
|
||||
class MyMesh : public BaseChatMesh {
|
||||
public:
|
||||
BaseSerialInterface* _serial = nullptr;
|
||||
Store store;
|
||||
Store* _store = &store;
|
||||
Terminal terminal;
|
||||
uint8_t out_frame[MAX_FRAME_SIZE] = {}, cmd_frame[MAX_FRAME_SIZE] = {};
|
||||
#include "production_entry.inc"
|
||||
AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE] = {};
|
||||
bool has_next_ack_expiry = false;
|
||||
unsigned long next_ack_expiry = 0;
|
||||
bool retry_active[256] = {};
|
||||
unsigned cancellations = 0, references_changed = 0;
|
||||
std::vector<uint8_t> learned;
|
||||
bool release_ok = true, restore_ok = true, schedule_ok = true;
|
||||
unsigned releases = 0, restores = 0, schedules = 0;
|
||||
unsigned ok_replies = 0, error_replies = 0;
|
||||
uint8_t last_error = 0;
|
||||
|
||||
bool cancelActiveRetries(const uint8_t* key) {
|
||||
++cancellations; retry_active[key[0]] = false; return true;
|
||||
}
|
||||
bool hasActiveRetries(const uint8_t* key) const { return retry_active[key[0]]; }
|
||||
void rememberOneKeyAck(ContactInfo& peer) { learned.push_back(peer.id.pub_key[0]); }
|
||||
bool hasTerminalOutput() const { return true; }
|
||||
Terminal& terminalOutput() { return terminal; }
|
||||
bool scheduleContactWriteAfterRelease(const ContactInfo&, uint16_t& slot) {
|
||||
++releases; slot = 7; return release_ok;
|
||||
}
|
||||
bool restoreContactWriteAfterRelease(const ContactInfo&, uint16_t) { ++restores; return restore_ok; }
|
||||
bool scheduleContactWrite(const ContactInfo&) { ++schedules; return schedule_ok; }
|
||||
void updateGpsTelemetryPolicy() {}
|
||||
void writeOKFrame() { ++ok_replies; }
|
||||
void writeErrFrame(uint8_t error) { ++error_replies; last_error = error; }
|
||||
void onContactReferenceChanged(const ContactInfo* old, ContactInfo* replacement) override {
|
||||
++references_changed;
|
||||
if (replacement != nullptr) assert(old->id.matches(replacement->id));
|
||||
applyContactReferenceChanged(old, replacement);
|
||||
}
|
||||
void applyContactReferenceChanged(const ContactInfo*, ContactInfo*);
|
||||
bool onContactOverwrite(const ContactInfo&) override;
|
||||
bool processAck(const uint8_t*, ContactInfo*&) override;
|
||||
void clearExpectedAck(AckTableEntry&, bool = true);
|
||||
void expireExpectedAcks();
|
||||
bool updateContactFromFrame(ContactInfo&, uint32_t&, const uint8_t*, int);
|
||||
void handleContactCommand(int);
|
||||
void acceptVerifiedOneKey(const mesh::Identity&);
|
||||
void service();
|
||||
};
|
||||
|
||||
#include "production_functions.inc"
|
||||
|
||||
struct Fixture {
|
||||
BufferStream usb_stream, other_stream;
|
||||
ArduinoSerialInterface usb, other;
|
||||
MultiSerialInterface manager;
|
||||
MyMesh node;
|
||||
|
||||
Fixture() {
|
||||
g_mock_millis = 100;
|
||||
allocations = fail_allocation = 0;
|
||||
assert(outstanding_allocations.empty());
|
||||
assert(node.references_changed == 0);
|
||||
assert(node.num_contacts == MAX_ANON_CONTACTS && node.contact_table_revision == 1);
|
||||
assert(node.txt_send_timeout == 0 && node._pendingLoopback == nullptr);
|
||||
for (const auto& connection : node.connections) {
|
||||
assert(connection.keep_alive_millis == 0 && connection.expected_ack == 0);
|
||||
assert(connection.next_ping == 0 && connection.last_activity == 0);
|
||||
}
|
||||
#ifdef MAX_GROUP_CHANNELS
|
||||
assert(node.num_channels == 0);
|
||||
for (const auto& channel : node.channels) {
|
||||
for (uint8_t byte : channel.channel.secret) assert(byte == 0);
|
||||
for (char byte : channel.name) assert(byte == 0);
|
||||
assert(channel.channel.tx_radio == mesh::RADIO_TX_AUTO);
|
||||
}
|
||||
#endif
|
||||
for (int i = 0; i < MAX_ANON_CONTACTS; ++i) {
|
||||
assert(node.contacts[i].type == ADV_TYPE_NONE && node.contacts[i].id.pub_key[0] == 0);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
assert(node.contacts[i].storage_slot == mesh::storage::CONTACT_SLOT_NONE);
|
||||
#endif
|
||||
}
|
||||
usb.begin(usb_stream); usb.enableFlowControl(true);
|
||||
other.begin(other_stream); other.enableFlowControl(true);
|
||||
assert(manager.addInterface(InterfaceType::USB, &usb));
|
||||
assert(manager.addInterface(InterfaceType::HardwareSerial, &other));
|
||||
manager.enable();
|
||||
node._serial = &manager;
|
||||
}
|
||||
ContactInfo* add(uint8_t key) {
|
||||
const ContactInfo contact = makeContact(key);
|
||||
assert(node.addContact(contact));
|
||||
return node.lookupPersistentContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
|
||||
}
|
||||
ContactInfo* transient(uint8_t key) {
|
||||
const ContactInfo contact = makeContact(key, ADV_TYPE_NONE);
|
||||
assert(node.addContact(contact));
|
||||
return node.lookupTransientContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
|
||||
}
|
||||
void fill() {
|
||||
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
|
||||
const int capacity = node.contact_capacity;
|
||||
#else
|
||||
const int capacity = MAX_ANON_CONTACTS + MAX_CONTACTS;
|
||||
#endif
|
||||
while (node.num_contacts < capacity) add(uint8_t(node.num_contacts + 10));
|
||||
}
|
||||
MyMesh::AckTableEntry& pending(ContactInfo* peer, unsigned slot = 0,
|
||||
uint32_t ack = 0x11223344, BaseSerialInterface* route = nullptr) {
|
||||
auto& entry = node.expected_ack_table[slot];
|
||||
entry.ack = ack; entry.msg_sent = 50; entry.expires_at = 10000;
|
||||
entry.message_timestamp = 1; entry.contact = peer;
|
||||
entry.reply_route = route == nullptr ? &usb : route;
|
||||
entry.retry_key[0] = uint8_t(slot + 1); node.retry_active[slot + 1] = true;
|
||||
entry.text_fingerprint[0] = 0xaa;
|
||||
return entry;
|
||||
}
|
||||
void blockUsb() {
|
||||
usb_stream.write_capacity = 0;
|
||||
const uint8_t required = 0;
|
||||
for (unsigned i = 0; i < 4; ++i) assert(manager.writeFrameToRoute(&usb, &required, 1) == 1);
|
||||
}
|
||||
bool ack(uint32_t crc, ContactInfo*& peer) {
|
||||
return node.processAck(reinterpret_cast<const uint8_t*>(&crc), peer);
|
||||
}
|
||||
void received(uint32_t crc) {
|
||||
mesh::Packet packet;
|
||||
packet.header = ROUTE_TYPE_FLOOD;
|
||||
node.txt_send_timeout = 9000;
|
||||
node.onAckRecv(&packet, crc);
|
||||
assert(packet.isMarkedDoNotRetransmit() && node.txt_send_timeout == 0);
|
||||
// Actual Packet marking replaces the route header. Preserve the current
|
||||
// (separately audited) no-return-path behavior rather than a fake Packet.
|
||||
assert(node.path_notices.empty());
|
||||
}
|
||||
void drain() {
|
||||
usb_stream.write_capacity = other_stream.write_capacity = 4096;
|
||||
for (unsigned i = 0; i < 8; ++i) manager.loop();
|
||||
}
|
||||
void command(uint8_t key, uint8_t code = CMD_ADD_UPDATE_CONTACT) {
|
||||
memset(node.cmd_frame, 0, sizeof(node.cmd_frame));
|
||||
node.cmd_frame[0] = code;
|
||||
memset(node.cmd_frame + 1, key, PUB_KEY_SIZE);
|
||||
if (code == CMD_REMOVE_CONTACT) { node.handleContactCommand(1 + PUB_KEY_SIZE); return; }
|
||||
node.cmd_frame[1 + PUB_KEY_SIZE] = ADV_TYPE_CHAT;
|
||||
node.cmd_frame[1 + PUB_KEY_SIZE + 2] = OUT_PATH_UNKNOWN;
|
||||
memcpy(node.cmd_frame + 1 + PUB_KEY_SIZE + 3 + MAX_PATH_SIZE, "updated", 8);
|
||||
node.handleContactCommand(CONTACT_UPDATE_FRAME_MIN_LEN);
|
||||
}
|
||||
void advert(uint8_t key, uint8_t hops = 0) {
|
||||
ContactInfo contact = makeContact(key);
|
||||
uint8_t data[MAX_ADVERT_DATA_SIZE];
|
||||
const size_t size = AdvertDataBuilder(ADV_TYPE_CHAT, "new-advert").encodeTo(data);
|
||||
mesh::Packet packet;
|
||||
packet.header = ROUTE_TYPE_FLOOD; packet.path_len = hops;
|
||||
node.onAdvertRecv(&packet, contact.id, 100, data, size);
|
||||
}
|
||||
};
|
||||
|
||||
static void unchanged(const MyMesh::AckTableEntry& current, const MyMesh::AckTableEntry& previous) {
|
||||
assert(current.ack == previous.ack && current.msg_sent == previous.msg_sent);
|
||||
assert(current.expires_at == previous.expires_at && current.confirmed == previous.confirmed);
|
||||
assert(current.message_timestamp == previous.message_timestamp && current.reply_route == previous.reply_route);
|
||||
assert(memcmp(current.retry_key, previous.retry_key, sizeof(current.retry_key)) == 0);
|
||||
assert(memcmp(current.text_fingerprint, previous.text_fingerprint, sizeof(current.text_fingerprint)) == 0);
|
||||
#if COMPANION_FEATURE_TEXT_TERMINAL
|
||||
assert(current.terminal_origin == previous.terminal_origin);
|
||||
#endif
|
||||
}
|
||||
static unsigned confirmations(const std::vector<uint8_t>& bytes, uint32_t ack = 0x11223344) {
|
||||
unsigned count = 0;
|
||||
size_t offset = 0;
|
||||
while (offset < bytes.size()) {
|
||||
assert(offset + 3 <= bytes.size() && bytes[offset] == '>');
|
||||
const size_t size = bytes[offset + 1] | (size_t(bytes[offset + 2]) << 8);
|
||||
offset += 3;
|
||||
assert(offset + size <= bytes.size());
|
||||
if (bytes[offset] == PUSH_CODE_SEND_CONFIRMED) {
|
||||
uint32_t crc, rtt;
|
||||
assert(size == 9);
|
||||
memcpy(&crc, bytes.data() + offset + 1, 4);
|
||||
memcpy(&rtt, bytes.data() + offset + 5, 4);
|
||||
if (crc == ack) { assert(rtt == 50); ++count; }
|
||||
}
|
||||
offset += size;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int main() {
|
||||
unsigned checks = 0;
|
||||
{
|
||||
Fixture f;
|
||||
// Execute the real constructor body without a live reference callback,
|
||||
// then prove a later runtime reset uses the hook and advances revision.
|
||||
assert(f.node.references_changed == 0 && f.node.contact_table_revision == 1);
|
||||
f.node.resetContacts();
|
||||
assert(f.node.references_changed == MAX_ANON_CONTACTS);
|
||||
assert(f.node.num_contacts == MAX_ANON_CONTACTS && f.node.contact_table_revision == 2);
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f;
|
||||
auto* a = f.add(1); auto* b = f.add(2); auto* c = f.add(3);
|
||||
auto& first = f.pending(b); auto& second = f.pending(c, 1, 0x55667788);
|
||||
auto& same_peer = f.pending(b, 2, 0x88776655);
|
||||
const auto prior = first;
|
||||
assert(f.node.removeContact(*a));
|
||||
assert(first.contact == &f.node.contacts[MAX_ANON_CONTACTS]);
|
||||
assert(second.contact == &f.node.contacts[MAX_ANON_CONTACTS + 1]);
|
||||
assert(same_peer.contact == first.contact);
|
||||
unchanged(first, prior);
|
||||
assert(first.contact->id.pub_key[0] == 2 && second.contact->id.pub_key[0] == 3);
|
||||
uint8_t path[MAX_PATH_SIZE];
|
||||
assert(first.contact->copyPathTo(path) && path[0] == 2);
|
||||
assert(second.contact->copyPathTo(path) && path[0] == 3);
|
||||
f.received(first.ack); f.received(second.ack); f.received(same_peer.ack);
|
||||
if (MESH_ENABLE_ONE_KEY_DM) assert((f.node.learned == std::vector<uint8_t>{2, 3, 2}));
|
||||
else assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* peer = f.add(1); f.blockUsb();
|
||||
auto& entry = f.pending(peer); const auto prior = entry;
|
||||
assert(f.node.removeContact(*peer) && entry.contact == nullptr);
|
||||
unchanged(entry, prior); assert(f.node.retry_active[1]);
|
||||
f.received(entry.ack);
|
||||
assert(entry.confirmed && entry.reply_route == &f.usb && entry.msg_sent == 50);
|
||||
assert(entry.expires_at == 10100 && f.node.cancellations == 1 && f.node.learned.empty());
|
||||
f.drain(); g_mock_millis = 110; f.node.service(); f.drain();
|
||||
assert(entry.ack == 0 && confirmations(f.usb_stream.output) == 1);
|
||||
assert(f.other_stream.output.empty());
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; f.add(1); auto* tail = f.add(2);
|
||||
auto& entry = f.pending(tail); const auto prior = entry;
|
||||
assert(f.node.removeContact(*tail) && entry.contact == nullptr);
|
||||
unchanged(entry, prior);
|
||||
f.add(3); assert(entry.contact == nullptr);
|
||||
f.received(entry.ack); assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* peer = f.add(1); auto* anon = f.transient(9);
|
||||
auto& entry = f.pending(peer); auto& transient_entry = f.pending(anon, 1, 0x55667788);
|
||||
const auto prior = entry;
|
||||
f.node.resetContacts();
|
||||
assert(entry.contact == nullptr && transient_entry.contact == nullptr);
|
||||
unchanged(entry, prior); f.add(2); assert(entry.contact == nullptr);
|
||||
f.received(entry.ack); assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
|
||||
const auto prior = entry;
|
||||
ContactInfo external = makeContact(10);
|
||||
assert(!f.node.clearTransientContact(external) && entry.contact == anon);
|
||||
assert(f.node.clearTransientContact(*anon) && entry.contact == nullptr);
|
||||
unchanged(entry, prior); f.received(entry.ack);
|
||||
assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
|
||||
const auto prior = entry;
|
||||
// Every unused transient has lastmod zero; force this exact slot to be the
|
||||
// oldest reusable slot so stale references cannot attach to another key.
|
||||
for (int i = 0; i < MAX_ANON_CONTACTS; ++i) f.node.contacts[i].lastmod = 1000;
|
||||
anon->lastmod = 1;
|
||||
assert(f.transient(10) == anon && entry.contact == nullptr);
|
||||
unchanged(entry, prior); f.received(entry.ack); assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
for (unsigned failure = 0; failure < 2; ++failure) {
|
||||
Fixture f; auto* peer = f.add(1); f.fill();
|
||||
auto& entry = f.pending(peer); const auto prior = entry;
|
||||
const auto notifications = f.node.references_changed;
|
||||
if (failure == 0) f.node.release_ok = false;
|
||||
else f.node.store.delete_ok = false;
|
||||
assert(!f.node.addContact(makeContact(99)));
|
||||
assert(entry.contact == peer && peer->id.pub_key[0] == 1);
|
||||
unchanged(entry, prior);
|
||||
assert(f.node.references_changed == notifications && f.node.cancellations == 0);
|
||||
assert(f.node.restores == (failure == 1 ? 1u : 0u));
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* peer = f.add(1); f.fill();
|
||||
auto& entry = f.pending(peer); const auto prior = entry;
|
||||
assert(f.node.addContact(makeContact(99)) && entry.contact == nullptr);
|
||||
assert(peer->id.pub_key[0] == 99); unchanged(entry, prior);
|
||||
f.received(entry.ack); assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
for (unsigned failure = 0; failure < 2; ++failure) {
|
||||
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
|
||||
const auto prior = entry;
|
||||
const auto prior_contact = *anon;
|
||||
if (failure == 0) { f.fill(); f.node.overwrite = false; }
|
||||
else f.node.schedule_ok = false;
|
||||
f.command(9);
|
||||
assert(f.node.error_replies == 1 && f.node.ok_replies == 0);
|
||||
assert(entry.contact == anon && anon->id.matches(prior_contact.id));
|
||||
assert(anon->lastmod == prior_contact.lastmod && anon->type == prior_contact.type);
|
||||
unchanged(entry, prior); assert(f.node.cancellations == 0);
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
|
||||
const auto prior = entry;
|
||||
f.command(9);
|
||||
const ContactInfo expected = makeContact(9);
|
||||
auto* accepted = f.node.lookupPersistentContactByPubKey(expected.id.pub_key, PUB_KEY_SIZE);
|
||||
assert(accepted != nullptr && entry.contact == accepted && anon->id.pub_key[0] == 0);
|
||||
assert(f.node.ok_replies == 1 && f.node.error_replies == 0);
|
||||
unchanged(entry, prior); f.received(entry.ack);
|
||||
if (MESH_ENABLE_ONE_KEY_DM) assert((f.node.learned == std::vector<uint8_t>{9}));
|
||||
++checks;
|
||||
}
|
||||
for (unsigned failure = 0; failure < 2; ++failure) {
|
||||
Fixture f; auto* persistent = f.add(9); auto* anon = f.transient(9);
|
||||
auto& entry = f.pending(anon); const auto prior = entry;
|
||||
f.node.schedule_ok = failure == 0;
|
||||
f.command(9);
|
||||
assert(entry.contact == (failure == 0 ? persistent : anon));
|
||||
assert(f.node.ok_replies == (failure == 0 ? 1u : 0u));
|
||||
assert(f.node.error_replies == (failure == 1 ? 1u : 0u));
|
||||
unchanged(entry, prior);
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* old = f.add(1); auto* next = f.add(2); auto* anon = f.transient(9); f.fill();
|
||||
auto& old_entry = f.pending(old);
|
||||
auto& next_entry = f.pending(next, 1, 0x55667788);
|
||||
auto& anon_entry = f.pending(anon, 2, 0x88776655);
|
||||
const auto old_state = old_entry, next_state = next_entry, anon_state = anon_entry;
|
||||
f.node.schedule_ok = false; f.command(9);
|
||||
assert(f.node.error_replies == 1 && old_entry.contact == nullptr);
|
||||
assert(next_entry.contact == &f.node.contacts[MAX_ANON_CONTACTS]);
|
||||
assert(next_entry.contact->id.pub_key[0] == 2 && anon_entry.contact == anon);
|
||||
unchanged(old_entry, old_state); unchanged(next_entry, next_state); unchanged(anon_entry, anon_state);
|
||||
assert(f.node.cancellations == 0);
|
||||
f.received(old_entry.ack); assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
for (unsigned refusal = 0; refusal < 3; ++refusal) {
|
||||
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
|
||||
const auto prior = entry;
|
||||
if (refusal == 0) f.node.auto_add = false;
|
||||
if (refusal == 1) f.node.max_hops = 1;
|
||||
if (refusal == 2) { f.fill(); f.node.overwrite = false; }
|
||||
f.advert(9, refusal == 1 ? 1 : 0);
|
||||
assert(entry.contact == anon && anon->id.pub_key[0] == 9);
|
||||
unchanged(entry, prior); assert(f.node.cancellations == 0);
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
|
||||
const auto prior = entry;
|
||||
f.advert(9);
|
||||
const ContactInfo identity = makeContact(9);
|
||||
auto* accepted = f.node.lookupPersistentContactByPubKey(identity.id.pub_key, PUB_KEY_SIZE);
|
||||
assert(accepted != nullptr && entry.contact == accepted && anon->id.pub_key[0] == 0);
|
||||
unchanged(entry, prior); f.received(entry.ack);
|
||||
++checks;
|
||||
}
|
||||
for (unsigned failure = 0; failure < 2; ++failure) {
|
||||
Fixture f; auto* anon = f.transient(9); auto& entry = f.pending(anon);
|
||||
const auto prior = entry;
|
||||
f.node.schedule_ok = failure == 0;
|
||||
const auto identity = makeContact(9).id;
|
||||
f.node.acceptVerifiedOneKey(identity);
|
||||
if (failure == 0) {
|
||||
assert(entry.contact == f.node.lookupPersistentContactByPubKey(identity.pub_key, PUB_KEY_SIZE));
|
||||
assert(anon->id.pub_key[0] == 0);
|
||||
} else assert(entry.contact == anon && anon->id.pub_key[0] == 9);
|
||||
unchanged(entry, prior);
|
||||
++checks;
|
||||
}
|
||||
for (unsigned failure = 0; failure < 2; ++failure) {
|
||||
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
|
||||
const auto prior = entry;
|
||||
if (failure == 0) f.node.release_ok = false;
|
||||
else f.node.store.delete_ok = false;
|
||||
f.command(1, CMD_REMOVE_CONTACT);
|
||||
assert(f.node.error_replies == 1 && entry.contact == peer && peer->id.pub_key[0] == 1);
|
||||
unchanged(entry, prior); assert(f.node.cancellations == 0);
|
||||
++checks;
|
||||
}
|
||||
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
|
||||
for (unsigned failure = 1; failure <= 2; ++failure) {
|
||||
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
|
||||
const auto prior = entry;
|
||||
const auto notifications = f.node.references_changed;
|
||||
fail_allocation = failure;
|
||||
assert(!f.node.initializeContactStorage());
|
||||
assert(entry.contact == peer && f.node.contacts == f.node.contacts_fallback);
|
||||
assert(outstanding_allocations.empty() && f.node.references_changed == notifications);
|
||||
unchanged(entry, prior);
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
|
||||
entry.confirmed = true; entry.msg_sent = 50; entry.expires_at = 10100;
|
||||
const auto prior = entry;
|
||||
assert(f.node.initializeContactStorage());
|
||||
assert(entry.contact != peer && entry.contact == &f.node.contacts[MAX_ANON_CONTACTS]);
|
||||
assert(entry.contact->id.pub_key[0] == 1);
|
||||
uint8_t path[MAX_PATH_SIZE]; assert(entry.contact->copyPathTo(path) && path[0] == 1);
|
||||
unchanged(entry, prior); assert(f.node.cancellations == 0);
|
||||
++checks;
|
||||
}
|
||||
#endif
|
||||
{
|
||||
Fixture f; auto* peer = f.add(1);
|
||||
auto& connection = f.node.connections[0];
|
||||
connection.server_id = peer->id; connection.keep_alive_millis = 1000;
|
||||
connection.last_activity = 1; connection.next_ping = 2;
|
||||
ContactInfo* result = peer;
|
||||
assert(!f.ack(0, result) && result == nullptr);
|
||||
assert(connection.last_activity == 1 && connection.next_ping == 2);
|
||||
connection.expected_ack = 0x11223344;
|
||||
assert(f.ack(connection.expected_ack, result) && result == peer);
|
||||
assert(connection.expected_ack == 0 && connection.last_activity == 100);
|
||||
result = peer; assert(!f.ack(0x11223344, result) && result == nullptr);
|
||||
connection.expected_ack = 0x11223344;
|
||||
assert(f.node.removeContact(*peer));
|
||||
f.received(connection.expected_ack);
|
||||
assert(connection.expected_ack == 0 && f.node.learned.empty());
|
||||
connection.expected_ack = 0x11223344; connection.keep_alive_millis = 0;
|
||||
assert(!f.ack(0x11223344, result) && result == nullptr);
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* peer = f.add(1); auto& entry = f.pending(peer);
|
||||
assert(f.node.removeContact(*peer));
|
||||
ContactInfo sender = makeContact(2); uint8_t path[] = {3};
|
||||
uint32_t crc = entry.ack;
|
||||
f.node.can_mutate = false; f.node.txt_send_timeout = 9000;
|
||||
assert(f.node.onContactPathRecv(sender, path, 1, path, 1, PAYLOAD_TYPE_ACK,
|
||||
reinterpret_cast<uint8_t*>(&crc), sizeof(crc)));
|
||||
assert(f.node.txt_send_timeout == 0 && f.node.cancellations == 1 && f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* a = f.add(1); auto* b = f.add(2); f.blockUsb();
|
||||
auto& entry = f.pending(b);
|
||||
ContactInfo* result = nullptr;
|
||||
assert(f.ack(entry.ack, result) && result == b && entry.confirmed);
|
||||
const auto prior = entry;
|
||||
const auto learned = f.node.learned;
|
||||
assert(f.node.removeContact(*a));
|
||||
assert(entry.contact == &f.node.contacts[MAX_ANON_CONTACTS]); unchanged(entry, prior);
|
||||
assert(!f.ack(entry.ack, result) && result == nullptr);
|
||||
assert(f.node.removeContact(*entry.contact) && entry.contact == nullptr);
|
||||
unchanged(entry, prior); assert(f.node.learned == learned && f.node.cancellations == 1);
|
||||
f.drain(); g_mock_millis = 110; f.node.service(); f.drain();
|
||||
assert(entry.ack == 0 && confirmations(f.usb_stream.output) == 1);
|
||||
++checks;
|
||||
}
|
||||
{
|
||||
Fixture f; auto* old = f.add(1); auto* live = f.add(2); f.blockUsb();
|
||||
auto& first = f.pending(old);
|
||||
auto& second = f.pending(live, 1, first.ack, &f.other);
|
||||
assert(f.node.removeContact(*old) && first.contact == nullptr && second.contact->id.pub_key[0] == 2);
|
||||
ContactInfo* result = live;
|
||||
assert(f.ack(first.ack, result) && result == nullptr && first.confirmed && !second.confirmed);
|
||||
assert(f.ack(first.ack, result) && result == &f.node.contacts[MAX_ANON_CONTACTS]);
|
||||
assert(f.node.cancellations == 2);
|
||||
if (MESH_ENABLE_ONE_KEY_DM) assert((f.node.learned == std::vector<uint8_t>{2}));
|
||||
else assert(f.node.learned.empty());
|
||||
++checks;
|
||||
}
|
||||
#if defined(ESP32_PLATFORM) && defined(BOARD_HAS_PSRAM)
|
||||
assert(checks == 31);
|
||||
#else
|
||||
assert(checks == 28);
|
||||
#endif
|
||||
assert(outstanding_allocations.empty());
|
||||
printf("PASS: %u production contact lifetime checks\n", checks);
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class CompanionAckDeliveryTests(unittest.TestCase):
|
||||
functions = "\n".join(extract_braced(source, signature) for signature in (
|
||||
"void MyMesh::clearExpectedAck(", "void MyMesh::expireExpectedAcks(",
|
||||
"MyMesh::AckTableEntry* MyMesh::findPendingTextMessage(",
|
||||
"ContactInfo* MyMesh::processAck(",
|
||||
"bool MyMesh::processAck(",
|
||||
"void MyMesh::cancelSerialOperationsForRoute(",
|
||||
"bool MyMesh::hasFiniteDelayedReplyForRoute(",
|
||||
))
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute production contact mutations and ACK ownership under sanitizers."""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/companion_contact_lifetime"
|
||||
SANITIZERS = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
|
||||
|
||||
|
||||
class CompanionContactLifetimeTests(unittest.TestCase):
|
||||
def test_production_mutations_and_owned_ack(self):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if compiler is None:
|
||||
self.skipTest("a host C++17 compiler is required")
|
||||
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
header = (ROOT / "examples/companion_radio/MyMesh.h").read_text()
|
||||
base = (ROOT / "src/helpers/BaseChatMesh.cpp").read_text()
|
||||
base_header = (ROOT / "src/helpers/BaseChatMesh.h").read_text()
|
||||
constructor = extract_braced(base_header, "BaseChatMesh(mesh::Radio&")
|
||||
constructor_body = constructor[constructor.index("{") + 1:-1]
|
||||
packet = (ROOT / "src/Packet.cpp").read_text()
|
||||
identity = (ROOT / "src/Identity.cpp").read_text()
|
||||
dispatcher = (ROOT / "src/Dispatcher.cpp").read_text()
|
||||
native = (ROOT / "test/test_serial_mode_switch/test_serial_mode_switch.cpp").read_text()
|
||||
functions = "\n".join(extract_braced(base, signature) for signature in (
|
||||
"void BaseChatMesh::resetContactValue(",
|
||||
"bool BaseChatMesh::initializeContactStorage(",
|
||||
"void BaseChatMesh::populateContactFromAdvert(",
|
||||
"void BaseChatMesh::onAdvertRecv(",
|
||||
"ContactInfo* BaseChatMesh::lookupContactByPubKey(",
|
||||
"ContactInfo* BaseChatMesh::lookupTransientContactByPubKey(",
|
||||
"ContactInfo* BaseChatMesh::lookupPersistentContactByPubKey(",
|
||||
"bool BaseChatMesh::isTransientContact(",
|
||||
"bool BaseChatMesh::clearTransientContact(",
|
||||
"bool BaseChatMesh::addContact(",
|
||||
"bool BaseChatMesh::removeContact(",
|
||||
"bool BaseChatMesh::checkConnectionsAck(",
|
||||
"bool BaseChatMesh::onContactPathRecv(",
|
||||
"void BaseChatMesh::onAckRecv(",
|
||||
))
|
||||
# Both preprocessor alternatives contain the same opening brace. Keep
|
||||
# the complete real definition, rather than selecting a copied branch.
|
||||
functions += "\n" + base[base.index("ContactInfo* BaseChatMesh::allocateContactSlot("):
|
||||
base.index("void BaseChatMesh::populateContactFromAdvert(")]
|
||||
functions += "\nvoid BaseChatMesh::" + extract_braced(base_header, "void resetContacts() {")[5:]
|
||||
functions += "\n" + extract_braced(
|
||||
(ROOT / "src/helpers/TxtDataHelpers.cpp").read_text(), "void StrHelper::strncpy(")
|
||||
functions += "\n" + "\n".join(extract_braced(companion, signature) for signature in (
|
||||
"void MyMesh::clearExpectedAck(", "void MyMesh::expireExpectedAcks(",
|
||||
"bool MyMesh::processAck(", "bool MyMesh::onContactOverwrite(",
|
||||
"bool MyMesh::updateContactFromFrame(",
|
||||
))
|
||||
functions += "\n" + extract_braced(companion, "void MyMesh::onContactReferenceChanged(").replace(
|
||||
"MyMesh::onContactReferenceChanged(", "MyMesh::applyContactReferenceChanged(", 1)
|
||||
functions += "\n" + "\n".join(extract_braced(dispatcher, signature).replace(
|
||||
"Dispatcher::", "BaseChatMesh::") for signature in (
|
||||
"bool Dispatcher::millisHasNowPassed(", "unsigned long Dispatcher::futureMillis("))
|
||||
functions += "\nvoid MyMesh::service() {\n" + extract_braced(companion, "if (has_next_ack_expiry\n") + "\n}\n"
|
||||
# Execute the actual admin branches. Framing/authentication is already
|
||||
# covered separately; the real parser and mutation/rollback are retained.
|
||||
functions += "\nvoid MyMesh::handleContactCommand(int len) { if (false) {}\n"
|
||||
functions += extract_braced(companion, "else if (cmd_frame[0] == CMD_ADD_UPDATE_CONTACT)")
|
||||
functions += "\n" + extract_braced(companion, "else if (cmd_frame[0] == CMD_REMOVE_CONTACT &&") + "\n}\n"
|
||||
verified = extract_braced(companion, "void MyMesh::onAnonDataRecv(")
|
||||
tail_start = verified.index(" if (!canMutateContacts()) return;")
|
||||
tail_end = verified.rindex("\n}")
|
||||
functions += "\nvoid MyMesh::acceptVerifiedOneKey(const mesh::Identity& sender) {\n"
|
||||
functions += " ContactInfo contact = makeContact(sender.pub_key[0]);\n"
|
||||
functions += verified[tail_start:tail_end] + "\n}\n"
|
||||
constants = "\n".join(re.findall(
|
||||
r"^#define (?:EXPECTED_ACK_[A-Z_]+|PUSH_CODE_SEND_CONFIRMED|PUSH_CODE_CONTACT_DELETED|"
|
||||
r"CMD_ADD_UPDATE_CONTACT|CMD_REMOVE_CONTACT|ERR_CODE_[A-Z_]+)\s+.+$", companion, re.MULTILINE))
|
||||
constants += "\n#define EXPECTED_ACK_TABLE_SIZE 8\n"
|
||||
first = companion.index("static constexpr int CONTACT_UPDATE_FRAME_MIN_LEN")
|
||||
last = companion.index("bool MyMesh::updateContactFromFrame(", first)
|
||||
constants += "\n" + companion[first:last]
|
||||
constructors = "namespace mesh {\n" + "\n".join(
|
||||
extract_braced(identity, signature) for signature in (
|
||||
"Identity::Identity()", "LocalIdentity::LocalIdentity()")) + "\n"
|
||||
constructors += "\n".join(extract_braced(packet, signature) for signature in (
|
||||
"Packet::Packet()", "bool Packet::isValidPathLen(",
|
||||
"size_t Packet::writePath(", "uint8_t Packet::copyPath(",
|
||||
"uint8_t Packet::writeTo(")) + "\n}\n"
|
||||
variants = (("generic", 0, 0, 0), ("generic", 1, 1, 1),
|
||||
("nrf52", 0, 1, 0), ("nrf52", 1, 1, 1),
|
||||
("psram", 0, 0, 0), ("psram", 0, 1, 1),
|
||||
("psram", 1, 0, 1), ("psram", 1, 1, 0))
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-contact-lifetime-") as directory:
|
||||
work = Path(directory)
|
||||
(work / "production_constants.inc").write_text(constants, encoding="ascii")
|
||||
(work / "production_types.inc").write_text(
|
||||
"namespace mesh {\n" + extract_braced((ROOT / "src/Mesh.h").read_text(), "class GroupChannel")
|
||||
+ ";\n}\n" + extract_braced((ROOT / "src/helpers/ChannelDetails.h").read_text(), "struct ChannelDetails")
|
||||
+ ";\n" + extract_braced(base_header, "struct ConnectionInfo") + ";\n", encoding="ascii")
|
||||
(work / "production_entry.inc").write_text(
|
||||
extract_braced(header, "struct AckTableEntry") + ";\n", encoding="ascii")
|
||||
(work / "production_stream.inc").write_text(
|
||||
extract_braced(native, "class BufferStream") + ";\n", encoding="ascii")
|
||||
(work / "production_functions.inc").write_text(functions, encoding="ascii")
|
||||
(work / "production_constructors.inc").write_text(constructors, encoding="ascii")
|
||||
(work / "production_base_constructor.inc").write_text(constructor_body, encoding="ascii")
|
||||
for platform, cache, terminal, one_key in variants:
|
||||
with self.subTest(platform=platform, cache=cache, terminal=terminal, one_key=one_key):
|
||||
defines = [f"-DMESH_CONTACT_CACHE={cache}",
|
||||
f"-DCOMPANION_FEATURE_TEXT_TERMINAL={terminal}",
|
||||
f"-DMESH_ENABLE_ONE_KEY_DM={one_key}"]
|
||||
if terminal:
|
||||
defines.append("-DMAX_GROUP_CHANNELS=4")
|
||||
if platform == "nrf52":
|
||||
defines.append("-DNRF52_PLATFORM=1")
|
||||
elif platform == "psram":
|
||||
defines += ["-DESP32_PLATFORM=1", "-DBOARD_HAS_PSRAM=1"]
|
||||
binary = work / f"lifetime-{platform}-{cache}-{terminal}-{one_key}.exe"
|
||||
compiled = subprocess.run([
|
||||
compiler, "-std=c++17", "-Werror", *SANITIZERS, *defines,
|
||||
f"-I{work}", f"-I{FIXTURE / 'mocks'}", f"-I{ROOT / 'test/mocks'}",
|
||||
f"-I{ROOT / 'src'}", str(FIXTURE / "test.cpp"),
|
||||
str(ROOT / "src/helpers/ArduinoSerialInterface.cpp"),
|
||||
str(ROOT / "src/helpers/AdvertDataHelpers.cpp"),
|
||||
"-o", str(binary),
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
|
||||
expected_checks = 31 if platform == "psram" else 28
|
||||
self.assertIn(f"PASS: {expected_checks} production contact lifetime checks", checked.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -152,7 +152,7 @@ class CompanionContactPersistenceContractTests(unittest.TestCase):
|
||||
)
|
||||
error = section.index("writeErrFrame(ERR_CODE_FILE_IO_ERROR)", rollback)
|
||||
ok = section.index("writeOKFrame()", schedule)
|
||||
clear_transient = section.index("clearTransientContact(*transient)", schedule)
|
||||
clear_transient = section.index("clearTransientContact(*transient, recipient)", schedule)
|
||||
self.assertLess(snapshot, mutation)
|
||||
self.assertLess(mutation, schedule)
|
||||
self.assertLess(schedule, clear_transient)
|
||||
@@ -227,10 +227,11 @@ class CompanionContactPersistenceContractTests(unittest.TestCase):
|
||||
self.assertLess(update_ok, update_rollback)
|
||||
self.assertLess(update_rollback, update_error)
|
||||
|
||||
promotion = section.index("Promotion must free the exact transient prefix slot")
|
||||
promotion = section.index("Preserve the transient and its pending ACK references")
|
||||
add_section = section[promotion:]
|
||||
clear = add_section.index("clearTransientContact(*transient)")
|
||||
add = add_section.index("addContact(candidate)")
|
||||
schedule = add_section.index("added != NULL && scheduleContactWrite(*added)")
|
||||
clear = add_section.index("clearTransientContact(*transient, added)")
|
||||
add_schedule = section.index(
|
||||
"added != NULL && scheduleContactWrite(*added)", update_error
|
||||
)
|
||||
@@ -239,11 +240,13 @@ class CompanionContactPersistenceContractTests(unittest.TestCase):
|
||||
add_error = section.index(
|
||||
"writeErrFrame(ERR_CODE_FILE_IO_ERROR)", add_rollback
|
||||
)
|
||||
self.assertLess(clear, add)
|
||||
self.assertLess(add, schedule)
|
||||
self.assertLess(schedule, clear)
|
||||
self.assertLess(add_schedule, add_ok)
|
||||
self.assertLess(add_ok, add_rollback)
|
||||
self.assertLess(add_rollback, add_error)
|
||||
self.assertIn("if (transient != NULL) *transient = previous_transient", add_section)
|
||||
self.assertNotIn("clearTransientContact", add_section[:schedule])
|
||||
self.assertNotIn("previous_transient", section)
|
||||
|
||||
def test_transient_prefix_is_never_compacted(self):
|
||||
transient_lookup = function_body(
|
||||
|
||||
@@ -73,7 +73,7 @@ public:
|
||||
void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) override {
|
||||
++discovered;
|
||||
}
|
||||
ContactInfo* processAck(const uint8_t*) override { return nullptr; }
|
||||
bool processAck(const uint8_t*, ContactInfo*& peer) override { peer = nullptr; return false; }
|
||||
void onContactPathUpdated(const ContactInfo&) override {}
|
||||
void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
|
||||
void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {}
|
||||
|
||||
Reference in new issue
Block a user