Compare commits

..

1 Commits

Author SHA1 Message Date
Artur Mukhamadiev
886e3f1879 [jsonrpccxx] moved to external rpc impl
Detailed:
  As base used jsonrpccxx implementation paired with TCP socket
  TCP socket updated to handle dynamic sized buffers
  TCP communication protocol changed to serialized packet size after
  which json string is presented
2026-02-20 17:23:30 +03:00
45 changed files with 249 additions and 2287 deletions

View File

@ -1,34 +0,0 @@
name: Verification
run-name: ${{ gitea.actor }} runs verification of the project
on:
push:
branches:
- "master"
jobs:
Is-Buildable:
runs-on: ubuntu-latest
steps:
- name: Ensure That Build system available
run: |
apt update
DEBIAN_FRONTEND=noninteractive apt install -y cmake make ninja-build gcc
- name: Prepare Environment
run: |
python3 -m venv $HOME/py/
source $HOME/py/bin/activate
echo PATH=$PATH >> $GITEA_ENV
pip install meson
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: "true"
- name: Build project
run: |
cd ${{ gitea.workspace }}
meson setup build
meson compile -C build -j2
- name: Unit Test Results
run: |
meson test -C build
- run: echo "🍏 This job's status is ${{ job.status }}."

5
.gitignore vendored
View File

@ -5,7 +5,4 @@ subprojects/glog/
subprojects/googletest-*
subprojects/nlohmann_json/
subprojects/packagecache/
subprojects/yaml-cpp-0.8.0
subprojects/base64-0.5.2/
.venv/
.worktrees/
subprojects/yaml-cpp-0.8.0

39
API.md
View File

@ -1,14 +1,6 @@
# JSON-RPC API Documentation
The Cloud Point RPC server implements the **JSON-RPC 2.0** protocol over TCP.
> **NOTE 1:** Base64 encoding of data should be implemented on Unity Side.
> **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)
The Cloud Point RPC server implements the **JSON-RPC 2.0** protocol over TCP.
## General Format
@ -67,11 +59,15 @@ Retrieves the intrinsic camera parameters as a flat 3x3 matrix (row-major).
```json
{
"jsonrpc": "2.0",
"result": <base64-encoded-array>,
"result": [
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0
],
"id": 1
}
```
*Type: `vector<double>` (size 9) encoded as base64*
*Type: `vector<double>` (size 9)*
### `get-extrinsic-params`
@ -90,11 +86,16 @@ Retrieves the extrinsic camera parameters as a flat 4x4 matrix (row-major).
```json
{
"jsonrpc": "2.0",
"result": <base64-encoded-array>,
"result": [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
],
"id": 2
}
```
*Type: `vector<double>` (size 16) encoded as base64*
*Type: `vector<double>` (size 16)*
### `get-cloud-point`
@ -113,12 +114,12 @@ Retrieves the current field of view point cloud.
```json
{
"jsonrpc": "2.0",
"result": {
"width": int,
"height": int,
"data": <base64-encoded-array>
},
"result": [
[0.1, 0.2, 0.3],
[1.1, 1.2, 1.3],
[5.5, 6.6, 7.7]
],
"id": 3
}
```
*Type of data: `matrix WxH` (List of [x, y, z] points) encoded as base 64*
*Type: `vector<vector<double>>` (List of [x, y, z] points)*

118
README.md
View File

@ -2,86 +2,118 @@
Communication JSON RPC protocol and implementation with Unity Scene.
## TODO
- [x] Server implementation with C-API for Unity
- [ ] Client correct implementation with OpenCV
## API Documentation
See [API.md](API.md) for detailed request/response formats.
## Comm model
```plantuml
@startuml
box ClientProcess #LightBlue
Participant Caller
Participant CloudPointClient
Participant TCPClient
end box
box UnityProcess #LightGreen
Participant TCPServer
Participant CloudPointServer
Participant UnityWorld
end box
UnityWorld -> CloudPointServer : init thread
activate CloudPointServer
CloudPointServer -> TCPServer : await for connection
activate TCPServer
->CloudPointClient : init thread
activate CloudPointClient
CloudPointClient -> TCPClient : createConnection
TCPClient -> TCPServer : establish connection
TCPServer -> CloudPointServer : established
deactivate TCPServer
CloudPointServer -> TCPServer : await for calls
TCPServer -> TCPServer : await for packet
Caller -> CloudPointClient : I want something
activate CloudPointClient
CloudPointClient -> CloudPointClient : CallMethod<Something>
CloudPointClient -> TCPClient : send(message)
activate TCPClient
TCPClient -> TCPServer : packet send
TCPServer -> TCPServer : await for packet
activate TCPServer
TCPServer -> TCPServer : read packet
TCPServer -> TCPClient : packet read
TCPClient -> CloudPointClient : done
deactivate TCPClient
CloudPointClient -> TCPClient : await for response
activate TCPClient
TCPClient -> TCPClient : await for packet
TCPServer -> CloudPointServer : callMethod
activate CloudPointServer
CloudPointServer -> UnityWorld : addToStaticQueue
UnityWorld -> UnityWorld : read from queue
activate UnityWorld
UnityWorld -> UnityWorld : callMethod
UnityWorld -> CloudPointServer: set task return value
deactivate UnityWorld
CloudPointServer -> TCPServer : return task
deactivate CloudPointServer
TCPServer -> TCPClient : send response
TCPClient -> TCPServer : response read
TCPClient -> CloudPointClient : response received
TCPServer -> CloudPointServer : done
deactivate TCPServer
CloudPointClient -> Caller : here what you wanted
deactivate CloudPointClient
Caller -> CloudPointClient : destruct
CloudPointClient -> TCPClient : finish waiting
deactivate TCPClient
deactivate CloudPointClient
UnityWorld -> CloudPointServer : destruct
deactivate CloudPointServer
@enduml
```
## Development
The project uses **Meson** build system and **C++20**.
### Dependencies
- Meson, Ninja
- GCC/Clang (C++20 support)
- Git (for subprojects)
### Build & Run
```bash
git submodule init
git submodule update
meson setup build
meson compile -C build
./build/src/cloud_point_rpc_server config.yaml
```
#### Build on windows
It's assumed that you have `GCC` and `make`/`ninja` installed on your system (and available in `PATH`)
```powershell
## FIRST OF ALL!
git submodule init
git submodule update
# Next python:
python3 -m venv .\venv
.\venv\Scripts\Activate.ps1
# or
.\venv\bin\Activate.ps1
pip install meson cmake
meson setup -Ddefault_library=static build
meson compile -C build
# To correctly get dlls:
meson devenv -C build
## .\build\tests\unit_tests < for dummy test
## .\build\src\.. < produced execs and libs
```
### Testing
```bash
meson test -C build -v
```
## Docker
You can build and run the cli using `Docker`.
You can build and run the cli using Docker.
### 1. Build Image
```bash
docker build -t cloud-point-rpc .
```
### 2. Run Container
The cli will try to connect to a **running server** on ip and port defined in config.yml file. (defined in `config.yaml` inside the image).
For simplicity, it's better to use a host network, so you will not have any headache with accessability.
The cli will try to connect to a **running server** on ip and port defined in config.yml file. (defined in `config.yaml` inside the image).
For simplicity, it's better to use a host network, so you will not have any headache with accessability.
> _Server is not configured to run through container, if you need, contact me_
> *Server is not configured to run through container, if you need, contact me*
You also can mount your own `config.yaml` to override the default settings:
```bash
docker run --network=host -it -v $(pwd)/my_config.yaml:/app/config.yaml cloud-point-rpc
```
## Communication model
![Communicatoin model plantuml diagram](docs/cm.png)

