From 4c897d243372670bdd23ac60ed11d8657c56cc0f Mon Sep 17 00:00:00 2001 From: Nils Homer Date: Mon, 22 Dec 2025 16:10:43 -0700 Subject: [PATCH] feat: add unit testing infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a lightweight unit test framework for DWGSIM: - tests/test_framework.h: Minimal test macros (ASSERT, ASSERT_EQ, etc.) - tests/test_main.c: Test runner with framework verification tests - Makefile updates: - `make test` runs both unit and integration tests - `make test-unit` runs only unit tests - `make test-integration` runs only integration tests - `make clean-tests` cleans test artifacts - `clean` target now includes `clean-tests` - .gitignore: Ignore test build artifacts - CI workflow: Run unit tests explicitly in pipeline, upgrade actions/checkout to v4 Closes #94 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/testing.yml | 8 +- .gitignore | 2 + Makefile | 26 +++- tests/test_framework.h | 224 ++++++++++++++++++++++++++++++++++ tests/test_main.c | 80 ++++++++++++ 5 files changed, 334 insertions(+), 6 deletions(-) create mode 100644 tests/test_framework.h create mode 100644 tests/test_main.c diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 72f2cf3..024cd5a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -12,11 +12,13 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: submodules: 'recursive' - name: Build run: make - - name: Test - run: make test + - name: Unit Tests + run: make test-unit + - name: Integration Tests + run: make test-integration diff --git a/.gitignore b/.gitignore index a568fbe..8097af9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ src/*.o dwgsim* +tests/*.o +tests/run_tests diff --git a/Makefile b/Makefile index 7c32d45..fddb491 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ all-recur lib-recur clean-recur cleanlocal-recur install-recur: all:$(PROG) -.PHONY:all lib clean cleanlocal +.PHONY:all lib clean cleanlocal test test-unit test-integration clean-tests .PHONY:all-recur lib-recur clean-recur cleanlocal-recur install-recur dwgsim:lib-recur $(DWGSIM_AOBJS) @@ -54,7 +54,7 @@ cleanlocal: fi; \ done; -clean:cleanlocal-recur +clean:cleanlocal-recur clean-tests dist:clean if [ -f dwgsim-${PACKAGE_VERSION}.tar.gz ]; then \ @@ -72,6 +72,26 @@ dist:clean gzip -9 dwgsim-${PACKAGE_VERSION}.tar; \ rm -rv dwgsim-${PACKAGE_VERSION}; -test: +# Run all tests (unit + integration) +test: test-unit test-integration + +# Integration tests +test-integration: if [ -d tmp ]; then rm -r tmp; fi /bin/bash testdata/test.sh + +# Unit test target +TEST_OBJS = tests/test_main.o +TEST_PROG = tests/run_tests + +tests/test_main.o: tests/test_main.c tests/test_framework.h + $(CC) -c $(CFLAGS) $(DFLAGS) -I. -Isrc tests/test_main.c -o $@ + +$(TEST_PROG): $(TEST_OBJS) + $(CC) $(CFLAGS) -o $@ $(TEST_OBJS) -lm + +test-unit: $(TEST_PROG) + ./$(TEST_PROG) + +clean-tests: + rm -f tests/*.o $(TEST_PROG) diff --git a/tests/test_framework.h b/tests/test_framework.h new file mode 100644 index 0000000..406a3e5 --- /dev/null +++ b/tests/test_framework.h @@ -0,0 +1,224 @@ +/* + * Minimal Unit Test Framework for DWGSIM + * + * Usage: + * #include "test_framework.h" + * + * TEST(test_name) { + * ASSERT(condition); + * ASSERT_EQ(expected, actual); + * ASSERT_STR_EQ(expected, actual); + * } + * + * int main(void) { + * RUN_TEST(test_name); + * TEST_SUMMARY(); + * return test_failures; + * } + */ + +#ifndef TEST_FRAMEWORK_H +#define TEST_FRAMEWORK_H + +#include +#include +#include +#include + +/* Global test counters */ +static int test_count = 0; +static int test_failures = 0; +static int test_assertions = 0; +static const char *current_test = NULL; + +/* Test function signature */ +typedef void (*test_fn)(void); + +/* Define a test function */ +#define TEST(name) static void name(void) + +/* Run a test */ +#define RUN_TEST(name) do { \ + current_test = #name; \ + test_count++; \ + int prev_failures = test_failures; \ + name(); \ + if (test_failures == prev_failures) { \ + printf(" [PASS] %s\n", #name); \ + } \ +} while (0) + +/* Print test summary */ +#define TEST_SUMMARY() do { \ + printf("\n----------------------------------------\n"); \ + printf("Tests: %d | Passed: %d | Failed: %d | Assertions: %d\n", \ + test_count, test_count - test_failures, test_failures, test_assertions); \ + printf("----------------------------------------\n"); \ +} while (0) + +/* Basic assertion */ +#define ASSERT(cond) do { \ + test_assertions++; \ + if (!(cond)) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Assertion failed: %s\n", #cond); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert with message */ +#define ASSERT_MSG(cond, msg) do { \ + test_assertions++; \ + if (!(cond)) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" %s\n", msg); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert equality for integers */ +#define ASSERT_EQ(expected, actual) do { \ + test_assertions++; \ + long long _exp = (long long)(expected); \ + long long _act = (long long)(actual); \ + if (_exp != _act) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected: %lld, Actual: %lld\n", _exp, _act); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert inequality */ +#define ASSERT_NE(not_expected, actual) do { \ + test_assertions++; \ + long long _nexp = (long long)(not_expected); \ + long long _act = (long long)(actual); \ + if (_nexp == _act) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected NOT: %lld, but got: %lld\n", _nexp, _act); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert string equality */ +#define ASSERT_STR_EQ(expected, actual) do { \ + test_assertions++; \ + const char *_exp = (expected); \ + const char *_act = (actual); \ + if (_exp == NULL || _act == NULL || strcmp(_exp, _act) != 0) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected: \"%s\", Actual: \"%s\"\n", \ + _exp ? _exp : "(null)", _act ? _act : "(null)"); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert floating point equality within epsilon */ +#define ASSERT_FLOAT_EQ(expected, actual, epsilon) do { \ + test_assertions++; \ + double _exp = (double)(expected); \ + double _act = (double)(actual); \ + double _eps = (double)(epsilon); \ + if (fabs(_exp - _act) > _eps) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected: %g, Actual: %g (epsilon: %g)\n", _exp, _act, _eps); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert pointer is not NULL */ +#define ASSERT_NOT_NULL(ptr) do { \ + test_assertions++; \ + if ((ptr) == NULL) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected non-NULL pointer\n"); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert pointer is NULL */ +#define ASSERT_NULL(ptr) do { \ + test_assertions++; \ + if ((ptr) != NULL) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected NULL pointer\n"); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert a < b */ +#define ASSERT_LT(a, b) do { \ + test_assertions++; \ + long long _a = (long long)(a); \ + long long _b = (long long)(b); \ + if (!(_a < _b)) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected %lld < %lld\n", _a, _b); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert a <= b */ +#define ASSERT_LE(a, b) do { \ + test_assertions++; \ + long long _a = (long long)(a); \ + long long _b = (long long)(b); \ + if (!(_a <= _b)) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected %lld <= %lld\n", _a, _b); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert a > b */ +#define ASSERT_GT(a, b) do { \ + test_assertions++; \ + long long _a = (long long)(a); \ + long long _b = (long long)(b); \ + if (!(_a > _b)) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected %lld > %lld\n", _a, _b); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Assert a >= b */ +#define ASSERT_GE(a, b) do { \ + test_assertions++; \ + long long _a = (long long)(a); \ + long long _b = (long long)(b); \ + if (!(_a >= _b)) { \ + printf(" [FAIL] %s\n", current_test); \ + printf(" Expected %lld >= %lld\n", _a, _b); \ + printf(" at %s:%d\n", __FILE__, __LINE__); \ + test_failures++; \ + return; \ + } \ +} while (0) + +/* Test suite name header */ +#define TEST_SUITE(name) printf("\n=== %s ===\n", name) + +#endif /* TEST_FRAMEWORK_H */ diff --git a/tests/test_main.c b/tests/test_main.c new file mode 100644 index 0000000..4df0ccb --- /dev/null +++ b/tests/test_main.c @@ -0,0 +1,80 @@ +/* + * DWGSIM Unit Test Runner + * + * This file runs all unit tests for DWGSIM. + * Individual test files are included below. + */ + +#include "test_framework.h" + +/* Include test files here as they are added */ +/* #include "test_position.c" */ +/* #include "test_memory.c" */ +/* #include "test_parsing.c" */ + +/* + * Placeholder tests to verify the test framework works + */ +TEST(test_framework_assert) { + ASSERT(1 == 1); + ASSERT(0 == 0); +} + +TEST(test_framework_assert_eq) { + ASSERT_EQ(42, 42); + ASSERT_EQ(-1, -1); + ASSERT_EQ(0, 0); +} + +TEST(test_framework_assert_ne) { + ASSERT_NE(1, 2); + ASSERT_NE(-1, 1); +} + +TEST(test_framework_assert_str_eq) { + ASSERT_STR_EQ("hello", "hello"); + ASSERT_STR_EQ("", ""); +} + +TEST(test_framework_assert_float_eq) { + ASSERT_FLOAT_EQ(3.14, 3.14, 0.001); + ASSERT_FLOAT_EQ(0.1 + 0.2, 0.3, 0.0001); +} + +TEST(test_framework_assert_comparisons) { + ASSERT_LT(1, 2); + ASSERT_LE(1, 1); + ASSERT_LE(1, 2); + ASSERT_GT(2, 1); + ASSERT_GE(2, 2); + ASSERT_GE(3, 2); +} + +TEST(test_framework_assert_null) { + int *ptr = NULL; + int val = 42; + ASSERT_NULL(ptr); + ASSERT_NOT_NULL(&val); +} + +int main(void) { + printf("DWGSIM Unit Tests\n"); + printf("========================================\n"); + + TEST_SUITE("Test Framework Verification"); + RUN_TEST(test_framework_assert); + RUN_TEST(test_framework_assert_eq); + RUN_TEST(test_framework_assert_ne); + RUN_TEST(test_framework_assert_str_eq); + RUN_TEST(test_framework_assert_float_eq); + RUN_TEST(test_framework_assert_comparisons); + RUN_TEST(test_framework_assert_null); + + /* Add test suites here as they are created */ + /* TEST_SUITE("Position Calculation Tests"); */ + /* Include tests from test_position.c */ + + TEST_SUMMARY(); + + return test_failures > 0 ? 1 : 0; +}