Add refactored nget commands and UT 54/2254/6
authorHeinonen Arvo <arvo.heinonen@nokia.com>
Thu, 16 Jan 2020 15:23:07 +0000 (17:23 +0200)
committerHeinonen Arvo <arvo.heinonen@nokia.com>
Thu, 23 Jan 2020 11:23:51 +0000 (13:23 +0200)
Add refactored nget commands 'nget.atomic' and 'nget.noatomic'.
Add new unit tests for these commands.

Signed-off-by: Arvo Heinonen <arvo.heinonen@nokia.com>
Change-Id: Icbb116bf59f3e84dfdcee1a55d4209affa6b4244

redismodule/Makefile.am
redismodule/run-tests.sh.in
redismodule/src/exstrings.c
redismodule/tst/include/ut_helpers.hpp [new file with mode: 0644]
redismodule/tst/mock/include/exstringsStub.h
redismodule/tst/mock/src/redismoduleNewStub.cpp [new file with mode: 0755]
redismodule/tst/src/exstrings_nget_test.cpp [new file with mode: 0644]
redismodule/tst/src/ut_helpers.cpp [new file with mode: 0644]

index 6faacad..4ba5411 100755 (executable)
@@ -27,7 +27,7 @@ clean-local:
 if UNIT_TEST_ENABLED
 # UT
 CPP_U_TEST=$(CPP_U_TEST_LATEST)
-check_PROGRAMS = redismodule_ut
+check_PROGRAMS = redismodule_ut redismodule_ut2
 #TESTS = ${check_PROGRAMS}
 redismodule_ut_SOURCES = \
        src/exstrings.c \
@@ -67,7 +67,48 @@ redismodule_ut_LDFLAGS = -Wl,-rpath=${libdir} ${UT_COVERAGE_LDFLAGS}
 redismodule_ut_LDADD = -L${libdir} $(LIBCPPUTEST_LIBS) -lgcov
 
 
-test: redismodule_ut
+redismodule_ut2_SOURCES = \
+       src/exstrings.c \
+       tst/include/ut_helpers.hpp \
+       tst/mock/include/commonStub.h \
+       tst/mock/include/exstringsStub.h \
+       tst/mock/include/redismodule.h  \
+       tst/mock/src/commonStub.cpp \
+       tst/mock/src/redismoduleNewStub.cpp \
+       tst/src/exstrings_nget_test.cpp \
+       tst/src/main.cpp \
+       tst/src/ut_helpers.cpp
+
+
+redismodule_ut2_CFLAGS = \
+               -std=c11 -g -Wall \
+               -fprofile-arcs -ftest-coverage \
+               -D__UT__ \
+               $(LIBCPPUTEST_CFLAGS) \
+               -I${top_srcdir}/tst/mock/include \
+               -I${includedir} \
+               -I${top_srcdir}/include \
+               -I${CPP_U_TEST_LATEST}/include \
+               -Wall -Werror -Wextra
+
+redismodule_ut2_CXXFLAGS = \
+       -std=c++11 -g -Wall \
+       -fprofile-arcs -ftest-coverage \
+       -D__UT__ \
+       $(LIBCPPUTEST_CFLAGS) \
+       -I${top_srcdir}/tst/mock/include \
+       -I${top_srcdir}/tst/include \
+       -I${includedir} \
+       -I${top_srcdir}/include \
+       -I${CPP_U_TEST_LATEST}/include \
+       -Wall -Werror -Wextra
+
+
+redismodule_ut2_LDFLAGS = -Wl,-rpath=${libdir} ${UT_COVERAGE_LDFLAGS}
+redismodule_ut2_LDADD = -L${libdir} $(LIBCPPUTEST_LIBS) -lgcov
+
+
+test: redismodule_ut redismodule_ut2
        ./run-tests.sh
 
 TESTS = run-tests.sh
index 194095d..4de7716 100755 (executable)
@@ -1,6 +1,7 @@
 #!/bin/sh
 
 valgrind --leak-check=full ./redismodule_ut
+valgrind --leak-check=full ./redismodule_ut2
 if [ $? != 0 ]
 then
    exit -1
index c1fe8d1..6f57753 100755 (executable)
  */
 
 #include "redismodule.h"
-#include <string.h>
 #include <pthread.h>
 #include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
 
 #ifdef __UT__
 #include "exstringsStub.h"
@@ -53,6 +55,16 @@ sends the error to the client and exit the current function if its */
 
 RedisModuleString *def_count_str = NULL, *match_str = NULL, *count_str = NULL, *zero_str = NULL;
 
