]> git.ipfire.org Git - thirdparty/FORT-validator.git/commitdiff
Add unit testing framework
authorAlberto Leiva Popper <ydahhrk@gmail.com>
Wed, 29 Aug 2018 22:23:03 +0000 (17:23 -0500)
committerAlberto Leiva Popper <ydahhrk@gmail.com>
Wed, 29 Aug 2018 22:51:43 +0000 (17:51 -0500)
And fix some bugs as a side effect.

12 files changed:
.gitignore
Makefile.am
configure.ac
src/rtr/pdu.c
src/rtr/primitive_reader.c
src/rtr/primitive_reader.h
test/Makefile.am [new file with mode: 0644]
test/README.md [new file with mode: 0644]
test/rtr/pdu_test.c [new file with mode: 0644]
test/rtr/primitive_reader_test.c [new file with mode: 0644]
test/rtr/stream.c [new file with mode: 0644]
test/rtr/stream.h [new file with mode: 0644]

index bbe7ecf71b683214b0d87bc3ebda7ed7701d1876..612676172bf89fc064105901b9a7e2c997341d0e 100644 (file)
@@ -85,6 +85,11 @@ missing
 *.tar
 *.zip
 
+# Check Framework
+*.test
+*.trs
+test-driver
+
 # Temporal files
 *~
 
index c791e70b92e10ef006c7833415dbdc70137efbcd..f2b73326b656ffd2814c19c2c784bff6bbd885bf 100644 (file)
@@ -11,4 +11,4 @@
 # Man, GNU conventions need a 21 century overhaul badly.
 AUTOMAKE_OPTIONS = foreign
 
-SUBDIRS = src man
+SUBDIRS = src man test
index 01b627e3e04364737ab1eae2ae2d2890b6c0d0ed..6f5e7f9d9135ca3891e4d93b4e41951811854e6a 100644 (file)
@@ -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)
index f8b7a143a4273760239d3a03659254c258f163a5..4cfafd2a76e66fa9d6e09b7290ad4a0e32b2eeb5 100644 (file)
@@ -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;
        }
 
index 4dea588b60a32d3e5d8bbe19631ccc991404382a..e0098050532c1e9a784908155b61cffa6f253e06 100644 (file)
@@ -7,8 +7,8 @@
 #include <netinet/in.h>
 
 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;
index bea9913f33fc30a83b8db544d5a9f67637535fc6..ad7581d4404ea23666b022b38d4e1e3682c72c80 100644 (file)
@@ -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 (file)
index 0000000..5fefa07
--- /dev/null
@@ -0,0 +1,26 @@
+# Reminder: Automake will automatically add this to any targets where
+# <mumble>_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 (file)
index 0000000..fab9e81
--- /dev/null
@@ -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 (file)
index 0000000..305db91
--- /dev/null
@@ -0,0 +1,251 @@
+#include <check.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#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 (file)
index 0000000..17fdde9
--- /dev/null
@@ -0,0 +1,346 @@
+#include <check.h>
+#include <error.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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 (file)
index 0000000..808cc29
--- /dev/null
@@ -0,0 +1,60 @@
+#include "stream.h"
+
+#include <err.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+/*
+ * 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 (file)
index 0000000..6f94337
--- /dev/null
@@ -0,0 +1,13 @@
+#ifndef TEST_RTR_STREAM_H_
+#define TEST_RTR_STREAM_H_
+
+#include <stddef.h>
+
+#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_ */