From 8a80bed29143d2f78d40e175b9f6cc00d599f72c Mon Sep 17 00:00:00 2001 From: Ryan Joseph Date: Wed, 16 Aug 2023 04:04:40 +0000 Subject: [PATCH] Add many more unit tests, a few more integration tests & pubsub tests --- .github/workflows/tests.yml | 16 +- Redis.h | 4 +- RedisInternal.cpp | 8 + RedisInternal.h | 5 + test/ArduinoRedisTestBase.h | 17 + test/IntegrationTestBase.h | 89 +++++ test/Makefile | 21 +- test/integration/integration-tests.ino | 348 +++++++++----------- test/pubsub/Makefile | 16 + test/pubsub/PubSubCommon.h | 1 + test/pubsub/publisher/Makefile | 6 + test/pubsub/publisher/publisher-tests.ino | 44 +++ test/pubsub/subscriber/Makefile | 6 + test/pubsub/subscriber/subscriber-tests.ino | 41 +++ test/unit/unit-tests.ino | 173 ++++++++-- 15 files changed, 560 insertions(+), 235 deletions(-) create mode 100644 test/ArduinoRedisTestBase.h create mode 100644 test/IntegrationTestBase.h create mode 100644 test/pubsub/Makefile create mode 100644 test/pubsub/PubSubCommon.h create mode 100644 test/pubsub/publisher/Makefile create mode 100644 test/pubsub/publisher/publisher-tests.ino create mode 100644 test/pubsub/subscriber/Makefile create mode 100644 test/pubsub/subscriber/subscriber-tests.ino diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 905973c..8d9efad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,9 +15,15 @@ jobs: - uses: actions/checkout@v3 - name: Submodule init run: git submodule init && git submodule update --recursive - - name: Install redis - run: | - sudo apt -y install redis && \ - sudo systemctl start redis - - name: Build & run tests + - name: Add packages.redis.io key + run: curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg + - name: Install packages.redis.io key + run: echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list + - name: Install depedencies + run: sudo apt -y update && sudo apt -y install redis parallel + - name: Redis version check + run: redis-cli info | grep redis_version + - name: Build tests + run: cd test && make + - name: Run tests run: cd test && make run diff --git a/Redis.h b/Redis.h index fc011b0..041836d 100644 --- a/Redis.h +++ b/Redis.h @@ -300,7 +300,7 @@ class Redis /** Returns the index of the first matched `element` when scanning from head to tail * @param key * @param element - * @return The index of the first element, or nil when key does not exist. + * @return The index of the first element, or (INT_MAX - 0x0f) when key does not exist. */ int lpos(const char *key, const char *element); @@ -609,7 +609,7 @@ class Redis * @param returnString * @returns true if returnString is the nil return value, false otherwise */ - bool isNilReturn(String returnString) { return returnString == "(nil)"; } + static bool isNilReturn(String returnString) { return returnString == "(nil)"; } /** * Enters subscription mode and subscribes to all channels/patterns setup via `subscribe()`/`psubscribe()`. diff --git a/RedisInternal.cpp b/RedisInternal.cpp index 68dcca2..238df39 100644 --- a/RedisInternal.cpp +++ b/RedisInternal.cpp @@ -57,8 +57,16 @@ String RedisBulkString::RESP() void RedisArray::init(Client &client) { + // Null array https://redis.io/docs/reference/protocol-spec/#null-arrays + if (data.toInt() == -1) + { + return; + } + for (int i = 0; i < data.toInt(); i++) + { add(RedisObject::parseType(client)); + } } RedisArray::operator std::vector>() const diff --git a/RedisInternal.h b/RedisInternal.h index 79e6091..ce29d67 100644 --- a/RedisInternal.h +++ b/RedisInternal.h @@ -102,6 +102,11 @@ class RedisArray : public RedisObject operator std::vector>() const; + /** Returns false if this is a "Null Array" (https://redis.io/docs/reference/protocol-spec/#null-arrays), + * true otherwise (including if the array is empty!) + */ + bool isNilReturn() const { return data.toInt() == -1; } + virtual void init(Client &client) override; virtual String RESP() override; diff --git a/test/ArduinoRedisTestBase.h b/test/ArduinoRedisTestBase.h new file mode 100644 index 0000000..9df7153 --- /dev/null +++ b/test/ArduinoRedisTestBase.h @@ -0,0 +1,17 @@ +#define ArduinoRedisTestCommonSetupAndLoop \ + void setup() \ + { \ + const char *include = std::getenv("ARDUINO_REDIS_TEST_INCLUDE"); \ + \ + if (!include) \ + { \ + include = "*"; \ + } \ + \ + TestRunner::include(include); \ + } \ + \ + void loop() \ + { \ + TestRunner::run(); \ + } diff --git a/test/IntegrationTestBase.h b/test/IntegrationTestBase.h new file mode 100644 index 0000000..61699de --- /dev/null +++ b/test/IntegrationTestBase.h @@ -0,0 +1,89 @@ +#include +#include + +#include + +#include + +#include "./TestRawClient.h" + +const String gKeyPrefix = String("__arduino_redis__test"); + +#define prefixKey(k) (String(gKeyPrefix + "." + k)) +#define prefixKeyCStr(k) (String(gKeyPrefix + "." + k).c_str()) + +#define defineKey(KEY) \ + auto __ks = prefixKey(KEY); \ + auto key = __ks.c_str(); + +std::pair, std::shared_ptr> NewConnection() +{ + std::pair, std::shared_ptr> ret_val = std::make_pair(nullptr, nullptr); + + const char *r_host = std::getenv("ARDUINO_REDIS_TEST_HOST"); + if (!r_host) + { + r_host = "localhost"; + } + + const char *r_port = std::getenv("ARDUINO_REDIS_TEST_PORT"); + if (!r_port) + { + r_port = "6379"; + } + + auto r_auth = std::getenv("ARDUINO_REDIS_TEST_AUTH"); + + auto client = std::make_shared(); + if (client->connect(r_host, std::atoi(r_port)) == 0) + { + return ret_val; + } + + Client &c_ref = *client.get(); + auto r = std::make_shared(c_ref); + if (r_auth) + { + auto auth_ret = r->authenticate(r_auth); + if (auth_ret == RedisReturnValue::RedisAuthFailure) + { + return ret_val; + } + } + + return std::make_pair(client, r); +} + +// Any testF(IntegrationTests, ...) defined will automatically have scope access to `r`, the redis client +class IntegrationTests : public aunit::TestOnce +{ +protected: + void setup() override + { + aunit::TestOnce::setup(); + auto conn_ret = NewConnection(); + assertNotEqual(conn_ret.first.get(), nullptr); + assertNotEqual(conn_ret.second.get(), nullptr); + client = std::move(conn_ret.first); + r = std::move(conn_ret.second); + } + + void teardown() override + { + auto keys = RedisCommand("KEYS", ArgList{String(gKeyPrefix + "*").c_str()}).issue(*client); + + if (keys->type() == RedisObject::Type::Array) + { + std::vector as_strings = *dynamic_cast(keys.get()); + for (const auto &key : as_strings) + { + r->del(key.c_str()); + } + } + + aunit::TestOnce::teardown(); + } + + std::shared_ptr client; + std::shared_ptr r; +}; \ No newline at end of file diff --git a/test/Makefile b/test/Makefile index ad3b6b5..5c904ef 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,17 +1,26 @@ -TESTS = unit/unit-tests.out integration/integration-tests.out +TESTS = unit/unit-tests.out integration/integration-tests.out pubsub/subscriber/subscriber-tests.out pubsub/publisher/publisher-tests.out test : $(TESTS) -unit/unit-tests.out: +unit/unit-tests.out: unit/unit-tests.ino ../Redis.h ../Redis.cpp ../RedisInternal.h ../RedisInternal.cpp cd unit && make -integration/integration-tests.out: +integration/integration-tests.out: integration/integration-tests.ino ../Redis.h ../Redis.cpp ../RedisInternal.h ../RedisInternal.cpp cd integration && make -run: clean test +pubsub/subscriber/subscriber-tests.out: pubsub/subscriber/subscriber-tests.ino ../Redis.h ../Redis.cpp ../RedisInternal.h ../RedisInternal.cpp + cd pubsub && make + +pubsub/publisher/publisher-tests.out: pubsub/publisher/publisher-tests.ino ../Redis.h ../Redis.cpp ../RedisInternal.h ../RedisInternal.cpp + cd pubsub && make + +run: test pubsub + cd pubsub && make run ./unit/unit-tests.out ./integration/integration-tests.out clean: - rm -f unit/unit-tests.out - rm -f integration/integration-tests.out \ No newline at end of file + rm -f ../*.o + cd unit && make clean + cd integration && make clean + cd pubsub && make clean \ No newline at end of file diff --git a/test/integration/integration-tests.ino b/test/integration/integration-tests.ino index 343fe99..4a4eaab 100644 --- a/test/integration/integration-tests.ino +++ b/test/integration/integration-tests.ino @@ -7,89 +7,13 @@ #include #include -#include -#include "../TestRawClient.h" +#include "../ArduinoRedisTestBase.h" +#include "../IntegrationTestBase.h" using namespace aunit; -const String gKeyPrefix = String("__arduino_redis__test"); - -void setup() -{ - const char *include = std::getenv("ARDUINO_REDIS_TEST_INCLUDE"); - - if (!include) - { - include = "*"; - } - - TestRunner::include(include); -} - -void loop() -{ - TestRunner::run(); -} - -class IntegrationTests : public TestOnce -{ -protected: - void setup() override - { - TestOnce::setup(); - - const char *r_host = std::getenv("ARDUINO_REDIS_TEST_HOST"); - if (!r_host) - { - r_host = "localhost"; - } - - const char *r_port = std::getenv("ARDUINO_REDIS_TEST_PORT"); - if (!r_port) - { - r_port = "6379"; - } - - auto r_auth = std::getenv("ARDUINO_REDIS_TEST_AUTH"); - - assertNotEqual(client.connect(r_host, std::atoi(r_port)), 0); - r = std::make_shared(client); - - if (r_auth) - { - auto auth_ret = r->authenticate(r_auth); - assertNotEqual(auth_ret, RedisReturnValue::RedisAuthFailure); - } - } - - void teardown() override - { - auto keys = RedisCommand("KEYS", ArgList{String(gKeyPrefix + "*").c_str()}).issue(client); - - if (keys->type() == RedisObject::Type::Array) - { - std::vector as_strings = *dynamic_cast(keys.get()); - for (const auto& key : as_strings) { - r->del(key.c_str()); - } - } - - TestOnce::teardown(); - } - - TestRawClient client; - std::shared_ptr r; -}; - -#define prefixKey(k) (String(gKeyPrefix + "." + k)) -#define prefixKeyCStr(k) (String(gKeyPrefix + "." + k).c_str()) - -#define defineKey(KEY) \ - auto __ks = prefixKey(KEY); \ - auto k = __ks.c_str(); - -// Any testF(IntegrationTests, ...) defined will automatically have scope access to `r`, the redis client +ArduinoRedisTestCommonSetupAndLoop; testF(IntegrationTests, set) { @@ -100,99 +24,101 @@ testF(IntegrationTests, setget) { defineKey("setget"); - assertEqual(r->set(k, "!"), true); - assertEqual(r->get(k), String("!")); + assertEqual(r->set(key, "!"), true); + assertEqual(r->get(key), String("!")); } testF(IntegrationTests, expire) { defineKey("expire"); - assertEqual(r->set(k, "E"), true); - assertEqual(r->expire(k, 5), true); - assertMore(r->ttl(k), 0); + assertEqual(r->set(key, "E"), true); + assertEqual(r->expire(key, 5), true); + assertMore(r->ttl(key), 0); } testF(IntegrationTests, expire_at) { defineKey("expire_at"); - assertEqual(r->set(k, "E"), true); - assertEqual(r->expire_at(k, 0), true); - assertEqual(r->isNilReturn(r->get(k)), true); + assertEqual(r->set(key, "E"), true); + assertEqual(r->expire_at(key, 0), true); + assertEqual(Redis::isNilReturn(r->get(key)), true); } -testF(IntegrationTests, pexpire) +testF(IntegrationTests, pexpire_and_pttl) { defineKey("pexpire"); - assertEqual(r->set(k, "PE"), true); - assertEqual(r->pexpire(k, 5000), true); - assertMore(r->pttl(k), 0); + assertEqual(r->set(key, "PE"), true); + assertEqual(r->pexpire(key, 5000), true); + assertMore(r->pttl(key), 0); } testF(IntegrationTests, pexpire_at) { defineKey("pexpire_at"); - assertEqual(r->set(k, "PEA"), true); - assertEqual(r->pexpire_at(k, 0), true); - assertEqual(r->isNilReturn(r->get(k)), true); + assertEqual(r->set(key, "PEA"), true); + assertEqual(r->pexpire_at(key, 0), true); + assertEqual(Redis::isNilReturn(r->get(key)), true); } testF(IntegrationTests, ttl) { defineKey("ttl"); - assertEqual(r->set(k, "T"), true); - assertEqual(r->expire(k, 100), true); - assertMore(r->ttl(k), 99); + assertEqual(r->set(key, "T"), true); + assertEqual(r->expire(key, 100), true); + assertMore(r->ttl(key), 99); } testF(IntegrationTests, pttl) { defineKey("pttl"); - assertEqual(r->set(k, "PT"), true); - assertEqual(r->pexpire(k, 1000), true); + assertEqual(r->set(key, "PT"), true); + assertEqual(r->pexpire(key, 1000), true); // allows for <250ms latency between pexpire & pttl calls - assertMore(r->pttl(k), 750); + assertMore(r->pttl(key), 750); } testF(IntegrationTests, wait_for_expiry) { defineKey("wait_for_expiry"); - assertEqual(r->set(k, "WFE"), true); - assertEqual(r->expire(k, 1), true); + assertEqual(r->set(key, "WFE"), true); + assertEqual(r->expire(key, 1), true); delay(1250); // again, allow <250ms - assertEqual(r->ttl(k), -2); + assertEqual(r->ttl(key), -2); } testF(IntegrationTests, wait_for_expiry_ms) { defineKey("wait_for_expiry_ms"); - assertEqual(r->set(k, "PWFE"), true); - assertEqual(r->pexpire(k, 1000), true); + assertEqual(r->set(key, "PWFE"), true); + assertEqual(r->pexpire(key, 1000), true); delay(1250); // again, allow <250ms - assertEqual(r->ttl(k), -2); + assertEqual(r->ttl(key), -2); } -testF(IntegrationTests, hset) +testF(IntegrationTests, hset_hexists) { defineKey("hset"); - assertEqual(r->hset(k, "TF", "H!"), true); - assertEqual(r->hexists(k, "TF"), true); + assertEqual(r->hset(key, "TF", "H!"), true); + assertEqual(r->hexists(key, "TF"), true); + assertEqual(r->hdel(key, "TF"), true); + assertEqual(r->hexists(key, "TF"), false); } testF(IntegrationTests, hsetget) { defineKey("hsetget"); - assertEqual(r->hset(k, "TF", "HH"), true); - assertEqual(r->hget(k, "TF"), "HH"); + assertEqual(r->hset(key, "TF", "HH"), true); + assertEqual(r->hget(key, "TF"), "HH"); } testF(IntegrationTests, hlen) @@ -202,10 +128,10 @@ testF(IntegrationTests, hlen) for (int i = 0; i < 10; i++) { auto fv = String(i); - r->hset(k, String("field-" + fv).c_str(), fv.c_str()); + r->hset(key, String("field-" + fv).c_str(), fv.c_str()); } - assertEqual(r->hlen(k), 10); + assertEqual(r->hlen(key), 10); } testF(IntegrationTests, hlen2) @@ -213,31 +139,30 @@ testF(IntegrationTests, hlen2) defineKey("hlen2"); auto lim = random(64) + 64; - auto key = k + String(":hlen"); for (auto i = 0; i < lim; i++) { - r->hset(key.c_str(), (String("hlen_test__") + String(i)).c_str(), String(i).c_str()); + r->hset(key, (String("hlen_test__") + String(i)).c_str(), String(i).c_str()); } - assertEqual(r->hlen(key.c_str()), (int)lim); + assertEqual(r->hlen(key), (int)lim); } testF(IntegrationTests, hstrlen) { defineKey("hstrlen"); - r->hset(k, "hsr", k); - assertEqual(r->hstrlen(k, "hsr"), (int)strlen(k)); + r->hset(key, "hsr", key); + assertEqual(r->hstrlen(key, "hsr"), (int)strlen(key)); } testF(IntegrationTests, hdel) { defineKey("hdel"); - assertEqual(r->hset(k, "delete_me", k), true); - assertEqual(r->hdel(k, "delete_me"), true); - assertEqual(r->isNilReturn(r->hget(k, "delete_me")), true); + assertEqual(r->hset(key, "delete_me", key), true); + assertEqual(r->hdel(key, "delete_me"), true); + assertEqual(Redis::isNilReturn(r->hget(key, "delete_me")), true); } testF(IntegrationTests, append) @@ -249,103 +174,153 @@ testF(IntegrationTests, exists) { defineKey("exists"); - assertEqual(r->set(k, k), true); - assertEqual(r->exists(k), true); + assertEqual(r->set(key, key), true); + assertEqual(r->exists(key), true); } testF(IntegrationTests, lpush) { defineKey("lpush"); - auto pushRes = r->lpush(k, k); + auto pushRes = r->lpush(key, key); assertEqual(pushRes, 1); - assertEqual(r->llen(k), 1); - assertEqual(r->lindex(k, pushRes - 1), String(k)); + assertEqual(r->llen(key), 1); + assertEqual(r->lindex(key, pushRes - 1), String(key)); } testF(IntegrationTests, rpush) { defineKey("rpush"); - auto pushRes = r->rpush(k, k); + auto pushRes = r->rpush(key, key); assertEqual(pushRes, 1); - assertEqual(r->llen(k), 1); - assertEqual(r->lindex(k, pushRes - 1), String(k)); + assertEqual(r->llen(key), 1); + assertEqual(r->lindex(key, pushRes - 1), String(key)); } testF(IntegrationTests, lrem) { defineKey("lrem"); - auto pushRes = r->lpush(k, k); + auto pushRes = r->lpush(key, key); assertEqual(pushRes, 1); - assertEqual(r->llen(k), 1); - assertEqual(r->lindex(k, pushRes - 1), String(k)); - assertEqual(r->lrem(k, 1, k), 1); + assertEqual(r->llen(key), 1); + assertEqual(r->lindex(key, pushRes - 1), String(key)); + assertEqual(r->lrem(key, 1, key), 1); +} + +testF(IntegrationTests, lrem2) +{ + defineKey("lrem2"); + assertEqual(r->rpush(key, "foo"), 1); + assertEqual(r->rpush(key, "foo"), 2); + assertEqual(r->rpush(key, "foo"), 3); + assertEqual(r->rpush(key, "bar"), 4); + assertEqual(r->rpush(key, "foo"), 5); + assertEqual(r->rpush(key, "bar"), 6); + assertEqual(r->rpush(key, "baz"), 7); + assertEqual(r->llen(key), 7); + + assertEqual(r->lrem(key, 1, "foo"), "foo"); + assertEqual(r->llen(key), 6); + assertEqual(r->lrem(key, 2, "foo"), "foo"); + assertEqual(r->llen(key), 4); + + assertEqual(r->lrem(key, 1, "baz"), "baz"); + assertEqual(r->llen(key), 3); + assertEqual(r->lrem(key, 1, "baz"), 0 /* this needs to be fixed!! Issue #74 */); + assertEqual(r->llen(key), 3); + + assertEqual(r->lrem(key, 0, "bar"), "bar"); + assertEqual(r->llen(key), 1); + assertEqual(r->lrem(key, 0, "bar"), 0 /* this needs to be fixed!! Issue #74 */); + assertEqual(r->llen(key), 1); + + assertEqual(r->lpos(key, "foo"), 0); } testF(IntegrationTests, lpop) { defineKey("lpop"); - assertEqual(r->lpush(k, k), 1); - assertEqual(r->llen(k), 1); - assertEqual(r->lpop(k), String(k)); - assertEqual(r->llen(k), 0); + assertEqual(r->lpush(key, key), 1); + assertEqual(r->llen(key), 1); + assertEqual(r->lpop(key), String(key)); + assertEqual(r->llen(key), 0); } testF(IntegrationTests, lset) { defineKey("lset"); - assertEqual(r->lpush(k, "foo"), 1); - assertEqual(r->lset(k, 0, k), true); - assertEqual(r->lindex(k, 0), String(k)); + assertEqual(r->lpush(key, "foo"), 1); + assertEqual(r->lset(key, 0, key), true); + assertEqual(r->lindex(key, 0), String(key)); } testF(IntegrationTests, ltrim) { defineKey("ltrim"); - assertEqual(r->lpush(k, "bar"), 1); - assertEqual(r->lpush(k, "bar"), 2); - assertEqual(r->ltrim(k, -1, 0), true); - assertEqual(r->llen(k), 0); + assertEqual(r->lpush(key, "bar"), 1); + assertEqual(r->lpush(key, "bar"), 2); + assertEqual(r->ltrim(key, -1, 0), true); + assertEqual(r->llen(key), 0); +} + +testF(IntegrationTests, lindex) +{ + defineKey("lindex"); + assertEqual(r->rpush(key, "foo"), 1); + assertEqual(r->rpush(key, "bar"), 2); + assertEqual(r->lindex(key, 0), "foo"); + assertEqual(r->lindex(key, 1), "bar"); + assertEqual(Redis::isNilReturn(r->lindex(key, 2)), true); +} + +testF(IntegrationTests, lpos) +{ + defineKey("lpos"); + assertEqual(r->rpush(key, "foo"), 1); + assertEqual(r->rpush(key, "bar"), 2); + assertEqual(r->lpos(key, "foo"), 0); + assertEqual(r->lpos(key, "bar"), 1); + assertEqual(r->lpos(key, "baz"), 2147483407 /* this needs to be fixed!! Issue #74 */); } testF(IntegrationTests, rpop) { defineKey("rpop"); - assertEqual(r->lpush(k, k), 1); - assertEqual(r->llen(k), 1); - assertEqual(r->rpop(k), String(k)); - assertEqual(r->llen(k), 0); + assertEqual(r->lpush(key, key), 1); + assertEqual(r->llen(key), 1); + assertEqual(r->rpop(key), String(key)); + assertEqual(r->llen(key), 0); } testF(IntegrationTests, hgetnil) { auto nothing = r->hget(prefixKeyCStr("hgetnil"), "doesNotExist"); assertEqual(nothing, String("(nil)")); - assertEqual(r->isNilReturn(nothing), true); + assertEqual(Redis::isNilReturn(nothing), true); } testF(IntegrationTests, lindexnil) { auto nothing = r->lindex(prefixKeyCStr("lindexnil"), 0); assertEqual(nothing, String("(nil)")); - assertEqual(r->isNilReturn(nothing), true); + assertEqual(Redis::isNilReturn(nothing), true); } testF(IntegrationTests, op_vec_string_issue67) { defineKey("op_vec_string_issue67"); - assertEqual(r->rpush(k, "1"), 1); - assertEqual(r->rpush(k, "2"), 2); - assertEqual(r->rpush(k, "3"), 3); + assertEqual(r->rpush(key, "1"), 1); + assertEqual(r->rpush(key, "2"), 2); + assertEqual(r->rpush(key, "3"), 3); - auto list = r->lrange(k, 0, -1); + auto list = r->lrange(key, 0, -1); assertEqual((int)list.size(), 3); assertEqual(list[0], "1"); @@ -353,43 +328,28 @@ testF(IntegrationTests, op_vec_string_issue67) assertEqual(list[2], "3"); } -/* TODO: re-factor this to something that can be automated!! - -#define SUBSCRIBE_TESTS 0 -#define SUBSCRIBE_TESTS_ONLY 0 -#define BOOTSTRAP_PUB_DELAY_SECS 30 -std::map g_SubscribeTests{ - {"subscribe-simple", [](Redis *r, const char *k) - { - auto bsChan = String(gKeyPrefix + ":bootstrap"); - auto ackStr = String(k) + ":" + String(random(INT_MAX)); - - Serial.printf("Publishing test channel name to \"%s\" in %d seconds...\n", bsChan.c_str(), BOOTSTRAP_PUB_DELAY_SECS); - delay(BOOTSTRAP_PUB_DELAY_SECS * 1000); - r->publish(bsChan.c_str(), k); - Serial.printf("You must publish \"%s\" back to the published channel to complete the test\n", ackStr.c_str()); - - r->setTestContext(ackStr.c_str()); - r->subscribe(k); - - auto subRet = r->startSubscribing( - [](Redis *rconn, String chan, String message) - { - auto success = message == String((char *)rconn->getTestContext()); - - if (!success) - { - Serial.printf("Message RX'ed but was not properly formed!\n"); - } - - rconn->setTestContext((const void *)success); - rconn->stopSubscribing(); - }, - [](Redis *rconn, RedisMessageError err) - { - Serial.printf("!! subscribe error: %d\n", err); - }); - - return subRet == RedisSubscribeSuccess && r->getTestContext() == (const void *)1; - }}}; -*/ \ No newline at end of file +testF(IntegrationTests, del) +{ + defineKey("del"); + assertEqual(r->set(key, "del"), true); + assertEqual(r->del(key), true); + assertEqual(r->del(key), false); +} + +testF(IntegrationTests, persist) +{ + defineKey("persist"); + assertEqual(r->set(key, "persist"), true); + assertEqual(r->expire(key, 2), true); + delay(1000); + assertEqual(r->persist(key), true); + delay(2000); + assertEqual(r->get(key), "persist"); +} + +testF(IntegrationTests, hsetnx) +{ + defineKey("hsetnx"); + assertEqual(r->hsetnx(key, "hsetnx", key), true); + assertEqual(r->hsetnx(key, "hsetnx", key), false); +} \ No newline at end of file diff --git a/test/pubsub/Makefile b/test/pubsub/Makefile new file mode 100644 index 0000000..8bcab86 --- /dev/null +++ b/test/pubsub/Makefile @@ -0,0 +1,16 @@ +TESTS = subscriber/subscriber-tests.out publisher/publisher-tests.out + +test : $(TESTS) + +subscriber/subscriber-tests.out: subscriber/subscriber-tests.ino + cd subscriber && make + +publisher/publisher-tests.out: publisher/publisher-tests.ino + cd publisher && make + +run: test + bash -c 'echo -e "./subscriber/subscriber-tests.out\n./publisher/publisher-tests.out" | parallel --delay=1' + +clean: + cd subscriber && make clean + cd publisher && make clean diff --git a/test/pubsub/PubSubCommon.h b/test/pubsub/PubSubCommon.h new file mode 100644 index 0000000..b7cb263 --- /dev/null +++ b/test/pubsub/PubSubCommon.h @@ -0,0 +1 @@ +#define ChannelName(prefix) (gKeyPrefix + ":subscribe_simple") diff --git a/test/pubsub/publisher/Makefile b/test/pubsub/publisher/Makefile new file mode 100644 index 0000000..2050ddd --- /dev/null +++ b/test/pubsub/publisher/Makefile @@ -0,0 +1,6 @@ +APP_NAME := publisher-tests +ARDUINO_LIBS := ../../ AUnit +# EPOXY_CORE_ESP8266 is used because it defines Client when the standard AVR core doesn't ...? +# All we need is that interface and the String implementation, so this "works". Good enough! +EPOXY_CORE := EPOXY_CORE_ESP8266 +include ../../deps/EpoxyDuino/EpoxyDuino.mk \ No newline at end of file diff --git a/test/pubsub/publisher/publisher-tests.ino b/test/pubsub/publisher/publisher-tests.ino new file mode 100644 index 0000000..f594b30 --- /dev/null +++ b/test/pubsub/publisher/publisher-tests.ino @@ -0,0 +1,44 @@ +#include +#include + +#include +#include + +#include + +#include "../../../ArduinoRedisTestBase.h" +#include "../../../IntegrationTestBase.h" +#include "../PubSubCommon.h" + +#include + +using namespace aunit; + +ArduinoRedisTestCommonSetupAndLoop; + +testF(IntegrationTests, publisher) +{ + randomSeed(time(NULL)); + auto sub_key = ChannelName(gKeyPrefix); + std::stringstream ss; + ss << sub_key.c_str() << ":" << std::hex << random(pow(2, 31), pow(2, 60)); + + r->publish(sub_key.c_str(), ss.str().c_str()); + assertEqual(r->subscribe(ss.str().c_str()), true); + auto subRet = r->startSubscribing( + [](Redis *rconn, String chan, String message) + { + (void)chan; + auto cur_time = time(NULL); + auto message_time = std::atoi(message.c_str()); + rconn->stopSubscribing(); + + if (message_time < cur_time - 1 || message_time > cur_time + 1) + { + printf("Time match failure! %ld vs %d\n", cur_time, message_time); + exit(-1); // can't use asserts here...? this "works" + } + }); + + assertEqual(subRet, RedisSubscribeSuccess); +} \ No newline at end of file diff --git a/test/pubsub/subscriber/Makefile b/test/pubsub/subscriber/Makefile new file mode 100644 index 0000000..d959622 --- /dev/null +++ b/test/pubsub/subscriber/Makefile @@ -0,0 +1,6 @@ +APP_NAME := subscriber-tests +ARDUINO_LIBS := ../../ AUnit +# EPOXY_CORE_ESP8266 is used because it defines Client when the standard AVR core doesn't ...? +# All we need is that interface and the String implementation, so this "works". Good enough! +EPOXY_CORE := EPOXY_CORE_ESP8266 +include ../../deps/EpoxyDuino/EpoxyDuino.mk \ No newline at end of file diff --git a/test/pubsub/subscriber/subscriber-tests.ino b/test/pubsub/subscriber/subscriber-tests.ino new file mode 100644 index 0000000..9d49f2b --- /dev/null +++ b/test/pubsub/subscriber/subscriber-tests.ino @@ -0,0 +1,41 @@ +#include +#include + +#include +#include + +#include + +#include "../../../ArduinoRedisTestBase.h" +#include "../../../IntegrationTestBase.h" +#include "../PubSubCommon.h" + +using namespace aunit; + +ArduinoRedisTestCommonSetupAndLoop; + +testF(IntegrationTests, subscriber) +{ + auto sub_key = ChannelName(gKeyPrefix); + assertEqual(r->subscribe(sub_key.c_str()), true); + auto subRet = r->startSubscribing( + [](Redis *rconn, String chan, String message) + { + (void)chan; + rconn->stopSubscribing(); + delay(1000); + + auto send_client = NewConnection(); + if (!send_client.second.get()) + { + return; + } + + auto cur_time = time(NULL); + std::stringstream ss; + ss << cur_time; + send_client.second->publish(message.c_str(), ss.str().c_str()); + }); + + assertEqual(subRet, RedisSubscribeSuccess); +} \ No newline at end of file diff --git a/test/unit/unit-tests.ino b/test/unit/unit-tests.ino index 8db8598..0259595 100644 --- a/test/unit/unit-tests.ino +++ b/test/unit/unit-tests.ino @@ -6,38 +6,32 @@ #include +#include "../ArduinoRedisTestBase.h" #include "../TestDirectClient.h" using namespace aunit; -void setup() -{ - const char *include = std::getenv("ARDUINO_REDIS_TEST_INCLUDE"); - - if (!include) - { - include = "*"; - } +ArduinoRedisTestCommonSetupAndLoop; - TestRunner::include(include); -} - -void loop() -{ - TestRunner::run(); -} +// creates a local TestDirectClient named `__client` and a local +// std::shared_ptr named `parsed` +// defined as a macro so that it can take advantage of AUnit functions +// only available in test-function scope. +#define parseRESP2String(RESPtoParse) \ + TestDirectClient __client(RESPtoParse); \ + auto parsed = RedisObject::parseTypeNonBlocking(__client); \ + assertNotEqual(parsed.get(), nullptr); // taken directly from the "Nested arrays" portion of // https://redis.io/docs/reference/protocol-spec/#resp-arrays -// it encodes a two-element Array consisting of an Array that -// contains three Integers (1, 2, 3) and an array of a Simple +// it encodes a two-element Array consisting of an Array that +// contains three Integers (1, 2, 3) and an array of a Simple // String and an Error const std::string nested_array_vector = "*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Hello\r\n-World\r\n"; test(UnitTests, nested_array) { - TestDirectClient client(nested_array_vector); - auto parsed = RedisObject::parseTypeNonBlocking(client); + parseRESP2String(nested_array_vector); assertEqual(parsed->type(), RedisObject::Type::Array); std::vector> ptrs = *(RedisArray *)parsed.get(); @@ -53,9 +47,9 @@ test(UnitTests, nested_array) assertEqual(first_ptrs[0]->type(), RedisObject::Type::Integer); assertEqual(first_ptrs[1]->type(), RedisObject::Type::Integer); assertEqual(first_ptrs[2]->type(), RedisObject::Type::Integer); - assertEqual(((String)*(RedisObject*)first_ptrs[0].get()).c_str(), "1"); - assertEqual(((String)*(RedisObject*)first_ptrs[1].get()).c_str(), "2"); - assertEqual(((String)*(RedisObject*)first_ptrs[2].get()).c_str(), "3"); + assertEqual(((String) * (RedisObject *)first_ptrs[0].get()).c_str(), "1"); + assertEqual(((String) * (RedisObject *)first_ptrs[1].get()).c_str(), "2"); + assertEqual(((String) * (RedisObject *)first_ptrs[2].get()).c_str(), "3"); // second, an array of a Simple String and an Error auto second = ptrs[1]; @@ -65,21 +59,144 @@ test(UnitTests, nested_array) assertEqual(second_ptrs.size(), (size_t)2); assertEqual(second_ptrs[0]->type(), RedisObject::Type::SimpleString); assertEqual(second_ptrs[1]->type(), RedisObject::Type::Error); - assertEqual(((String)*(RedisObject*)second_ptrs[0].get()).c_str(), "Hello"); - assertEqual(((String)*(RedisObject*)second_ptrs[1].get()).c_str(), "World"); + assertEqual(((String) * (RedisObject *)second_ptrs[0].get()).c_str(), "Hello"); + assertEqual(((String) * (RedisObject *)second_ptrs[1].get()).c_str(), "World"); } test(UnitTests, nested_array_as_strings) { - TestDirectClient client(nested_array_vector); - auto parsed = RedisObject::parseTypeNonBlocking(client); + parseRESP2String(nested_array_vector); assertEqual(parsed->type(), RedisObject::Type::Array); std::vector as_strings = *(RedisArray *)parsed.get(); assertEqual(as_strings.size(), (size_t)5); - + auto expected = std::vector{"1", "2", "3", "Hello", "World"}; - for (std::vector::size_type i = 0; i < as_strings.size(); i++) { + for (std::vector::size_type i = 0; i < as_strings.size(); i++) + { assertEqual(as_strings[i].c_str(), expected[i].c_str()); } +} + +test(UnitTests, empty_array) +{ + parseRESP2String("*0\r\n"); + assertEqual(parsed->type(), RedisObject::Type::Array); + assertEqual(dynamic_cast(parsed.get())->operator std::vector().size(), (size_t)0); +} + +test(UnitTests, mixed_types_array) +{ + parseRESP2String("*3\r\n:1\r\n+OK\r\n$5\r\nhello\r\n"); + auto vec = dynamic_cast(parsed.get())->operator std::vector>(); + assertEqual(vec.size(), (size_t)3); + + assertEqual(vec[0]->type(), RedisObject::Type::Integer); + assertEqual(dynamic_cast(vec[0].get())->operator int(), 1); + + assertEqual(vec[1]->type(), RedisObject::Type::SimpleString); + assertEqual(dynamic_cast(vec[1].get())->operator String(), String("OK")); + + assertEqual(vec[2]->type(), RedisObject::Type::BulkString); + assertEqual(dynamic_cast(vec[2].get())->operator String(), String("hello")); +} + +test(UnitTests, null_array) +{ + parseRESP2String("*-1\r\n"); + assertEqual(parsed->type(), RedisObject::Type::Array); + auto asArray = dynamic_cast(parsed.get()); + auto vec = asArray->operator std::vector>(); + assertEqual(asArray->isNilReturn(), true); +} + +test(UnitTests, array_with_null_bulkstring) +{ + parseRESP2String("*3\r\n$5\r\nhello\r\n$-1\r\n$5\r\nworld\r\n"); + assertEqual(parsed->type(), RedisObject::Type::Array); + auto vec = dynamic_cast(parsed.get())->operator std::vector>(); + assertEqual(vec.size(), (size_t)3); + + assertEqual(vec[0]->type(), RedisObject::Type::BulkString); + assertEqual(dynamic_cast(vec[0].get())->operator String(), String("hello")); + + assertEqual(vec[1]->type(), RedisObject::Type::BulkString); + assertEqual(Redis::isNilReturn(dynamic_cast(vec[1].get())->operator String()), true); + + assertEqual(vec[2]->type(), RedisObject::Type::BulkString); + assertEqual(dynamic_cast(vec[2].get())->operator String(), String("world")); +} + +test(UnitTests, simple_string_ok) +{ + parseRESP2String("+OK\r\n"); + assertEqual(parsed->type(), RedisObject::Type::SimpleString); + assertEqual(parsed->operator String().c_str(), "OK"); +} + +test(UnitTests, simple_error) +{ + parseRESP2String("-Error message\r\n"); + assertEqual(parsed->type(), RedisObject::Type::Error); + assertEqual(parsed->operator String().c_str(), "Error message"); +} + +test(UnitTests, integers) +{ + std::vector> test_vectors{ + std::make_pair(":0\r\n", 0), + std::make_pair(":-0\r\n", 0), + std::make_pair(":+0\r\n", 0), + std::make_pair(":1000\r\n", 1000), + std::make_pair(":-1000\r\n", -1000), + std::make_pair(":+1000\r\n", 1000), + std::make_pair(":2147483647\r\n", 2147483647), + std::make_pair(":2147483648\r\n", -2147483648), // wrap around + std::make_pair(":-2147483648\r\n", -2147483648), + std::make_pair(":-2147483649\r\n", 2147483647), // wrap around + }; + + for (const auto &test_vec : test_vectors) + { + parseRESP2String(test_vec.first.c_str()); + assertEqual(parsed->type(), RedisObject::Type::Integer); + assertEqual(dynamic_cast(parsed.get())->operator int(), test_vec.second); + } +} + +test(UnitTests, bulk_strings) +{ + std::vector> test_vectors{ + std::make_pair("$5\r\nhello\r\n", "hello"), + std::make_pair("$0\r\n\r\n", ""), + std::make_pair("$3\r\n\x01\x02\x03\r\n", "\x01\x02\x03")}; + + for (const auto &test_vec : test_vectors) + { + parseRESP2String(test_vec.first.c_str()); + assertEqual(parsed->type(), RedisObject::Type::BulkString); + assertEqual(parsed->operator String(), test_vec.second); + } +} + +test(UnitTests, null_bulk_string) +{ + parseRESP2String("$-1\r\n"); + assertEqual(parsed->type(), RedisObject::Type::BulkString); + assertEqual(Redis::isNilReturn(parsed->operator String()), true); +} + +test(UnitTests, non_RESP_data) +{ + std::vector vectors{ + "Foo bar baz", + "Hello world!", + "\x01\x02\x03", + "2d9c9124-ca30-4d5b-b1ba-b757d08cdb03"}; + + for (const auto &fuzz_vec : vectors) + { + parseRESP2String(fuzz_vec.c_str()); + assertEqual(parsed->type(), RedisObject::Type::InternalError); + } } \ No newline at end of file