Foundry/swarm/tests/test_swarm.c

363 lines
10 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/*
* Swarm Module Tests
*
* Tests for protocol, config, compression, and basic functionality.
* Note: Full integration tests require running coordinator and workers.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <assert.h>
#include "../protocol.h"
#include "../config.h"
#include "../network.h"
#include "../coordinator.h"
#include "../worker.h"
#include "../../tensor/tensor.h"
static int tests_passed = 0;
static int tests_failed = 0;
#define TEST(name) static void test_##name(void)
#define RUN_TEST(name) do { \
printf(" Testing %s... ", #name); \
fflush(stdout); \
test_##name(); \
tests_passed++; \
printf("PASSED\n"); \
} while(0)
#define ASSERT(cond) do { \
if (!(cond)) { \
printf("FAILED\n"); \
printf(" Assertion failed: %s\n", #cond); \
printf(" At %s:%d\n", __FILE__, __LINE__); \
tests_failed++; \
return; \
} \
} while(0)
#define ASSERT_EQ(a, b) ASSERT((a) == (b))
#define ASSERT_NE(a, b) ASSERT((a) != (b))
#define ASSERT_NEAR(a, b, eps) ASSERT(fabs((a) - (b)) < (eps))
#define ASSERT_STR_EQ(a, b) ASSERT(strcmp((a), (b)) == 0)
/* ============================================================= */
/* Protocol Tests */
/* ============================================================= */
TEST(fp16_conversion) {
/* Test FP16 round-trip conversion */
float values[] = {0.0f, 1.0f, -1.0f, 0.5f, 100.0f, 0.001f};
int n = sizeof(values) / sizeof(values[0]);
uint16_t* fp16 = malloc(n * sizeof(uint16_t));
float* result = malloc(n * sizeof(float));
sw_fp32_to_fp16(values, fp16, n);
sw_fp16_to_fp32(fp16, result, n);
for (int i = 0; i < n; i++) {
/* FP16 has limited precision */
float eps = fabs(values[i]) * 0.01f + 0.001f;
ASSERT_NEAR(values[i], result[i], eps);
}
free(fp16);
free(result);
}
TEST(topk_compression) {
/* Test TopK compression */
float values[] = {0.1f, 5.0f, 0.2f, -3.0f, 0.3f, 2.0f, 0.0f, -1.0f};
int n = 8;
int k = 3;
uint32_t* indices = malloc(k * sizeof(uint32_t));
float* topk_vals = malloc(k * sizeof(float));
sw_topk_compress(values, n, k, indices, topk_vals);
/* Should find the top 3 by absolute value: 5.0 (idx 1), -3.0 (idx 3), 2.0 (idx 5) */
/* Values should be sorted by magnitude */
ASSERT_NEAR(topk_vals[0], 5.0f, 0.001f);
ASSERT_EQ(indices[0], 1);
ASSERT_NEAR(topk_vals[1], -3.0f, 0.001f);
ASSERT_EQ(indices[1], 3);
ASSERT_NEAR(topk_vals[2], 2.0f, 0.001f);
ASSERT_EQ(indices[2], 5);
/* Test decompression */
float* decompressed = calloc(n, sizeof(float));
sw_topk_decompress(indices, topk_vals, k, decompressed, n);
ASSERT_NEAR(decompressed[1], 5.0f, 0.001f);
ASSERT_NEAR(decompressed[3], -3.0f, 0.001f);
ASSERT_NEAR(decompressed[5], 2.0f, 0.001f);
ASSERT_NEAR(decompressed[0], 0.0f, 0.001f); /* Not in top-k */
free(indices);
free(topk_vals);
free(decompressed);
}
TEST(message_alloc_free) {
/* Test message allocation and validation */
uint8_t* msg = sw_msg_alloc(SW_MSG_REGISTER, 100);
ASSERT_NE(msg, NULL);
sw_msg_header* hdr = sw_msg_get_header(msg);
ASSERT_EQ(hdr->type, SW_MSG_REGISTER);
ASSERT_EQ(hdr->length, 100);
ASSERT(sw_msg_validate(msg, SW_HEADER_SIZE + 100));
sw_msg_free(msg);
}
TEST(hardware_detection) {
/* Test hardware profile detection */
sw_hardware_profile profile = sw_detect_profile();
/* Should return a valid profile */
ASSERT(profile >= SW_PROFILE_RPI3 && profile <= SW_PROFILE_UNKNOWN);
/* Test batch size recommendation */
uint32_t batch = sw_profile_batch_size(profile);
ASSERT(batch >= 2 && batch <= 256);
}
/* ============================================================= */
/* Config Tests */
/* ============================================================= */
TEST(config_parse) {
/* Create a test config file */
const char* config_path = "/tmp/test_swarm_config.toml";
FILE* f = fopen(config_path, "w");
ASSERT_NE(f, NULL);
fprintf(f, "# Test config\n");
fprintf(f, "[network]\n");
fprintf(f, "port = 7100\n");
fprintf(f, "host = \"localhost\"\n");
fprintf(f, "\n");
fprintf(f, "[training]\n");
fprintf(f, "learning_rate = 1e-4\n");
fprintf(f, "batch_size = 32\n");
fprintf(f, "enabled = true\n");
fclose(f);
/* Load and test */
sw_config* cfg = sw_config_load(config_path);
ASSERT_NE(cfg, NULL);
ASSERT_EQ(sw_config_get_int(cfg, "network", "port", 0), 7100);
ASSERT_STR_EQ(sw_config_get_str(cfg, "network", "host", ""), "localhost");
ASSERT_NEAR(sw_config_get_float(cfg, "training", "learning_rate", 0), 1e-4f, 1e-6f);
ASSERT_EQ(sw_config_get_int(cfg, "training", "batch_size", 0), 32);
ASSERT_EQ(sw_config_get_bool(cfg, "training", "enabled", false), true);
/* Test defaults */
ASSERT_EQ(sw_config_get_int(cfg, "network", "nonexistent", 42), 42);
ASSERT_EQ(sw_config_has_key(cfg, "network", "port"), true);
ASSERT_EQ(sw_config_has_key(cfg, "network", "nonexistent"), false);
sw_config_free(cfg);
remove(config_path);
}
/* ============================================================= */
/* Coordinator Tests */
/* ============================================================= */
TEST(coordinator_default_config) {
sw_coordinator_config config = sw_coordinator_default_config();
ASSERT_EQ(config.port, SW_DEFAULT_PORT);
ASSERT(config.learning_rate > 0);
ASSERT(config.beta1 > 0 && config.beta1 < 1);
ASSERT(config.beta2 > 0 && config.beta2 < 1);
ASSERT(config.min_workers > 0);
}
TEST(coordinator_create_free) {
/* Create dummy weights */
int64_t shape[] = {64, 64};
tm_tensor* w1 = tm_randn(shape, 2, TM_F32);
tm_tensor* weights[] = {w1};
sw_coordinator_config config = sw_coordinator_default_config();
config.min_workers = 1;
sw_coordinator* coord = sw_coordinator_new(weights, 1, &config);
ASSERT_NE(coord, NULL);
ASSERT_EQ(sw_coordinator_get_step(coord), 0);
ASSERT_EQ(sw_coordinator_num_workers(coord), 0);
sw_coordinator_free(coord);
tm_free(w1);
}
/* ============================================================= */
/* Worker Tests */
/* ============================================================= */
TEST(worker_default_config) {
sw_worker_config config = sw_worker_default_config();
ASSERT_EQ(config.coordinator_port, SW_DEFAULT_PORT);
ASSERT_STR_EQ(config.coordinator_host, "localhost");
ASSERT(config.reconnect_attempts > 0);
}
TEST(worker_create_free) {
sw_worker_config config = sw_worker_default_config();
strcpy(config.worker_id, "test-worker");
sw_worker* worker = sw_worker_new(&config);
ASSERT_NE(worker, NULL);
ASSERT_STR_EQ(sw_worker_get_id(worker), "test-worker");
ASSERT_EQ((int)sw_worker_get_state(worker), (int)SW_WSTATE_DISCONNECTED);
ASSERT_EQ(sw_worker_is_connected(worker), false);
sw_worker_free(worker);
}
TEST(worker_auto_id) {
sw_worker_config config = sw_worker_default_config();
/* Leave worker_id empty */
sw_worker* worker = sw_worker_new(&config);
ASSERT_NE(worker, NULL);
/* Should auto-generate an ID */
const char* id = sw_worker_get_id(worker);
ASSERT_NE(id, NULL);
ASSERT(strlen(id) > 0);
sw_worker_free(worker);
}
/* ============================================================= */
/* Tensor In-place Operations Tests */
/* ============================================================= */
TEST(tensor_zeros_like) {
int64_t shape[] = {3, 4};
tm_tensor* t = tm_randn(shape, 2, TM_F32);
tm_tensor* z = tm_zeros_like(t);
ASSERT_NE(z, NULL);
ASSERT_EQ(z->shape[0], 3);
ASSERT_EQ(z->shape[1], 4);
ASSERT_EQ(tm_numel(z), 12);
/* All elements should be zero */
float* data = tm_data_f32(z);
for (int i = 0; i < 12; i++) {
ASSERT_NEAR(data[i], 0.0f, 1e-6f);
}
tm_free(t);
tm_free(z);
}
TEST(tensor_fill_inplace) {
int64_t shape[] = {2, 3};
tm_tensor* t = tm_zeros(shape, 2, TM_F32);
tm_fill_(t, 3.14f);
float* data = tm_data_f32(t);
for (int i = 0; i < 6; i++) {
ASSERT_NEAR(data[i], 3.14f, 1e-6f);
}
tm_free(t);
}
TEST(tensor_add_inplace) {
int64_t shape[] = {4};
tm_tensor* a = tm_full(shape, 1, TM_F32, 1.0);
tm_tensor* b = tm_full(shape, 1, TM_F32, 2.0);
tm_add_(a, b);
float* data = tm_data_f32(a);
for (int i = 0; i < 4; i++) {
ASSERT_NEAR(data[i], 3.0f, 1e-6f);
}
tm_free(a);
tm_free(b);
}
TEST(tensor_scale_inplace) {
int64_t shape[] = {3};
tm_tensor* t = tm_full(shape, 1, TM_F32, 2.0);
tm_scale_(t, 0.5f);
float* data = tm_data_f32(t);
for (int i = 0; i < 3; i++) {
ASSERT_NEAR(data[i], 1.0f, 1e-6f);
}
tm_free(t);
}
/* ============================================================= */
/* Main */
/* ============================================================= */
int main(void) {
printf("\n");
printf("===========================================\n");
printf(" Foundry Swarm Tests\n");
printf("===========================================\n\n");
printf("Protocol Tests:\n");
RUN_TEST(fp16_conversion);
RUN_TEST(topk_compression);
RUN_TEST(message_alloc_free);
RUN_TEST(hardware_detection);
printf("\n");
printf("Config Tests:\n");
RUN_TEST(config_parse);
printf("\n");
printf("Coordinator Tests:\n");
RUN_TEST(coordinator_default_config);
RUN_TEST(coordinator_create_free);
printf("\n");
printf("Worker Tests:\n");
RUN_TEST(worker_default_config);
RUN_TEST(worker_create_free);
RUN_TEST(worker_auto_id);
printf("\n");
printf("Tensor In-place Tests:\n");
RUN_TEST(tensor_zeros_like);
RUN_TEST(tensor_fill_inplace);
RUN_TEST(tensor_add_inplace);
RUN_TEST(tensor_scale_inplace);
printf("\n");
printf("===========================================\n");
printf(" Results: %d passed, %d failed\n", tests_passed, tests_failed);
printf("===========================================\n\n");
return tests_failed > 0 ? 1 : 0;
}