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. */
TestMatchReply(RedisModuleCallReply * reply,char * str)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. */
TestCall(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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. */
TestStringAppend(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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. */
TestStringAppendAM(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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. */
TestStringPrintf(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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 
failTest(RedisModuleCtx * ctx,const char * msg)124 int failTest(RedisModuleCtx *ctx, const char *msg) {
125     RedisModule_ReplyWithError(ctx, msg);
126     return REDISMODULE_ERR;
127 }
128 
TestUnlink(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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 
NotifyCallback(RedisModuleCtx * ctx,int type,const char * event,RedisModuleString * key)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. */
TestNotifications(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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. */
TestCtxFlags(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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. */
TestAssertStringReply(RedisModuleCtx * ctx,RedisModuleCallReply * reply,char * str,size_t len)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. */
TestAssertIntegerReply(RedisModuleCtx * ctx,RedisModuleCallReply * reply,long long expected)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. */
TestIt(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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 
RedisModule_OnLoad(RedisModuleCtx * ctx,RedisModuleString ** argv,int argc)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