Squashed 'external/toxcore/c-toxcore/' changes from 1828c5356..c9cdae001

c9cdae001 fix(toxav): remove extra copy of video frame on encode
4f6d4546b test: Improve the fake network library.
a2581e700 refactor(toxcore): generate `Friend_Request` and `Dht_Nodes_Response`
2aaa11770 refactor(toxcore): use Tox_Memory in generated events
5c367452b test(toxcore): fix incorrect mutex in tox_scenario_get_time
8f92e710f perf: Add a timed limit of number of cookie requests.
695b6417a test: Add some more simulated network support.
815ae9ce9 test(toxcore): fix thread-safety in scenario framework
6d85c754e test(toxcore): add unit tests for net_crypto
9c22e79cc test(support): add SimulatedEnvironment for deterministic testing
f34fcb195 chore: Update windows Dockerfile to debian stable (trixie).
ece0e8980 fix(group_moderation): allow validating unsorted sanction list signatures
a4fa754d7 refactor: rename struct Packet to struct Net_Packet
d6f330f85 cleanup: Fix some warnings from coverity.
e206bffa2 fix(group_chats): fix sync packets reverting topics
0e4715598 test: Add new scenario testing framework.
668291f44 refactor(toxcore): decouple Network_Funcs from sockaddr via IP_Port
fc4396cef fix: potential division by zero in toxav and unsafe hex parsing
8e8b352ab refactor: Add nullable annotations to struct members.
7740bb421 refactor: decouple net_crypto from DHT
1936d4296 test: add benchmark for toxav audio and video
46bfdc2df fix: correct printf format specifiers for unsigned integers
REVERT: 1828c5356 fix(toxav): remove extra copy of video frame on encode

git-subtree-dir: external/toxcore/c-toxcore
git-subtree-split: c9cdae001341e701fca980c9bb9febfeb95d2902
This commit is contained in:
Green Sky
2026-01-11 14:42:31 +01:00
parent e95f2cbb1c
commit 565efa4f39
328 changed files with 19057 additions and 13982 deletions

View File

@@ -0,0 +1,79 @@
# Tox Scenario Framework
Tests should read like protocol specifications. Instead of managing manual loops
and shared global state, each "node" in a test is assigned a script (sequence of
tasks) that it executes concurrently with other nodes in a simulated
environment.
### 1. `ToxScenario`
The container for a single test case.
- Manages a collection of `ToxNode` instances.
- Owns the **Virtual Clock**. All nodes in a scenario share the same timeline,
making timeouts and delays deterministic.
- Handles the orchestration of the event loop (`tox_iterate`).
### 2. `ToxNode`
A wrapper around a `Tox` instance and its associated state.
- Each node has its own `Tox_Dispatch`.
- Nodes are identified by a simple index or a string alias (e.g., "Alice",
"Bob").
- Encapsulates the node's progress through its assigned Script.
### 3. `tox_node_script_cb`
A C function that defines what a node does. Because it runs in its own thread,
you can use standard C control flow, local variables, and loops.
## Example Usage
```c
void alice_script(ToxNode *self, void *ctx) {
// Step 1: Wait for DHT connection
WAIT_UNTIL(tox_node_is_self_connected(self));
// Step 2: Send a message to Bob (friend 0)
uint8_t msg[] = "Hello Bob";
tox_friend_send_message(tox_node_get_tox(self), 0, TOX_MESSAGE_TYPE_NORMAL, msg, sizeof(msg), NULL);
// Step 3: Wait for a specific response event
WAIT_FOR_EVENT(TOX_EVENT_FRIEND_MESSAGE);
}
void test_simple_interaction() {
ToxScenario *s = tox_scenario_new(argc, argv, 10000); // 10s virtual timeout
ToxNode *alice = tox_scenario_add_node(s, "Alice", alice_script, NULL);
ToxNode *bob = tox_scenario_add_node(s, "Bob", bob_script, NULL);
tox_node_bootstrap(bob, alice);
tox_node_friend_add(alice, bob);
tox_node_friend_add(bob, alice);
ToxScenarioStatus res = tox_scenario_run(s);
ck_assert(res == TOX_SCENARIO_DONE);
tox_scenario_free(s);
}
```
## Execution Model: Cooperative Multi-threading
The scenario framework uses `pthread` to provide a natural programming model
while maintaining a deterministic virtual clock:
1. **Runner**: Orchestrates the scenario in "Ticks" (default 10ms).
2. **Nodes**: Run concurrently in their own threads but **synchronize** at
every tick.
3. **Yielding**: When a script calls `WAIT_UNTIL`, `WAIT_FOR_EVENT`, or
`tox_scenario_yield`, it yields control back to the runner.
4. **Time Advancement**: The runner advances the virtual clock and signals all
nodes to proceed only after all active nodes have yielded.
5. **Barriers**: Nodes can synchronize their progress using
`tox_scenario_barrier_wait(self)`. This function blocks the calling node
until all other active nodes have also reached the barrier. This is useful
for ensuring setup steps (like group creation or connection establishment)
are complete across all nodes before proceeding to the next phase of the
test.

