You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
14 KiB
412 lines
14 KiB
#include "timeout_heap.h" |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <assert.h> |
|
#include <time.h> |
|
#include <stdint.h> |
|
|
|
#define TEST_ASSERT(cond, msg) do { \ |
|
if (!(cond)) { \ |
|
fprintf(stderr, "FAIL: %s:%d: %s\n", __FILE__, __LINE__, (msg)); \ |
|
exit(1); \ |
|
} \ |
|
} while (0) |
|
|
|
// Element structure for large random test |
|
typedef struct { |
|
uint64_t expiration; |
|
void *data; |
|
int cancelled; |
|
} HeapElement; |
|
|
|
// Helper: compare two entries (for qsort) |
|
static int compare_entries(const void *a, const void *b) { |
|
const TimeoutEntry *ea = (const TimeoutEntry *)a; |
|
const TimeoutEntry *eb = (const TimeoutEntry *)b; |
|
if (ea->expiration < eb->expiration) return -1; |
|
if (ea->expiration > eb->expiration) return 1; |
|
return 0; |
|
} |
|
|
|
// Test 1: basic operations |
|
static void test_basic(void) { |
|
printf("Test 1: basic operations...\n"); |
|
TimeoutHeap *h = timeout_heap_create(10); |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
TEST_ASSERT(h->size == 0, "initial size zero"); |
|
|
|
// Push one element |
|
int data1 = 42; |
|
int ret = timeout_heap_push(h, 100, &data1); |
|
TEST_ASSERT(ret == 0, "push success"); |
|
TEST_ASSERT(h->size == 1, "size after push"); |
|
|
|
// Peek |
|
TimeoutEntry entry; |
|
ret = timeout_heap_peek(h, &entry); |
|
TEST_ASSERT(ret == 0, "peek success"); |
|
TEST_ASSERT(entry.expiration == 100, "peek expiration"); |
|
TEST_ASSERT(entry.data == &data1, "peek data"); |
|
TEST_ASSERT(entry.deleted == 0, "peek deleted flag"); |
|
|
|
// Pop |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop success"); |
|
TEST_ASSERT(entry.expiration == 100, "pop expiration"); |
|
TEST_ASSERT(entry.data == &data1, "pop data"); |
|
TEST_ASSERT(h->size == 0, "size after pop"); |
|
|
|
// Pop empty |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == -1, "pop empty returns -1"); |
|
|
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
// Test 2: ordering with random values |
|
static void test_ordering(size_t num_elements) { |
|
printf("Test 2: ordering with %zu elements...\n", num_elements); |
|
TimeoutHeap *h = timeout_heap_create(4); // small initial capacity |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
|
|
// Generate random expirations and store them |
|
TimeoutEntry *expected = malloc(num_elements * sizeof(TimeoutEntry)); |
|
TEST_ASSERT(expected != NULL, "alloc expected array"); |
|
|
|
srand((unsigned int)time(NULL)); |
|
for (size_t i = 0; i < num_elements; i++) { |
|
// Generate unique expiration to avoid ties |
|
uint64_t exp = i * 1000000ULL + (rand() % 1000000); |
|
int *data = malloc(sizeof(int)); |
|
*data = (int)i; |
|
int ret = timeout_heap_push(h, exp, data); |
|
TEST_ASSERT(ret == 0, "push success"); |
|
expected[i].expiration = exp; |
|
expected[i].data = data; |
|
expected[i].deleted = 0; |
|
} |
|
|
|
// Sort expected by expiration |
|
qsort(expected, num_elements, sizeof(TimeoutEntry), compare_entries); |
|
|
|
// Pop and verify order |
|
for (size_t i = 0; i < num_elements; i++) { |
|
TimeoutEntry entry; |
|
int ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop success"); |
|
TEST_ASSERT(entry.expiration == expected[i].expiration, "expiration order"); |
|
TEST_ASSERT(entry.data == expected[i].data, "data matches"); |
|
TEST_ASSERT(entry.deleted == 0, "not deleted"); |
|
free(entry.data); // cleanup |
|
} |
|
|
|
// Heap should be empty |
|
TimeoutEntry entry; |
|
int ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == -1, "heap empty after all pops"); |
|
|
|
free(expected); |
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
// Test 3: cancel operation |
|
static void test_cancel(void) { |
|
printf("Test 3: cancel operation...\n"); |
|
TimeoutHeap *h = timeout_heap_create(10); |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
|
|
int data1 = 1, data2 = 2, data3 = 3; |
|
timeout_heap_push(h, 100, &data1); |
|
timeout_heap_push(h, 200, &data2); |
|
timeout_heap_push(h, 300, &data3); |
|
|
|
// Cancel middle element |
|
int ret = timeout_heap_cancel(h, 200, &data2); |
|
TEST_ASSERT(ret == 0, "cancel success"); |
|
|
|
// Pop and verify we get data1 then data3, not data2 |
|
TimeoutEntry entry; |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop 1 success"); |
|
TEST_ASSERT(entry.expiration == 100, "first expiration"); |
|
TEST_ASSERT(entry.data == &data1, "first data"); |
|
|
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop 2 success"); |
|
TEST_ASSERT(entry.expiration == 300, "second expiration"); |
|
TEST_ASSERT(entry.data == &data3, "second data"); |
|
|
|
// Heap empty |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == -1, "heap empty"); |
|
|
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
// Test 4: duplicate expiration times |
|
static void test_duplicate_expirations(void) { |
|
printf("Test 4: duplicate expirations...\n"); |
|
TimeoutHeap *h = timeout_heap_create(10); |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
|
|
int data1 = 1, data2 = 2, data3 = 3; |
|
// Push same expiration for data1 and data2 |
|
timeout_heap_push(h, 100, &data1); |
|
timeout_heap_push(h, 100, &data2); |
|
timeout_heap_push(h, 200, &data3); |
|
|
|
// Pop should give both items with expiration 100 (order not guaranteed) |
|
TimeoutEntry entry; |
|
int ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "first pop success"); |
|
TEST_ASSERT(entry.expiration == 100, "first pop expiration"); |
|
// Record which data pointer we got |
|
void *first_data = entry.data; |
|
TEST_ASSERT(first_data == &data1 || first_data == &data2, "first pop data matches one of duplicates"); |
|
|
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "second pop success"); |
|
TEST_ASSERT(entry.expiration == 100, "second pop expiration"); |
|
void *second_data = entry.data; |
|
TEST_ASSERT(second_data == &data1 || second_data == &data2, "second pop data matches one of duplicates"); |
|
TEST_ASSERT(first_data != second_data, "two different data pointers"); |
|
|
|
// Third pop should give expiration 200 |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "third pop success"); |
|
TEST_ASSERT(entry.expiration == 200, "third pop expiration"); |
|
TEST_ASSERT(entry.data == &data3, "third pop data"); |
|
|
|
// Heap empty |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == -1, "heap empty"); |
|
|
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
// Test 5: deleted flag handling (multiple deleted at root) |
|
static void test_deleted_root(void) { |
|
printf("Test 5: deleted root handling...\n"); |
|
TimeoutHeap *h = timeout_heap_create(10); |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
|
|
int data1 = 1, data2 = 2, data3 = 3, data4 = 4; |
|
timeout_heap_push(h, 100, &data1); |
|
timeout_heap_push(h, 50, &data2); // earliest |
|
timeout_heap_push(h, 150, &data3); |
|
timeout_heap_push(h, 75, &data4); |
|
|
|
// Cancel the earliest (root) |
|
timeout_heap_cancel(h, 50, &data2); |
|
|
|
// Peek should skip deleted root and give next earliest (75) |
|
TimeoutEntry entry; |
|
int ret = timeout_heap_peek(h, &entry); |
|
TEST_ASSERT(ret == 0, "peek success after root deleted"); |
|
TEST_ASSERT(entry.expiration == 75, "peek expiration after skip"); |
|
TEST_ASSERT(entry.data == &data4, "peek data after skip"); |
|
|
|
// Pop should also skip deleted root |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop success after root deleted"); |
|
TEST_ASSERT(entry.expiration == 75, "pop expiration after skip"); |
|
|
|
// Next pop should be 100 |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop second"); |
|
TEST_ASSERT(entry.expiration == 100, "second pop expiration"); |
|
|
|
// Cancel 150, then push another earlier |
|
timeout_heap_cancel(h, 150, &data3); |
|
int data5 = 5; |
|
timeout_heap_push(h, 60, &data5); // earlier than 150 but after 100 |
|
|
|
// Pop should give 60 |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop after new push"); |
|
TEST_ASSERT(entry.expiration == 60, "expiration of newly pushed"); |
|
|
|
// Heap empty (150 deleted) |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == -1, "heap empty after deleted remaining"); |
|
|
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
// Test 6: capacity growth |
|
static void test_growth(void) { |
|
printf("Test 6: capacity growth...\n"); |
|
TimeoutHeap *h = timeout_heap_create(2); // tiny capacity |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
TEST_ASSERT(h->capacity == 2, "initial capacity"); |
|
|
|
// Push 10 elements |
|
for (int i = 0; i < 10; i++) { |
|
int ret = timeout_heap_push(h, i * 10, NULL); |
|
TEST_ASSERT(ret == 0, "push success"); |
|
} |
|
TEST_ASSERT(h->size == 10, "size after pushes"); |
|
TEST_ASSERT(h->capacity >= 10, "capacity grown"); |
|
|
|
// Pop all, verify order |
|
uint64_t prev = 0; |
|
for (int i = 0; i < 10; i++) { |
|
TimeoutEntry entry; |
|
int ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop success"); |
|
TEST_ASSERT(entry.expiration == prev, "order after growth"); |
|
prev = entry.expiration + 10; |
|
} |
|
|
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
// Comparison function for HeapElement |
|
static int compare_elements(const void *a, const void *b) { |
|
const HeapElement *ea = (const HeapElement *)a; |
|
const HeapElement *eb = (const HeapElement *)b; |
|
if (ea->expiration < eb->expiration) return -1; |
|
if (ea->expiration > eb->expiration) return 1; |
|
return 0; |
|
} |
|
|
|
// Test 8: large number of elements, random order, with some cancellations |
|
static void test_large_random(size_t num_elements, size_t cancel_percent) { |
|
printf("Test 8: large random test (%zu elements, cancel %zu%%)...\n", |
|
num_elements, cancel_percent); |
|
TimeoutHeap *h = timeout_heap_create(100); |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
|
|
// Generate elements with unique expiration values |
|
HeapElement *elems = malloc(num_elements * sizeof(HeapElement)); |
|
TEST_ASSERT(elems != NULL, "alloc elems"); |
|
|
|
// Create array of unique expiration values |
|
uint64_t *expirations = malloc(num_elements * sizeof(uint64_t)); |
|
TEST_ASSERT(expirations != NULL, "alloc expirations"); |
|
for (size_t i = 0; i < num_elements; i++) { |
|
expirations[i] = i; // base unique value |
|
} |
|
// Shuffle using Fisher-Yates |
|
srand((unsigned int)time(NULL) ^ 0x1234); |
|
for (size_t i = num_elements - 1; i > 0; i--) { |
|
size_t j = rand() % (i + 1); |
|
uint64_t tmp = expirations[i]; |
|
expirations[i] = expirations[j]; |
|
expirations[j] = tmp; |
|
} |
|
|
|
for (size_t i = 0; i < num_elements; i++) { |
|
elems[i].expiration = expirations[i]; |
|
elems[i].data = malloc(1); // unique pointer |
|
elems[i].cancelled = 0; |
|
int ret = timeout_heap_push(h, elems[i].expiration, elems[i].data); |
|
TEST_ASSERT(ret == 0, "push success"); |
|
} |
|
free(expirations); |
|
|
|
// Cancel some random elements |
|
size_t cancel_count = num_elements * cancel_percent / 100; |
|
for (size_t i = 0; i < cancel_count; i++) { |
|
size_t idx = rand() % num_elements; |
|
if (!elems[idx].cancelled) { |
|
int ret = timeout_heap_cancel(h, elems[idx].expiration, elems[idx].data); |
|
TEST_ASSERT(ret == 0, "cancel success"); |
|
elems[idx].cancelled = 1; |
|
} |
|
} |
|
|
|
// Build array of non-cancelled elements, sort by expiration |
|
HeapElement *remaining = malloc(num_elements * sizeof(HeapElement)); |
|
TEST_ASSERT(remaining != NULL, "alloc remaining"); |
|
size_t remain_count = 0; |
|
for (size_t i = 0; i < num_elements; i++) { |
|
if (!elems[i].cancelled) { |
|
remaining[remain_count++] = elems[i]; |
|
} |
|
} |
|
qsort(remaining, remain_count, sizeof(HeapElement), compare_elements); |
|
|
|
// Pop and verify order |
|
uint64_t last_expiration = 0; |
|
for (size_t i = 0; i < remain_count; i++) { |
|
TimeoutEntry entry; |
|
int ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop success"); |
|
TEST_ASSERT(entry.expiration == remaining[i].expiration, "expiration order"); |
|
TEST_ASSERT(entry.data == remaining[i].data, "data matches"); |
|
TEST_ASSERT(entry.expiration >= last_expiration, "non-decreasing"); |
|
last_expiration = entry.expiration; |
|
free(entry.data); |
|
} |
|
|
|
// Heap empty |
|
TimeoutEntry entry; |
|
int ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == -1, "heap empty after all pops"); |
|
|
|
// Free cancelled elements data |
|
for (size_t i = 0; i < num_elements; i++) { |
|
if (elems[i].cancelled) { |
|
free(elems[i].data); |
|
} |
|
} |
|
|
|
free(elems); |
|
free(remaining); |
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
// Test 7: peek should not remove |
|
static void test_peek_no_remove(void) { |
|
printf("Test 7: peek does not remove...\n"); |
|
TimeoutHeap *h = timeout_heap_create(10); |
|
TEST_ASSERT(h != NULL, "heap creation"); |
|
|
|
int data1 = 1, data2 = 2; |
|
timeout_heap_push(h, 100, &data1); |
|
timeout_heap_push(h, 200, &data2); |
|
|
|
TimeoutEntry entry; |
|
int ret = timeout_heap_peek(h, &entry); |
|
TEST_ASSERT(ret == 0, "peek success"); |
|
TEST_ASSERT(entry.expiration == 100, "peek expiration"); |
|
TEST_ASSERT(h->size == 2, "size unchanged after peek"); |
|
|
|
ret = timeout_heap_peek(h, &entry); |
|
TEST_ASSERT(ret == 0, "second peek success"); |
|
TEST_ASSERT(entry.expiration == 100, "peek same element"); |
|
|
|
// Pop should still give same element |
|
ret = timeout_heap_pop(h, &entry); |
|
TEST_ASSERT(ret == 0, "pop after peek"); |
|
TEST_ASSERT(entry.expiration == 100, "pop expiration"); |
|
|
|
timeout_heap_destroy(h); |
|
printf(" Passed\n"); |
|
} |
|
|
|
int main(void) { |
|
printf("=== Timeout Heap Tests ===\n"); |
|
|
|
test_basic(); |
|
test_ordering(100); // moderate number |
|
test_ordering(1000); // more elements |
|
test_cancel(); |
|
test_duplicate_expirations(); |
|
test_deleted_root(); |
|
test_growth(); |
|
test_peek_no_remove(); |
|
test_large_random(5000, 10); // 5000 elements, 10% cancelled |
|
test_large_random(10000, 5); // 10000 elements, 5% cancelled |
|
|
|
printf("\n=== All tests passed! ===\n"); |
|
return 0; |
|
} |