Commit Diff


commit - dfbe39864b6e5026765fb16732426ea0100f396e
commit + da735c49230ce27cc18449d4d5d7f3bcfecaa7de
blob - 221cf5558f79da78e54a00bb1e33cb3b7b840279
blob + 82a7d72d0085fded034b6622d51eb0a1b5456546
--- .gitignore
+++ .gitignore
@@ -7,5 +7,6 @@ libflint.so
 test
 tcptest
 testrunner
+test_*
 .idea
 netmanual
blob - 2fd17e00ad933ad40ed49fb4cce439bb2a8c93ac
blob + c24d9315fdc198311dd387eec1a0fe4f21aae62c
--- CMakeLists.txt
+++ CMakeLists.txt
@@ -36,21 +36,51 @@ if ((${CMAKE_SYSTEM_NAME} STREQUAL "Linux"))
 endif()
 
 if(${CMAKE_PROJECT_NAME} STREQUAL flint)
-    add_executable(tests tests/tests.c)
-    target_include_directories(tests PRIVATE include)
+    enable_testing()
 
     if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
-        target_link_libraries(tests flint pthread bsd)
+        set(FLINT_TEST_LIBS flint pthread bsd)
     else()
-        target_link_libraries(tests flint pthread)
+        set(FLINT_TEST_LIBS flint pthread)
     endif()
 
+    set(TESTS
+        linkedlist
+        set
+        stack
+        binarytree
+        math
+        vector
+        string
+        crypto
+        parsing
+        memory
+    )
+
+    foreach(t ${TESTS})
+        add_executable(test_${t} tests/test_${t}.c)
+        target_include_directories(test_${t} PRIVATE include)
+        target_link_libraries(test_${t} ${FLINT_TEST_LIBS})
+        add_test(NAME ${t} COMMAND test_${t})
+    endforeach()
+
+    # Network test (slow, uses sleep + sockets)
+    add_executable(test_network tests/test_network.c)
+    target_include_directories(test_network PRIVATE include)
+    target_link_libraries(test_network ${FLINT_TEST_LIBS})
+    add_test(NAME network COMMAND test_network)
+    set_tests_properties(network PROPERTIES LABELS "slow")
+
+    # macOS-only test
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
+        add_executable(test_macos tests/test_macos.c)
+        target_include_directories(test_macos PRIVATE include)
+        target_link_libraries(test_macos ${FLINT_TEST_LIBS})
+        add_test(NAME macos COMMAND test_macos)
+        set_tests_properties(macos PROPERTIES LABELS "slow")
+    endif()
+
     add_executable(netmanual tests/netmanual.c)
     target_include_directories(netmanual PRIVATE include)
-
-    if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
-        target_link_libraries(netmanual flint pthread bsd)
-    else()
-        target_link_libraries(netmanual flint pthread)
-    endif()
+    target_link_libraries(netmanual ${FLINT_TEST_LIBS})
 endif()
blob - 2768f4d06b3602b115b09dcf2cd98a2de2a7f17b
blob + 171a33440699c49f52cca8fb2fd1d322cfe13875
--- README.md
+++ README.md
@@ -4,8 +4,7 @@ My personal library of common C data structures and al
 
 ## Documentation
 