View File

@@ -0,0 +1,800 @@
#include "framework.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <time.h>
#include <stdarg.h>
#include "../../../testing/misc_tools.h"
#include "../../../toxcore/tox_struct.h"
#include "../../../toxcore/network.h"
#define MAX_NODES 128
typedef struct {
Tox_Connection connection_status;
bool finished;
bool offline;
uint8_t public_key[TOX_PUBLIC_KEY_SIZE];
uint8_t dht_id[TOX_PUBLIC_KEY_SIZE];
uint8_t address[TOX_ADDRESS_SIZE];
uint16_t udp_port;
} ToxNodeMirror;
struct ToxNode {
Tox *tox;
Tox_Options *options;
Tox_Dispatch *dispatch;
uint32_t index;
char *alias;
ToxScenario *scenario;
pthread_t thread;
tox_node_script_cb *script;
void *script_ctx;
size_t script_ctx_size;
void *mirrored_ctx;
void *mirrored_ctx_public;
ToxNodeMirror mirror;
ToxNodeMirror mirror_public;
bool finished;
bool offline;
uint32_t barrier_index;
uint64_t last_tick;
bool seen_events[256];
};
struct ToxScenario {
ToxNode *nodes[MAX_NODES];
uint32_t num_nodes;
uint32_t num_active;
uint32_t num_ready;
uint64_t virtual_clock;
uint64_t timeout_ms;
uint64_t tick_count;
pthread_mutex_t mutex;
pthread_mutex_t clock_mutex;
pthread_cond_t cond_runner;
pthread_cond_t cond_nodes;
bool run_started;
bool trace_enabled;
bool event_log_enabled;
struct {
const char *name;
uint32_t count;
} barrier;
};
static uint64_t get_scenario_clock(void *user_data)
{
ToxScenario *s = (ToxScenario *)user_data;
pthread_mutex_lock(&s->clock_mutex);
uint64_t time = s->virtual_clock;
pthread_mutex_unlock(&s->clock_mutex);
return time;
}
static void framework_debug_log(Tox *tox, Tox_Log_Level level, const char *file, uint32_t line,
const char *func, const char *message, void *user_data)
{
ToxNode *node = (ToxNode *)user_data;
ck_assert(node != nullptr);
if (level == TOX_LOG_LEVEL_TRACE) {
if (node == nullptr || !node->scenario->trace_enabled) {
return;
}
}
const char *level_name = "UNKNOWN";
switch (level) {
case TOX_LOG_LEVEL_TRACE:
level_name = "TRACE";
break;
case TOX_LOG_LEVEL_DEBUG:
level_name = "DEBUG";
break;
case TOX_LOG_LEVEL_INFO:
level_name = "INFO";
break;
case TOX_LOG_LEVEL_WARNING:
level_name = "WARN";
break;
case TOX_LOG_LEVEL_ERROR:
level_name = "ERROR";
break;
}
const uint64_t relative_time = node ? (get_scenario_clock(node->scenario) - 1000) : 0;
fprintf(stderr, "[%08lu] [%s] %s %s:%u %s: %s\n", (unsigned long)relative_time,
node != nullptr ? node->alias : "Unknown", level_name, file, line, func, message);
}
void tox_node_log(ToxNode *node, const char *format, ...)
{
ck_assert(node != nullptr);
va_list args;
va_start(args, format);
uint64_t relative_time = get_scenario_clock(node->scenario) - 1000;
fprintf(stderr, "[%08lu] [%s] ", (unsigned long)relative_time, node->alias ? node->alias : "Unknown");
vfprintf(stderr, format, args);
fprintf(stderr, "\n");
va_end(args);
}
void tox_scenario_log(const ToxScenario *s, const char *format, ...)
{
va_list args;
va_start(args, format);
uint64_t relative_time = get_scenario_clock((void *)(uintptr_t)s) - 1000;
fprintf(stderr, "[%08lu] [Runner] ", (unsigned long)relative_time);
vfprintf(stderr, format, args);
fprintf(stderr, "\n");
va_end(args);
}
ToxScenario *tox_scenario_new(int argc, char *const argv[], uint64_t timeout_ms)
{
static bool seeded = false;
if (!seeded) {
srand(time(nullptr));
seeded = true;
}
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--wait-time") == 0 && i + 1 < argc) {
timeout_ms = (uint64_t)atoll(argv[i + 1]) * 1000;
break;
}
}
ToxScenario *s = (ToxScenario *)calloc(1, sizeof(ToxScenario));
ck_assert(s != nullptr);
s->timeout_ms = timeout_ms;
s->virtual_clock = 1000;
s->trace_enabled = (getenv("TOX_TRACE") != nullptr);
s->event_log_enabled = (getenv("TOX_EVENT_LOG") != nullptr);
pthread_mutex_init(&s->mutex, nullptr);
pthread_mutex_init(&s->clock_mutex, nullptr);
pthread_cond_init(&s->cond_runner, nullptr);
pthread_cond_init(&s->cond_nodes, nullptr);
return s;
}
void tox_scenario_free(ToxScenario *s)
{
for (uint32_t i = 0; i < s->num_nodes; ++i) {
ToxNode *node = s->nodes[i];
ck_assert(node != nullptr);
tox_dispatch_free(node->dispatch);
tox_kill(node->tox);
tox_options_free(node->options);
free(node->alias);
free(node->mirrored_ctx);
free(node->mirrored_ctx_public);
free(node);
}
pthread_mutex_destroy(&s->mutex);
pthread_mutex_destroy(&s->clock_mutex);
pthread_cond_destroy(&s->cond_runner);
pthread_cond_destroy(&s->cond_nodes);
free(s);
}
Tox *tox_node_get_tox(const ToxNode *node)
{
ck_assert(node != nullptr);
ToxScenario *s = node->scenario;
pthread_mutex_lock(&s->mutex);
Tox *tox = node->tox;
pthread_mutex_unlock(&s->mutex);
return tox;
}
void tox_node_get_address(const ToxNode *node, uint8_t *address)
{
ck_assert(node != nullptr);
ck_assert(address != nullptr);
memcpy(address, node->mirror_public.address, TOX_ADDRESS_SIZE);
}
ToxScenario *tox_node_get_scenario(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->scenario;
}
Tox_Connection tox_node_get_connection_status(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->mirror_public.connection_status;
}
Tox_Connection tox_node_get_friend_connection_status(const ToxNode *node, uint32_t friend_number)
{
ck_assert(node != nullptr);
// Note: Friend status is not currently mirrored.
// This is safe if called by the node on itself.
// If called on a peer, it might still race.
Tox_Err_Friend_Query err;
Tox_Connection conn = tox_friend_get_connection_status(node->tox, friend_number, &err);
if (err != TOX_ERR_FRIEND_QUERY_OK) {
return TOX_CONNECTION_NONE;
}
return conn;
}
bool tox_node_is_self_connected(const ToxNode *node)
{
return tox_node_get_connection_status(node) != TOX_CONNECTION_NONE;
}
bool tox_node_is_friend_connected(const ToxNode *node, uint32_t friend_number)
{
return tox_node_get_friend_connection_status(node, friend_number) != TOX_CONNECTION_NONE;
}
uint32_t tox_node_get_conference_peer_count(const ToxNode *node, uint32_t conference_number)
{
ck_assert(node != nullptr);
Tox_Err_Conference_Peer_Query err;
uint32_t count = tox_conference_peer_count(node->tox, conference_number, &err);
if (err != TOX_ERR_CONFERENCE_PEER_QUERY_OK) {
return 0;
}
return count;
}
void tox_node_set_offline(ToxNode *node, bool offline)
{
ck_assert(node != nullptr);
node->offline = offline;
}
bool tox_node_is_offline(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->mirror_public.offline;
}
bool tox_node_is_finished(const ToxNode *node)
{
ck_assert(node != nullptr);
ToxScenario *s = node->scenario;
pthread_mutex_lock(&s->mutex);
bool finished = node->mirror_public.finished;
pthread_mutex_unlock(&s->mutex);
return finished;
}
bool tox_scenario_is_running(ToxNode *self)
{
bool running;
pthread_mutex_lock(&self->scenario->mutex);
running = self->scenario->run_started;
pthread_mutex_unlock(&self->scenario->mutex);
return running;
}
bool tox_node_friend_name_is(const ToxNode *node, uint32_t friend_number, const uint8_t *name, size_t length)
{
ck_assert(node != nullptr);
Tox_Err_Friend_Query err;
size_t actual_len = tox_friend_get_name_size(node->tox, friend_number, &err);
if (err != TOX_ERR_FRIEND_QUERY_OK || actual_len != length) {
return false;
}
uint8_t *actual_name = (uint8_t *)malloc(length);
ck_assert(actual_name != nullptr);
tox_friend_get_name(node->tox, friend_number, actual_name, nullptr);
bool match = (memcmp(actual_name, name, length) == 0);
free(actual_name);
return match;
}
bool tox_node_friend_status_message_is(const ToxNode *node, uint32_t friend_number, const uint8_t *msg, size_t length)
{
ck_assert(node != nullptr);
Tox_Err_Friend_Query err;
size_t actual_len = tox_friend_get_status_message_size(node->tox, friend_number, &err);
if (err != TOX_ERR_FRIEND_QUERY_OK || actual_len != length) {
return false;
}
uint8_t *actual_msg = (uint8_t *)malloc(length);
ck_assert(actual_msg != nullptr);
tox_friend_get_status_message(node->tox, friend_number, actual_msg, nullptr);
bool match = (memcmp(actual_msg, msg, length) == 0);
free(actual_msg);
return match;
}
bool tox_node_friend_typing_is(const ToxNode *node, uint32_t friend_number, bool is_typing)
{
ck_assert(node != nullptr);
Tox_Err_Friend_Query err;
bool actual = tox_friend_get_typing(node->tox, friend_number, &err);
return (err == TOX_ERR_FRIEND_QUERY_OK && actual == is_typing);
}
void tox_scenario_yield(ToxNode *self)
{
// 1. Poll Tox (Strictly in the node's thread), only if not offline
if (!tox_node_is_offline(self)) {
Tox_Err_Events_Iterate ev_err;
Tox_Events *events = tox_events_iterate(self->tox, false, &ev_err);
if (events != nullptr) {
uint32_t size = tox_events_get_size(events);
for (uint32_t j = 0; j < size; ++j) {
const Tox_Event *ev = tox_events_get(events, j);
const Tox_Event_Type type = tox_event_get_type(ev);
self->seen_events[type] = true;
if (self->scenario->event_log_enabled) {
tox_node_log(self, "Received event: %s (%u)", tox_event_type_to_string(type), type);
}
}
tox_dispatch_invoke(self->dispatch, events, self);
tox_events_free(events);
}
}
ToxScenario *s = self->scenario;
// 2. Update mirror (Outside lock to reduce contention)
self->mirror.connection_status = tox_self_get_connection_status(self->tox);
self->mirror.offline = self->offline;
tox_self_get_public_key(self->tox, self->mirror.public_key);
tox_self_get_dht_id(self->tox, self->mirror.dht_id);
tox_self_get_address(self->tox, self->mirror.address);
Tox_Err_Get_Port port_err;
self->mirror.udp_port = tox_self_get_udp_port(self->tox, &port_err);
if (self->mirrored_ctx != nullptr && self->script_ctx != nullptr) {
memcpy(self->mirrored_ctx, self->script_ctx, self->script_ctx_size);
}
pthread_mutex_lock(&s->mutex);
self->mirror.finished = self->finished;
self->last_tick = s->tick_count;
s->num_ready++;
// Wake runner
pthread_cond_signal(&s->cond_runner);
// Wait for next tick
while (self->last_tick == s->tick_count && s->run_started) {
pthread_cond_wait(&s->cond_nodes, &s->mutex);
}
pthread_mutex_unlock(&s->mutex);
// Give the OS a chance to deliver UDP packets
c_sleep(1);
}
void tox_scenario_wait_for_event(ToxNode *self, Tox_Event_Type type)
{
while (!self->seen_events[type] && tox_scenario_is_running(self)) {
tox_scenario_yield(self);
}
}
void tox_scenario_barrier_wait(ToxNode *self)
{
ToxScenario *s = self->scenario;
pthread_mutex_lock(&s->mutex);
uint32_t my_barrier_index = ++self->barrier_index;
pthread_mutex_unlock(&s->mutex);
while (tox_scenario_is_running(self)) {
// Even if all nodes have reached the barrier, we MUST yield at least once
// to ensure the runner has a chance to synchronize the snapshots for this barrier.
tox_scenario_yield(self);
pthread_mutex_lock(&s->mutex);
uint32_t reached = 0;
for (uint32_t i = 0; i < s->num_nodes; ++i) {
if (s->nodes[i]->barrier_index >= my_barrier_index || s->nodes[i]->finished) {
reached++;
}
}
const bool done = reached >= s->num_nodes;
pthread_mutex_unlock(&s->mutex);
if (done) {
break;
}
}
}
void tox_node_wait_for_self_connected(ToxNode *self)
{
WAIT_UNTIL(tox_node_is_self_connected(self));
}
void tox_node_wait_for_friend_connected(ToxNode *self, uint32_t friend_number)
{
WAIT_UNTIL(tox_node_is_friend_connected(self, friend_number));
}
static void *node_thread_wrapper(void *arg)
{
ToxNode *node = (ToxNode *)arg;
ck_assert(node != nullptr);
ToxScenario *s = node->scenario;
// Wait for the starting signal from runner
pthread_mutex_lock(&s->mutex);
s->num_ready++;
pthread_cond_signal(&s->cond_runner);
while (s->tick_count == 0 && s->run_started) {
pthread_cond_wait(&s->cond_nodes, &s->mutex);
}
pthread_mutex_unlock(&s->mutex);
// Run the actual script
srand(time(nullptr) + node->index);
node->script(node, node->script_ctx);
// After the script is done, keep polling until the scenario is over
pthread_mutex_lock(&s->mutex);
node->finished = true;
s->num_active--;
uint32_t active = s->num_active;
pthread_mutex_unlock(&s->mutex);
tox_node_log(node, "finished script, active nodes remaining: %u", active);
pthread_mutex_lock(&s->mutex);
pthread_cond_signal(&s->cond_runner);
while (s->run_started) {
uint64_t tick_at_start = s->tick_count;
s->num_ready++;
pthread_cond_signal(&s->cond_runner);
while (tick_at_start == s->tick_count && s->run_started) {
pthread_cond_wait(&s->cond_nodes, &s->mutex);
}
pthread_mutex_unlock(&s->mutex);
// 2. Update mirror outside lock
node->mirror.connection_status = tox_self_get_connection_status(node->tox);
node->mirror.finished = node->finished;
node->mirror.offline = node->offline;
if (node->mirrored_ctx != nullptr && node->script_ctx != nullptr) {
memcpy(node->mirrored_ctx, node->script_ctx, node->script_ctx_size);
}
// Poll Tox even when script is "finished"
Tox_Err_Events_Iterate ev_err;
Tox_Events *events = tox_events_iterate(node->tox, false, &ev_err);
if (events != nullptr) {
tox_dispatch_invoke(node->dispatch, events, node);
tox_events_free(events);
}
pthread_mutex_lock(&s->mutex);
}
pthread_mutex_unlock(&s->mutex);
return nullptr;
}
static Tox *create_tox_with_port_retry(Tox_Options *opts, void *log_user_data, Tox_Err_New *out_err)
{
tox_options_set_log_user_data(opts, log_user_data);
if (tox_options_get_start_port(opts) == 0 && tox_options_get_end_port(opts) == 0) {
tox_options_set_start_port(opts, TOX_PORT_DEFAULT);
tox_options_set_end_port(opts, 65535);
}
uint16_t tcp_port = tox_options_get_tcp_port(opts);
Tox *tox = nullptr;
for (int i = 0; i < 50; ++i) {
tox = tox_new(opts, out_err);
if (*out_err != TOX_ERR_NEW_PORT_ALLOC || tcp_port == 0) {
break;
}
tcp_port++;
tox_options_set_tcp_port(opts, tcp_port);
}
return tox;
}
ToxNode *tox_scenario_add_node_ex(ToxScenario *s, const char *alias, tox_node_script_cb *script, void *ctx, size_t ctx_size, const Tox_Options *options)
{
if (s->num_nodes >= MAX_NODES) {
return nullptr;
}
ToxNode *node = (ToxNode *)calloc(1, sizeof(ToxNode));
ck_assert(node != nullptr);
if (alias != nullptr) {
size_t len = strlen(alias) + 1;
node->alias = (char *)malloc(len);
ck_assert(node->alias != nullptr);
memcpy(node->alias, alias, len);
}
node->index = s->num_nodes;
node->scenario = s;
node->script = script;
node->script_ctx = ctx;
node->script_ctx_size = ctx_size;
if (ctx_size > 0) {
node->mirrored_ctx = calloc(1, ctx_size);
ck_assert(node->mirrored_ctx != nullptr);
node->mirrored_ctx_public = calloc(1, ctx_size);
ck_assert(node->mirrored_ctx_public != nullptr);
if (ctx != nullptr) {
memcpy(node->mirrored_ctx, ctx, ctx_size);
memcpy(node->mirrored_ctx_public, ctx, ctx_size);
}
}
Tox_Options *opts = tox_options_new(nullptr);
if (options != nullptr) {
tox_options_copy(opts, options);
} else {
tox_options_set_ipv6_enabled(opts, false);
tox_options_set_local_discovery_enabled(opts, false);
}
tox_options_set_log_callback(opts, framework_debug_log);
Tox_Err_New err;
node->tox = create_tox_with_port_retry(opts, node, &err);
if (err == TOX_ERR_NEW_OK) {
ck_assert(node->tox != nullptr);
node->options = tox_options_new(nullptr);
tox_options_copy(node->options, opts);
}
tox_options_free(opts);
if (err != TOX_ERR_NEW_OK) {
tox_scenario_log(s, "Failed to create Tox instance for node %s: %u", alias, err);
free(node);
return nullptr;
}
node->dispatch = tox_dispatch_new(nullptr);
tox_events_init(node->tox);
mono_time_set_current_time_callback(node->tox->mono_time, get_scenario_clock, s);
// Initial mirror population
node->mirror.connection_status = tox_self_get_connection_status(node->tox);
tox_self_get_public_key(node->tox, node->mirror.public_key);
tox_self_get_dht_id(node->tox, node->mirror.dht_id);
tox_self_get_address(node->tox, node->mirror.address);
Tox_Err_Get_Port port_err;
node->mirror.udp_port = tox_self_get_udp_port(node->tox, &port_err);
node->mirror_public = node->mirror;
s->nodes[s->num_nodes++] = node;
return node;
}
ToxNode *tox_scenario_add_node(ToxScenario *s, const char *alias, tox_node_script_cb *script, void *ctx, size_t ctx_size)
{
return tox_scenario_add_node_ex(s, alias, script, ctx, ctx_size, nullptr);
}
ToxNode *tox_scenario_get_node(ToxScenario *s, uint32_t index)
{
if (index >= s->num_nodes) {
return nullptr;
}
return s->nodes[index];
}
const char *tox_node_get_alias(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->alias;
}
uint32_t tox_node_get_index(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->index;
}
void *tox_node_get_script_ctx(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->script_ctx;
}
const void *tox_node_get_peer_ctx(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->mirrored_ctx_public;
}
Tox_Dispatch *tox_node_get_dispatch(const ToxNode *node)
{
ck_assert(node != nullptr);
return node->dispatch;
}
uint64_t tox_scenario_get_time(ToxScenario *s)
{
pthread_mutex_lock(&s->clock_mutex);
uint64_t time = s->virtual_clock;
pthread_mutex_unlock(&s->clock_mutex);
return time;
}
ToxScenarioStatus tox_scenario_run(ToxScenario *s)
{
pthread_mutex_lock(&s->mutex);
s->num_active = s->num_nodes;
s->num_ready = 0;
s->tick_count = 0;
s->run_started = true;
pthread_mutex_unlock(&s->mutex);
// Start all node threads
for (uint32_t i = 0; i < s->num_nodes; ++i) {
pthread_create(&s->nodes[i]->thread, nullptr, node_thread_wrapper, s->nodes[i]);
}
pthread_mutex_lock(&s->mutex);
uint64_t start_clock = s->virtual_clock;
uint64_t deadline = start_clock + s->timeout_ms;
while (s->num_active > 0 && s->virtual_clock < deadline) {
// 1. Wait until all nodes (including finished ones) have reached the barrier
while (s->num_ready < s->num_nodes) {
pthread_cond_wait(&s->cond_runner, &s->mutex);
}
// 2. Synchronize Snapshots
for (uint32_t i = 0; i < s->num_nodes; ++i) {
ToxNode *node = s->nodes[i];
ck_assert(node != nullptr);
node->mirror_public = node->mirror;
if (node->mirrored_ctx_public && node->mirrored_ctx) {
memcpy(node->mirrored_ctx_public, node->mirrored_ctx, node->script_ctx_size);
}
}
// 3. Advance tick and clock
s->num_ready = 0;
s->tick_count++;
pthread_mutex_lock(&s->clock_mutex);
s->virtual_clock += TOX_SCENARIO_TICK_MS;
pthread_mutex_unlock(&s->clock_mutex);
if (s->tick_count % 100 == 0) {
uint64_t tick = s->tick_count;
uint32_t active = s->num_active;
pthread_mutex_unlock(&s->mutex);
tox_scenario_log(s, "Tick %lu, active nodes: %u", (unsigned long)tick, active);
pthread_mutex_lock(&s->mutex);
}
// 4. Release nodes for the new tick
pthread_cond_broadcast(&s->cond_nodes);
}
ToxScenarioStatus result = (s->num_active == 0) ? TOX_SCENARIO_DONE : TOX_SCENARIO_TIMEOUT;
if (result == TOX_SCENARIO_TIMEOUT) {
tox_scenario_log(s, "Scenario TIMEOUT after %lu ms (virtual time)", (unsigned long)(s->virtual_clock - start_clock));
}
// Stop nodes
s->run_started = false;
pthread_cond_broadcast(&s->cond_nodes);
pthread_mutex_unlock(&s->mutex);
for (uint32_t i = 0; i < s->num_nodes; ++i) {
pthread_join(s->nodes[i]->thread, nullptr);
}
return result;
}
void tox_node_bootstrap(ToxNode *a, ToxNode *b)
{
uint8_t pk[TOX_PUBLIC_KEY_SIZE];
memcpy(pk, b->mirror_public.dht_id, TOX_PUBLIC_KEY_SIZE);
const uint16_t port = b->mirror_public.udp_port;
Tox_Err_Bootstrap err;
tox_bootstrap(a->tox, "127.0.0.1", port, pk, &err);
if (err != TOX_ERR_BOOTSTRAP_OK) {
tox_node_log(a, "Error bootstrapping from %s: %u", b->alias, err);
}
}
void tox_node_friend_add(ToxNode *a, ToxNode *b)
{
uint8_t pk[TOX_PUBLIC_KEY_SIZE];
memcpy(pk, b->mirror_public.public_key, TOX_PUBLIC_KEY_SIZE);
Tox_Err_Friend_Add err;
tox_friend_add_norequest(a->tox, pk, &err);
if (err != TOX_ERR_FRIEND_ADD_OK) {
tox_node_log(a, "Error adding friend %s: %u", b->alias, err);
}
}
void tox_node_reload(ToxNode *node)
{
ck_assert(node != nullptr);
ToxScenario *s = node->scenario;
pthread_mutex_lock(&s->mutex);
// 1. Save data
size_t size = tox_get_savedata_size(node->tox);
uint8_t *data = (uint8_t *)malloc(size);
ck_assert(data != nullptr);
tox_get_savedata(node->tox, data);
Tox *old_tox = node->tox;
Tox_Dispatch *old_dispatch = node->dispatch;
// Invalidate node state while reloading
node->tox = nullptr;
node->dispatch = nullptr;
pthread_mutex_unlock(&s->mutex);
// 2. Kill old instance
tox_dispatch_free(old_dispatch);
tox_kill(old_tox);
// 3. Create new instance from save
Tox_Options *opts = tox_options_new(nullptr);
ck_assert(opts != nullptr);
tox_options_copy(opts, node->options);
tox_options_set_savedata_type(opts, TOX_SAVEDATA_TYPE_TOX_SAVE);
tox_options_set_savedata_data(opts, data, size);
Tox_Err_New err;
Tox *new_tox = create_tox_with_port_retry(opts, node, &err);
tox_options_free(opts);
free(data);
if (err != TOX_ERR_NEW_OK) {
tox_node_log(node, "Failed to reload Tox instance: %u", err);
return;
}
ck_assert_msg(new_tox != nullptr, "tox_new said OK but returned NULL");
Tox_Dispatch *new_dispatch = tox_dispatch_new(nullptr);
tox_events_init(new_tox);
mono_time_set_current_time_callback(new_tox->mono_time, get_scenario_clock, s);
pthread_mutex_lock(&s->mutex);
node->tox = new_tox;
node->dispatch = new_dispatch;
pthread_mutex_unlock(&s->mutex);
// Re-bootstrap from siblings
for (uint32_t i = 0; i < s->num_nodes; ++i) {
if (i != node->index) {
tox_node_bootstrap(node, s->nodes[i]);
}
}
}

