From cc9d03297177eb8504c7353db673b9dc6f64ea07 Mon Sep 17 00:00:00 2001 From: Michael Grunder Date: Thu, 2 Apr 2020 22:41:34 -0700 Subject: Win32 tests and timeout fix (#776) Unit tests in Windows and a Windows timeout fix This commit gets our unit tests compiling and running on Windows as well as removes a duplicated `timeval` -> `DWORD` conversion logic in sockcompat.c There are minor differences in behavior between Linux and Windows to note: 1. In Windows, opening a non-existent hangs forever in WSAPoll whereas it correctly returns with a "Connection refused" error on Linux. For that reason, I simply skip this test in Windows. It may be related to this known issue: https://daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/ 2. Timeouts are handled slightly differently in Windows and Linux. In Linux, we intentionally set REDIS_ERR_IO for connection timeouts whereas in Windows we set REDIS_ERR_TIMEOUT. It may be prudent to fix this discrepancy although there are almost certainly users relying on the current behavior. --- test.c | 135 ++++++++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 100 insertions(+), 35 deletions(-) (limited to 'test.c') diff --git a/test.c b/test.c index 9c0de6d..c9e23fe 100644 --- a/test.c +++ b/test.c @@ -1,13 +1,13 @@ #include "fmacros.h" +#include "sockcompat.h" #include #include #include +#ifndef _WIN32 #include -#include #include -#include +#endif #include -#include #include #include #include @@ -17,6 +17,7 @@ #include "hiredis_ssl.h" #endif #include "net.h" +#include "win32.h" enum connection_type { CONN_TCP, @@ -48,14 +49,21 @@ struct config { }; /* The following lines make up our testing "framework" :) */ -static int tests = 0, fails = 0; +static int tests = 0, fails = 0, skips = 0; #define test(_s) { printf("#%02d ", ++tests); printf(_s); } #define test_cond(_c) if(_c) printf("\033[0;32mPASSED\033[0;0m\n"); else {printf("\033[0;31mFAILED\033[0;0m\n"); fails++;} +#define test_skipped() { printf("\033[01;33mSKIPPED\033[0;0m\n"); skips++; } static long long usec(void) { +#ifndef _MSC_VER struct timeval tv; gettimeofday(&tv,NULL); return (((long long)tv.tv_sec)*1000000)+tv.tv_usec; +#else + FILETIME ft; + GetSystemTimeAsFileTime(&ft); + return (((long long)ft.dwHighDateTime << 32) | ft.dwLowDateTime) / 10; +#endif } /* The assert() calls below have side effects, so we need assert() @@ -491,19 +499,19 @@ static void test_blocking_connection_errors(void) { (strcmp(c->errstr, "Name or service not known") == 0 || strcmp(c->errstr, "Can't resolve: " HIREDIS_BAD_DOMAIN) == 0 || strcmp(c->errstr, "Name does not resolve") == 0 || - strcmp(c->errstr, - "nodename nor servname provided, or not known") == 0 || + strcmp(c->errstr, "nodename nor servname provided, or not known") == 0 || strcmp(c->errstr, "No address associated with hostname") == 0 || strcmp(c->errstr, "Temporary failure in name resolution") == 0 || - strcmp(c->errstr, - "hostname nor servname provided, or not known") == 0 || - strcmp(c->errstr, "no address associated with name") == 0)); + strcmp(c->errstr, "hostname nor servname provided, or not known") == 0 || + strcmp(c->errstr, "no address associated with name") == 0 || + strcmp(c->errstr, "No such host is known. ") == 0)); redisFree(c); } else { printf("Skipping NXDOMAIN test. Found evil ISP!\n"); freeaddrinfo(ai_tmp); } +#ifndef _WIN32 test("Returns error when the port is not open: "); c = redisConnect((char*)"localhost", 1); test_cond(c->err == REDIS_ERR_IO && @@ -514,6 +522,7 @@ static void test_blocking_connection_errors(void) { c = redisConnectUnix((char*)"/tmp/idontexist.sock"); test_cond(c->err == REDIS_ERR_IO); /* Don't care about the message... */ redisFree(c); +#endif } static void test_blocking_connection(struct config config) { @@ -599,11 +608,28 @@ static void test_blocking_connection(struct config config) { disconnect(c, 0); } +/* Send DEBUG SLEEP 0 to detect if we have this command */ +static int detect_debug_sleep(redisContext *c) { + int detected; + redisReply *reply = redisCommand(c, "DEBUG SLEEP 0\r\n"); + + if (reply == NULL || c->err) { + const char *cause = c->err ? c->errstr : "(none)"; + fprintf(stderr, "Error testing for DEBUG SLEEP (Redis error: %s), exiting\n", cause); + exit(-1); + } + + detected = reply->type == REDIS_REPLY_STATUS; + freeReplyObject(reply); + + return detected; +} + static void test_blocking_connection_timeouts(struct config config) { redisContext *c; redisReply *reply; ssize_t s; - const char *cmd = "DEBUG SLEEP 3\r\n"; + const char *sleep_cmd = "DEBUG SLEEP 3\r\n"; struct timeval tv; c = do_connect(config); @@ -620,14 +646,24 @@ static void test_blocking_connection_timeouts(struct config config) { c = do_connect(config); test("Does not return a reply when the command times out: "); - redisAppendFormattedCommand(c, cmd, strlen(cmd)); - s = c->funcs->write(c); - tv.tv_sec = 0; - tv.tv_usec = 10000; - redisSetTimeout(c, tv); - reply = redisCommand(c, "GET foo"); - test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_IO && strcmp(c->errstr, "Resource temporarily unavailable") == 0); - freeReplyObject(reply); + if (detect_debug_sleep(c)) { + redisAppendFormattedCommand(c, sleep_cmd, strlen(sleep_cmd)); + s = c->funcs->write(c); + tv.tv_sec = 0; + tv.tv_usec = 10000; + redisSetTimeout(c, tv); + reply = redisCommand(c, "GET foo"); +#ifndef _WIN32 + test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_IO && + strcmp(c->errstr, "Resource temporarily unavailable") == 0); +#else + test_cond(s > 0 && reply == NULL && c->err == REDIS_ERR_TIMEOUT && + strcmp(c->errstr, "recv timeout") == 0); +#endif + freeReplyObject(reply); + } else { + test_skipped(); + } test("Reconnect properly reconnects after a timeout: "); do_reconnect(c, config); @@ -679,6 +715,7 @@ static void test_blocking_io_errors(struct config config) { test_cond(reply == NULL); } +#ifndef _WIN32 /* On 2.0, QUIT will cause the connection to be closed immediately and * the read(2) for the reply on QUIT will set the error to EOF. * On >2.0, QUIT will return with OK and another read(2) needed to be @@ -686,14 +723,19 @@ static void test_blocking_io_errors(struct config config) { * conditions, the error will be set to EOF. */ assert(c->err == REDIS_ERR_EOF && strcmp(c->errstr,"Server closed the connection") == 0); +#endif redisFree(c); c = do_connect(config); test("Returns I/O error on socket timeout: "); struct timeval tv = { 0, 1000 }; assert(redisSetTimeout(c,tv) == REDIS_OK); - test_cond(redisGetReply(c,&_reply) == REDIS_ERR && - c->err == REDIS_ERR_IO && errno == EAGAIN); + int respcode = redisGetReply(c,&_reply); +#ifndef _WIN32 + test_cond(respcode == REDIS_ERR && c->err == REDIS_ERR_IO && errno == EAGAIN); +#else + test_cond(respcode == REDIS_ERR && c->err == REDIS_ERR_TIMEOUT); +#endif redisFree(c); } @@ -921,9 +963,8 @@ int main(int argc, char **argv) { }; int throughput = 1; int test_inherit_fd = 1; - - /* Ignore broken pipe signal (for I/O error tests). */ - signal(SIGPIPE, SIG_IGN); + int skips_as_fails = 0; + int test_unix_socket; /* Parse command line options. */ argv++; argc--; @@ -941,6 +982,8 @@ int main(int argc, char **argv) { throughput = 0; } else if (argc >= 1 && !strcmp(argv[0],"--skip-inherit-fd")) { test_inherit_fd = 0; + } else if (argc >= 1 && !strcmp(argv[0],"--skips-as-fails")) { + skips_as_fails = 1; #ifdef HIREDIS_TEST_SSL } else if (argc >= 2 && !strcmp(argv[0],"--ssl-port")) { argv++; argc--; @@ -965,6 +1008,16 @@ int main(int argc, char **argv) { argv++; argc--; } +#ifndef _WIN32 + /* Ignore broken pipe signal (for I/O error tests). */ + signal(SIGPIPE, SIG_IGN); + + test_unix_socket = access(cfg.unix_sock.path, F_OK) == 0; +#else + /* Unix sockets don't exist in Windows */ + test_unix_socket = 0; +#endif + test_format_commands(); test_reply_reader(); test_blocking_connection_errors(); @@ -979,12 +1032,17 @@ int main(int argc, char **argv) { test_append_formatted_commands(cfg); if (throughput) test_throughput(cfg); - printf("\nTesting against Unix socket connection (%s):\n", cfg.unix_sock.path); - cfg.type = CONN_UNIX; - test_blocking_connection(cfg); - test_blocking_connection_timeouts(cfg); - test_blocking_io_errors(cfg); - if (throughput) test_throughput(cfg); + printf("\nTesting against Unix socket connection (%s): ", cfg.unix_sock.path); + if (test_unix_socket) { + printf("\n"); + cfg.type = CONN_UNIX; + test_blocking_connection(cfg); + test_blocking_connection_timeouts(cfg); + test_blocking_io_errors(cfg); + if (throughput) test_throughput(cfg); + } else { + test_skipped(); + } #ifdef HIREDIS_TEST_SSL if (cfg.ssl.port && cfg.ssl.host) { @@ -1001,17 +1059,24 @@ int main(int argc, char **argv) { #endif if (test_inherit_fd) { - printf("\nTesting against inherited fd (%s):\n", cfg.unix_sock.path); - cfg.type = CONN_FD; - test_blocking_connection(cfg); + printf("\nTesting against inherited fd (%s): ", cfg.unix_sock.path); + if (test_unix_socket) { + printf("\n"); + cfg.type = CONN_FD; + test_blocking_connection(cfg); + } else { + test_skipped(); + } } - - if (fails) { + if (fails || (skips_as_fails && skips)) { printf("*** %d TESTS FAILED ***\n", fails); + if (skips) { + printf("*** %d TESTS SKIPPED ***\n", skips); + } return 1; } - printf("ALL TESTS PASSED\n"); + printf("ALL TESTS PASSED (%d skipped)\n", skips); return 0; } -- cgit v1.2.3