View File

@ -1,3 +0,0 @@
server:
ip: "127.0.0.1"
port: 9095

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

@ -1,66 +0,0 @@
@startuml comm
box ClientProcess #LightBlue
Participant Caller
Participant CloudPointClient
Participant TCPClient
end box
box UnityProcess #LightGreen
Participant TCPServer
Participant CloudPointServer
Participant UnityWorld
end box
UnityWorld -> CloudPointServer : init thread
activate CloudPointServer
CloudPointServer -> TCPServer : await for connection
activate TCPServer
->CloudPointClient : init thread
activate CloudPointClient
CloudPointClient -> TCPClient : createConnection
TCPClient -> TCPServer : establish connection
TCPServer -> CloudPointServer : established
deactivate TCPServer
CloudPointServer -> TCPServer : await for calls
TCPServer -> TCPServer : await for packet
Caller -> CloudPointClient : I want something
activate CloudPointClient
CloudPointClient -> CloudPointClient : CallMethod<Something>
CloudPointClient -> TCPClient : send(message)
activate TCPClient
TCPClient -> TCPServer : packet send
TCPServer -> TCPServer : await for packet
activate TCPServer
TCPServer -> TCPServer : read packet
TCPServer -> TCPClient : packet read
TCPClient -> CloudPointClient : done
deactivate TCPClient
CloudPointClient -> TCPClient : await for response
activate TCPClient
TCPClient -> TCPClient : await for packet
TCPServer -> CloudPointServer : callMethod
activate CloudPointServer
CloudPointServer -> UnityWorld : addToStaticQueue
UnityWorld -> UnityWorld : read from queue
activate UnityWorld
UnityWorld -> UnityWorld : callMethod
UnityWorld -> CloudPointServer: set task return value
deactivate UnityWorld
CloudPointServer -> TCPServer : return task
deactivate CloudPointServer
TCPServer -> TCPClient : send response
TCPClient -> TCPServer : response read
TCPClient -> CloudPointClient : response received
TCPServer -> CloudPointServer : done
deactivate TCPServer
CloudPointClient -> Caller : here what you wanted
deactivate CloudPointClient
Caller -> CloudPointClient : destruct
CloudPointClient -> TCPClient : finish waiting
deactivate TCPClient
deactivate CloudPointClient
UnityWorld -> CloudPointServer : destruct
deactivate CloudPointServer
@enduml

View File

