-
Notifications
You must be signed in to change notification settings - Fork 1
Unit Testing
How to write unit tests for Hammer parsers using the glib testing framework and Hammer's test_suite.h helpers.
You need glib installed (the glib-2.0 development package):
sudo apt install libglib2.0-dev # Debian/UbuntuVerify it's available:
pkg-config --cflags --libs glib-2.0Separate 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
#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();
}-
g_test_initinitializes the glib testing framework. -
g_test_add_funcregisters a test by path string and function pointer. The path is for organizational display (use/to group tests). -
g_test_runruns all registered tests and reports results. - Test functions take no parameters and return nothing. Use the parser via a global variable.
gcc -o test_parser my_parser_tests.c my_parser.c \
$(pkg-config --cflags --libs libhammer) \
$(pkg-config --cflags --libs glib-2.0)
./test_parserExpected 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| 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 |
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 |
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)));
}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);
}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);
}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)));
}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-
Test at multiple levels. Test individual sub-parsers (e.g., just the header), not only the top-level parser. This localizes failures.
-
Use
h_pprintfor 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);
}-
Test boundary conditions. Empty input, minimum-length input, maximum-length input, and off-by-one sizes are common sources of bugs.
-
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
Learn Hammer
Protocol Examples
NTP
- NTP Overview
- Parsing the Header
- Parsing Data Fields
- Extension Fields and MAC
- Assembling the Parser
- Hex Input Preprocessing
- Running and Testing
DNS
TFTP
- TFTP Overview
- RRQ/WRQ Packets
- DATA Packets
- ACK Packets
- ERROR Packets
- Assembling the Parser
- Running and Testing
References
- Hammer Quick Reference
- Parsing Backends
- Unit Testing
- Using RTEMS
- Extending Hammer
- Adding a New Example
- Adding a New Binding
Further Reading