+typedef struct _NgetArgs {
+    RedisModuleString *key;
+    RedisModuleString *count;
+} NgetArgs;
+
+typedef struct RedisModuleBlockedClientArgs {
+    RedisModuleBlockedClient *bc;
+    NgetArgs nget_args;
+} RedisModuleBlockedClientArgs;
+
 void InitStaticVariable()
 {
     if (def_count_str == NULL)
@@ -100,6 +112,165 @@ typedef struct _DelParams {
     size_t length;
 } DelParams;
 
+typedef enum _ExstringsStatus {
+    EXSTRINGS_STATUS_NO_ERRORS = 0,
+    EXSTRINGS_STATUS_ERROR_AND_REPLY_SENT,
+    EXSTRINGS_STATUS_NOT_SET
+} ExstringsStatus;
+
+void readNgetArgs(RedisModuleCtx *ctx, RedisModuleString **argv, int argc,
+                  NgetArgs* nget_args, ExstringsStatus* status)
+{
+    size_t str_len;
+    long long number;
+
+    if(argc == 2) {
+        nget_args->key = argv[1];
+        nget_args->count = def_count_str;
+    } else if (argc == 4) {
+        if (strcasecmp(RedisModule_StringPtrLen(argv[2], &str_len), "count")) {
+            RedisModule_ReplyWithError(ctx,"-ERR syntax error");
+            *status = EXSTRINGS_STATUS_ERROR_AND_REPLY_SENT;
+            return;
+        }
+
+        int ret = RedisModule_StringToLongLong(argv[3], &number) != REDISMODULE_OK;
+        if (ret != REDISMODULE_OK || number < 1) {
+            RedisModule_ReplyWithError(ctx,"-ERR value is not an integer or out of range");
+            *status = EXSTRINGS_STATUS_ERROR_AND_REPLY_SENT;
+            return;
+        }
+
+        nget_args->key = argv[1];
+        nget_args->count = argv[3];
+    } else {
+        /* In redis there is a bug (or undocumented feature see link)
+         * where calling 'RedisModule_WrongArity'
+         * within a blocked client will crash redis.
+         *
+         * Therefore we need to call this function to validate args
+         * before putting the client into blocking mode.
+         *
+         * Link to issue:
+         * https://github.com/antirez/redis/issues/6382
+         * 'If any thread tries to access the command arguments from
+         *  within the ThreadSafeContext they will crash redis' */
+        RedisModule_WrongArity(ctx);
+        *status = EXSTRINGS_STATUS_ERROR_AND_REPLY_SENT;
+        return;
+    }
+
+    *status = EXSTRINGS_STATUS_NO_ERRORS;
+    return;
+}
+
+long long callReplyLongLong(RedisModuleCallReply* reply)
+{
+    const char* cursor_str_ptr = RedisModule_CallReplyStringPtr(reply, NULL);
+    return strtoll(cursor_str_ptr, NULL, 10);
+}
+
+void forwardIfError(RedisModuleCtx *ctx, RedisModuleCallReply *reply, ExstringsStatus* status)
+{
+    if (RedisModule_CallReplyType(reply) == REDISMODULE_REPLY_ERROR) {
+        RedisModule_ReplyWithCallReply(ctx, reply);
+        RedisModule_FreeCallReply(reply);
+        *status = EXSTRINGS_STATUS_ERROR_AND_REPLY_SENT;
+    }
+    *status = EXSTRINGS_STATUS_NO_ERRORS;
+}
+
+typedef struct _ScannedKeys {
+    RedisModuleString **keys;
+    size_t len;
+} ScannedKeys;
+
+ScannedKeys* allocScannedKeys(size_t len)
+{
+    ScannedKeys *sk = RedisModule_Alloc(sizeof(ScannedKeys));
+    if (sk) {
+        sk->len = len;
+        sk->keys = RedisModule_Alloc(sizeof(RedisModuleString *)*len);
+    }
+    return sk;
+}
+
+void freeScannedKeys(RedisModuleCtx *ctx, ScannedKeys* sk)
+{
+    if (sk) {
+        size_t j;
+        for (j = 0; j < sk->len; j++)
+            RedisModule_FreeString(ctx, sk->keys[j]);
+        RedisModule_Free(sk->keys);
+    }
+    RedisModule_Free(sk);
+}
+
+typedef struct _ScanSomeState {
+    RedisModuleString *key;
+    RedisModuleString *count;
+    long long cursor;
+} ScanSomeState;
+
+ScannedKeys *scanSome(RedisModuleCtx* ctx, ScanSomeState* state, ExstringsStatus* status)
+{
+    RedisModuleString *scanargv[SCANARGC] = {NULL};
+
+    scanargv[0] = RedisModule_CreateStringFromLongLong(ctx, state->cursor);
+    scanargv[1] = match_str;
+    scanargv[2] = state->key;
+    scanargv[3] = count_str;
+    scanargv[4] = state->count;
+
+    RedisModuleCallReply *reply;
+    reply = RedisModule_Call(ctx, "SCAN", "v", scanargv, SCANARGC);
+    RedisModule_FreeString(ctx, scanargv[0]);
+    forwardIfError(ctx, reply, status);
+    if (*status == EXSTRINGS_STATUS_ERROR_AND_REPLY_SENT)
+        return NULL;
+
+    state->cursor = callReplyLongLong(RedisModule_CallReplyArrayElement(reply, 0));
+    RedisModuleCallReply *cr_keys =
+        RedisModule_CallReplyArrayElement(reply, 1);
+
+    size_t scanned_keys_len = RedisModule_CallReplyLength(cr_keys);
+    if (scanned_keys_len == 0) {
+        RedisModule_FreeCallReply(reply);
+        *status = EXSTRINGS_STATUS_NO_ERRORS;
+        return NULL;
+    }
+
+    ScannedKeys *scanned_keys = allocScannedKeys(scanned_keys_len);
+    if (scanned_keys == NULL) {
+        RedisModule_FreeCallReply(reply);
+        RedisModule_ReplyWithError(ctx,"-ERR Out of memory");
+        *status = EXSTRINGS_STATUS_ERROR_AND_REPLY_SENT;
+        return NULL;
+    }
+
+    scanned_keys->len = scanned_keys_len;
+    size_t j;
+    for (j = 0; j < scanned_keys_len; j++) {
+        RedisModuleString *rms = RedisModule_CreateStringFromCallReply(RedisModule_CallReplyArrayElement(cr_keys,j));
+        scanned_keys->keys[j] = rms;
+    }
+    RedisModule_FreeCallReply(reply);
+    *status = EXSTRINGS_STATUS_NO_ERRORS;
+    return scanned_keys;
+}
+
+inline void unlockThreadsafeContext(RedisModuleCtx *ctx, bool using_threadsafe_context)
+{
+    if (using_threadsafe_context)
+        RedisModule_ThreadSafeContextUnlock(ctx);
+}
+
+inline void lockThreadsafeContext(RedisModuleCtx *ctx, bool using_threadsafe_context)
+{
+    if (using_threadsafe_context)
+        RedisModule_ThreadSafeContextLock(ctx);
+}
+
 void multiPubCommand(RedisModuleCtx *ctx, PubParams* pubParams)
 {
     RedisModuleCallReply *reply = NULL;
@@ -539,6 +710,136 @@ int DelNEPub_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int arg
     return delIENEPubStringCommon(ctx, argv, argc, OBJ_OP_NE);
 }
 
+int Nget_RedisCommand(RedisModuleCtx *ctx, NgetArgs* nget_args, bool using_threadsafe_context)
+{
+    int ret = REDISMODULE_OK;
+    size_t replylen = 0;
+    RedisModuleCallReply *reply = NULL;
+    ExstringsStatus status = EXSTRINGS_STATUS_NOT_SET;
+    ScanSomeState scan_state;
+    ScannedKeys *scanned_keys;
+
+    scan_state.key = nget_args->key;
+    scan_state.count = nget_args->count;
+    scan_state.cursor = 0;
+
+    RedisModule_ReplyWithArray(ctx, REDISMODULE_POSTPONED_ARRAY_LEN);
+    do {
+        lockThreadsafeContext(ctx, using_threadsafe_context);
+
+        status = EXSTRINGS_STATUS_NOT_SET;
+        scanned_keys = scanSome(ctx, &scan_state, &status);
+
+        if (status != EXSTRINGS_STATUS_NO_ERRORS) {
+            unlockThreadsafeContext(ctx, using_threadsafe_context);
+            ret = REDISMODULE_ERR;
+            break;
+        } else if (scanned_keys == NULL) {
+            unlockThreadsafeContext(ctx, using_threadsafe_context);
+            continue;
+        }
+
+        reply = RedisModule_Call(ctx, "MGET", "v", scanned_keys->keys, scanned_keys->len);
+
+        unlockThreadsafeContext(ctx, using_threadsafe_context);
+
+        status = EXSTRINGS_STATUS_NOT_SET;
+        forwardIfError(ctx, reply, &status);
+        if (status != EXSTRINGS_STATUS_NO_ERRORS) {
+            freeScannedKeys(ctx, scanned_keys);
+            ret = REDISMODULE_ERR;
+            break;
+        }
+
+        size_t i;
+        for (i = 0; i < scanned_keys->len; i++) {
+            RedisModuleString *rms = RedisModule_CreateStringFromCallReply(RedisModule_CallReplyArrayElement(reply, i));
+            if (rms) {
+                RedisModule_ReplyWithString(ctx, scanned_keys->keys[i]);
+                RedisModule_ReplyWithString(ctx, rms);
+                RedisModule_FreeString(ctx, rms);
+                replylen += 2;
+            }
+        }
+        RedisModule_FreeCallReply(reply);
+        freeScannedKeys(ctx, scanned_keys);
+    } while (scan_state.cursor != 0);
+
+    RedisModule_ReplySetArrayLength(ctx,replylen);
+    return ret;
+}
+
+/* The thread entry point that actually executes the blocking part
+ * of the command nget.noatomic
+ */
+void *NGet_NoAtomic_ThreadMain(void *arg)
+{
+    RedisModuleBlockedClientArgs *bca = arg;
+    RedisModuleBlockedClient *bc = bca->bc;
+    RedisModuleCtx *ctx = RedisModule_GetThreadSafeContext(bc);
+
+    Nget_RedisCommand(ctx, &bca->nget_args, true);
+    RedisModule_FreeThreadSafeContext(ctx);
+    RedisModule_UnblockClient(bc, NULL);
+    RedisModule_Free(bca);
+    return NULL;
+}
+
+int NGet_NoAtomic_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+    RedisModule_AutoMemory(ctx);
+    pthread_t tid;
+
+    InitStaticVariable();
+
+    RedisModuleBlockedClientArgs *bca = RedisModule_Alloc(sizeof(RedisModuleBlockedClientArgs));
+    if (bca == NULL) {
+        RedisModule_ReplyWithError(ctx,"-ERR Out of memory");
+        return REDISMODULE_ERR;
+    }
+
+    ExstringsStatus status = EXSTRINGS_STATUS_NOT_SET;
+    readNgetArgs(ctx, argv, argc, &bca->nget_args, &status);
+    if (status != EXSTRINGS_STATUS_NO_ERRORS) {
+        RedisModule_Free(bca);
+        return REDISMODULE_ERR;
+    }
+
+    /* Note that when blocking the client we do not set any callback: no
+     * timeout is possible since we passed '0', nor we need a reply callback
+     * because we'll use the thread safe context to accumulate a reply. */
+    RedisModuleBlockedClient *bc = RedisModule_BlockClient(ctx,NULL,NULL,NULL,0);
+
+    bca->bc = bc;
+
+    /* Now that we setup a blocking client, we need to pass the control
+     * to the thread. However we need to pass arguments to the thread:
+     * the reference to the blocked client handle. */
+    if (pthread_create(&tid,NULL,NGet_NoAtomic_ThreadMain,bca) != 0) {
+        RedisModule_AbortBlock(bc);
+        RedisModule_Free(bca);
+        return RedisModule_ReplyWithError(ctx,"-ERR Can't start thread");
+    }
+
+    return REDISMODULE_OK;
+}
+
+int NGet_Atomic_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
+{
+    RedisModule_AutoMemory(ctx);
+    NgetArgs nget_args;
+    ExstringsStatus status = EXSTRINGS_STATUS_NOT_SET;
+
+    InitStaticVariable();
+
+    readNgetArgs(ctx, argv, argc, &nget_args, &status);
+    if (status != EXSTRINGS_STATUS_NO_ERRORS) {
+        return REDISMODULE_ERR;
+    }
+
+    return Nget_RedisCommand(ctx, &nget_args, false);
+}
+
 /* This function must be present on each Redis module. It is used in order to
  * register the commands into the Redis server. */
 int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