-Extensive documentation can be found [here](https://burkey.co/Software+Documentation/libflint/libflint). You can also check out `tests/tests.c` to
-see example usage from most of the library's API.
+Extensive documentation can be found [here](https://burkey.co/Software+Documentation/libflint/libflint). You can also check out the per-module test files in `tests/` for example usage.
 
 ## Building
 
@@ -22,6 +21,15 @@ target_include_directories(${TARGET} PRIVATE lib/libfl
 target_link_libraries(${TARGET} PRIVATE flint )
 ```
 
+## Testing
+
+```sh
+cmake -B build && cmake --build build
+ctest --test-dir build                # run all tests
+ctest --test-dir build --parallel     # run in parallel
+ctest --test-dir build --label-exclude slow  # skip slow tests (network, macos)
+```
+
 ## Requirements
 
 Building on Linux requires `libbsd`. Building on macOS, OpenBSD, or FreeBSD requires no extra dependencies.
blob - 9bcb2fd8c09c5f4ce210a98b0d1240e559cd0c60
blob + af1bf32c978a6307eeb41f46b079b5affe93f477
--- include/lfmemory.h
+++ include/lfmemory.h
@@ -5,9 +5,9 @@
 
 #include "lflinkedlist.h"
 
-#ifndef DEFAULT_ALIGNMENT
+#ifndef LF_DEFAULT_ALIGNMENT
 #define LF_DEFAULT_ALIGNMENT (2*sizeof(void*))
-#endif // DEFAULT_ALIGNMENT
+#endif // LF_DEFAULT_ALIGNMENT
 
 typedef struct {
     unsigned char* buf;
blob - /dev/null
blob + 1f3824fd374d29737e21ca07953ec4764eaa03ae (mode 644)
--- /dev/null
+++ include/lftest.h
@@ -0,0 +1,60 @@
+#ifndef LFTEST_H
+#define LFTEST_H
+
+#include <stdio.h>
+#include <string.h>
+
+static int lf_tests_total;
+static int lf_tests_failed;
+
+#define ASSERT_TRUE(expr) do { \
+    lf_tests_total++; \
+    if (!(expr)) { \
+        lf_tests_failed++; \
+        fprintf(stderr, "  FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \
+    } \
+} while(0)
+
+#define ASSERT_FALSE(expr) ASSERT_TRUE(!(expr))
+
+#define ASSERT_EQ(a, b) do { \
+    lf_tests_total++; \
+    if ((a) != (b)) { \
+        lf_tests_failed++; \
+        fprintf(stderr, "  FAIL %s:%d: %s != %s\n", __FILE__, __LINE__, #a, #b); \
+    } \
+} while(0)
+
+#define ASSERT_NEQ(a, b) do { \
+    lf_tests_total++; \
+    if ((a) == (b)) { \
+        lf_tests_failed++; \
+        fprintf(stderr, "  FAIL %s:%d: %s == %s\n", __FILE__, __LINE__, #a, #b); \
+    } \
+} while(0)
+
+#define ASSERT_LT(a, b) do { \
+    lf_tests_total++; \
+    if (!((a) < (b))) { \
+        lf_tests_failed++; \
+        fprintf(stderr, "  FAIL %s:%d: %s >= %s\n", __FILE__, __LINE__, #a, #b); \
+    } \
+} while(0)
+
+#define ASSERT_STR_EQ(a, b) do { \
+    lf_tests_total++; \
+    if (strcmp((a), (b)) != 0) { \
+        lf_tests_failed++; \
+        fprintf(stderr, "  FAIL %s:%d: \"%s\" != \"%s\"\n", __FILE__, __LINE__, (a), (b)); \
+    } \
+} while(0)
+
+#define ASSERT_NULL(expr) ASSERT_EQ((void *)(expr), NULL)
+#define ASSERT_NOT_NULL(expr) ASSERT_NEQ((void *)(expr), NULL)
+
+#define TEST_REPORT() do { \
+    printf("%d/%d passed\n", lf_tests_total - lf_tests_failed, lf_tests_total); \
+    return lf_tests_failed ? 1 : 0; \
+} while(0)
+
+#endif /* LFTEST_H */
blob - ba1a8603f7586d7eeb17a88cfc6ff4b521905727 (mode 755)
blob + /dev/null
--- run_tests.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-set -e
-
-#./testrunner
-./tcptest &
-tcpout=$(echo "hello" | nc localhost 18632)
-echo "tcpout: $tcpout" 
-if [ "$tcpout" != "TEST SEND" ]; then
- echo "Error: \"$tcpout\" != \"TEST SEND\""
- exit 1
-fi
-
-exit 0
blob - a0f755715046807ad86e477d29bea7d3dd73b17b
blob + 037cd2638c2f251bc8df208e644d4732c6b0fe71
--- src/binarytree.c
+++ src/binarytree.c
@@ -131,6 +131,7 @@ int bintree_merge(BinTree *merge, BinTree *left, BinTr
 
     merge->root->left = left->root;
     merge->root->right = right->root;
+    merge->size = 1 + left->size + right->size;
 
     left->root = NULL;
     left->size = 0;
blob - 3d47bb055dcc82e60c8aa50c424b65ade9a9e5de
blob + 7dc6dc22e56c5d9d88b4e00d81e93377619c0ffc
--- src/crypto.c
+++ src/crypto.c
@@ -129,7 +129,7 @@ unsigned char *b64_decode(const char *s, size_t sz, si
             break;
         }
         // Not base64 characters
-        if (!isalnum(s[j]) || s[j] == '+' || s[j] == '/') {
+        if (!isalnum(s[j]) && s[j] != '+' && s[j] != '/') {
             break;
         }
 
blob - 23d54c9df9d8498a4e6c0e3b3fc2ff863ecb955e
blob + dfb3e5a33f8c3184643b3c716537b25f2949adcc
--- src/input.c
+++ src/input.c
@@ -135,5 +135,6 @@ const char *capture_system(const char *cmd, int buf_sz
         return NULL;
     }
     fgets(buf, buf_sz, tmp);
+    pclose(tmp);
     return buf;
 }
\ No newline at end of file
blob - 1eb166ac02c8c4fd1e7e973c8040eb3088d7aa80
blob + 53ec9d9a88a03fb3090e822ddcc9959b4fef0560
--- src/linkedlist.c
+++ src/linkedlist.c
@@ -107,14 +107,14 @@ int ll_remove(List *list, ListNode *node, void **data)
 }
 
 int ll_remove_next(List *list, ListNode *node, void **data) {
-    if (node->next == NULL) {
+    if (node == NULL || node->next == NULL) {
         return -1;
     }
     return ll_remove(list, node->next, data);
 }
 
 int ll_remove_prev(List *list, ListNode *node, void **data) {
-    if (node->prev == NULL) {
+    if (node == NULL || node->prev == NULL) {
         return -1;
     }
     return ll_remove(list, node->prev, data);
blob - 14e275b908de1b802c8ebf3e5d73acc95d2f19aa
blob + 5659ee6ca791e2ed0df101b495a7236946c2a812
--- src/math.c
+++ src/math.c
@@ -78,5 +78,5 @@ Point *bresenham_p(Point p1, Point p2, size_t *sz) {
 }
 
 int is_power_of_two(int i) {
-    return (i & (i - 1)) == 0;
+    return i > 0 && (i & (i - 1)) == 0;
 }
blob - 6cfe72d28dc88b08e240782f73007000a8b8da3c
blob + 83289bdda46e17a6c8bf99a1fbcdf72cef59c53c
--- src/memory.c
+++ src/memory.c
@@ -19,8 +19,14 @@ void arena_init(ArenaAllocator *allocator, size_t buf_
 }
 
 void arena_free(ArenaAllocator *allocator) {
+    if (allocator == NULL) {
+        return;
+    }
     free(allocator->buf);
-    free(allocator);
+    allocator->buf = NULL;
+    allocator->buf_sz = 0;
+    allocator->offset_cur = 0;
+    allocator->offset_prev = 0;
 }
 
 void arena_clear(ArenaAllocator *allocator) {
@@ -28,9 +34,9 @@ void arena_clear(ArenaAllocator *allocator) {
     allocator->offset_prev = 0;
 }
 
-static uintptr_t align_forward_uintptr(const uintptr_t ptr, const uintptr_t align) {
+static uintptr_t align_forward(const uintptr_t ptr, const uintptr_t align) {
     if (!is_power_of_two(align)) {
-        // TODO: Error
+        return 0;
     }
     uintptr_t p = ptr;
     const uintptr_t m = p & (align - 1);
@@ -41,24 +47,11 @@ static uintptr_t align_forward_uintptr(const uintptr_t
     return p;
 }
 
-static uintptr_t align_forward_size(const size_t ptr, const size_t align) {
-    if (!is_power_of_two(align)) {
-        // TODO: Error
-    }
-    uintptr_t p = ptr;
-    const uintptr_t m = p & (align - 1);
-
-    if (m != 0) {
-        p += align - m;
-    }
-    return p;
-}
-
 static void *arena_malloc_align(ArenaAllocator *allocator, const size_t size, size_t align) {
     uintptr_t cur_ptr = (uintptr_t)allocator->buf + allocator->offset_cur;
 
     // Push forward to align, then change to relative offset
-    uintptr_t offset = align_forward_uintptr(cur_ptr, align);
+    uintptr_t offset = align_forward(cur_ptr, align);
     offset -= (uintptr_t)allocator->buf;
 
     if (offset + size <= allocator->buf_sz) {
@@ -76,7 +69,7 @@ static void *arena_malloc_align(ArenaAllocator *alloca
 static void *arena_resize_align(ArenaAllocator *allocator, void *mem, const size_t old_sz, const size_t new_sz, size_t align) {
     unsigned char *old_mem = mem;
     if (!is_power_of_two(align)) {
-        // TODO: Error handling
+        return NULL;
     }
 
     if (old_mem == NULL || old_sz == 0) {
@@ -85,15 +78,20 @@ static void *arena_resize_align(ArenaAllocator *alloca
 
     if (allocator->buf <= (unsigned char*)mem && (unsigned char*)mem < allocator->buf + allocator->buf_sz) {
         if (allocator->buf + allocator->offset_prev == old_mem) {
+            if (allocator->offset_prev + new_sz > allocator->buf_sz) {
+                return NULL;
+            }
             allocator->offset_cur = allocator->offset_prev + new_sz;
             if (new_sz > old_sz) {
-                // Zero out memory
-                memset(&allocator->buf[allocator->offset_cur], 0, new_sz - old_sz);
+                memset(&allocator->buf[allocator->offset_prev + old_sz], 0, new_sz - old_sz);
             }
             return old_mem;
         }
 
         void *new_mem = arena_malloc_align(allocator, new_sz, align);
+        if (new_mem == NULL) {
+            return NULL;
+        }
         size_t copy_size = old_sz < new_sz ? old_sz : new_sz;
         memmove(new_mem, old_mem, copy_size);
         return new_mem;
@@ -103,7 +101,12 @@ static void *arena_resize_align(ArenaAllocator *alloca
 }
 
 void arena_resize_buf(ArenaAllocator *allocator, const size_t new_sz) {
-    allocator->buf = realloc(allocator->buf, sizeof(unsigned char) * new_sz);
+    unsigned char *new_buf = realloc(allocator->buf, sizeof(unsigned char) * new_sz);
+    if (new_buf == NULL) {
+        return;
+    }
+    allocator->buf = new_buf;
+    allocator->buf_sz = new_sz;
 }
 
 void *arena_malloc(ArenaAllocator *allocator, const size_t size) {
@@ -121,12 +124,14 @@ void pool_init(PoolAllocator *allocator, size_t buf_sz
 
     allocator->buf = malloc(sizeof(unsigned char) * buf_sz);
     uintptr_t istart = (uintptr_t)allocator->buf;
-    uintptr_t start = align_forward_uintptr(istart, chunk_align);
+    uintptr_t start = align_forward(istart, chunk_align);
     allocator->buf_sz = buf_sz - (start - istart);
 
-    allocator->chunk_size = align_forward_size(chunk_sz, chunk_align);
+    allocator->chunk_size = align_forward(chunk_sz, chunk_align);
     if (allocator->chunk_size < sizeof(void *) || allocator->buf_sz < allocator->chunk_size) {
-        //TODO: Handle error better
+        free(allocator->buf);
+        allocator->buf = NULL;
+        allocator->buf_sz = 0;
         return;
     }
 
@@ -137,7 +142,6 @@ void pool_init(PoolAllocator *allocator, size_t buf_sz
 }
 
 void pool_free(PoolAllocator *allocator, void *ptr) {
-    ListNode *node = NULL;
     const void *start = allocator->buf;
     const void *end = &allocator->buf[allocator->buf_sz];
 
@@ -146,7 +150,6 @@ void pool_free(PoolAllocator *allocator, void *ptr) {
     }
 
     if (!(start <= ptr && ptr < end)) {
-        // TODO: Handle error better
         return;
     }
 
@@ -154,6 +157,9 @@ void pool_free(PoolAllocator *allocator, void *ptr) {
 }
 
 void pool_free_all(PoolAllocator *allocator) {
+    ll_destroy(allocator->free_list);
+    ll_init(allocator->free_list, NULL);
+
     size_t chunk_count = allocator->buf_sz / allocator->chunk_size;
     for (size_t i = 0; i < chunk_count; ++i) {
         ll_ins_next(allocator->free_list, allocator->free_list->head, &allocator->buf[i * allocator->chunk_size]);
@@ -163,7 +169,6 @@ void pool_free_all(PoolAllocator *allocator) {
 void *pool_alloc(PoolAllocator *allocator) {
     ListNode *node = allocator->free_list->head;
     if (node == NULL) {
-        // TODO: Handle error better
         return NULL;
     }
 
@@ -173,6 +178,9 @@ void *pool_alloc(PoolAllocator *allocator) {
 }
 
 void pool_destroy(PoolAllocator *allocator) {
+    if (allocator == NULL) {
+        return;
+    }
     ll_destroy(allocator->free_list);
     free(allocator->free_list);
     free(allocator->buf);
blob - 29841222c56782549f59bf4c7bff135b55aafe67
blob + b4f2ebfa3ef9b4d8dd0f24d89f8d435b77ab21c0
--- src/network.c
+++ src/network.c
@@ -96,7 +96,8 @@ int serve(Server *s, int backlog_size) {
     // This is just for macOS and BSDs
     #if !defined(__linux__) && !defined(_WIN32)
     struct sigaction sa;
-    sa.sa_handler = sighandler; 
+    sigemptyset(&sa.sa_mask);
+    sa.sa_handler = sighandler;
     sa.sa_flags = SA_RESTART;
     if (sigaction(SIGCHLD, &sa, NULL) == -1) {
         fprintf(stderr, "Failed to set sigaction\n");
@@ -123,12 +124,13 @@ static void *tcp_echo_thread(void *vargp) {
             break;
         }
 
-        if (send(fd, recv_buf, strlen(recv_buf), 0) == -1) {
+        if (send(fd, recv_buf, r, 0) == -1) {
             fprintf(stderr, "Failed to send echo. Errno: %d\n", errno);
             close(fd);
             break;
         }
     }
+    return NULL;
 }
 
 void handler_tcp_echo(Server *s) {
blob - 0857c95fd03a046ef2ae07d90a6aa4cb1618b109
blob + 9e68fc54b2da44997bbd2dcc4d980f68c25aaef4
--- src/set.c
+++ src/set.c
@@ -29,7 +29,7 @@ int set_remove(Set *set, void **data) {
     if (node == NULL) {
         return -1;
     }
-    return ll_remove_next(set, node, data);
+    return ll_remove(set, node, data);
 }
 
 int set_union(Set *setu, const Set *a, const Set *b) {
@@ -117,7 +117,7 @@ int set_is_subset(const Set *a, const Set *b) {
 }
 
 int set_is_equal(const Set *a, const Set *b) {
-    if (a->size == b->size) {
+    if (a->size != b->size) {
         return 0;
     }
     return set_is_subset(a, b);
blob - 4fe56033bd86f75901823d8367d78b305597e022
blob + 2cb1d2572d1af0dac58eb454a6b5905d9111c579
--- src/string.c
+++ src/string.c
@@ -46,7 +46,7 @@ char* substr(const char* str, size_t idx, size_t len) 
        return NULL;
    }
 
-   char *substr = malloc(sizeof(char) * len + 1);
+   char *substr = malloc(sizeof(char) * (len + 1));
    if (substr == NULL) {
        return NULL;
    }
blob - 83da588467cb044d00a0cbb1bc97c7cdcc691505
blob + 86f65c6f6b4f52c38a382ae4a35ab935d0a45eb9
--- src/vector.c
+++ src/vector.c
@@ -153,6 +153,9 @@ int vec_shrink(Vector *vec) {
 }
 
 const void *vec_min(const Vector *vec, int(*cmp)(const void *a, const void *b)) {
+    if (vec_len(vec) == 0) {
+        return NULL;
+    }
     void *a = vec->elements[0];
     for (size_t i = 1; i < vec_len(vec); ++i) {
         if (cmp(a, vec->elements[i]) > 0) {
@@ -163,6 +166,9 @@ const void *vec_min(const Vector *vec, int(*cmp)(const
 }
 
 const void *vec_max(const Vector *vec, int(*cmp)(const void *a, const void *b)) {
+    if (vec_len(vec) == 0) {
+        return NULL;
+    }
     void *a = vec->elements[0];
     for (size_t i = 1; i < vec_len(vec); ++i) {
         if (cmp(a, vec->elements[i]) < 0) {
@@ -186,8 +192,8 @@ int vec_cmp_int(const void *a, const void *b) {
 }
 
 int vec_cmp_char(const void *a, const void *b) {
-    const char x = *(int*)a;
-    const char y = *(int*)b;
+    const char x = *(char*)a;
+    const char y = *(char*)b;
 
     if (x > y) {
         return 1;
blob - bf4544915dcf359a7e1d70423f9d57eba614ec03 (mode 644)
blob + /dev/null
--- tests/tests.c
+++ /dev/null
@@ -1,546 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <assert.h>
-#include <unistd.h>
-#include <arpa/inet.h>
-#include <sys/socket.h>
-#include <pthread.h>
-
-#include "lflinkedlist.h"
-#include "lfnetwork.h"
-#include "lfset.h"
-#include "lfstack.h"
-#include "lfbinarytree.h"
-#include "lfvector.h"
-#include "lfmath.h"
-#include "lfstring.h"
-#include "lfcrypto.h"
-#include "lfparsing.h"
-#include "lfinput.h"
-#include "lfmemory.h"
-
-#if defined(__APPLE__) || defined(__MACH__)
-#include "lfmacos.h"
-#endif /* defined(__APPLE__) || defined(__MACH__) */
-
-void print_ll(List *list) {
-    LL_ITER(list) {
-        printf(" %d", *((int *) node->data));
-    }
-    printf("\n");
-}
-
-void test_ll() {
-    printf("\n--- LIST TEST ---\n");
-    List *list = malloc(sizeof(List));
-    ll_init(list, NULL);
-
-    int i = 1;
-    int j = 2;
-    int k = 4;
-
-    ll_ins_next(list, list->head, (void *) &i);
-    ll_ins_next(list, list->tail, (void *) &j);
-    ll_ins_next(list, list->tail, (void *) &k);
-
-    printf("List: ");
-    print_ll(list);
-
-    void *data;
-    ll_remove_next(list, list->head, &data);
-
-    printf("List: ");
-    print_ll(list);
-    assert(*(int*)data == 2);
-    printf("Removed: %d\n", *((int *) data));
-
-    ll_destroy(list);
-    free(list);
-}
-
-int int_match(const void *a, const void *b) {
-    return *((int *) a) == *((int *) b);
-}
-
-void test_set() {
-    printf("\n--- SET TEST ---\n");
-    Set *set = malloc(sizeof(Set));
-    set_init(set, int_match, NULL);
-
-    int i = 1;
-    int j = 2;
-    int k = 2;
-
-    set_insert(set, &i);
-    set_insert(set, &j);
-    set_insert(set, &k);
-
-    int i2 = 1;
-    int j2 = 4;
-
-    Set *set2 = malloc(sizeof(Set));
-    set_init(set2, int_match, NULL);
-
-    set_insert(set2, &i2);
-    set_insert(set2, &j2);
-
-    printf("Set 1:");
-    print_ll(set);
-
-    printf("Set 2:");
-    print_ll(set2);
-    printf("\n");
-
-    Set *set_u = malloc(sizeof(Set));
-    Set *set_i = malloc(sizeof(Set));
-    Set *set_d = malloc(sizeof(Set));
-
-    set_union(set_u, set, set2);
-    printf("Union:");
-    print_ll(set_u);
-
-    set_difference(set_d, set, set2);
-    printf("Difference:");
-    print_ll(set_d);
-
-    set_intersection(set_i, set, set2);
-    printf("Intersection:");
-    print_ll(set_i);
-
-    set_destroy(set);
-    set_destroy(set2);
-    set_destroy(set_u);
-    set_destroy(set_i);
-    set_destroy(set_d);
-    free(set);
-    free(set2);
-    free(set_u);
-    free(set_i);
-    free(set_d);
-}
-
-void test_stack() {
-    printf("\n--- STACK TEST ---\n");
-    Stack *stack = malloc(sizeof(Stack));
-    stack_init(stack, NULL);
-
-    int a = 1, b = 2;
-    stack_push(stack, &a);
-    stack_push(stack, &b);
-    printf("Stack size: %lu\n", stack->size);
-
-    int *p = NULL;
-    stack_pop(stack, (void **) &p);
-    printf("b = %d\n", *p);
-
-    stack_pop(stack, (void **) &p);
-    printf("a = %d\n", *p);
-    printf("Stack size: %lu\n", stack->size);
-
-    stack_destroy(stack);
-    free(stack);
-    stack = NULL;
-}
-
-void test_bintree() {
-    printf("\n--- BINARY TREE TEST ---\n");
-    BinTree *tree = malloc(sizeof(BinTree));
-    bintree_init(tree, NULL);
-
-    int root = 0;
-    int l1 = 1;
-    int l2 = 2;
-    int r1 = 12;
-    int r2 = 200;
-
-    bintree_ins_left(tree, NULL, &root);
-    bintree_ins_left(tree, tree->root, &l1);
-    bintree_ins_left(tree, tree->root->left, &l2);
-    bintree_ins_right(tree, tree->root->left, &r2);
-    bintree_ins_right(tree, tree->root, &r1);
-    bintree_ins_right(tree, tree->root->right, &r2);
-    bintree_ins_left(tree, tree->root->right, &l1);
-
-    bintree_debug_print(tree);
-
-    printf("Changing r2\n");
-    r2 = 100;
-    bintree_debug_print(tree);
-
-    bintree_destroy(tree);
-    free(tree);
-    tree = NULL;
-}
-
-void test_math() {
-    printf("\n--- MATH TEST ---\n");
-    int i = 1, j = 2;
-    assert(max_int(i, j) == j);
-    printf("Between %d and %d, %d is larger\n", i, j, max_int(i, j));
-    printf("Between %d and %d, %d is smaller\n", i, j, min_int(i, j));
-
-    char *s = "10101101";
-    printf("Binary: %s\n", s);
-    printf("Decimal: %d\n", binstr_to_int(s));
-
-    char *s2 = "1010_1101";
-    printf("Binary: %s\n", s2);
-    printf("Decimal: %d\n", binstr_to_int(s2));
-
-    i = 10;
-    i = clamp_int(i, 2, 5);
-    assert(i == 5);
-
-    printf("\nGenerate line from 0,0 to 2,5\n");
-    size_t sz = 0;
-    Point *line = bresenham(0, 0, 2, 5, &sz);
-    for (size_t idx = 0; idx < sz; idx++) {
-        printf("%d,%d ", line[idx].x, line[idx].y);
-    }
-    printf("\n");
-    free(line);
-}
-
-void print_vector(Vector *vec) {
-    for (size_t i = 0; i < vec->length; ++i) {
-        int t = *(int *) vec_at(vec, i);
-        printf("%d ", t);
-    }
-    printf("\n");
-}
-
-void test_vector() {
-    printf("\n--- VECTOR TEST ---\n");
-    Vector *v = malloc(sizeof(Vector));
-    vec_init(v, NULL);
-
-    int e0 = 0;
-    int e1 = 1;
-    int e2 = 2;
-    int e3 = 3;
-    int e4 = 4;
-
-    vec_push(v, &e0);
-    assert(v->length == 1);
-    int *t = vec_at(v, 0);
-    assert(*t == 0);
-
-    vec_push(v, &e1);
-    vec_push(v, &e2);
-    assert(v->length == 3);
-
-    // test access outside bounds
-    t = (int *) vec_safe_at(v, 3);
-    assert(t == NULL);
-
-    printf("Before insert: ");
-    print_vector(v);
-    vec_push(v, &e3);
-    vec_insert(v, &e4, 1);
-    printf("After insert:  ");
-    print_vector(v);
-
-    t = (int *) vec_at(v, 4);
-    assert(*t == e3);
-    t = (int *) vec_at(v, 1);
-    assert(*t == e4);
-
-    const int *min = vec_min(v, vec_cmp_int);
-    const int *max = vec_max(v, vec_cmp_int);
-    printf("min: %d\n", *min);
-    printf("max: %d\n", *max);
-    assert(*min == e0);
-    assert(*max == e4);
-
-    t = (int *) vec_remove(v, 1);
-    assert(t != NULL);
-    assert(*t == 4);
-    printf("After removal: ");
-    print_vector(v);
-
-    t = (int *) vec_remove(v, 10);
-    assert(t == NULL);
-
-    printf("\ncap before shrink: %zu\n", vec_cap(v));
-    vec_shrink(v);
-    assert(vec_len(v) == vec_cap(v));
-    printf("cap after shrink: %zu\n", vec_cap(v));
-
-    vec_clear(v);
-    assert(vec_len(v) == 0);
-    printf("\ncap after clear: %zu\n", vec_cap(v));
-    printf("len after clear: %zu\n", vec_len(v));
-
-    vec_grow_to(v, 10);
-    assert(vec_cap(v) == 10);
-    assert(vec_len(v) == 0);
-
-    vec_destroy(v);
-    free(v);
-}
-
-void test_string() {
-    printf("\n--- STRING TEST ---\n");
-    const char *haystack =
-            "Test one two one and also maybe two but not Gabe's least favorite number, which is not one.";
-    const char *needles[] = {
-        "one",
-        "two",
-        "Gabe"
-    };
-
-    size_t sub_sz = 0;
-    size_t *subs = NULL;
-    find_substrings(haystack, needles[0], &sub_sz, &subs);
-
-    assert(sub_sz == 3);
-    assert(subs[0] == 5);
-    assert(subs[1] == 13);
-    assert(subs[2] == 87);
-
-    char *s = substr(haystack, subs[0], strlen(needles[0]));
-    assert(strcmp(s, needles[0]) == 0);
-
-    free(s);
-    free(subs);
-    subs = NULL;
-
-    find_substrings(haystack, needles[1], &sub_sz, &subs);
-    assert(sub_sz == 2);
-    assert(subs[0] == 9);
-
-    free(subs);
-    subs = NULL;
-
-    find_substrings("test one two", "nope", &sub_sz, &subs);
-    assert(sub_sz == 0);
-    assert(subs == NULL);
-    free(subs);
-    subs = NULL;
-
-    find_substrings("123", "nopes", &sub_sz, &subs);
-    assert(sub_sz == 0);
-    assert(subs == NULL);
-    free(subs);
-    subs = NULL;
-
-    printf("Passes all string tests\n");
-}
-
-void test_crypto() {
-    printf("\n--- CRYPTO TEST ---\n");
-
-    char *in = "BUTT";
-    unsigned char *s = b64_encode(in, strlen(in));
-    assert(strcmp(s, "QlVUVA==") == 0);
-    free(s);
-
-    char *in2 = "a longer base64 test, apparently";
-    s = b64_encode(in2, strlen(in2));
-    assert(strcmp(s, "YSBsb25nZXIgYmFzZTY0IHRlc3QsIGFwcGFyZW50bHk=") == 0);
-    free(s);
-
-    char *out2 = "YSBsb25nZXIgYmFzZTY0IHRlc3QsIGFwcGFyZW50bHk=";
-    size_t s_sz = 0;
-    s = (char *) b64_decode(out2, strlen(out2), &s_sz);
-    assert(strcmp(s, "a longer base64 test, apparently") == 0);
-    assert(strlen(s) == s_sz);
-    free(s);
-
-    s = hex_decode("DEADBEEF", &s_sz);
-    unsigned char h[4] = {
-        0xDE, 0xAD, 0xBE, 0xEF
-    };
-    for (size_t i = 0; i < 4; ++i) {
-        assert(s[i] == h[i]);
-    }
-    free(s);
-
-    // Odd number of characters
-    s = hex_decode("f00f5", &s_sz);
-    unsigned char h2[4] = {
-        0x0F, 0x00, 0xF5
-    };
-    for (size_t i = 0; i < 3; ++i) {
-        assert(s[i] == h2[i]);
-    }
-    free(s);
-
-    // leading 0x
-    s = hex_decode("0xf00f5", &s_sz);
-    for (size_t i = 0; i < 3; ++i) {
-        assert(s[i] == h2[i]);
-    }
-    free(s);
-
-    s = hex_encode(h, 4);
-    assert(strcmp(s, "deadbeef") == 0);
-    free(s);
-
-    // "Sup?"
-    unsigned char hexsup[4] = {
-        0x53, 0x75, 0x70, 0x3F
-    };
-    s = hex_to_str(hexsup, 4);
-    assert(strcmp(s, "Sup?") == 0);
-    free(s);
-
-    s = repeating_key_xor_s("TEST", "HI");
-    char *enc = hex_encode(s, 4);
-    assert(strcmp(enc, "1c0c1b1d") == 0);
-    free(enc);
-    free(s);
-
-    unsigned char ua[2] = {0x2, 0xF};
-    unsigned char ub[2] = {0x4, 0xE};
-    unsigned int hamming = hamming_distance(ua, ub, 2);
-    assert(hamming == 3);
-
-    hamming = hamming_distance_s("this is a test", "wokka wokka!!!");
-    assert(hamming == 37);
-
-    printf("Passes all crypto tests\n");
-}
-
-void test_parsing() {
-    printf("\n--- PARSING TEST ---\n");
-
-    char *nonsense = "8d82jI|dms~<>s2d";
-    char *english = "This is an English sentence!";
-    assert(simple_english_scoring(english) > simple_english_scoring(nonsense));
-
-    printf("Passes all parsing tests\n");
-}
-
-#define NET_MSG "TEST SEND"
-
-void tcp_test_handler(Server *s) {
-    struct sockaddr_storage client_addr;
-    socklen_t client_addr_sz = sizeof(client_addr);
-    int new_fd = accept(s->fd, (struct sockaddr *) &client_addr, &client_addr_sz);
-    assert(new_fd != -1);
-    assert(send(new_fd, NET_MSG, 10, 0) != -1);
-    close(new_fd);
-}
-
-void *tcp_server_thread(void *vargp) {
-    Server *server = new_server(SERVERTYPE_TCP, "18632", tcp_test_handler);
-    serve(server, DEFAULT_BACKLOG);
-    delete_server(server);
-}
-
-void udp_test_handler(Server *s) {
-    struct sockaddr_storage client_addr;
-    socklen_t client_addr_sz = sizeof(client_addr);
-    char recv_buf[128];
-
-    int r = (int) recvfrom(s->fd, recv_buf, 128, 0, (struct sockaddr *) &client_addr, &client_addr_sz);
-    assert(r > 0);
-    assert(strcmp(recv_buf, NET_MSG) == 0);
-}
-
-void *udp_server_thread(void *vargp) {
-    Server *server = new_server(SERVERTYPE_UDP, "18633", udp_test_handler);
-    serve(server, DEFAULT_BACKLOG);
-    delete_server(server);
-}
-
-void test_network() {
-    printf("\n--- NETWORK TEST ---\n");
-    pthread_t srv_tid;
-    pthread_create(&srv_tid, NULL, tcp_server_thread, NULL);
-
-    sleep(1);
-    const char *s = capture_system("echo hello | nc localhost 18632", 0);
-    assert(strcmp(s, NET_MSG) == 0);
-    free((char *) s);
-
-    pthread_join(srv_tid, NULL);
-    printf("Passed TCP test\n");
-
-    pthread_create(&srv_tid, NULL, udp_server_thread, NULL);
-    sleep(1);
-    system("echo hello | nc localhost 18633");
-
-    pthread_join(srv_tid, NULL);
-    printf("Passed UDP test\n");
-}
-
-#if defined(__APPLE__) || defined(__MACH__)
-void test_macos() {
-    printf("\n--- macOS TEST ---\n");
-
-    pid_t pid = getpid();
-    ProcessData *pd = new_ProcessData();
-    for (int i = 0; i < 4; i++) {
-        update_process(pid, pd);
-        printf("CPU: %.2f\n", pd->percent_cpu);
-        sleep(1);
-    }
-    free(pd);
-}
-#endif
-
-void test_memory() {
-    printf("\n--- MEMORY TEST ---\n");
-    ArenaAllocator *arena = malloc(sizeof(ArenaAllocator));
-    arena_init(arena, 1024);
-
-    int *i1 = arena_malloc(arena, sizeof(int));
-    int *i2 = arena_malloc(arena, sizeof(int));
-
-    *i1 = 1;
-    *i2 = 2;
-
-    assert(i1 < i2);
-    assert(*i1 < *i2);
-
-    long *l = arena_resize(arena, i1, sizeof(int), sizeof(long));
-    assert(*l == 1);
-
-    unsigned char *char_test = arena_resize(arena, i2, sizeof(int), sizeof(unsigned char));
-    assert(*char_test == 2);
-
-    arena_free(arena);
-    arena = NULL;
-
-    PoolAllocator *pool = malloc(sizeof(PoolAllocator));
-    pool_init(pool, 64, 16, LF_DEFAULT_ALIGNMENT);
-    void *a = pool_alloc(pool);
-    void *b = pool_alloc(pool);
-    void *c = pool_alloc(pool);
-    void *d = pool_alloc(pool);
-
-    assert(a != NULL);
-    assert(b != NULL);
-    assert(c != NULL);
-    assert(d != NULL);
-
-    assert(pool_count_available(pool) == 0);
-    pool_free(pool, d);
-    d = NULL;
-    assert(pool_count_available(pool) == 1);
-
-    pool_destroy(pool);
-    printf("Passes all memory tests\n");
-}
-
-int main() {
-    test_ll();
-    test_set();
-    test_stack();
-    test_bintree();
-    test_math();
-    test_vector();
-    test_string();
-    test_crypto();
-    test_parsing();
-    test_network();
-    test_memory();
-
-#if defined(__APPLE__) || defined(__MACH__)
-    test_macos();
-#endif
-
-    return 0;
-}
blob - /dev/null
blob + fe79293ff6446784eaeff8ae652c0ab90891af70 (mode 644)
--- /dev/null
+++ tests/test_binarytree.c
@@ -0,0 +1,34 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "lftest.h"
+#include "lfbinarytree.h"
+
+int main() {
+    BinTree *tree = malloc(sizeof(BinTree));
+    bintree_init(tree, NULL);
+
+    int root = 0;
+    int l1 = 1;
+    int l2 = 2;
+    int r1 = 12;
+    int r2 = 200;
+
+    bintree_ins_left(tree, NULL, &root);
+    ASSERT_NOT_NULL(tree->root);
+
+    bintree_ins_left(tree, tree->root, &l1);
+    bintree_ins_left(tree, tree->root->left, &l2);
+    bintree_ins_right(tree, tree->root->left, &r2);
+    bintree_ins_right(tree, tree->root, &r1);
+    bintree_ins_right(tree, tree->root->right, &r2);
+    bintree_ins_left(tree, tree->root->right, &l1);
+
+    ASSERT_EQ(*(int *)tree->root->data, 0);
+    ASSERT_EQ(*(int *)tree->root->left->data, 1);
+    ASSERT_EQ(*(int *)tree->root->right->data, 12);
+
+    bintree_destroy(tree);
+    free(tree);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + 42b3af2038c8a4078c1f6260551b08f1dd30c637 (mode 644)
--- /dev/null
+++ tests/test_crypto.c
@@ -0,0 +1,69 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "lftest.h"
+#include "lfcrypto.h"
+
+int main() {
+    char *in = "BUTT";
+    unsigned char *s = b64_encode(in, strlen(in));
+    ASSERT_STR_EQ((char *)s, "QlVUVA==");
+    free(s);
+
+    char *in2 = "a longer base64 test, apparently";
+    s = b64_encode(in2, strlen(in2));
+    ASSERT_STR_EQ((char *)s, "YSBsb25nZXIgYmFzZTY0IHRlc3QsIGFwcGFyZW50bHk=");
+    free(s);
+
+    char *out2 = "YSBsb25nZXIgYmFzZTY0IHRlc3QsIGFwcGFyZW50bHk=";
+    size_t s_sz = 0;
+    s = (unsigned char *) b64_decode(out2, strlen(out2), &s_sz);
+    ASSERT_STR_EQ((char *)s, "a longer base64 test, apparently");
+    ASSERT_EQ(strlen((char *)s), s_sz);
+    free(s);
+
+    s = hex_decode("DEADBEEF", &s_sz);
+    unsigned char h[4] = { 0xDE, 0xAD, 0xBE, 0xEF };
+    for (size_t i = 0; i < 4; ++i) {
+        ASSERT_EQ(s[i], h[i]);
+    }
+    free(s);
+
+    s = hex_decode("f00f5", &s_sz);
+    unsigned char h2[3] = { 0x0F, 0x00, 0xF5 };
+    for (size_t i = 0; i < 3; ++i) {
+        ASSERT_EQ(s[i], h2[i]);
+    }
+    free(s);
+
+    s = hex_decode("0xf00f5", &s_sz);
+    for (size_t i = 0; i < 3; ++i) {
+        ASSERT_EQ(s[i], h2[i]);
+    }
+    free(s);
+
+    char *enc_s = hex_encode(h, 4);
+    ASSERT_STR_EQ(enc_s, "deadbeef");
+    free(enc_s);
+
+    unsigned char hexsup[4] = { 0x53, 0x75, 0x70, 0x3F };
+    enc_s = hex_to_str(hexsup, 4);
+    ASSERT_STR_EQ(enc_s, "Sup?");
+    free(enc_s);
+
+    unsigned char *xor_s = repeating_key_xor_s("TEST", "HI");
+    enc_s = hex_encode(xor_s, 4);
+    ASSERT_STR_EQ(enc_s, "1c0c1b1d");
+    free(enc_s);
+    free(xor_s);
+
+    unsigned char ua[2] = {0x2, 0xF};
+    unsigned char ub[2] = {0x4, 0xE};
+    unsigned int hamming = hamming_distance(ua, ub, 2);
+    ASSERT_EQ(hamming, 3);
+
+    hamming = hamming_distance_s("this is a test", "wokka wokka!!!");
+    ASSERT_EQ(hamming, 37);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + 02121eab523233f0ce728df2fc6931aba6901619 (mode 644)
--- /dev/null
+++ tests/test_linkedlist.c
@@ -0,0 +1,29 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "lftest.h"
+#include "lflinkedlist.h"
+
+int main() {
+    List *list = malloc(sizeof(List));
+    ll_init(list, NULL);
+
+    int i = 1;
+    int j = 2;
+    int k = 4;
+
+    ll_ins_next(list, list->head, (void *) &i);
+    ll_ins_next(list, list->tail, (void *) &j);
+    ll_ins_next(list, list->tail, (void *) &k);
+
+    ASSERT_EQ(list->size, 3);
+
+    void *data;
+    ll_remove_next(list, list->head, &data);
+    ASSERT_EQ(*(int *)data, 2);
+    ASSERT_EQ(list->size, 2);
+
+    ll_destroy(list);
+    free(list);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + 48c994ca9950ca8f7a752bc05da4cd93b1d76514 (mode 644)
--- /dev/null
+++ tests/test_macos.c
@@ -0,0 +1,28 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include "lftest.h"
+
+#if defined(__APPLE__) || defined(__MACH__)
+#include "lfmacos.h"
+
+int main() {
+    pid_t pid = getpid();
+    ProcessData *pd = new_ProcessData();
+    ASSERT_NOT_NULL(pd);
+
+    for (int i = 0; i < 2; i++) {
+        update_process(pid, pd);
+        sleep(1);
+    }
+    ASSERT_TRUE(pd->percent_cpu >= 0.0);
+
+    free(pd);
+    TEST_REPORT();
+}
+#else
+int main() {
+    printf("Skipped (not macOS)\n");
+    return 0;
+}
+#endif
blob - /dev/null
blob + 780eca704baf4f47eec03ef622bf50095d48d0bb (mode 644)
--- /dev/null
+++ tests/test_math.c
@@ -0,0 +1,28 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "lftest.h"
+#include "lfmath.h"
+
+int main() {
+    int i = 1, j = 2;
+    ASSERT_EQ(max_int(i, j), j);
+    ASSERT_EQ(min_int(i, j), i);
+
+    char *s = "10101101";
+    ASSERT_EQ(binstr_to_int(s), 173);
+
+    char *s2 = "1010_1101";
+    ASSERT_EQ(binstr_to_int(s2), 173);
+
+    i = 10;
+    i = clamp_int(i, 2, 5);
+    ASSERT_EQ(i, 5);
+
+    size_t sz = 0;
+    Point *line = bresenham(0, 0, 2, 5, &sz);
+    ASSERT_TRUE(sz > 0);
+    ASSERT_NOT_NULL(line);
+    free(line);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + 11d06ce65252457ffd479c6d1f893f96c1b21ca5 (mode 644)
--- /dev/null
+++ tests/test_memory.c
@@ -0,0 +1,92 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "lftest.h"
+#include "lfmemory.h"
+
+int main() {
+    ArenaAllocator *arena = malloc(sizeof(ArenaAllocator));
+    arena_init(arena, 1024);
+
+    int *i1 = arena_malloc(arena, sizeof(int));
+    int *i2 = arena_malloc(arena, sizeof(int));
+
+    *i1 = 1;
+    *i2 = 2;
+
+    ASSERT_LT(i1, i2);
+    ASSERT_LT(*i1, *i2);
+
+    long *l = arena_resize(arena, i1, sizeof(int), sizeof(long));
+    ASSERT_EQ(*l, 1);
+
+    unsigned char *char_test = arena_resize(arena, i2, sizeof(int), sizeof(unsigned char));
+    ASSERT_EQ(*char_test, 2);
+
+    /* Test in-place resize (most recent allocation) */
+    int *last = arena_malloc(arena, sizeof(int));
+    *last = 42;
+    long *last_resized = arena_resize(arena, last, sizeof(int), sizeof(long));
+    ASSERT_EQ((void *)last_resized, (void *)last);
+    ASSERT_EQ(*last_resized, 42);
+
+    /* Test resize returns NULL when arena is full */
+    void *too_big = arena_resize(arena, last_resized, sizeof(long), 2048);
+    ASSERT_NULL(too_big);
+
+    /* Test arena_malloc returns NULL when full */
+    void *full = arena_malloc(arena, 2048);
+    ASSERT_NULL(full);
+
+    /* Test arena_clear resets offsets */
+    arena_clear(arena);
+    int *after_clear = arena_malloc(arena, sizeof(int));
+    ASSERT_NOT_NULL(after_clear);
+
+    /* Test arena_resize_buf */
+    arena_resize_buf(arena, 2048);
+    void *big_alloc = arena_malloc(arena, 1500);
+    ASSERT_NOT_NULL(big_alloc);
+
+    arena_free(arena);
+    free(arena);
+
+    /* Test stack-allocated arena */
+    ArenaAllocator stack_arena;
+    arena_init(&stack_arena, 256);
+    int *si = arena_malloc(&stack_arena, sizeof(int));
+    ASSERT_NOT_NULL(si);
+    *si = 99;
+    ASSERT_EQ(*si, 99);
+    arena_free(&stack_arena);
+
+    PoolAllocator *pool = malloc(sizeof(PoolAllocator));
+    pool_init(pool, 64, 16, LF_DEFAULT_ALIGNMENT);
+    void *a = pool_alloc(pool);
+    void *b = pool_alloc(pool);
+    void *c = pool_alloc(pool);
+    void *d = pool_alloc(pool);
+
+    ASSERT_NOT_NULL(a);
+    ASSERT_NOT_NULL(b);
+    ASSERT_NOT_NULL(c);
+    ASSERT_NOT_NULL(d);
+
+    ASSERT_EQ(pool_count_available(pool), 0);
+    pool_free(pool, d);
+    ASSERT_EQ(pool_count_available(pool), 1);
+
+    /* Test pool_free_all resets properly */
+    pool_free_all(pool);
+    size_t chunk_count = 64 / 16;
+    ASSERT_EQ(pool_count_available(pool), chunk_count);
+
+    /* Allocate again after free_all to verify no corruption */
+    a = pool_alloc(pool);
+    ASSERT_NOT_NULL(a);
+    b = pool_alloc(pool);
+    ASSERT_NOT_NULL(b);
+
+    pool_destroy(pool);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + 9fa89c42d0b00b733699a8d0ad057431232d3eda (mode 644)
--- /dev/null
+++ tests/test_network.c
@@ -0,0 +1,67 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <pthread.h>
+#include "lftest.h"
+#include "lfnetwork.h"
+#include "lfinput.h"
+
+#define NET_MSG "TEST SEND"
+
+static void tcp_test_handler(Server *s) {
+    struct sockaddr_storage client_addr;
+    socklen_t client_addr_sz = sizeof(client_addr);
+    int new_fd = accept(s->fd, (struct sockaddr *) &client_addr, &client_addr_sz);
+    ASSERT_NEQ(new_fd, -1);
+    ASSERT_NEQ((int)send(new_fd, NET_MSG, 10, 0), -1);
+    close(new_fd);
+}
+
+static void *tcp_server_thread(void *vargp) {
+    (void)vargp;
+    Server *server = new_server(SERVERTYPE_TCP, "18632", tcp_test_handler);
+    serve(server, DEFAULT_BACKLOG);
+    delete_server(server);
+    return NULL;
+}
+
+static void udp_test_handler(Server *s) {
+    struct sockaddr_storage client_addr;
+    socklen_t client_addr_sz = sizeof(client_addr);
+    char recv_buf[128];
+
+    int r = (int) recvfrom(s->fd, recv_buf, 128, 0, (struct sockaddr *) &client_addr, &client_addr_sz);
+    ASSERT_TRUE(r > 0);
+    ASSERT_STR_EQ(recv_buf, NET_MSG);
+}
+
+static void *udp_server_thread(void *vargp) {
+    (void)vargp;
+    Server *server = new_server(SERVERTYPE_UDP, "18633", udp_test_handler);
+    serve(server, DEFAULT_BACKLOG);
+    delete_server(server);
+    return NULL;
+}
+
+int main() {
+    pthread_t srv_tid;
+    pthread_create(&srv_tid, NULL, tcp_server_thread, NULL);
+
+    sleep(1);
+    const char *s = capture_system("echo hello | nc localhost 18632", 0);
+    ASSERT_STR_EQ(s, NET_MSG);
+    free((char *) s);
+
+    pthread_join(srv_tid, NULL);
+
+    pthread_create(&srv_tid, NULL, udp_server_thread, NULL);
+    sleep(1);
+    system("echo hello | nc localhost 18633");
+
+    pthread_join(srv_tid, NULL);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + 57abe442572fbbbc6526d2271a7729463d8b5d61 (mode 644)
--- /dev/null
+++ tests/test_parsing.c
@@ -0,0 +1,11 @@
+#include <stdio.h>
+#include "lftest.h"
+#include "lfparsing.h"
+
+int main() {
+    char *nonsense = "8d82jI|dms~<>s2d";
+    char *english = "This is an English sentence!";
+    ASSERT_TRUE(simple_english_scoring(english) > simple_english_scoring(nonsense));
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + 278cc30436e944c2424022d7e15b7ed2b6d3e657 (mode 644)
--- /dev/null
+++ tests/test_set.c
@@ -0,0 +1,56 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "lftest.h"
+#include "lfset.h"
+
+static int int_match(const void *a, const void *b) {
+    return *((int *) a) == *((int *) b);
+}
+
+int main() {
+    Set *set = malloc(sizeof(Set));
+    set_init(set, int_match, NULL);
+
+    int i = 1;
+    int j = 2;
+    int k = 2;
+
+    set_insert(set, &i);
+    set_insert(set, &j);
+    set_insert(set, &k);
+
+    ASSERT_EQ(set->size, 2);  /* duplicate should not be inserted */
+
+    int i2 = 1;
+    int j2 = 4;
+
+    Set *set2 = malloc(sizeof(Set));
+    set_init(set2, int_match, NULL);
+    set_insert(set2, &i2);
+    set_insert(set2, &j2);
+
+    Set *set_u = malloc(sizeof(Set));
+    Set *set_i = malloc(sizeof(Set));
+    Set *set_d = malloc(sizeof(Set));
+
+    set_union(set_u, set, set2);
+    set_difference(set_d, set, set2);
+    set_intersection(set_i, set, set2);
+
+    ASSERT_EQ(set_u->size, 3);  /* {1, 2, 4} */
+    ASSERT_EQ(set_d->size, 1);  /* {2} */
+    ASSERT_EQ(set_i->size, 1);  /* {1} */
+
+    set_destroy(set);
+    set_destroy(set2);
+    set_destroy(set_u);
+    set_destroy(set_i);
+    set_destroy(set_d);
+    free(set);
+    free(set2);
+    free(set_u);
+    free(set_i);
+    free(set_d);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + ae60a2174307462510938d3759e5dd662d312bdf (mode 644)
--- /dev/null
+++ tests/test_stack.c
@@ -0,0 +1,27 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "lftest.h"
+#include "lfstack.h"
+
+int main() {
+    Stack *stack = malloc(sizeof(Stack));
+    stack_init(stack, NULL);
+
+    int a = 1, b = 2;
+    stack_push(stack, &a);
+    stack_push(stack, &b);
+    ASSERT_EQ(stack->size, 2);
+
+    int *p = NULL;
+    stack_pop(stack, (void **) &p);
+    ASSERT_EQ(*p, 2);
+
+    stack_pop(stack, (void **) &p);
+    ASSERT_EQ(*p, 1);
+    ASSERT_EQ(stack->size, 0);
+
+    stack_destroy(stack);
+    free(stack);
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + ba720d22e8bd1104d7cf1fc2952c5df00d8f4b50 (mode 644)
--- /dev/null
+++ tests/test_string.c
@@ -0,0 +1,52 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "lftest.h"
+#include "lfstring.h"
+
+int main() {
+    const char *haystack =
+            "Test one two one and also maybe two but not Gabe's least favorite number, which is not one.";
+    const char *needles[] = {
+        "one",
+        "two",
+        "Gabe"
+    };
+
+    size_t sub_sz = 0;
+    size_t *subs = NULL;
+    find_substrings(haystack, needles[0], &sub_sz, &subs);
+
+    ASSERT_EQ(sub_sz, 3);
+    ASSERT_EQ(subs[0], 5);
+    ASSERT_EQ(subs[1], 13);
+    ASSERT_EQ(subs[2], 87);
+
+    char *s = substr(haystack, subs[0], strlen(needles[0]));
+    ASSERT_STR_EQ(s, needles[0]);
+
+    free(s);
+    free(subs);
+    subs = NULL;
+
+    find_substrings(haystack, needles[1], &sub_sz, &subs);
+    ASSERT_EQ(sub_sz, 2);
+    ASSERT_EQ(subs[0], 9);
+
+    free(subs);
+    subs = NULL;
+
+    find_substrings("test one two", "nope", &sub_sz, &subs);
+    ASSERT_EQ(sub_sz, 0);
+    ASSERT_NULL(subs);
+    free(subs);
+    subs = NULL;
+
+    find_substrings("123", "nopes", &sub_sz, &subs);
+    ASSERT_EQ(sub_sz, 0);
+    ASSERT_NULL(subs);
+    free(subs);
+    subs = NULL;
+
+    TEST_REPORT();
+}
blob - /dev/null
blob + efca273644b392b0885d8e9f10840b81d4e34dc7 (mode 644)
--- /dev/null
+++ tests/test_vector.c
@@ -0,0 +1,62 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include "lftest.h"
+#include "lfvector.h"
+
+int main() {
+    Vector *v = malloc(sizeof(Vector));
+    vec_init(v, NULL);
+
+    int e0 = 0;
+    int e1 = 1;
+    int e2 = 2;
+    int e3 = 3;
+    int e4 = 4;
+
+    vec_push(v, &e0);
+    ASSERT_EQ(v->length, 1);
+    int *t = vec_at(v, 0);
+    ASSERT_EQ(*t, 0);
+
+    vec_push(v, &e1);
+    vec_push(v, &e2);
+    ASSERT_EQ(v->length, 3);
+
+    t = (int *) vec_safe_at(v, 3);
+    ASSERT_NULL(t);
+
+    vec_push(v, &e3);
+    vec_insert(v, &e4, 1);
+
+    t = (int *) vec_at(v, 4);
+    ASSERT_EQ(*t, e3);
+    t = (int *) vec_at(v, 1);
+    ASSERT_EQ(*t, e4);
+
+    const int *min = vec_min(v, vec_cmp_int);
+    const int *max = vec_max(v, vec_cmp_int);
+    ASSERT_EQ(*min, e0);
+    ASSERT_EQ(*max, e4);
+
+    t = (int *) vec_remove(v, 1);
+    ASSERT_NOT_NULL(t);
+    ASSERT_EQ(*t, 4);
+
+    t = (int *) vec_remove(v, 10);
+    ASSERT_NULL(t);
+
+    vec_shrink(v);
+    ASSERT_EQ(vec_len(v), vec_cap(v));
+
+    vec_clear(v);
+    ASSERT_EQ(vec_len(v), 0);
+
+    vec_grow_to(v, 10);
+    ASSERT_EQ(vec_cap(v), 10);
+    ASSERT_EQ(vec_len(v), 0);
+
+    vec_destroy(v);
+    free(v);
+
+    TEST_REPORT();
+}