1 /* Module designed to test the Redis modules subsystem. 2 * 3 * ----------------------------------------------------------------------------- 4 * 5 * Copyright (c) 2016, Salvatore Sanfilippo <antirez at gmail dot com> 6 * All rights reserved. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions are met: 10 * 11 * * Redistributions of source code must retain the above copyright notice, 12 * this list of conditions and the following disclaimer. 13 * * Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in the 15 * documentation and/or other materials provided with the distribution. 16 * * Neither the name of Redis nor the names of its contributors may be used 17 * to endorse or promote products derived from this software without 18 * specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 24 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 * POSSIBILITY OF SUCH DAMAGE. 31 */ 32 33 #define REDISMODULE_EXPERIMENTAL_API 34 #include "../redismodule.h" 35 #include <string.h> 36 37 /* --------------------------------- Helpers -------------------------------- */ 38 39 /* Return true if the reply and the C null term string matches. */ 40 int TestMatchReply(RedisModuleCallReply *reply, char *str) { 41 RedisModuleString *mystr; 42 mystr = RedisModule_CreateStringFromCallReply(reply); 43 if (!mystr) return 0; 44 const char *ptr = RedisModule_StringPtrLen(mystr,NULL); 45 return strcmp(ptr,str) == 0; 46 } 47 48 /* ------------------------------- Test units ------------------------------- */ 49 50 /* TEST.CALL -- Test Call() API. */ 51 int TestCall(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 52 REDISMODULE_NOT_USED(argv); 53 REDISMODULE_NOT_USED(argc); 54 55 RedisModule_AutoMemory(ctx); 56 RedisModuleCallReply *reply; 57 58 RedisModule_Call(ctx,"DEL","c","mylist"); 59 RedisModuleString *mystr = RedisModule_CreateString(ctx,"foo",3); 60 RedisModule_Call(ctx,"RPUSH","csl","mylist",mystr,(long long)1234); 61 reply = RedisModule_Call(ctx,"LRANGE","ccc","mylist","0","-1"); 62 long long items = RedisModule_CallReplyLength(reply); 63 if (items != 2) goto fail; 64 65 RedisModuleCallReply *item0, *item1; 66 67 item0 = RedisModule_CallReplyArrayElement(reply,0); 68 item1 = RedisModule_CallReplyArrayElement(reply,1); 69 if (!TestMatchReply(item0,"foo")) goto fail; 70 if (!TestMatchReply(item1,"1234")) goto fail; 71 72 RedisModule_ReplyWithSimpleString(ctx,"OK"); 73 return REDISMODULE_OK; 74 75 fail: 76 RedisModule_ReplyWithSimpleString(ctx,"ERR"); 77 return REDISMODULE_OK; 78 } 79 80 /* TEST.STRING.APPEND -- Test appending to an existing string object. */ 81 int TestStringAppend(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 82 REDISMODULE_NOT_USED(argv); 83 REDISMODULE_NOT_USED(argc); 84 85 RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3); 86 RedisModule_StringAppendBuffer(ctx,s,"bar",3); 87 RedisModule_ReplyWithString(ctx,s); 88 RedisModule_FreeString(ctx,s); 89 return REDISMODULE_OK; 90 } 91 92 /* TEST.STRING.APPEND.AM -- Test append with retain when auto memory is on. */ 93 int TestStringAppendAM(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 94 REDISMODULE_NOT_USED(argv); 95 REDISMODULE_NOT_USED(argc); 96 97 RedisModule_AutoMemory(ctx); 98 RedisModuleString *s = RedisModule_CreateString(ctx,"foo",3); 99 RedisModule_RetainString(ctx,s); 100 RedisModule_StringAppendBuffer(ctx,s,"bar",3); 101 RedisModule_ReplyWithString(ctx,s); 102 RedisModule_FreeString(ctx,s); 103 return REDISMODULE_OK; 104 } 105 106 /* TEST.STRING.PRINTF -- Test string formatting. */ 107 int TestStringPrintf(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 108 RedisModule_AutoMemory(ctx); 109 if (argc < 3) { 110 return RedisModule_WrongArity(ctx); 111 } 112 RedisModuleString *s = RedisModule_CreateStringPrintf(ctx, 113 "Got %d args. argv[1]: %s, argv[2]: %s", 114 argc, 115 RedisModule_StringPtrLen(argv[1], NULL), 116 RedisModule_StringPtrLen(argv[2], NULL) 117 ); 118 119 RedisModule_ReplyWithString(ctx,s); 120 121 return REDISMODULE_OK; 122 } 123 124 int failTest(RedisModuleCtx *ctx, const char *msg) { 125 RedisModule_ReplyWithError(ctx, msg); 126 return REDISMODULE_ERR; 127 } 128 129 int TestUnlink(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 130 RedisModule_AutoMemory(ctx); 131 REDISMODULE_NOT_USED(argv); 132 REDISMODULE_NOT_USED(argc); 133 134 RedisModuleKey *k = RedisModule_OpenKey(ctx, RedisModule_CreateStringPrintf(ctx, "unlinked"), REDISMODULE_WRITE | REDISMODULE_READ); 135 if (!k) return failTest(ctx, "Could not create key"); 136 137 if (REDISMODULE_ERR == RedisModule_StringSet(k, RedisModule_CreateStringPrintf(ctx, "Foobar"))) { 138 return failTest(ctx, "Could not set string value"); 139 } 140 141 RedisModuleCallReply *rep = RedisModule_Call(ctx, "EXISTS", "c", "unlinked"); 142 if (!rep || RedisModule_CallReplyInteger(rep) != 1) { 143 return failTest(ctx, "Key does not exist before unlink"); 144 } 145 146 if (REDISMODULE_ERR == RedisModule_UnlinkKey(k)) { 147 return failTest(ctx, "Could not unlink key"); 148 } 149 150 rep = RedisModule_Call(ctx, "EXISTS", "c", "unlinked"); 151 if (!rep || RedisModule_CallReplyInteger(rep) != 0) { 152 return failTest(ctx, "Could not verify key to be unlinked"); 153 } 154 return RedisModule_ReplyWithSimpleString(ctx, "OK"); 155 156 } 157 158 int NotifyCallback(RedisModuleCtx *ctx, int type, const char *event, 159 RedisModuleString *key) { 160 /* Increment a counter on the notifications: for each key notified we 161 * increment a counter */ 162 RedisModule_Log(ctx, "notice", "Got event type %d, event %s, key %s", type, 163 event, RedisModule_StringPtrLen(key, NULL)); 164 165 RedisModule_Call(ctx, "HINCRBY", "csc", "notifications", key, "1"); 166 return REDISMODULE_OK; 167 } 168 169 /* TEST.NOTIFICATIONS -- Test Keyspace Notifications. */ 170 int TestNotifications(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 171 REDISMODULE_NOT_USED(argv); 172 REDISMODULE_NOT_USED(argc); 173 174 #define FAIL(msg, ...) \ 175 { \ 176 RedisModule_Log(ctx, "warning", "Failed NOTIFY Test. Reason: " #msg, ##__VA_ARGS__); \ 177 goto err; \ 178 } 179 RedisModule_Call(ctx, "FLUSHDB", ""); 180 181 RedisModule_Call(ctx, "SET", "cc", "foo", "bar"); 182 RedisModule_Call(ctx, "SET", "cc", "foo", "baz"); 183 RedisModule_Call(ctx, "SADD", "cc", "bar", "x"); 184 RedisModule_Call(ctx, "SADD", "cc", "bar", "y"); 185 186 RedisModule_Call(ctx, "HSET", "ccc", "baz", "x", "y"); 187 /* LPUSH should be ignored and not increment any counters */ 188 RedisModule_Call(ctx, "LPUSH", "cc", "l", "y"); 189 RedisModule_Call(ctx, "LPUSH", "cc", "l", "y"); 190 191 size_t sz; 192 const char *rep; 193 RedisModuleCallReply *r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "foo"); 194 if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { 195 FAIL("Wrong or no reply for foo"); 196 } else { 197 rep = RedisModule_CallReplyStringPtr(r, &sz); 198 if (sz != 1 || *rep != '2') { 199 FAIL("Got reply '%s'. expected '2'", RedisModule_CallReplyStringPtr(r, NULL)); 200 } 201 } 202 203 r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "bar"); 204 if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { 205 FAIL("Wrong or no reply for bar"); 206 } else { 207 rep = RedisModule_CallReplyStringPtr(r, &sz); 208 if (sz != 1 || *rep != '2') { 209 FAIL("Got reply '%s'. expected '2'", rep); 210 } 211 } 212 213 r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "baz"); 214 if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_STRING) { 215 FAIL("Wrong or no reply for baz"); 216 } else { 217 rep = RedisModule_CallReplyStringPtr(r, &sz); 218 if (sz != 1 || *rep != '1') { 219 FAIL("Got reply '%.*s'. expected '1'", sz, rep); 220 } 221 } 222 /* For l we expect nothing since we didn't subscribe to list events */ 223 r = RedisModule_Call(ctx, "HGET", "cc", "notifications", "l"); 224 if (r == NULL || RedisModule_CallReplyType(r) != REDISMODULE_REPLY_NULL) { 225 FAIL("Wrong reply for l"); 226 } 227 228 RedisModule_Call(ctx, "FLUSHDB", ""); 229 230 return RedisModule_ReplyWithSimpleString(ctx, "OK"); 231 err: 232 RedisModule_Call(ctx, "FLUSHDB", ""); 233 234 return RedisModule_ReplyWithSimpleString(ctx, "ERR"); 235 } 236 237 /* TEST.CTXFLAGS -- Test GetContextFlags. */ 238 int TestCtxFlags(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 239 REDISMODULE_NOT_USED(argc); 240 REDISMODULE_NOT_USED(argv); 241 242 RedisModule_AutoMemory(ctx); 243 244 int ok = 1; 245 const char *errString = NULL; 246 #undef FAIL 247 #define FAIL(msg) \ 248 { \ 249 ok = 0; \ 250 errString = msg; \ 251 goto end; \ 252 } 253 254 int flags = RedisModule_GetContextFlags(ctx); 255 if (flags == 0) { 256 FAIL("Got no flags"); 257 } 258 259 if (flags & REDISMODULE_CTX_FLAGS_LUA) FAIL("Lua flag was set"); 260 if (flags & REDISMODULE_CTX_FLAGS_MULTI) FAIL("Multi flag was set"); 261 262 if (flags & REDISMODULE_CTX_FLAGS_AOF) FAIL("AOF Flag was set") 263 /* Enable AOF to test AOF flags */ 264 RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "yes"); 265 flags = RedisModule_GetContextFlags(ctx); 266 if (!(flags & REDISMODULE_CTX_FLAGS_AOF)) FAIL("AOF Flag not set after config set"); 267 268 if (flags & REDISMODULE_CTX_FLAGS_RDB) FAIL("RDB Flag was set"); 269 /* Enable RDB to test RDB flags */ 270 RedisModule_Call(ctx, "config", "ccc", "set", "save", "900 1"); 271 flags = RedisModule_GetContextFlags(ctx); 272 if (!(flags & REDISMODULE_CTX_FLAGS_RDB)) FAIL("RDB Flag was not set after config set"); 273 274 if (!(flags & REDISMODULE_CTX_FLAGS_MASTER)) FAIL("Master flag was not set"); 275 if (flags & REDISMODULE_CTX_FLAGS_SLAVE) FAIL("Slave flag was set"); 276 if (flags & REDISMODULE_CTX_FLAGS_READONLY) FAIL("Read-only flag was set"); 277 if (flags & REDISMODULE_CTX_FLAGS_CLUSTER) FAIL("Cluster flag was set"); 278 279 if (flags & REDISMODULE_CTX_FLAGS_MAXMEMORY) FAIL("Maxmemory flag was set"); 280 281 RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "100000000"); 282 flags = RedisModule_GetContextFlags(ctx); 283 if (!(flags & REDISMODULE_CTX_FLAGS_MAXMEMORY)) 284 FAIL("Maxmemory flag was not set after config set"); 285 286 if (flags & REDISMODULE_CTX_FLAGS_EVICT) FAIL("Eviction flag was set"); 287 RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy", "allkeys-lru"); 288 flags = RedisModule_GetContextFlags(ctx); 289 if (!(flags & REDISMODULE_CTX_FLAGS_EVICT)) FAIL("Eviction flag was not set after config set"); 290 291 end: 292 /* Revert config changes */ 293 RedisModule_Call(ctx, "config", "ccc", "set", "appendonly", "no"); 294 RedisModule_Call(ctx, "config", "ccc", "set", "save", ""); 295 RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory", "0"); 296 RedisModule_Call(ctx, "config", "ccc", "set", "maxmemory-policy", "noeviction"); 297 298 if (!ok) { 299 RedisModule_Log(ctx, "warning", "Failed CTXFLAGS Test. Reason: %s", errString); 300 return RedisModule_ReplyWithSimpleString(ctx, "ERR"); 301 } 302 303 return RedisModule_ReplyWithSimpleString(ctx, "OK"); 304 } 305 306 /* ----------------------------- Test framework ----------------------------- */ 307 308 /* Return 1 if the reply matches the specified string, otherwise log errors 309 * in the server log and return 0. */ 310 int TestAssertStringReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, char *str, size_t len) { 311 RedisModuleString *mystr, *expected; 312 313 if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_STRING) { 314 RedisModule_Log(ctx,"warning","Unexpected reply type %d", 315 RedisModule_CallReplyType(reply)); 316 return 0; 317 } 318 mystr = RedisModule_CreateStringFromCallReply(reply); 319 expected = RedisModule_CreateString(ctx,str,len); 320 if (RedisModule_StringCompare(mystr,expected) != 0) { 321 const char *mystr_ptr = RedisModule_StringPtrLen(mystr,NULL); 322 const char *expected_ptr = RedisModule_StringPtrLen(expected,NULL); 323 RedisModule_Log(ctx,"warning", 324 "Unexpected string reply '%s' (instead of '%s')", 325 mystr_ptr, expected_ptr); 326 return 0; 327 } 328 return 1; 329 } 330 331 /* Return 1 if the reply matches the specified integer, otherwise log errors 332 * in the server log and return 0. */ 333 int TestAssertIntegerReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply, long long expected) { 334 if (RedisModule_CallReplyType(reply) != REDISMODULE_REPLY_INTEGER) { 335 RedisModule_Log(ctx,"warning","Unexpected reply type %d", 336 RedisModule_CallReplyType(reply)); 337 return 0; 338 } 339 long long val = RedisModule_CallReplyInteger(reply); 340 if (val != expected) { 341 RedisModule_Log(ctx,"warning", 342 "Unexpected integer reply '%lld' (instead of '%lld')", 343 val, expected); 344 return 0; 345 } 346 return 1; 347 } 348 349 #define T(name,...) \ 350 do { \ 351 RedisModule_Log(ctx,"warning","Testing %s", name); \ 352 reply = RedisModule_Call(ctx,name,__VA_ARGS__); \ 353 } while (0); 354 355 /* TEST.IT -- Run all the tests. */ 356 int TestIt(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 357 REDISMODULE_NOT_USED(argv); 358 REDISMODULE_NOT_USED(argc); 359 360 RedisModule_AutoMemory(ctx); 361 RedisModuleCallReply *reply; 362 363 /* Make sure the DB is empty before to proceed. */ 364 T("dbsize",""); 365 if (!TestAssertIntegerReply(ctx,reply,0)) goto fail; 366 367 T("ping",""); 368 if (!TestAssertStringReply(ctx,reply,"PONG",4)) goto fail; 369 370 T("test.call",""); 371 if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; 372 373 T("test.ctxflags",""); 374 if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; 375 376 T("test.string.append",""); 377 if (!TestAssertStringReply(ctx,reply,"foobar",6)) goto fail; 378 379 T("test.unlink",""); 380 if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; 381 382 T("test.string.append.am",""); 383 if (!TestAssertStringReply(ctx,reply,"foobar",6)) goto fail; 384 385 T("test.string.printf", "cc", "foo", "bar"); 386 if (!TestAssertStringReply(ctx,reply,"Got 3 args. argv[1]: foo, argv[2]: bar",38)) goto fail; 387 388 T("test.notify", ""); 389 if (!TestAssertStringReply(ctx,reply,"OK",2)) goto fail; 390 391 RedisModule_ReplyWithSimpleString(ctx,"ALL TESTS PASSED"); 392 return REDISMODULE_OK; 393 394 fail: 395 RedisModule_ReplyWithSimpleString(ctx, 396 "SOME TEST NOT PASSED! Check server logs"); 397 return REDISMODULE_OK; 398 } 399 400 int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 401 REDISMODULE_NOT_USED(argv); 402 REDISMODULE_NOT_USED(argc); 403 404 if (RedisModule_Init(ctx,"test",1,REDISMODULE_APIVER_1) 405 == REDISMODULE_ERR) return REDISMODULE_ERR; 406 407 if (RedisModule_CreateCommand(ctx,"test.call", 408 TestCall,"write deny-oom",1,1,1) == REDISMODULE_ERR) 409 return REDISMODULE_ERR; 410 411 if (RedisModule_CreateCommand(ctx,"test.string.append", 412 TestStringAppend,"write deny-oom",1,1,1) == REDISMODULE_ERR) 413 return REDISMODULE_ERR; 414 415 if (RedisModule_CreateCommand(ctx,"test.string.append.am", 416 TestStringAppendAM,"write deny-oom",1,1,1) == REDISMODULE_ERR) 417 return REDISMODULE_ERR; 418 419 if (RedisModule_CreateCommand(ctx,"test.string.printf", 420 TestStringPrintf,"write deny-oom",1,1,1) == REDISMODULE_ERR) 421 return REDISMODULE_ERR; 422 423 if (RedisModule_CreateCommand(ctx,"test.ctxflags", 424 TestCtxFlags,"readonly",1,1,1) == REDISMODULE_ERR) 425 return REDISMODULE_ERR; 426 427 if (RedisModule_CreateCommand(ctx,"test.unlink", 428 TestUnlink,"write deny-oom",1,1,1) == REDISMODULE_ERR) 429 return REDISMODULE_ERR; 430 431 if (RedisModule_CreateCommand(ctx,"test.it", 432 TestIt,"readonly",1,1,1) == REDISMODULE_ERR) 433 return REDISMODULE_ERR; 434 435 RedisModule_SubscribeToKeyspaceEvents(ctx, 436 REDISMODULE_NOTIFY_HASH | 437 REDISMODULE_NOTIFY_SET | 438 REDISMODULE_NOTIFY_STRING, 439 NotifyCallback); 440 if (RedisModule_CreateCommand(ctx,"test.notify", 441 TestNotifications,"write deny-oom",1,1,1) == REDISMODULE_ERR) 442 return REDISMODULE_ERR; 443 444 return REDISMODULE_OK; 445 } 446