SkillAgentSearch skills...

Attest

Cross-platform, heap-free C test runner with parameterized and lifecycle-aware tests.

Install / Use

/learn @tugglecore/Attest
About this skill

Quality Score

0/100

Supported Platforms

Zed

README

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 setup and teardown hooks 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 quotes
  • test_context: Has type TestContext and 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 type TestContext and 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 type TestContext and 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 type TestContext and 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 type TestContext and 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; } where name is an optional name for the test case and data is 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 of ParamContext.
  • 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; } where name is an optional name for the test case and data is 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

View on GitHub
GitHub Stars4
CategoryDevelopment
Updated1mo ago
Forks1

Languages

C

Security Score

90/100

Audited on Feb 24, 2026

No findings