English | 日本語
This document describes the architecture, design decisions, and platform-specific implementation details of attest.
attest/
├── include/attest/
│ └── attest.h # Public API
├── src/
│ ├── attest.c # Entry point, test execution loop
│ ├── attest_main.c # main() wrapper calling attest_main()
│ ├── attest_cli.c # CLI parsing
│ ├── attest_assert.c # Assertion implementations, context management
│ ├── attest_capture.c # stderr capture
│ ├── attest_fixture.c # Fixture registry and execution
│ ├── attest_parallel.c # Parallel execution (worker pool)
│ └── internal/
│ ├── attest_internal.h # Internal types, platform macros
│ ├── attest_context.h # Context types, timeout/fixture accessors
│ ├── attest_timeout.h # Platform-agnostic timeout interface
│ ├── attest_timeout_posix.c # POSIX timeout (SIGALRM, includes Human68k stubs)
│ ├── attest_timeout_win32.c # Windows timeout (thread + event)
│ └── attest_registry.c # Test registration storage
└── tests/
└── selftest_main.c # Framework self-tests
File: src/internal/attest_registry.c
- Stores registered test cases with suite name, test name, file location
- Frozen after initialization (no late registration)
- Tests stored in registration order
File: src/attest.c
attest_main()entry point- Uses
setjmp/longjmpfor fatal assertion handling - Each test runs in its own
setjmpcontext - Execution flow: registry freeze → filter → run → summarize → exit code
File: src/attest_assert.c
_Genericmacro dispatches to type-specific handlers:att_handle_compare_signed— signed integers (cast tolong long)att_handle_compare_unsigned— unsigned integers (cast tounsigned long long)att_handle_compare_double—float/doubleatt_handle_compare_long_double—long doubleatt_handle_compare_pointer— pointers (compared asuintptr_t)
- Context state tracks: current test, failure counts, longjmp buffer, timeout state, info stack
- The signed / unsigned / double / long-double comparator, formatter, and
handler families share a single textual template each. They are emitted
by file-local
ATT_DEFINE_COMPARE,ATT_DEFINE_FORMATTER, andATT_DEFINE_HANDLERmacros (seesrc/attest_assert.c), so adding a new numeric type reduces to one macro invocation per family. Pointer, bool, and string handlers stay hand-written because they need bespoke formatting (hex addresses,(null)sentinels, multi-line diff output).
File: src/attest_cli.c
- Parses
--list,--filter,--no-color,--timeout-ms,--shuffle,--jobs,--format,--output --formatacceptsdefault,tap, orjunit;--outputis only valid with--format=junitand otherwise produces an error--jobs=autoand--jobs=0both resolve to the detected CPU count viaatt_get_cpu_count()(sysconf on POSIX,GetSystemInfoon Windows)- Filter syntax: wildcards (
*,?), shorthand (Suite→Suite.*), multiple patterns (;separator), negative filters (-Pattern) - Unknown options → exit code 2
File: src/attest_fixture.c
- Fixture entries registered at startup
- Setup/teardown functions looked up by fixture type name
- Teardown always runs (even after ASSERT failure or skip)
File: src/attest_capture.c
att_capture_begin()redirects stderr to internal bufferatt_capture_end()returns captured content- Non-reentrant (nesting not supported)
att_capture_supported()returnsfalseon platforms where capture is compiled out (e.g. Human68k), whereatt_capture_begin()is a no-op stub returning-1. DefiningATT_CAPTURE_DISABLEDforces the stub path on any platform, for simulating Human68k on a host build.
File: src/attest_parallel.c
- Worker pool architecture with configurable thread count
- Each worker has thread-local context (
g_ctx) - Results collected per-test, output in registration order
- Mutex-protected work queue for test distribution
- Compiled and entered only under
ATT_THREADS_POSIX. Other thread backends (ATT_THREADS_C11,ATT_THREADS_WIN32,ATT_THREADS_NONE) currently fall through to the sequential runner regardless of--jobs=N. - The parallel runner emits only the default human-readable format. TAP
per-test lines and JUnit XML output are produced exclusively by the
sequential path, so requesting
--format=tapor--format=junittogether with--jobs > 1is not supported.
Defined in src/internal/attest_internal.h:
| Macro | Meaning |
|---|---|
ATT_PLATFORM_WINDOWS |
Windows (any) |
ATT_PLATFORM_POSIX |
POSIX-compliant (Linux, macOS, etc.) |
ATT_PLATFORM_HUMAN68K |
Sharp X680x0 (Human68k) — single-threaded, no timeout, no stderr capture |
ATT_COMPILER_MSVC |
Microsoft Visual C++ |
ATT_COMPILER_GCC_LIKE |
GCC or Clang |
| Macro | Condition |
|---|---|
ATT_THREADS_C11 |
C11 <threads.h> available |
ATT_THREADS_POSIX |
POSIX threads (pthread) |
ATT_THREADS_WIN32 |
Windows threads |
ATT_THREADS_NONE |
No thread support |
POSIX uses sigsetjmp/siglongjmp to preserve signal masks; Windows uses standard setjmp/longjmp.
typedef att_jmp_buf;
#define att_setjmp(env) // Platform-specific
#define att_longjmp(env, v) // Platform-specificCritical constraint: setjmp must be called directly in the caller's stack frame (not through a function call). This is enforced via macro expansion.
| Attribute | GCC/Clang | MSVC | mcc |
|---|---|---|---|
| Constructor | __attribute__((constructor)) |
.CRT$XCU section |
__attribute__((constructor)) |
| Alignment | __attribute__((aligned(n))) |
__declspec(align(n)) |
_Alignas(n) (mcc is not ATT_COMPILER_GCC_LIKE, so ATT_ALIGN falls through to the C11 form) |
| Cleanup | __attribute__((cleanup(fn))) |
Not supported | __attribute__((cleanup(fn))) |
| Thread-local | __thread or _Thread_local |
__declspec(thread) |
Not applicable — Human68k has no thread support, so ATT_THREAD_LOCAL expands to nothing and test contexts use plain global variables |
mcc is detected via __MCC__ in attest.h only for the constructor and
cleanup attribute selection above (ATT_AUTOREG / SCOPED_INFO); it is not
added to ATT_COMPILER_GCC_LIKE in src/internal/attest_internal.h.
Aligned allocation for context structures:
| Platform | Function |
|---|---|
| Linux/GCC | aligned_alloc() (C11) |
| MSVC | _aligned_malloc() / _aligned_free() |
| Other | Standard malloc() / free() |
- Uses
sigaction()andsetitimer(ITIMER_REAL) - Signal handler sets timeout flag and calls
longjmp - Works for infinite loops (signal interrupts execution)
- Spawns a dedicated timer thread with
_beginthreadex() - The timer thread waits on a manual-reset
CreateEvent()handle viaWaitForSingleObject(event, timeout_ms).WAIT_TIMEOUTmeans the deadline expired — the thread then sets thetriggeredflag and re-signals the event so the main thread can observe it.WAIT_OBJECT_0meansatt_timeout_stop()cancelled the wait early. - The main thread polls the same event from
att_context_record_assert()usingWaitForSingleObject(event, 0), throttled to once every 32 assertions to keep the per-assertion cost negligible. On a hit, it callsatt_context_abort()to longjmp back to the test runner. - Limitation: Because the main thread only learns about the timeout when
it next executes an assertion macro, a tight loop with no assertions cannot
be interrupted on Windows. The timer thread itself still fires, but the
failure is recorded only when control returns through an assertion or to
att_context_end(). For long-running code paths, ensure periodic assertions (orEXPECT_TRUE(true)checkpoints) so the timeout can take effect.
[Main Thread]
│
├─ Freeze registry
├─ Initialize work queue (mutex-protected index)
├─ Allocate results array
│
├─────────────────────────────────────────────┐
▼ ▼
[Worker 1] [Worker N]
│ │
├─ Loop: ├─ Loop:
│ ├─ Lock mutex │ ├─ Lock mutex
│ ├─ Get next test index │ ├─ Get next test index
│ ├─ Unlock mutex │ ├─ Unlock mutex
│ ├─ Init thread-local context │ ├─ Init thread-local context
│ ├─ Run test │ ├─ Run test
│ └─ Store result │ └─ Store result
│ │
└─────────────────────────────────────────────┘
│
▼
[Main Thread]
│
├─ Join all workers
├─ Output results (registration order)
└─ Return summary
Each worker thread has its own g_ctx:
#if defined(ATT_THREADS_C11)
_Thread_local att_context_state *g_ctx;
#elif defined(ATT_THREADS_POSIX)
__thread att_context_state *g_ctx;
#elif defined(ATT_THREADS_WIN32)
__declspec(thread) att_context_state *g_ctx;
#endif- Each test's output is captured to a per-test buffer
- After all workers complete, main thread outputs in registration order
- Prevents interleaved output from concurrent tests
Symptom: SIGILL / SIGFPE / SIGSEGV when running the full self-test suite
on Apple Silicon, observed across GCC 12.5, 13.4, 15.2 and Clang 22.1 at
-O2 and above. GCC 14.2 (an earlier point release) and a regression in
Clang 22 made this very visible; previous releases attributed the symptom to
"GCC 14.2.0 sigsetjmp bug" and shipped an -O1 ARM64 workaround in
CMakeLists.txt.
Root cause: attest's own test runner. The original att_context_protect()
was an out-of-line function in src/attest_assert.c that called setjmp
and returned. By the time the runner in src/attest.c (or
src/attest_parallel.c) tried to longjmp back, the jmp_buf referred to
a stack frame that no longer existed. This is undefined behavior, and
modern GCC/Clang optimizers happily reason on the assumption that it never
happens — so they hoist or eliminate code that the runner relied on.
Fix: att_context_protect() is now a macro in
src/internal/attest_context.h:
#define att_context_protect() att_setjmp(*att__get_abort_env_ptr())Both attest.c and attest_parallel.c invoke this macro inside the same
stack frame that handles the longjmp, so the jmp_buf stays valid.
The -O1 workaround, the -fno-omit-frame-pointer /
-fno-optimize-sibling-calls mitigations, and the explicit 16-byte
sigjmp_buf alignment guards have all been removed; verified across
GCC 12–15 and Clang 16–22 on Apple Silicon at the default Release
optimization level.
The same constraint already applied to subtests via
att_subtest_scope_protect (a macro in include/attest/attest.h).
Problem: STATUS_BAD_STACK (0xC0000028) when using setjmp through function calls.
Solution: setjmp must be macro-expanded directly into caller's stack frame:
// Wrong: function call wrapping setjmp
int helper(void) { return setjmp(env); } // env invalid after return
// Correct: macro expansion
#define PROTECT() setjmp(*get_env_ptr())
if (PROTECT() == 0) { ... } // setjmp in this stack frameThis is the same constraint that the cross-frame setjmp fix above
extends to the top-level test runner.
MSVC doesn't support __attribute__((cleanup)). SCOPED_INFO pushes context but doesn't auto-pop. Stack resets at test end, so functionality is preserved.
- Format:
Suite.Name - Duplicates detected at initialization → exit code 3
- Stored with file path and line number for error reporting
| Type | Behavior | Implementation |
|---|---|---|
ASSERT_* |
Abort current test | longjmp to test's setjmp point |
EXPECT_* |
Record failure, continue | Increment failure counter |
NULL == NULL→ trueNULLvs non-NULL → false- Output shows
"(null)"for NULL pointers
NEAR:fabs(a - b) <= epsilonNEAR_REL:fabs(a - b) <= rel_eps * max(fabs(a), fabs(b))- Near-zero values (< 1e-15): uses absolute comparison
ULP_EQ: IEEE 754 bit representation distance- Handles denormals and sign transitions
- NaN always fails
- ±Infinity must match exactly (same sign)
| Code | Condition |
|---|---|
| 0 | All tests passed, or --list mode |
| 1 | One or more failures |
| 2 | CLI error (unknown option) |
| 3 | Initialization error (duplicate names, internal failure) |
- WebKit-based: tabs (width 4), right pointer alignment
- Run
clang-format -i <file>before committing - Target 100 columns line length
| Type | Convention | Example |
|---|---|---|
| Public API | att_ prefix, snake_case |
att_run_subtest |
| Macros | UPPER_SNAKE_CASE | ASSERT_EQ |
| Files | snake_case | attest_assert.c |
- Add self-tests in
tests/selftest_main.c - Run full suite:
./build/attest_selftest - Test CLI behavior:
--list,--filter=... - Verify on multiple platforms if possible