View File

@@ -0,0 +1,215 @@
#ifndef TOX_TEST_FRAMEWORK_H
#define TOX_TEST_FRAMEWORK_H
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "../../../toxcore/attributes.h"
#include "../../../toxcore/ccompat.h"
#include "../../../toxcore/tox.h"
#include "../../../toxcore/tox_dispatch.h"
// --- Constants ---
#define TOX_SCENARIO_TICK_MS 50
// --- Opaque Types ---
typedef struct ToxNode ToxNode;
typedef struct ToxScenario ToxScenario;
typedef enum {
TOX_SCENARIO_OK,
TOX_SCENARIO_RUNNING,
TOX_SCENARIO_DONE,
TOX_SCENARIO_ERROR,
TOX_SCENARIO_TIMEOUT
} ToxScenarioStatus;
/**
* A script function that defines the behavior of a node.
*/
typedef void tox_node_script_cb(ToxNode *self, void *ctx);
// --- Core API ---
ToxScenario *tox_scenario_new(int argc, char *const argv[], uint64_t timeout_ms);
void tox_scenario_free(ToxScenario *s);
/**
* Adds a node to the scenario and assigns it a script.
* @param ctx_size The size of the context struct to be mirrored (snapshot) at each tick.
*/
ToxNode *tox_scenario_add_node(ToxScenario *s, const char *alias, tox_node_script_cb *script, void *ctx, size_t ctx_size);
/**
* Extended version of add_node that accepts custom Tox_Options.
*/
ToxNode *tox_scenario_add_node_ex(ToxScenario *s, const char *alias, tox_node_script_cb *script, void *ctx, size_t ctx_size, const Tox_Options *options);
/**
* Returns a node by its index in the scenario.
*/
ToxNode *tox_scenario_get_node(ToxScenario *s, uint32_t index);
/**
* Runs the scenario until all nodes complete their scripts or timeout occurs.
*/
ToxScenarioStatus tox_scenario_run(ToxScenario *s);
// --- Logging API ---
/**
* Logs a message with the node's alias/index and current virtual time.
*/
void tox_node_log(ToxNode *node, const char *format, ...) GNU_PRINTF(2, 3);
/**
* Logs a message from the scenario runner with current virtual time.
*/
void tox_scenario_log(const ToxScenario *s, const char *format, ...) GNU_PRINTF(2, 3);
// --- Script API (Callable from within scripts) ---
/**
* Returns the underlying Tox instance.
*/
Tox *tox_node_get_tox(const ToxNode *node);
/**
* Returns the mirrored address of the node.
*/
void tox_node_get_address(const ToxNode *node, uint8_t *address);
/**
* Returns the scenario this node belongs to.
*/
ToxScenario *tox_node_get_scenario(const ToxNode *node);
/**
* Returns the alias of the node.
*/
const char *tox_node_get_alias(const ToxNode *node);
/**
* Returns the index of the node in the scenario.
*/
uint32_t tox_node_get_index(const ToxNode *node);
/**
* Returns the script context associated with this node.
*/
void *tox_node_get_script_ctx(const ToxNode *node);
/**
* Returns a read-only snapshot of a peer's context.
* The snapshot is updated at every tick.
*/
const void *tox_node_get_peer_ctx(const ToxNode *node);
/**
* Returns the event dispatcher associated with this node.
*/
Tox_Dispatch *tox_node_get_dispatch(const ToxNode *node);
/**
* Predicate helpers for common wait conditions.
*/
bool tox_node_is_self_connected(const ToxNode *node);
bool tox_node_is_friend_connected(const ToxNode *node, uint32_t friend_number);
Tox_Connection tox_node_get_connection_status(const ToxNode *node);
Tox_Connection tox_node_get_friend_connection_status(const ToxNode *node, uint32_t friend_number);
/**
* Conference helpers.
*/
uint32_t tox_node_get_conference_peer_count(const ToxNode *node, uint32_t conference_number);
/**
* Network simulation.
*/
void tox_node_set_offline(ToxNode *node, bool offline);
bool tox_node_is_offline(const ToxNode *node);
/**
* Returns true if the node has finished its script.
*/
bool tox_node_is_finished(const ToxNode *node);
/**
* High-level predicates for specific state changes.
*/
bool tox_node_friend_name_is(const ToxNode *node, uint32_t friend_number, const uint8_t *name, size_t length);
bool tox_node_friend_status_message_is(const ToxNode *node, uint32_t friend_number, const uint8_t *msg, size_t length);
bool tox_node_friend_typing_is(const ToxNode *node, uint32_t friend_number, bool is_typing);
/**
* Yields execution to the next tick.
*/
void tox_scenario_yield(ToxNode *self);
/**
* Returns the current virtual time in milliseconds.
*/
uint64_t tox_scenario_get_time(ToxScenario *s);
/**
* Returns true if the scenario is still running.
*/
bool tox_scenario_is_running(ToxNode *self);
/**
* Blocks until a condition is true. Polls every tick.
*/
#define WAIT_UNTIL(cond) do { while(!(cond) && tox_scenario_is_running(self)) { tox_scenario_yield(self); } } while(0)
/**
* Blocks until a specific event is observed.
*/
void tox_scenario_wait_for_event(ToxNode *self, Tox_Event_Type type);
#define WAIT_FOR_EVENT(type) tox_scenario_wait_for_event(self, type)
/**
* Synchronization barriers.
* Blocks until all active nodes have reached this barrier.
*/
void tox_scenario_barrier_wait(ToxNode *self);
/**
* High-level wait helpers.
*/
void tox_node_wait_for_self_connected(ToxNode *self);
void tox_node_wait_for_friend_connected(ToxNode *self, uint32_t friend_number);
// --- High-Level Helpers ---
void tox_node_bootstrap(ToxNode *a, ToxNode *b);
void tox_node_friend_add(ToxNode *a, ToxNode *b);
/**
* Reloads a node from its own savedata.
* This simulates a client restart.
*/
void tox_node_reload(ToxNode *node);
#ifndef ck_assert
#define ck_assert(ok) do { \
if (!(ok)) { \
fprintf(stderr, "%s:%d: failed `%s'\n", __FILE__, __LINE__, #ok); \
exit(7); \
} \
} while (0)
#define ck_assert_msg(ok, ...) do { \
if (!(ok)) { \
fprintf(stderr, "%s:%d: failed `%s': ", __FILE__, __LINE__, #ok); \
fprintf(stderr, __VA_ARGS__); \
fprintf(stderr, "\n"); \
exit(7); \
} \
} while (0)
#endif
#endif // TOX_TEST_FRAMEWORK_H