Skip to content

Unit Testing

Elbasiouny, Mahmoud edited this page Feb 19, 2026 · 1 revision

How to write unit tests for Hammer parsers using the glib testing framework and Hammer's test_suite.h helpers.


Prerequisites

You need glib installed (the glib-2.0 development package):

sudo apt install libglib2.0-dev    # Debian/Ubuntu

Verify it's available:

pkg-config --cflags --libs glib-2.0

Project Structure

Separate your parser definition from the code that uses it, so the test file can include the parser independently:

my_project/
├── my_parser.c          # Defines the parser (returns HParser *)
├── my_parser.h          # Header declaring the parser function
├── main.c               # Uses the parser on real input
└── my_parser_tests.c    # Unit tests

Writing Tests

1. Set Up the Test File

#include <hammer/hammer.h>
#include <hammer/glue.h>
#include "my_parser.h"

HParser *parser_under_test;

static void test_valid_input(void) {
    uint8_t input[] = {0xE3, 0x00, 0x03, 0xFA};
    HParseResult *result = h_parse(parser_under_test, input, sizeof(input));
    g_assert_nonnull(result);
}

static void test_invalid_input(void) {
    uint8_t input[] = {0xFF};
    HParseResult *result = h_parse(parser_under_test, input, sizeof(input));
    g_assert_null(result);
}

static void test_result_value(void) {
    uint8_t input[] = {0x42};
    HParseResult *result = h_parse(parser_under_test, input, sizeof(input));
    g_assert_nonnull(result);
    g_assert_cmpuint(result->ast->token_type, ==, TT_UINT);
    g_assert_cmpuint(result->ast->uint, ==, 0x42);
}

int main(int argc, char *argv[]) {
    g_test_init(&argc, &argv, NULL);

    parser_under_test = my_parser();

    g_test_add_func("/my_parser/valid_input", test_valid_input);
    g_test_add_func("/my_parser/invalid_input", test_invalid_input);
    g_test_add_func("/my_parser/result_value", test_result_value);

    return g_test_run();
}

2. Key Points

  • g_test_init initializes the glib testing framework.
  • g_test_add_func registers a test by path string and function pointer. The path is for organizational display (use / to group tests).
  • g_test_run runs all registered tests and reports results.
  • Test functions take no parameters and return nothing. Use the parser via a global variable.

3. Compile and Run

gcc -o test_parser my_parser_tests.c my_parser.c \
    $(pkg-config --cflags --libs libhammer) \
    $(pkg-config --cflags --libs glib-2.0)

./test_parser

Expected output:

/my_parser/valid_input: OK
/my_parser/invalid_input: OK
/my_parser/result_value: OK

If a test fails, g_test_run terminates by default. Pass -k ("keep going") to run the remaining tests:

./test_parser -k

Useful Assertion Macros

From glib

Macro Purpose
g_assert_nonnull(ptr) Assert pointer is not NULL (parse succeeded)
g_assert_null(ptr) Assert pointer is NULL (parse failed)
g_assert_true(expr) Assert expression is true
g_assert_false(expr) Assert expression is false
g_assert_cmpint(a, op, b) Compare signed integers with operator (e.g., ==, <)
g_assert_cmpuint(a, op, b) Compare unsigned integers
g_assert_cmpmem(a, alen, b, blen) Compare memory blocks

From Hammer's test_suite.h

Hammer's test_suite.h (in the Hammer source distribution) provides convenience wrappers. Include it with:

#include <hammer/test_suite.h>
Macro Purpose
g_check_cmp_uint32(a, op, b) Compare uint32_t values with a given operator
g_check_cmp_int32(a, op, b) Compare int32_t values
g_check_cmp_uint64(a, op, b) Compare uint64_t values
g_check_cmp_int64(a, op, b) Compare int64_t values

Common Test Patterns

Test Parse Success/Failure

static void test_accepts_valid(void) {
    uint8_t input[] = { /* valid bytes */ };
    g_assert_nonnull(h_parse(parser, input, sizeof(input)));
}

static void test_rejects_invalid(void) {
    uint8_t input[] = { /* invalid bytes */ };
    g_assert_null(h_parse(parser, input, sizeof(input)));
}

Test Specific Token Values

static void test_field_value(void) {
    uint8_t input[] = {0x03, 0x05};
    HParseResult *result = h_parse(parser, input, sizeof(input));
    g_assert_nonnull(result);

    const HParsedToken *first = h_seq_index(result->ast, 0);
    g_assert_cmpuint(first->uint, ==, 3);

    const HParsedToken *second = h_seq_index(result->ast, 1);
    g_assert_cmpuint(second->uint, ==, 5);
}

Test Sequence Length

static void test_sequence_count(void) {
    uint8_t input[] = {0x01, 0x02, 0x03};
    HParseResult *result = h_parse(parser, input, sizeof(input));
    g_assert_nonnull(result);
    g_assert_cmpuint(result->ast->seq->used, ==, 3);
}

Test Trailing Garbage Rejection

static void test_rejects_trailing_data(void) {
    uint8_t input[] = { /* valid bytes followed by extra bytes */ };
    g_assert_null(h_parse(parser, input, sizeof(input)));
}

Makefile Integration

Add a test target to your Makefile:

CC = gcc
CFLAGS = -g $(shell pkg-config --cflags libhammer) $(shell pkg-config --cflags glib-2.0)
LDFLAGS = $(shell pkg-config --libs libhammer) $(shell pkg-config --libs glib-2.0)

test: my_parser_tests
	./my_parser_tests

my_parser_tests: my_parser_tests.c my_parser.c
	$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
make test

Tips

  1. Test at multiple levels. Test individual sub-parsers (e.g., just the header), not only the top-level parser. This localizes failures.

  2. Use h_pprint for debugging. When a test fails unexpectedly, dump the parse tree to understand what was actually parsed:

if (result) {
    h_pprint(stdout, result->ast, 0, 4);
}
  1. Test boundary conditions. Empty input, minimum-length input, maximum-length input, and off-by-one sizes are common sources of bugs.

  2. Test value boundaries. For h_int_range(p, 1, 4), test with values 0, 1, 4, and 5 to confirm the boundaries are correct.


Back to: Hammer Fundamentals · Home

Clone this wiki locally