From 8ade561fc2cea61f241d34d6e69b532081ac6752 Mon Sep 17 00:00:00 2001 From: Sebastian Harl Date: Mon, 16 Sep 2013 09:07:20 +0200 Subject: [PATCH] unit tests: Added basic tests for unixsock utils; mocking libc I/O functions. GNU libc's fopencookie is used to mock fdopen() and socket handling. This allows to mock a UNIX socket. --- .gitignore | 1 + configure.ac | 14 ++ t/Makefile.am | 12 +- t/libsysdb_net_test.c | 72 ++++++++ t/libsysdb_test.c | 21 ++- t/libsysdb_test.h | 15 ++ t/utils/unixsock_test.c | 375 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 500 insertions(+), 10 deletions(-) create mode 100644 t/libsysdb_net_test.c create mode 100644 t/utils/unixsock_test.c diff --git a/.gitignore b/.gitignore index 19a424f..943a4ca 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,5 @@ sysdb.h # unit testing output t/libsysdb_test +t/libsysdb_net_test diff --git a/configure.ac b/configure.ac index 0e43055..a4f2185 100644 --- a/configure.ac +++ b/configure.ac @@ -207,6 +207,18 @@ if test "x$with_libdbi" = "xyes"; then fi AM_CONDITIONAL([BUILD_WITH_LIBDBI], test "x$with_libdbi" = "xyes") +dnl Required for mocking FILE related functions. +orig_CFLAGS="$CFLAGS" +CFLAGS="$CFLAGS -D_GNU_SOURCE" +AC_CHECK_FUNCS([fopencookie], + [have_fopencookie="yes"], + [have_fopencookie="no (fopencookie not available)"]) +CFLAGS="$orig_CFLAGS" +AM_CONDITIONAL([BUILD_WITH_FOPENCOOKIE], test "x$have_fopencookie" = "xyes") +if test "x$have_fopencookie" = "xyes"; then + AC_DEFINE([HAVE_FOPENCOOKIE], 1) +fi + dnl Feature checks. build_documentation="yes" @@ -280,6 +292,7 @@ AC_MSG_RESULT() AC_MSG_RESULT([ Features:]) AC_MSG_RESULT([ documentation: . . . . . . $build_documentation]) AC_MSG_RESULT([ unit testing: . . . . . . . $build_testing]) +AC_MSG_RESULT([ stdio mocking: . . . . . $have_fopencookie]) AC_MSG_RESULT() AC_MSG_RESULT([ Libraries:]) AC_MSG_RESULT([ libdbi: . . . . . . . . . . $with_libdbi]) @@ -296,3 +309,4 @@ AC_MSG_RESULT([This package is maintained by $PACKAGE_MAINTAINER.]) AC_MSG_RESULT([Please report bugs to $PACKAGE_BUGREPORT.]) AC_MSG_RESULT() +dnl vim: set tw=78 sw=4 ts=4 noexpandtab : diff --git a/t/Makefile.am b/t/Makefile.am index e8e0dc6..e4cc803 100644 --- a/t/Makefile.am +++ b/t/Makefile.am @@ -1,9 +1,9 @@ AM_CFLAGS = @STRICT_CFLAGS@ AM_CPPFLAGS = -I$(top_builddir)/src/include -TESTS = libsysdb_test +TESTS = libsysdb_test libsysdb_net_test +check_PROGRAMS = libsysdb_test libsysdb_net_test -check_PROGRAMS = libsysdb_test libsysdb_test_SOURCES = \ libsysdb_test.c libsysdb_test.h \ utils/dbi_test.c \ @@ -12,5 +12,13 @@ libsysdb_test_SOURCES = \ libsysdb_test_CFLAGS = $(AM_CFLAGS) @CHECK_CFLAGS@ libsysdb_test_LDADD = $(top_builddir)/src/libsysdb.la @CHECK_LIBS@ +libsysdb_net_test_SOURCES = \ + libsysdb_net_test.c libsysdb_test.h +if BUILD_WITH_FOPENCOOKIE +libsysdb_net_test_SOURCES += utils/unixsock_test.c +endif +libsysdb_net_test_CFLAGS = $(AM_CFLAGS) @CHECK_CFLAGS@ +libsysdb_net_test_LDADD = $(top_builddir)/src/libsysdb.la @CHECK_LIBS@ + test: check diff --git a/t/libsysdb_net_test.c b/t/libsysdb_net_test.c new file mode 100644 index 0000000..aede0d6 --- /dev/null +++ b/t/libsysdb_net_test.c @@ -0,0 +1,72 @@ +/* + * SysDB - t/libsysdb_net_test.c + * Copyright (C) 2013 Sebastian 'tokkee' Harl + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* + * testing component involving networking operations + */ + +#include "libsysdb_test.h" + +#include +#include + +int +main(void) +{ + int failed = 0; + size_t i; + + suite_creator_t creators[] = { +#ifdef HAVE_FOPENCOOKIE + { util_unixsock_suite, NULL }, +#else + { NULL, "Skipping util::unixsock; missing fopencookie" }, +#endif /* HAVE_FOPENCOOKIE */ + }; + + for (i = 0; i < SDB_STATIC_ARRAY_LEN(creators); ++i) { + SRunner *sr; + Suite *s; + + if (creators[i].msg) + printf("%s\n", creators[i].msg); + + if (!creators[i].creator) + continue; + + s = creators[i].creator(); + sr = srunner_create(s); + srunner_run_all(sr, CK_NORMAL); + failed += srunner_ntests_failed(sr); + srunner_free(sr); + } + + return failed; +} /* main */ + +/* vim: set tw=78 sw=4 ts=4 noexpandtab : */ + diff --git a/t/libsysdb_test.c b/t/libsysdb_test.c index 8d35dfd..3241aba 100644 --- a/t/libsysdb_test.c +++ b/t/libsysdb_test.c @@ -25,11 +25,10 @@ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include - #include "libsysdb_test.h" -typedef Suite *(*suite_creator)(void); +#include +#include int main(void) @@ -37,17 +36,23 @@ main(void) int failed = 0; size_t i; - suite_creator creators[] = { - util_llist_suite, - util_dbi_suite, - util_strbuf_suite, + suite_creator_t creators[] = { + { util_llist_suite, NULL }, + { util_dbi_suite, NULL }, + { util_strbuf_suite, NULL }, }; for (i = 0; i < SDB_STATIC_ARRAY_LEN(creators); ++i) { SRunner *sr; Suite *s; - s = creators[i](); + if (creators[i].msg) + printf("%s\n", creators[i].msg); + + if (!creators[i].creator) + continue; + + s = creators[i].creator(); sr = srunner_create(s); srunner_run_all(sr, CK_NORMAL); failed += srunner_ntests_failed(sr); diff --git a/t/libsysdb_test.h b/t/libsysdb_test.h index ebe1959..9e4be80 100644 --- a/t/libsysdb_test.h +++ b/t/libsysdb_test.h @@ -28,6 +28,8 @@ #ifndef T_LIBSYSDB_H #define T_LIBSYSDB_H 1 +#include "config.h" + #include "sysdb.h" #include "core/object.h" @@ -44,6 +46,15 @@ /* type = */ { sizeof(sdb_object_t), NULL, NULL }, \ /* ref_cnt = */ 1, /* name = */ (name) } +/* + * test-related data-types + */ + +typedef struct { + Suite *(*creator)(void); + const char *msg; +} suite_creator_t; + /* * test suites */ @@ -60,6 +71,10 @@ util_llist_suite(void); Suite * util_strbuf_suite(void); +/* t/utils/unixsock_test */ +Suite * +util_unixsock_suite(void); + #endif /* T_LIBSYSDB_H */ /* vim: set tw=78 sw=4 ts=4 noexpandtab : */ diff --git a/t/utils/unixsock_test.c b/t/utils/unixsock_test.c new file mode 100644 index 0000000..0122d5f --- /dev/null +++ b/t/utils/unixsock_test.c @@ -0,0 +1,375 @@ +/* + * SysDB - t/utils/unixsock_test.c + * Copyright (C) 2013 Sebastian 'tokkee' Harl + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* required for fopencookie support */ +#define _GNU_SOURCE + +#include "utils/unixsock.h" +#include "libsysdb_test.h" + +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include + +#include + +/* + * I/O "hook" functions + */ + +typedef struct { + int fd; + size_t pos; +} io_cookie_t; + +static struct { + const char *data; + size_t len; +} golden_data[] = { + { "a", 1 }, + { "abc", 3 }, + { "12345", 5 }, + { "", 0 }, +}; + +static char *last_write = NULL; + +static unsigned long long mock_read_called = 0; +static ssize_t +mock_read(void *cookie, char *buf, size_t size) +{ + io_cookie_t *c = cookie; + ssize_t ret; + + ++mock_read_called; + + if (c->pos >= SDB_STATIC_ARRAY_LEN(golden_data)) + return 0; + + ret = snprintf(buf, size, "%s\n", golden_data[c->pos].data); + ++c->pos; + return ret; +} /* mock_read */ + +static unsigned long long mock_write_called = 0; +static ssize_t +mock_write(void *cookie, const char *buf, size_t size) +{ + io_cookie_t *c = cookie; + + ++mock_write_called; + + if (c->pos >= SDB_STATIC_ARRAY_LEN(golden_data)) + return 0; + + if (last_write) + free(last_write); + last_write = strdup(buf); + ++c->pos; + return (ssize_t)size; +} /* mock_write */ + +/* unsupported: int seek(void *cookie, off64_t *offset, int whence) */ + +static int +mock_close(void *cookie) +{ + io_cookie_t *c = cookie; + + if (! c) + return EBADF; + + close(c->fd); + free(c); + return 0; +} /* mock_close */ + +static cookie_io_functions_t mock_io = { + /* read = */ mock_read, + /* write = */ mock_write, + /* seek = */ NULL, + /* close = */ mock_close, +}; + +/* + * mocked functions + */ + +static int myfd = -1; + +static void * +dlopen_libc(void) +{ + static void *libc = NULL; + + if (libc) + return libc; + + libc = dlopen("libc.so.6", RTLD_LAZY); + if (! libc) + fail("INTERNAL ERROR: failed to load libc"); + return libc; +} /* dlopen_libc */ + +int +socket(int domain, int __attribute__((unused)) type, + int __attribute__((unused)) protocol) +{ + char tmp_file[] = "unixsock_test_socket.XXXXXX"; + int tmp_fd; + + /* we only want to mock UNIX sockets; return an error else */ + if (domain != AF_UNIX) { + errno = EAFNOSUPPORT; + return -1; + } + + /* create an 'anonymous' file to have a valid file-descriptor + * which can be close()ed by the caller */ + tmp_fd = mkstemp(tmp_file); + if (tmp_fd < 0) + return -1; + + unlink(tmp_file); + myfd = tmp_fd; + return tmp_fd; +} /* socket */ + +int +connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) +{ + if (sockfd < 0) { + errno = EBADF; + return -1; + } + + /* we only want to mock UNIX sockets; return an error else */ + if ((addrlen != sizeof(struct sockaddr_un)) || (! addr) + || (! ((const struct sockaddr_un *)addr)->sun_path)) { + errno = EAFNOSUPPORT; + return -1; + } + return 0; +} /* connect */ + +FILE * +fdopen(int fd, const char *mode) +{ + io_cookie_t *cookie; + + if (fd < 0) { + errno = EBADF; + return NULL; + } + + /* check also uses fdopen; in that case we need + * to use the original implementation */ + if (fd != myfd) { + void *libc = dlopen_libc(); + FILE *(*orig_fdopen)(int, const char *) = dlsym(libc, "fdopen"); + + if (! orig_fdopen) + fail("INTERNAL ERROR: failed to load fdopen() from libc"); + + return orig_fdopen(fd, mode); + } + + cookie = calloc(sizeof(*cookie), 1); + if (! cookie) + return NULL; + + cookie->fd = fd; + cookie->pos = 0; + return fopencookie(cookie, mode, mock_io); +} /* fdopen */ + +/* + * private variables + */ + +static sdb_unixsock_client_t *client; + +static void +setup(void) +{ + client = sdb_unixsock_client_create("unixsock_test_path"); + fail_unless(client != NULL, + "sdb_unixsock_client_create() = NULL; " + "expected unixsock client object"); +} /* setup */ + +static void +teardown(void) +{ + sdb_unixsock_client_destroy(client); + client = NULL; +} /* teardown */ + +static void +conn(void) +{ + int check; + + check = sdb_unixsock_client_connect(client); + fail_unless(check == 0, + "sdb_unixsock_client_connect() = %i; expected: 0", check); +} /* conn */ + +/* + * tests + */ + +START_TEST(test_sdb_unixsock_client_create) +{ + sdb_unixsock_client_t *c; + const char *check; + + c = sdb_unixsock_client_create(NULL); + fail_unless(c == NULL, + "sdb_unixsock_client_create() = %p; expected: NULL", c); + + c = sdb_unixsock_client_create("unixsock_test_path"); + fail_unless(c != NULL, + "sdb_unixsock_client_create() = NULL; " + "expected unixsock client object"); + + check = sdb_unixsock_client_path(c); + fail_unless(check != NULL, + "sdb_unixsock_client_create() did not store path name"); + fail_unless(!strcmp(check, "unixsock_test_path"), + "sdb_unixsock_client_create() did not store correct path name; " + "got: '%s'; expected: 'unixsock_test_path'", check); + sdb_unixsock_client_destroy(c); +} +END_TEST + +START_TEST(test_sdb_unixsock_client_connect) +{ + int check; + + check = sdb_unixsock_client_connect(client); + fail_unless(check == 0, + "sdb_unixsock_client_connect() = %i; expected: 0", check); +} +END_TEST + +START_TEST(test_sdb_unixsock_client_send) +{ + size_t i; + + conn(); + + for (i = 0; i < SDB_STATIC_ARRAY_LEN(golden_data); ++i) { + int check; + + mock_write_called = 0; + check = sdb_unixsock_client_send(client, golden_data[i].data); + /* client_send appends \r\n */ + fail_unless((size_t)check == golden_data[i].len + 2, + "sdb_unixsock_client_send() = %i; expected: %zu", + check, golden_data[i].len + 2); + fail_unless(mock_write_called == 1, + "sdb_unixsock_client_send() called mock_write %llu times; " + "expected: 1", mock_write_called); + fail_unless(last_write != NULL, + "INTERNAL ERROR: mock_write did not record last write"); + fail_unless((last_write[check - 1] == '\n') + && (last_write[check - 2] == '\r'), + "sdb_unixsock_client_send() did not append \\r\\n " + "before sending; got: '%s'", last_write); + fail_unless(!strncmp(last_write, golden_data[i].data, + (size_t)check - 2), + "sdb_unixsock_client_send() sent unexpected string '%s'; " + "expected: '%s'", last_write, golden_data[i].data); + free(last_write); + last_write = NULL; + } +} +END_TEST + +START_TEST(test_sdb_unixsock_client_recv) +{ + size_t i; + + conn(); + + for (i = 0; i < SDB_STATIC_ARRAY_LEN(golden_data); ++i) { + char *check; + char buf[64]; + + mock_read_called = 0; + check = sdb_unixsock_client_recv(client, buf, sizeof(buf)); + fail_unless(check != NULL, + "sdb_unixsock_client_recv() = NULL; expected: a string"); + fail_unless(check == buf, + "sdb_unixsock_client_recv() did not return a pointer " + "to the user-provided buffer"); + fail_unless(mock_read_called == 1, + "sdb_unixsock_client_recv() called mock_read %llu times; " + "expected: 1", mock_read_called); + fail_unless(strlen(check) == golden_data[i].len, + "sdb_unixsock_client_recv() returned string of length " + "%zu ('%s'); expected: %zu", + strlen(check), check, golden_data[i].len); + fail_unless(check[golden_data[i].len] != '\n', + "sdb_unixsock_client_recv() did not strip newline"); + fail_unless(!strcmp(check, golden_data[i].data), + "sdb_unixsock_client_recv() = '%s'; expected: '%s'", + check, golden_data[i].data); + } +} +END_TEST + +Suite * +util_unixsock_suite(void) +{ + Suite *s = suite_create("utils::unixsock"); + TCase *tc; + + tc = tcase_create("core"); + tcase_add_checked_fixture(tc, setup, teardown); + tcase_add_test(tc, test_sdb_unixsock_client_create); + tcase_add_test(tc, test_sdb_unixsock_client_connect); + tcase_add_test(tc, test_sdb_unixsock_client_send); + tcase_add_test(tc, test_sdb_unixsock_client_recv); + suite_add_tcase(s, tc); + + return s; +} /* util_unixsock_suite */ + +/* vim: set tw=78 sw=4 ts=4 noexpandtab : */ + -- 2.30.2