From 3edbe979da1a502110068f98085252419f3c16be Mon Sep 17 00:00:00 2001 From: Ryan Joseph Date: Wed, 16 Aug 2023 04:04:40 +0000 Subject: [PATCH] . WIP --- .github/workflows/tests.yml | 4 +- Redis.h | 2 +- RedisInternal.cpp | 8 + RedisInternal.h | 5 + test/ArduinoRedisTestBase.h | 17 +++ test/IntegrationTestBase.h | 89 +++++++++++ test/Makefile | 15 +- test/integration/integration-tests.ino | 158 +++++++++----------- test/pubsub/Makefile | 16 ++ test/pubsub/PubSubCommon.h | 1 + test/pubsub/publisher/Makefile | 6 + test/pubsub/publisher/publisher-tests.ino | 43 ++++++ test/pubsub/subscriber/Makefile | 6 + test/pubsub/subscriber/subscriber-tests.ino | 40 +++++ test/unit/unit-tests.ino | 158 ++++++++++++++++---- 15 files changed, 449 insertions(+), 119 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..aabc02a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,5 +19,7 @@ jobs: run: | sudo apt -y install redis && \ sudo systemctl start redis - - name: Build & run tests + - 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..2418c68 100644 --- a/Redis.h +++ b/Redis.h @@ -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 5c09582..acd5f91 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..e265377 --- /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..6482ec1 --- /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 k = __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..af52691 100644 --- a/test/Makefile +++ b/test/Makefile @@ -2,16 +2,21 @@ TESTS = unit/unit-tests.out integration/integration-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: pubsub/subscriber/subscriber-tests.out pubsub/publisher/publisher-tests.out + cd pubsub && make + +run: test pubsub ./unit/unit-tests.out ./integration/integration-tests.out + cd pubsub && make run 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 \ No newline at end of file diff --git a/test/integration/integration-tests.ino b/test/integration/integration-tests.ino index 343fe99..ffc4855 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) { @@ -119,7 +43,7 @@ testF(IntegrationTests, expire_at) assertEqual(r->set(k, "E"), true); assertEqual(r->expire_at(k, 0), true); - assertEqual(r->isNilReturn(r->get(k)), true); + assertEqual(Redis::isNilReturn(r->get(k)), true); } testF(IntegrationTests, pexpire) @@ -137,7 +61,7 @@ testF(IntegrationTests, pexpire_at) assertEqual(r->set(k, "PEA"), true); assertEqual(r->pexpire_at(k, 0), true); - assertEqual(r->isNilReturn(r->get(k)), true); + assertEqual(Redis::isNilReturn(r->get(k)), true); } testF(IntegrationTests, ttl) @@ -237,7 +161,7 @@ testF(IntegrationTests, 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(Redis::isNilReturn(r->hget(k, "delete_me")), true); } testF(IntegrationTests, append) @@ -327,14 +251,14 @@ 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) @@ -353,6 +277,72 @@ testF(IntegrationTests, op_vec_string_issue67) assertEqual(list[2], "3"); } +class SubscriberTests : 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(client1.connect(r_host, std::atoi(r_port)), 0); + assertNotEqual(client2.connect(r_host, std::atoi(r_port)), 0); + redis1 = std::make_shared(client1); + redis2 = std::make_shared(client1); + + if (r_auth) + { + auto auth_ret = redis1->authenticate(r_auth); + assertNotEqual(auth_ret, RedisReturnValue::RedisAuthFailure); + auth_ret = redis2->authenticate(r_auth); + assertNotEqual(auth_ret, RedisReturnValue::RedisAuthFailure); + } + printf("BALLS %p %p\n", redis1.get(), redis2.get()); + } + + TestRawClient client1, client2; + std::shared_ptr redis1, redis2; +}; + +testF(SubscriberTests, subscribe_simple) +{ + /* + auto sub_key = gKeyPrefix + ":subscribe_simple"; + printf("CHAN %s\n", sub_key.c_str()); + assertEqual(redis1->subscribe(sub_key.c_str()), true); + + redis2->publish(sub_key.c_str(), "test1!"); + auto sub_res = redis1->startSubscribingNonBlocking([](Redis *rconn, String chan, String message) { + printf("MESSAGE!!!! chan=%s '%s'\n", chan.c_str(), message.c_str()); + }, + []() { + printf("LOOP!\n"); + }, + [](Redis *redisInst, RedisMessageError err) { + printf("ERROR!\n"); + }); + + printf("sub_res %d\n", sub_res); + + redis2->publish(sub_key.c_str(), "test2!"); + delay(1000); + printf("DONE!\n"); + */ +} + /* TODO: re-factor this to something that can be automated!! #define SUBSCRIBE_TESTS 0 diff --git a/test/pubsub/Makefile b/test/pubsub/Makefile new file mode 100644 index 0000000..1d1bee4 --- /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 + parallel -- ./subscriber/subscriber-tests.out ./publisher/publisher-tests.out + +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..731166d --- /dev/null +++ b/test/pubsub/publisher/publisher-tests.ino @@ -0,0 +1,43 @@ +#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)); + + delay(1000); + 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) { + 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 %ld\n", cur_time, message_time); + exit(-1); // can use asserts here... + } + }); + + 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..c9a4f4a --- /dev/null +++ b/test/pubsub/subscriber/subscriber-tests.ino @@ -0,0 +1,40 @@ +#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) { + rconn->stopSubscribing(); + printf("MSG %s\n", message.c_str()); + 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..1f6609c 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"); +ArduinoRedisTestCommonSetupAndLoop; - if (!include) - { - include = "*"; - } - - 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,129 @@ 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", ""), + }; + + 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); } \ No newline at end of file