From: Alberto Leiva Popper Date: Wed, 29 Aug 2018 22:23:03 +0000 (-0500) Subject: Add unit testing framework X-Git-Tag: v0.0.2~52^2~72 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f402fcbee8bdf9979bb0a171d403adf311d89e75;p=thirdparty%2FFORT-validator.git Add unit testing framework And fix some bugs as a side effect. --- diff --git a/.gitignore b/.gitignore index bbe7ecf7..61267617 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,11 @@ missing *.tar *.zip +# Check Framework +*.test +*.trs +test-driver + # Temporal files *~ diff --git a/Makefile.am b/Makefile.am index c791e70b..f2b73326 100644 --- a/Makefile.am +++ b/Makefile.am @@ -11,4 +11,4 @@ # Man, GNU conventions need a 21 century overhaul badly. AUTOMAKE_OPTIONS = foreign -SUBDIRS = src man +SUBDIRS = src man test diff --git a/configure.ac b/configure.ac index 01b627e3..6f5e7f9d 100644 --- a/configure.ac +++ b/configure.ac @@ -25,5 +25,9 @@ AC_FUNC_MALLOC AC_CHECK_FUNCS([memset socket]) AC_SEARCH_LIBS([pthread_create], [pthread]) +# Uhhh... this one starts with "PKG_" so it's probably different. +# No idea. +PKG_CHECK_MODULES([CHECK], [check]) + # Spit out the makefiles. -AC_OUTPUT(Makefile src/Makefile man/Makefile) +AC_OUTPUT(Makefile src/Makefile man/Makefile test/Makefile) diff --git a/src/rtr/pdu.c b/src/rtr/pdu.c index f8b7a143..4cfafd2a 100644 --- a/src/rtr/pdu.c +++ b/src/rtr/pdu.c @@ -35,13 +35,13 @@ pdu_load(int fd, void **pdu, struct pdu_metadata const **metadata) if (!meta) return -ENOENT; /* TODO try to skip it anyway? */ - pdu = malloc(meta->length); - if (!pdu) + *pdu = malloc(meta->length); + if (*pdu == NULL) return -ENOMEM; - err = meta->from_stream(&header, fd, pdu); + err = meta->from_stream(&header, fd, *pdu); if (err) { - free(pdu); + free(*pdu); return err; } diff --git a/src/rtr/primitive_reader.c b/src/rtr/primitive_reader.c index 4dea588b..e0098050 100644 --- a/src/rtr/primitive_reader.c +++ b/src/rtr/primitive_reader.c @@ -7,8 +7,8 @@ #include static int read_exact(int, unsigned char *, size_t); -static int read_and_waste(int, unsigned char *, size_t, u_int64_t); -static int get_octets(rtr_char); +static int read_and_waste(int, unsigned char *, size_t, u_int32_t); +static int get_octets(unsigned char); static void place_null_character(rtr_char *, size_t); static int @@ -26,7 +26,7 @@ read_exact(int fd, unsigned char *buffer, size_t buffer_len) return err; } if (read_result == 0) { - warn("Stream ended mid-PDU"); + warnx("Stream ended mid-PDU."); return -EPIPE; } } @@ -95,7 +95,7 @@ read_in6_addr(int fd, struct in6_addr *result) * It is required that @str_len <= @total_len. */ static int -read_and_waste(int fd, unsigned char *str, size_t str_len, u_int64_t total_len) +read_and_waste(int fd, unsigned char *str, size_t str_len, u_int32_t total_len) { #define TLEN 1024 /* "Trash length" */ unsigned char trash[TLEN]; @@ -123,9 +123,9 @@ read_and_waste(int fd, unsigned char *str, size_t str_len, u_int64_t total_len) * @first_octet. */ static int -get_octets(rtr_char first_octet) +get_octets(unsigned char first_octet) { - if ((first_octet & 0xC0) == 0) + if ((first_octet & 0x80) == 0) return 1; if ((first_octet >> 5) == 6) /* 0b110 */ return 2; @@ -136,12 +136,13 @@ get_octets(rtr_char first_octet) return EINVALID_UTF8; } +/* This is just a cast. The barebones version is too cluttered. */ +#define UCHAR(c) ((unsigned char *)c) + /* * This also sanitizes the string, BTW. - * (Because it places the null chara in the first invalid character. + * (Because it overrides the first invalid character with the null chara. * The rest is silently ignored.) - * - * TODO test the hell out of this. */ static void place_null_character(rtr_char *str, size_t len) @@ -162,22 +163,39 @@ place_null_character(rtr_char *str, size_t len) null_chara_pos = str; cursor = str; - while (cursor < str + len) { - octets = get_octets(*cursor); + while (cursor < str + len - 1) { + octets = get_octets(*UCHAR(cursor)); if (octets == EINVALID_UTF8) break; + cursor++; + for (octet = 1; octet < octets; octet++) { - if (cursor >= str + len - 1 || cursor[1] >> 6 != 0x10) - break; + /* Memory ends in the middle of this code point? */ + if (cursor >= str + len - 1) + goto end; + /* All continuation octets must begin with 0b10. */ + if ((*(UCHAR(cursor)) >> 6) != 2 /* 0b10 */) + goto end; cursor++; } null_chara_pos = cursor; } +end: *null_chara_pos = '\0'; } +/* + * Reads an RTR string from the file descriptor @fd. Returns the string as a + * normal UTF-8 C string (NULL-terminated). + * + * Will consume the entire string from the stream, but @result can be truncated. + * This is because RTR strings are technically allowed to be 4 GBs long. + * + * The result is allocated in the heap. It will length 4096 characters at most. + * (Including the NULL chara.) + */ int read_string(int fd, rtr_char **result) { @@ -200,6 +218,7 @@ read_string(int fd, rtr_char **result) err = read_int32(fd, &full_length32); if (err) return err; + full_length64 = ((u_int64_t) full_length32) + 1; alloc_length = (full_length64 > 4096) ? 4096 : full_length64; @@ -207,7 +226,7 @@ read_string(int fd, rtr_char **result) if (!str) return -ENOMEM; - err = read_and_waste(fd, str, alloc_length - 1, full_length64); + err = read_and_waste(fd, UCHAR(str), alloc_length - 1, full_length32); if (err) { free(str); return err; diff --git a/src/rtr/primitive_reader.h b/src/rtr/primitive_reader.h index bea9913f..ad7581d4 100644 --- a/src/rtr/primitive_reader.h +++ b/src/rtr/primitive_reader.h @@ -5,7 +5,7 @@ #include "../common.h" -typedef unsigned char rtr_char; +typedef char rtr_char; __BEGIN_DECLS int read_int8(int, u_int8_t *); diff --git a/test/Makefile.am b/test/Makefile.am new file mode 100644 index 00000000..5fefa073 --- /dev/null +++ b/test/Makefile.am @@ -0,0 +1,26 @@ +# Reminder: Automake will automatically add this to any targets where +# _CFLAGS is not defined. +# Otherwise it must be included manually: +# mumble_mumble_CFLAGS = $(AM_CFLAGS) flag1 flag2 flag3 ... +AM_CFLAGS = -pedantic -Wall -std=gnu11 -I../src @CHECK_CFLAGS@ + +# Reminder: As opposed to AM_CFLAGS, "AM_LDADD" is not idiomatic automake, and +# autotools will even reprehend us if we declare it. Therefore, I came up with +# "my" own "ldadd". Unlike AM_CFLAGS, it needs to be manually added to every +# target. +MY_LDADD = $(CHECK_LIBS) + +check_PROGRAMS = rtr/primitive_reader.test rtr/pdu.test +TESTS = $(check_PROGRAMS) + +rtr_primitive_reader_test_SOURCES = \ + rtr/primitive_reader_test.c \ + rtr/stream.c +rtr_primitive_reader_test_LDADD = $(MY_LDADD) + +rtr_pdu_test_SOURCES = \ + rtr/pdu_test.c \ + rtr/stream.c \ + $(top_builddir)/src/rtr/primitive_reader.c \ + $(top_builddir)/src/rtr/pdu_handler.c +rtr_pdu_test_LDADD = $(MY_LDADD) diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..fab9e811 --- /dev/null +++ b/test/README.md @@ -0,0 +1,47 @@ +# Introduction + +I decided to use the [Check Framework](https://libcheck.github.io/check/) for +unit testing. + +This is how I understand it: + +- Each `main()` has one or more "Suite"s. +- Each "Suite" has multiple "Test Case"s. + "Test Case" is an odd name. They should be called "Test Type"s in my + opinion. In the examples, one "test case" is "Core" (happy path tests), + another one is "Limits" (the corner cases), and I imagine that one can add + "Errors" and "Performance" and whatever. +- Each "Test Case" has multiple "test"s. Each "test" is a `_test` function. + (Defined by `START_TEST` and `END_TEST`.) +- Each "test" is allowed to contain more than one function call, but it should + normally only throw one kind of challenge to the target function. + +The framework seems to want each suite to test one module, but in my opinion, +that's autotools's job. (Each `check_PROGRAM` in `Makefile.am` should test one +module, otherwise why the F would it allow multiple entries.) So I guess the +rule of thumb is as follows: + + Each "Suite" should test one (and only one) function within a module. + +So then, each entry in `Makefile.am` is a module (within `/test/`) that tests +another module (within `/src/`). (If the name of the tested module is `A.c`, then +the name of the testing module is `A_test.c`.) Each testing module has one suite +per function within the tested module. Each suite has one test suite per test +type. And so on. + +Testing private functions is totally allowed. Simply `#include` the `.c` (not the +`.h`) to do this. + +# Running + +The following commands are preparatory and only need to be run the first time, +_in the current directory's parent_: + + ./autogen.sh + ./configure + +Then, whenever you want to run the tests, enter the current directory and run + + make check + +There's at least one very long test that lasts about a full minute. diff --git a/test/rtr/pdu_test.c b/test/rtr/pdu_test.c new file mode 100644 index 00000000..305db91d --- /dev/null +++ b/test/rtr/pdu_test.c @@ -0,0 +1,251 @@ +#include +#include +#include + +#include "stream.h" +#include "rtr/pdu.c" + +/* + * Just a wrapper for `buffer2fd()`. Boilerplate one-liner. + */ +#define BUFFER2FD(buffer, cb, obj) { \ + struct pdu_header header; \ + int fd, err; \ + \ + fd = buffer2fd(buffer, sizeof(buffer)); \ + ck_assert_int_ge(fd, 0); \ + init_pdu_header(&header); \ + err = cb(&header, fd, obj); \ + close(fd); \ + ck_assert_int_eq(err, 0); \ + assert_pdu_header(&(obj)->header); \ +} + +static void +init_pdu_header(struct pdu_header *header) +{ + header->protocol_version = 0; + header->pdu_type = 22; + header->reserved = 12345; + header->length = 0xFFAA9955; +} + +static void +assert_pdu_header(struct pdu_header *header) +{ + ck_assert_uint_eq(header->protocol_version, 0); + ck_assert_uint_eq(header->pdu_type, 22); + ck_assert_uint_eq(header->reserved, 12345); + ck_assert_uint_eq(header->length, 0xFFAA9955); +} + +START_TEST(test_pdu_header_from_stream) +{ + unsigned char input[] = { 0, 1, 2, 3, 4, 5, 6, 7 }; + struct pdu_header header; + int fd; + int err; + + fd = buffer2fd(input, sizeof(input)); + ck_assert_int_ge(fd, 0); + err = pdu_header_from_stream(fd, &header); + close(fd); + ck_assert_int_eq(err, 0); + + ck_assert_uint_eq(header.protocol_version, 0); + ck_assert_uint_eq(header.pdu_type, 1); + ck_assert_uint_eq(header.reserved, 0x0203); + ck_assert_uint_eq(header.length, 0x04050607); +} +END_TEST + +START_TEST(test_serial_notify_from_stream) +{ + unsigned char input[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + struct serial_notify_pdu pdu; + + BUFFER2FD(input, serial_notify_from_stream, &pdu); + ck_assert_uint_eq(pdu.serial_number, 0x010203); +} +END_TEST + +START_TEST(test_serial_query_from_stream) +{ + unsigned char input[] = { 13, 14, 15, 16, 17 }; + struct serial_query_pdu pdu; + + BUFFER2FD(input, serial_query_from_stream, &pdu); + ck_assert_uint_eq(pdu.serial_number, 0x0d0e0f10); +} +END_TEST + +START_TEST(test_reset_query_from_stream) +{ + unsigned char input[] = { 18, 19 }; + struct reset_query_pdu pdu; + + BUFFER2FD(input, reset_query_from_stream, &pdu); +} +END_TEST + +START_TEST(test_cache_response_from_stream) +{ + unsigned char input[] = { 18, 19 }; + struct cache_response_pdu pdu; + + BUFFER2FD(input, cache_response_from_stream, &pdu); +} +END_TEST + +START_TEST(test_ipv4_prefix_from_stream) +{ + unsigned char input[] = { 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, + 29, 30, 31, 32 }; + struct ipv4_prefix_pdu pdu; + + BUFFER2FD(input, ipv4_prefix_from_stream, &pdu); + ck_assert_uint_eq(pdu.flags, 18); + ck_assert_uint_eq(pdu.prefix_length, 19); + ck_assert_uint_eq(pdu.max_length, 20); + ck_assert_uint_eq(pdu.zero, 21); + ck_assert_uint_eq(pdu.ipv4_prefix.s_addr, 0x16171819); + ck_assert_uint_eq(pdu.asn, 0x1a1b1c1d); +} +END_TEST + +START_TEST(test_ipv6_prefix_from_stream) +{ + unsigned char input[] = { 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60 }; + struct ipv6_prefix_pdu pdu; + + BUFFER2FD(input, ipv6_prefix_from_stream, &pdu); + ck_assert_uint_eq(pdu.flags, 33); + ck_assert_uint_eq(pdu.prefix_length, 34); + ck_assert_uint_eq(pdu.max_length, 35); + ck_assert_uint_eq(pdu.zero, 36); + ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[0], 0x25262728); + ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[1], 0x292a2b2c); + ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[2], 0x2d2e2f30); + ck_assert_uint_eq(pdu.ipv6_prefix.s6_addr32[3], 0x31323334); + ck_assert_uint_eq(pdu.asn, 0x35363738); +} +END_TEST + +START_TEST(test_end_of_data_from_stream) +{ + unsigned char input[] = { 61, 62, 63, 64 }; + struct end_of_data_pdu pdu; + + BUFFER2FD(input, end_of_data_from_stream, &pdu); + ck_assert_uint_eq(pdu.serial_number, 0x3d3e3f40); +} +END_TEST + +START_TEST(test_cache_reset_from_stream) +{ + unsigned char input[] = { 65, 66, 67 }; + struct cache_reset_pdu pdu; + + BUFFER2FD(input, cache_reset_from_stream, &pdu); +} +END_TEST + +START_TEST(test_error_report_from_stream) +{ + unsigned char input[] = { + /* Sub-pdu length */ + 0, 0, 0, 12, + /* Sub-pdu */ + 1, 0, 2, 3, 0, 0, 0, 12, 1, 2, 3, 4, + /* Error msg length */ + 0, 0, 0, 5, + /* Error msg */ + 'h', 'e', 'l', 'l', 'o', + /* Garbage */ + 1, 2, 3, 4, + }; + struct error_report_pdu *pdu; + struct serial_notify_pdu *sub_pdu; + + pdu = malloc(sizeof(struct error_report_pdu)); + if (!pdu) + ck_abort_msg("PDU allocation failure"); + + BUFFER2FD(input, error_report_from_stream, pdu); + + sub_pdu = pdu->erroneous_pdu; + ck_assert_uint_eq(sub_pdu->header.protocol_version, 1); + ck_assert_uint_eq(sub_pdu->header.pdu_type, 0); + ck_assert_uint_eq(sub_pdu->header.reserved, 0x0203); + ck_assert_uint_eq(sub_pdu->header.length, 12); + ck_assert_uint_eq(sub_pdu->serial_number, 0x01020304); + ck_assert_str_eq(pdu->error_message, "hello"); + + /* + * Yes, this test memory leaks on failure. + * Not sure how to fix it without making a huge mess. + */ + error_report_destroy(pdu); +} +END_TEST + +START_TEST(test_interrupted) +{ + unsigned char input[] = { 0, 1 }; + struct serial_notify_pdu pdu; + struct pdu_header header; + int fd, err; + + fd = buffer2fd(input, sizeof(input)); + ck_assert_int_ge(fd, 0); + init_pdu_header(&header); + err = serial_notify_from_stream(&header, fd, &pdu); + close(fd); + ck_assert_int_eq(err, -EPIPE); +} +END_TEST + +Suite *pdu_suite(void) +{ + Suite *suite; + TCase *core, *errors; + + core = tcase_create("Core"); + tcase_add_test(core, test_pdu_header_from_stream); + tcase_add_test(core, test_serial_notify_from_stream); + tcase_add_test(core, test_serial_notify_from_stream); + tcase_add_test(core, test_serial_query_from_stream); + tcase_add_test(core, test_reset_query_from_stream); + tcase_add_test(core, test_cache_response_from_stream); + tcase_add_test(core, test_ipv4_prefix_from_stream); + tcase_add_test(core, test_ipv6_prefix_from_stream); + tcase_add_test(core, test_end_of_data_from_stream); + tcase_add_test(core, test_cache_reset_from_stream); + tcase_add_test(core, test_error_report_from_stream); + + errors = tcase_create("Errors"); + tcase_add_test(errors, test_interrupted); + + suite = suite_create("PDU"); + suite_add_tcase(suite, core); + suite_add_tcase(suite, errors); + return suite; +} + +int main(void) +{ + Suite *suite; + SRunner *runner; + int tests_failed; + + suite = pdu_suite(); + + runner = srunner_create(suite); + srunner_run_all(runner, CK_NORMAL); + tests_failed = srunner_ntests_failed(runner); + srunner_free(runner); + + return (tests_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/test/rtr/primitive_reader_test.c b/test/rtr/primitive_reader_test.c new file mode 100644 index 00000000..17fdde9f --- /dev/null +++ b/test/rtr/primitive_reader_test.c @@ -0,0 +1,346 @@ +#include +#include +#include +#include +#include +#include + +#include "stream.h" +#include "rtr/primitive_reader.c" + +/* + * Wrapper for `read_string()`, for easy testing. + */ +static int +__read_string(unsigned char *input, size_t size, rtr_char **result) +{ + int fd; + int err; + + fd = buffer2fd(input, size); + if (fd < 0) + return fd; + + err = read_string(fd, result); + close(fd); + return err; +} + +static void +test_read_string_success(unsigned char *input, size_t length, + rtr_char *expected) +{ + rtr_char *actual; + int err; + + err = __read_string(input, length, &actual); + ck_assert_int_eq(0, err); + if (!err) { + ck_assert_str_eq(expected, actual); + free(actual); + } +} + +static void +test_read_string_fail(unsigned char *input, size_t length, int expected) +{ + rtr_char *result; + int err; + + err = __read_string(input, length, &result); + ck_assert_int_eq(expected, err); + + if (!err) + free(result); +} + +START_TEST(read_string_ascii) +{ + unsigned char input[] = { 0, 0, 0, 4, 'a', 'b', 'c', 'd' }; + test_read_string_success(input, sizeof(input), "abcd"); +} +END_TEST + +START_TEST(read_string_unicode) +{ + unsigned char input0[] = { 0, 0, 0, 7, 's', 'a', 'n', 'd', 0xc3, 0xad, + 'a' }; + test_read_string_success(input0, sizeof(input0), "sandía"); + + unsigned char input1[] = { 0, 0, 0, 12, + 0xe1, 0x88, 0x90, 0xe1, 0x89, 0xa5, 0xe1, 0x88, 0x90, 0xe1, 0x89, + 0xa5 }; + test_read_string_success(input1, sizeof(input1), "ሐብሐብ"); + + unsigned char input2[] = { 0, 0, 0, 12, + 0xd8, 0xa7, 0xd9, 0x84, 0xd8, 0xa8, 0xd8, 0xb7, 0xd9, 0x8a, 0xd8, + 0xae }; + test_read_string_success(input2, sizeof(input2), "البطيخ"); + + unsigned char input3[] = { 0, 0, 0, 25, + 0xd5, 0xb1, 0xd5, 0xb4, 0xd5, 0xa5, 0xd6, 0x80, 0xd5, 0xb8, 0xd6, + 0x82, 0xd5, 0xaf, 0x20, 0xd0, 0xba, 0xd0, 0xb0, 0xd0, 0xb2, 0xd1, + 0x83, 0xd0, 0xbd }; + test_read_string_success(input3, sizeof(input3), "ձմերուկ кавун"); + + unsigned char input4[] = { 0, 0, 0, 36, + 0xe0, 0xa6, 0xa4, 0xe0, 0xa6, 0xb0, 0xe0, 0xa6, 0xae, 0xe0, 0xa7, + 0x81, 0xe0, 0xa6, 0x9c, 0x20, 0xd0, 0xb4, 0xd0, 0xb8, 0xd0, 0xbd, + 0xd1, 0x8f, 0x20, 0xe8, 0xa5, 0xbf, 0xe7, 0x93, 0x9c, 0x20, 0xf0, + 0x9f, 0x8d, 0x89 }; + test_read_string_success(input4, sizeof(input4), "তরমুজ диня 西瓜 🍉"); +} +END_TEST + +START_TEST(read_string_empty) +{ + unsigned char input[] = { 0, 0, 0, 0 }; + test_read_string_success(input, sizeof(input), ""); +} +END_TEST + +struct thread_param { + int fd; + u_int32_t msg_size; + int err; +}; + +#define WRITER_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" + +/* + * Writes a @param_void->msg_size-sized RTR string in @param_void->fd. + */ +static void * +writer_thread_cb(void *param_void) +{ + struct thread_param *param; + rtr_char *pattern; + size_t pattern_len; + unsigned char header[4]; + + param = param_void; + pattern = WRITER_PATTERN; + pattern_len = strlen(pattern); + + /* Write the string length */ + header[0] = (param->msg_size >> 24) & 0xFF; + header[1] = (param->msg_size >> 16) & 0xFF; + header[2] = (param->msg_size >> 8) & 0xFF; + header[3] = (param->msg_size >> 0) & 0xFF; + param->err = write_exact(param->fd, header, sizeof(header)); + if (param->err) + return param; + + /* Write the string */ + for (; param->msg_size > pattern_len; param->msg_size -= pattern_len) { + param->err = write_exact(param->fd, UCHAR(pattern), pattern_len); + if (param->err) + return param; + } + param->err = write_exact(param->fd, UCHAR(pattern), param->msg_size); + return param; +} + +/* + * Checks that the string @str is made up of @expected_len characters composed + * of the @WRITER_PATTERN pattern repeatedly. + */ +static void +validate_massive_string(u_int32_t expected_len, rtr_char *str) +{ + size_t actual_len; + rtr_char *pattern; + size_t pattern_len; + rtr_char *cursor; + rtr_char *end; + + actual_len = strlen(str); + if (expected_len != actual_len) { + free(str); + ck_abort_msg("Expected length %zu != Actual length %zu", + expected_len, actual_len); + } + + pattern = WRITER_PATTERN; + pattern_len = strlen(pattern); + end = str + expected_len; + for (cursor = str; cursor + pattern_len < end; cursor += pattern_len) { + if (strncmp(pattern, cursor, pattern_len) != 0) { + free(str); + ck_abort_msg("String does not match expected pattern"); + } + } + + if (strncmp(pattern, cursor, strlen(cursor)) != 0) { + free(str); + ck_abort_msg("String end does not match expected pattern"); + } + + free(str); + /* Success */ +} + +/* + * Sends @full_string_length characters to the fd, validates the parsed string + * contains the first @return_length characters. + */ +static void +test_massive_string(u_int32_t return_length, u_int32_t full_string_length) +{ + int fd[2]; + pthread_t writer_thread; + struct thread_param *arg; + rtr_char *result_string; + int err, err2, err3; + + if (pipe(fd) == -1) + ck_abort_msg("pipe(fd) threw errcode %d", errno); + /* Need to close @fd[0] and @fd[1] from now on */ + + arg = malloc(sizeof(struct thread_param)); + if (!arg) { + close(fd[0]); + close(fd[1]); + ck_abort_msg("Thread parameter allocation failure"); + } + /* Need to free @arg from now on */ + + arg->fd = fd[1]; + arg->msg_size = full_string_length; + arg->err = 0; + + err = pthread_create(&writer_thread, NULL, writer_thread_cb, arg); + if (err) { + close(fd[0]); + close(fd[1]); + free(arg); + ck_abort_msg("pthread_create() threw errcode %d", err); + } + /* The writer thread owns @arg now; do not touch it until retrievel */ + + err = read_string(fd[0], &result_string); + /* Need to free @result_string from now on */ + err2 = pthread_join(writer_thread, (void **)&arg); + /* @arg is now retrieved. */ + err3 = arg->err; + + close(fd[0]); + close(fd[1]); + free(arg); + /* Don't need to close @fd[0], @fd[1] nor free @arg from now on */ + + if (err || err2 || err3) { + free(result_string); + ck_abort_msg("read_string:%d pthread_join:%d write_exact:%d", + err, err2, err3); + } + + /* This function now owns @result_string */ + validate_massive_string(return_length, result_string); +} + +START_TEST(read_string_massive) +{ + test_massive_string(2000, 2000); + test_massive_string(4000, 4000); + test_massive_string(4094, 4094); + test_massive_string(4095, 4095); + test_massive_string(4095, 4096); + test_massive_string(4095, 4097); + test_massive_string(4095, 8000); + test_massive_string(4095, 16000); + test_massive_string(4095, 0xFFFFFFFF); +} +END_TEST + +START_TEST(read_string_null) +{ + test_read_string_fail(NULL, 0, -EPIPE); +} +END_TEST + +START_TEST(read_string_truncated) +{ + unsigned char input0[] = { 0, 0, 0, 7, 'a', 'b' }; + test_read_string_fail(input0, sizeof(input0), -EPIPE); + + unsigned char input1[] = { 0, 0 }; + test_read_string_fail(input1, sizeof(input1), -EPIPE); +} +END_TEST + +START_TEST(read_string_unicode_mix) +{ + /* One octet failure */ + unsigned char input0[] = { 0, 0, 0, 3, 'a', 0x80, 'z' }; + test_read_string_success(input0, sizeof(input0), "a"); + + /* Two octets success */ + unsigned char input1[] = { 0, 0, 0, 4, 'a', 0xdf, 0x9a, 'z' }; + test_read_string_success(input1, sizeof(input1), "aߚz"); + /* Two octets failure */ + unsigned char input2[] = { 0, 0, 0, 4, 'a', 0xdf, 0xda, 'z' }; + test_read_string_success(input2, sizeof(input2), "a"); + + /* Three characters success */ + unsigned char input3[] = { 0, 0, 0, 5, 'a', 0xe2, 0x82, 0xac, 'z' }; + test_read_string_success(input3, sizeof(input3), "a€z"); + /* Three characters failure */ + unsigned char input4[] = { 0, 0, 0, 5, 'a', 0xe2, 0x82, 0x2c, 'z' }; + test_read_string_success(input4, sizeof(input4), "a"); + + /* Four characters success */ + unsigned char i5[] = { 0, 0, 0, 6, 'a', 0xf0, 0x90, 0x86, 0x97, 'z' }; + test_read_string_success(i5, sizeof(i5), "a𐆗z"); + /* Four characters failure */ + unsigned char i6[] = { 0, 0, 0, 6, 'a', 0xf0, 0x90, 0x90, 0x17, 'z' }; + test_read_string_success(i6, sizeof(i6), "a"); +} +END_TEST + +Suite *read_string_suite(void) +{ + Suite *suite; + TCase *core, *limits, *errors; + + core = tcase_create("Core"); + tcase_add_test(core, read_string_ascii); + tcase_add_test(core, read_string_unicode); + + limits = tcase_create("Limits"); + tcase_add_test(limits, read_string_empty); + tcase_add_test(limits, read_string_massive); + /* The 0xFFFFFFFF test lasts 1:02 minutes on my computer. */ + tcase_set_timeout(limits, 120); + + errors = tcase_create("Errors"); + tcase_add_test(errors, read_string_null); + tcase_add_test(errors, read_string_truncated); + tcase_add_test(errors, read_string_unicode_mix); + + suite = suite_create("read_string()"); + suite_add_tcase(suite, core); + suite_add_tcase(suite, limits); + suite_add_tcase(suite, errors); + return suite; +} + +int main(void) +{ + Suite *suite; + SRunner *runner; + int tests_failed; + + /* + * This is it. We won't test the other functions because they are + * already reasonably manhandled in the PDU units. + */ + suite = read_string_suite(); + + runner = srunner_create(suite); + srunner_run_all(runner, CK_NORMAL); + tests_failed = srunner_ntests_failed(runner); + srunner_free(runner); + + return (tests_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/test/rtr/stream.c b/test/rtr/stream.c new file mode 100644 index 00000000..808cc296 --- /dev/null +++ b/test/rtr/stream.c @@ -0,0 +1,60 @@ +#include "stream.h" + +#include +#include +#include +#include + +/* + * Writes exactly @length bytes from @buffer to the file descriptor @fd. + * All or nothing. + * + * The result is zero on success, nonzero on failure. + */ +int +write_exact(int fd, unsigned char *buffer, size_t length) +{ + size_t written; + int written_now; + + for (written = 0; written < length; written += written_now) { + written_now = write(fd, buffer + written, length - written); + if (written_now == -1) + return errno; + } + + return 0; +} + +/* + * "Converts" the buffer @buffer (sized @size) to a file descriptor (FD). + * You will get @buffer if you `read()` the FD. + * + * If the result is not negative, then you're receiving the resulting FD. + * If the result is negative, it's an error code. + * + * Note that you need to close the FD when you're done reading it. + */ +int +buffer2fd(unsigned char *buffer, size_t size) +{ + int fd[2]; + int err; + + if (pipe(fd) == -1) { + err = errno; + warn("Pipe creation failed"); + return -abs(err); + } + + err = write_exact(fd[1], buffer, size); + close(fd[1]); + if (err) { + errno = err; + warn("Pipe write failed"); + close(fd[0]); + return -abs(err); + } + + return fd[0]; +} diff --git a/test/rtr/stream.h b/test/rtr/stream.h new file mode 100644 index 00000000..6f94337f --- /dev/null +++ b/test/rtr/stream.h @@ -0,0 +1,13 @@ +#ifndef TEST_RTR_STREAM_H_ +#define TEST_RTR_STREAM_H_ + +#include + +#include "common.h" + +__BEGIN_DECLS +int write_exact(int, unsigned char *, size_t); +int buffer2fd(unsigned char *, size_t); +__END_DECLS + +#endif /* TEST_RTR_STREAM_H_ */