@@ -564,6 +865,14 @@ int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc)
         DelNE_RedisCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
         return REDISMODULE_ERR;
 
+    if (RedisModule_CreateCommand(ctx,"nget.atomic",
+        NGet_Atomic_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR)
+        return REDISMODULE_ERR;
+
+    if (RedisModule_CreateCommand(ctx,"nget.noatomic",
+        NGet_NoAtomic_RedisCommand,"readonly",1,1,1) == REDISMODULE_ERR)
+        return REDISMODULE_ERR;
+
     if (RedisModule_CreateCommand(ctx,"msetpub",
         SetPub_RedisCommand,"write deny-oom",1,1,1) == REDISMODULE_ERR)
         return REDISMODULE_ERR;
diff --git a/redismodule/tst/include/ut_helpers.hpp b/redismodule/tst/include/ut_helpers.hpp
new file mode 100644 (file)
index 0000000..c183f66
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2018-2020 Nokia.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+/*
+ * This source code is part of the near-RT RIC (RAN Intelligent Controller)
+ * platform project (RICP).
+ */
+
+#ifndef EXSTRING_UT_HELPERS_H_
+#define EXSTRING_UT_HELPERS_H_
+
+extern "C" {
+#include "exstringsStub.h"
+#include "redismodule.h"
+}
+
+#define UT_DUMMY_BUFFER_SIZE 1
+#define UT_DUMMY_PTR_ADDRESS 1234
+
+RedisModuleString **createRedisStrVec(size_t size);
+
+void returnNKeysFromScanSome(long keys);
+
+#endif
index 756e5de..fd90662 100755 (executable)
@@ -49,5 +49,8 @@ int DelMPub_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc
 int DelIEPub_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
 int DelIEMPub_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
 int DelNEPub_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
+int NGet_Atomic_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
+int NGet_NoAtomic_RedisCommand(RedisModuleCtx *ctx, RedisModuleString **argv, int argc);
+void *NGet_NoAtomic_ThreadMain(void *arg);
 
 #endif
diff --git a/redismodule/tst/mock/src/redismoduleNewStub.cpp b/redismodule/tst/mock/src/redismoduleNewStub.cpp
new file mode 100755 (executable)
index 0000000..5a25867
--- /dev/null
@@ -0,0 +1,360 @@
+/*
+ * Copyright (c) 2018-2020 Nokia.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+/*
+ * This source code is part of the near-RT RIC (RAN Intelligent Controller)
+ * platform project (RICP).
+ */
+
+
+#include <unistd.h>
+#include <string.h>
+
+extern "C" {
+#include "redismodule.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <ctype.h>
+}
+
+#include <CppUTest/TestHarness.h>
+#include <CppUTestExt/MockSupport.h>
+
+#include "ut_helpers.hpp"
+
+RedisModuleCallReply *RedisModule_Call(RedisModuleCtx *ctx, const char *cmdname, const char *fmt, ...)
+{
+    (void)ctx;
+    (void)fmt;
+    return (RedisModuleCallReply *)mock().actualCall("RedisModule_Call")
+                                         .withParameter("cmdname", cmdname)
+                                         .returnPointerValueOrDefault(malloc(UT_DUMMY_BUFFER_SIZE));
+}
+
+int RedisModule_ReplyWithString(RedisModuleCtx *ctx, RedisModuleString *str)
+{
+    (void)ctx;
+    (void)str;
+    return mock()
+        .actualCall("RedisModule_ReplyWithString")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+RedisModuleString *RedisModule_CreateStringFromCallReply(RedisModuleCallReply *reply)
+{
+    (void)reply;
+    return (RedisModuleString *)mock().actualCall("RedisModule_CreateStringFromCallReply")
+        .returnPointerValue();
+}
+
+void RedisModule_CloseKey(RedisModuleKey *kp)
+{
+    (void)kp;
+    mock().actualCall("RedisModule_CloseKey");
+}
+
+size_t RedisModule_CallReplyLength(RedisModuleCallReply *reply)
+{
+    (void)reply;
+    return (size_t)mock().actualCall("RedisModule_CallReplyLength")
+                         .returnIntValue();
+}
+
+int RedisModule_ReplyWithArray(RedisModuleCtx *ctx, long len)
+{
+    (void)ctx;
+    return (int)mock().actualCall("RedisModule_ReplyWithArray")
+                      .withParameter("len", len)
+                      .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+void RedisModule_ReplySetArrayLength(RedisModuleCtx *ctx, long len)
+{
+    (void)ctx;
+    mock().actualCall("RedisModule_ReplySetArrayLength")
+                      .withParameter("len", len);
+}
+
+RedisModuleString *RedisModule_CreateString(RedisModuleCtx *ctx, const char *ptr, size_t len)
+{
+    (void)ctx;
+    (void)ptr;
+    (void)len;
+    void* buf = malloc(UT_DUMMY_BUFFER_SIZE);
+    return (RedisModuleString *) mock()
+        .actualCall("RedisModule_CreateString")
+        .returnPointerValueOrDefault(buf);
+}
+
+RedisModuleString *RedisModule_CreateStringFromLongLong(RedisModuleCtx *ctx, long long ll)
+{
+    (void)ctx;
+    (void)ll;
+    void* buf = malloc(UT_DUMMY_BUFFER_SIZE);
+    return (RedisModuleString *)mock()
+        .actualCall("RedisModule_CreateStringFromLongLong")
+        .returnPointerValueOrDefault(buf);
+}
+
+void RedisModule_AutoMemory(RedisModuleCtx *ctx)
+{
+    (void)ctx;
+    mock().actualCall("RedisModule_AutoMemory");
+}
+
+void RedisModule_FreeString(RedisModuleCtx *ctx, RedisModuleString *str)
+{
+    (void)ctx;
+    free(str);
+    mock().actualCall("RedisModule_FreeString");
+}
+
+int RedisModule_StringToLongLong(const RedisModuleString *str, long long *ll)
+{
+    (void)str;
+    return (int)mock().actualCall("RedisModule_StringToLongLong")
+                      .withOutputParameter("ll", ll)
+                      .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+void RedisModule_FreeCallReply(RedisModuleCallReply *reply)
+{
+    free(reply);
+    mock().actualCall("RedisModule_FreeCallReply");
+}
+
+RedisModuleCallReply *RedisModule_CallReplyArrayElement(RedisModuleCallReply *reply, size_t idx)
+{
+    (void)reply;
+    (void)idx;
+    return (RedisModuleCallReply *)mock()
+        .actualCall("RedisModule_CallReplyArrayElement")
+        .returnPointerValueOrDefault(NULL);
+}
+
+int RedisModule_ReplyWithLongLong(RedisModuleCtx *ctx, long long ll)
+{
+    (void)ctx;
+    return (int)mock()
+        .actualCall("RedisModule_ReplyWithLongLong")
+        .withParameter("ll", (int)ll)
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+long long RedisModule_CallReplyInteger(RedisModuleCallReply *reply)
+{
+    (void)reply;
+    return (long long)mock()
+        .actualCall("RedisModule_CallReplyInteger")
+        .returnIntValue();
+}
+
+int RedisModule_CallReplyType(RedisModuleCallReply *reply)
+{
+    (void)reply;
+    return (int)mock()
+        .actualCall("RedisModule_CallReplyType")
+        .returnIntValue();
+}
+
+int RedisModule_WrongArity(RedisModuleCtx *ctx)
+{
+    (void)ctx;
+    return (int)mock()
+        .actualCall("RedisModule_WrongArity")
+        .returnIntValueOrDefault(REDISMODULE_ERR);
+}
+
+int RedisModule_ReplyWithError(RedisModuleCtx *ctx, const char *err)
+{
+    (void)ctx;
+    (void)err;
+    return (int)mock()
+        .actualCall("RedisModule_ReplyWithError")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+int RedisModule_ReplyWithCallReply(RedisModuleCtx *ctx, RedisModuleCallReply *reply)
+{
+    (void)ctx;
+    (void)reply;
+    return (int)mock()
+        .actualCall("RedisModule_ReplyWithCallReply")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+void *RedisModule_OpenKey(RedisModuleCtx *ctx, RedisModuleString *keyname, int mode)
+{
+    (void)ctx;
+    (void)keyname;
+    (void)mode;
+    return (void *)mock()
+        .actualCall("RedisModule_OpenKey")
+        .returnPointerValue();
+}
+
+int RedisModule_KeyType(RedisModuleKey *kp)
+{
+    (void)kp;
+    return (int)mock()
+        .actualCall("RedisModule_KeyType")
+        .returnIntValue();
+}
+
+const char *RedisModule_StringPtrLen(const RedisModuleString *str, size_t *len)
+{
+    (void)str;
+    if (len != NULL) {
+        return (const char *)mock()
+            .actualCall("RedisModule_StringPtrLen")
+            .withOutputParameter("len", len)
+            .returnPointerValue();
+    } else {
+        return (const char *)mock()
+            .actualCall("RedisModule_StringPtrLen")
+            .returnPointerValue();
+    }
+}
+
+RedisModuleBlockedClient *RedisModule_BlockClient(RedisModuleCtx *ctx, RedisModuleCmdFunc reply_callback,
+                                                  RedisModuleCmdFunc timeout_callback,
+                                                  void (*free_privdata)(RedisModuleCtx*,void*),
+                                                  long long timeout_ms)
+{
+    (void)ctx;
+    (void)reply_callback;
+    (void)timeout_callback;
+    (void)free_privdata;
+    (void)timeout_ms;
+
+    void *buf = malloc(UT_DUMMY_BUFFER_SIZE);
+    return (RedisModuleBlockedClient *)mock()
+        .actualCall("RedisModule_BlockClient")
+        .returnPointerValueOrDefault(buf);
+}
+
+int RedisModule_UnblockClient(RedisModuleBlockedClient *bc, void *privdata)
+{
+    (void)privdata;
+
+    free(bc);
+    return (int)mock()
+        .actualCall("RedisModule_UnblockClient")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+const char *RedisModule_CallReplyStringPtr(RedisModuleCallReply *reply, size_t *len)
+{
+    (void)reply;
+    (void)len;
+
+    static char cursor_zero_literal[] = "0";
+    return (const char *)mock()
+        .actualCall("RedisModule_CallReplyStringPtr")
+        .returnPointerValueOrDefault(cursor_zero_literal);
+}
+
+int RedisModule_AbortBlock(RedisModuleBlockedClient *bc)
+{
+    free(bc);
+    return mock()
+        .actualCall("RedisModule_AbortBlock")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+int RedisModule_ReplyWithNull(RedisModuleCtx *ctx)
+{
+    (void)ctx;
+    return mock()
+        .actualCall("RedisModule_ReplyWithNull")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+void RedisModule_ThreadSafeContextUnlock(RedisModuleCtx *ctx)
+{
+    (void)ctx;
+    int tmp = mock().getData("TimesThreadSafeContextWasUnlocked").getIntValue();
+    mock().setData("TimesThreadSafeContextWasUnlocked", tmp + 1);
+    mock()
+        .actualCall("RedisModule_ThreadSafeContextUnlock");
+}
+
+void RedisModule_ThreadSafeContextLock(RedisModuleCtx *ctx)
+{
+    (void)ctx;
+    int tmp = mock().getData("TimesThreadSafeContextWasLocked").getIntValue();
+    mock().setData("TimesThreadSafeContextWasLocked", tmp + 1);
+    mock()
+        .actualCall("RedisModule_ThreadSafeContextLock");
+}
+
+RedisModuleCtx *RedisModule_GetThreadSafeContext(RedisModuleBlockedClient *bc)
+{
+    (void)bc;
+    return (RedisModuleCtx *)mock()
+        .actualCall("RedisModule_GetThreadSafeContext")
+        .returnPointerValueOrDefault(0);
+}
+
+void RedisModule_FreeThreadSafeContext(RedisModuleCtx *ctx)
+{
+    (void)ctx;
+    mock()
+        .actualCall("RedisModule_FreeThreadSafeContext");
+}
+
+/* This is included inline inside each Redis module. */
+int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int apiver)
+{
+    (void)ctx;
+    (void)name;
+    (void)ver;
+    (void)apiver;
+
+    return mock()
+        .actualCall("RedisModule_Init")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+int RedisModule_CreateCommand(RedisModuleCtx *ctx, const char *name, RedisModuleCmdFunc cmdfunc, const char *strflags, int firstkey, int lastkey, int keystep)
+{
+    (void)ctx;
+    (void)name;
+    (void)cmdfunc;
+    (void)strflags;
+    (void)firstkey;
+    (void)lastkey;
+    (void)keystep;
+
+    return mock()
+        .actualCall("RedisModule_CreateCommand")
+        .returnIntValueOrDefault(REDISMODULE_OK);
+}
+
+void *RedisModule_Alloc(size_t bytes)
+{
+    void *buf = malloc(bytes);
+    return mock()
+        .actualCall("RedisModule_Alloc")
+        .returnPointerValueOrDefault(buf);
+}
+
+void RedisModule_Free(void *ptr)
+{
+    free(ptr);
+    mock()
+        .actualCall("RedisModule_Free");
+}
diff --git a/redismodule/tst/src/exstrings_nget_test.cpp b/redismodule/tst/src/exstrings_nget_test.cpp
new file mode 100644 (file)
index 0000000..317769f
--- /dev/null
@@ -0,0 +1,519 @@
+/*
+ * Copyright (c) 2018-2020 Nokia.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+/*
+ * This source code is part of the near-RT RIC (RAN Intelligent Controller)
+ * platform project (RICP).
+ */
+
+extern "C" {
+#include "exstringsStub.h"
+#include "redismodule.h"
+}
+
+#include "CppUTest/TestHarness.h"
+#include "CppUTestExt/MockSupport.h"
+
+#include "ut_helpers.hpp"
+
+TEST_GROUP(exstrings_nget)
+{
+    void setup()
+    {
+        mock().enable();
+        mock().ignoreOtherCalls();
+    }
+
+    void teardown()
+    {
+        mock().clear();
+        mock().disable();
+    }
+
+};
+
+void nKeysFoundMget(long keys)
+{
+    for (long i = 0 ; i < keys ; i++) {
+        mock().expectOneCall("RedisModule_CreateStringFromCallReply")
+              .andReturnValue(malloc(UT_DUMMY_BUFFER_SIZE));
+        mock().expectNCalls(2, "RedisModule_ReplyWithString");
+    }
+}
+
+void nKeysNotFoundMget(long keys)
+{
+    void* ptr = NULL;
+    mock().expectNCalls(keys, "RedisModule_CreateStringFromCallReply")
+          .andReturnValue(ptr);
+    mock().expectNoCall("RedisModule_ReplyWithString");
+}
+
+void expectNReplies(long count)
+{
+    mock().expectOneCall("RedisModule_ReplySetArrayLength")
+          .withParameter("len", 2*count);
+}
+
+void threadSafeContextLockedAndUnlockedEqualTimes()
+{
+    int locked = mock().getData("TimesThreadSafeContextWasLocked").getIntValue();
+    int unlocked = mock().getData("TimesThreadSafeContextWasUnlocked").getIntValue();
+    CHECK_EQUAL(locked, unlocked);
+}
+
+TEST(exstrings_nget, nget_atomic_automemory_enabled)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+    long keys_found_with_scan = 0;
+
+    mock().expectOneCall("RedisModule_AutoMemory");
+    mock().expectOneCall("RedisModule_CallReplyLength")
+          .andReturnValue((int)keys_found_with_scan);
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  2);
+    mock().checkExpectations();
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_parameter_number_incorrect)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().expectOneCall("RedisModule_WrongArity");
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  3);
+    CHECK_EQUAL(ret, REDISMODULE_ERR);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_3rd_parameter_was_not_equal_to_COUNT)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(4);
+    const char * not_count_literal = "NOT_COUNT";
+    size_t not_count_len = strlen(not_count_literal);
+
+    mock().expectOneCall("RedisModule_StringPtrLen")
+          .withOutputParameterReturning("len", &not_count_len, sizeof(size_t))
+          .andReturnValue((void*)not_count_literal);
+    mock().expectOneCall("RedisModule_ReplyWithError");
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  4);
+    CHECK_EQUAL(ret, REDISMODULE_ERR);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_4th_parameter_was_not_integer)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(4);
+    const char * count_literal = "COUNT";
+    size_t count_len = strlen(count_literal);
+    size_t count_number = 123;
+
+    mock().expectOneCall("RedisModule_StringPtrLen")
+          .withOutputParameterReturning("len", &count_len, sizeof(size_t))
+          .andReturnValue((void*)count_literal);
+    mock().expectOneCall("RedisModule_StringToLongLong")
+          .withOutputParameterReturning("ll", &count_number, sizeof(size_t))
+          .andReturnValue(REDISMODULE_ERR);
+    mock().expectOneCall("RedisModule_ReplyWithError");
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  4);
+    CHECK_EQUAL(ret, REDISMODULE_ERR);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_4th_parameter_was_negative)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(4);
+    const char * count_literal = "COUNT";
+    size_t count_len = strlen(count_literal);
+    size_t count_number = -123;
+
+    mock().expectOneCall("RedisModule_StringPtrLen")
+          .withOutputParameterReturning("len", &count_len, sizeof(size_t))
+          .andReturnValue((void*)count_literal);
+    mock().expectOneCall("RedisModule_StringToLongLong")
+          .withOutputParameterReturning("ll", &count_number, sizeof(size_t))
+          .andReturnValue(REDISMODULE_OK);
+    mock().expectOneCall("RedisModule_ReplyWithError");
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  4);
+    CHECK_EQUAL(ret, REDISMODULE_ERR);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_scan_returned_zero_keys)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(0);
+    expectNReplies(0);
+    mock().expectNoCall("RedisModule_Call");
+
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  2);
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_3_keys_scanned_0_keys_mget)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "MGET");
+    nKeysNotFoundMget(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    expectNReplies(0);
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  2);
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_3_keys_scanned_3_keys_mget)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "MGET");
+    nKeysFoundMget(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    expectNReplies(3);
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  2);
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_atomic_command_3_keys_scanned_2_keys_mget)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "MGET");
+    nKeysFoundMget(2);
+    nKeysNotFoundMget(1);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    expectNReplies(2);
+    int ret = NGet_Atomic_RedisCommand(&ctx, redisStrVec,  2);
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_automemory_enabled)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().setData("pthread_create_free_block_client_args", 1);
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_AutoMemory");
+
+    int ret = NGet_NoAtomic_RedisCommand(&ctx, redisStrVec,  2);
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_thread_create_success)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().setData("pthread_create_free_block_client_args", 1);
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_BlockClient");
+    mock().expectOneCall("pthread_create");
+    mock().expectNoCall("RedisModule_AbortBlock");
+
+    int ret = NGet_NoAtomic_RedisCommand(&ctx, redisStrVec,  2);
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_thread_create_fail)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_BlockClient");
+    mock().expectOneCall("pthread_create")
+          .andReturnValue(1);
+    mock().expectOneCall("RedisModule_AbortBlock");
+
+    int ret = NGet_NoAtomic_RedisCommand(&ctx, redisStrVec,  2);
+    CHECK_EQUAL(ret, REDISMODULE_OK);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_parameter_number_incorrect)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(4);
+
+    mock().expectOneCall("RedisModule_WrongArity");
+    mock().expectNoCall("RedisModule_BlockClient");
+
+    int ret = NGet_NoAtomic_RedisCommand(&ctx, redisStrVec,  3);
+
+    CHECK_EQUAL(ret, REDISMODULE_ERR);
+    mock().checkExpectations();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_threadmain_3rd_parameter_was_not_equal_to_COUNT)
+{
+    RedisModuleCtx ctx;
+    RedisModuleString ** redisStrVec = createRedisStrVec(4);
+    const char * not_count_literal = "NOT_COUNT";
+    size_t not_count_len = strlen(not_count_literal);
+
+    mock().expectOneCall("RedisModule_StringPtrLen")
+          .withOutputParameterReturning("len", &not_count_len, sizeof(size_t))
+          .andReturnValue((void*)not_count_literal);
+    mock().expectOneCall("RedisModule_ReplyWithError");
+    mock().expectNoCall("RedisModule_BlockClient");
+
+    int ret = NGet_NoAtomic_RedisCommand(&ctx, redisStrVec,  4);
+
+    CHECK_EQUAL(ret, REDISMODULE_ERR);
+
+    mock().checkExpectations();
+    threadSafeContextLockedAndUnlockedEqualTimes();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_4th_parameter_was_not_integer)
+{
+    RedisModuleCtx ctx;
+    const char * count_literal = "COUNT";
+    size_t count_len = strlen(count_literal);
+    size_t count_number = -123;
+    RedisModuleString ** redisStrVec = createRedisStrVec(4);
+
+    mock().expectOneCall("RedisModule_StringPtrLen")
+          .withOutputParameterReturning("len", &count_len, sizeof(size_t))
+          .andReturnValue((void*)count_literal);
+    mock().expectOneCall("RedisModule_StringToLongLong")
+          .withOutputParameterReturning("ll", &count_number, sizeof(size_t))
+          .andReturnValue(REDISMODULE_OK);
+    mock().expectOneCall("RedisModule_ReplyWithError");
+
+    int ret = NGet_NoAtomic_RedisCommand(&ctx, redisStrVec,  4);
+
+    CHECK_EQUAL(ret, REDISMODULE_ERR);
+    mock().checkExpectations();
+    threadSafeContextLockedAndUnlockedEqualTimes();
+
+    delete []redisStrVec;
+}
+
+typedef struct RedisModuleBlockedClientArgs {
+    RedisModuleBlockedClient *bc;
+    RedisModuleString **argv;
+    int argc;
+} RedisModuleBlockedClientArgs;
+
+TEST(exstrings_nget, nget_noatomic_threadmain_3_keys_scanned_3_keys_mget)
+{
+    RedisModuleCtx ctx;
+    RedisModuleBlockedClientArgs *bca =
+        (RedisModuleBlockedClientArgs*)RedisModule_Alloc(sizeof(RedisModuleBlockedClientArgs));
+    RedisModuleBlockedClient *bc = RedisModule_BlockClient(&ctx,NULL,NULL,NULL,0);
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    bca->bc = bc;
+    bca->argv = redisStrVec;
+    bca->argc = 2;
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "MGET");
+    nKeysFoundMget(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    expectNReplies(3);
+    mock().expectOneCall("RedisModule_FreeThreadSafeContext");
+    mock().expectOneCall("RedisModule_UnblockClient");
+
+    NGet_NoAtomic_ThreadMain((void*)bca);
+
+    mock().checkExpectations();
+    threadSafeContextLockedAndUnlockedEqualTimes();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_threadmain_3_keys_scanned_0_keys_mget)
+{
+    RedisModuleCtx ctx;
+    RedisModuleBlockedClientArgs *bca = (RedisModuleBlockedClientArgs*)malloc(sizeof(RedisModuleBlockedClientArgs));
+    RedisModuleBlockedClient *bc = RedisModule_BlockClient(&ctx,NULL,NULL,NULL,0);
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    bca->bc = bc;
+    bca->argv = redisStrVec;
+    bca->argc = 2;
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "MGET");
+    nKeysNotFoundMget(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    expectNReplies(0);
+    mock().expectOneCall("RedisModule_FreeThreadSafeContext");
+    mock().expectOneCall("RedisModule_UnblockClient");
+
+    NGet_NoAtomic_ThreadMain((void*)bca);
+
+    mock().checkExpectations();
+    threadSafeContextLockedAndUnlockedEqualTimes();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_threadmain_3_keys_scanned_2_keys_mget)
+{
+    RedisModuleCtx ctx;
+    RedisModuleBlockedClientArgs *bca = (RedisModuleBlockedClientArgs*)malloc(sizeof(RedisModuleBlockedClientArgs));
+    RedisModuleBlockedClient *bc = RedisModule_BlockClient(&ctx,NULL,NULL,NULL,0);
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    bca->bc = bc;
+    bca->argv = redisStrVec;
+    bca->argc = 2;
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(3);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "MGET");
+    nKeysNotFoundMget(1);
+    nKeysFoundMget(2);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    expectNReplies(2);
+    mock().expectOneCall("RedisModule_FreeThreadSafeContext");
+    mock().expectOneCall("RedisModule_UnblockClient");
+
+    NGet_NoAtomic_ThreadMain((void*)bca);
+
+    mock().checkExpectations();
+    threadSafeContextLockedAndUnlockedEqualTimes();
+
+    delete []redisStrVec;
+}
+
+TEST(exstrings_nget, nget_noatomic_threadmain_scan_returned_zero_keys)
+{
+    RedisModuleCtx ctx;
+    RedisModuleBlockedClientArgs *bca = (RedisModuleBlockedClientArgs*)malloc(sizeof(RedisModuleBlockedClientArgs));
+    RedisModuleBlockedClient *bc = RedisModule_BlockClient(&ctx,NULL,NULL,NULL,0);
+    RedisModuleString ** redisStrVec = createRedisStrVec(2);
+
+    bca->bc = bc;
+    bca->argv = redisStrVec;
+    bca->argc = 2;
+
+    mock().ignoreOtherCalls();
+    mock().expectOneCall("RedisModule_ReplyWithArray")
+          .withParameter("len", (long)REDISMODULE_POSTPONED_ARRAY_LEN);
+    mock().expectOneCall("RedisModule_Call")
+          .withParameter("cmdname", "SCAN");
+    returnNKeysFromScanSome(0);
+    mock().expectOneCall("RedisModule_FreeCallReply");
+    mock().expectNoCall("RedisModule_Call");
+    mock().expectOneCall("RedisModule_FreeThreadSafeContext");
+    mock().expectOneCall("RedisModule_UnblockClient");
+
+    NGet_NoAtomic_ThreadMain((void*)bca);
+
+    mock().checkExpectations();
+    threadSafeContextLockedAndUnlockedEqualTimes();
+
+    delete []redisStrVec;
+}
diff --git a/redismodule/tst/src/ut_helpers.cpp b/redismodule/tst/src/ut_helpers.cpp
new file mode 100644 (file)
index 0000000..2d63393
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2018-2020 Nokia.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+/*
+ * This source code is part of the near-RT RIC (RAN Intelligent Controller)
+ * platform project (RICP).
+ */
+
+#include <stdlib.h>
+
+#include "redismodule.h"
+#include "ut_helpers.hpp"
+
+#include <CppUTest/TestHarness.h>
+#include <CppUTestExt/MockSupport.h>
+
+RedisModuleString **createRedisStrVec(size_t size)
+{
+    RedisModuleString ** redisStrVec = new RedisModuleString*[size];
+    for (size_t i = 0 ; i < size ; i++) {
+        redisStrVec[i] = (RedisModuleString *)UT_DUMMY_PTR_ADDRESS;
+    }
+    return redisStrVec;
+}
+
+void returnNKeysFromScanSome(long keys)
+{
+    mock().expectOneCall("RedisModule_CallReplyLength")
+          .andReturnValue((int)keys);
+    for (long i = 0 ; i < keys ; i++) {
+        mock().expectOneCall("RedisModule_CreateStringFromCallReply")
+              .andReturnValue(malloc(UT_DUMMY_BUFFER_SIZE));
+    }
+}
+