Foundry/data/tests/test_data.c

449 lines
12 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
#include "../../lib/test.h"
#include "../data.h"
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
/* ============================================================= */
/* Sample Tests */
/* ============================================================= */
static void test_sample_create(void) {
fd_sample* s = fd_sample_new();
ASSERT_NOT_NULL(s);
ASSERT_INT_EQ(0, s->num_fields);
fd_sample_free(s);
}
static void test_sample_set_get(void) {
fd_sample* s = fd_sample_new();
fd_sample_set(s, "text", "hello world");
fd_sample_set(s, "label", "positive");
ASSERT_INT_EQ(2, s->num_fields);
const char* text = fd_sample_get(s, "text");
const char* label = fd_sample_get(s, "label");
ASSERT_NOT_NULL(text);
ASSERT_NOT_NULL(label);
ASSERT_STR_EQ("hello world", text);
ASSERT_STR_EQ("positive", label);
ASSERT_NULL(fd_sample_get(s, "nonexistent"));
fd_sample_free(s);
}
static void test_sample_has(void) {
fd_sample* s = fd_sample_new();
fd_sample_set(s, "key", "value");
ASSERT_TRUE(fd_sample_has(s, "key"));
ASSERT_FALSE(fd_sample_has(s, "missing"));
fd_sample_free(s);
}
static void test_sample_update(void) {
fd_sample* s = fd_sample_new();
fd_sample_set(s, "key", "original");
ASSERT_STR_EQ("original", fd_sample_get(s, "key"));
fd_sample_set(s, "key", "updated");
ASSERT_STR_EQ("updated", fd_sample_get(s, "key"));
ASSERT_INT_EQ(1, s->num_fields);
fd_sample_free(s);
}
static void test_sample_clone(void) {
fd_sample* s = fd_sample_new();
fd_sample_set(s, "a", "1");
fd_sample_set(s, "b", "2");
fd_sample* clone = fd_sample_clone(s);
ASSERT_NOT_NULL(clone);
ASSERT_INT_EQ(2, clone->num_fields);
ASSERT_STR_EQ("1", fd_sample_get(clone, "a"));
ASSERT_STR_EQ("2", fd_sample_get(clone, "b"));
/* Modify original, clone should be unaffected */
fd_sample_set(s, "a", "changed");
ASSERT_STR_EQ("1", fd_sample_get(clone, "a"));
fd_sample_free(s);
fd_sample_free(clone);
}
/* ============================================================= */
/* Dataset Tests */
/* ============================================================= */
static void test_dataset_create(void) {
fd_dataset* ds = fd_new();
ASSERT_NOT_NULL(ds);
ASSERT_INT_EQ(0, fd_len(ds));
fd_free(ds);
}
static void test_dataset_add_samples(void) {
fd_dataset* ds = fd_new();
fd_sample* s1 = fd_sample_new();
fd_sample_set(s1, "text", "sample 1");
fd_add_sample(ds, s1);
fd_sample* s2 = fd_sample_new();
fd_sample_set(s2, "text", "sample 2");
fd_add_sample(ds, s2);
ASSERT_INT_EQ(2, fd_len(ds));
fd_sample* retrieved = fd_get(ds, 0);
ASSERT_NOT_NULL(retrieved);
ASSERT_STR_EQ("sample 1", fd_sample_get(retrieved, "text"));
retrieved = fd_get(ds, 1);
ASSERT_STR_EQ("sample 2", fd_sample_get(retrieved, "text"));
ASSERT_NULL(fd_get(ds, 2));
ASSERT_NULL(fd_get(ds, -1));
fd_free(ds);
}
static void test_dataset_shuffle(void) {
fd_dataset* ds = fd_new();
/* Add 100 samples */
for (int i = 0; i < 100; i++) {
fd_sample* s = fd_sample_new();
char val[16];
snprintf(val, sizeof(val), "%d", i);
fd_sample_set(s, "idx", val);
fd_add_sample(ds, s);
}
fd_seed(42);
fd_shuffle(ds);
/* After shuffle, order should likely be different */
/* At minimum, check that we still have all 100 samples */
ASSERT_INT_EQ(100, fd_len(ds));
/* Check all values are still present (counting 0-99) */
int found[100] = {0};
for (int i = 0; i < 100; i++) {
const char* val = fd_sample_get(fd_get(ds, i), "idx");
int idx = atoi(val);
found[idx] = 1;
}
for (int i = 0; i < 100; i++) {
ASSERT_TRUE(found[i]);
}
fd_free(ds);
}
static void test_dataset_split(void) {
fd_dataset* ds = fd_new();
for (int i = 0; i < 100; i++) {
fd_sample* s = fd_sample_new();
char val[16];
snprintf(val, sizeof(val), "%d", i);
fd_sample_set(s, "idx", val);
fd_add_sample(ds, s);
}
fd_dataset* train = NULL;
fd_dataset* test = NULL;
fd_split(ds, 0.8f, &train, &test);
ASSERT_NOT_NULL(train);
ASSERT_NOT_NULL(test);
ASSERT_INT_EQ(80, fd_len(train));
ASSERT_INT_EQ(20, fd_len(test));
fd_free(ds);
fd_free(train);
fd_free(test);
}
/* ============================================================= */
/* JSONL Loading Tests */
/* ============================================================= */
static void test_load_jsonl(void) {
const char* path = "/tmp/test_foundry_data.jsonl";
/* Write test file */
FILE* f = fopen(path, "w");
fprintf(f, "{\"text\": \"hello\", \"label\": \"positive\"}\n");
fprintf(f, "{\"text\": \"goodbye\", \"label\": \"negative\"}\n");
fprintf(f, "{\"text\": \"test\", \"label\": \"neutral\"}\n");
fclose(f);
fd_dataset* ds = fd_load_jsonl(path);
ASSERT_NOT_NULL(ds);
ASSERT_INT_EQ(3, fd_len(ds));
fd_sample* s0 = fd_get(ds, 0);
ASSERT_STR_EQ("hello", fd_sample_get(s0, "text"));
ASSERT_STR_EQ("positive", fd_sample_get(s0, "label"));
fd_sample* s1 = fd_get(ds, 1);
ASSERT_STR_EQ("goodbye", fd_sample_get(s1, "text"));
fd_free(ds);
unlink(path);
}
static void test_load_jsonl_with_numbers(void) {
const char* path = "/tmp/test_foundry_data2.jsonl";
FILE* f = fopen(path, "w");
fprintf(f, "{\"value\": 42, \"score\": 3.14}\n");
fprintf(f, "{\"value\": -10, \"score\": 2.71}\n");
fclose(f);
fd_dataset* ds = fd_load_jsonl(path);
ASSERT_NOT_NULL(ds);
ASSERT_INT_EQ(2, fd_len(ds));
/* Numbers are stored as strings */
ASSERT_STR_EQ("42", fd_sample_get(fd_get(ds, 0), "value"));
ASSERT_STR_EQ("3.14", fd_sample_get(fd_get(ds, 0), "score"));
fd_free(ds);
unlink(path);
}
static void test_load_jsonl_empty_lines(void) {
const char* path = "/tmp/test_foundry_data3.jsonl";
FILE* f = fopen(path, "w");
fprintf(f, "{\"a\": \"1\"}\n");
fprintf(f, "\n");
fprintf(f, " \n");
fprintf(f, "{\"a\": \"2\"}\n");
fclose(f);
fd_dataset* ds = fd_load_jsonl(path);
ASSERT_NOT_NULL(ds);
ASSERT_INT_EQ(2, fd_len(ds));
fd_free(ds);
unlink(path);
}
static void test_load_nonexistent_file(void) {
fd_dataset* ds = fd_load_jsonl("/nonexistent/path/data.jsonl");
ASSERT_NULL(ds);
}
/* ============================================================= */
/* Data Loader Tests */
/* ============================================================= */
static void test_loader_create(void) {
fd_dataset* ds = fd_new();
for (int i = 0; i < 10; i++) {
fd_sample* s = fd_sample_new();
fd_sample_set(s, "x", "y");
fd_add_sample(ds, s);
}
fd_loader* loader = fd_loader_new(ds, 4, false);
ASSERT_NOT_NULL(loader);
ASSERT_INT_EQ(3, fd_loader_num_batches(loader)); /* ceil(10/4) = 3 */
fd_loader_free(loader);
fd_free(ds);
}
static void test_loader_iterate(void) {
fd_dataset* ds = fd_new();
for (int i = 0; i < 10; i++) {
fd_sample* s = fd_sample_new();
char val[16];
snprintf(val, sizeof(val), "%d", i);
fd_sample_set(s, "idx", val);
fd_add_sample(ds, s);
}
fd_loader* loader = fd_loader_new(ds, 4, false);
/* First batch: 4 samples */
fd_batch* batch = fd_loader_next(loader);
ASSERT_NOT_NULL(batch);
ASSERT_INT_EQ(4, batch->size);
ASSERT_STR_EQ("0", fd_sample_get(batch->samples[0], "idx"));
fd_batch_free(batch);
/* Second batch: 4 samples */
batch = fd_loader_next(loader);
ASSERT_NOT_NULL(batch);
ASSERT_INT_EQ(4, batch->size);
ASSERT_STR_EQ("4", fd_sample_get(batch->samples[0], "idx"));
fd_batch_free(batch);
/* Third batch: 2 samples (remaining) */
batch = fd_loader_next(loader);
ASSERT_NOT_NULL(batch);
ASSERT_INT_EQ(2, batch->size);
ASSERT_STR_EQ("8", fd_sample_get(batch->samples[0], "idx"));
fd_batch_free(batch);
/* No more batches */
batch = fd_loader_next(loader);
ASSERT_NULL(batch);
fd_loader_free(loader);
fd_free(ds);
}
static void test_loader_reset(void) {
fd_dataset* ds = fd_new();
for (int i = 0; i < 5; i++) {
fd_sample* s = fd_sample_new();
fd_sample_set(s, "x", "y");
fd_add_sample(ds, s);
}
fd_loader* loader = fd_loader_new(ds, 2, false);
/* Exhaust the loader */
while (fd_loader_next(loader)) {}
/* Reset and check we can iterate again */
fd_loader_reset(loader);
fd_batch* batch = fd_loader_next(loader);
ASSERT_NOT_NULL(batch);
ASSERT_INT_EQ(2, batch->size);
fd_batch_free(batch);
fd_loader_free(loader);
fd_free(ds);
}
static void test_loader_shuffle(void) {
fd_dataset* ds = fd_new();
for (int i = 0; i < 100; i++) {
fd_sample* s = fd_sample_new();
char val[16];
snprintf(val, sizeof(val), "%d", i);
fd_sample_set(s, "idx", val);
fd_add_sample(ds, s);
}
fd_seed(123);
fd_loader* loader = fd_loader_new(ds, 10, true);
/* First batch with shuffle - unlikely to start with "0" */
fd_batch* batch = fd_loader_next(loader);
ASSERT_NOT_NULL(batch);
ASSERT_INT_EQ(10, batch->size);
/* Just verify we get 100 samples total in random order */
int count = batch->size;
fd_batch_free(batch);
while ((batch = fd_loader_next(loader)) != NULL) {
count += batch->size;
fd_batch_free(batch);
}
ASSERT_INT_EQ(100, count);
fd_loader_free(loader);
fd_free(ds);
}
/* ============================================================= */
/* Edge Cases */
/* ============================================================= */
static void test_null_safety(void) {
ASSERT_INT_EQ(0, fd_len(NULL));
ASSERT_NULL(fd_get(NULL, 0));
ASSERT_NULL(fd_sample_get(NULL, "key"));
ASSERT_FALSE(fd_sample_has(NULL, "key"));
ASSERT_NULL(fd_sample_clone(NULL));
ASSERT_NULL(fd_loader_next(NULL));
ASSERT_INT_EQ(0, fd_loader_num_batches(NULL));
fd_sample_free(NULL); /* Should not crash */
fd_free(NULL); /* Should not crash */
fd_loader_free(NULL); /* Should not crash */
fd_batch_free(NULL); /* Should not crash */
fd_shuffle(NULL); /* Should not crash */
fd_loader_reset(NULL); /* Should not crash */
ASSERT_TRUE(1);
}
static void test_empty_dataset_loader(void) {
fd_dataset* ds = fd_new();
fd_loader* loader = fd_loader_new(ds, 4, false);
ASSERT_NOT_NULL(loader);
ASSERT_INT_EQ(0, fd_loader_num_batches(loader));
fd_batch* batch = fd_loader_next(loader);
ASSERT_NULL(batch);
fd_loader_free(loader);
fd_free(ds);
}
/* ============================================================= */
/* Main */
/* ============================================================= */
int main(void) {
printf("\n Running data tests...\n\n");
/* Sample tests */
RUN_TEST(sample_create);
RUN_TEST(sample_set_get);
RUN_TEST(sample_has);
RUN_TEST(sample_update);
RUN_TEST(sample_clone);
/* Dataset tests */
RUN_TEST(dataset_create);
RUN_TEST(dataset_add_samples);
RUN_TEST(dataset_shuffle);
RUN_TEST(dataset_split);
/* JSONL loading tests */
RUN_TEST(load_jsonl);
RUN_TEST(load_jsonl_with_numbers);
RUN_TEST(load_jsonl_empty_lines);
RUN_TEST(load_nonexistent_file);
/* Data loader tests */
RUN_TEST(loader_create);
RUN_TEST(loader_iterate);
RUN_TEST(loader_reset);
RUN_TEST(loader_shuffle);
/* Edge cases */
RUN_TEST(null_safety);
RUN_TEST(empty_dataset_loader);
TEST_REPORT();
return __ft_failed > 0 ? 1 : 0;
}