├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── module.c ├── redismodule.h ├── requirements.txt ├── rmtest ├── __init__.py ├── cluster.py ├── config.py └── disposableredis │ ├── __init__.py │ └── cluster.py ├── setup.py └── test.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/repo 5 | 6 | docker: 7 | - image: redislabsmodules/rmbuilder:latest 8 | 9 | steps: 10 | - checkout 11 | 12 | - run: 13 | name: Get toolchain 14 | command: | 15 | apt-get -y update 16 | apt-get -y install python3 python3-redis python3-setuptools pylint3 17 | 18 | - run: 19 | name: Run check 20 | command: | 21 | find rmtest -name \*.py | \ 22 | while read file; do echo $file; pylint3 $file; done 23 | 24 | - run: 25 | name: Run build 26 | command: python3 setup.py build 27 | 28 | # Currently we cannot run tests because we do not have a version of 29 | # the python3-redis package available with cluster support. 30 | # - run: 31 | # name: Run tests 32 | # command: python3 setup.py test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !/rmtest/**/ 3 | !/rmtest 4 | !**/*.py 5 | !.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2018, Redis Labs 2 | Portions Copyright (c) 2018, Red Hat 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rmtest 2 | 3 | A simple nano-framework for testing redis modules using python unit test, and a disposable ephemeral redis subprocess. 4 | 5 | [![CircleCI](https://circleci.com/gh/RedisLabs/rmtest.svg?style=svg)](https://circleci.com/gh/RedisLabs/rmtest) 6 | 7 | ## Example: 8 | 9 | ```py 10 | 11 | import unittest 12 | from rmtest import ModuleTestCase 13 | 14 | class MyTestCase(ModuleTestCase('../module.so', module_args = ('foo', 'bar'))): 15 | 16 | def testCmd(self): 17 | self.assertOk(self.cmd('mymodule.dosomething', 'foo', 'bar')) 18 | 19 | if __name__ == '__main__': 20 | unittest.main() 21 | 22 | ``` 23 | 24 | ## Controlling parameters with Environment Variables 25 | 26 | It is possible to control the path to the redis server executable, the path to the module and an optional fixed port, from environment variables. 27 | 28 | ### REDIS_MODULE_PATH 29 | 30 | Controls the path to the module, either absolute, or relative to where the test resides. 31 | 32 | ### REDIS_PATH 33 | 34 | Controls the path to redis-server. By default we assume it's in `$PATH` 35 | 36 | ### REDIS_PORT 37 | 38 | Causes the tests to run against a fixed redis port instead of spawning ephemeral disposable redis instances for each test. 39 | 40 | This is useful for debugging failures with `redis-cli MONITOR`. 41 | 42 | ## REDIS_DEBUGGER 43 | 44 | Causes the tests to be run under a debugger (e.g. `valgrind`). The value to this 45 | environment variable is the path to the debugger. Does not work if there are spaces 46 | in the path. 47 | 48 | 49 | ## Installing from pypi 50 | 51 | ```sh 52 | $ pip install rmtest 53 | ``` 54 | 55 | ## Installing from Git 56 | 57 | 1. Clone this repo 58 | 59 | 2. `sudo python setup.py install` 60 | -------------------------------------------------------------------------------- /module.c: -------------------------------------------------------------------------------- 1 | #include "redismodule.h" 2 | int TestCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 3 | 4 | return RedisModule_ReplyWithSimpleString(ctx, "OK"); 5 | } 6 | 7 | int ErrCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 8 | return RedisModule_ReplyWithError(ctx, "ERR"); 9 | } 10 | 11 | /* Registering the module */ 12 | int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { 13 | if (RedisModule_Init(ctx, "test", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) { 14 | return REDISMODULE_ERR; 15 | } 16 | if (RedisModule_CreateCommand(ctx, "test.test", TestCommand, "readonly", 0,0,0) == REDISMODULE_ERR) { 17 | return REDISMODULE_ERR; 18 | } 19 | if (RedisModule_CreateCommand(ctx, "test.error", ErrCommand, "readonly", 0,0,0) == REDISMODULE_ERR) { 20 | return REDISMODULE_ERR; 21 | } 22 | return REDISMODULE_OK; 23 | } -------------------------------------------------------------------------------- /redismodule.h: -------------------------------------------------------------------------------- 1 | #ifndef REDISMODULE_H 2 | #define REDISMODULE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | /* ---------------- Defines common between core and modules --------------- */ 9 | 10 | /* Error status return values. */ 11 | #define REDISMODULE_OK 0 12 | #define REDISMODULE_ERR 1 13 | 14 | /* API versions. */ 15 | #define REDISMODULE_APIVER_1 1 16 | 17 | /* API flags and constants */ 18 | #define REDISMODULE_READ (1<<0) 19 | #define REDISMODULE_WRITE (1<<1) 20 | 21 | #define REDISMODULE_LIST_HEAD 0 22 | #define REDISMODULE_LIST_TAIL 1 23 | 24 | /* Key types. */ 25 | #define REDISMODULE_KEYTYPE_EMPTY 0 26 | #define REDISMODULE_KEYTYPE_STRING 1 27 | #define REDISMODULE_KEYTYPE_LIST 2 28 | #define REDISMODULE_KEYTYPE_HASH 3 29 | #define REDISMODULE_KEYTYPE_SET 4 30 | #define REDISMODULE_KEYTYPE_ZSET 5 31 | #define REDISMODULE_KEYTYPE_MODULE 6 32 | 33 | /* Reply types. */ 34 | #define REDISMODULE_REPLY_UNKNOWN -1 35 | #define REDISMODULE_REPLY_STRING 0 36 | #define REDISMODULE_REPLY_ERROR 1 37 | #define REDISMODULE_REPLY_INTEGER 2 38 | #define REDISMODULE_REPLY_ARRAY 3 39 | #define REDISMODULE_REPLY_NULL 4 40 | 41 | /* Postponed array length. */ 42 | #define REDISMODULE_POSTPONED_ARRAY_LEN -1 43 | 44 | /* Expire */ 45 | #define REDISMODULE_NO_EXPIRE -1 46 | 47 | /* Sorted set API flags. */ 48 | #define REDISMODULE_ZADD_XX (1<<0) 49 | #define REDISMODULE_ZADD_NX (1<<1) 50 | #define REDISMODULE_ZADD_ADDED (1<<2) 51 | #define REDISMODULE_ZADD_UPDATED (1<<3) 52 | #define REDISMODULE_ZADD_NOP (1<<4) 53 | 54 | /* Hash API flags. */ 55 | #define REDISMODULE_HASH_NONE 0 56 | #define REDISMODULE_HASH_NX (1<<0) 57 | #define REDISMODULE_HASH_XX (1<<1) 58 | #define REDISMODULE_HASH_CFIELDS (1<<2) 59 | #define REDISMODULE_HASH_EXISTS (1<<3) 60 | 61 | /* A special pointer that we can use between the core and the module to signal 62 | * field deletion, and that is impossible to be a valid pointer. */ 63 | #define REDISMODULE_HASH_DELETE ((RedisModuleString*)(long)1) 64 | 65 | /* Error messages. */ 66 | #define REDISMODULE_ERRORMSG_WRONGTYPE "WRONGTYPE Operation against a key holding the wrong kind of value" 67 | 68 | #define REDISMODULE_POSITIVE_INFINITE (1.0/0.0) 69 | #define REDISMODULE_NEGATIVE_INFINITE (-1.0/0.0) 70 | 71 | #define REDISMODULE_NOT_USED(V) ((void) V) 72 | 73 | /* ------------------------- End of common defines ------------------------ */ 74 | 75 | #ifndef REDISMODULE_CORE 76 | 77 | typedef long long mstime_t; 78 | 79 | /* Incomplete structures for compiler checks but opaque access. */ 80 | typedef struct RedisModuleCtx RedisModuleCtx; 81 | typedef struct RedisModuleKey RedisModuleKey; 82 | typedef struct RedisModuleString RedisModuleString; 83 | typedef struct RedisModuleCallReply RedisModuleCallReply; 84 | typedef struct RedisModuleIO RedisModuleIO; 85 | typedef struct RedisModuleType RedisModuleType; 86 | typedef struct RedisModuleDigest RedisModuleDigest; 87 | typedef struct RedisModuleBlockedClient RedisModuleBlockedClient; 88 | 89 | typedef int (*RedisModuleCmdFunc) (RedisModuleCtx *ctx, RedisModuleString **argv, int argc); 90 | 91 | typedef void *(*RedisModuleTypeLoadFunc)(RedisModuleIO *rdb, int encver); 92 | typedef void (*RedisModuleTypeSaveFunc)(RedisModuleIO *rdb, void *value); 93 | typedef void (*RedisModuleTypeRewriteFunc)(RedisModuleIO *aof, RedisModuleString *key, void *value); 94 | typedef size_t (*RedisModuleTypeMemUsageFunc)(const void *value); 95 | typedef void (*RedisModuleTypeDigestFunc)(RedisModuleDigest *digest, void *value); 96 | typedef void (*RedisModuleTypeFreeFunc)(void *value); 97 | 98 | #define REDISMODULE_TYPE_METHOD_VERSION 1 99 | typedef struct RedisModuleTypeMethods { 100 | uint64_t version; 101 | RedisModuleTypeLoadFunc rdb_load; 102 | RedisModuleTypeSaveFunc rdb_save; 103 | RedisModuleTypeRewriteFunc aof_rewrite; 104 | RedisModuleTypeMemUsageFunc mem_usage; 105 | RedisModuleTypeDigestFunc digest; 106 | RedisModuleTypeFreeFunc free; 107 | } RedisModuleTypeMethods; 108 | 109 | #define REDISMODULE_GET_API(name) \ 110 | RedisModule_GetApi("RedisModule_" #name, ((void **)&RedisModule_ ## name)) 111 | 112 | #define REDISMODULE_API_FUNC(x) (*x) 113 | 114 | 115 | void *REDISMODULE_API_FUNC(RedisModule_Alloc)(size_t bytes); 116 | void *REDISMODULE_API_FUNC(RedisModule_Realloc)(void *ptr, size_t bytes); 117 | void REDISMODULE_API_FUNC(RedisModule_Free)(void *ptr); 118 | void *REDISMODULE_API_FUNC(RedisModule_Calloc)(size_t nmemb, size_t size); 119 | char *REDISMODULE_API_FUNC(RedisModule_Strdup)(const char *str); 120 | int REDISMODULE_API_FUNC(RedisModule_GetApi)(const char *, void *); 121 | int REDISMODULE_API_FUNC(RedisModule_CreateCommand)(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep); 122 | int REDISMODULE_API_FUNC(RedisModule_SetModuleAttribs)(RedisModuleCtx *ctx, const char *name, int ver, int apiver); 123 | int REDISMODULE_API_FUNC(RedisModule_WrongArity)(RedisModuleCtx *ctx); 124 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithLongLong)(RedisModuleCtx *ctx, long long ll); 125 | int REDISMODULE_API_FUNC(RedisModule_GetSelectedDb)(RedisModuleCtx *ctx); 126 | int REDISMODULE_API_FUNC(RedisModule_SelectDb)(RedisModuleCtx *ctx, int newid); 127 | void *REDISMODULE_API_FUNC(RedisModule_OpenKey)(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode); 128 | void REDISMODULE_API_FUNC(RedisModule_CloseKey)(RedisModuleKey *kp); 129 | int REDISMODULE_API_FUNC(RedisModule_KeyType)(RedisModuleKey *kp); 130 | size_t REDISMODULE_API_FUNC(RedisModule_ValueLength)(RedisModuleKey *kp); 131 | int REDISMODULE_API_FUNC(RedisModule_ListPush)(RedisModuleKey *kp, int where, RedisModuleString *ele); 132 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_ListPop)(RedisModuleKey *key, int where); 133 | RedisModuleCallReply *REDISMODULE_API_FUNC(RedisModule_Call)(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...); 134 | const char *REDISMODULE_API_FUNC(RedisModule_CallReplyProto)(RedisModuleCallReply *reply, size_t *len); 135 | void REDISMODULE_API_FUNC(RedisModule_FreeCallReply)(RedisModuleCallReply *reply); 136 | int REDISMODULE_API_FUNC(RedisModule_CallReplyType)(RedisModuleCallReply *reply); 137 | long long REDISMODULE_API_FUNC(RedisModule_CallReplyInteger)(RedisModuleCallReply *reply); 138 | size_t REDISMODULE_API_FUNC(RedisModule_CallReplyLength)(RedisModuleCallReply *reply); 139 | RedisModuleCallReply *REDISMODULE_API_FUNC(RedisModule_CallReplyArrayElement)(RedisModuleCallReply *reply, size_t idx); 140 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateString)(RedisModuleCtx *ctx, const char *ptr, size_t len); 141 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromLongLong)(RedisModuleCtx *ctx, long long ll); 142 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromString)(RedisModuleCtx *ctx, const RedisModuleString *str); 143 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringPrintf)(RedisModuleCtx *ctx, const char *fmt, ...); 144 | void REDISMODULE_API_FUNC(RedisModule_FreeString)(RedisModuleCtx *ctx, RedisModuleString *str); 145 | const char *REDISMODULE_API_FUNC(RedisModule_StringPtrLen)(const RedisModuleString *str, size_t *len); 146 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithError)(RedisModuleCtx *ctx, const char *err); 147 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithSimpleString)(RedisModuleCtx *ctx, const char *msg); 148 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithArray)(RedisModuleCtx *ctx, long len); 149 | void REDISMODULE_API_FUNC(RedisModule_ReplySetArrayLength)(RedisModuleCtx *ctx, long len); 150 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithStringBuffer)(RedisModuleCtx *ctx, const char *buf, size_t len); 151 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithString)(RedisModuleCtx *ctx, RedisModuleString *str); 152 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithNull)(RedisModuleCtx *ctx); 153 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithDouble)(RedisModuleCtx *ctx, double d); 154 | int REDISMODULE_API_FUNC(RedisModule_ReplyWithCallReply)(RedisModuleCtx *ctx, RedisModuleCallReply *reply); 155 | int REDISMODULE_API_FUNC(RedisModule_StringToLongLong)(const RedisModuleString *str, long long *ll); 156 | int REDISMODULE_API_FUNC(RedisModule_StringToDouble)(const RedisModuleString *str, double *d); 157 | void REDISMODULE_API_FUNC(RedisModule_AutoMemory)(RedisModuleCtx *ctx); 158 | int REDISMODULE_API_FUNC(RedisModule_Replicate)(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...); 159 | int REDISMODULE_API_FUNC(RedisModule_ReplicateVerbatim)(RedisModuleCtx *ctx); 160 | const char *REDISMODULE_API_FUNC(RedisModule_CallReplyStringPtr)(RedisModuleCallReply *reply, size_t *len); 161 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_CreateStringFromCallReply)(RedisModuleCallReply *reply); 162 | int REDISMODULE_API_FUNC(RedisModule_DeleteKey)(RedisModuleKey *key); 163 | int REDISMODULE_API_FUNC(RedisModule_StringSet)(RedisModuleKey *key, RedisModuleString *str); 164 | char *REDISMODULE_API_FUNC(RedisModule_StringDMA)(RedisModuleKey *key, size_t *len, int mode); 165 | int REDISMODULE_API_FUNC(RedisModule_StringTruncate)(RedisModuleKey *key, size_t newlen); 166 | mstime_t REDISMODULE_API_FUNC(RedisModule_GetExpire)(RedisModuleKey *key); 167 | int REDISMODULE_API_FUNC(RedisModule_SetExpire)(RedisModuleKey *key, mstime_t expire); 168 | int REDISMODULE_API_FUNC(RedisModule_ZsetAdd)(RedisModuleKey *key, double score, RedisModuleString *ele, int *flagsptr); 169 | int REDISMODULE_API_FUNC(RedisModule_ZsetIncrby)(RedisModuleKey *key, double score, RedisModuleString *ele, int *flagsptr, double *newscore); 170 | int REDISMODULE_API_FUNC(RedisModule_ZsetScore)(RedisModuleKey *key, RedisModuleString *ele, double *score); 171 | int REDISMODULE_API_FUNC(RedisModule_ZsetRem)(RedisModuleKey *key, RedisModuleString *ele, int *deleted); 172 | void REDISMODULE_API_FUNC(RedisModule_ZsetRangeStop)(RedisModuleKey *key); 173 | int REDISMODULE_API_FUNC(RedisModule_ZsetFirstInScoreRange)(RedisModuleKey *key, double min, double max, int minex, int maxex); 174 | int REDISMODULE_API_FUNC(RedisModule_ZsetLastInScoreRange)(RedisModuleKey *key, double min, double max, int minex, int maxex); 175 | int REDISMODULE_API_FUNC(RedisModule_ZsetFirstInLexRange)(RedisModuleKey *key, RedisModuleString *min, RedisModuleString *max); 176 | int REDISMODULE_API_FUNC(RedisModule_ZsetLastInLexRange)(RedisModuleKey *key, RedisModuleString *min, RedisModuleString *max); 177 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_ZsetRangeCurrentElement)(RedisModuleKey *key, double *score); 178 | int REDISMODULE_API_FUNC(RedisModule_ZsetRangeNext)(RedisModuleKey *key); 179 | int REDISMODULE_API_FUNC(RedisModule_ZsetRangePrev)(RedisModuleKey *key); 180 | int REDISMODULE_API_FUNC(RedisModule_ZsetRangeEndReached)(RedisModuleKey *key); 181 | int REDISMODULE_API_FUNC(RedisModule_HashSet)(RedisModuleKey *key, int flags, ...); 182 | int REDISMODULE_API_FUNC(RedisModule_HashGet)(RedisModuleKey *key, int flags, ...); 183 | int REDISMODULE_API_FUNC(RedisModule_IsKeysPositionRequest)(RedisModuleCtx *ctx); 184 | void REDISMODULE_API_FUNC(RedisModule_KeyAtPos)(RedisModuleCtx *ctx, int pos); 185 | unsigned long long REDISMODULE_API_FUNC(RedisModule_GetClientId)(RedisModuleCtx *ctx); 186 | void *REDISMODULE_API_FUNC(RedisModule_PoolAlloc)(RedisModuleCtx *ctx, size_t bytes); 187 | RedisModuleType *REDISMODULE_API_FUNC(RedisModule_CreateDataType)(RedisModuleCtx *ctx, const char *name, int encver, RedisModuleTypeMethods *typemethods); 188 | int REDISMODULE_API_FUNC(RedisModule_ModuleTypeSetValue)(RedisModuleKey *key, RedisModuleType *mt, void *value); 189 | RedisModuleType *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetType)(RedisModuleKey *key); 190 | void *REDISMODULE_API_FUNC(RedisModule_ModuleTypeGetValue)(RedisModuleKey *key); 191 | void REDISMODULE_API_FUNC(RedisModule_SaveUnsigned)(RedisModuleIO *io, uint64_t value); 192 | uint64_t REDISMODULE_API_FUNC(RedisModule_LoadUnsigned)(RedisModuleIO *io); 193 | void REDISMODULE_API_FUNC(RedisModule_SaveSigned)(RedisModuleIO *io, int64_t value); 194 | int64_t REDISMODULE_API_FUNC(RedisModule_LoadSigned)(RedisModuleIO *io); 195 | void REDISMODULE_API_FUNC(RedisModule_EmitAOF)(RedisModuleIO *io, const char *cmdname, const char *fmt, ...); 196 | void REDISMODULE_API_FUNC(RedisModule_SaveString)(RedisModuleIO *io, RedisModuleString *s); 197 | void REDISMODULE_API_FUNC(RedisModule_SaveStringBuffer)(RedisModuleIO *io, const char *str, size_t len); 198 | RedisModuleString *REDISMODULE_API_FUNC(RedisModule_LoadString)(RedisModuleIO *io); 199 | char *REDISMODULE_API_FUNC(RedisModule_LoadStringBuffer)(RedisModuleIO *io, size_t *lenptr); 200 | void REDISMODULE_API_FUNC(RedisModule_SaveDouble)(RedisModuleIO *io, double value); 201 | double REDISMODULE_API_FUNC(RedisModule_LoadDouble)(RedisModuleIO *io); 202 | void REDISMODULE_API_FUNC(RedisModule_SaveFloat)(RedisModuleIO *io, float value); 203 | float REDISMODULE_API_FUNC(RedisModule_LoadFloat)(RedisModuleIO *io); 204 | void REDISMODULE_API_FUNC(RedisModule_Log)(RedisModuleCtx *ctx, const char *level, const char *fmt, ...); 205 | void REDISMODULE_API_FUNC(RedisModule_LogIOError)(RedisModuleIO *io, const char *levelstr, const char *fmt, ...); 206 | int REDISMODULE_API_FUNC(RedisModule_StringAppendBuffer)(RedisModuleCtx *ctx, RedisModuleString *str, const char *buf, size_t len); 207 | void REDISMODULE_API_FUNC(RedisModule_RetainString)(RedisModuleCtx *ctx, RedisModuleString *str); 208 | int REDISMODULE_API_FUNC(RedisModule_StringCompare)(RedisModuleString *a, RedisModuleString *b); 209 | RedisModuleCtx *REDISMODULE_API_FUNC(RedisModule_GetContextFromIO)(RedisModuleIO *io); 210 | RedisModuleBlockedClient *REDISMODULE_API_FUNC(RedisModule_BlockClient)(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback, RedisModuleCmdFunc timeout_callback, void (*free_privdata)(void*), long long timeout_ms); 211 | int REDISMODULE_API_FUNC(RedisModule_UnblockClient)(RedisModuleBlockedClient *bc, void *privdata); 212 | int REDISMODULE_API_FUNC(RedisModule_IsBlockedReplyRequest)(RedisModuleCtx *ctx); 213 | int REDISMODULE_API_FUNC(RedisModule_IsBlockedTimeoutRequest)(RedisModuleCtx *ctx); 214 | void *REDISMODULE_API_FUNC(RedisModule_GetBlockedClientPrivateData)(RedisModuleCtx *ctx); 215 | int REDISMODULE_API_FUNC(RedisModule_AbortBlock)(RedisModuleBlockedClient *bc); 216 | long long REDISMODULE_API_FUNC(RedisModule_Milliseconds)(void); 217 | 218 | /* This is included inline inside each Redis module. */ 219 | static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) __attribute__((unused)); 220 | static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver) { 221 | void *getapifuncptr = ((void**)ctx)[0]; 222 | RedisModule_GetApi = (int (*)(const char *, void *)) (unsigned long)getapifuncptr; 223 | REDISMODULE_GET_API(Alloc); 224 | REDISMODULE_GET_API(Calloc); 225 | REDISMODULE_GET_API(Free); 226 | REDISMODULE_GET_API(Realloc); 227 | REDISMODULE_GET_API(Strdup); 228 | REDISMODULE_GET_API(CreateCommand); 229 | REDISMODULE_GET_API(SetModuleAttribs); 230 | REDISMODULE_GET_API(WrongArity); 231 | REDISMODULE_GET_API(ReplyWithLongLong); 232 | REDISMODULE_GET_API(ReplyWithError); 233 | REDISMODULE_GET_API(ReplyWithSimpleString); 234 | REDISMODULE_GET_API(ReplyWithArray); 235 | REDISMODULE_GET_API(ReplySetArrayLength); 236 | REDISMODULE_GET_API(ReplyWithStringBuffer); 237 | REDISMODULE_GET_API(ReplyWithString); 238 | REDISMODULE_GET_API(ReplyWithNull); 239 | REDISMODULE_GET_API(ReplyWithCallReply); 240 | REDISMODULE_GET_API(ReplyWithDouble); 241 | REDISMODULE_GET_API(ReplySetArrayLength); 242 | REDISMODULE_GET_API(GetSelectedDb); 243 | REDISMODULE_GET_API(SelectDb); 244 | REDISMODULE_GET_API(OpenKey); 245 | REDISMODULE_GET_API(CloseKey); 246 | REDISMODULE_GET_API(KeyType); 247 | REDISMODULE_GET_API(ValueLength); 248 | REDISMODULE_GET_API(ListPush); 249 | REDISMODULE_GET_API(ListPop); 250 | REDISMODULE_GET_API(StringToLongLong); 251 | REDISMODULE_GET_API(StringToDouble); 252 | REDISMODULE_GET_API(Call); 253 | REDISMODULE_GET_API(CallReplyProto); 254 | REDISMODULE_GET_API(FreeCallReply); 255 | REDISMODULE_GET_API(CallReplyInteger); 256 | REDISMODULE_GET_API(CallReplyType); 257 | REDISMODULE_GET_API(CallReplyLength); 258 | REDISMODULE_GET_API(CallReplyArrayElement); 259 | REDISMODULE_GET_API(CallReplyStringPtr); 260 | REDISMODULE_GET_API(CreateStringFromCallReply); 261 | REDISMODULE_GET_API(CreateString); 262 | REDISMODULE_GET_API(CreateStringFromLongLong); 263 | REDISMODULE_GET_API(CreateStringFromString); 264 | REDISMODULE_GET_API(CreateStringPrintf); 265 | REDISMODULE_GET_API(FreeString); 266 | REDISMODULE_GET_API(StringPtrLen); 267 | REDISMODULE_GET_API(AutoMemory); 268 | REDISMODULE_GET_API(Replicate); 269 | REDISMODULE_GET_API(ReplicateVerbatim); 270 | REDISMODULE_GET_API(DeleteKey); 271 | REDISMODULE_GET_API(StringSet); 272 | REDISMODULE_GET_API(StringDMA); 273 | REDISMODULE_GET_API(StringTruncate); 274 | REDISMODULE_GET_API(GetExpire); 275 | REDISMODULE_GET_API(SetExpire); 276 | REDISMODULE_GET_API(ZsetAdd); 277 | REDISMODULE_GET_API(ZsetIncrby); 278 | REDISMODULE_GET_API(ZsetScore); 279 | REDISMODULE_GET_API(ZsetRem); 280 | REDISMODULE_GET_API(ZsetRangeStop); 281 | REDISMODULE_GET_API(ZsetFirstInScoreRange); 282 | REDISMODULE_GET_API(ZsetLastInScoreRange); 283 | REDISMODULE_GET_API(ZsetFirstInLexRange); 284 | REDISMODULE_GET_API(ZsetLastInLexRange); 285 | REDISMODULE_GET_API(ZsetRangeCurrentElement); 286 | REDISMODULE_GET_API(ZsetRangeNext); 287 | REDISMODULE_GET_API(ZsetRangePrev); 288 | REDISMODULE_GET_API(ZsetRangeEndReached); 289 | REDISMODULE_GET_API(HashSet); 290 | REDISMODULE_GET_API(HashGet); 291 | REDISMODULE_GET_API(IsKeysPositionRequest); 292 | REDISMODULE_GET_API(KeyAtPos); 293 | REDISMODULE_GET_API(GetClientId); 294 | REDISMODULE_GET_API(PoolAlloc); 295 | REDISMODULE_GET_API(CreateDataType); 296 | REDISMODULE_GET_API(ModuleTypeSetValue); 297 | REDISMODULE_GET_API(ModuleTypeGetType); 298 | REDISMODULE_GET_API(ModuleTypeGetValue); 299 | REDISMODULE_GET_API(SaveUnsigned); 300 | REDISMODULE_GET_API(LoadUnsigned); 301 | REDISMODULE_GET_API(SaveSigned); 302 | REDISMODULE_GET_API(LoadSigned); 303 | REDISMODULE_GET_API(SaveString); 304 | REDISMODULE_GET_API(SaveStringBuffer); 305 | REDISMODULE_GET_API(LoadString); 306 | REDISMODULE_GET_API(LoadStringBuffer); 307 | REDISMODULE_GET_API(SaveDouble); 308 | REDISMODULE_GET_API(LoadDouble); 309 | REDISMODULE_GET_API(SaveFloat); 310 | REDISMODULE_GET_API(LoadFloat); 311 | REDISMODULE_GET_API(EmitAOF); 312 | REDISMODULE_GET_API(Log); 313 | REDISMODULE_GET_API(LogIOError); 314 | REDISMODULE_GET_API(StringAppendBuffer); 315 | REDISMODULE_GET_API(RetainString); 316 | REDISMODULE_GET_API(StringCompare); 317 | REDISMODULE_GET_API(GetContextFromIO); 318 | REDISMODULE_GET_API(BlockClient); 319 | REDISMODULE_GET_API(UnblockClient); 320 | REDISMODULE_GET_API(IsBlockedReplyRequest); 321 | REDISMODULE_GET_API(IsBlockedTimeoutRequest); 322 | REDISMODULE_GET_API(GetBlockedClientPrivateData); 323 | REDISMODULE_GET_API(AbortBlock); 324 | REDISMODULE_GET_API(Milliseconds); 325 | 326 | RedisModule_SetModuleAttribs(ctx,name,ver,apiver); 327 | return REDISMODULE_OK; 328 | } 329 | 330 | #else 331 | 332 | /* Things only defined for the modules core, not exported to modules 333 | * including this file. */ 334 | #define RedisModuleString robj 335 | 336 | #endif /* REDISMODULE_CORE */ 337 | #endif /* REDISMOUDLE_H */ 338 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | redis 2 | -------------------------------------------------------------------------------- /rmtest/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name, duplicate-code, attribute-defined-outside-init 2 | 3 | import unittest 4 | import os 5 | import contextlib 6 | from redis import ResponseError 7 | 8 | from rmtest.disposableredis import DisposableRedis 9 | from rmtest import config 10 | 11 | REDIS_MODULE_PATH_ENVVAR = 'REDIS_MODULE_PATH' 12 | REDIS_PATH_ENVVAR = 'REDIS_PATH' 13 | REDIS_PORT_ENVVAR = 'REDIS_PORT' 14 | 15 | 16 | class BaseModuleTestCase(unittest.TestCase): 17 | """ 18 | You can inherit from this base class directly. The server, port, and module 19 | settings can be defined either directly via the config module (see the 20 | config.py file), or via the rmtest.config file in the current directoy (i.e. 21 | of the process, not the file), or via environment variables. 22 | """ 23 | 24 | def tearDown(self): 25 | if hasattr(self, '_server'): 26 | self._server.stop() 27 | self._server = None 28 | self._client = None 29 | 30 | super(BaseModuleTestCase, self).tearDown() 31 | 32 | @property 33 | def server(self): 34 | self._ensure_server() 35 | return self._server 36 | 37 | @property 38 | def client(self): 39 | self._ensure_server() 40 | return self._client 41 | 42 | def spawn_server(self, **kwargs): 43 | if hasattr(self, '_server'): 44 | raise Exception('Server already spawned!') 45 | self._ensure_server(**kwargs) 46 | 47 | def restart_and_reload(self): 48 | self._server.dump_and_reload(restart_process=True) 49 | self._client = self._server.client() 50 | 51 | def _ensure_server(self, **kwargs): 52 | if getattr(self, '_server', None): 53 | return 54 | self._server = self.redis(**kwargs) 55 | self._server.start() 56 | self._client = self._server.client() 57 | 58 | @property 59 | def module_args(self): 60 | """ 61 | Module-specific arguments required 62 | """ 63 | return [] 64 | 65 | @property 66 | def server_args(self): 67 | """ 68 | Server-specific arguments required 69 | """ 70 | return {} 71 | 72 | @property 73 | def is_external_server(self): 74 | """ 75 | :return: True if the connected-to server is already launched 76 | """ 77 | return config.REDIS_PORT 78 | 79 | def redis(self, **redis_args): 80 | """ 81 | Return a connection to a server, creating one or connecting to an 82 | existing server. 83 | """ 84 | if not config.REDIS_MODULE: 85 | raise Exception('No module specified. Use config file or environment!') 86 | redis_args.update(self.server_args) 87 | redis_args.update( 88 | {'loadmodule': [config.REDIS_MODULE] + self.module_args}) 89 | return DisposableRedis(port=config.REDIS_PORT, path=config.REDIS_BINARY, **redis_args) 90 | 91 | def cmd(self, *args, **kwargs): 92 | return self.client.execute_command(*args, **kwargs) 93 | 94 | def assertOk(self, oks, msg=None): 95 | if isinstance(oks, (bytes, bytearray)): 96 | self.assertEqual(b"OK", oks, msg) 97 | else: 98 | self.assertEqual("OK", oks, msg) 99 | 100 | def assertCmdOk(self, cmd, *args, **kwargs): 101 | self.assertOk(self.cmd(cmd, *args, **kwargs)) 102 | 103 | def assertExists(self, r, key, msg=None): 104 | self.assertTrue(r.exists(key), msg) 105 | 106 | def assertNotExists(self, r, key, msg=None): 107 | self.assertFalse(r.exists(key), msg) 108 | 109 | def assertInitArgsFail(self): 110 | try: 111 | c, s = self.client, self.server 112 | except Exception: 113 | delattr(self, '_server') 114 | self.assertOk('OK') 115 | else: 116 | self.assertOk('NotOK') 117 | 118 | def assertInitArgsSuccess(self): 119 | c, s = self.client, self.server 120 | self.assertEqual(True, self.cmd('PING')) 121 | 122 | def retry_with_reload(self): 123 | return self.client.retry_with_rdb_reload() 124 | 125 | @contextlib.contextmanager 126 | def assertResponseError(self, msg=None): 127 | """ 128 | Assert that a context block with a redis command triggers a redis error response. 129 | 130 | For Example: 131 | 132 | with self.assertResponseError(): 133 | r.execute_command('non_existing_command') 134 | """ 135 | 136 | try: 137 | yield 138 | except ResponseError: 139 | pass 140 | else: 141 | self.fail("Expected redis ResponseError " + (msg or '')) 142 | 143 | 144 | def ModuleTestCase(module_path, redis_path='redis-server', module_args=None): 145 | """ 146 | DEPRECATED. Use base class directly. 147 | 148 | Inherit your test class from the class generated by calling this function 149 | module_path is where your module.so resides, override it with REDIS_MODULE_PATH in env 150 | redis_path is the executable's path, override it with REDIS_PATH in env 151 | redis_port is an optional port for an already running redis 152 | module_args is an optional tuple or list of arguments to pass to the module on loading 153 | """ 154 | 155 | module_path = config.REDIS_MODULE if config.REDIS_MODULE else module_path 156 | redis_path = config.REDIS_BINARY if config.REDIS_BINARY else redis_path 157 | fixed_port = config.REDIS_PORT if config.REDIS_PORT else None 158 | port = fixed_port if fixed_port else None 159 | 160 | # If we have module args, create a list of arguments 161 | loadmodule_args = module_path \ 162 | if not module_args else [module_path] + list(module_args) 163 | 164 | class _ModuleTestCase(BaseModuleTestCase): 165 | _loadmodule_args = loadmodule_args 166 | 167 | @property 168 | def module_args(self): 169 | args = super(_ModuleTestCase, self).module_args 170 | if module_args: 171 | args += module_args 172 | 173 | def redis(self, **kwargs): 174 | return DisposableRedis(port=port, path=redis_path, 175 | loadmodule=self._loadmodule_args, **kwargs) 176 | 177 | return _ModuleTestCase 178 | -------------------------------------------------------------------------------- /rmtest/cluster.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=line-too-long, missing-docstring, invalid-name, duplicate-code 2 | 3 | import os 4 | import contextlib 5 | import unittest 6 | from redis import Redis, ConnectionPool, ResponseError 7 | from .disposableredis.cluster import Cluster 8 | 9 | REDIS_MODULE_PATH_ENVVAR = 'REDIS_MODULE_PATH' 10 | REDIS_PATH_ENVVAR = 'REDIS_PATH' 11 | REDIS_PORT_ENVVAR = 'REDIS_PORT' 12 | 13 | def ClusterModuleTestCase(module_path, num_nodes=3, redis_path='redis-server', fixed_port=None, module_args=tuple()): 14 | """ 15 | Inherit your test class from the class generated by calling this function 16 | module_path is where your module.so resides, override it with REDIS_MODULE_PATH in env 17 | redis_path is the executable's path, override it with REDIS_PATH in env 18 | redis_port is an optional port for an already running redis 19 | module_args is an optional tuple or list of arguments to pass to the module on loading 20 | """ 21 | 22 | module_path = os.getenv(REDIS_MODULE_PATH_ENVVAR, module_path) 23 | redis_path = os.getenv(REDIS_PATH_ENVVAR, redis_path) 24 | fixed_port = os.getenv(REDIS_PORT_ENVVAR, fixed_port) 25 | 26 | # If we have module args, create a list of arguments 27 | loadmodule_args = module_path if not module_args else [module_path] + list(module_args) 28 | 29 | class _ModuleTestCase(unittest.TestCase): 30 | 31 | 32 | @classmethod 33 | def setUpClass(cls): 34 | if fixed_port: 35 | cls._cluster = None 36 | cls._client = Redis(port=fixed_port, connection_pool=ConnectionPool(port=fixed_port)) 37 | else: 38 | cls._cluster = Cluster(num_nodes, path=redis_path, loadmodule=loadmodule_args) 39 | cls._ports = cls._cluster.start() 40 | cls._client = cls._cluster.nodes[0].client() 41 | 42 | @classmethod 43 | def tearDownClass(cls): 44 | if cls._cluster: 45 | cls._cluster.stop() 46 | 47 | 48 | def client(self): 49 | return self._client 50 | 51 | 52 | def client_for_key(self, key): 53 | if not self._cluster: 54 | return self._client 55 | return self._cluster.client_for_key(key) 56 | 57 | def key_cmd(self, cmd, key, *args, **kwargs): 58 | """ 59 | Execute a command where the key needs to be known 60 | """ 61 | conn = self.client_for_key(key) 62 | return conn.execute_command(cmd, key, *args, **kwargs) 63 | 64 | def cmd(self, *args, **kwargs): 65 | """ 66 | Execute a non-sharded command without selecting the right client 67 | """ 68 | return self._client.execute_command(*args, **kwargs) 69 | 70 | def assertOk(self, okstr, msg=None): 71 | if isinstance(okstr, (bytes, bytearray)): 72 | self.assertEqual(b"OK", okstr, msg) 73 | else: 74 | self.assertEqual("OK", okstr, msg) 75 | 76 | def assertCmdOk(self, cmd, *args, **kwargs): 77 | self.assertOk(self.cmd(cmd, *args, **kwargs)) 78 | 79 | def assertExists(self, key, msg=None): 80 | conn = self.client_for_key(key) 81 | self.assertTrue(conn.exists(key), msg) 82 | 83 | def assertNotExists(self, key, msg=None): 84 | conn = self.client_for_key(key) 85 | self.assertFalse(conn.exists(key), msg) 86 | 87 | 88 | def retry_with_rdb_reload(self): 89 | """ 90 | Send DEBUG RELOAD to all nodes and test the result 91 | """ 92 | yield 1 93 | if self._cluster: 94 | self._cluster.broadcast('DEBUG', 'RELOAD') 95 | else: 96 | self._client.execute_command('DEBUG', 'RELOAD') 97 | yield 2 98 | 99 | 100 | @contextlib.contextmanager 101 | def assertResponseError(self, msg=None): 102 | """ 103 | Assert that a context block with a redis command triggers a redis error response. 104 | 105 | For Example: 106 | 107 | with self.assertResponseError(): 108 | r.execute_command('non_existing_command') 109 | """ 110 | 111 | try: 112 | yield 113 | except ResponseError: 114 | pass 115 | else: 116 | self.fail("Expected redis ResponseError " + (msg or '')) 117 | 118 | 119 | return _ModuleTestCase 120 | -------------------------------------------------------------------------------- /rmtest/config.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name, too-few-public-methods 2 | 3 | """ 4 | Configuration file for determining port, path, etc. of Redis. This is used 5 | by all loaded tests. 6 | 7 | This file exports three variables, REDIS_BINARY which contains the executable 8 | path for Redis; REDIS_MODULE which contains the path to the module to test, 9 | and REDIS_PORT which connects to an already-existent redis server. 10 | 11 | The `REDIS_PATH`, `REDIS_MODULE_PATH`, and `REDIS_PORT` environment variables 12 | can all be used to override these settings. 13 | """ 14 | 15 | import os 16 | try: 17 | from configparser import ConfigParser, NoOptionError, NoSectionError 18 | except ImportError: 19 | from ConfigParser import ConfigParser, NoOptionError, NoSectionError 20 | 21 | 22 | class ConfigVar(object): 23 | def __init__(self, env, prop, default=None): 24 | self.env = env 25 | self.prop = prop 26 | self.default = default 27 | self.value = default 28 | 29 | 30 | cfg = ConfigParser() 31 | cfg.read(['rmtest.config']) 32 | 33 | entries = { 34 | 'path': ConfigVar('REDIS_PATH', 'executable', 'redis-server'), 35 | 'module': ConfigVar('REDIS_MODULE_PATH', 'module'), 36 | 'port': ConfigVar('REDIS_PORT', 'existing_port') 37 | } 38 | 39 | for _, ent in entries.items(): 40 | try: 41 | ent.value = cfg.get('server', ent.prop) 42 | except (NoOptionError, NoSectionError): 43 | pass 44 | 45 | # Override from environment 46 | if ent.env in os.environ: 47 | ent.value = os.environ[ent.env] 48 | 49 | 50 | REDIS_BINARY = entries['path'].value 51 | REDIS_MODULE = entries['module'].value 52 | REDIS_PORT = entries['port'].value 53 | if REDIS_PORT: 54 | REDIS_PORT = int(REDIS_PORT) 55 | -------------------------------------------------------------------------------- /rmtest/disposableredis/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name, broad-except, too-many-instance-attributes 2 | 3 | import subprocess 4 | import socket 5 | import time 6 | import os 7 | import os.path 8 | import sys 9 | import warnings 10 | import random 11 | import redis 12 | 13 | REDIS_DEBUGGER = os.environ.get('REDIS_DEBUGGER', None) 14 | REDIS_SHOW_OUTPUT = int(os.environ.get( 15 | 'REDIS_VERBOSE', 1 if REDIS_DEBUGGER else 0)) 16 | 17 | 18 | def get_random_port(): 19 | 20 | while True: 21 | port = random.randrange(1025, 10000) 22 | sock = socket.socket() 23 | try: 24 | sock.listen(port) 25 | except Exception: 26 | continue 27 | #_, port = sock.getsockname() 28 | sock.close() 29 | return port 30 | 31 | 32 | class Client(redis.StrictRedis): 33 | 34 | def __init__(self, disposable_redis, port): 35 | redis.StrictRedis.__init__(self, port=port, decode_responses=True) 36 | self.dr = disposable_redis 37 | 38 | def retry_with_rdb_reload(self): 39 | yield 1 40 | self.dr.dump_and_reload() 41 | yield 2 42 | 43 | 44 | class DisposableRedis(object): 45 | 46 | def __init__(self, port=None, path='redis-server', **extra_args): 47 | """ 48 | :param port: port number to start the redis server on. 49 | Specify none to automatically generate 50 | :type port: int|None 51 | :param extra_args: any extra arguments kwargs will 52 | be passed to redis server as --key val 53 | """ 54 | self._port = port 55 | 56 | # this will hold the actual port the redis is listening on. 57 | # It's equal to `_port` unless `_port` is None 58 | # in that case `port` is randomly generated 59 | self.port = None 60 | self._is_external = True if port else False 61 | self.use_aof = extra_args.pop('use_aof', False) 62 | self.args = [] 63 | self.extra_args = [] 64 | for k, v in extra_args.items(): 65 | self.extra_args.append('--%s' % k) 66 | if isinstance(v, (list, tuple)): 67 | self.extra_args += list(v) 68 | else: 69 | self.extra_args.append(v) 70 | 71 | self.path = path 72 | self.errored = False 73 | self.dumpfile = None 74 | self.aoffile = None 75 | self.pollfile = None 76 | self.process = None 77 | 78 | def force_start(self): 79 | self._is_external = False 80 | 81 | def _get_output(self): 82 | if not self.process: 83 | return '' 84 | return '' if REDIS_SHOW_OUTPUT else self.process.stdout.read() 85 | 86 | def _start_process(self): 87 | if self._is_external: 88 | return 89 | 90 | if REDIS_DEBUGGER: 91 | debugger = REDIS_DEBUGGER.split() 92 | args = debugger + self.args 93 | else: 94 | args = self.args 95 | stdout = None if REDIS_SHOW_OUTPUT else subprocess.PIPE 96 | if REDIS_SHOW_OUTPUT: 97 | sys.stderr.write("Executing: {}".format(repr(args))) 98 | self.process = subprocess.Popen( 99 | args, 100 | stdin=sys.stdin, 101 | stdout=stdout, 102 | stderr=sys.stderr, 103 | ) 104 | 105 | begin = time.time() 106 | while True: 107 | try: 108 | self.client().ping() 109 | break 110 | except (redis.ConnectionError, redis.ResponseError): 111 | self.process.poll() 112 | if self.process.returncode is not None: 113 | raise RuntimeError( 114 | "Process has exited with code {}\n. Redis output: {}" 115 | .format(self.process.returncode, self._get_output())) 116 | 117 | if time.time() - begin > 300: 118 | raise RuntimeError( 119 | 'Cannot initialize client (waited 5mins)') 120 | 121 | time.sleep(0.1) 122 | 123 | def start(self): 124 | """ 125 | Start the server. To stop the server you should call stop() 126 | accordingly 127 | """ 128 | if self._port is None: 129 | self.port = get_random_port() 130 | else: 131 | self.port = self._port 132 | 133 | if not self.dumpfile: 134 | self.dumpfile = 'dump.%s.rdb' % self.port 135 | if not self.aoffile: 136 | self.aoffile = 'appendonly.%s.aof' % self.port 137 | 138 | self.args = [self.path, 139 | '--port', str(self.port), 140 | '--save', '', 141 | '--dbfilename', self.dumpfile] 142 | if self.use_aof: 143 | self.args += ['--appendonly', 'yes', 144 | '--appendfilename', self.aoffile] 145 | 146 | self.args += self.extra_args 147 | 148 | self._start_process() 149 | 150 | def _cleanup_files(self): 151 | for f in (self.aoffile, self.dumpfile): 152 | try: 153 | os.unlink(f) 154 | except OSError: 155 | pass 156 | 157 | def stop(self, for_restart=False): 158 | if self._is_external: 159 | return 160 | 161 | self.process.terminate() 162 | self.process.wait() 163 | if not for_restart: 164 | self._cleanup_files() 165 | 166 | def __enter__(self): 167 | self.start() 168 | return self.client() 169 | 170 | def __exit__(self, exc_type, exc_val, exc_tb): 171 | self.stop() 172 | if exc_val or self.errored: 173 | sys.stderr.write("Redis output: {}\n".format(self._get_output())) 174 | 175 | def _wait_for_child(self): 176 | # Wait until file is available 177 | r = self.client() 178 | while True: 179 | info = r.info('persistence') 180 | if info['aof_rewrite_scheduled'] or info['aof_rewrite_in_progress']: 181 | time.sleep(0.1) 182 | else: 183 | break 184 | 185 | def dump_and_reload(self, restart_process=False): 186 | """ 187 | Dump the rdb and reload it, to test for serialization errors 188 | """ 189 | conn = self.client() 190 | 191 | if restart_process: 192 | if self._is_external: 193 | warnings.warn('Tied to an external process. Cannot restart') 194 | return 195 | conn.bgrewriteaof() 196 | self._wait_for_child() 197 | 198 | self.stop(for_restart=True) 199 | self.start() 200 | else: 201 | conn.save() 202 | try: 203 | conn.execute_command('DEBUG', 'RELOAD') 204 | except redis.RedisError as err: 205 | self.errored = True 206 | raise err 207 | 208 | def client(self): 209 | """ 210 | :rtype: redis.StrictRedis 211 | """ 212 | return Client(self, self.port) 213 | -------------------------------------------------------------------------------- /rmtest/disposableredis/cluster.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=missing-docstring, invalid-name, broad-except 2 | 3 | import os 4 | import time 5 | import uuid 6 | import logging as log 7 | from . import DisposableRedis 8 | 9 | class Cluster(object): 10 | 11 | def __init__(self, num_nodes=3, path='redis-server', **extra_args): 12 | 13 | self.common_conf = { 14 | 'cluster-enabled': 'yes', 15 | 'cluster-node-timeout': '5000', 16 | } 17 | self.common_conf.update(extra_args) 18 | self.num_nodes = num_nodes 19 | self.nodes = [] 20 | self.ports = [] 21 | self.confs = [] 22 | self.redis_path = path 23 | self.extra_args = extra_args 24 | 25 | def _node_by_slot(self, slot): 26 | 27 | slots_per_node = int(16384 / len(self.ports)) + 1 28 | for i, node in enumerate(self.nodes): 29 | 30 | start_slot = i * slots_per_node 31 | end_slot = start_slot + slots_per_node - 1 32 | if end_slot > 16383: 33 | end_slot = 16383 34 | if start_slot <= slot <= end_slot: 35 | return node 36 | 37 | return None 38 | 39 | def _setup_cluster(self): 40 | 41 | for i, node in enumerate(self.nodes): 42 | conn = node.client() 43 | conn.cluster('RESET') 44 | 45 | slots_per_node = int(16384 / len(self.ports)) + 1 46 | for i, node in enumerate(self.nodes): 47 | assert isinstance(node, DisposableRedis) 48 | conn = node.client() 49 | for port in self.ports: 50 | conn.cluster('MEET', '127.0.0.1', port) 51 | 52 | start_slot = i * slots_per_node 53 | end_slot = start_slot + slots_per_node 54 | if end_slot > 16384: 55 | end_slot = 16384 56 | 57 | conn.cluster('ADDSLOTS', *(str(x) for x in range(start_slot, end_slot))) 58 | 59 | 60 | def _wait_cluster(self, timeout_sec): 61 | 62 | st = time.time() 63 | ok = 0 64 | 65 | while st + timeout_sec > time.time(): 66 | ok = 0 67 | for node in self.nodes: 68 | status = node.client().cluster('INFO') 69 | if status.get('cluster_state') == 'ok': 70 | ok += 1 71 | if ok == len(self.nodes): 72 | print("All nodes OK!") 73 | return 74 | 75 | time.sleep(0.1) 76 | raise RuntimeError("Cluster OK wait loop timed out after %s seconds" % timeout_sec) 77 | 78 | 79 | 80 | def _start_nodes(self): 81 | 82 | # Assign a random "session id" 83 | uid = uuid.uuid4().hex 84 | for i in range(self.num_nodes): 85 | 86 | conf = self.common_conf.copy() 87 | nodeconf = 'node-%s.%d.conf' % (uid, i) 88 | conf['cluster-config-file'] = nodeconf 89 | self.confs.append(nodeconf) 90 | 91 | 92 | node = DisposableRedis(path=self.redis_path, **conf) 93 | node.force_start() 94 | node.start() 95 | 96 | self.nodes.append(node) 97 | self.ports.append(node.port) 98 | 99 | 100 | def start(self): 101 | 102 | self._start_nodes() 103 | self._setup_cluster() 104 | 105 | self._wait_cluster(10) 106 | 107 | return self.ports 108 | 109 | 110 | def broadcast(self, *args): 111 | 112 | rs = [] 113 | for node in self.nodes: 114 | 115 | conn = node.client() 116 | rs.append(conn.execute_command(*args)) 117 | 118 | return rs 119 | 120 | 121 | def stop(self): 122 | 123 | for i, node in enumerate(self.nodes): 124 | assert isinstance(node, DisposableRedis) 125 | try: 126 | node.stop() 127 | except Exception as err: 128 | log.error("Error stopping node: %s", err) 129 | try: 130 | os.unlink(self.confs[i]) 131 | except OSError: 132 | pass 133 | 134 | def client_for_key(self, key): 135 | 136 | conn = self.nodes[0].client() 137 | slot = conn.cluster('KEYSLOT', key) 138 | node = self._node_by_slot(slot) 139 | 140 | return node.client() 141 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup( 5 | name='rmtest', 6 | version='1.0.0', 7 | description='Redis Module Testing Utility', 8 | url='http://github.com/RedisLabs/rmtest', 9 | packages=find_packages(), 10 | install_requires=['redis'], 11 | classifiers=[ 12 | 'Development Status :: 5 - Production/Stable', 13 | 'Intended Audience :: Developers', 14 | 'License :: OSI Approved :: BSD License', 15 | 'Operating System :: OS Independent', 16 | 'Programming Language :: Python :: 2.7', 17 | 'Programming Language :: Python :: 3.4', 18 | 'Programming Language :: Python :: 3.5', 19 | 'Programming Language :: Python :: 3.6', 20 | 'Programming Language :: Python :: 3.7', 21 | 'Topic :: Database', 22 | 'Topic :: Software Development :: Testing' 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from subprocess import Popen 2 | import unittest 3 | import os.path 4 | from rmtest import ModuleTestCase 5 | from rmtest.cluster import ClusterModuleTestCase 6 | from rmtest.disposableredis import cluster 7 | 8 | 9 | MODULE_PATH = os.path.abspath(os.path.dirname(__file__)) + '/' + 'module.so' 10 | 11 | 12 | def build_module(): 13 | csrc = MODULE_PATH[0:-3] + '.c' 14 | po = Popen(['cc', '-o', MODULE_PATH, '-shared', '-fPIC', csrc]) 15 | po.communicate() 16 | po.wait() 17 | if po.returncode != 0: 18 | raise Exception('Failed to compile module') 19 | 20 | 21 | class TestTestCase(ModuleTestCase(MODULE_PATH, module_args=('foo','bar'))): 22 | @classmethod 23 | def setUpClass(cls): 24 | super(TestTestCase, cls).setUpClass() 25 | # Check for the presence of the module 26 | if not os.path.exists(MODULE_PATH): 27 | build_module() 28 | 29 | def testContext(self): 30 | with self.redis() as r: 31 | with self.redis() as r: 32 | for _ in r.retry_with_rdb_reload(): 33 | self.assertOk(r.execute_command('TEST.TEST')) 34 | with self.assertResponseError(): 35 | r.execute_command('TEST.ERR') 36 | 37 | def testBasic(self): 38 | self.assertTrue(self.server) 39 | self.assertTrue(self.client) 40 | with self.assertResponseError(): 41 | self.cmd('TEST.ERR') 42 | 43 | 44 | class ClusterTestCase(unittest.TestCase): 45 | @classmethod 46 | def setUpClass(cls): 47 | super(ClusterTestCase, cls).setUpClass() 48 | # Check for the presence of the module 49 | if not os.path.exists(MODULE_PATH): 50 | build_module() 51 | 52 | def setUp(self): 53 | self.cl = cluster.Cluster(num_nodes=3) 54 | 55 | def testCluster(self): 56 | ports = self.cl.start() 57 | 58 | self.assertEqual(3, len(ports)) 59 | 60 | res = self.cl.broadcast('ping') 61 | self.assertListEqual(['PONG', 'PONG', 'PONG'], res) 62 | 63 | def tearDown(self): 64 | self.cl.stop() 65 | 66 | class ClusterTestCaseWithModule(ClusterModuleTestCase(MODULE_PATH, num_nodes=5, module_args=('foo','bar'))): 67 | 68 | def testCluster(self): 69 | 70 | client = self.client() 71 | self.assertIsNotNone(client) 72 | 73 | for _ in self.retry_with_rdb_reload(): 74 | self.assertOk(client.execute_command('TEST.TEST')) 75 | self.assertOk(self.cmd('TEST.TEST')) 76 | 77 | node = self.client_for_key("foobar") 78 | self.assertIsNotNone(node) 79 | with self.assertResponseError(): 80 | client.execute_command('TEST.ERR') 81 | 82 | if __name__ == '__main__': 83 | unittest.main() 84 | --------------------------------------------------------------------------------