X-Git-Url: https://git.saurik.com/redis.git/blobdiff_plain/24f753a8b996a364273019ea791fe89f87cff678..b835f77933c0863605054eea716d0263aa634052:/deps/hiredis/test.c diff --git a/deps/hiredis/test.c b/deps/hiredis/test.c index 9afc6fa4..5945b655 100644 --- a/deps/hiredis/test.c +++ b/deps/hiredis/test.c @@ -1,3 +1,4 @@ +#include "fmacros.h" #include #include #include @@ -5,13 +6,33 @@ #include #include #include +#include +#include #include "hiredis.h" +enum connection_type { + CONN_TCP, + CONN_UNIX +}; + +struct config { + enum connection_type type; + + struct { + const char *host; + int port; + } tcp; + + struct { + const char *path; + } unix; +}; + /* The following lines make up our testing "framework" :) */ static int tests = 0, fails = 0; #define test(_s) { printf("#%02d ", ++tests); printf(_s); } -#define test_cond(_c) if(_c) printf("PASSED\n"); else {printf("FAILED\n"); fails++;} +#define test_cond(_c) if(_c) printf("\033[0;32mPASSED\033[0;0m\n"); else {printf("\033[0;31mFAILED\033[0;0m\n"); fails++;} static long long usec(void) { struct timeval tv; @@ -19,18 +40,63 @@ static long long usec(void) { return (((long long)tv.tv_sec)*1000000)+tv.tv_usec; } -static int use_unix = 0; -static redisContext *blocking_context = NULL; -static void __connect(redisContext **target) { - *target = blocking_context = (use_unix ? - redisConnectUnix("/tmp/redis.sock") : redisConnect((char*)"127.0.0.1", 6379)); - if (blocking_context->err) { - printf("Connection error: %s\n", blocking_context->errstr); +static redisContext *select_database(redisContext *c) { + redisReply *reply; + + /* Switch to DB 9 for testing, now that we know we can chat. */ + reply = redisCommand(c,"SELECT 9"); + assert(reply != NULL); + freeReplyObject(reply); + + /* Make sure the DB is emtpy */ + reply = redisCommand(c,"DBSIZE"); + assert(reply != NULL); + if (reply->type == REDIS_REPLY_INTEGER && reply->integer == 0) { + /* Awesome, DB 9 is empty and we can continue. */ + freeReplyObject(reply); + } else { + printf("Database #9 is not empty, test can not continue\n"); + exit(1); + } + + return c; +} + +static void disconnect(redisContext *c) { + redisReply *reply; + + /* Make sure we're on DB 9. */ + reply = redisCommand(c,"SELECT 9"); + assert(reply != NULL); + freeReplyObject(reply); + reply = redisCommand(c,"FLUSHDB"); + assert(reply != NULL); + freeReplyObject(reply); + + /* Free the context as well. */ + redisFree(c); +} + +static redisContext *connect(struct config config) { + redisContext *c = NULL; + + if (config.type == CONN_TCP) { + c = redisConnect(config.tcp.host, config.tcp.port); + } else if (config.type == CONN_UNIX) { + c = redisConnectUnix(config.unix.path); + } else { + assert(NULL); + } + + if (c->err) { + printf("Connection error: %s\n", c->errstr); exit(1); } + + return select_database(c); } -static void test_format_commands() { +static void test_format_commands(void) { char *cmd; int len; @@ -46,17 +112,79 @@ static void test_format_commands() { len == 4+4+(3+2)+4+(3+2)+4+(3+2)); free(cmd); + test("Format command with %%s and an empty string: "); + len = redisFormatCommand(&cmd,"SET %s %s","foo",""); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(0+2)); + free(cmd); + + test("Format command with an empty string in between proper interpolations: "); + len = redisFormatCommand(&cmd,"SET %s %s","","foo"); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$0\r\n\r\n$3\r\nfoo\r\n",len) == 0 && + len == 4+4+(3+2)+4+(0+2)+4+(3+2)); + free(cmd); + test("Format command with %%b string interpolation: "); len = redisFormatCommand(&cmd,"SET %b %b","foo",3,"b\0r",3); test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nb\0r\r\n",len) == 0 && len == 4+4+(3+2)+4+(3+2)+4+(3+2)); free(cmd); + test("Format command with %%b and an empty string: "); + len = redisFormatCommand(&cmd,"SET %b %b","foo",3,"",0); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$0\r\n\r\n",len) == 0 && + len == 4+4+(3+2)+4+(3+2)+4+(0+2)); + free(cmd); + + test("Format command with literal %%: "); + len = redisFormatCommand(&cmd,"SET %% %%"); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$1\r\n%\r\n$1\r\n%\r\n",len) == 0 && + len == 4+4+(3+2)+4+(1+2)+4+(1+2)); + free(cmd); + + /* Vararg width depends on the type. These tests make sure that the + * width is correctly determined using the format and subsequent varargs + * can correctly be interpolated. */ +#define INTEGER_WIDTH_TEST(fmt, type) do { \ + type value = 123; \ + test("Format command with printf-delegation (" #type "): "); \ + len = redisFormatCommand(&cmd,"key:%08" fmt " str:%s", value, "hello"); \ + test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:00000123\r\n$9\r\nstr:hello\r\n",len) == 0 && \ + len == 4+5+(12+2)+4+(9+2)); \ + free(cmd); \ +} while(0) + +#define FLOAT_WIDTH_TEST(type) do { \ + type value = 123.0; \ + test("Format command with printf-delegation (" #type "): "); \ + len = redisFormatCommand(&cmd,"key:%08.3f str:%s", value, "hello"); \ + test_cond(strncmp(cmd,"*2\r\n$12\r\nkey:0123.000\r\n$9\r\nstr:hello\r\n",len) == 0 && \ + len == 4+5+(12+2)+4+(9+2)); \ + free(cmd); \ +} while(0) + + INTEGER_WIDTH_TEST("d", int); + INTEGER_WIDTH_TEST("hhd", char); + INTEGER_WIDTH_TEST("hd", short); + INTEGER_WIDTH_TEST("ld", long); + INTEGER_WIDTH_TEST("lld", long long); + INTEGER_WIDTH_TEST("u", unsigned int); + INTEGER_WIDTH_TEST("hhu", unsigned char); + INTEGER_WIDTH_TEST("hu", unsigned short); + INTEGER_WIDTH_TEST("lu", unsigned long); + INTEGER_WIDTH_TEST("llu", unsigned long long); + FLOAT_WIDTH_TEST(float); + FLOAT_WIDTH_TEST(double); + + test("Format command with invalid printf format: "); + len = redisFormatCommand(&cmd,"key:%08p %b",(void*)1234,"foo",3); + test_cond(len == -1); + const char *argv[3]; argv[0] = "SET"; - argv[1] = "foo"; + argv[1] = "foo\0xxx"; argv[2] = "bar"; - size_t lens[3] = { 3, 3, 3 }; + size_t lens[3] = { 3, 7, 3 }; int argc = 3; test("Format command by passing argc/argv without lengths: "); @@ -67,59 +195,122 @@ static void test_format_commands() { test("Format command by passing argc/argv with lengths: "); len = redisFormatCommandArgv(&cmd,argc,argv,lens); - test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",len) == 0 && - len == 4+4+(3+2)+4+(3+2)+4+(3+2)); + test_cond(strncmp(cmd,"*3\r\n$3\r\nSET\r\n$7\r\nfoo\0xxx\r\n$3\r\nbar\r\n",len) == 0 && + len == 4+4+(3+2)+4+(7+2)+4+(3+2)); free(cmd); } -static void test_blocking_connection() { - redisContext *c; - redisReply *reply; +static void test_reply_reader(void) { + redisReader *reader; + void *reply; + int ret; - __connect(&c); - test("Returns I/O error when the connection is lost: "); - reply = redisCommand(c,"QUIT"); - test_cond(strcasecmp(reply->str,"OK") == 0 && redisCommand(c,"PING") == NULL); - - /* Two conditions may happen, depending on the type of connection. - * When connected via TCP, the socket will not yet be aware of the closed - * connection and the write(2) call will succeed, but the read(2) will - * result in an EOF. When connected via Unix sockets, the socket will be - * immediately aware that it was closed and fail on the write(2) call. */ - if (use_unix) { - fprintf(stderr,"Error: %s\n", c->errstr); - assert(c->err == REDIS_ERR_IO && - strcmp(c->errstr,"Broken pipe") == 0); - } else { - fprintf(stderr,"Error: %s\n", c->errstr); - assert(c->err == REDIS_ERR_EOF && - strcmp(c->errstr,"Server closed the connection") == 0); - } + test("Error handling in reply parser: "); + reader = redisReaderCreate(); + redisReaderFeed(reader,(char*)"@foo\r\n",6); + ret = redisReaderGetReply(reader,NULL); + test_cond(ret == REDIS_ERR && + strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0); + redisReaderFree(reader); + + /* when the reply already contains multiple items, they must be free'd + * on an error. valgrind will bark when this doesn't happen. */ + test("Memory cleanup in reply parser: "); + reader = redisReaderCreate(); + redisReaderFeed(reader,(char*)"*2\r\n",4); + redisReaderFeed(reader,(char*)"$5\r\nhello\r\n",11); + redisReaderFeed(reader,(char*)"@foo\r\n",6); + ret = redisReaderGetReply(reader,NULL); + test_cond(ret == REDIS_ERR && + strcasecmp(reader->errstr,"Protocol error, got \"@\" as reply type byte") == 0); + redisReaderFree(reader); + + test("Set error on nested multi bulks with depth > 2: "); + reader = redisReaderCreate(); + redisReaderFeed(reader,(char*)"*1\r\n",4); + redisReaderFeed(reader,(char*)"*1\r\n",4); + redisReaderFeed(reader,(char*)"*1\r\n",4); + redisReaderFeed(reader,(char*)"*1\r\n",4); + ret = redisReaderGetReply(reader,NULL); + test_cond(ret == REDIS_ERR && + strncasecmp(reader->errstr,"No support for",14) == 0); + redisReaderFree(reader); + + test("Works with NULL functions for reply: "); + reader = redisReaderCreate(); + reader->fn = NULL; + redisReaderFeed(reader,(char*)"+OK\r\n",5); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_OK && reply == (void*)REDIS_REPLY_STATUS); + redisReaderFree(reader); + + test("Works when a single newline (\\r\\n) covers two calls to feed: "); + reader = redisReaderCreate(); + reader->fn = NULL; + redisReaderFeed(reader,(char*)"+OK\r",4); + ret = redisReaderGetReply(reader,&reply); + assert(ret == REDIS_OK && reply == NULL); + redisReaderFeed(reader,(char*)"\n",1); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_OK && reply == (void*)REDIS_REPLY_STATUS); + redisReaderFree(reader); + + test("Don't reset state after protocol error: "); + reader = redisReaderCreate(); + reader->fn = NULL; + redisReaderFeed(reader,(char*)"x",1); + ret = redisReaderGetReply(reader,&reply); + assert(ret == REDIS_ERR); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_ERR && reply == NULL); + redisReaderFree(reader); + + /* Regression test for issue #45 on GitHub. */ + test("Don't do empty allocation for empty multi bulk: "); + reader = redisReaderCreate(); + redisReaderFeed(reader,(char*)"*0\r\n",4); + ret = redisReaderGetReply(reader,&reply); + test_cond(ret == REDIS_OK && + ((redisReply*)reply)->type == REDIS_REPLY_ARRAY && + ((redisReply*)reply)->elements == 0); freeReplyObject(reply); + redisReaderFree(reader); +} + +static void test_blocking_connection_errors(void) { + redisContext *c; + + test("Returns error when host cannot be resolved: "); + c = redisConnect((char*)"idontexist.local", 6379); + test_cond(c->err == REDIS_ERR_OTHER && + (strcmp(c->errstr,"Name or service not known") == 0 || + strcmp(c->errstr,"Can't resolve: idontexist.local") == 0)); redisFree(c); - __connect(&c); /* reconnect */ + test("Returns error when the port is not open: "); + c = redisConnect((char*)"localhost", 1); + test_cond(c->err == REDIS_ERR_IO && + strcmp(c->errstr,"Connection refused") == 0); + redisFree(c); + + test("Returns error when the unix socket path doesn't accept connections: "); + c = redisConnectUnix((char*)"/tmp/idontexist.sock"); + test_cond(c->err == REDIS_ERR_IO); /* Don't care about the message... */ + redisFree(c); +} + +static void test_blocking_connection(struct config config) { + redisContext *c; + redisReply *reply; + + c = connect(config); + test("Is able to deliver commands: "); reply = redisCommand(c,"PING"); test_cond(reply->type == REDIS_REPLY_STATUS && strcasecmp(reply->str,"pong") == 0) freeReplyObject(reply); - /* Switch to DB 9 for testing, now that we know we can chat. */ - reply = redisCommand(c,"SELECT 9"); - freeReplyObject(reply); - - /* Make sure the DB is emtpy */ - reply = redisCommand(c,"DBSIZE"); - if (reply->type != REDIS_REPLY_INTEGER || - reply->integer != 0) { - printf("Sorry DB 9 is not empty, test can not continue\n"); - exit(1); - } else { - printf("DB 9 is empty... test can continue\n"); - } - freeReplyObject(reply); - test("Is a able to send commands verbatim: "); reply = redisCommand(c,"SET foo bar"); test_cond (reply->type == REDIS_REPLY_STATUS && @@ -182,80 +373,124 @@ static void test_blocking_connection() { reply->element[1]->type == REDIS_REPLY_STATUS && strcasecmp(reply->element[1]->str,"pong") == 0); freeReplyObject(reply); + + disconnect(c); } -static void test_reply_reader() { - void *reader; - char *err; - int ret; +static void test_blocking_io_errors(struct config config) { + redisContext *c; + redisReply *reply; + void *_reply; + int major, minor; + + /* Connect to target given by config. */ + c = connect(config); + { + /* Find out Redis version to determine the path for the next test */ + const char *field = "redis_version:"; + char *p, *eptr; + + reply = redisCommand(c,"INFO"); + p = strstr(reply->str,field); + major = strtol(p+strlen(field),&eptr,10); + p = eptr+1; /* char next to the first "." */ + minor = strtol(p,&eptr,10); + freeReplyObject(reply); + } - test("Error handling in reply parser: "); - reader = redisReplyReaderCreate(NULL); - redisReplyReaderFeed(reader,(char*)"@foo\r\n",6); - ret = redisReplyReaderGetReply(reader,NULL); - err = redisReplyReaderGetError(reader); - test_cond(ret == REDIS_ERR && - strcasecmp(err,"protocol error, got \"@\" as reply type byte") == 0); - redisReplyReaderFree(reader); + test("Returns I/O error when the connection is lost: "); + reply = redisCommand(c,"QUIT"); + if (major >= 2 && minor > 0) { + /* > 2.0 returns OK on QUIT and read() should be issued once more + * to know the descriptor is at EOF. */ + test_cond(strcasecmp(reply->str,"OK") == 0 && + redisGetReply(c,&_reply) == REDIS_ERR); + freeReplyObject(reply); + } else { + test_cond(reply == NULL); + } - /* when the reply already contains multiple items, they must be free'd - * on an error. valgrind will bark when this doesn't happen. */ - test("Memory cleanup in reply parser: "); - reader = redisReplyReaderCreate(NULL); - redisReplyReaderFeed(reader,(char*)"*2\r\n",4); - redisReplyReaderFeed(reader,(char*)"$5\r\nhello\r\n",11); - redisReplyReaderFeed(reader,(char*)"@foo\r\n",6); - ret = redisReplyReaderGetReply(reader,NULL); - err = redisReplyReaderGetError(reader); - test_cond(ret == REDIS_ERR && - strcasecmp(err,"protocol error, got \"@\" as reply type byte") == 0); - redisReplyReaderFree(reader); + /* 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 + * issued to find out the socket was closed by the server. In both + * conditions, the error will be set to EOF. */ + assert(c->err == REDIS_ERR_EOF && + strcmp(c->errstr,"Server closed the connection") == 0); + redisFree(c); + + c = 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); + redisFree(c); } -static void test_throughput() { - int i; - long long t1, t2; - redisContext *c = blocking_context; +static void test_throughput(struct config config) { + redisContext *c = connect(config); redisReply **replies; + int i, num; + long long t1, t2; test("Throughput:\n"); for (i = 0; i < 500; i++) freeReplyObject(redisCommand(c,"LPUSH mylist foo")); - replies = malloc(sizeof(redisReply*)*1000); + num = 1000; + replies = malloc(sizeof(redisReply*)*num); t1 = usec(); - for (i = 0; i < 1000; i++) { + for (i = 0; i < num; i++) { replies[i] = redisCommand(c,"PING"); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_STATUS); } t2 = usec(); - for (i = 0; i < 1000; i++) freeReplyObject(replies[i]); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); free(replies); - printf("\t(1000x PING: %.2fs)\n", (t2-t1)/1000000.0); + printf("\t(%dx PING: %.3fs)\n", num, (t2-t1)/1000000.0); - replies = malloc(sizeof(redisReply*)*1000); + replies = malloc(sizeof(redisReply*)*num); t1 = usec(); - for (i = 0; i < 1000; i++) { + for (i = 0; i < num; i++) { replies[i] = redisCommand(c,"LRANGE mylist 0 499"); assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_ARRAY); assert(replies[i] != NULL && replies[i]->elements == 500); } t2 = usec(); - for (i = 0; i < 1000; i++) freeReplyObject(replies[i]); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); free(replies); - printf("\t(1000x LRANGE with 500 elements: %.2fs)\n", (t2-t1)/1000000.0); -} + printf("\t(%dx LRANGE with 500 elements: %.3fs)\n", num, (t2-t1)/1000000.0); -static void cleanup() { - redisContext *c = blocking_context; - redisReply *reply; + num = 10000; + replies = malloc(sizeof(redisReply*)*num); + for (i = 0; i < num; i++) + redisAppendCommand(c,"PING"); + t1 = usec(); + for (i = 0; i < num; i++) { + assert(redisGetReply(c, (void*)&replies[i]) == REDIS_OK); + assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_STATUS); + } + t2 = usec(); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); + free(replies); + printf("\t(%dx PING (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); - /* Make sure we're on DB 9 */ - reply = redisCommand(c,"SELECT 9"); - assert(reply != NULL); freeReplyObject(reply); - reply = redisCommand(c,"FLUSHDB"); - assert(reply != NULL); freeReplyObject(reply); - redisFree(c); + replies = malloc(sizeof(redisReply*)*num); + for (i = 0; i < num; i++) + redisAppendCommand(c,"LRANGE mylist 0 499"); + t1 = usec(); + for (i = 0; i < num; i++) { + assert(redisGetReply(c, (void*)&replies[i]) == REDIS_OK); + assert(replies[i] != NULL && replies[i]->type == REDIS_REPLY_ARRAY); + assert(replies[i] != NULL && replies[i]->elements == 500); + } + t2 = usec(); + for (i = 0; i < num; i++) freeReplyObject(replies[i]); + free(replies); + printf("\t(%dx LRANGE with 500 elements (pipelined): %.3fs)\n", num, (t2-t1)/1000000.0); + + disconnect(c); } // static long __test_callback_flags = 0; @@ -277,7 +512,7 @@ static void cleanup() { // static redisContext *__connect_nonblock() { // /* Reset callback flags */ // __test_callback_flags = 0; -// return redisConnectNonBlock("127.0.0.1", 6379, NULL); +// return redisConnectNonBlock("127.0.0.1", port, NULL); // } // // static void test_nonblocking_connection() { @@ -358,23 +593,62 @@ static void cleanup() { // } int main(int argc, char **argv) { - if (argc > 1) { - if (strcmp(argv[1],"-s") == 0) - use_unix = 1; + struct config cfg = { + .tcp = { + .host = "127.0.0.1", + .port = 6379 + }, + .unix = { + .path = "/tmp/redis.sock" + } + }; + int throughput = 1; + + /* Ignore broken pipe signal (for I/O error tests). */ + signal(SIGPIPE, SIG_IGN); + + /* Parse command line options. */ + argv++; argc--; + while (argc) { + if (argc >= 2 && !strcmp(argv[0],"-h")) { + argv++; argc--; + cfg.tcp.host = argv[0]; + } else if (argc >= 2 && !strcmp(argv[0],"-p")) { + argv++; argc--; + cfg.tcp.port = atoi(argv[0]); + } else if (argc >= 2 && !strcmp(argv[0],"-s")) { + argv++; argc--; + cfg.unix.path = argv[0]; + } else if (argc >= 1 && !strcmp(argv[0],"--skip-throughput")) { + throughput = 0; + } else { + fprintf(stderr, "Invalid argument: %s\n", argv[0]); + exit(1); + } + argv++; argc--; } - signal(SIGPIPE, SIG_IGN); test_format_commands(); - test_blocking_connection(); test_reply_reader(); - // test_nonblocking_connection(); - test_throughput(); - cleanup(); + test_blocking_connection_errors(); - if (fails == 0) { - printf("ALL TESTS PASSED\n"); - } else { + printf("\nTesting against TCP connection (%s:%d):\n", cfg.tcp.host, cfg.tcp.port); + cfg.type = CONN_TCP; + test_blocking_connection(cfg); + test_blocking_io_errors(cfg); + if (throughput) test_throughput(cfg); + + printf("\nTesting against Unix socket connection (%s):\n", cfg.unix.path); + cfg.type = CONN_UNIX; + test_blocking_connection(cfg); + test_blocking_io_errors(cfg); + if (throughput) test_throughput(cfg); + + if (fails) { printf("*** %d TESTS FAILED ***\n", fails); + return 1; } + + printf("ALL TESTS PASSED\n"); return 0; }