[tests] unit tests to cover edge cases
Some checks failed
Verification / Is-Buildable (push) Failing after 3m12s

This commit is contained in:
Artur Mukhamadiev 2026-04-21 16:54:21 +03:00
parent 3dcaf9fbae
commit b85fa6fc76
8 changed files with 1055 additions and 1 deletions

4
API.md
View File

@ -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 > **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 ## General Format
All requests and responses are JSON objects. All requests and responses are JSON objects.

View File

@ -1,10 +1,16 @@
test_sources = files( test_sources = files(
'test_rpc.cpp', 'test_rpc.cpp',
'test_rpc_edge_cases.cpp',
'test_integration.cpp', 'test_integration.cpp',
'test_tcp.cpp', 'test_tcp.cpp',
'test_tcp_edge_cases.cpp',
'test_cli.cpp', 'test_cli.cpp',
'test_c_api.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', test_exe = executable('unit_tests',

View File

@ -0,0 +1,181 @@
#include "cloud_point_rpc/rpc_coder.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using namespace score;
class Base64EdgeCaseTest : public ::testing::Test {
protected:
Base64RPCCoder coder;
};
// Empty input
TEST_F(Base64EdgeCaseTest, EmptyEncode) {
std::vector<char> 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<char> data{'A'};
auto encoded = coder.encode(data);
EXPECT_EQ(encoded, "QQ==");
}
TEST_F(Base64EdgeCaseTest, OneByteRoundTrip) {
std::vector<char> 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<char> data{'A', 'B'};
auto encoded = coder.encode(data);
EXPECT_EQ(encoded, "QUI=");
}
TEST_F(Base64EdgeCaseTest, TwoBytesRoundTrip) {
std::vector<char> 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<char> data{'A', 'B', 'C'};
auto encoded = coder.encode(data);
EXPECT_EQ(encoded, "QUJD");
}
TEST_F(Base64EdgeCaseTest, ThreeBytesRoundTrip) {
std::vector<char> 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<char> input;
std::string expected;
};
std::vector<TestCase> 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<char> 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<char> data(256);
for (int i = 0; i < 256; ++i) {
data[i] = static_cast<char>(i);
}
auto encoded = coder.encode(data);
auto decoded = coder.decode(encoded);
EXPECT_EQ(decoded, data);
}
// Repeated patterns
TEST_F(Base64EdgeCaseTest, RepeatedPattern) {
std::vector<char> 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<char> 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<char> 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<char> data(1024 * 1024);
for (size_t i = 0; i < data.size(); ++i) {
data[i] = static_cast<char>(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<char> 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);
}

View File

@ -0,0 +1,213 @@
#include "cloud_point_rpc/rpc_server.hpp"
#include "server_api.h"
#include "test_api.h"
#include <fstream>
#include <glog/logging.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
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<rpc_string *> 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();
}

View File

@ -0,0 +1,253 @@
#include "cloud_point_rpc/rpc_server.hpp"
#include "server_api.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
#include <thread>
#include <vector>
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<std::string>(); });
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<int>() + j.at("b").get<int>();
});
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<int> 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<int>(); });
}
});
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);
}

96
tests/test_serialize.cpp Normal file
View File

@ -0,0 +1,96 @@
#include "cloud_point_rpc/serialize.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <limits>
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<uint8_t>(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<int32_t>(buf), value);
}
// uint64_t round-trip with max value
TEST_F(SerializeEdgeCaseTest, Uint64MaxRoundTrip) {
uint64_t value = std::numeric_limits<uint64_t>::max();
auto buf = serialize(value);
EXPECT_EQ(buf.size(), sizeof(uint64_t));
EXPECT_EQ(deserialize<uint64_t>(buf), value);
}
// int64_t round-trip with min value
TEST_F(SerializeEdgeCaseTest, Int64MinRoundTrip) {
int64_t value = std::numeric_limits<int64_t>::min();
auto buf = serialize(value);
EXPECT_EQ(buf.size(), sizeof(int64_t));
EXPECT_EQ(deserialize<int64_t>(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<float>(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<double>(buf), value);
}
// zero values
TEST_F(SerializeEdgeCaseTest, ZeroValues) {
EXPECT_EQ(deserialize<uint64_t>(serialize<uint64_t>(0)), 0);
EXPECT_EQ(deserialize<int32_t>(serialize<int32_t>(0)), 0);
EXPECT_FLOAT_EQ(deserialize<float>(serialize<float>(0.0f)), 0.0f);
EXPECT_DOUBLE_EQ(deserialize<double>(serialize<double>(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<uint64_t>(
std::vector<uint8_t>(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<uint64_t>(
std::vector<uint8_t>(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<uint8_t> 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<uint64_t>(small_buf), std::runtime_error);
(void)small_buf;
}

114
tests/test_service.cpp Normal file
View File

@ -0,0 +1,114 @@
#include "cloud_point_rpc/service.hpp"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
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<double>({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<double>({1.0, 2.0, 3.0}));
EXPECT_EQ(cloud[1], std::vector<double>({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<double>(i),
static_cast<double>(i + 1),
static_cast<double>(i + 2)});
}
Service service(data);
auto cloud = service.get_cloud_point();
EXPECT_EQ(cloud.size(), 10000);
EXPECT_EQ(cloud[9999], std::vector<double>({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<double>({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);
}

View File

@ -0,0 +1,187 @@
#include "cloud_point_rpc/tcp_connector.hpp"
#include "cloud_point_rpc/tcp_server.hpp"
#include <asio.hpp>
#include <chrono>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <thread>
using namespace score;
class TcpEdgeCaseTest : public ::testing::Test {
protected:
std::unique_ptr<TcpServer> server_;
std::thread server_thread_;
void StartServer(int port, TcpServer::RequestProcessor processor) {
server_ = std::make_unique<TcpServer>("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<int> counter{0};
StartServer(19004, [&counter](const std::string &req) {
++counter;
return req;
});
constexpr int N = 10;
std::vector<std::thread> 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);
}