From f24ec48b27d4cb8dd0555840206dedbb45f1423c Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Sat, 24 Oct 2026 01:17:50 +0200 Subject: Add a unit test harness and a first set of tests make check builds tests/runner from the modules that link without the io subsystem and runs it. --- .gitignore | 2 +- Makefile | 20 ++- tests/runner.c | 34 +++++ tests/stubs.c | 14 ++ tests/t_iovec.c | 124 ++++++++++++++++++ tests/t_scan_urlencoded_query.c | 274 ++++++++++++++++++++++++++++++++++++++++ tests/t_vector.c | 161 +++++++++++++++++++++++ tests/test.c | 58 +++++++++ tests/test.h | 83 ++++++++++++ 9 files changed, 768 insertions(+), 2 deletions(-) create mode 100644 tests/runner.c create mode 100644 tests/stubs.c create mode 100644 tests/t_iovec.c create mode 100644 tests/t_scan_urlencoded_query.c create mode 100644 tests/t_vector.c create mode 100644 tests/test.c create mode 100644 tests/test.h diff --git a/.gitignore b/.gitignore index 874c63c..8b4bec1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ *.o - +tests/runner diff --git a/Makefile b/Makefile index 706b742..8f967f7 100644 --- a/Makefile +++ b/Makefile @@ -99,8 +99,26 @@ proxy.debug: $(OBJECTS_proxy_debug) $(HEADERS) .c.o : $(HEADERS) $(CC) -c -o $@ $(CFLAGS_production) $< +# Unit tests. The modules under test are compiled straight into the test +# binary, so a test may reach a static function by including the source if it +# has to. tests/stubs.c supplies the few symbols those modules reference but +# the tests never reach. +TESTBIN=tests/runner +TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_scan_urlencoded_query.c tests/t_iovec.c tests/t_vector.c +TEST_UNITS=scan_urlencoded_query.c ot_iovec.c ot_vector.c +TEST_HEADERS=tests/test.h $(HEADERS) +CFLAGS_test = $(CFLAGS) -I. -g -O1 $(FEATURES) + +$(TESTBIN): $(TEST_SOURCES) $(TEST_UNITS) $(TEST_HEADERS) + $(CC) -o $@ $(CFLAGS_test) $(TEST_SOURCES) $(TEST_UNITS) + +check: $(TESTBIN) + ./$(TESTBIN) + +.PHONY: all check clean install + clean: - rm -rf opentracker opentracker.debug *.o *~ + rm -rf opentracker opentracker.debug $(TESTBIN) *.o *~ install: install -m 755 opentracker $(DESTDIR)$(BINDIR) diff --git a/tests/runner.c b/tests/runner.c new file mode 100644 index 0000000..d73c516 --- /dev/null +++ b/tests/runner.c @@ -0,0 +1,34 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include + +/* Opentracker */ +#include "test.h" + +struct module { + const char *name; + void (*run)(void); +}; + +static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urlencoded_query}, + {"ot_iovec", test_iovec}, + {"ot_vector", test_vector}, + {NULL, NULL}}; + +/* Run every module, or only those whose name contains argv[1] */ +int main(int argc, char **argv) { + const struct module *m; + + for (m = modules; m->name; ++m) { + if (argc > 1 && !strstr(m->name, argv[1])) + continue; + test_module(m->name); + m->run(); + } + + return test_summary(); +} diff --git a/tests/stubs.c b/tests/stubs.c new file mode 100644 index 0000000..ae40306 --- /dev/null +++ b/tests/stubs.c @@ -0,0 +1,14 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* Stubs for symbols the modules under test reference but never reach along + the paths the tests exercise. If a test ever does reach one, the missing + behaviour should be supplied here rather than the test weakened. */ + +/* Opentracker */ +#include "trackerlogic.h" + +/* ot_vector.c calls this from vector_clean_list, which no test drives yet. */ +void free_peerlist(ot_peerlist *peer_list) { (void)peer_list; } diff --git a/tests/t_iovec.c b/tests/t_iovec.c new file mode 100644 index 0000000..89f3fda --- /dev/null +++ b/tests/t_iovec.c @@ -0,0 +1,124 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include +#include + +/* Opentracker */ +#include "ot_iovec.h" +#include "test.h" + +static void case_increase_and_length(void) { + size_t entries = 0; + struct iovec *vector = NULL; + void *first, *second; + + test_case("increase appends an entry and length is their sum"); + first = iovec_increase(&entries, &vector, 100); + CHECK(first != NULL); + CHECK_INT(entries, 1); + CHECK_INT(vector[0].iov_len, 100); + CHECK_PTR(vector[0].iov_base, first); + + second = iovec_increase(&entries, &vector, 50); + CHECK(second != NULL); + CHECK_INT(entries, 2); + CHECK_INT(vector[1].iov_len, 50); + CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 150); + + iovec_free(&entries, &vector); +} + +static void case_free_resets(void) { + size_t entries = 0; + struct iovec *vector = NULL; + + test_case("free releases the entries and resets the vector"); + CHECK(iovec_increase(&entries, &vector, 10) != NULL); + CHECK(iovec_increase(&entries, &vector, 10) != NULL); + iovec_free(&entries, &vector); + CHECK_INT(entries, 0); + CHECK_PTR(vector, NULL); + CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 0); +} + +static void case_fixlast_shrinks(void) { + size_t entries = 0; + struct iovec *vector = NULL; + char *block; + + test_case("fixlast trims the last entry to the bytes actually used"); + CHECK(iovec_increase(&entries, &vector, 64) != NULL); + block = iovec_increase(&entries, &vector, 1000); + CHECK(block != NULL); + memset(block, 'x', 20); + + iovec_fixlast(&entries, &vector, block + 20); + CHECK_INT(entries, 2); + CHECK_INT(vector[1].iov_len, 20); + CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 84); + CHECK_MEM(vector[1].iov_base, "xxxxxxxxxxxxxxxxxxxx", 20); + + iovec_free(&entries, &vector); +} + +static void case_fixlast_on_empty(void) { + size_t entries = 0; + struct iovec *vector = NULL; + + test_case("fixlast on an empty vector does nothing"); + iovec_fixlast(&entries, &vector, NULL); + CHECK_INT(entries, 0); + CHECK_PTR(vector, NULL); +} + +static void case_append_takes_ownership(void) { + size_t entries = 0; + struct iovec *vector = NULL; + struct iovec source; + + test_case("append moves the buffer and clears the source"); + source.iov_base = malloc(10); + CHECK(source.iov_base != NULL); + memcpy(source.iov_base, "0123456789", 10); + source.iov_len = 10; + + CHECK(iovec_append(&entries, &vector, &source) != NULL); + CHECK_INT(entries, 1); + CHECK_INT(vector[0].iov_len, 10); + CHECK_MEM(vector[0].iov_base, "0123456789", 10); + CHECK_PTR(source.iov_base, NULL); + CHECK_INT(source.iov_len, 0); + + iovec_free(&entries, &vector); +} + +static void case_fix_increase(void) { + size_t entries = 0; + struct iovec *vector = NULL; + char *block; + + test_case("fix_increase trims the previous entry and adds a new one"); + block = iovec_increase(&entries, &vector, 500); + CHECK(block != NULL); + block = iovec_fix_increase_or_free(&entries, &vector, block + 30, 200); + CHECK(block != NULL); + CHECK_INT(entries, 2); + CHECK_INT(vector[0].iov_len, 30); + CHECK_INT(vector[1].iov_len, 200); + + iovec_free(&entries, &vector); +} + +void test_iovec(void) { + case_increase_and_length(); + case_free_resets(); + case_fixlast_shrinks(); + case_fixlast_on_empty(); + case_append_takes_ownership(); + case_fix_increase(); +} diff --git a/tests/t_scan_urlencoded_query.c b/tests/t_scan_urlencoded_query.c new file mode 100644 index 0000000..4c1ef23 --- /dev/null +++ b/tests/t_scan_urlencoded_query.c @@ -0,0 +1,274 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include + +/* Opentracker */ +#include "scan_urlencoded_query.h" +#include "test.h" + +/* The scanner returns the number of bytes written to the destination, or + -1 for a parse error and -2 for a hard terminator encountered before any + output was produced. It advances the source pointer past the separator it + stopped on, except for hard terminators and the '&' ending a parameter, + which it leaves for the next call to see. */ + +static void case_path(void) { + char query[] = "/announce?info_hash=A"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("path stops at the question mark"); + n = scan_urlencoded_query(&p, dest, SCAN_PATH); + CHECK_INT(n, 9); + dest[n] = 0; + CHECK_STR(dest, "/announce"); + CHECK_INT(*p, 'i'); +} + +static void case_param_and_value(void) { + char query[] = "info_hash=abc&port=80"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("parameter stops at '=', value stops at '&'"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, 9); + dest[n] = 0; + CHECK_STR(dest, "info_hash"); + + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + dest[n] = 0; + CHECK_STR(dest, "abc"); + + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, 4); + dest[n] = 0; + CHECK_STR(dest, "port"); + + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 2); + dest[n] = 0; + CHECK_STR(dest, "80"); +} + +static void case_percent_decoding(void) { + char query[] = "%41%42%43&x=1"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("percent escapes decode"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + CHECK_MEM(dest, "ABC", 3); +} + +static void case_binary_info_hash(void) { + /* The twenty byte info_hash is what the announce path really decodes */ + char query[] = "%00%01%02%ff%fe%7f%80ABCDEFGHIJKLM&x=1"; + unsigned char want[] = {0x00, 0x01, 0x02, 0xff, 0xfe, 0x7f, 0x80, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M'}; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("twenty byte binary value survives decoding"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 20); + CHECK_MEM(dest, want, 20); +} + +static void case_in_place(void) { + char query[] = "%41%42&x=1"; + char *p = query; + ssize_t n; + + test_case("decoding in place over the source is safe"); + n = scan_urlencoded_query(&p, query, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 2); + CHECK_MEM(query, "AB", 2); +} + +static void case_bad_escape(void) { + char query[] = "%zz"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("a non hexadecimal escape is a parse error"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -1); +} + +static void case_truncated_escape(void) { + char query[] = "%4"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("an escape cut short by the terminator is a parse error"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -1); +} + +static void case_terminator(void) { + char empty[] = ""; + char space[] = " rest"; + char dest[64]; + char *p = empty; + ssize_t n; + + test_case("a hard terminator before any output reports end of input"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, -2); + + p = space; + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, -2); +} + +static void case_terminator_after_output(void) { + char query[] = "abc"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("a hard terminator after output is left for the next call"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + CHECK_INT(*p, 0); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -2); +} + +/* The character matrix makes '?' terminate a path only, and '=' terminate a + path or a parameter only. Both are ordinary characters inside a value, so a + value is free to contain a nested query string. */ + +static void case_separators_are_contextual(void) { + char question[] = "a?b&x=1"; + char equals[] = "a=b&x=1"; + char dest[64]; + char *p = question; + ssize_t n; + + test_case("a question mark is literal inside a value"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + dest[n] = 0; + CHECK_STR(dest, "a?b"); + + test_case("an equals sign is literal inside a value"); + p = equals; + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + dest[n] = 0; + CHECK_STR(dest, "a=b"); +} + +static void case_ampersand_in_path(void) { + char query[] = "/path&x"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("an ampersand in a path is a parse error"); + n = scan_urlencoded_query(&p, dest, SCAN_PATH); + CHECK_INT(n, -1); +} + +static void case_raw_high_byte(void) { + char query[] = {'a', 'b', (char)0x80, 0}; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("an undecoded byte above ASCII is a parse error"); + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -1); +} + +static void case_skipvalue(void) { + char query[] = "abc&next=1"; + char bare[] = "abc"; + char *p = query; + + test_case("skipvalue steps over the value and its separator"); + scan_urlencoded_skipvalue(&p); + CHECK_STR(p, "next=1"); + + test_case("skipvalue leaves a hard terminator in place"); + p = bare; + scan_urlencoded_skipvalue(&p); + CHECK_INT(*p, 0); +} + +static void case_find_keywords(void) { + static const ot_keywords keywords[] = {{"port", 1}, {"left", 2}, {"compact", 3}, {NULL, -3}}; + char match[] = "left=0"; + char nomatch[] = "bogus=1"; + char prefix[] = "por=1"; + char *p = match; + + test_case("a known keyword returns its value"); + CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), 2); + + test_case("an unknown keyword returns -3"); + p = nomatch; + CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); + + test_case("a prefix of a keyword does not match it"); + p = prefix; + CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); +} + +static void case_fixed_int(void) { + char positive[] = "123"; + char negative[] = "-45"; + char trailing[] = "12a"; + int value; + + test_case("scan_fixed_int returns the count of unparsed bytes"); + CHECK_INT(scan_fixed_int(positive, 3, &value), 0); + CHECK_INT(value, 123); + + CHECK_INT(scan_fixed_int(negative, 3, &value), 0); + CHECK_INT(value, -45); + + CHECK_INT(scan_fixed_int(trailing, 3, &value), 1); + CHECK_INT(value, 12); +} + +static void case_fixed_int_bounded(void) { + char digits[] = "12345"; + int value; + + test_case("scan_fixed_int honours the length it is given"); + CHECK_INT(scan_fixed_int(digits, 3, &value), 0); + CHECK_INT(value, 123); +} + +void test_scan_urlencoded_query(void) { + case_path(); + case_param_and_value(); + case_percent_decoding(); + case_binary_info_hash(); + case_in_place(); + case_bad_escape(); + case_truncated_escape(); + case_terminator(); + case_terminator_after_output(); + case_separators_are_contextual(); + case_ampersand_in_path(); + case_raw_high_byte(); + case_skipvalue(); + case_find_keywords(); + case_fixed_int(); + case_fixed_int_bounded(); +} diff --git a/tests/t_vector.c b/tests/t_vector.c new file mode 100644 index 0000000..fee336b --- /dev/null +++ b/tests/t_vector.c @@ -0,0 +1,161 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include + +/* Opentracker */ +#include "trackerlogic.h" +#include "test.h" + +/* binary_search returns the matching member when there is one, and otherwise + the position at which the key would have to be inserted to keep the vector + sorted. exactmatch distinguishes the two. */ + +static const unsigned char sorted[5][4] = {{0, 0, 0, 1}, {0, 0, 0, 3}, {0, 0, 0, 5}, {0, 0, 0, 7}, {0, 0, 0, 9}}; + +static void case_search_finds_member(void) { + const unsigned char key[4] = {0, 0, 0, 5}; + int exactmatch; + void *found; + + test_case("an existing key is found"); + found = binary_search(key, sorted, 5, 4, 4, &exactmatch); + CHECK(exactmatch != 0); + CHECK_PTR(found, sorted[2]); +} + +static void case_search_finds_first_and_last(void) { + const unsigned char first[4] = {0, 0, 0, 1}; + const unsigned char last[4] = {0, 0, 0, 9}; + int exactmatch; + + test_case("the first and last members are found"); + CHECK_PTR(binary_search(first, sorted, 5, 4, 4, &exactmatch), sorted[0]); + CHECK(exactmatch != 0); + CHECK_PTR(binary_search(last, sorted, 5, 4, 4, &exactmatch), sorted[4]); + CHECK(exactmatch != 0); +} + +static void case_search_insert_position(void) { + const unsigned char middle[4] = {0, 0, 0, 4}; + const unsigned char before[4] = {0, 0, 0, 0}; + int exactmatch; + void *slot; + + test_case("a missing key yields its insert position"); + slot = binary_search(middle, sorted, 5, 4, 4, &exactmatch); + CHECK_INT(exactmatch, 0); + CHECK_PTR(slot, sorted[2]); + + slot = binary_search(before, sorted, 5, 4, 4, &exactmatch); + CHECK_INT(exactmatch, 0); + CHECK_PTR(slot, sorted[0]); +} + +static void case_search_past_the_end(void) { + const unsigned char beyond[4] = {0, 0, 0, 0xff}; + int exactmatch; + void *slot; + + test_case("a key above every member yields the position past the end"); + slot = binary_search(beyond, sorted, 5, 4, 4, &exactmatch); + CHECK_INT(exactmatch, 0); + CHECK_PTR(slot, sorted[5]); +} + +static void case_search_empty(void) { + int exactmatch; + void *base = (void *)sorted; + + test_case("searching an empty vector reports no match at the base"); + CHECK_PTR(binary_search(sorted[0], base, 0, 4, 4, &exactmatch), base); + CHECK_INT(exactmatch, 0); +} + +static void case_insert_keeps_order(void) { + ot_vector vector = {NULL, 0, 0}; + const unsigned char five[4] = {0, 0, 0, 5}; + const unsigned char one[4] = {0, 0, 0, 1}; + const unsigned char nine[4] = {0, 0, 0, 9}; + int exactmatch; + void *slot; + + test_case("inserts land in sorted order and grow the vector"); + slot = vector_find_or_insert(&vector, (void *)five, 4, 4, &exactmatch); + CHECK(slot != NULL); + CHECK_INT(exactmatch, 0); + memcpy(slot, five, 4); + CHECK_INT(vector.size, 1); + CHECK_INT(vector.space, OT_VECTOR_MIN_MEMBERS); + + slot = vector_find_or_insert(&vector, (void *)one, 4, 4, &exactmatch); + CHECK(slot != NULL); + CHECK_INT(exactmatch, 0); + memcpy(slot, one, 4); + CHECK_INT(vector.size, 2); + + slot = vector_find_or_insert(&vector, (void *)nine, 4, 4, &exactmatch); + CHECK(slot != NULL); + CHECK_INT(exactmatch, 0); + memcpy(slot, nine, 4); + CHECK_INT(vector.size, 3); + CHECK_INT(vector.space, OT_VECTOR_MIN_MEMBERS * OT_VECTOR_GROW_RATIO); + + CHECK_MEM(vector.data, "\0\0\0\1\0\0\0\5\0\0\0\11", 12); + + free(vector.data); +} + +static void case_insert_finds_existing(void) { + ot_vector vector = {NULL, 0, 0}; + const unsigned char five[4] = {0, 0, 0, 5}; + int exactmatch; + void *slot, *again; + + test_case("a second insert of the same key returns the existing member"); + slot = vector_find_or_insert(&vector, (void *)five, 4, 4, &exactmatch); + CHECK(slot != NULL); + memcpy(slot, five, 4); + + again = vector_find_or_insert(&vector, (void *)five, 4, 4, &exactmatch); + CHECK(exactmatch != 0); + CHECK_PTR(again, vector.data); + CHECK_INT(vector.size, 1); + + free(vector.data); +} + +static void case_insert_compare_prefix(void) { + ot_vector vector = {NULL, 0, 0}; + unsigned char first[6] = {1, 2, 3, 0xaa, 0xaa, 0xaa}; + unsigned char second[6] = {1, 2, 3, 0xbb, 0xbb, 0xbb}; + int exactmatch; + void *slot; + + test_case("only the compare prefix decides identity"); + slot = vector_find_or_insert(&vector, first, 6, 3, &exactmatch); + CHECK(slot != NULL); + memcpy(slot, first, 6); + + slot = vector_find_or_insert(&vector, second, 6, 3, &exactmatch); + CHECK(exactmatch != 0); + CHECK_INT(vector.size, 1); + CHECK_MEM(slot, first, 6); + + free(vector.data); +} + +void test_vector(void) { + case_search_finds_member(); + case_search_finds_first_and_last(); + case_search_insert_position(); + case_search_past_the_end(); + case_search_empty(); + case_insert_keeps_order(); + case_insert_finds_existing(); + case_insert_compare_prefix(); +} diff --git a/tests/test.c b/tests/test.c new file mode 100644 index 0000000..4686369 --- /dev/null +++ b/tests/test.c @@ -0,0 +1,58 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include +#include + +/* Opentracker */ +#include "test.h" + +unsigned long test_checks; +unsigned long test_failures; + +static const char *current_module = ""; +static const char *current_case = ""; + +void test_module(const char *name) { + current_module = name; + current_case = ""; + printf("%s\n", name); +} + +void test_case(const char *name) { current_case = name; } + +static void test_where(const char *file, int line) { + ++test_failures; + printf(" FAIL %s/%s (%s:%d)\n ", current_module, current_case, file, line); +} + +void test_failf(const char *file, int line, const char *fmt, ...) { + va_list ap; + test_where(file, line); + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); + printf("\n"); +} + +void test_failmem(const char *file, int line, const char *what, const void *got, const void *want, size_t len) { + const unsigned char *g = got, *w = want; + size_t i; + + test_where(file, line); + printf("%s: %zu bytes differ\n got ", what, len); + for (i = 0; i < len; ++i) + printf("%02x", g[i]); + printf("\n want "); + for (i = 0; i < len; ++i) + printf("%02x", w[i]); + printf("\n"); +} + +int test_summary(void) { + printf("\n%lu checks, %lu failures\n", test_checks, test_failures); + return test_failures != 0; +} diff --git a/tests/test.h b/tests/test.h new file mode 100644 index 0000000..941e058 --- /dev/null +++ b/tests/test.h @@ -0,0 +1,83 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +#ifndef OT_TEST_H__ +#define OT_TEST_H__ + +#include +#include + +/* Minimal scaffolding for opentracker's unit tests. No external dependencies. + + Tests are grouped into modules, one per source file under test. A module is + a list of small case functions, each pinning one behaviour. A failing CHECK + reports file, line and the offending values, then returns from the case it + is in, so a single run reports as many independent problems as it can. + Because the macros return, they may only be used in functions returning + void. */ + +void test_module(const char *name); +void test_case(const char *name); +void test_failf(const char *file, int line, const char *fmt, ...); +void test_failmem(const char *file, int line, const char *what, const void *got, const void *want, size_t len); +int test_summary(void); + +extern unsigned long test_checks; +extern unsigned long test_failures; + +#define CHECK(cond) \ + do { \ + ++test_checks; \ + if (!(cond)) { \ + test_failf(__FILE__, __LINE__, "expected %s", #cond); \ + return; \ + } \ + } while (0) + +#define CHECK_INT(got, want) \ + do { \ + long long got_ = (long long)(got), want_ = (long long)(want); \ + ++test_checks; \ + if (got_ != want_) { \ + test_failf(__FILE__, __LINE__, "%s: got %lld, want %lld", #got, got_, want_); \ + return; \ + } \ + } while (0) + +#define CHECK_PTR(got, want) \ + do { \ + const void *got_ = (const void *)(got), *want_ = (const void *)(want); \ + ++test_checks; \ + if (got_ != want_) { \ + test_failf(__FILE__, __LINE__, "%s: got %p, want %p", #got, got_, want_); \ + return; \ + } \ + } while (0) + +#define CHECK_MEM(got, want, len) \ + do { \ + ++test_checks; \ + if (memcmp((got), (want), (len))) { \ + test_failmem(__FILE__, __LINE__, #got, (got), (want), (len)); \ + return; \ + } \ + } while (0) + +#define CHECK_STR(got, want) \ + do { \ + const char *got_ = (got), *want_ = (want); \ + ++test_checks; \ + if (strcmp(got_, want_)) { \ + test_failf(__FILE__, __LINE__, "%s: got \"%s\", want \"%s\"", #got, got_, want_); \ + return; \ + } \ + } while (0) + +/* One entry point per module under test */ +void test_scan_urlencoded_query(void); +void test_iovec(void); +void test_vector(void); + +#endif -- cgit v1.3