Skip to content

tugglecore/attest

Repository files navigation

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_DIFF_STRING(a, b) char*, char* Confirm different strings.
EXPECT_SAME_CHAR(a, b) char, char Confirm same character.
EXPECT_DIFF_CHAR(a, b) char, char Confirm different character.
EXPECT_SAME_MEMORY(a, b) void*, void* Confirm same memory.
EXPECT_DIFF_MEMORY(a, b) void*, void* Confirm different memory.
EXPECT_SAME_PTR(a, b) *, * Confirm same pointer.
EXPECT_DIFF_PTR(a, b) *, * Confirm different pointer.
EXPECT_NULL(x) * Confirm NULL pointer.
EXPECT_NOT_NULL(x) * Confirm not NULL pointer
EXPECT_EQ(a, b) <any integer>, <any integer> Cast each argument to a long long int and check a == b.
EXPECT_NEQ(a, b) <any integer>, <any integer> Cast each argument to a long long int and check a != b.
EXPECT_LT(a, b) <any integer>, <any integer> Cast each argument to a long long int and check a < b.
EXPECT_LTE(a, b) <any integer>, <any integer> Cast each argument to a long long int and check a <= b.
EXPECT_GT(a, b) <any integer>, <any integer> Cast each argument to a long long int and check a > b.
EXPECT_GTE(a, b) <any integer>, <any integer> Cast each argument to a long long int and check a >= b.
EXPECT_EQ_U(a, b) <any integer>, <any integer> Cast each argument to a unsigned long long int and check a == b.
EXPECT_NEQ_U(a, b) <any integer>, <any integer> Cast each argument to a unsigned long long int and check a != b.
EXPECT_GTE_U(a, b) <any integer>, <any integer> Cast each argument to a unsigned long long int and check a >= b.
EXPECT_GT_U(a, b) <any integer>, <any integer> Cast each argument to a unsigned long long int and check a > b.
EXPECT_LT_U(a, b) <any integer>, <any integer> Cast each argument to a unsigned long long int and check a < b.
EXPECT_LTE_U(a, b) <any integer>, <any integer> Cast each argument to a unsigned long long int and check a <= b.

Runner options

Macros to change the behavior of Attest. You must define runner options before including attest.h.

Options:

Macro Type Default Description
ATTEST_MAX_TESTS int 128 Max number of tests allowed per binary.
ATTEST_NO_COLOR bool false Disables ANSI color codes in output report.
ATTEST_NO_UTF8 bool NULL Disables UTF8 output.
ATTEST_MAX_FAILURES int 16 Max amount of failures per test.
ATTEST_MAX_TEST_ATTEMPTS int 32 Max amount of attempts per test.
ATTEST_MAX_TAGS int 8 Max amount of tags per test.
ATTEST_MAX_TAG_SIZE int 21 Max tag size.
ATTEST_VALUE_BUF int 128 The max size of buffer used in failure messages.
ATTEST_MAX_PARAMERTERIZE_RESULTS int 32 The max amount of failures for a parameterize test.
ATTEST_CASE_NAME_SIZE int 128 The max size for the case name of a parameterize test.

Example:

#define ATTEST_NO_COLOR 1
#include "attest.h"

Attest behavior

Compatibility:

Supports Clang on Windows and GCC/Clang on all *nixes. The team built Attest with Clang on Windows MacOS and Fedora Linux.

On Windows, Attest does not compile with MSVC. Although, Attest is not compatible with MSVC currently but this is on the Roadmap and important to author.

Platform support:

  • *nixes (GCC/Clang)
  • MacOS (GCC/Clang)
  • Windows (Clang)

Undefined behavior policy such as segmentation faults

Attest does not catch or recover from segmentation faults. If the user's code segfaults, the OS terminates the test process immediately, just like any normal C program. Attest does not intercept signals, fork processes, or attempt to continue execution after undefined behavior.

Test execution order:

Normal Tests:

  1. BEFORE_ALL
  2. BEFORE_EACH
  3. TEST or TEST_CTX
  4. AFTER_EACH
  5. AFTER_ALL

Parameterized Tests:

  1. BEFORE_ALL
  2. BEFORE_ALL_CASES
  3. BEFORE_EACH_CASE
  4. PARAM_TEST or PARAM_TEST_CTX
  5. AFTER_EACH_CASE
  6. AFTER_ALL_CASES
  7. AFTER_ALL

About

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

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors