diff --git a/tests/core_cunit.c b/tests/core_cunit.c new file mode 100644 index 0000000..d8ce75e --- /dev/null +++ b/tests/core_cunit.c @@ -0,0 +1,114 @@ +#include +#include +#include +#include +#include + +#include "cache.h" +#include "linalg.h" +#include "test_utils.h" +#include "utils.h" + +/** + * @file core_cunit.c + * @brief CUnit tests for core linear algebra and cache functionalities. + */ + +#include +#include +#include +#include +#include + +#include "cache.h" +#include "linalg.h" +#include "test_utils.h" +#include "utils.h" + +/** + * @brief Tests the creation and freeing of a matrix. + * Verifies that `create_matrix` allocates memory correctly and `free_matrix` + * deallocates it without issues. + */ +void test_create_free_matrix() { + Matrix* m = create_matrix(2, 3); + CU_ASSERT_PTR_NOT_NULL(m); + CU_ASSERT_EQUAL(m->rows, 2); + CU_ASSERT_EQUAL(m->cols, 3); + free_matrix(m); +} + +/** + * @brief Tests the element-wise addition of two matrices. + * Verifies that `add_matrix` correctly sums corresponding elements of two + * matrices. + */ +void test_add_matrix() { + Matrix* a = create_matrix(2, 2); + Matrix* b = create_matrix(2, 2); + Matrix* expected = create_matrix(2, 2); + + a->matrix_data[0] = 1; + a->matrix_data[1] = 2; + a->matrix_data[2] = 3; + a->matrix_data[3] = 4; + + b->matrix_data[0] = 5; + b->matrix_data[1] = 6; + b->matrix_data[2] = 7; + b->matrix_data[3] = 8; + + expected->matrix_data[0] = 6; + expected->matrix_data[1] = 8; + expected->matrix_data[2] = 10; + expected->matrix_data[3] = 12; + + Matrix* result = add_matrix(a, b); + CU_ASSERT_TRUE(compare_matrices(result, expected, 1e-9)); + + free_matrix(a); + free_matrix(b); + free_matrix(expected); + free_matrix(result); +} + +/** + * @brief Tests the functionality of the cache (put and get operations). + * Verifies that matrices can be stored and retrieved correctly from the cache, + * and that updates work as expected. + */ +void test_cache_functionality() { + Cache* cache = create_cache(); + CU_ASSERT_PTR_NOT_NULL(cache); + + Matrix* m1 = create_matrix(1, 1); + m1->matrix_data[0] = 10.0; + cache_put(cache, "test_key", m1); // cache_put takes ownership of m1 + + Matrix* retrieved = cache_get(cache, "test_key"); + CU_ASSERT_PTR_NOT_NULL(retrieved); + CU_ASSERT_DOUBLE_EQUAL(retrieved->matrix_data[0], 10.0, 1e-9); + + // Test updating a cached value + Matrix* m2 = create_matrix(1, 1); + m2->matrix_data[0] = 20.0; + cache_put(cache, "test_key", + m2); // cache_put takes ownership of m2, frees old m1 + + Matrix* updated_retrieved = cache_get(cache, "test_key"); + CU_ASSERT_PTR_NOT_NULL(updated_retrieved); + CU_ASSERT_DOUBLE_EQUAL(updated_retrieved->matrix_data[0], 20.0, 1e-9); + + free_matrix(retrieved); + free_matrix(updated_retrieved); + free_cache(cache); +} + +/** + * @brief Array of CU_TestInfo structures for core tests. + */ +CU_TestInfo core_tests[] = { + {"test_create_free_matrix", test_create_free_matrix}, + {"test_add_matrix", test_add_matrix}, + {"test_cache_functionality", test_cache_functionality}, + CU_TEST_INFO_NULL}; diff --git a/tests/cunit_runner.c b/tests/cunit_runner.c new file mode 100644 index 0000000..9d27459 --- /dev/null +++ b/tests/cunit_runner.c @@ -0,0 +1,62 @@ +/** + * @file cunit_runner.c + * @brief Main test runner for CUnit tests. + * This file initializes the CUnit test registry, adds test suites, and runs all + * tests. + */ + +#include +#include +#include + +// Declare test suites from other test files +extern CU_TestInfo core_tests[]; +extern CU_TestInfo nn_tests[]; + +/** + * @brief Adds a suite of tests to the CUnit registry. + * @param suite_name The name of the test suite. + * @param tests An array of CU_TestInfo structures defining the tests in the + * suite. + * @return 0 on success, or a CUnit error code on failure. + */ +int add_suite(const char* suite_name, CU_TestInfo tests[]) { + CU_pSuite pSuite = NULL; + pSuite = CU_add_suite(suite_name, NULL, NULL); + if (NULL == pSuite) { + CU_cleanup_registry(); + return CU_get_error(); + } + for (int i = 0; tests[i].pName != NULL; i++) { + if (NULL == CU_add_test(pSuite, tests[i].pName, tests[i].pTestFunc)) { + CU_cleanup_registry(); + return CU_get_error(); + } + } + return 0; +} + +/** + * @brief Main function for running CUnit tests. + * Initializes the registry, adds all defined test suites, runs tests, and + * cleans up. + * @return The CUnit error code after test execution. + */ +int main() { + // Initialize the CUnit test registry + if (CUE_SUCCESS != CU_initialize_registry()) { + return CU_get_error(); + } + + // Add suites + if (add_suite("Core Tests", core_tests) != 0) return CU_get_error(); + if (add_suite("Neural Network Tests", nn_tests) != 0) return CU_get_error(); + + // Run all tests using the Basic interface + CU_basic_set_mode(CU_BRM_VERBOSE); + CU_basic_run_tests(); + + // Clean up registry and return results + CU_cleanup_registry(); + return CU_get_error(); +} diff --git a/tests/main.c b/tests/main.c new file mode 100644 index 0000000..53465e2 --- /dev/null +++ b/tests/main.c @@ -0,0 +1,294 @@ +/** + * @file main.c + * @brief Main test file for various neural network components. + * This file contains individual test functions and a main runner to execute + * them. + */ + +#include +#include +#include +#include + +#include "activation.h" +#include "cache.h" +#include "linalg.h" +#include "neural_network.h" +#include "utils.h" + +#define TEST_ASSERT(condition, message) \ + do { \ + if (!(condition)) { \ + LOG_ERROR("TEST FAILED: %s", message); \ + return 0; \ + } \ + } while (0) + +/** + * @brief Macro to run a single test function. + * Executes the given test function and reports its success or failure. + * @param test_func The name of the test function to run. + */ +#define RUN_TEST(test_func) \ + do { \ + printf("Running %s...\n", #test_func); \ + if (test_func()) { \ + printf(" %s: PASSED\n", #test_func); \ + tests_passed++; \ + } else { \ + printf(" %s: FAILED\n", #test_func); \ + tests_failed++; \ + } \ + tests_run++; \ + } while (0) + +static int tests_run = 0; +static int tests_passed = 0; +static int tests_failed = 0; + +/** + * @brief Helper function to compare two matrices for approximate equality. + * @param m1 Pointer to the first matrix. + * @param m2 Pointer to the second matrix. + * @param epsilon The maximum allowed difference between corresponding elements. + * @return 1 if the matrices are approximately equal, 0 otherwise. + */ +int compare_matrices(Matrix* m1, Matrix* m2, double epsilon) { + if (m1 == NULL || m2 == NULL) return 0; + if (m1->rows != m2->rows || m1->cols != m2->cols) return 0; + + size_t total_elements = m1->rows * m1->cols; + for (size_t i = 0; i < total_elements; i++) { + if (fabs(m1->matrix_data[i] - m2->matrix_data[i]) > epsilon) { + return 0; + } + } + return 1; +} + +/** + * @brief Tests the creation and freeing of a matrix. + * Verifies that `create_matrix` allocates memory correctly and `free_matrix` + * deallocates it without issues. + * @return 1 on success, 0 on failure. + */ +int test_create_free_matrix() { + Matrix* m = create_matrix(2, 3); + TEST_ASSERT(m != NULL, "create_matrix returned NULL"); + TEST_ASSERT(m->rows == 2 && m->cols == 3, + "create_matrix dimensions incorrect"); + free_matrix(m); + return 1; +} + +/** + * @brief Tests the element-wise addition of two matrices. + * Verifies that `add_matrix` correctly sums corresponding elements of two + * matrices. + * @return 1 on success, 0 on failure. + */ +int test_add_matrix() { + Matrix* a = create_matrix(2, 2); + Matrix* b = create_matrix(2, 2); + Matrix* expected = create_matrix(2, 2); + + a->matrix_data[0] = 1; + a->matrix_data[1] = 2; + a->matrix_data[2] = 3; + a->matrix_data[3] = 4; + + b->matrix_data[0] = 5; + b->matrix_data[1] = 6; + b->matrix_data[2] = 7; + b->matrix_data[3] = 8; + + expected->matrix_data[0] = 6; + expected->matrix_data[1] = 8; + expected->matrix_data[2] = 10; + expected->matrix_data[3] = 12; + + Matrix* result = add_matrix(a, b); + TEST_ASSERT(compare_matrices(result, expected, 1e-9), "add_matrix failed"); + + free_matrix(a); + free_matrix(b); + free_matrix(expected); + free_matrix(result); + return 1; +} + +/** + * @brief Tests the dot product (matrix multiplication) of two matrices. + * Verifies that `dot_matrix` correctly computes the matrix product. + * @return 1 on success, 0 on failure. + */ +int test_dot_matrix() { + Matrix* a = create_matrix(2, 2); + Matrix* b = create_matrix(2, 2); + Matrix* expected = create_matrix(2, 2); + + a->matrix_data[0] = 1; + a->matrix_data[1] = 2; + a->matrix_data[2] = 3; + a->matrix_data[3] = 4; + + b->matrix_data[0] = 5; + b->matrix_data[1] = 6; + b->matrix_data[2] = 7; + b->matrix_data[3] = 8; + + expected->matrix_data[0] = 19; + expected->matrix_data[1] = 22; + expected->matrix_data[2] = 43; + expected->matrix_data[3] = 50; + + Matrix* result = dot_matrix(a, b); + TEST_ASSERT(compare_matrices(result, expected, 1e-9), "dot_matrix failed"); + + free_matrix(a); + free_matrix(b); + free_matrix(expected); + free_matrix(result); + return 1; +} + +/** + * @brief Tests the transposition of a matrix. + * Verifies that `transpose_matrix` correctly computes the transpose of a given + * matrix. + * @return 1 on success, 0 on failure. + */ +int test_transpose_matrix() { + Matrix* m = create_matrix(2, 3); + Matrix* expected = create_matrix(3, 2); + + m->matrix_data[0] = 1; + m->matrix_data[1] = 2; + m->matrix_data[2] = 3; + m->matrix_data[3] = 4; + m->matrix_data[4] = 5; + m->matrix_data[5] = 6; + + expected->matrix_data[0] = 1; + expected->matrix_data[1] = 4; + expected->matrix_data[2] = 2; + expected->matrix_data[3] = 5; + expected->matrix_data[4] = 3; + expected->matrix_data[5] = 6; + + Matrix* result = transpose_matrix(m); + TEST_ASSERT(compare_matrices(result, expected, 1e-9), + "transpose_matrix failed"); + + free_matrix(m); + free_matrix(expected); + free_matrix(result); + return 1; +} + +/** + * @brief Tests the matrix_argmax function. + * Verifies that `matrix_argmax` returns the correct index of the maximum + * element. + * @return 1 on success, 0 on failure. + */ +int test_matrix_argmax() { + Matrix* m = create_matrix(1, 5); + m->matrix_data[0] = 0.1; + m->matrix_data[1] = 0.9; + m->matrix_data[2] = 0.2; + m->matrix_data[3] = 0.8; + m->matrix_data[4] = 0.5; + + size_t argmax_idx = matrix_argmax(m); + TEST_ASSERT(argmax_idx == 1, "matrix_argmax failed"); + + free_matrix(m); + return 1; +} + +/** + * @brief Tests the sigmoid activation function. + * Verifies that the sigmoid function produces correct outputs for given inputs. + * @return 1 on success, 0 on failure. + */ +int test_sigmoid() { + Matrix* m = create_matrix(1, 2); + Matrix* expected = create_matrix(1, 2); + m->matrix_data[0] = 0; + m->matrix_data[1] = 1; + expected->matrix_data[0] = 0.5; + expected->matrix_data[1] = 1.0 / (1.0 + exp(-1.0)); // approx 0.731 + + Matrix* result = sigmoid(m); + TEST_ASSERT(compare_matrices(result, expected, 1e-9), "sigmoid failed"); + + free_matrix(m); + free_matrix(expected); + free_matrix(result); + return 1; +} + +/** + * @brief Tests the basic put and get functionality of the cache. + * Verifies that matrices can be stored, retrieved, and updated correctly. + * @return 1 on success, 0 on failure. + */ +int test_cache_functionality() { + Cache* cache = create_cache(); + TEST_ASSERT(cache != NULL, "create_cache failed"); + + Matrix* m1 = create_matrix(1, 1); + m1->matrix_data[0] = 10.0; + cache_put(cache, "test_key", m1); // cache_put takes ownership of m1 + + Matrix* retrieved = cache_get(cache, "test_key"); + TEST_ASSERT(retrieved != NULL, "cache_get failed to retrieve matrix"); + TEST_ASSERT(retrieved->matrix_data[0] == 10.0, + "cached matrix data incorrect"); + + // Test updating a cached value + Matrix* m2 = create_matrix(1, 1); + m2->matrix_data[0] = 20.0; + cache_put(cache, "test_key", + m2); // cache_put takes ownership of m2, frees old m1 + + Matrix* updated_retrieved = cache_get(cache, "test_key"); + TEST_ASSERT(updated_retrieved != NULL, + "cache_get failed to retrieve updated matrix"); + TEST_ASSERT(updated_retrieved->matrix_data[0] == 20.0, + "updated cached matrix data incorrect"); + + free_matrix(retrieved); + free_matrix(updated_retrieved); + free_cache(cache); + return 1; +} + +/** + * @brief Main function for the test suite. + * Runs all individual test functions and prints a summary of the results. + * @return EXIT_SUCCESS if all tests pass, EXIT_FAILURE otherwise. + */ +int main() { + printf("Starting all tests...\n"); + + RUN_TEST(test_create_free_matrix); + RUN_TEST(test_add_matrix); + RUN_TEST(test_dot_matrix); + RUN_TEST(test_transpose_matrix); + RUN_TEST(test_matrix_argmax); + RUN_TEST(test_sigmoid); + RUN_TEST(test_cache_functionality); + + printf("\n--- Test Summary ---\n"); + printf("Total tests run: %d\n", tests_run); + printf("Tests passed: %d\n", tests_passed); + printf("Tests failed: %d\n", tests_failed); + + if (tests_failed > 0) { + return EXIT_FAILURE; + } else { + return EXIT_SUCCESS; + } +} diff --git a/tests/nn_cunit.c b/tests/nn_cunit.c new file mode 100644 index 0000000..3f1a9e5 --- /dev/null +++ b/tests/nn_cunit.c @@ -0,0 +1,159 @@ +/** + * @file nn_cunit.c + * @brief CUnit tests for the neural network core functionalities. + */ + +#include +#include +#include +#include +#include + +#include "activation.h" +#include "backprop.h" +#include "feedforward.h" +#include "linalg.h" +#include "loss.h" +#include "neural_network.h" +#include "test_utils.h" +#include "utils.h" + +/** + * @brief Tests the sigmoid activation function. + * Verifies that the sigmoid function produces correct outputs for given inputs. + */ +void test_sigmoid() { + Matrix* m = create_matrix(1, 2); + Matrix* expected = create_matrix(1, 2); + m->matrix_data[0] = 0; + m->matrix_data[1] = 1; + expected->matrix_data[0] = 0.5; + expected->matrix_data[1] = 1.0 / (1.0 + exp(-1.0)); // approx 0.731 + + Matrix* result = sigmoid(m); + CU_ASSERT_TRUE(compare_matrices(result, expected, 1e-9)); + + free_matrix(m); + free_matrix(expected); + free_matrix(result); +} + +/** + * @brief Tests the creation and freeing of a neural network. + * Verifies that `create_network` allocates memory correctly and `free_network` + * deallocates it without issues. + */ +void test_create_free_network() { + NeuralNetwork* nn = create_network(2); + CU_ASSERT_PTR_NOT_NULL(nn); + CU_ASSERT_EQUAL(nn->num_layers, 2); + CU_ASSERT_PTR_NOT_NULL(nn->cache); + free_network(nn); +} + +/** + * @brief Tests a simple feedforward pass through a neural network. + * Sets up a basic network and verifies the output of a forward pass. + */ +void test_feedforward_simple() { + NeuralNetwork* nn = create_network(1); + CU_ASSERT_PTR_NOT_NULL(nn); + + // Create a simple layer + Layer* layer = (Layer*)malloc(sizeof(Layer)); + CU_ASSERT_PTR_NOT_NULL(layer); + layer->weights = create_matrix(2, 1); // 2 inputs, 1 output + layer->weights->matrix_data[0] = 0.5; + layer->weights->matrix_data[1] = 0.5; + layer->bias = create_matrix(1, 1); + layer->bias->matrix_data[0] = 0.1; + layer->activation_type = SIGMOID; + layer->leak_parameter = 0.0; // Not used for sigmoid + nn->layers[0] = layer; + + // Input matrix + Matrix* input = create_matrix(1, 2); // 1 sample, 2 features + input->matrix_data[0] = 1.0; + input->matrix_data[1] = 1.0; + + // Expected output: sigmoid((1*0.5) + (1*0.5) + 0.1) = sigmoid(1.1) + Matrix* expected_output = create_matrix(1, 1); + expected_output->matrix_data[0] = 1.0 / (1.0 + exp(-1.1)); + + Matrix* output = feedforward(nn, input); + CU_ASSERT_PTR_NOT_NULL(output); + CU_ASSERT_TRUE(compare_matrices(output, expected_output, 1e-9)); + + free_matrix(input); + free_matrix(output); + free_matrix(expected_output); + free_network(nn); +} + +/** + * @brief Tests the backpropagation algorithm with Softmax activation and + * Categorical Cross-Entropy loss. Verifies that the delta calculated during + * backpropagation is correct for this specific combination. + */ +void test_backpropagate_softmax_cce() { + NeuralNetwork* nn = create_network(1); + CU_ASSERT_PTR_NOT_NULL(nn); + + // Create a simple layer with Softmax activation + Layer* layer = (Layer*)malloc(sizeof(Layer)); + CU_ASSERT_PTR_NOT_NULL(layer); + layer->weights = create_matrix(2, 2); // 2 inputs, 2 outputs + fill_matrix(layer->weights, 0.5); + layer->bias = create_matrix(1, 2); + fill_matrix(layer->bias, 0.1); + layer->activation_type = SOFTMAX; + layer->leak_parameter = 0.0; + nn->layers[0] = layer; + + // Input matrix + Matrix* input = create_matrix(1, 2); // 1 sample, 2 features + input->matrix_data[0] = 1.0; + input->matrix_data[1] = 1.0; + + // True labels + Matrix* y_true = create_matrix(1, 2); + y_true->matrix_data[0] = 0.0; + y_true->matrix_data[1] = 1.0; + + // Run feedforward to populate cache + Matrix* output = feedforward(nn, input); + CU_ASSERT_PTR_NOT_NULL(output); + + // Expected delta for softmax + CCE: y_hat - y_true + Matrix* expected_delta = subtract_matrix(output, y_true); + + // Run backpropagate + backpropagate( + nn, y_true, CCE, + categorical_cross_entropy_gradient); // CCE is the LossFunctionType + + // Retrieve delta from cache + char delta_key[32]; + sprintf(delta_key, "delta_%zu", (size_t)0); + Matrix* actual_delta = cache_get(nn->cache, delta_key); + + CU_ASSERT_PTR_NOT_NULL(actual_delta); + CU_ASSERT_TRUE(compare_matrices(actual_delta, expected_delta, 1e-9)); + + free_matrix(input); + free_matrix(y_true); + free_matrix(output); + free_matrix(expected_delta); + free_matrix(actual_delta); + free_network(nn); +} + +/** + * @brief Array of CU_TestInfo structures for neural network tests. + */ +CU_TestInfo nn_tests[] = { + {"test_sigmoid", test_sigmoid}, + {"test_create_free_network", test_create_free_network}, + {"test_feedforward_simple", test_feedforward_simple}, + {"test_backpropagate_softmax_cce", test_backpropagate_softmax_cce}, + CU_TEST_INFO_NULL}; diff --git a/tests/test_utils.c b/tests/test_utils.c new file mode 100644 index 0000000..eb356c7 --- /dev/null +++ b/tests/test_utils.c @@ -0,0 +1,28 @@ +/** + * @file test_utils.c + * @brief Utility functions for testing purposes. + */ + +#include "test_utils.h" + +#include + +/** + * @brief Compares two matrices for approximate equality. + * @param m1 Pointer to the first matrix. + * @param m2 Pointer to the second matrix. + * @param epsilon The maximum allowed difference between corresponding elements. + * @return 1 if the matrices are approximately equal, 0 otherwise. + */ +int compare_matrices(Matrix* m1, Matrix* m2, double epsilon) { + if (m1 == NULL || m2 == NULL) return 0; + if (m1->rows != m2->rows || m1->cols != m2->cols) return 0; + + size_t total_elements = m1->rows * m1->cols; + for (size_t i = 0; i < total_elements; i++) { + if (fabs(m1->matrix_data[i] - m2->matrix_data[i]) > epsilon) { + return 0; + } + } + return 1; +} diff --git a/tests/test_utils.h b/tests/test_utils.h new file mode 100644 index 0000000..b14da32 --- /dev/null +++ b/tests/test_utils.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "linalg.h" + +/** + * @file test_utils.h + * @brief Header for utility functions used in tests. + */ + +#pragma once + +#include + +#include "linalg.h" + +/** + * @brief Helper function to compare two matrices for CUnit assertions. + * @param m1 Pointer to the first matrix. + * @param m2 Pointer to the second matrix. + * @param epsilon The maximum allowed difference between corresponding elements. + * @return 1 if the matrices are approximately equal, 0 otherwise. + */ +int compare_matrices(Matrix* m1, Matrix* m2, double epsilon);