mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-20 05:49:37 +08:00
The test-impact selector has to hand the C runner individual tests, and the map behind it has to know what each test executes. The runner could do neither: argv selects whole suites. Selection. CBM_TEST_ONLY (comma-separated) and CBM_TEST_ONLY_FILE (one per line; blank lines and # comments allowed) take suite:test tokens, and are unioned when both are set. A suite no token names never executes its body. Inside a named suite only the named tests run; the rest are counted as deselected and the summary reports the count. Argv still selects suites and the tokens narrow inside that selection, so the parallel harness hands every per-suite process the same list with no change of its own. Tokens are strict: one that names a suite this build does not register, or a test its suite never ran, is printed and fails the run. An unreadable file, a malformed token, and a selection that names no test stop the run before any suite. Without a selection the output is byte-identical to before, and --list-suites ignores the selection. Coverage. TEST_COVERAGE=1 compiles clang source coverage into our own code (the one-shot sources, the LSP unity object, the preprocessor; vendored objects stay uninstrumented) and the runner's hooks into the test runner. `make test-runner-cov` and `make cbm-cov` build the instrumented runner and product binary into build/cov. With CBM_TEST_COVERAGE_DIR set the runner writes <suite>/<test>.parent.profraw for itself around each test, points LLVM_PROFILE_FILE at <suite>/<test>.%p.profraw for the children the test spawns, and sends what runs between tests to _setup.<n>.profraw. No other build compiles any of it. tests/test_harness_test_only.sh pins the selection contract against the built runner and runs as Step 2b of scripts/test.sh, before the suites. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
304 lines
18 KiB
C
304 lines
18 KiB
C
/*
|
|
* test_framework.h — Minimal C test framework.
|
|
*
|
|
* Features:
|
|
* - Assertion macros with file:line on failure
|
|
* - Test suite grouping
|
|
* - Pass/fail counting with summary
|
|
* - Color output (when isatty)
|
|
* - Per-test selection (CBM_TEST_ONLY) and per-test coverage profiles
|
|
*
|
|
* Usage:
|
|
* TEST(my_test) {
|
|
* ASSERT_EQ(1 + 1, 2);
|
|
* ASSERT_STR_EQ("hello", "hello");
|
|
* PASS();
|
|
* }
|
|
*
|
|
* SUITE(my_suite) {
|
|
* RUN_TEST(my_test);
|
|
* }
|
|
*
|
|
* int main(void) {
|
|
* RUN_SUITE(my_suite);
|
|
* TEST_SUMMARY();
|
|
* }
|
|
*/
|
|
#ifndef TEST_FRAMEWORK_H
|
|
#define TEST_FRAMEWORK_H
|
|
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
#include <unistd.h>
|
|
|
|
/* ── Global counters (defined in test_main.c) ──────────────────── */
|
|
|
|
extern int tf_pass_count;
|
|
extern int tf_fail_count;
|
|
extern int tf_skip_count;
|
|
extern int tf_deselected_count;
|
|
|
|
/* ── Per-test selection (defined by the runner) ────────────────── */
|
|
|
|
/* CBM_TEST_ONLY / CBM_TEST_ONLY_FILE narrow a run to `suite:test` tokens (see
|
|
* test_main.c). tf_suite_enter decides whether a suite's body executes at all;
|
|
* inside it tf_test_selected decides each RUN_TEST and counts the tests it
|
|
* leaves out. Without a selection both are always true. */
|
|
extern const char *tf_current_suite;
|
|
bool tf_suite_enter(const char *suite);
|
|
void tf_suite_leave(const char *suite);
|
|
bool tf_test_selected(const char *test);
|
|
|
|
/* ── Per-test coverage (CBM_TEST_COVERAGE builds only) ─────────── */
|
|
|
|
/* A coverage-instrumented runner (make test-runner-cov) writes one profile per
|
|
* test under CBM_TEST_COVERAGE_DIR. No other build compiles any of it. */
|
|
#ifdef CBM_TEST_COVERAGE
|
|
void tf_coverage_test_begin(const char *test);
|
|
void tf_coverage_test_end(const char *test);
|
|
#define TF_COVERAGE_TEST_BEGIN(test) tf_coverage_test_begin(test)
|
|
#define TF_COVERAGE_TEST_END(test) tf_coverage_test_end(test)
|
|
#else
|
|
#define TF_COVERAGE_TEST_BEGIN(test) ((void)0)
|
|
#define TF_COVERAGE_TEST_END(test) ((void)0)
|
|
#endif
|
|
|
|
/* ── Color helpers ─────────────────────────────────────────────── */
|
|
|
|
static inline const char *tf_green(void) {
|
|
return isatty(1) ? "\033[32m" : "";
|
|
}
|
|
static inline const char *tf_red(void) {
|
|
return isatty(1) ? "\033[31m" : "";
|
|
}
|
|
static inline const char *tf_dim(void) {
|
|
return isatty(1) ? "\033[90m" : "";
|
|
}
|
|
static inline const char *tf_reset(void) {
|
|
return isatty(1) ? "\033[0m" : "";
|
|
}
|
|
|
|
/* ── Test definition ───────────────────────────────────────────── */
|
|
|
|
#define TEST(name) static int test_##name(void)
|
|
|
|
#define PASS() return 0
|
|
|
|
#define SKIP(reason) \
|
|
do { \
|
|
printf("%sSKIP%s (%s)\n", tf_dim(), tf_reset(), reason); \
|
|
tf_skip_count++; \
|
|
return -1; \
|
|
} while (0)
|
|
|
|
/* Hard failure with a message — for setup/environment failures that must NOT be
|
|
* silently skipped (a test that cannot establish its preconditions has FAILED,
|
|
* not "skipped"). Mirrors the ASSERT failure path (red FAIL + file:line). */
|
|
#define FAIL(reason) \
|
|
do { \
|
|
printf(" %sFAIL%s %s:%d: %s\n", tf_red(), tf_reset(), __FILE__, __LINE__, (reason)); \
|
|
return 1; \
|
|
} while (0)
|
|
|
|
/* The ONLY tolerable skip: a test that is inherently platform-specific and does
|
|
* not apply on the current OS (e.g. a Windows-only test running on macOS).
|
|
* Distinct from SKIP() so the no-skips linter (scripts/check-no-test-skips.sh)
|
|
* can allow these and reject every other skip. Use sparingly + with a reason
|
|
* that names the platform. Prefer compile-gating (#ifdef) where practical. */
|
|
#define SKIP_PLATFORM(reason) \
|
|
do { \
|
|
printf("%sSKIP%s (platform: %s)\n", tf_dim(), tf_reset(), reason); \
|
|
tf_skip_count++; \
|
|
return -1; \
|
|
} while (0)
|
|
|
|
/* ── Assertions ────────────────────────────────────────────────── */
|
|
|
|
#define ASSERT(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
printf(" %sFAIL%s %s:%d: ASSERT(%s)\n", tf_red(), tf_reset(), __FILE__, __LINE__, \
|
|
#cond); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_EQ(a, b) \
|
|
do { \
|
|
long long _a = (long long)(a), _b = (long long)(b); \
|
|
if (_a != _b) { \
|
|
printf(" %sFAIL%s %s:%d: %s == %lld, expected %s == %lld\n", tf_red(), tf_reset(), \
|
|
__FILE__, __LINE__, #a, _a, #b, _b); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_NEQ(a, b) \
|
|
do { \
|
|
long long _a = (long long)(a), _b = (long long)(b); \
|
|
if (_a == _b) { \
|
|
printf(" %sFAIL%s %s:%d: %s == %s (both %lld)\n", tf_red(), tf_reset(), __FILE__, \
|
|
__LINE__, #a, #b, _a); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_GT(a, b) \
|
|
do { \
|
|
long long _a = (long long)(a), _b = (long long)(b); \
|
|
if (_a <= _b) { \
|
|
printf(" %sFAIL%s %s:%d: %s (%lld) not > %s (%lld)\n", tf_red(), tf_reset(), \
|
|
__FILE__, __LINE__, #a, _a, #b, _b); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_GTE(a, b) \
|
|
do { \
|
|
long long _a = (long long)(a), _b = (long long)(b); \
|
|
if (_a < _b) { \
|
|
printf(" %sFAIL%s %s:%d: %s (%lld) not >= %s (%lld)\n", tf_red(), tf_reset(), \
|
|
__FILE__, __LINE__, #a, _a, #b, _b); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_LT(a, b) \
|
|
do { \
|
|
long long _a = (long long)(a), _b = (long long)(b); \
|
|
if (_a >= _b) { \
|
|
printf(" %sFAIL%s %s:%d: %s (%lld) not < %s (%lld)\n", tf_red(), tf_reset(), \
|
|
__FILE__, __LINE__, #a, _a, #b, _b); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_LTE(a, b) \
|
|
do { \
|
|
long long _a = (long long)(a), _b = (long long)(b); \
|
|
if (_a > _b) { \
|
|
printf(" %sFAIL%s %s:%d: %s (%lld) not <= %s (%lld)\n", tf_red(), tf_reset(), \
|
|
__FILE__, __LINE__, #a, _a, #b, _b); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_STR_EQ(a, b) \
|
|
do { \
|
|
const char *_a = (a), *_b = (b); \
|
|
if (_a == NULL && _b == NULL) \
|
|
break; \
|
|
if (_a == NULL || _b == NULL || strcmp(_a, _b) != 0) { \
|
|
printf(" %sFAIL%s %s:%d: \"%s\" != \"%s\"\n", tf_red(), tf_reset(), __FILE__, \
|
|
__LINE__, _a ? _a : "(null)", _b ? _b : "(null)"); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_STR_NEQ(a, b) \
|
|
do { \
|
|
const char *_a = (a), *_b = (b); \
|
|
if (_a != NULL && _b != NULL && strcmp(_a, _b) == 0) { \
|
|
printf(" %sFAIL%s %s:%d: strings equal: \"%s\"\n", tf_red(), tf_reset(), __FILE__, \
|
|
__LINE__, _a); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_NULL(a) \
|
|
do { \
|
|
if ((a) != NULL) { \
|
|
printf(" %sFAIL%s %s:%d: %s is not NULL\n", tf_red(), tf_reset(), __FILE__, __LINE__, \
|
|
#a); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_NOT_NULL(a) \
|
|
do { \
|
|
if ((a) == NULL) { \
|
|
printf(" %sFAIL%s %s:%d: %s is NULL\n", tf_red(), tf_reset(), __FILE__, __LINE__, \
|
|
#a); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_TRUE(a) ASSERT(a)
|
|
#define ASSERT_FALSE(a) ASSERT(!(a))
|
|
|
|
#define ASSERT_MEM_EQ(a, b, n) \
|
|
do { \
|
|
if (memcmp((a), (b), (n)) != 0) { \
|
|
printf(" %sFAIL%s %s:%d: memory differs (%zu bytes)\n", tf_red(), tf_reset(), \
|
|
__FILE__, __LINE__, (size_t)(n)); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
#define ASSERT_FLOAT_EQ(a, b, eps) \
|
|
do { \
|
|
double _a = (double)(a), _b = (double)(b); \
|
|
if (fabs(_a - _b) > (eps)) { \
|
|
printf(" %sFAIL%s %s:%d: %s (%g) != %s (%g) within eps %g\n", tf_red(), tf_reset(), \
|
|
__FILE__, __LINE__, #a, _a, #b, _b, (double)(eps)); \
|
|
return 1; \
|
|
} \
|
|
} while (0)
|
|
|
|
/* ── Test runner ───────────────────────────────────────────────── */
|
|
|
|
#define RUN_TEST(name) \
|
|
do { \
|
|
if (!tf_test_selected(#name)) { \
|
|
break; \
|
|
} \
|
|
printf(" %-55s", #name); \
|
|
fflush(stdout); \
|
|
TF_COVERAGE_TEST_BEGIN(#name); \
|
|
int _result = test_##name(); \
|
|
TF_COVERAGE_TEST_END(#name); \
|
|
if (_result == 0) { \
|
|
printf("%sPASS%s\n", tf_green(), tf_reset()); \
|
|
tf_pass_count++; \
|
|
} else if (_result == -1) { \
|
|
/* skip — already printed */ \
|
|
} else { \
|
|
tf_fail_count++; \
|
|
} \
|
|
} while (0)
|
|
|
|
/* ── Suite grouping ────────────────────────────────────────────── */
|
|
|
|
#define SUITE(name) void suite_##name(void)
|
|
|
|
#define RUN_SUITE(name) \
|
|
do { \
|
|
if (!tf_suite_enter(#name)) { \
|
|
break; \
|
|
} \
|
|
printf("\n%s=== %s ===%s\n", tf_dim(), #name, tf_reset()); \
|
|
suite_##name(); \
|
|
tf_suite_leave(#name); \
|
|
} while (0)
|
|
|
|
/* ── Summary ───────────────────────────────────────────────────── */
|
|
|
|
#define TEST_SUMMARY() \
|
|
do { \
|
|
printf("\n────────────────────────────────────────────\n"); \
|
|
printf(" %s%d passed%s", tf_green(), tf_pass_count, tf_reset()); \
|
|
if (tf_fail_count > 0) \
|
|
printf(", %s%d failed%s", tf_red(), tf_fail_count, tf_reset()); \
|
|
if (tf_skip_count > 0) \
|
|
printf(", %s%d skipped%s", tf_dim(), tf_skip_count, tf_reset()); \
|
|
if (tf_deselected_count > 0) \
|
|
printf(", %s%d deselected%s", tf_dim(), tf_deselected_count, tf_reset()); \
|
|
printf("\n────────────────────────────────────────────\n\n"); \
|
|
return tf_fail_count > 0 ? 1 : 0; \
|
|
} while (0)
|
|
|
|
#endif /* TEST_FRAMEWORK_H */
|