diff options
| author | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-24 01:17:50 +0200 |
|---|---|---|
| committer | Dirk Engling <erdgeist@erdgeist.org> | 2026-10-24 01:17:50 +0200 |
| commit | f24ec48b27d4cb8dd0555840206dedbb45f1423c (patch) | |
| tree | 4cf772a476be6b06f52b80abc05f952a33209711 | |
| parent | f33203e15a00449152283cab2b878c0886fcede5 (diff) | |
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.
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | Makefile | 20 | ||||
| -rw-r--r-- | tests/runner.c | 34 | ||||
| -rw-r--r-- | tests/stubs.c | 14 | ||||
| -rw-r--r-- | tests/t_iovec.c | 124 | ||||
| -rw-r--r-- | tests/t_scan_urlencoded_query.c | 274 | ||||
| -rw-r--r-- | tests/t_vector.c | 161 | ||||
| -rw-r--r-- | tests/test.c | 58 | ||||
| -rw-r--r-- | tests/test.h | 83 |
9 files changed, 768 insertions, 2 deletions
| @@ -1,2 +1,2 @@ | |||
| 1 | *.o | 1 | *.o |
| 2 | 2 | tests/runner | |
| @@ -99,8 +99,26 @@ proxy.debug: $(OBJECTS_proxy_debug) $(HEADERS) | |||
| 99 | .c.o : $(HEADERS) | 99 | .c.o : $(HEADERS) |
| 100 | $(CC) -c -o $@ $(CFLAGS_production) $< | 100 | $(CC) -c -o $@ $(CFLAGS_production) $< |
| 101 | 101 | ||
| 102 | # Unit tests. The modules under test are compiled straight into the test | ||
| 103 | # binary, so a test may reach a static function by including the source if it | ||
| 104 | # has to. tests/stubs.c supplies the few symbols those modules reference but | ||
| 105 | # the tests never reach. | ||
| 106 | TESTBIN=tests/runner | ||
| 107 | 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 | ||
| 108 | TEST_UNITS=scan_urlencoded_query.c ot_iovec.c ot_vector.c | ||
| 109 | TEST_HEADERS=tests/test.h $(HEADERS) | ||
| 110 | CFLAGS_test = $(CFLAGS) -I. -g -O1 $(FEATURES) | ||
| 111 | |||
| 112 | $(TESTBIN): $(TEST_SOURCES) $(TEST_UNITS) $(TEST_HEADERS) | ||
| 113 | $(CC) -o $@ $(CFLAGS_test) $(TEST_SOURCES) $(TEST_UNITS) | ||
| 114 | |||
| 115 | check: $(TESTBIN) | ||
| 116 | ./$(TESTBIN) | ||
| 117 | |||
| 118 | .PHONY: all check clean install | ||
| 119 | |||
| 102 | clean: | 120 | clean: |
| 103 | rm -rf opentracker opentracker.debug *.o *~ | 121 | rm -rf opentracker opentracker.debug $(TESTBIN) *.o *~ |
| 104 | 122 | ||
| 105 | install: | 123 | install: |
| 106 | install -m 755 opentracker $(DESTDIR)$(BINDIR) | 124 | 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 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | /* Opentracker */ | ||
| 10 | #include "test.h" | ||
| 11 | |||
| 12 | struct module { | ||
| 13 | const char *name; | ||
| 14 | void (*run)(void); | ||
| 15 | }; | ||
| 16 | |||
| 17 | static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urlencoded_query}, | ||
| 18 | {"ot_iovec", test_iovec}, | ||
| 19 | {"ot_vector", test_vector}, | ||
| 20 | {NULL, NULL}}; | ||
| 21 | |||
| 22 | /* Run every module, or only those whose name contains argv[1] */ | ||
| 23 | int main(int argc, char **argv) { | ||
| 24 | const struct module *m; | ||
| 25 | |||
| 26 | for (m = modules; m->name; ++m) { | ||
| 27 | if (argc > 1 && !strstr(m->name, argv[1])) | ||
| 28 | continue; | ||
| 29 | test_module(m->name); | ||
| 30 | m->run(); | ||
| 31 | } | ||
| 32 | |||
| 33 | return test_summary(); | ||
| 34 | } | ||
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 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* Stubs for symbols the modules under test reference but never reach along | ||
| 7 | the paths the tests exercise. If a test ever does reach one, the missing | ||
| 8 | behaviour should be supplied here rather than the test weakened. */ | ||
| 9 | |||
| 10 | /* Opentracker */ | ||
| 11 | #include "trackerlogic.h" | ||
| 12 | |||
| 13 | /* ot_vector.c calls this from vector_clean_list, which no test drives yet. */ | ||
| 14 | 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 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <stdlib.h> | ||
| 8 | #include <string.h> | ||
| 9 | #include <sys/uio.h> | ||
| 10 | |||
| 11 | /* Opentracker */ | ||
| 12 | #include "ot_iovec.h" | ||
| 13 | #include "test.h" | ||
| 14 | |||
| 15 | static void case_increase_and_length(void) { | ||
| 16 | size_t entries = 0; | ||
| 17 | struct iovec *vector = NULL; | ||
| 18 | void *first, *second; | ||
| 19 | |||
| 20 | test_case("increase appends an entry and length is their sum"); | ||
| 21 | first = iovec_increase(&entries, &vector, 100); | ||
| 22 | CHECK(first != NULL); | ||
| 23 | CHECK_INT(entries, 1); | ||
| 24 | CHECK_INT(vector[0].iov_len, 100); | ||
| 25 | CHECK_PTR(vector[0].iov_base, first); | ||
| 26 | |||
| 27 | second = iovec_increase(&entries, &vector, 50); | ||
| 28 | CHECK(second != NULL); | ||
| 29 | CHECK_INT(entries, 2); | ||
| 30 | CHECK_INT(vector[1].iov_len, 50); | ||
| 31 | CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 150); | ||
| 32 | |||
| 33 | iovec_free(&entries, &vector); | ||
| 34 | } | ||
| 35 | |||
| 36 | static void case_free_resets(void) { | ||
| 37 | size_t entries = 0; | ||
| 38 | struct iovec *vector = NULL; | ||
| 39 | |||
| 40 | test_case("free releases the entries and resets the vector"); | ||
| 41 | CHECK(iovec_increase(&entries, &vector, 10) != NULL); | ||
| 42 | CHECK(iovec_increase(&entries, &vector, 10) != NULL); | ||
| 43 | iovec_free(&entries, &vector); | ||
| 44 | CHECK_INT(entries, 0); | ||
| 45 | CHECK_PTR(vector, NULL); | ||
| 46 | CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 0); | ||
| 47 | } | ||
| 48 | |||
| 49 | static void case_fixlast_shrinks(void) { | ||
| 50 | size_t entries = 0; | ||
| 51 | struct iovec *vector = NULL; | ||
| 52 | char *block; | ||
| 53 | |||
| 54 | test_case("fixlast trims the last entry to the bytes actually used"); | ||
| 55 | CHECK(iovec_increase(&entries, &vector, 64) != NULL); | ||
| 56 | block = iovec_increase(&entries, &vector, 1000); | ||
| 57 | CHECK(block != NULL); | ||
| 58 | memset(block, 'x', 20); | ||
| 59 | |||
| 60 | iovec_fixlast(&entries, &vector, block + 20); | ||
| 61 | CHECK_INT(entries, 2); | ||
| 62 | CHECK_INT(vector[1].iov_len, 20); | ||
| 63 | CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 84); | ||
| 64 | CHECK_MEM(vector[1].iov_base, "xxxxxxxxxxxxxxxxxxxx", 20); | ||
| 65 | |||
| 66 | iovec_free(&entries, &vector); | ||
| 67 | } | ||
| 68 | |||
| 69 | static void case_fixlast_on_empty(void) { | ||
| 70 | size_t entries = 0; | ||
| 71 | struct iovec *vector = NULL; | ||
| 72 | |||
| 73 | test_case("fixlast on an empty vector does nothing"); | ||
| 74 | iovec_fixlast(&entries, &vector, NULL); | ||
| 75 | CHECK_INT(entries, 0); | ||
| 76 | CHECK_PTR(vector, NULL); | ||
| 77 | } | ||
| 78 | |||
| 79 | static void case_append_takes_ownership(void) { | ||
| 80 | size_t entries = 0; | ||
| 81 | struct iovec *vector = NULL; | ||
| 82 | struct iovec source; | ||
| 83 | |||
| 84 | test_case("append moves the buffer and clears the source"); | ||
| 85 | source.iov_base = malloc(10); | ||
| 86 | CHECK(source.iov_base != NULL); | ||
| 87 | memcpy(source.iov_base, "0123456789", 10); | ||
| 88 | source.iov_len = 10; | ||
| 89 | |||
| 90 | CHECK(iovec_append(&entries, &vector, &source) != NULL); | ||
| 91 | CHECK_INT(entries, 1); | ||
| 92 | CHECK_INT(vector[0].iov_len, 10); | ||
| 93 | CHECK_MEM(vector[0].iov_base, "0123456789", 10); | ||
| 94 | CHECK_PTR(source.iov_base, NULL); | ||
| 95 | CHECK_INT(source.iov_len, 0); | ||
| 96 | |||
| 97 | iovec_free(&entries, &vector); | ||
| 98 | } | ||
| 99 | |||
| 100 | static void case_fix_increase(void) { | ||
| 101 | size_t entries = 0; | ||
| 102 | struct iovec *vector = NULL; | ||
| 103 | char *block; | ||
| 104 | |||
| 105 | test_case("fix_increase trims the previous entry and adds a new one"); | ||
| 106 | block = iovec_increase(&entries, &vector, 500); | ||
| 107 | CHECK(block != NULL); | ||
| 108 | block = iovec_fix_increase_or_free(&entries, &vector, block + 30, 200); | ||
| 109 | CHECK(block != NULL); | ||
| 110 | CHECK_INT(entries, 2); | ||
| 111 | CHECK_INT(vector[0].iov_len, 30); | ||
| 112 | CHECK_INT(vector[1].iov_len, 200); | ||
| 113 | |||
| 114 | iovec_free(&entries, &vector); | ||
| 115 | } | ||
| 116 | |||
| 117 | void test_iovec(void) { | ||
| 118 | case_increase_and_length(); | ||
| 119 | case_free_resets(); | ||
| 120 | case_fixlast_shrinks(); | ||
| 121 | case_fixlast_on_empty(); | ||
| 122 | case_append_takes_ownership(); | ||
| 123 | case_fix_increase(); | ||
| 124 | } | ||
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 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <string.h> | ||
| 8 | |||
| 9 | /* Opentracker */ | ||
| 10 | #include "scan_urlencoded_query.h" | ||
| 11 | #include "test.h" | ||
| 12 | |||
| 13 | /* The scanner returns the number of bytes written to the destination, or | ||
| 14 | -1 for a parse error and -2 for a hard terminator encountered before any | ||
| 15 | output was produced. It advances the source pointer past the separator it | ||
| 16 | stopped on, except for hard terminators and the '&' ending a parameter, | ||
| 17 | which it leaves for the next call to see. */ | ||
| 18 | |||
| 19 | static void case_path(void) { | ||
| 20 | char query[] = "/announce?info_hash=A"; | ||
| 21 | char dest[64]; | ||
| 22 | char *p = query; | ||
| 23 | ssize_t n; | ||
| 24 | |||
| 25 | test_case("path stops at the question mark"); | ||
| 26 | n = scan_urlencoded_query(&p, dest, SCAN_PATH); | ||
| 27 | CHECK_INT(n, 9); | ||
| 28 | dest[n] = 0; | ||
| 29 | CHECK_STR(dest, "/announce"); | ||
| 30 | CHECK_INT(*p, 'i'); | ||
| 31 | } | ||
| 32 | |||
| 33 | static void case_param_and_value(void) { | ||
| 34 | char query[] = "info_hash=abc&port=80"; | ||
| 35 | char dest[64]; | ||
| 36 | char *p = query; | ||
| 37 | ssize_t n; | ||
| 38 | |||
| 39 | test_case("parameter stops at '=', value stops at '&'"); | ||
| 40 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | ||
| 41 | CHECK_INT(n, 9); | ||
| 42 | dest[n] = 0; | ||
| 43 | CHECK_STR(dest, "info_hash"); | ||
| 44 | |||
| 45 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 46 | CHECK_INT(n, 3); | ||
| 47 | dest[n] = 0; | ||
| 48 | CHECK_STR(dest, "abc"); | ||
| 49 | |||
| 50 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | ||
| 51 | CHECK_INT(n, 4); | ||
| 52 | dest[n] = 0; | ||
| 53 | CHECK_STR(dest, "port"); | ||
| 54 | |||
| 55 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 56 | CHECK_INT(n, 2); | ||
| 57 | dest[n] = 0; | ||
| 58 | CHECK_STR(dest, "80"); | ||
| 59 | } | ||
| 60 | |||
| 61 | static void case_percent_decoding(void) { | ||
| 62 | char query[] = "%41%42%43&x=1"; | ||
| 63 | char dest[64]; | ||
| 64 | char *p = query; | ||
| 65 | ssize_t n; | ||
| 66 | |||
| 67 | test_case("percent escapes decode"); | ||
| 68 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 69 | CHECK_INT(n, 3); | ||
| 70 | CHECK_MEM(dest, "ABC", 3); | ||
| 71 | } | ||
| 72 | |||
| 73 | static void case_binary_info_hash(void) { | ||
| 74 | /* The twenty byte info_hash is what the announce path really decodes */ | ||
| 75 | char query[] = "%00%01%02%ff%fe%7f%80ABCDEFGHIJKLM&x=1"; | ||
| 76 | unsigned char want[] = {0x00, 0x01, 0x02, 0xff, 0xfe, 0x7f, 0x80, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M'}; | ||
| 77 | char dest[64]; | ||
| 78 | char *p = query; | ||
| 79 | ssize_t n; | ||
| 80 | |||
| 81 | test_case("twenty byte binary value survives decoding"); | ||
| 82 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 83 | CHECK_INT(n, 20); | ||
| 84 | CHECK_MEM(dest, want, 20); | ||
| 85 | } | ||
| 86 | |||
| 87 | static void case_in_place(void) { | ||
| 88 | char query[] = "%41%42&x=1"; | ||
| 89 | char *p = query; | ||
| 90 | ssize_t n; | ||
| 91 | |||
| 92 | test_case("decoding in place over the source is safe"); | ||
| 93 | n = scan_urlencoded_query(&p, query, SCAN_SEARCHPATH_VALUE); | ||
| 94 | CHECK_INT(n, 2); | ||
| 95 | CHECK_MEM(query, "AB", 2); | ||
| 96 | } | ||
| 97 | |||
| 98 | static void case_bad_escape(void) { | ||
| 99 | char query[] = "%zz"; | ||
| 100 | char dest[64]; | ||
| 101 | char *p = query; | ||
| 102 | ssize_t n; | ||
| 103 | |||
| 104 | test_case("a non hexadecimal escape is a parse error"); | ||
| 105 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 106 | CHECK_INT(n, -1); | ||
| 107 | } | ||
| 108 | |||
| 109 | static void case_truncated_escape(void) { | ||
| 110 | char query[] = "%4"; | ||
| 111 | char dest[64]; | ||
| 112 | char *p = query; | ||
| 113 | ssize_t n; | ||
| 114 | |||
| 115 | test_case("an escape cut short by the terminator is a parse error"); | ||
| 116 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 117 | CHECK_INT(n, -1); | ||
| 118 | } | ||
| 119 | |||
| 120 | static void case_terminator(void) { | ||
| 121 | char empty[] = ""; | ||
| 122 | char space[] = " rest"; | ||
| 123 | char dest[64]; | ||
| 124 | char *p = empty; | ||
| 125 | ssize_t n; | ||
| 126 | |||
| 127 | test_case("a hard terminator before any output reports end of input"); | ||
| 128 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | ||
| 129 | CHECK_INT(n, -2); | ||
| 130 | |||
| 131 | p = space; | ||
| 132 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); | ||
| 133 | CHECK_INT(n, -2); | ||
| 134 | } | ||
| 135 | |||
| 136 | static void case_terminator_after_output(void) { | ||
| 137 | char query[] = "abc"; | ||
| 138 | char dest[64]; | ||
| 139 | char *p = query; | ||
| 140 | ssize_t n; | ||
| 141 | |||
| 142 | test_case("a hard terminator after output is left for the next call"); | ||
| 143 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 144 | CHECK_INT(n, 3); | ||
| 145 | CHECK_INT(*p, 0); | ||
| 146 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 147 | CHECK_INT(n, -2); | ||
| 148 | } | ||
| 149 | |||
| 150 | /* The character matrix makes '?' terminate a path only, and '=' terminate a | ||
| 151 | path or a parameter only. Both are ordinary characters inside a value, so a | ||
| 152 | value is free to contain a nested query string. */ | ||
| 153 | |||
| 154 | static void case_separators_are_contextual(void) { | ||
| 155 | char question[] = "a?b&x=1"; | ||
| 156 | char equals[] = "a=b&x=1"; | ||
| 157 | char dest[64]; | ||
| 158 | char *p = question; | ||
| 159 | ssize_t n; | ||
| 160 | |||
| 161 | test_case("a question mark is literal inside a value"); | ||
| 162 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 163 | CHECK_INT(n, 3); | ||
| 164 | dest[n] = 0; | ||
| 165 | CHECK_STR(dest, "a?b"); | ||
| 166 | |||
| 167 | test_case("an equals sign is literal inside a value"); | ||
| 168 | p = equals; | ||
| 169 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 170 | CHECK_INT(n, 3); | ||
| 171 | dest[n] = 0; | ||
| 172 | CHECK_STR(dest, "a=b"); | ||
| 173 | } | ||
| 174 | |||
| 175 | static void case_ampersand_in_path(void) { | ||
| 176 | char query[] = "/path&x"; | ||
| 177 | char dest[64]; | ||
| 178 | char *p = query; | ||
| 179 | ssize_t n; | ||
| 180 | |||
| 181 | test_case("an ampersand in a path is a parse error"); | ||
| 182 | n = scan_urlencoded_query(&p, dest, SCAN_PATH); | ||
| 183 | CHECK_INT(n, -1); | ||
| 184 | } | ||
| 185 | |||
| 186 | static void case_raw_high_byte(void) { | ||
| 187 | char query[] = {'a', 'b', (char)0x80, 0}; | ||
| 188 | char dest[64]; | ||
| 189 | char *p = query; | ||
| 190 | ssize_t n; | ||
| 191 | |||
| 192 | test_case("an undecoded byte above ASCII is a parse error"); | ||
| 193 | n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); | ||
| 194 | CHECK_INT(n, -1); | ||
| 195 | } | ||
| 196 | |||
| 197 | static void case_skipvalue(void) { | ||
| 198 | char query[] = "abc&next=1"; | ||
| 199 | char bare[] = "abc"; | ||
| 200 | char *p = query; | ||
| 201 | |||
| 202 | test_case("skipvalue steps over the value and its separator"); | ||
| 203 | scan_urlencoded_skipvalue(&p); | ||
| 204 | CHECK_STR(p, "next=1"); | ||
| 205 | |||
| 206 | test_case("skipvalue leaves a hard terminator in place"); | ||
| 207 | p = bare; | ||
| 208 | scan_urlencoded_skipvalue(&p); | ||
| 209 | CHECK_INT(*p, 0); | ||
| 210 | } | ||
| 211 | |||
| 212 | static void case_find_keywords(void) { | ||
| 213 | static const ot_keywords keywords[] = {{"port", 1}, {"left", 2}, {"compact", 3}, {NULL, -3}}; | ||
| 214 | char match[] = "left=0"; | ||
| 215 | char nomatch[] = "bogus=1"; | ||
| 216 | char prefix[] = "por=1"; | ||
| 217 | char *p = match; | ||
| 218 | |||
| 219 | test_case("a known keyword returns its value"); | ||
| 220 | CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), 2); | ||
| 221 | |||
| 222 | test_case("an unknown keyword returns -3"); | ||
| 223 | p = nomatch; | ||
| 224 | CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); | ||
| 225 | |||
| 226 | test_case("a prefix of a keyword does not match it"); | ||
| 227 | p = prefix; | ||
| 228 | CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); | ||
| 229 | } | ||
| 230 | |||
| 231 | static void case_fixed_int(void) { | ||
| 232 | char positive[] = "123"; | ||
| 233 | char negative[] = "-45"; | ||
| 234 | char trailing[] = "12a"; | ||
| 235 | int value; | ||
| 236 | |||
| 237 | test_case("scan_fixed_int returns the count of unparsed bytes"); | ||
| 238 | CHECK_INT(scan_fixed_int(positive, 3, &value), 0); | ||
| 239 | CHECK_INT(value, 123); | ||
| 240 | |||
| 241 | CHECK_INT(scan_fixed_int(negative, 3, &value), 0); | ||
| 242 | CHECK_INT(value, -45); | ||
| 243 | |||
| 244 | CHECK_INT(scan_fixed_int(trailing, 3, &value), 1); | ||
| 245 | CHECK_INT(value, 12); | ||
| 246 | } | ||
| 247 | |||
| 248 | static void case_fixed_int_bounded(void) { | ||
| 249 | char digits[] = "12345"; | ||
| 250 | int value; | ||
| 251 | |||
| 252 | test_case("scan_fixed_int honours the length it is given"); | ||
| 253 | CHECK_INT(scan_fixed_int(digits, 3, &value), 0); | ||
| 254 | CHECK_INT(value, 123); | ||
| 255 | } | ||
| 256 | |||
| 257 | void test_scan_urlencoded_query(void) { | ||
| 258 | case_path(); | ||
| 259 | case_param_and_value(); | ||
| 260 | case_percent_decoding(); | ||
| 261 | case_binary_info_hash(); | ||
| 262 | case_in_place(); | ||
| 263 | case_bad_escape(); | ||
| 264 | case_truncated_escape(); | ||
| 265 | case_terminator(); | ||
| 266 | case_terminator_after_output(); | ||
| 267 | case_separators_are_contextual(); | ||
| 268 | case_ampersand_in_path(); | ||
| 269 | case_raw_high_byte(); | ||
| 270 | case_skipvalue(); | ||
| 271 | case_find_keywords(); | ||
| 272 | case_fixed_int(); | ||
| 273 | case_fixed_int_bounded(); | ||
| 274 | } | ||
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 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <stdlib.h> | ||
| 8 | #include <string.h> | ||
| 9 | |||
| 10 | /* Opentracker */ | ||
| 11 | #include "trackerlogic.h" | ||
| 12 | #include "test.h" | ||
| 13 | |||
| 14 | /* binary_search returns the matching member when there is one, and otherwise | ||
| 15 | the position at which the key would have to be inserted to keep the vector | ||
| 16 | sorted. exactmatch distinguishes the two. */ | ||
| 17 | |||
| 18 | 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}}; | ||
| 19 | |||
| 20 | static void case_search_finds_member(void) { | ||
| 21 | const unsigned char key[4] = {0, 0, 0, 5}; | ||
| 22 | int exactmatch; | ||
| 23 | void *found; | ||
| 24 | |||
| 25 | test_case("an existing key is found"); | ||
| 26 | found = binary_search(key, sorted, 5, 4, 4, &exactmatch); | ||
| 27 | CHECK(exactmatch != 0); | ||
| 28 | CHECK_PTR(found, sorted[2]); | ||
| 29 | } | ||
| 30 | |||
| 31 | static void case_search_finds_first_and_last(void) { | ||
| 32 | const unsigned char first[4] = {0, 0, 0, 1}; | ||
| 33 | const unsigned char last[4] = {0, 0, 0, 9}; | ||
| 34 | int exactmatch; | ||
| 35 | |||
| 36 | test_case("the first and last members are found"); | ||
| 37 | CHECK_PTR(binary_search(first, sorted, 5, 4, 4, &exactmatch), sorted[0]); | ||
| 38 | CHECK(exactmatch != 0); | ||
| 39 | CHECK_PTR(binary_search(last, sorted, 5, 4, 4, &exactmatch), sorted[4]); | ||
| 40 | CHECK(exactmatch != 0); | ||
| 41 | } | ||
| 42 | |||
| 43 | static void case_search_insert_position(void) { | ||
| 44 | const unsigned char middle[4] = {0, 0, 0, 4}; | ||
| 45 | const unsigned char before[4] = {0, 0, 0, 0}; | ||
| 46 | int exactmatch; | ||
| 47 | void *slot; | ||
| 48 | |||
| 49 | test_case("a missing key yields its insert position"); | ||
| 50 | slot = binary_search(middle, sorted, 5, 4, 4, &exactmatch); | ||
| 51 | CHECK_INT(exactmatch, 0); | ||
| 52 | CHECK_PTR(slot, sorted[2]); | ||
| 53 | |||
| 54 | slot = binary_search(before, sorted, 5, 4, 4, &exactmatch); | ||
| 55 | CHECK_INT(exactmatch, 0); | ||
| 56 | CHECK_PTR(slot, sorted[0]); | ||
| 57 | } | ||
| 58 | |||
| 59 | static void case_search_past_the_end(void) { | ||
| 60 | const unsigned char beyond[4] = {0, 0, 0, 0xff}; | ||
| 61 | int exactmatch; | ||
| 62 | void *slot; | ||
| 63 | |||
| 64 | test_case("a key above every member yields the position past the end"); | ||
| 65 | slot = binary_search(beyond, sorted, 5, 4, 4, &exactmatch); | ||
| 66 | CHECK_INT(exactmatch, 0); | ||
| 67 | CHECK_PTR(slot, sorted[5]); | ||
| 68 | } | ||
| 69 | |||
| 70 | static void case_search_empty(void) { | ||
| 71 | int exactmatch; | ||
| 72 | void *base = (void *)sorted; | ||
| 73 | |||
| 74 | test_case("searching an empty vector reports no match at the base"); | ||
| 75 | CHECK_PTR(binary_search(sorted[0], base, 0, 4, 4, &exactmatch), base); | ||
| 76 | CHECK_INT(exactmatch, 0); | ||
| 77 | } | ||
| 78 | |||
| 79 | static void case_insert_keeps_order(void) { | ||
| 80 | ot_vector vector = {NULL, 0, 0}; | ||
| 81 | const unsigned char five[4] = {0, 0, 0, 5}; | ||
| 82 | const unsigned char one[4] = {0, 0, 0, 1}; | ||
| 83 | const unsigned char nine[4] = {0, 0, 0, 9}; | ||
| 84 | int exactmatch; | ||
| 85 | void *slot; | ||
| 86 | |||
| 87 | test_case("inserts land in sorted order and grow the vector"); | ||
| 88 | slot = vector_find_or_insert(&vector, (void *)five, 4, 4, &exactmatch); | ||
| 89 | CHECK(slot != NULL); | ||
| 90 | CHECK_INT(exactmatch, 0); | ||
| 91 | memcpy(slot, five, 4); | ||
| 92 | CHECK_INT(vector.size, 1); | ||
| 93 | CHECK_INT(vector.space, OT_VECTOR_MIN_MEMBERS); | ||
| 94 | |||
| 95 | slot = vector_find_or_insert(&vector, (void *)one, 4, 4, &exactmatch); | ||
| 96 | CHECK(slot != NULL); | ||
| 97 | CHECK_INT(exactmatch, 0); | ||
| 98 | memcpy(slot, one, 4); | ||
| 99 | CHECK_INT(vector.size, 2); | ||
| 100 | |||
| 101 | slot = vector_find_or_insert(&vector, (void *)nine, 4, 4, &exactmatch); | ||
| 102 | CHECK(slot != NULL); | ||
| 103 | CHECK_INT(exactmatch, 0); | ||
| 104 | memcpy(slot, nine, 4); | ||
| 105 | CHECK_INT(vector.size, 3); | ||
| 106 | CHECK_INT(vector.space, OT_VECTOR_MIN_MEMBERS * OT_VECTOR_GROW_RATIO); | ||
| 107 | |||
| 108 | CHECK_MEM(vector.data, "\0\0\0\1\0\0\0\5\0\0\0\11", 12); | ||
| 109 | |||
| 110 | free(vector.data); | ||
| 111 | } | ||
| 112 | |||
| 113 | static void case_insert_finds_existing(void) { | ||
| 114 | ot_vector vector = {NULL, 0, 0}; | ||
| 115 | const unsigned char five[4] = {0, 0, 0, 5}; | ||
| 116 | int exactmatch; | ||
| 117 | void *slot, *again; | ||
| 118 | |||
| 119 | test_case("a second insert of the same key returns the existing member"); | ||
| 120 | slot = vector_find_or_insert(&vector, (void *)five, 4, 4, &exactmatch); | ||
| 121 | CHECK(slot != NULL); | ||
| 122 | memcpy(slot, five, 4); | ||
| 123 | |||
| 124 | again = vector_find_or_insert(&vector, (void *)five, 4, 4, &exactmatch); | ||
| 125 | CHECK(exactmatch != 0); | ||
| 126 | CHECK_PTR(again, vector.data); | ||
| 127 | CHECK_INT(vector.size, 1); | ||
| 128 | |||
| 129 | free(vector.data); | ||
| 130 | } | ||
| 131 | |||
| 132 | static void case_insert_compare_prefix(void) { | ||
| 133 | ot_vector vector = {NULL, 0, 0}; | ||
| 134 | unsigned char first[6] = {1, 2, 3, 0xaa, 0xaa, 0xaa}; | ||
| 135 | unsigned char second[6] = {1, 2, 3, 0xbb, 0xbb, 0xbb}; | ||
| 136 | int exactmatch; | ||
| 137 | void *slot; | ||
| 138 | |||
| 139 | test_case("only the compare prefix decides identity"); | ||
| 140 | slot = vector_find_or_insert(&vector, first, 6, 3, &exactmatch); | ||
| 141 | CHECK(slot != NULL); | ||
| 142 | memcpy(slot, first, 6); | ||
| 143 | |||
| 144 | slot = vector_find_or_insert(&vector, second, 6, 3, &exactmatch); | ||
| 145 | CHECK(exactmatch != 0); | ||
| 146 | CHECK_INT(vector.size, 1); | ||
| 147 | CHECK_MEM(slot, first, 6); | ||
| 148 | |||
| 149 | free(vector.data); | ||
| 150 | } | ||
| 151 | |||
| 152 | void test_vector(void) { | ||
| 153 | case_search_finds_member(); | ||
| 154 | case_search_finds_first_and_last(); | ||
| 155 | case_search_insert_position(); | ||
| 156 | case_search_past_the_end(); | ||
| 157 | case_search_empty(); | ||
| 158 | case_insert_keeps_order(); | ||
| 159 | case_insert_finds_existing(); | ||
| 160 | case_insert_compare_prefix(); | ||
| 161 | } | ||
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 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | /* System */ | ||
| 7 | #include <stdarg.h> | ||
| 8 | #include <stdio.h> | ||
| 9 | |||
| 10 | /* Opentracker */ | ||
| 11 | #include "test.h" | ||
| 12 | |||
| 13 | unsigned long test_checks; | ||
| 14 | unsigned long test_failures; | ||
| 15 | |||
| 16 | static const char *current_module = ""; | ||
| 17 | static const char *current_case = ""; | ||
| 18 | |||
| 19 | void test_module(const char *name) { | ||
| 20 | current_module = name; | ||
| 21 | current_case = ""; | ||
| 22 | printf("%s\n", name); | ||
| 23 | } | ||
| 24 | |||
| 25 | void test_case(const char *name) { current_case = name; } | ||
| 26 | |||
| 27 | static void test_where(const char *file, int line) { | ||
| 28 | ++test_failures; | ||
| 29 | printf(" FAIL %s/%s (%s:%d)\n ", current_module, current_case, file, line); | ||
| 30 | } | ||
| 31 | |||
| 32 | void test_failf(const char *file, int line, const char *fmt, ...) { | ||
| 33 | va_list ap; | ||
| 34 | test_where(file, line); | ||
| 35 | va_start(ap, fmt); | ||
| 36 | vprintf(fmt, ap); | ||
| 37 | va_end(ap); | ||
| 38 | printf("\n"); | ||
| 39 | } | ||
| 40 | |||
| 41 | void test_failmem(const char *file, int line, const char *what, const void *got, const void *want, size_t len) { | ||
| 42 | const unsigned char *g = got, *w = want; | ||
| 43 | size_t i; | ||
| 44 | |||
| 45 | test_where(file, line); | ||
| 46 | printf("%s: %zu bytes differ\n got ", what, len); | ||
| 47 | for (i = 0; i < len; ++i) | ||
| 48 | printf("%02x", g[i]); | ||
| 49 | printf("\n want "); | ||
| 50 | for (i = 0; i < len; ++i) | ||
| 51 | printf("%02x", w[i]); | ||
| 52 | printf("\n"); | ||
| 53 | } | ||
| 54 | |||
| 55 | int test_summary(void) { | ||
| 56 | printf("\n%lu checks, %lu failures\n", test_checks, test_failures); | ||
| 57 | return test_failures != 0; | ||
| 58 | } | ||
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 @@ | |||
| 1 | /* This software was written by Dirk Engling <erdgeist@erdgeist.org> | ||
| 2 | It is considered beerware. Prost. Skol. Cheers or whatever. | ||
| 3 | |||
| 4 | $id$ */ | ||
| 5 | |||
| 6 | #ifndef OT_TEST_H__ | ||
| 7 | #define OT_TEST_H__ | ||
| 8 | |||
| 9 | #include <stddef.h> | ||
| 10 | #include <string.h> | ||
| 11 | |||
| 12 | /* Minimal scaffolding for opentracker's unit tests. No external dependencies. | ||
| 13 | |||
| 14 | Tests are grouped into modules, one per source file under test. A module is | ||
| 15 | a list of small case functions, each pinning one behaviour. A failing CHECK | ||
| 16 | reports file, line and the offending values, then returns from the case it | ||
| 17 | is in, so a single run reports as many independent problems as it can. | ||
| 18 | Because the macros return, they may only be used in functions returning | ||
| 19 | void. */ | ||
| 20 | |||
| 21 | void test_module(const char *name); | ||
| 22 | void test_case(const char *name); | ||
| 23 | void test_failf(const char *file, int line, const char *fmt, ...); | ||
| 24 | void test_failmem(const char *file, int line, const char *what, const void *got, const void *want, size_t len); | ||
| 25 | int test_summary(void); | ||
| 26 | |||
| 27 | extern unsigned long test_checks; | ||
| 28 | extern unsigned long test_failures; | ||
| 29 | |||
| 30 | #define CHECK(cond) \ | ||
| 31 | do { \ | ||
| 32 | ++test_checks; \ | ||
| 33 | if (!(cond)) { \ | ||
| 34 | test_failf(__FILE__, __LINE__, "expected %s", #cond); \ | ||
| 35 | return; \ | ||
| 36 | } \ | ||
| 37 | } while (0) | ||
| 38 | |||
| 39 | #define CHECK_INT(got, want) \ | ||
| 40 | do { \ | ||
| 41 | long long got_ = (long long)(got), want_ = (long long)(want); \ | ||
| 42 | ++test_checks; \ | ||
| 43 | if (got_ != want_) { \ | ||
| 44 | test_failf(__FILE__, __LINE__, "%s: got %lld, want %lld", #got, got_, want_); \ | ||
| 45 | return; \ | ||
| 46 | } \ | ||
| 47 | } while (0) | ||
| 48 | |||
| 49 | #define CHECK_PTR(got, want) \ | ||
| 50 | do { \ | ||
| 51 | const void *got_ = (const void *)(got), *want_ = (const void *)(want); \ | ||
| 52 | ++test_checks; \ | ||
| 53 | if (got_ != want_) { \ | ||
| 54 | test_failf(__FILE__, __LINE__, "%s: got %p, want %p", #got, got_, want_); \ | ||
| 55 | return; \ | ||
| 56 | } \ | ||
| 57 | } while (0) | ||
| 58 | |||
| 59 | #define CHECK_MEM(got, want, len) \ | ||
| 60 | do { \ | ||
| 61 | ++test_checks; \ | ||
| 62 | if (memcmp((got), (want), (len))) { \ | ||
| 63 | test_failmem(__FILE__, __LINE__, #got, (got), (want), (len)); \ | ||
| 64 | return; \ | ||
| 65 | } \ | ||
| 66 | } while (0) | ||
| 67 | |||
| 68 | #define CHECK_STR(got, want) \ | ||
| 69 | do { \ | ||
| 70 | const char *got_ = (got), *want_ = (want); \ | ||
| 71 | ++test_checks; \ | ||
| 72 | if (strcmp(got_, want_)) { \ | ||
| 73 | test_failf(__FILE__, __LINE__, "%s: got \"%s\", want \"%s\"", #got, got_, want_); \ | ||
| 74 | return; \ | ||
| 75 | } \ | ||
| 76 | } while (0) | ||
| 77 | |||
| 78 | /* One entry point per module under test */ | ||
| 79 | void test_scan_urlencoded_query(void); | ||
| 80 | void test_iovec(void); | ||
| 81 | void test_vector(void); | ||
| 82 | |||
| 83 | #endif | ||
