Attest
Cross-platform, heap-free C test runner with parameterized and lifecycle-aware tests.
Install / Use
/learn @tugglecore/AttestREADME
Attest
Cross-platform, heap-free C test framework with lifecycle hooks and parameterized functions, assertions with ad-hoc formatting and detailed failure diagnostics.
Features
- Automatic Test Registration: Attest automatically discover tests.
- Parameterized Testing: Reduce boilerplate by running the same test logic against different data sets.
- Lifecycle Management: Includes
setupandteardownhooks with context-passing. - Test Categorization: Use tags to organize your suite, allowing you to filter groups of tests.
- Rich Assertions:
expect-style assertions with support for ad-hoc formatting for descriptive messages. - Zero Dynamic Allocation: Performs no heap allocation. It operates on static storage.
- Fine-Grained Orchestration: Built-in support for skipping or retrying tests to handle any environment.
- Lightweight & Cross-Platorm: Supports Windows, MacOS and Linux with a minimal memory footprint.
Basic usage
#include "attest.h"
TEST(math) {
int expected = 7;
int actual = 3 + 4;
EXPECT_EQ(actual, expected);
}
Installation
Drop the header file anywhere in your project's include directory. Then include it like so:
#include "attest.h"
API
Note on naming, all macros have lowercase versions.
TEST(name, [options...])
Defines a test case. This macro automatically registers the test case with the Attest.
Parameters:
name: Unique name for the test case. No spaces or quotes allowed.
Options:
The options may appear in any order. You pass the options as arguments prefixed with a dot.
|Option |Type |Default |Description |
|-------------------|-------------|---------|-----------------------------|
|.disabled |bool |false |If true, the runner ignores the test and doesn't report |
|.skip |bool |false |If true, the runner ignores the test but still report it |
|.attempts |int |1 |The number of times to execute the test body |
|.tags |char*[ATTEST_MAX_TAGS] |NULL |tags associated with the function. |
|.before |void(*)(TextContext*) |NULL |A function that runs before the test.|
|.after |void(*)(TextContext*)|NULL|A function that runs after the test. |
Example:
TEST(hit_api, .attempts = 10, .tags = { "slow" }) {
int result = handle_api_request();
EXPECT_EQ(result, 200);
}
TEST_CTX(name, test_context, [options...])
Defines a test case that accepts a Context object.
Parameters:
name: Unique name for the test case. No spaces or quotestest_context: Has typeTestContextand allows sharing allocated data.
Options:
Accept all the options available to TEST.
Example:
#include "attest.h"
BEFORE_ALL(test_context)
{
int* foo = malloc(sizeof(int));
*foo = 7;
test_context->shared = (void*)foo;
}
TEST_CTX(with_a_context, test_context)
{
int global_num = *(int*)test_context->shared;
EXPECT_EQ(14 + global_num, 21);
}
TestContext
Attest passes a TestContext object to each lifecycle function and TEST_CTX function. This object has fields containing user custom data. User's responsibility to clean up data.
Fields:
|name |Type |Description |
|--------|--------|------------------------------------------|
|all |void* |Data shared amongst all tests and lifecycle functions. The value is available for the entire program.|
|each |void* |Data created for each test. This field is freed after each test and should be set for each test inside of the before_each lifecycle function.|
|self |void* |Data intended for a single test. This field will be freed at the end of a test and should be set inside of a .before lifecycle function.|
Example:
#include "attest.h"
BEFORE_ALL(test_context)
{
int* foo = malloc(sizeof(int));
*foo = 7;
test_context->shared = (void*)foo;
}
AFTER_ALL(test_context)
{
free(test_context->shared);
}
TEST_CTX(with_a_context, test_context)
{
int global_num = *(int*)test_context->shared;
EXPECT_EQ(14 + global_num, 21);
}
GlobalContext
Attest passes a GlobalContext object to BEFORE_ALL and AFTER_ALL lifecycle function. This object has fields containing user custom data. User's responsibility to clean up data.
Fields:
|name |Type |Description |
|--------|--------|------------------------------------------|
|all |void* |Data shared amongst all tests and lifecycle functions. The value is available for the entire program.|
Example:
#include "attest.h"
BEFORE_ALL(test_context)
{
int* foo = malloc(sizeof(int));
*foo = 7;
test_context->shared = (void*)foo;
}
BEFORE_ALL(test_context)
A lifecycle function that runs before all tests and lifecycle functions.
Parameters:
test_context: Has typeTestContextand allows sharing custom data.
Example:
BEFORE_ALL(test_context)
{
int* foo = malloc(sizeof(int));
*foo = 7;
test_context->shared = (void*)foo;
}
BEFORE_EACH(test_context)
A lifecycle function that runs before each test.
Parameters:
test_context: Has typeTestContextand allows sharing custom data.
Example:
BEFORE_EACH(test_context)
{
int* foo = malloc(sizeof(int));
*foo = *(int*)test_context->shared - 3;
test_context->local = (void*)foo;
}
AFTER_EACH(test_context)
A lifecycle function that runs after each tests.
Parameters:
test_context: Has typeTestContextand allows sharing custom data.
Example:
AFTER_EACH(test_context)
{
free(test_context->local);
test_context->local = NULL;
}
AFTER_ALL(test_context)
A lifecycle function that runs after all tests and lifecycle functions.
Parameters:
test_context: Has typeTestContextand allows sharing custom data.
Example:
AFTER_ALL(test_context)
{
free(test_context->shared);
}
PARAM_TEST(name, case_type, case_name, (values), [options...])
Parameters:
name: Unique name for the parameterized test. No spaces or quotes.case_type: Case data type.case_name: Name of case data.values: List of structures enclosed in parenthesis of type{ char[ATTEST_CASE_NAME_SIZE] name; case_type data; }wherenameis an optional name for the test case anddatais the value to passed to the test.
Options:
Accepts all the options available to TEST this macro accepts:
|Option |Type |Description |
|--------------------|------------------------|----------------------------|
|.before_all_cases |void(*)(ParamContext*)|A test that runs before all cases.|
|.after_all_cases |void(*)(ParamContext*)|A test that runs after all cases. |
|.before_each_case |void(*)(ParamContext*)|A test that runs before each case.|
|.after_each_cases |void(*)(ParamContext*)|A test that runs after each case.|
Example:
#include "attest.h"
PARAM_TEST(fruit_basket,
int,
case_num,
({ 1, "one" } , { 2, "two" } , { 3, "three" }))
{
EXPECT_EQ(case_num, 1);
}
PARAM_TEST_CTX(name, param_contest, case_type, case_name, (values), [options...])
Parameters:
name: Unique name for the parameterized test. No spaces or quotes.param_context: Name ofParamContext.case_type: case data type.case_name: Name of case data.values: a list of structures enclosed in parenthesis of type{ char[ATTEST_CASE_NAME_SIZE] name; case_type data; }wherenameis an optional name for the test case anddatais the value to passed to the test.
Options:
Accepts all the options available to PARAM_TEST and TEST.
Example:
#include "attest.h"
PARAM_TEST_CTX(basket_case,
param_context,
int,
case_num,
({ 1, "one" } , { 2, "two" } , { 3, "three" }))
{
int global_num = *(int*)context->shared;
EXPECT_EQ(global_num, case_num);
}
ParamContext
Attest passes a ParamContext object to each test case of a parameterized tests and each parameterized lifecycle function.
Fields:
|name |Type |Description |
|----------|----------|----------------------------------------------|
|all |void* |Data intended shared amongst all tests and lifecycle functions. Value available for entire program.|
|set |void* |Data intended for entire parameterized test. Set by .before_all_cases.|
|self |void* |Data intended for each case. Set by .before_each_case.|
Example:
#include "attest.h"
PARAM_TEST_CTX(basket_case,
param_context,
int,
case_num,
({ 1, "one" } , { 2, "two" } , { 3, "three" }))
{
int shared_num = *(int*)param_context->shared;
EXPECT_EQ(shared_num, case_num);
}
Expectations
Attest only provide expectations. Expectations don't stop the test, attest execute their arguments exactly once and tests can have more than one.
For each expectation, you can pass it variable amount arguments passed to it and used those arguments to create a formatted message.
Example:
TEST(hit_api, .attempts = 10) {
int expected_status = 200;
int result = handle_api_request();
EXPECT_EQ(result, expected_status, "Should return %d, but got %d", expected_status, result);
}
EXPECT_: Records a failure but continues the test execution.
List of Expectation:
|Macro | Arguments |Description |
|--------------------------|------------------|------------------------- |
|EXPECT(x) | bool |Confirm true expression. |
|EXPECT_FALSE(x) | bool |Confirm false expression. |
|EXPECT_SAME_STRING(a, b) | char*, char* |Confirm same strings. |
|EXPECT
Related Skills
gh-issues
347.2kFetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]
node-connect
347.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
108.0kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
Writing Hookify Rules
108.0kThis skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
