diff --git a/API.md b/API.md index adac3b6..9951ba6 100644 --- a/API.md +++ b/API.md @@ -6,6 +6,10 @@ The Cloud Point RPC server implements the **JSON-RPC 2.0** protocol over TCP. > **NOTE 2:** Unit Tests were not written for the described API yet +Unity side expected: +- receive value of `params` field of request:`{}` +- return value of `result` field of response (string or json, both ASCII compliant) + ## General Format All requests and responses are JSON objects. diff --git a/tests/meson.build b/tests/meson.build index 2e02e8a..c13bbbf 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,10 +1,16 @@ test_sources = files( 'test_rpc.cpp', + 'test_rpc_edge_cases.cpp', 'test_integration.cpp', 'test_tcp.cpp', + 'test_tcp_edge_cases.cpp', 'test_cli.cpp', 'test_c_api.cpp', - 'test_base64.cpp' + 'test_c_api_edge_cases.cpp', + 'test_base64.cpp', + 'test_base64_edge_cases.cpp', + 'test_service.cpp', + 'test_serialize.cpp' ) test_exe = executable('unit_tests', diff --git a/tests/test_base64_edge_cases.cpp b/tests/test_base64_edge_cases.cpp new file mode 100644 index 0000000..a50ae40 --- /dev/null +++ b/tests/test_base64_edge_cases.cpp @@ -0,0 +1,181 @@ +#include "cloud_point_rpc/rpc_coder.hpp" +#include +#include + +using namespace score; + +class Base64EdgeCaseTest : public ::testing::Test { + protected: + Base64RPCCoder coder; +}; + +// Empty input +TEST_F(Base64EdgeCaseTest, EmptyEncode) { + std::vector empty; + auto encoded = coder.encode(empty); + EXPECT_TRUE(encoded.empty()); +} + +TEST_F(Base64EdgeCaseTest, EmptyDecode) { + std::string empty; + auto decoded = coder.decode(empty); + EXPECT_TRUE(decoded.empty()); +} + +// 1 byte input +TEST_F(Base64EdgeCaseTest, OneByteEncode) { + std::vector data{'A'}; + auto encoded = coder.encode(data); + EXPECT_EQ(encoded, "QQ=="); +} + +TEST_F(Base64EdgeCaseTest, OneByteRoundTrip) { + std::vector data{'A'}; + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// 2 bytes input +TEST_F(Base64EdgeCaseTest, TwoBytesEncode) { + std::vector data{'A', 'B'}; + auto encoded = coder.encode(data); + EXPECT_EQ(encoded, "QUI="); +} + +TEST_F(Base64EdgeCaseTest, TwoBytesRoundTrip) { + std::vector data{'A', 'B'}; + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// 3 bytes input (no padding) +TEST_F(Base64EdgeCaseTest, ThreeBytesEncode) { + std::vector data{'A', 'B', 'C'}; + auto encoded = coder.encode(data); + EXPECT_EQ(encoded, "QUJD"); +} + +TEST_F(Base64EdgeCaseTest, ThreeBytesRoundTrip) { + std::vector data{'A', 'B', 'C'}; + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// Standard test vectors +TEST_F(Base64EdgeCaseTest, StandardVectors) { + struct TestCase { + std::vector input; + std::string expected; + }; + std::vector cases = { + {{'f'}, "Zg=="}, + {{'f', 'o'}, "Zm8="}, + {{'f', 'o', 'o'}, "Zm9v"}, + {{'f', 'o', 'o', 'b'}, "Zm9vYg=="}, + {{'f', 'o', 'o', 'b', 'a'}, "Zm9vYmE="}, + {{'f', 'o', 'o', 'b', 'a', 'r'}, "Zm9vYmFy"}, + }; + for (const auto &tc : cases) { + auto encoded = coder.encode(tc.input); + EXPECT_EQ(encoded, tc.expected); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, tc.input); + } +} + +// Binary data with null bytes +TEST_F(Base64EdgeCaseTest, BinaryWithNullBytes) { + std::vector data{'H', 'e', 'l', 'l', 'o', '\0', + 'W', 'o', 'r', 'l', 'd'}; + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// All byte values 0-255 +TEST_F(Base64EdgeCaseTest, AllByteValues) { + std::vector data(256); + for (int i = 0; i < 256; ++i) { + data[i] = static_cast(i); + } + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// Repeated patterns +TEST_F(Base64EdgeCaseTest, RepeatedPattern) { + std::vector data(1024, 'A'); + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// Invalid base64 characters +TEST_F(Base64EdgeCaseTest, InvalidCharactersDecode) { + // base64_decode should handle invalid chars gracefully or fail + std::string invalid = "!!!"; + auto decoded = coder.decode(invalid); + // libbase64 may return empty or partial result; just verify no crash + (void)decoded; +} + +TEST_F(Base64EdgeCaseTest, MixedValidInvalid) { + std::string mixed = "QU!!JD"; + auto decoded = coder.decode(mixed); + (void)decoded; // no crash expected +} + +// Padding edge cases +TEST_F(Base64EdgeCaseTest, NoPaddingDecode) { + std::string no_pad = "QUJD"; // "ABC" without explicit padding + auto decoded = coder.decode(no_pad); + std::vector expected{'A', 'B', 'C'}; + EXPECT_EQ(decoded, expected); +} + +TEST_F(Base64EdgeCaseTest, ExtraPadding) { + std::string extra_pad = "QQ==="; + auto decoded = coder.decode(extra_pad); + (void)decoded; // no crash expected +} + +// Large input +TEST_F(Base64EdgeCaseTest, LargeInputRoundTrip) { + std::vector data(100000, 'x'); + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// Very large input (1MB) +TEST_F(Base64EdgeCaseTest, OneMegabyteRoundTrip) { + std::vector data(1024 * 1024); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast(i % 256); + } + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); +} + +// Whitespace in encoded string +TEST_F(Base64EdgeCaseTest, WhitespaceInEncoded) { + std::string with_space = "Q U J D"; + auto decoded = coder.decode(with_space); + (void)decoded; // libbase64 behavior varies; ensure no crash +} + +// Non-ASCII characters in input (UTF-8) +TEST_F(Base64EdgeCaseTest, Utf8RoundTrip) { + std::string utf8 = "Hello, 世界! 🌍"; + std::vector data(utf8.begin(), utf8.end()); + auto encoded = coder.encode(data); + auto decoded = coder.decode(encoded); + EXPECT_EQ(decoded, data); + std::string decoded_str(decoded.begin(), decoded.end()); + EXPECT_EQ(decoded_str, utf8); +} diff --git a/tests/test_c_api_edge_cases.cpp b/tests/test_c_api_edge_cases.cpp new file mode 100644 index 0000000..73529da --- /dev/null +++ b/tests/test_c_api_edge_cases.cpp @@ -0,0 +1,213 @@ +#include "cloud_point_rpc/rpc_server.hpp" +#include "server_api.h" +#include "test_api.h" +#include +#include +#include +#include + +class CApiEdgeCaseTest : public ::testing::Test { + protected: + void SetUp() override { + FLAGS_logtostderr = true; + if (!google::IsGoogleLoggingInitialized()) + google::InitGoogleLogging("TestRPC"); + } +}; + +// Null pointer tests for string functions +TEST_F(CApiEdgeCaseTest, StrGetDataNullptr) { + EXPECT_EQ(crpc_str_get_data(nullptr), nullptr); +} + +TEST_F(CApiEdgeCaseTest, StrGetSizeNullptr) { + EXPECT_EQ(crpc_str_get_size(nullptr), 0); +} + +TEST_F(CApiEdgeCaseTest, StrCreateNullptrData) { + EXPECT_EQ(crpc_str_create(nullptr, 10), nullptr); +} + +TEST_F(CApiEdgeCaseTest, StrCreateEmptyString) { + auto str = crpc_str_create("", 0); + EXPECT_NE(str, nullptr); + EXPECT_EQ(crpc_str_get_size(str), 0); + EXPECT_EQ(std::string_view(crpc_str_get_data(str)), ""); + crpc_str_destroy(str); +} + +TEST_F(CApiEdgeCaseTest, StrDestroyNullptr) { + // Should not crash + EXPECT_NO_THROW(crpc_str_destroy(nullptr)); +} + +// Double destroy should be safe-ish (will just not find it) +TEST_F(CApiEdgeCaseTest, StrDoubleDestroy) { + auto str = crpc_str_create("test", 4); + ASSERT_NE(str, nullptr); + crpc_str_destroy(str); + // Second destroy should not crash (pointer not in gc anymore) + EXPECT_NO_THROW(crpc_str_destroy(str)); +} + +// Create and destroy many strings +TEST_F(CApiEdgeCaseTest, StrCreateDestroyMany) { + constexpr int N = 1000; + std::vector ptrs; + ptrs.reserve(N); + for (int i = 0; i < N; ++i) { + auto str = crpc_str_create("x", 1); + ASSERT_NE(str, nullptr); + ptrs.push_back(str); + } + // Destroy half + for (int i = 0; i < N / 2; ++i) { + crpc_str_destroy(ptrs[i]); + } + // Create more + for (int i = 0; i < N / 2; ++i) { + auto str = crpc_str_create("y", 1); + ASSERT_NE(str, nullptr); + } + // Destroy remaining original + for (int i = N / 2; i < N; ++i) { + crpc_str_destroy(ptrs[i]); + } +} + +// Null pointer tests for add_method +TEST_F(CApiEdgeCaseTest, AddMethodNullName) { + auto cb = + +[](rpc_string *) -> rpc_string * { return crpc_str_create("res", 3); }; + // Should not crash, just log and return + EXPECT_NO_THROW(crpc_add_method(cb, nullptr)); +} + +TEST_F(CApiEdgeCaseTest, AddMethodNullCallback) { + rpc_string name{"test", 4}; + EXPECT_NO_THROW(crpc_add_method(nullptr, &name)); +} + +TEST_F(CApiEdgeCaseTest, AddMethodBothNull) { + EXPECT_NO_THROW(crpc_add_method(nullptr, nullptr)); +} + +// crpc_init edge cases +TEST_F(CApiEdgeCaseTest, InitWithNullptr) { + // Should not crash, just log error and return + EXPECT_NO_THROW(crpc_init(nullptr)); +} + +TEST_F(CApiEdgeCaseTest, InitWithInvalidPath) { + // Should catch exception and log, not crash + EXPECT_NO_THROW(crpc_init("/nonexistent/path/config.yaml")); +} + +// Full lifecycle: init -> add method -> deinit +TEST_F(CApiEdgeCaseTest, FullLifecycle) { + std::ofstream config_file("test_config.yaml"); + config_file << "server:\n" + << " ip: \"127.0.0.1\"\n" + << " port: 19191\n"; + config_file.close(); + + EXPECT_NO_THROW(crpc_init("test_config.yaml")); + + rpc_string name{"echo", 4}; + auto cb = +[](rpc_string *req) -> rpc_string * { + return crpc_str_create(req->s.data(), req->s.size()); + }; + EXPECT_NO_THROW(crpc_add_method(cb, &name)); + + EXPECT_NO_THROW(crpc_deinit()); + std::remove("test_config.yaml"); +} + +// Deinit without init should not crash +TEST_F(CApiEdgeCaseTest, DeinitWithoutInit) { EXPECT_NO_THROW(crpc_deinit()); } + +// Multiple init/deinit cycles +TEST_F(CApiEdgeCaseTest, MultipleInitDeinitCycles) { + std::ofstream config_file("test_config.yaml"); + config_file << "server:\n" + << " ip: \"127.0.0.1\"\n" + << " port: 19192\n"; + config_file.close(); + + for (int i = 0; i < 3; ++i) { + EXPECT_NO_THROW(crpc_init("test_config.yaml")); + EXPECT_NO_THROW(crpc_deinit()); + } + std::remove("test_config.yaml"); +} + +// GC cleanup on deinit +TEST_F(CApiEdgeCaseTest, GcCleanupOnDeinit) { + auto str1 = crpc_str_create("one", 3); + auto str2 = crpc_str_create("two", 3); + ASSERT_NE(str1, nullptr); + ASSERT_NE(str2, nullptr); + + // Destroy one, leave one + crpc_str_destroy(str1); + + // deinit should clear gc including str2 + EXPECT_NO_THROW(crpc_deinit()); +} + +// Large string creation +TEST_F(CApiEdgeCaseTest, LargeStringCreate) { + std::string large(1000000, 'x'); + auto str = crpc_str_create(large.data(), large.size()); + ASSERT_NE(str, nullptr); + EXPECT_EQ(crpc_str_get_size(str), large.size()); + EXPECT_EQ(std::string(crpc_str_get_data(str), large.size()), large); + crpc_str_destroy(str); +} + +// String with embedded null bytes +TEST_F(CApiEdgeCaseTest, StringWithNullBytes) { + std::string data("Hello\0World", 11); + auto str = crpc_str_create(data.data(), data.size()); + ASSERT_NE(str, nullptr); + EXPECT_EQ(crpc_str_get_size(str), 11); + EXPECT_EQ(std::string(crpc_str_get_data(str), 11), data); + crpc_str_destroy(str); +} + +// Test API edge cases +TEST_F(CApiEdgeCaseTest, TestInitDeinit) { + EXPECT_NO_THROW(crpc_test_init()); + EXPECT_NO_THROW(crpc_test_deinit()); +} + +TEST_F(CApiEdgeCaseTest, TestRemoveNonexistentMethod) { + crpc_test_init(); + rpc_string name{"nonexistent", 11}; + EXPECT_EQ(crpc_test_remove_method(&name), -1); + crpc_test_deinit(); +} + +TEST_F(CApiEdgeCaseTest, TestAutoCallToggle) { + crpc_test_init(); + EXPECT_NO_THROW(crpc_test_auto_call(0)); + EXPECT_NO_THROW(crpc_test_auto_call(1)); + EXPECT_NO_THROW(crpc_test_auto_call(0)); + crpc_test_deinit(); +} + +TEST_F(CApiEdgeCaseTest, TestChangeDuration) { + crpc_test_init(); + EXPECT_NO_THROW(crpc_test_change_duration(100)); + EXPECT_EQ(crpc_test_duration(), 100); + EXPECT_NO_THROW(crpc_test_change_duration(500)); + EXPECT_EQ(crpc_test_duration(), 500); + crpc_test_deinit(); +} + +TEST_F(CApiEdgeCaseTest, TestScheduleCallNonexistent) { + crpc_test_init(); + rpc_string name{"nonexistent", 11}; + EXPECT_NO_THROW(crpc_test_schedule_call(&name)); + crpc_test_deinit(); +} diff --git a/tests/test_rpc_edge_cases.cpp b/tests/test_rpc_edge_cases.cpp new file mode 100644 index 0000000..277a2f4 --- /dev/null +++ b/tests/test_rpc_edge_cases.cpp @@ -0,0 +1,253 @@ +#include "cloud_point_rpc/rpc_server.hpp" +#include "server_api.h" +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; +using namespace score; + +class RpcServerEdgeCaseTest : public ::testing::Test { + protected: + RpcServer server; + + void SetUp() override { + server.register_method( + "echo", [&](const json &j) { return j.get(); }); + server.register_method("thrower", [&](const json &) -> std::string { + throw std::runtime_error("intentional error"); + }); + } +}; + +// Empty request string +TEST_F(RpcServerEdgeCaseTest, EmptyRequestReturnsParseError) { + std::string response_str = server.process(""); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32700); +} + +// Valid JSON but primitive types (not object) +// NOTE: These currently throw nlohmann::json::type_error instead of returning +// Invalid Request. This documents a known bug in type validation. +TEST_F(RpcServerEdgeCaseTest, JsonArrayThrowsTypeError) { + EXPECT_THROW(server.process(R"([1, 2, 3])"), nlohmann::json::type_error); +} + +TEST_F(RpcServerEdgeCaseTest, JsonStringThrowsTypeError) { + EXPECT_THROW(server.process(R"("just a string")"), + nlohmann::json::type_error); +} + +TEST_F(RpcServerEdgeCaseTest, JsonNumberThrowsTypeError) { + EXPECT_THROW(server.process("42"), nlohmann::json::type_error); +} + +TEST_F(RpcServerEdgeCaseTest, JsonNullThrowsTypeError) { + EXPECT_THROW(server.process("null"), nlohmann::json::type_error); +} + +// Missing required fields +TEST_F(RpcServerEdgeCaseTest, MissingJsonrpcField) { + std::string response_str = server.process(R"({"method": "echo", "id": 1})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32600); +} + +TEST_F(RpcServerEdgeCaseTest, WrongJsonrpcVersion) { + std::string response_str = + server.process(R"({"jsonrpc": "1.0", "method": "echo", "id": 1})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32600); +} + +TEST_F(RpcServerEdgeCaseTest, MissingMethodField) { + std::string response_str = server.process(R"({"jsonrpc": "2.0", "id": 1})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32600); +} + +TEST_F(RpcServerEdgeCaseTest, MissingIdField) { + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "echo"})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32600); +} + +// Method field type validation +// NOTE: These currently throw nlohmann::json::type_error instead of returning +// Invalid Request. This documents a known bug in type validation. +TEST_F(RpcServerEdgeCaseTest, MethodIsNumberThrowsTypeError) { + EXPECT_THROW( + server.process(R"({"jsonrpc": "2.0", "method": 123, "id": 1})"), + nlohmann::json::type_error); +} + +TEST_F(RpcServerEdgeCaseTest, MethodIsNullThrowsTypeError) { + EXPECT_THROW( + server.process(R"({"jsonrpc": "2.0", "method": null, "id": 1})"), + nlohmann::json::type_error); +} + +TEST_F(RpcServerEdgeCaseTest, MethodIsArrayThrowsTypeError) { + EXPECT_THROW( + server.process(R"({"jsonrpc": "2.0", "method": ["echo"], "id": 1})"), + nlohmann::json::type_error); +} + +TEST_F(RpcServerEdgeCaseTest, MethodIsObjectThrowsTypeError) { + EXPECT_THROW( + server.process( + R"({"jsonrpc": "2.0", "method": {"name": "echo"}, "id": 1})"), + nlohmann::json::type_error); +} + +// Handler exceptions +TEST_F(RpcServerEdgeCaseTest, HandlerThrowsReturnsServerError) { + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "thrower", "id": 42})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32000); + // Should not leak internal details ideally, but current impl does + EXPECT_EQ(response["error"]["message"], "intentional error"); +} + +// Valid request with params +TEST_F(RpcServerEdgeCaseTest, RequestWithParams) { + server.register_method("add", [&](const json &j) { + return j.at("a").get() + j.at("b").get(); + }); + std::string response_str = server.process( + R"({"jsonrpc": "2.0", "method": "add", "id": 1, "params": {"a": 2, "b": 3}})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("result")); + EXPECT_EQ(response["result"], 5); +} + +// Request with empty params object +// The echo handler expects a string but gets an empty object, so it throws. +TEST_F(RpcServerEdgeCaseTest, RequestWithEmptyParamsHandlerThrows) { + std::string response_str = server.process( + R"({"jsonrpc": "2.0", "method": "echo", "id": 1, "params": {}})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32000); +} + +// C callback edge cases +// NOTE: When the C callback returns nullptr, the wrapper returns {} which +// value-initializes the variant's first alternative (json null). This is a +// bug: it should throw to trigger a proper error response. +TEST_F(RpcServerEdgeCaseTest, CCallbackReturnsNullProducesNullResult) { + server.register_method( + "null_cb", [](rpc_string *) -> rpc_string * { return nullptr; }); + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "null_cb", "id": 1})"); + json response = json::parse(response_str); + // Current behavior: returns success with null result due to variant + // value-initialization bug + ASSERT_TRUE(response.contains("result")); + EXPECT_TRUE(response["result"].is_null()); +} + +TEST_F(RpcServerEdgeCaseTest, CCallbackReturnsNonJsonString) { + server.register_method("raw_cb", [](rpc_string *) -> rpc_string * { + return crpc_str_create("hello world", 11); + }); + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "raw_cb", "id": 1})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("result")); + EXPECT_EQ(response["result"], "hello world"); +} + +TEST_F(RpcServerEdgeCaseTest, CCallbackReturnsValidJson) { + server.register_method("json_cb", [](rpc_string *) -> rpc_string * { + return crpc_str_create(R"({"key": "value"})", 16); + }); + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "json_cb", "id": 1})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("result")); + EXPECT_EQ(response["result"]["key"], "value"); +} + +// Thread safety: concurrent register and process +TEST_F(RpcServerEdgeCaseTest, ConcurrentRegisterAndProcess) { + constexpr int kIterations = 100; + std::atomic success_count{0}; + + std::thread registrar([&]() { + for (int i = 0; i < kIterations; ++i) { + server.register_method("dyn_" + std::to_string(i), + [&](const json &j) { return j.get(); }); + } + }); + + std::thread processor([&]() { + for (int i = 0; i < kIterations; ++i) { + std::string req = R"({"jsonrpc": "2.0", "method": "echo", "id": )" + + std::to_string(i) + "}"; + try { + auto res = server.process(req); + if (!res.empty()) + ++success_count; + } catch (...) { + // ignore races + } + } + }); + + registrar.join(); + processor.join(); + + EXPECT_EQ(success_count, kIterations); +} + +// Unicode and special characters in method name +TEST_F(RpcServerEdgeCaseTest, UnicodeMethodNameNotFound) { + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "метод", "id": 1})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32601); +} + +TEST_F(RpcServerEdgeCaseTest, MethodWithNewlineNotFound) { + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "echo\n", "id": 1})"); + json response = json::parse(response_str); + ASSERT_TRUE(response.contains("error")); + EXPECT_EQ(response["error"]["code"], -32601); +} + +// Id edge cases +TEST_F(RpcServerEdgeCaseTest, StringIdPreserved) { + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "echo", "id": "abc"})"); + json response = json::parse(response_str); + EXPECT_EQ(response["id"], "abc"); +} + +TEST_F(RpcServerEdgeCaseTest, NullIdPreserved) { + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "echo", "id": null})"); + json response = json::parse(response_str); + EXPECT_TRUE(response["id"].is_null()); +} + +TEST_F(RpcServerEdgeCaseTest, ZeroIdPreserved) { + std::string response_str = + server.process(R"({"jsonrpc": "2.0", "method": "echo", "id": 0})"); + json response = json::parse(response_str); + EXPECT_EQ(response["id"], 0); +} diff --git a/tests/test_serialize.cpp b/tests/test_serialize.cpp new file mode 100644 index 0000000..371674b --- /dev/null +++ b/tests/test_serialize.cpp @@ -0,0 +1,96 @@ +#include "cloud_point_rpc/serialize.hpp" +#include +#include +#include + +using namespace score; + +class SerializeEdgeCaseTest : public ::testing::Test {}; + +// uint8_t round-trip +TEST_F(SerializeEdgeCaseTest, Uint8RoundTrip) { + uint8_t value = 42; + auto buf = serialize(value); + EXPECT_EQ(buf.size(), sizeof(uint8_t)); + EXPECT_EQ(deserialize(buf), value); +} + +// int32_t round-trip +TEST_F(SerializeEdgeCaseTest, Int32RoundTrip) { + int32_t value = -12345; + auto buf = serialize(value); + EXPECT_EQ(buf.size(), sizeof(int32_t)); + EXPECT_EQ(deserialize(buf), value); +} + +// uint64_t round-trip with max value +TEST_F(SerializeEdgeCaseTest, Uint64MaxRoundTrip) { + uint64_t value = std::numeric_limits::max(); + auto buf = serialize(value); + EXPECT_EQ(buf.size(), sizeof(uint64_t)); + EXPECT_EQ(deserialize(buf), value); +} + +// int64_t round-trip with min value +TEST_F(SerializeEdgeCaseTest, Int64MinRoundTrip) { + int64_t value = std::numeric_limits::min(); + auto buf = serialize(value); + EXPECT_EQ(buf.size(), sizeof(int64_t)); + EXPECT_EQ(deserialize(buf), value); +} + +// float round-trip +TEST_F(SerializeEdgeCaseTest, FloatRoundTrip) { + float value = 3.14159f; + auto buf = serialize(value); + EXPECT_EQ(buf.size(), sizeof(float)); + EXPECT_FLOAT_EQ(deserialize(buf), value); +} + +// double round-trip +TEST_F(SerializeEdgeCaseTest, DoubleRoundTrip) { + double value = 2.718281828459045; + auto buf = serialize(value); + EXPECT_EQ(buf.size(), sizeof(double)); + EXPECT_DOUBLE_EQ(deserialize(buf), value); +} + +// zero values +TEST_F(SerializeEdgeCaseTest, ZeroValues) { + EXPECT_EQ(deserialize(serialize(0)), 0); + EXPECT_EQ(deserialize(serialize(0)), 0); + EXPECT_FLOAT_EQ(deserialize(serialize(0.0f)), 0.0f); + EXPECT_DOUBLE_EQ(deserialize(serialize(0.0)), 0.0); +} + +// inplace_size_embedding +TEST_F(SerializeEdgeCaseTest, InplaceSizeEmbedding) { + std::string msg = "Hello"; + inplace_size_embedding(msg); + EXPECT_EQ(msg.size(), 5 + sizeof(uint64_t)); + // First 8 bytes should be the size (5) + uint64_t size = deserialize( + std::vector(msg.begin(), msg.begin() + sizeof(uint64_t))); + EXPECT_EQ(size, 5); + // Remaining bytes should be the message + EXPECT_EQ(msg.substr(sizeof(uint64_t)), "Hello"); +} + +TEST_F(SerializeEdgeCaseTest, InplaceSizeEmbeddingEmpty) { + std::string msg; + inplace_size_embedding(msg); + EXPECT_EQ(msg.size(), sizeof(uint64_t)); + uint64_t size = deserialize( + std::vector(msg.begin(), msg.begin() + sizeof(uint64_t))); + EXPECT_EQ(size, 0); +} + +// Buffer too small for deserialize (unsafe but should not crash in test) +TEST_F(SerializeEdgeCaseTest, DeserializeSmallBuffer) { + std::vector small_buf{0x01, 0x02}; + // This is undefined behavior in current implementation, but we document it + // In a hardened implementation, this should throw + // For now, just verify it compiles and runs (it's unsafe API usage) + // EXPECT_THROW(deserialize(small_buf), std::runtime_error); + (void)small_buf; +} diff --git a/tests/test_service.cpp b/tests/test_service.cpp new file mode 100644 index 0000000..929bcc8 --- /dev/null +++ b/tests/test_service.cpp @@ -0,0 +1,114 @@ +#include "cloud_point_rpc/service.hpp" +#include +#include + +using namespace score; + +class ServiceEdgeCaseTest : public ::testing::Test {}; + +// Default constructor (no data) +TEST_F(ServiceEdgeCaseTest, DefaultConstructorFallbacks) { + Service service; + + auto intrinsic = service.get_intrinsic_params(); + EXPECT_EQ(intrinsic.size(), 9); + EXPECT_EQ(intrinsic[0], 1.0); + EXPECT_EQ(intrinsic[4], 1.0); + EXPECT_EQ(intrinsic[8], 1.0); + + auto extrinsic = service.get_extrinsic_params(); + EXPECT_EQ(extrinsic.size(), 16); + EXPECT_EQ(extrinsic[0], 1.0); + EXPECT_EQ(extrinsic[5], 1.0); + EXPECT_EQ(extrinsic[10], 1.0); + EXPECT_EQ(extrinsic[15], 1.0); + + auto cloud = service.get_cloud_point(); + EXPECT_EQ(cloud.size(), 3); + EXPECT_EQ(cloud[0], std::vector({0.1, 0.2, 0.3})); +} + +// Empty TestData explicitly +TEST_F(ServiceEdgeCaseTest, ExplicitEmptyData) { + TestData empty_data; + Service service(empty_data); + + auto intrinsic = service.get_intrinsic_params(); + EXPECT_EQ(intrinsic.size(), 9); + + auto extrinsic = service.get_extrinsic_params(); + EXPECT_EQ(extrinsic.size(), 16); + + auto cloud = service.get_cloud_point(); + EXPECT_EQ(cloud.size(), 3); +} + +// Custom intrinsic params +TEST_F(ServiceEdgeCaseTest, CustomIntrinsicParams) { + TestData data; + data.intrinsic_params = {100.0, 0.0, 50.0, 0.0, 100.0, 50.0, 0.0, 0.0, 1.0}; + Service service(data); + + auto intrinsic = service.get_intrinsic_params(); + EXPECT_EQ(intrinsic, data.intrinsic_params); +} + +// Custom extrinsic params +TEST_F(ServiceEdgeCaseTest, CustomExtrinsicParams) { + TestData data; + data.extrinsic_params = {1, 0, 0, 1, 0, 1, 0, 2, 0, 0, 1, 3, 0, 0, 0, 1}; + Service service(data); + + auto extrinsic = service.get_extrinsic_params(); + EXPECT_EQ(extrinsic, data.extrinsic_params); +} + +// Custom cloud point +TEST_F(ServiceEdgeCaseTest, CustomCloudPoint) { + TestData data; + data.cloud_point = {{1.0, 2.0, 3.0}, {4.0, 5.0, 6.0}}; + Service service(data); + + auto cloud = service.get_cloud_point(); + EXPECT_EQ(cloud.size(), 2); + EXPECT_EQ(cloud[0], std::vector({1.0, 2.0, 3.0})); + EXPECT_EQ(cloud[1], std::vector({4.0, 5.0, 6.0})); +} + +// Large point cloud +TEST_F(ServiceEdgeCaseTest, LargePointCloud) { + TestData data; + for (int i = 0; i < 10000; ++i) { + data.cloud_point.push_back({static_cast(i), + static_cast(i + 1), + static_cast(i + 2)}); + } + Service service(data); + + auto cloud = service.get_cloud_point(); + EXPECT_EQ(cloud.size(), 10000); + EXPECT_EQ(cloud[9999], std::vector({9999.0, 10000.0, 10001.0})); +} + +// Single point cloud +TEST_F(ServiceEdgeCaseTest, SinglePointCloud) { + TestData data; + data.cloud_point = {{0.0, 0.0, 0.0}}; + Service service(data); + + auto cloud = service.get_cloud_point(); + EXPECT_EQ(cloud.size(), 1); + EXPECT_EQ(cloud[0], std::vector({0.0, 0.0, 0.0})); +} + +// Negative values +TEST_F(ServiceEdgeCaseTest, NegativeValues) { + TestData data; + data.intrinsic_params = {-100.0, 0.0, -50.0, 0.0, -100.0, + -50.0, 0.0, 0.0, -1.0}; + Service service(data); + + auto intrinsic = service.get_intrinsic_params(); + EXPECT_EQ(intrinsic[0], -100.0); + EXPECT_EQ(intrinsic[8], -1.0); +} diff --git a/tests/test_tcp_edge_cases.cpp b/tests/test_tcp_edge_cases.cpp new file mode 100644 index 0000000..d2c6f04 --- /dev/null +++ b/tests/test_tcp_edge_cases.cpp @@ -0,0 +1,187 @@ +#include "cloud_point_rpc/tcp_connector.hpp" +#include "cloud_point_rpc/tcp_server.hpp" +#include +#include +#include +#include +#include + +using namespace score; + +class TcpEdgeCaseTest : public ::testing::Test { + protected: + std::unique_ptr server_; + std::thread server_thread_; + + void StartServer(int port, TcpServer::RequestProcessor processor) { + server_ = std::make_unique("127.0.0.1", port, processor); + server_thread_ = std::thread([this]() { server_->start(); }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + void StopServer() { + if (server_) { + server_->stop(); + } + if (server_thread_.joinable()) { + server_thread_.join(); + } + } + + void TearDown() override { StopServer(); } +}; + +// Empty payload +// NOTE: The server silently ignores empty payloads (payload_length == 0) +// and closes the connection without sending a response. +TEST_F(TcpEdgeCaseTest, EmptyPayloadGetsNoResponse) { + StartServer(19001, [](const std::string &req) { + if (req.empty()) { + return std::string("empty"); + } + return req; + }); + + TCPConnector connector("127.0.0.1", 19001); + auto res = connector.Send(""); + EXPECT_EQ(res, ""); +} + +// Very small payload (1 byte) +TEST_F(TcpEdgeCaseTest, SingleBytePayload) { + StartServer(19002, [](const std::string &req) { return req; }); + + TCPConnector connector("127.0.0.1", 19002); + auto res = connector.Send("x"); + EXPECT_EQ(res, "x\n"); +} + +// Multiple sequential connections +TEST_F(TcpEdgeCaseTest, MultipleSequentialConnections) { + StartServer(19003, [](const std::string &req) { return req; }); + + for (int i = 0; i < 10; ++i) { + TCPConnector connector("127.0.0.1", 19003); + auto msg = "msg_" + std::to_string(i); + auto res = connector.Send(msg); + EXPECT_EQ(res, msg + "\n"); + } +} + +// Multiple concurrent connections +TEST_F(TcpEdgeCaseTest, MultipleConcurrentConnections) { + std::atomic counter{0}; + StartServer(19004, [&counter](const std::string &req) { + ++counter; + return req; + }); + + constexpr int N = 10; + std::vector threads; + threads.reserve(N); + + for (int i = 0; i < N; ++i) { + threads.emplace_back([i]() { + TCPConnector connector("127.0.0.1", 19004); + auto msg = "concurrent_" + std::to_string(i); + auto res = connector.Send(msg); + EXPECT_EQ(res, msg + "\n"); + }); + } + + for (auto &t : threads) { + t.join(); + } + + EXPECT_EQ(counter, N); +} + +// Server stop and restart on same port +TEST_F(TcpEdgeCaseTest, StopRestartSamePort) { + StartServer(19005, [](const std::string &req) { return req; }); + + { + TCPConnector connector("127.0.0.1", 19005); + auto res = connector.Send("first"); + EXPECT_EQ(res, "first\n"); + } + + StopServer(); + + // Restart on same port + StartServer(19005, [](const std::string &req) { return req + "_v2"; }); + + { + TCPConnector connector("127.0.0.1", 19005); + auto res = connector.Send("second"); + EXPECT_EQ(res, "second_v2\n"); + } +} + +// Connection to wrong port fails +TEST_F(TcpEdgeCaseTest, ConnectionToWrongPortFails) { + StartServer(19006, [](const std::string &req) { return req; }); + + EXPECT_THROW(TCPConnector connector("127.0.0.1", 19007), + std::runtime_error); +} + +// Large payload +TEST_F(TcpEdgeCaseTest, LargePayload) { + std::string large_data(100000, 'L'); + StartServer(19008, [&large_data](const std::string &req) { + if (req == large_data) { + return std::string("OK"); + } + return std::string("MISMATCH"); + }); + + TCPConnector connector("127.0.0.1", 19008); + auto res = connector.Send(large_data); + EXPECT_EQ(res, "OK\n"); +} + +// Server processor throws exception +TEST_F(TcpEdgeCaseTest, ProcessorThrowsException) { + StartServer(19009, [](const std::string &) -> std::string { + throw std::runtime_error("processor error"); + }); + + TCPConnector connector("127.0.0.1", 19009); + // Should not crash, client may get partial or no response + EXPECT_NO_THROW(connector.Send("trigger")); +} + +// Server start failure (port already in use) +TEST_F(TcpEdgeCaseTest, PortAlreadyInUse) { + StartServer(19010, [](const std::string &req) { return req; }); + + EXPECT_THROW( + { + TcpServer duplicate("127.0.0.1", 19010, + [](const std::string &req) { return req; }); + duplicate.start(); + }, + std::exception); +} + +// Explicit join after stop +TEST_F(TcpEdgeCaseTest, ExplicitJoinAfterStop) { + StartServer(19011, [](const std::string &req) { return req; }); + + server_->stop(); + EXPECT_NO_THROW(server_->join()); +} + +// Destructor cleanup without explicit stop +TEST_F(TcpEdgeCaseTest, DestructorCleanup) { + { + TcpServer local_server("127.0.0.1", 19012, + [](const std::string &req) { return req; }); + local_server.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + // destructor should clean up without explicit stop + } + // If we get here without hanging, destructor works + EXPECT_TRUE(true); +}