@ -2,8 +2,8 @@
#include <iostream>
#include <string>
#include "export.h"
namespace score {
namespace cloud_point_rpc {
/**
* @brief Runs the CLI client.
@ -14,7 +14,7 @@ namespace score {
* @param port Server Port
* @return int exit code
*/
int CRPC_EXPORT run_cli(std::istream &input, std::ostream &output, const std::string &ip,
int run_cli(std::istream &input, std::ostream &output, const std::string &ip,
int port);
} // namespace cloud_point_rpc

View File

@ -6,11 +6,11 @@
#include <vector>
#include <yaml-cpp/yaml.h>
namespace score {
namespace cloud_point_rpc {
struct ServerConfig {
std::string ip;
int port{0};
int port;
};
struct TestData {

View File

@ -6,7 +6,8 @@
#include <jsonrpccxx/client.hpp>
#include <nlohmann/json.hpp>
#include <vector>
namespace score {
namespace cloud_point_rpc {
class RpcClient : public jsonrpccxx::JsonRpcClient {
public:

View File

@ -1,28 +0,0 @@
//
// Created by vptyp on 11.03.2026.
//
#pragma once
#include <string>
#include <vector>
#include "export.h"
namespace score {
class CRPC_EXPORT IRPCCoder {
public:
virtual ~IRPCCoder() = default;
virtual std::vector<char> decode(const std::string& encoded) = 0;
virtual std::string encode(const std::vector<char>& data) = 0;
};
class CRPC_EXPORT Base64RPCCoder final : public IRPCCoder {
public:
Base64RPCCoder();
~Base64RPCCoder() override;
std::vector<char> decode(const std::string& encoded) override;
std::string encode(const std::vector<char>& data) override;
};
}

View File

@ -1,37 +1,22 @@
#pragma once
#include "export.h"
#include <functional>
#include <jsonrpccxx/server.hpp>
#include <map>
#include <nlohmann/json.hpp>
#include <string>
#include <variant>
extern "C" {
struct rpc_string {
rpc_string(const char *data, uint64_t size) : s(data, size) {}
rpc_string() = default;
namespace cloud_point_rpc {
std::string s;
};
}
namespace score {
class CRPC_EXPORT RpcServer {
class RpcServer {
public:
using Handler = std::function<std::variant<nlohmann::json, std::string>(
const nlohmann::json &)>;
using callback_t = rpc_string *(*)(rpc_string *);
using Handler = std::function<nlohmann::json(const nlohmann::json &)>;
void register_method(const std::string &name, Handler handler);
void register_method(const std::string &name, callback_t handler);
///@param request_str json rpc 2.0 formatted string
[[nodiscard]] std::string process(const std::string &request_str);
private:
std::map<std::string, Handler> handlers_;
};
} // namespace score
} // namespace cloud_point_rpc

View File

@ -5,7 +5,7 @@
#include <type_traits>
#include <vector>
namespace score {
namespace cloud_point_rpc {
template <typename T>
concept NumericType = requires(T param) {

View File

@ -2,10 +2,10 @@
#include "cloud_point_rpc/config.hpp"
#include <vector>
#include "export.h"
namespace score {
class CRPC_EXPORT Service {
namespace cloud_point_rpc {
class Service {
public:
explicit Service(const TestData &data = {});

View File

@ -2,18 +2,17 @@
#include "cloud_point_rpc/serialize.hpp"
#include "jsonrpccxx/iclientconnector.hpp"
#include <asio.hpp>
#include <cloud_point_rpc/tcp_read.hpp>
#include <glog/logging.h>
#include <string>
#include "export.h"
namespace score {
namespace cloud_point_rpc {
/**
* TCPConnector main purpose is to implement jsonrpccxx::IClientConnector Send
* method As an internal implementation, TCPConnector adds to the beginning of
* the json "expected size" of a packet in case when the packet was for some
* reason on any level fractured.
*/
class CRPC_EXPORT TCPConnector : public jsonrpccxx::IClientConnector {
class TCPConnector : public jsonrpccxx::IClientConnector {
public:
TCPConnector(const std::string &ip, size_t port) noexcept(false)
: io_context_(), socket_(io_context_) {
@ -35,7 +34,42 @@ class CRPC_EXPORT TCPConnector : public jsonrpccxx::IClientConnector {
auto remove_const = request;
inplace_size_embedding(remove_const);
asio::write(socket_, asio::buffer(remove_const));
return tcp_read(socket_, "TCPConnector] ");
return read();
}
protected:
std::string read() {
std::string result;
std::array<char, 8> header;
LOG(INFO) << "trying to read";
try {
size_t len =
asio::read(socket_, asio::buffer(header, header.size()));
if (len != sizeof(uint64_t)) {
LOG(ERROR) << "failed to read header";
return result;
}
std::vector<uint8_t> v(header.begin(), header.begin() + 8);
uint64_t packet_size = deserialize<uint64_t>(v);
LOG(INFO) << "Received packet of size: " << packet_size;
std::vector<char> payload(packet_size);
len = asio::read(socket_, asio::buffer(payload));
// if failed to fetch in one pack, try to read until size
while (len < packet_size) {
len += asio::read(socket_, asio::buffer(payload.data() + len,
payload.size() - len));
}
LOG(INFO) << std::format("Was able to read len={}", len);
result = std::string(payload.begin(), payload.end());
LOG(INFO) << "Payload: \n" << result;
} catch (const std::exception &e) {
LOG(WARNING) << "Connector handling error: " << e.what();
}
return result;
}
private:

View File

@ -1,43 +0,0 @@
#pragma once
#include <array>
#include <asio.hpp>
#include <cloud_point_rpc/serialize.hpp>
#include <glog/logging.h>
namespace score {
static inline std::string tcp_read(asio::ip::tcp::socket &socket,
std::string_view prefix) {
std::string result;
std::array<char, 8> header;
LOG(INFO) << prefix.data() << "trying to read";
try {
size_t len = asio::read(socket, asio::buffer(header, header.size()));
if (len != sizeof(uint64_t)) {
LOG(ERROR) << prefix.data() << "failed to read header";
return result;
}
std::vector<uint8_t> v(header.begin(), header.begin() + 8);
uint64_t packet_size = deserialize<uint64_t>(v);
LOG(INFO) << prefix.data()
<< "Received packet of size: " << packet_size;
std::vector<char> payload(packet_size);
len = asio::read(socket, asio::buffer(payload));
// if failed to fetch in one pack, try to read until size
while (len < packet_size) {
len += asio::read(socket, asio::buffer(payload.data() + len,
payload.size() - len));
}
LOG(INFO) << prefix.data()
<< std::format("Was able to read len={}", len);
result = std::string(payload.begin(), payload.end());
DLOG(INFO) << prefix.data() << "Payload: \n" << result;
} catch (const std::exception &e) {
LOG(WARNING) << prefix.data() << "handling error: " << e.what();
}
return result;
}
} // namespace cloud_point_rpc

View File

@ -1,18 +1,16 @@
#pragma once
#include "export.h"
#include "cloud_point_rpc/serialize.hpp"
#include <asio.hpp>
#include <atomic>
#include <cloud_point_rpc/tcp_read.hpp>
#include <functional>
#include <glog/logging.h>
#include <list>
#include <ranges>
#include <string>
#include <thread>
namespace score {
class CRPC_EXPORT TcpServer {
namespace cloud_point_rpc {
class TcpServer {
public:
using RequestProcessor = std::function<std::string(const std::string &)>;
@ -20,16 +18,9 @@ class CRPC_EXPORT TcpServer {
: ip_(ip), port_(port), processor_(std::move(processor)),
acceptor_(io_context_), running_(false) {}
~TcpServer() {
stop();
std::lock_guard lock(cliThrMtx_);
for (auto &thread : client_threads_ | std::views::keys) {
thread.join();
}
}
~TcpServer() { stop(); }
void start() {
using namespace std::chrono_literals;
try {
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip_),
port_);
@ -44,38 +35,18 @@ class CRPC_EXPORT TcpServer {
accept_thread_ = std::jthread([this]() {
LOG(INFO) << "Accept thread started";
while (running_) {
{
std::lock_guard lock(cliThrMtx_);
client_threads_.remove_if([](auto &client_info) {
bool result = false;
if (client_info.second.wait_for(0ms) ==
std::future_status::ready) {
client_info.first.join();
result = true;
}
return result;
});
}
try {
auto socket = std::make_shared<asio::ip::tcp::socket>(
io_context_);
{
std::lock_guard lock(acceptorMtx_);
acceptor_.accept(*socket);
}
acceptor_.accept(*socket);
LOG(INFO)
<< "New connection from "
<< socket->remote_endpoint().address().to_string();
auto done = std::make_shared<std::promise<bool>>();
{
std::lock_guard lock(cliThrMtx_);
client_threads_.push_back(std::make_pair(
std::jthread([this, socket, done]() {
handle_client(socket);
done->set_value(true);
}),
done->get_future()));
}
std::jthread([this, socket]() {
handle_client(socket);
}).detach();
} catch (const std::system_error &e) {
LOG(INFO) << "Accept exception: " << e.what();
if (running_) {
@ -96,9 +67,15 @@ class CRPC_EXPORT TcpServer {
return;
LOG(INFO) << "Stopping server...";
running_ = false;
// Ensure accept unblocks by connecting a dummy socket FIRST,
// while the acceptor is still open. This avoids a race where close()
// removes the listen endpoint before the connect completes.
// Closing acceptor unblocks accept() call usually, but sometimes we
// need to prod it
asio::error_code ec;
std::ignore = acceptor_.close(ec);
if (ec.value()) {
LOG(ERROR) << std::format(
"acceptor closed with a value returned = {}", ec.value());
}
// Ensure accept unblocks by connecting a dummy socket
try {
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip_),
port_);
@ -108,16 +85,6 @@ class CRPC_EXPORT TcpServer {
} catch (...) {
// Ignore
}
// Now close the acceptor to unblock any pending accept()
asio::error_code ec;
{
std::lock_guard lock(acceptorMtx_);
std::ignore = acceptor_.close(ec);
}
if (ec.value()) {
LOG(ERROR) << std::format(
"acceptor closed with a value returned = {}", ec.value());
}
LOG(INFO) << "Acceptor closed";
}
@ -131,14 +98,32 @@ class CRPC_EXPORT TcpServer {
private:
void handle_client(std::shared_ptr<asio::ip::tcp::socket> socket) {
LOG(INFO) << "Server reading from client...";
try {
auto payload = tcp_read(*socket, "TCPServer] ");
size_t payload_length = payload.size();
LOG(INFO) << "Server reading from client...";
std::array<char, 8> header;
size_t header_length = asio::read(*socket, asio::buffer(header));
if (header_length != 8) {
LOG(WARNING) << "Invalid header length: " << header_length;
return;
}
uint64_t packet_size = *reinterpret_cast<uint64_t *>(header.data());
LOG(INFO) << "Expected packet size: " << packet_size;
std::vector<char> payload(packet_size);
size_t payload_length = asio::read(*socket, asio::buffer(payload));
LOG(INFO) << "Read payload length: " << payload_length;
if (payload_length != packet_size) {
LOG(WARNING) << "Payload length mismatch: expected "
<< packet_size << ", got " << payload_length;
return;
}
if (payload_length > 0) {
std::string response = processor_(payload);
std::string request(payload.data(), payload_length);
LOG(INFO) << "Server processing request: " << request;
std::string response = processor_(request);
response += "\n";
DLOG(INFO) << "Server sending response: " << response;
LOG(INFO) << "Server sending response: " << response;
inplace_size_embedding(response);
asio::write(*socket, asio::buffer(response));
LOG(INFO) << "Server sent response";
@ -156,10 +141,7 @@ class CRPC_EXPORT TcpServer {
asio::ip::tcp::acceptor acceptor_;
std::atomic<bool> running_;
std::list<std::pair<std::jthread, std::future<bool>>> client_threads_;
std::mutex cliThrMtx_;
std::mutex acceptorMtx_;
std::jthread accept_thread_;
};
} // namespace score
} // namespace cloud_point_rpc

View File

@ -1,14 +0,0 @@
#pragma once
#if defined(_WIN32)
#ifdef CRPC_SERVER_API_EXPORT
#define CRPC_EXPORT __declspec(dllexport)
#else
#define CRPC_EXPORT __declspec(dllimport)
#endif
#elif defined(__GNUC__) // GCC, Clang, etc.
// Linux, macOS, etc.
#define CRPC_EXPORT __attribute__((visibility("default")))
#else
#define CRPC_EXPORT
#pragma warning Unknown dynamic link import/export semantics.
#endif

View File

@ -1,39 +0,0 @@
#ifndef CRPC_SERVER_API
#define CRPC_SERVER_API
#include <cstdint>
#include "export.h"
#ifdef __cplusplus
extern "C" {
#endif //cpp
/**
* @brief basically just std::string wrapper:
* struct rpc_string {
* std::string s;
* };
* has internal gc and would be automatically deallocated on deinit call,
* but it is better to call destroy manually, to prevent exceeding memory usage
*/
struct CRPC_EXPORT rpc_string;
CRPC_EXPORT const char* crpc_str_get_data(const rpc_string*);
CRPC_EXPORT uint64_t crpc_str_get_size(const rpc_string*);
CRPC_EXPORT rpc_string* crpc_str_create(const char* data, uint64_t size);
CRPC_EXPORT void crpc_str_destroy(rpc_string*);
typedef rpc_string*(*callback_t)(rpc_string*);
CRPC_EXPORT void crpc_init(const char* config_path);
CRPC_EXPORT void crpc_init_with_address(const char* ip, int port);
CRPC_EXPORT void crpc_deinit();
CRPC_EXPORT void crpc_add_method(callback_t cb, rpc_string* name);
#ifdef __cplusplus
}
#endif //cpp
#endif //CRPC_SERVER_API

View File

@ -1,50 +0,0 @@
#ifndef CRPC_TEST_API
#define CRPC_TEST_API
#include <cstdint>
#include "export.h"
#ifdef __cplusplus
extern "C" {
#endif //cpp
struct CRPC_EXPORT rpc_string;
/// @brief callback function intendent to handle json and retrieve string data
typedef rpc_string*(*callback_t)(rpc_string*);
/// @brief call it to initialize library thread;
/// by default would call available methods one by one
CRPC_EXPORT void crpc_test_init();
/// @brief please, call it, when you're done working with the lib
/// @note blocking operation
CRPC_EXPORT void crpc_test_deinit();
/// @brief crpc_test_add_method will add to the available set/map
/// @note blocking operation
CRPC_EXPORT void crpc_test_add_method(callback_t cb, rpc_string* name);
/// @brief crpc_test_remove_method would
/// @param name pointer to name string; memory management not transferred
CRPC_EXPORT int crpc_test_remove_method(rpc_string* name);
/// @brief crpc_schedule_call would add to the execution queue
/// may be called one after current
/// @note blocking operation
CRPC_EXPORT void crpc_test_schedule_call(rpc_string* name);
/// @brief crpc_test_change_duration will change duration between regular calls
/// @note blocking operation
CRPC_EXPORT void crpc_test_change_duration(uint64_t duration_ms);
/// @note blocking operation
CRPC_EXPORT uint64_t crpc_test_duration();
/// @brief crpc_test_auto_call change internal state of auto call
/// @param state 0 - turn off auto call on sleep; 1 - turn on auto call on sleep
CRPC_EXPORT void crpc_test_auto_call(uint32_t state);
#ifdef __cplusplus
}
#endif //cpp
#endif //CRPC_SERVER_API

View File

@ -6,25 +6,11 @@ project('cloud_point_rpc', 'cpp',
json_dep = dependency('nlohmann_json', fallback : ['nlohmann_json', 'nlohmann_json_dep'])
thread_dep = dependency('threads')
asio_dep = dependency('asio', fallback : ['asio', 'asio_dep'])
base64_dep = dependency('base64', fallback: ['aklomp-base64', 'base64'])
# GLog via CMake fallback
cmake = import('cmake')
glog_opt = cmake.subproject_options()
glog_opt.add_cmake_defines({
'WITH_GFLAGS': 'OFF',
'WITH_GTEST': 'OFF',
'CMAKE_POLICY_VERSION_MINIMUM': '3.5'
})
libtype = get_option('default_library')
if libtype == 'static'
message('Will share static state with glog')
glog_opt.add_cmake_defines({
'BUILD_SHARED_LIBS': 'OFF',
})
add_project_arguments('-DBASE64_STATIC_DEFINE', '-DYAML_CPP_STATIC_DEFINE', language: 'cpp')
endif
glog_opt.add_cmake_defines({'WITH_GFLAGS': 'OFF', 'WITH_GTEST': 'OFF'})
glog_proj = cmake.subproject('glog', options: glog_opt)
glog_dep = glog_proj.dependency('glog')
@ -40,18 +26,3 @@ inc = include_directories(['include', 'rpc/include'])
subdir('src')
subdir('tests')
if host_machine.system() == 'windows' and libtype == 'shared'
message('We are on Windows, so to prevent our ass from pain')
devenv = environment()
message('By the way, I hate windows :O')
prefixed = meson.global_build_root() / 'subprojects'
message('Prefixed dir is: ', prefixed)
devenv.append('PATH', prefixed / 'glog')
devenv.append('PATH', prefixed / 'googletest-1.17.0' / 'googletest')
devenv.append('PATH', prefixed / 'googletest-1.17.0' / 'googlemock')
devenv.append('PATH', prefixed / 'yaml-cpp-0.8.0')
meson.add_devenv(devenv)
endif

View File

@ -3,7 +3,7 @@
#include <glog/logging.h>
#include <string>
namespace score {
namespace cloud_point_rpc {
void print_menu(std::ostream &output) {
output << "\n=== Cloud Point RPC CLI ===" << std::endl;

View File

@ -24,8 +24,8 @@ int main(int argc, char *argv[]) {
f.close();
try {
auto config = score::ConfigLoader::load(config_path);
return score::run_cli(std::cin, std::cout, config.server.ip,
auto config = cloud_point_rpc::ConfigLoader::load(config_path);
return cloud_point_rpc::run_cli(std::cin, std::cout, config.server.ip,
config.server.port);
} catch (const std::exception &e) {
std::cerr << "Failed to start CLI: " << e.what() << std::endl;

View File

@ -1,79 +1,28 @@
add_project_arguments('-DCRPC_SERVER_API_EXPORT', language: 'cpp')
cloud_point_rpc_sources = files(
'rpc_coder.cpp',
'rpc_server.cpp',
'server_api.cpp',
'service.cpp',
'rpc_server.cpp',
'service.cpp',
'cli.cpp'
)
libcloud_point_rpc = shared_library(
'cloud_point_rpc',
cloud_point_rpc_sources,
include_directories: inc,
dependencies: [json_dep, thread_dep, glog_dep, yaml_dep, asio_dep, base64_dep],
install: true,
install_rpath: '$ORIGIN',
)
libcloud_point_rpc = static_library('cloud_point_rpc',
cloud_point_rpc_sources,
include_directories : inc,
dependencies : [json_dep, thread_dep, glog_dep, yaml_dep, asio_dep],
install : true)
cloud_point_rpc_dep = declare_dependency(
include_directories: inc,
link_with: libcloud_point_rpc,
dependencies: [json_dep, glog_dep, yaml_dep, asio_dep, base64_dep],
)
# Test lib
libcloud_point_rpc_test = shared_library(
'test_cloud_point',
'test_api.cpp',
dependencies: cloud_point_rpc_dep,
install: true,
install_rpath: '$ORIGIN',
)
cloud_point_rpc_test_dep = declare_dependency(
include_directories: inc,
link_with: libcloud_point_rpc_test,
dependencies: [cloud_point_rpc_dep],
)
libcloud_point_rpc_cli = shared_library(
'libcloud_point_rpc_cli',
'cli.cpp',
include_directories: inc,
dependencies: [cloud_point_rpc_dep],
install: true,
)
cloud_point_rpc_cli_dep = declare_dependency(
include_directories: inc,
link_with: libcloud_point_rpc_cli,
dependencies: [cloud_point_rpc_dep],
)
include_directories : inc,
link_with : libcloud_point_rpc,
dependencies : [json_dep, glog_dep, yaml_dep, asio_dep])
# Client/CLI tool (legacy stdin/stdout)
executable(
'cloud_point_rpc_cli',
[
'main.cpp',
],
dependencies: cloud_point_rpc_cli_dep,
install: true,
)
executable('cloud_point_rpc_cli',
'main.cpp',
dependencies : cloud_point_rpc_dep,
install : true)
# Server executable (TCP)
executable(
'cloud_point_rpc_server',
'server_main.cpp',
dependencies: cloud_point_rpc_dep,
link_args: '-pthread',
install: true,
)
# Minimal client example
executable(
'minimal_client',
'minimal_client.cpp',
dependencies: cloud_point_rpc_dep,
install: true,
)
executable('cloud_point_rpc_server',
'server_main.cpp',
dependencies : cloud_point_rpc_dep,
install : true)

View File

@ -1,34 +0,0 @@
#include "cloud_point_rpc/config.hpp"
#include "cloud_point_rpc/tcp_connector.hpp"
#include <glog/logging.h>
#include <iostream>
#include <string>
int main(int argc, char *argv[]) {
google::InitGoogleLogging(argv[0]);
FLAGS_logtostderr = 1;
std::string config_path = "config.yml";
if (argc > 1) {
config_path = argv[1];
}
try {
auto config = score::ConfigLoader::load(config_path);
score::TCPConnector connector(config.server.ip,
static_cast<size_t>(config.server.port));
const std::string request =
R"({"jsonrpc":"2.0","method":"ping","params":{},"id":1})";
std::string response = connector.Send(request);
std::cout << response << std::endl;
} catch (const std::exception &e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -1,46 +0,0 @@
//
// Created by vptyp on 11.03.2026.
//
#include "cloud_point_rpc/rpc_coder.hpp"
#include "libbase64.h"
#include <glog/logging.h>
namespace score {
Base64RPCCoder::Base64RPCCoder() = default;
Base64RPCCoder::~Base64RPCCoder() = default;
/**
* Tries to decode ASCII complained string to the
* @param encoded ASCII complained base64 encoded string
* @return vector of raw bytes << allocated on encoded.size() / 4 * 3 + 1 size
*/
std::vector<char> Base64RPCCoder::decode(const std::string &encoded) {
if (encoded.length() > (std::numeric_limits<size_t>::max() / 3) * 4)
throw std::length_error("Base64 input too large");
DLOG(INFO) << "Base64RPCCoder::decode";
std::vector<char> result((encoded.length() >> 2) * 3 + 1);
size_t result_len = 0;
base64_decode(encoded.data(), encoded.size(), result.data(), &result_len,
0);
DLOG(INFO) << "result_len: " << result_len;
result.resize(result_len);
return result;
}
/**
*
* @param data raw byte stream
* @return encoded base64 string
*/
std::string Base64RPCCoder::encode(const std::vector<char> &data) {
if (data.size() > (std::numeric_limits<size_t>::max() / 4) * 3)
throw std::length_error("raw input is too large");
DLOG(INFO) << "Base64RPCCoder::encode";
size_t result_len = 0;
std::string result((data.size() + 2) / 3 * 4, 0);
base64_encode(data.data(), data.size(), result.data(), &result_len, 0);
DLOG(INFO) << "result_len: " << result_len;
result.resize(result_len);
return result;
}
} // namespace score

View File

@ -1,10 +1,8 @@
#include "cloud_point_rpc/rpc_server.hpp"
#include "server_api.h"
#include <glog/logging.h>
#include <variant>
using json = nlohmann::json;
namespace score {
namespace cloud_point_rpc {
namespace {
json create_error(int code, const std::string &message,
@ -14,68 +12,21 @@ json create_error(int code, const std::string &message,
{"id", id}};
}
struct CreateSuccess {
json obj;
json id;
void operator()(const json &result) {
obj = {{"jsonrpc", "2.0"}, {"result", result}, {"id", id}};
}
void operator()(const std::string &result) {
obj = {{"jsonrpc", "2.0"}, {"result", result}, {"id", id}};
}
};
json create_success(const json &result, const json &id) {
return {{"jsonrpc", "2.0"}, {"result", result}, {"id", id}};
}
} // namespace
template <typename T> struct Deleter {
void operator()(T *element) {
(void)element;
LOG(ERROR) << "Called default deleter";
}
};
template <> struct Deleter<rpc_string> {
void operator()(rpc_string *element) {
if (element) {
crpc_str_destroy(element);
}
}
};
using rpcStringPtr = std::unique_ptr<rpc_string, Deleter<rpc_string>>;
void RpcServer::register_method(const std::string &name, Handler handler) {
handlers_[name] = std::move(handler);
}
void RpcServer::register_method(const std::string &name, callback_t handler) {
handlers_[name] = [handler](const nlohmann::json &j)
-> std::variant<nlohmann::json, std::string> {
rpc_string tmp;
tmp.s = j.dump();
auto res = rpcStringPtr(handler(&tmp));
if (!res) {
LOG(ERROR) << "Method is invalid";
return {};
}
std::variant<nlohmann::json, std::string> ret;
try {
ret = json::parse(res->s);
} catch (std::exception &e) {
DLOG(INFO) << "return value is not a json";
ret = res->s;
}
return ret;
};
}
std::string RpcServer::process(const std::string &request_str) {
LOG(INFO) << request_str;
json request;
try {
request = json::parse(request_str);
} catch (const json::parse_error &) {
LOG(ERROR) << "json parse error; " << __func__;
LOG(ERROR) << "json parse error" << __func__;
return create_error(-32700, "Parse error").dump();
}
@ -98,14 +49,11 @@ std::string RpcServer::process(const std::string &request_str) {
}
try {
auto result = it->second(params);
CreateSuccess visitor;
visitor.id = id;
std::visit(visitor, result);
return visitor.obj.dump();
json result = it->second(params);
return create_success(result, id).dump();
} catch (const std::exception &e) {
return create_error(-32000, e.what(), id).dump(); // Server error
}
}
} // namespace score
} // namespace cloud_point_rpc

View File

@ -1,114 +0,0 @@
#include "server_api.h"
#include "cloud_point_rpc/config.hpp"
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/tcp_server.hpp"
#include <algorithm>
#include <glog/logging.h>
#include <list>
#include <memory>
#include <string>
static std::list<std::unique_ptr<rpc_string>> gc;
std::mutex gc_mtx;
std::mutex server_mtx;
score::RpcServer rpc_server;
std::unique_ptr<score::TcpServer> server = nullptr;
extern "C" {
const char *crpc_str_get_data(const rpc_string *that) {
if (!that) {
LOG(ERROR) << "Tried to get data on nullptr";
return nullptr;
}
return that->s.c_str();
}
uint64_t crpc_str_get_size(const rpc_string *that) {
if (!that) {
LOG(ERROR) << "Tried to get size on nullptr";
return 0;
}
return that->s.size();
}
rpc_string *crpc_str_create(const char *data, uint64_t size) {
if (!data) {
LOG(ERROR) << "Tried to create with nullptr data";
return nullptr;
}
std::lock_guard lock(gc_mtx);
gc.push_back(std::make_unique<rpc_string>(data, size));
return gc.back().get();
}
void crpc_str_destroy(rpc_string *that) {
if (!that) {
LOG(ERROR) << "Tried to destroy on nullptr";
return;
}
std::lock_guard lock(gc_mtx);
auto it = std::ranges::find(gc, that, &std::unique_ptr<rpc_string>::get);
if (it != gc.end())
gc.erase(it);
}
void crpc_init(const char *config_path) {
if (!google::IsGoogleLoggingInitialized())
google::InitGoogleLogging("CloudPointRPC");
if (config_path == nullptr) {
LOG(ERROR) << "config_path was not provided";
return;
}
try {
auto config = score::ConfigLoader::load(config_path);
LOG(INFO) << "Loaded config from " << config_path;
server = std::make_unique<score::TcpServer>(
config.server.ip, config.server.port,
[&](const std::string &request) {
std::lock_guard lock(server_mtx);
return rpc_server.process(request);
});
server->start();
} catch (const std::exception &e) {
LOG(ERROR) << "Fatal error: " << e.what();
}
}
void crpc_init_with_address(const char *ip, int port) {
if (!google::IsGoogleLoggingInitialized())
google::InitGoogleLogging("CloudPointRPC");
if (!ip) {
LOG(ERROR) << "ip was not provided";
return;
}
try {
server = std::make_unique<score::TcpServer>(
std::string(ip), static_cast<size_t>(port),
[&](const std::string &request) {
std::lock_guard lock(server_mtx);
return rpc_server.process(request);
});
server->start();
LOG(INFO) << "Server started on " << ip << ":" << port;
} catch (const std::exception &e) {
LOG(ERROR) << "Fatal error: " << e.what();
}
}
void crpc_deinit() {
server.reset();
std::lock_guard lock(gc_mtx);
gc.clear();
}
void crpc_add_method(callback_t cb, rpc_string *name) {
if (!name || !cb) {
LOG(ERROR) << "Invalid arguments (nullptr)";
return;
}
std::lock_guard lock(server_mtx);
rpc_server.register_method(name->s, cb);
}
}

View File

@ -20,12 +20,12 @@ int main(int argc, char *argv[]) {
LOG(INFO) << "Starting Cloud Point RPC Server (Test Mock)...";
try {
auto config = score::ConfigLoader::load(config_path);
auto config = cloud_point_rpc::ConfigLoader::load(config_path);
LOG(INFO) << "Loaded config from " << config_path;
// Inject test data into service
score::Service service(config.test_data);
score::RpcServer rpc_server;
cloud_point_rpc::Service service(config.test_data);
cloud_point_rpc::RpcServer rpc_server;
rpc_server.register_method("get-intrinsic-params", [&](const json &) {
return service.get_intrinsic_params();
@ -39,7 +39,7 @@ int main(int argc, char *argv[]) {
return service.get_cloud_point();
});
score::TcpServer server(config.server.ip, config.server.port,
cloud_point_rpc::TcpServer server(config.server.ip, config.server.port,
[&](const std::string &request) {
return rpc_server.process(
request);

View File

@ -1,6 +1,6 @@
#include "cloud_point_rpc/service.hpp"
namespace score {
namespace cloud_point_rpc {
Service::Service(const TestData &data) : data_(data) {}

View File

@ -1,192 +0,0 @@
#include "test_api.h"
#include "cloud_point_rpc/rpc_server.hpp"
#include "server_api.h"
#include <condition_variable>
#include <glog/logging.h>
#include <mutex>
#include <queue>
#include <set>
#include <stop_token>
#include <thread>
class TestThread {
static std::string make_jsonrpc(const std::string &method_name) {
return std::format(R"(
{{
"jsonrpc": "2.0",
"method": "{}",
"params": {{}},
"id": 1
}}
)",
method_name);
}
public:
TestThread() = default;
~TestThread() { join(); }
void routine() {
size_t distance{0};
std::unique_lock lock(mtx);
const std::stop_token stoken = thr.get_stop_token();
lock.unlock();
while (!stoken.stop_requested()) {
lock.lock();
if (!calls_queue.empty()) {
auto front = calls_queue.front();
DLOG(INFO) << front << " will try to call";
calls_queue.pop();
LOG(INFO) << server.process(make_jsonrpc(front));
} else if (state.load() && !methods.empty()) {
auto it = methods.begin();
std::advance(it, distance++ % methods.size());
DLOG(INFO) << *it << " : Method at this position";
LOG(INFO) << server.process(make_jsonrpc(*it));
}
if (state.load() && calls_queue.empty())
cv.wait_for(lock, thr_sleep,
[&] { return stoken.stop_requested(); });
lock.unlock();
}
LOG(INFO) << "Stopped";
}
void start() { thr = std::jthread(&TestThread::routine, this); }
void join() {
if (thr.joinable()) {
DLOG(INFO) << "Requested thread stop";
thr.request_stop();
cv.notify_one();
thr.join();
}
}
void add_method(const callback_t cb, rpc_string *name) {
if(!name || !name->s.size()) {
LOG(ERROR) << "Tried to add method with invalid name";
return;
}
LOG(INFO) << "Trying to add method: " << name->s;
std::lock_guard lock(mtx);
if (methods.contains(name->s)) {
LOG(INFO) << "Method already exists: " << name->s;
return;
}
methods.emplace(name->s);
server.register_method(name->s, cb);
}
int remove_method(const rpc_string *name) {
if(!name || !name->s.size()) {
LOG(ERROR) << "Tried to remove method with invalid name";
return -1;
}
LOG(INFO) << "Trying to remove method: " << name->s;
std::lock_guard lock(mtx);
int result = 0;
auto it = std::find(methods.begin(), methods.end(), name->s);
if (it != methods.end()) {
methods.erase(it);
} else {
LOG(ERROR) << "Method not found: " << name->s;
result = -1;
}
return result;
}
void call(const rpc_string *name) {
if (!name) {
LOG(ERROR) << "Called with nullptr name";
return;
}
std::lock_guard lock(mtx);
LOG(INFO) << server.process(name->s);
}
void set_duration(uint64_t duration_ms) {
LOG(INFO) << "Trying to install sleep duration: " << duration_ms;
std::lock_guard lock(mtx);
thr_sleep = std::chrono::milliseconds(duration_ms);
}
uint64_t get_duration() {
std::lock_guard lock(mtx);
return thr_sleep.count();
}
void add_queue_call(const std::string &name) {
std::lock_guard lock(mtx);
calls_queue.emplace(name);
}
void auto_call(bool state) {
this->state.store(state, std::memory_order_relaxed);
}
void reset() {
std::lock_guard lock(mtx);
calls_queue = std::queue<std::string>();
methods.clear();
state.store(true, std::memory_order_relaxed);
server = score::RpcServer();
}
private:
std::atomic<bool> state{true};
std::condition_variable cv;
std::queue<std::string> calls_queue{};
std::set<std::string> methods{};
std::jthread thr;
std::mutex mtx;
score::RpcServer server;
std::chrono::duration<int64_t, std::milli> thr_sleep{50};
} test;
extern "C" {
void crpc_test_init() {
if (!google::IsGoogleLoggingInitialized()) {
google::InitGoogleLogging("TestRPC");
google::LogToStderr();
}
try {
test.start();
} catch (const std::exception &e) {
LOG(ERROR) << "Fatal error: " << e.what();
}
}
void crpc_test_deinit() {
test.join();
crpc_deinit();
test.reset();
}
void crpc_test_add_method(callback_t cb, rpc_string *name) {
test.add_method(cb, name);
}
void crpc_test_change_duration(uint64_t duration_ms) {
test.set_duration(duration_ms);
}
uint64_t crpc_test_duration() { return test.get_duration(); }
int crpc_test_remove_method(rpc_string *name) {
return test.remove_method(name);
}
void crpc_test_schedule_call(rpc_string *name) {
if (!name) {
LOG(ERROR) << "Called with name nullptr";
return;
}
test.add_queue_call(name->s);
}
void crpc_test_auto_call(uint32_t state) {
test.auto_call(static_cast<bool>(state));
}
}

View File

@ -1,14 +0,0 @@
[wrap-file]
directory = base64-0.5.2
source_url = https://github.com/aklomp/base64/archive/refs/tags/v0.5.2.tar.gz
source_filename = base64-0.5.2.tar.gz
source_hash = 723a0f9f4cf44cf79e97bcc315ec8f85e52eb104c8882942c3f2fba95acc080d
source_fallback_url = https://wrapdb.mesonbuild.com/v2/aklomp-base64_0.5.2-1/get_source/base64-0.5.2.tar.gz
patch_filename = aklomp-base64_0.5.2-1_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/aklomp-base64_0.5.2-1/get_patch
patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/aklomp-base64_0.5.2-1/aklomp-base64_0.5.2-1_patch.zip
patch_hash = 9805354b8c0333fe0123c10d8c62356ef1d0d67a2689a348d18f73bddc1e2b10
wrapdb_version = 0.5.2-1
[provide]
dependency_names = base64

View File

@ -1,20 +1,12 @@
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_c_api_edge_cases.cpp',
'test_base64.cpp',
'test_base64_edge_cases.cpp',
'test_service.cpp',
'test_serialize.cpp'
'test_tcp.cpp'
)
test_exe = executable('unit_tests',
test_sources,
dependencies : [cloud_point_rpc_dep, cloud_point_rpc_cli_dep, cloud_point_rpc_test_dep, json_dep, gtest_dep, gtest_main_dep, gmock_dep])
dependencies : [cloud_point_rpc_dep, gtest_dep, gtest_main_dep, gmock_dep])
test('unit_tests', test_exe)

View File

@ -1,31 +0,0 @@
//
// Created by vptyp on 11.03.2026.
//
#include <chrono>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <thread>
#include "cloud_point_rpc/config.hpp"
#include "cloud_point_rpc/rpc_coder.hpp"
class Base64Test : public ::testing::Test {
protected:
void SetUp() override {
}
void TearDown() override {
}
};
TEST_F(Base64Test, EncodeDecode) {
std::vector raw{'H', 'e', 'l', 'l', 'o', 'w', '\0'};
score::Base64RPCCoder coder;
auto encoded = coder.encode(raw);
LOG(INFO) << "encoded: " << encoded;
auto decoded = coder.decode(encoded);
EXPECT_EQ(std::ranges::equal(decoded, raw), true);
LOG(INFO) << "done";
}

View File

@ -1,181 +0,0 @@
#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

@ -1,176 +0,0 @@
#include "cloud_point_rpc/rpc_server.hpp"
#include "server_api.h"
#include "test_api.h"
#include <future>
#include <glog/logging.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
class TestCApi : public ::testing::Test {
protected:
void SetUp() override {
FLAGS_logtostderr = true;
if (!google::IsGoogleLoggingInitialized())
google::InitGoogleLogging("TestRPC");
EXPECT_NO_THROW(crpc_test_init());
}
void TearDown() override { crpc_test_deinit(); }
};
TEST_F(TestCApi, Base) {
rpc_string name;
name.s = "test";
static std::promise<bool> task;
std::future<bool> called = task.get_future();
DLOG(INFO) << "Test";
crpc_test_add_method(
+[](rpc_string *) -> rpc_string * {
static bool installed = false;
DLOG(INFO) << "Trying to do something";
if (!installed) {
DLOG(INFO) << "Trying to install";
installed = true;
task.set_value(installed);
}
DLOG(INFO) << "Go out";
return crpc_str_create("res", sizeof("res") - 1);
},
&name);
using namespace std::chrono_literals;
const std::future_status res = called.wait_for(500ms);
EXPECT_NE(res, std::future_status::timeout);
EXPECT_EQ(called.get(), true);
DLOG(INFO) << "DONE";
}
TEST_F(TestCApi, AddedMultiple) {
constexpr int N = 4;
std::array<std::promise<bool>, N> tasks;
std::array<std::pair<std::future<bool>, rpc_string>, N> called;
// The Bridge: A static pointer local to this test function
static std::array<std::promise<bool>, N> *bridge;
bridge = &tasks;
for (int i = 0; i < N; i++) {
called[i].first = tasks[i].get_future();
std::string n = "test" + std::to_string(i);
called[i].second = rpc_string{n.c_str(), n.size()};
}
auto register_idx = [&]<size_t I>() {
crpc_test_add_method(
+[](rpc_string *) -> rpc_string * {
static bool installed = false;
if (!installed) {
installed = true;
(*bridge)[I].set_value(true);
}
return crpc_str_create("res", sizeof("res"));
},
&called[I].second);
};
register_idx.template operator()<0>();
register_idx.template operator()<1>();
register_idx.template operator()<2>();
register_idx.template operator()<3>();
auto test_idx = [&]<size_t I>() {
using namespace std::chrono_literals;
const std::future_status res = called[I].first.wait_for(1000ms);
EXPECT_NE(res, std::future_status::timeout);
EXPECT_EQ(called[I].first.get(), true);
LOG(INFO) << "Done" << "; task=" << I;
};
test_idx.template operator()<0>();
test_idx.template operator()<1>();
test_idx.template operator()<2>();
test_idx.template operator()<3>();
}
TEST_F(TestCApi, RemoveMethod) {
rpc_string name{"test", sizeof("test") - 1};
EXPECT_EQ(crpc_test_remove_method(&name), -1);
}
TEST_F(TestCApi, ChangeDuration) {
constexpr int dur = 1e3;
EXPECT_NO_THROW(crpc_test_change_duration(dur));
EXPECT_EQ(crpc_test_duration(), dur);
LOG(INFO) << "Test";
}
TEST_F(TestCApi, ScheduleCall) {
EXPECT_NO_THROW(crpc_test_auto_call(0));
constexpr int N = 4;
std::array<std::promise<bool>, N> tasks;
std::array<std::pair<std::future<bool>, rpc_string>, N> called;
// The Bridge: A static pointer local to this test function
static std::array<std::promise<bool>, N> *bridge;
bridge = &tasks;
LOG(INFO) << "Started Schedule Call";
for (int i = 0; i < N; i++) {
called[i].first = tasks[i].get_future();
std::string n = "test" + std::to_string(i);
called[i].second = rpc_string{n.c_str(), n.size()};
}
auto register_idx = [&]<size_t I>() {
crpc_test_add_method(
+[](rpc_string *) -> rpc_string * {
static bool installed = false;
if (!installed) {
installed = true;
(*bridge)[I].set_value(true);
}
std::string_view res = "res";
return crpc_str_create(res.data(), res.size());
},
&called[I].second);
};
auto test_idx = [&]<size_t I>() {
using namespace std::chrono_literals;
const std::future_status res = called[I].first.wait_for(500ms);
EXPECT_NE(res, std::future_status::timeout);
EXPECT_EQ(called[I].first.get(), true);
};
auto schedule_call = []<size_t I>() {
rpc_string test{"test", sizeof("test") - 1};
test.s += std::to_string(I);
LOG(INFO) << "Trying to add :" << test.s;
crpc_test_schedule_call(&test);
};
register_idx.template operator()<0>();
register_idx.template operator()<1>();
register_idx.template operator()<2>();
register_idx.template operator()<3>();
schedule_call.template operator()<0>();
schedule_call.template operator()<1>();
schedule_call.template operator()<2>();
schedule_call.template operator()<3>();
test_idx.template operator()<0>();
test_idx.template operator()<1>();
test_idx.template operator()<2>();
test_idx.template operator()<3>();
}
TEST_F(TestCApi, String) {
rpc_string name;
name.s = "test";
EXPECT_EQ(name.s.c_str(), crpc_str_get_data(&name));
EXPECT_EQ(name.s.size(), crpc_str_get_size(&name));
std::string_view testString = "test 2222";
auto creation =
crpc_str_create(testString.data(), testString.size());
EXPECT_EQ(std::string_view(crpc_str_get_data(creation)), testString);
EXPECT_NO_THROW(crpc_str_destroy(creation));
}

View File

@ -1,213 +0,0 @@
#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

@ -9,26 +9,24 @@
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/tcp_server.hpp"
using namespace score;
using namespace cloud_point_rpc;
class CliTest : public ::testing::Test {
public:
void start() {
tcp_server->start();
}
protected:
void SetUp() override {
server_ip = "127.0.0.1";
server_port = 9096;
rpc_server = std::make_unique<RpcServer>();
rpc_server->register_method(
"hello", [](const nlohmann::json &) { return "world"; });
tcp_server = std::make_unique<TcpServer>(
server_ip, server_port, [this](const std::string &req) {
return rpc_server->process(req);
});
tcp_server->start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
@ -51,7 +49,7 @@ TEST_F(CliTest, SendsInputToServerAndReceivesResponse) {
"get-intrinsic-params", [](const nlohmann::json &) {
return std::vector<double>{14589.0, 22489.0, 3123124.555};
});
this->start();
input << "1" << std::endl;
input << "0" << std::endl;

View File

@ -11,7 +11,7 @@
#include <fstream>
using namespace score;
using namespace cloud_point_rpc;
class IntegrationTest : public ::testing::Test {
protected:
@ -69,7 +69,7 @@ class IntegrationTest : public ::testing::Test {
std::remove("config.yaml");
}
Config config_{};
Config config_;
std::unique_ptr<Service> service_;
std::unique_ptr<RpcServer> rpc_server_;
std::unique_ptr<TcpServer> tcp_server_;

View File

@ -7,7 +7,7 @@
#include <vector>
using json = nlohmann::json;
using namespace score;
using namespace cloud_point_rpc;
class RpcServerTest : public ::testing::Test {
protected:

View File

@ -1,253 +0,0 @@
#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);
}

View File

@ -1,96 +0,0 @@
#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;
}

View File

@ -1,114 +0,0 @@
#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

@ -15,11 +15,11 @@ class TcpTest : public ::testing::Test {
protected:
void SetUp() override {
server_ = std::make_unique<score::TcpServer>(
server_ = std::make_unique<cloud_point_rpc::TcpServer>(
"127.0.0.1", 12345, [this](const std::string &request) {
EXPECT_EQ(request, expected_);
std::string msg = "Echo: " + request;
auto v = score::serialize(msg.length());
auto v = cloud_point_rpc::serialize(msg.length());
std::string res(v.begin(), v.end());
res += msg;
return res;
@ -33,26 +33,26 @@ class TcpTest : public ::testing::Test {
}
std::string expected_;
std::unique_ptr<score::TcpServer> server_;
std::unique_ptr<cloud_point_rpc::TcpServer> server_;
};
TEST(SerializeTest, Base) {
uint64_t value{123};
auto res = score::serialize(value);
EXPECT_EQ(value, score::deserialize<uint64_t>(res));
auto res = cloud_point_rpc::serialize(value);
EXPECT_EQ(value, cloud_point_rpc::deserialize<uint64_t>(res));
}
TEST_F(TcpTest, EchoTest) {
constexpr std::string_view msg = "Hello, TCP Server!";
ExpectedResponse(msg.data());
score::TCPConnector connector("127.0.0.1", 12345);
cloud_point_rpc::TCPConnector connector("127.0.0.1", 12345);
auto res = connector.Send(msg.data());
}
TEST_F(TcpTest, HugeBuffer) {
static constexpr uint64_t w = 1280, h = 720, c = 3;
static constexpr uint64_t w = 1920, h = 1080, c = 3;
std::string data(w * h * c, 77);
ExpectedResponse(data);
score::TCPConnector connector("127.0.0.1", 12345);
cloud_point_rpc::TCPConnector connector("127.0.0.1", 12345);
auto res = connector.Send(data);
}

View File

@ -1,187 +0,0 @@
#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);
}