[base] hail vibe-coding

Minimal API functionality with unit tests,
could be used for integration tests with Unity server.
For internal testing used RPC_server implementation
This commit is contained in:
Artur Mukhamadiev 2026-01-26 00:14:19 +03:00
commit cbe56d9193
24 changed files with 1028 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
build/
.cache/
subprojects/asio-1.36.0/
subprojects/glog/
subprojects/googletest-*
subprojects/nlohmann_json/
subprojects/packagecache/
subprojects/yaml-cpp-0.8.0

125
API.md Normal file
View File

@ -0,0 +1,125 @@
# JSON-RPC API Documentation
The Cloud Point RPC server implements the **JSON-RPC 2.0** protocol over TCP.
## General Format
All requests and responses are JSON objects.
### Request
```json
{
"jsonrpc": "2.0",
"method": "<method_name>",
"params": {},
"id": <integer|string>
}
```
*Note: `params` is currently ignored by the implemented methods but is part of the standard.*
### Response (Success)
```json
{
"jsonrpc": "2.0",
"result": <method_specific_result>,
"id": <matching_request_id>
}
```
### Response (Error)
```json
{
"jsonrpc": "2.0",
"error": {
"code": <integer>,
"message": "<string>"
},
"id": <matching_request_id>
}
```
---
## Methods
### `get-intrinsic-params`
Retrieves the intrinsic camera parameters as a flat 3x3 matrix (row-major).
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "get-intrinsic-params",
"id": 1
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"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)*
### `get-extrinsic-params`
Retrieves the extrinsic camera parameters as a flat 4x4 matrix (row-major).
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "get-extrinsic-params",
"id": 2
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"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)*
### `get-cloud-point`
Retrieves the current field of view point cloud.
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "get-cloud-point",
"id": 3
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"result": [
[0.1, 0.2, 0.3],
[1.1, 1.2, 1.3],
[5.5, 6.6, 7.7]
],
"id": 3
}
```
*Type: `vector<vector<double>>` (List of [x, y, z] points)*

40
Dockerfile Normal file
View File

@ -0,0 +1,40 @@
# Use Ubuntu 24.04 as base (matching development environment)
FROM ubuntu:24.04
# Avoid interactive prompts during package installation
ENV DEBIAN_FRONTEND=noninteractive
# Install dependencies
# - build-essential: Compiler (gcc/g++)
# - meson/ninja-build: Build system
# - git: For fetching subprojects
# - pkg-config, cmake: For dependency resolution
# - libssl-dev: Often needed for cmake fetches/networking
RUN apt-get update && apt-get install -y \
build-essential \
meson \
ninja-build \
git \
pkg-config \
cmake \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Set working directory
WORKDIR /app
# Copy project files
COPY . .
# Setup build directory and compile
# We allow git to fetch subprojects (glog, gtest, asio, etc.)
RUN meson setup build && \
meson compile -C build
# Expose the default port (can be changed in config.yaml)
EXPOSE 8080
# Run the server by default
# We assume the config.yaml is in the root /app or we copy it.
# The build output is in build/src/cloud_point_rpc_server
CMD ["./build/src/cloud_point_rpc_server", "config.yaml"]

56
README.md Normal file
View File

@ -0,0 +1,56 @@
# Cloud Point RPC
Communication JSON RPC protocol and implementation with Unity Scene.
## API Documentation
See [API.md](API.md) for detailed request/response formats.
## Development
The project uses **Meson** build system and **C++20**.
### Dependencies
- Meson, Ninja
- GCC/Clang (C++20 support)
- Git (for subprojects)
### Build & Run
```bash
meson setup build
meson compile -C build
./build/src/cloud_point_rpc_server config.yaml
```
### Testing
```bash
meson test -C build -v
```
## Docker
You can build and run the server using Docker.
### 1. Build Image
```bash
docker build -t cloud-point-rpc .
```
### 2. Run Container
The server runs on port **8080** by default (defined in `config.yaml` inside the image).
**Option A: Port Mapping**
```bash
docker run -p 8080:8080 cloud-point-rpc
```
**Option B: Host Network (For Simplicity/Performance)**
```bash
docker run --network host cloud-point-rpc
```
**Custom Configuration:**
You can mount your own `config.yaml` to override the default settings:
```bash
docker run -p 8080:8080 -v $(pwd)/my_config.yaml:/app/config.yaml cloud-point-rpc
```

11
config.yaml Normal file
View File

@ -0,0 +1,11 @@
server:
ip: "127.0.0.1"
port: 8085
test_data:
intrinsic_params: [1.1, 0.0, 0.0, 0.0, 1.1, 0.0, 0.0, 0.0, 1.0]
extrinsic_params: [1.0, 0.0, 0.0, 0.5, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]
cloud_point:
- [0.1, 0.2, 0.3]
- [1.1, 1.2, 1.3]
- [5.5, 6.6, 7.7]

View File

@ -0,0 +1,66 @@
#pragma once
#include <string>
#include <vector>
#include <yaml-cpp/yaml.h>
#include <glog/logging.h>
#include <stdexcept>
namespace cloud_point_rpc {
struct ServerConfig {
std::string ip;
int port;
};
struct TestData {
std::vector<double> intrinsic_params;
std::vector<double> extrinsic_params;
std::vector<std::vector<double>> cloud_point;
};
struct Config {
ServerConfig server;
TestData test_data;
};
class ConfigLoader {
public:
static Config load(const std::string& path) {
try {
YAML::Node config = YAML::LoadFile(path);
Config c;
// Server
if (config["server"]) {
c.server.ip = config["server"]["ip"].as<std::string>("127.0.0.1");
c.server.port = config["server"]["port"].as<int>(8080);
} else {
c.server.ip = "127.0.0.1";
c.server.port = 8080;
LOG(WARNING) << "No 'server' section, using defaults.";
}
// Test Data
if (config["test_data"]) {
c.test_data.intrinsic_params = config["test_data"]["intrinsic_params"].as<std::vector<double>>();
c.test_data.extrinsic_params = config["test_data"]["extrinsic_params"].as<std::vector<double>>();
// Parse cloud_point (list of lists)
auto cp_node = config["test_data"]["cloud_point"];
for (const auto& point_node : cp_node) {
c.test_data.cloud_point.push_back(point_node.as<std::vector<double>>());
}
} else {
LOG(WARNING) << "No 'test_data' section, using empty/defaults.";
}
return c;
} catch (const YAML::Exception& e) {
LOG(ERROR) << "Failed to load config: " << e.what();
throw std::runtime_error("Config load failed");
}
}
};
} // namespace cloud_point_rpc

View File

@ -0,0 +1,80 @@
#pragma once
#include <glog/logging.h>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#include <asio.hpp>
#include <stdexcept>
namespace cloud_point_rpc {
class RpcClient {
public:
RpcClient() : socket_(io_context_) {}
void connect(const std::string& ip, int port) {
try {
LOG(INFO) << "Client connecting to " << ip << ":" << port;
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip), port);
socket_.connect(endpoint);
LOG(INFO) << "Client connected";
} catch (const std::exception& e) {
throw std::runtime_error(std::string("Connection Failed: ") + e.what());
}
}
[[nodiscard]] std::vector<double> get_intrinsic_params() {
return call_method<std::vector<double>>("get-intrinsic-params");
}
[[nodiscard]] std::vector<double> get_extrinsic_params() {
return call_method<std::vector<double>>("get-extrinsic-params");
}
[[nodiscard]] std::vector<std::vector<double>> get_cloud_point() {
return call_method<std::vector<std::vector<double>>>("get-cloud-point");
}
~RpcClient() {
// Socket closes automatically
}
private:
template<typename T>
T call_method(const std::string& method) {
using json = nlohmann::json;
// Create Request
json request = {
{"jsonrpc", "2.0"},
{"method", method},
{"id", ++id_counter_}
};
std::string request_str = request.dump();
// Send
LOG(INFO) << "Client sending: " << request_str;
asio::write(socket_, asio::buffer(request_str));
// Read Response
LOG(INFO) << "Client reading response...";
char buffer[4096];
size_t len = socket_.read_some(asio::buffer(buffer, 4096));
LOG(INFO) << "Client read " << len << " bytes";
json response = json::parse(std::string(buffer, len));
if (response.contains("error")) {
throw std::runtime_error(response["error"]["message"].get<std::string>());
}
return response["result"].get<T>();
}
asio::io_context io_context_;
asio::ip::tcp::socket socket_;
int id_counter_ = 0;
};
} // namespace cloud_point_rpc

View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <functional>
#include <map>
#include <nlohmann/json.hpp>
namespace cloud_point_rpc {
class RpcServer {
public:
using Handler = std::function<nlohmann::json(const nlohmann::json&)>;
void register_method(const std::string& name, Handler handler);
[[nodiscard]] std::string process(const std::string& request_str);
private:
std::map<std::string, Handler> handlers_;
};
} // namespace cloud_point_rpc

View File

@ -0,0 +1,20 @@
#pragma once
#include <vector>
#include "cloud_point_rpc/config.hpp"
namespace cloud_point_rpc {
class Service {
public:
explicit Service(const TestData& data = {});
[[nodiscard]] std::vector<double> get_intrinsic_params() const;
[[nodiscard]] std::vector<double> get_extrinsic_params() const;
[[nodiscard]] std::vector<std::vector<double>> get_cloud_point() const;
private:
TestData data_;
};
} // namespace cloud_point_rpc

View File

@ -0,0 +1,129 @@
#pragma once
#include <string>
#include <functional>
#include <glog/logging.h>
#include <thread>
#include <atomic>
#include <asio.hpp>
namespace cloud_point_rpc {
class TcpServer {
public:
using RequestProcessor = std::function<std::string(const std::string&)>;
TcpServer(const std::string& ip, int port, RequestProcessor processor)
: ip_(ip), port_(port), processor_(std::move(processor)),
acceptor_(io_context_), running_(false) {}
~TcpServer() { stop(); }
void start() {
try {
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip_), port_);
acceptor_.open(endpoint.protocol());
acceptor_.set_option(asio::ip::tcp::acceptor::reuse_address(true));
acceptor_.bind(endpoint);
acceptor_.listen();
running_ = true;
LOG(INFO) << "Server listening on " << ip_ << ":" << port_;
accept_thread_ = std::jthread([this]() {
LOG(INFO) << "Accept thread started";
while (running_) {
try {
auto socket = std::make_shared<asio::ip::tcp::socket>(io_context_);
acceptor_.accept(*socket);
LOG(INFO) << "New connection from " << socket->remote_endpoint().address().to_string();
std::jthread([this, socket]() {
handle_client(socket);
}).detach();
} catch (const std::system_error& e) {
LOG(INFO) << "Accept exception: " << e.what();
if (running_) {
LOG(WARNING) << "Accept failed: " << e.what();
}
}
}
LOG(INFO) << "Accept thread exiting";
});
} catch (const std::exception& e) {
LOG(ERROR) << "Server start failed: " << e.what();
throw;
}
}
void stop() {
if (!running_) return;
LOG(INFO) << "Stopping server...";
running_ = false;
// Closing acceptor unblocks accept() call usually, but sometimes we need to prod it
asio::error_code ec;
acceptor_.close(ec);
// Ensure accept unblocks by connecting a dummy socket
try {
asio::ip::tcp::endpoint endpoint(asio::ip::make_address(ip_), port_);
asio::ip::tcp::socket dummy_sock(io_context_);
asio::error_code connect_ec;
dummy_sock.connect(endpoint, connect_ec);
} catch (...) {
// Ignore
}
LOG(INFO) << "Acceptor closed";
}
void join() {
if (accept_thread_.joinable()) {
LOG(INFO) << "Joining accept thread...";
accept_thread_.join();
LOG(INFO) << "Accept thread joined";
}
}
private:
void handle_client(std::shared_ptr<asio::ip::tcp::socket> socket) {
try {
asio::streambuf buffer;
// Read until newline or EOF/error
// Note: This matches the client implementation which should send a newline
// However, previous implementation read 4096 bytes raw.
// Let's emulate "read some" to match previous simple behavior, or use read_until if we enforce framing.
// Given this is a prototype, let's read once.
LOG(INFO) << "Server reading from client...";
char data[4096];
size_t length = socket->read_some(asio::buffer(data, 4096)); // Error will throw
LOG(INFO) << "Server read " << length << " bytes";
if (length > 0) {
std::string request(data, length);
LOG(INFO) << "Server processing request: " << request;
std::string response = processor_(request);
response += "\n";
LOG(INFO) << "Server sending response: " << response;
asio::write(*socket, asio::buffer(response));
LOG(INFO) << "Server sent response";
}
} catch (const std::exception& e) {
LOG(WARNING) << "Client handling error: " << e.what();
}
// Socket closes when shared_ptr dies
}
std::string ip_;
int port_;
RequestProcessor processor_;
asio::io_context io_context_;
asio::ip::tcp::acceptor acceptor_;
std::atomic<bool> running_;
std::jthread accept_thread_;
};
} // namespace cloud_point_rpc

28
meson.build Normal file
View File

@ -0,0 +1,28 @@
project('cloud_point_rpc', 'cpp',
version : '0.1',
default_options : ['warning_level=3', 'cpp_std=c++20'])
# Dependencies
json_dep = dependency('nlohmann_json', fallback : ['nlohmann_json', 'nlohmann_json_dep'])
thread_dep = dependency('threads')
asio_dep = dependency('asio', fallback : ['asio', 'asio_dep'])
# GLog via CMake fallback
cmake = import('cmake')
glog_opt = cmake.subproject_options()
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')
yaml_dep = dependency('yaml-cpp', fallback : ['yaml-cpp', 'yaml_cpp_dep'])
# GTest & GMock via explicit subproject
gtest_proj = subproject('gtest')
gtest_dep = gtest_proj.get_variable('gtest_dep')
gtest_main_dep = gtest_proj.get_variable('gtest_main_dep')
gmock_dep = gtest_proj.get_variable('gmock_dep')
inc = include_directories('include')
subdir('src')
subdir('tests')

33
src/main.cpp Normal file
View File

@ -0,0 +1,33 @@
#include <iostream>
#include <string>
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/service.hpp"
int main() {
cloud_point_rpc::Service service;
cloud_point_rpc::RpcServer server;
// Bind service methods to RPC handlers
server.register_method("get-intrinsic-params", [&](const nlohmann::json&) {
return service.get_intrinsic_params();
});
server.register_method("get-extrinsic-params", [&](const nlohmann::json&) {
return service.get_extrinsic_params();
});
server.register_method("get-cloud-point", [&](const nlohmann::json&) {
return service.get_cloud_point();
});
std::cout << "Cloud Point RPC Server initialized." << std::endl;
std::cout << "Enter JSON-RPC requests (Ctrl+D to exit):" << std::endl;
std::string line;
while (std::getline(std::cin, line)) {
if (line.empty()) continue;
std::cout << server.process(line) << std::endl;
}
return 0;
}

27
src/meson.build Normal file
View File

@ -0,0 +1,27 @@
cloud_point_rpc_sources = files(
'rpc_server.cpp',
'service.cpp'
)
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])
# Client/CLI tool (legacy stdin/stdout)
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,
install : true)

61
src/rpc_server.cpp Normal file
View File

@ -0,0 +1,61 @@
#include "cloud_point_rpc/rpc_server.hpp"
#include <iostream>
using json = nlohmann::json;
namespace cloud_point_rpc {
namespace {
json create_error(int code, const std::string& message, const json& id = nullptr) {
return {
{"jsonrpc", "2.0"},
{"error", {{"code", code}, {"message", message}}},
{"id", id}
};
}
json create_success(const json& result, const json& id) {
return {
{"jsonrpc", "2.0"},
{"result", result},
{"id", id}
};
}
} // namespace
void RpcServer::register_method(const std::string& name, Handler handler) {
handlers_[name] = std::move(handler);
}
std::string RpcServer::process(const std::string& request_str) {
json request;
try {
request = json::parse(request_str);
} catch (const json::parse_error&) {
return create_error(-32700, "Parse error").dump();
}
// Batch requests are not supported in this minimal version, assume single object
if (!request.contains("jsonrpc") || request["jsonrpc"] != "2.0" ||
!request.contains("method") || !request.contains("id")) {
return create_error(-32600, "Invalid Request", request.value("id", json(nullptr))).dump();
}
std::string method = request["method"];
json id = request["id"];
json params = request.value("params", json::object());
auto it = handlers_.find(method);
if (it == handlers_.end()) {
return create_error(-32601, "Method not found", id).dump();
}
try {
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 cloud_point_rpc

58
src/server_main.cpp Normal file
View File

@ -0,0 +1,58 @@
#include <iostream>
#include <string>
#include <glog/logging.h>
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/service.hpp"
#include "cloud_point_rpc/config.hpp"
#include "cloud_point_rpc/tcp_server.hpp"
using json = nlohmann::json;
int main(int argc, char* argv[]) {
google::InitGoogleLogging(argv[0]);
google::InstallFailureSignalHandler();
FLAGS_alsologtostderr = 1;
std::string config_path = "config.yaml";
if (argc > 1) {
config_path = argv[1];
}
LOG(INFO) << "Starting Cloud Point RPC Server (Test Mock)...";
try {
auto config = cloud_point_rpc::ConfigLoader::load(config_path);
LOG(INFO) << "Loaded config from " << config_path;
// Inject test data into service
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();
});
rpc_server.register_method("get-extrinsic-params", [&](const json&) {
return service.get_extrinsic_params();
});
rpc_server.register_method("get-cloud-point", [&](const json&) {
return service.get_cloud_point();
});
cloud_point_rpc::TcpServer server(config.server.ip, config.server.port,
[&](const std::string& request) {
return rpc_server.process(request);
}
);
server.start();
server.join();
} catch (const std::exception& e) {
LOG(ERROR) << "Fatal error: " << e.what();
return 1;
}
return 0;
}

38
src/service.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "cloud_point_rpc/service.hpp"
namespace cloud_point_rpc {
Service::Service(const TestData& data) : data_(data) {}
std::vector<double> Service::get_intrinsic_params() const {
if (data_.intrinsic_params.empty()) {
// Fallback if no config loaded
return {1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0};
}
return data_.intrinsic_params;
}
std::vector<double> Service::get_extrinsic_params() const {
if (data_.extrinsic_params.empty()) {
return {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};
}
return data_.extrinsic_params;
}
std::vector<std::vector<double>> Service::get_cloud_point() const {
if (data_.cloud_point.empty()) {
return {
{0.1, 0.2, 0.3},
{1.1, 1.2, 1.3},
{2.1, 2.2, 2.3}
};
}
return data_.cloud_point;
}
} // namespace cloud_point_rpc

13
subprojects/asio.wrap Normal file
View File

@ -0,0 +1,13 @@
[wrap-file]
directory = asio-1.36.0
source_url = https://sourceforge.net/projects/asio/files/asio/1.36.0%20%28Stable%29/asio-1.36.0.tar.gz/download
source_filename = asio-1.36.0.tar.gz
source_hash = 55d5c64e78b1bedd0004423e695c2cfc191fc71914eaaa7f042329ff99ee6155
patch_filename = asio_1.36.0-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/asio_1.36.0-2/get_patch
patch_hash = 0562360a9d6ef6fc5316f9a0b1d433b799259c70e0ce8f109e949b0b79797cd9
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/asio_1.36.0-2/asio-1.36.0.tar.gz
wrapdb_version = 1.36.0-2
[provide]
asio = asio_dep

7
subprojects/glog.wrap Normal file
View File

@ -0,0 +1,7 @@
[wrap-git]
url = https://github.com/google/glog.git
revision = v0.6.0
depth = 1
[provide]
glog = glog_dep

16
subprojects/gtest.wrap Normal file
View File

@ -0,0 +1,16 @@
[wrap-file]
directory = googletest-1.17.0
source_url = https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz
source_filename = googletest-1.17.0.tar.gz
source_hash = 65fab701d9829d38cb77c14acdc431d2108bfdbf8979e40eb8ae567edf10b27c
patch_filename = gtest_1.17.0-4_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/gtest_1.17.0-4/get_patch
patch_hash = 3abf7662d09db706453a5b064a1e914678c74b9d9b0b19382747ca561d0d8750
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/gtest_1.17.0-4/googletest-1.17.0.tar.gz
wrapdb_version = 1.17.0-4
[provide]
gtest = gtest_dep
gtest_main = gtest_main_dep
gmock = gmock_dep
gmock_main = gmock_main_dep

View File

@ -0,0 +1,7 @@
[wrap-git]
url = https://github.com/nlohmann/json.git
revision = v3.11.2
depth = 1
[provide]
nlohmann_json = nlohmann_json_dep

13
subprojects/yaml-cpp.wrap Normal file
View File

@ -0,0 +1,13 @@
[wrap-file]
directory = yaml-cpp-0.8.0
source_url = https://github.com/jbeder/yaml-cpp/archive/refs/tags/0.8.0.zip
source_filename = yaml-cpp-0.8.0.zip
source_hash = 334e80ab7b52e14c23f94e041c74bab0742f2281aad55f66be2f19f4b7747071
source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/yaml-cpp_0.8.0-2/yaml-cpp-0.8.0.zip
patch_filename = yaml-cpp_0.8.0-2_patch.zip
patch_url = https://wrapdb.mesonbuild.com/v2/yaml-cpp_0.8.0-2/get_patch
patch_hash = e7424f2804f2bb9e99f8ecea0c3c53e6de813f93043130243a27adfef3526573
wrapdb_version = 0.8.0-2
[provide]
dependency_names = yaml-cpp

10
tests/meson.build Normal file
View File

@ -0,0 +1,10 @@
test_sources = files(
'test_rpc.cpp',
'test_integration.cpp'
)
test_exe = executable('unit_tests',
test_sources,
dependencies : [cloud_point_rpc_dep, gtest_dep, gtest_main_dep, gmock_dep])
test('unit_tests', test_exe)

100
tests/test_integration.cpp Normal file
View File

@ -0,0 +1,100 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <thread>
#include <chrono>
#include "cloud_point_rpc/config.hpp"
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/service.hpp"
#include "cloud_point_rpc/tcp_server.hpp"
#include "cloud_point_rpc/rpc_client.hpp"
#include <fstream>
using namespace cloud_point_rpc;
class IntegrationTest : public ::testing::Test {
protected:
void SetUp() override {
// Create a temporary config file for testing
std::ofstream config_file("config.yaml");
config_file << "server:\n"
<< " ip: \"127.0.0.1\"\n"
<< " port: 9095\n"
<< "test_data:\n"
<< " intrinsic_params: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]\n"
<< " extrinsic_params: [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]\n"
<< " cloud_point:\n"
<< " - [0.1, 0.2, 0.3]\n";
config_file.close();
// Setup Mock Server
try {
config_ = ConfigLoader::load("config.yaml");
} catch (...) {
// If config fails, we can't proceed, but we should avoid crashing in TearDown
throw;
}
service_ = std::make_unique<Service>(config_.test_data);
rpc_server_ = std::make_unique<RpcServer>();
rpc_server_->register_method("get-intrinsic-params", [&](const nlohmann::json&) {
return service_->get_intrinsic_params();
});
// Start Server Thread
tcp_server_ = std::make_unique<TcpServer>(config_.server.ip, config_.server.port,
[&](const std::string& req) {
return rpc_server_->process(req);
}
);
server_thread_ = std::thread([&]() {
tcp_server_->start();
});
// Give server time to start
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
void TearDown() override {
if (tcp_server_) {
tcp_server_->stop();
}
if (server_thread_.joinable()) {
server_thread_.join();
}
// Clean up
std::remove("config.yaml");
}
Config config_;
std::unique_ptr<Service> service_;
std::unique_ptr<RpcServer> rpc_server_;
std::unique_ptr<TcpServer> tcp_server_;
std::thread server_thread_;
};
TEST_F(IntegrationTest, ClientCanConnectAndRetrieveValues) {
RpcClient client;
// Act: Connect
ASSERT_NO_THROW(client.connect(config_.server.ip, config_.server.port));
// Act: Call Method
auto params = client.get_intrinsic_params();
// Assert: Values match config
const auto& expected = config_.test_data.intrinsic_params;
ASSERT_EQ(params.size(), expected.size());
for (size_t i = 0; i < params.size(); ++i) {
EXPECT_DOUBLE_EQ(params[i], expected[i]);
}
}
TEST_F(IntegrationTest, ClientHandlesConnectionError) {
RpcClient client;
// Try connecting to a closed port
EXPECT_THROW(client.connect("127.0.0.1", 9999), std::runtime_error);
}

61
tests/test_rpc.cpp Normal file
View File

@ -0,0 +1,61 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <string>
#include <vector>
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/service.hpp"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace cloud_point_rpc;
class RpcServerTest : public ::testing::Test {
protected:
RpcServer server;
Service service;
void SetUp() override {
server.register_method("get-intrinsic-params", [&](const json&) {
return service.get_intrinsic_params();
});
}
};
TEST_F(RpcServerTest, GetIntrinsicParamsReturnsMatrix) {
std::string request = R"({"jsonrpc": "2.0", "method": "get-intrinsic-params", "id": 1})";
std::string response_str = server.process(request);
json response = json::parse(response_str);
ASSERT_EQ(response["jsonrpc"], "2.0");
ASSERT_EQ(response["id"], 1);
ASSERT_TRUE(response.contains("result"));
auto result = response["result"].get<std::vector<double>>();
EXPECT_EQ(result.size(), 9);
// Verify Identity Matrix
EXPECT_EQ(result[0], 1.0);
EXPECT_EQ(result[4], 1.0);
EXPECT_EQ(result[8], 1.0);
}
TEST_F(RpcServerTest, MethodNotFoundReturnsError) {
std::string request = R"({"jsonrpc": "2.0", "method": "unknown-method", "id": 99})";
std::string response_str = server.process(request);
json response = json::parse(response_str);
ASSERT_TRUE(response.contains("error"));
EXPECT_EQ(response["error"]["code"], -32601);
EXPECT_EQ(response["error"]["message"], "Method not found");
}
TEST_F(RpcServerTest, InvalidJsonReturnsParseError) {
std::string request = R"({"jsonrpc": "2.0", "method": "broken-json...)";
std::string response_str = server.process(request);
json response = json::parse(response_str);
ASSERT_TRUE(response.contains("error"));
EXPECT_EQ(response["error"]["code"], -32700);
}