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