commit cbe56d919387eb5111b43d39f130b190233d515d Author: Artur Mukhamadiev Date: Mon Jan 26 00:14:19 2026 +0300 [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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80024b1 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/API.md b/API.md new file mode 100644 index 0000000..1cee888 --- /dev/null +++ b/API.md @@ -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": "", + "params": {}, + "id": +} +``` +*Note: `params` is currently ignored by the implemented methods but is part of the standard.* + +### Response (Success) +```json +{ + "jsonrpc": "2.0", + "result": , + "id": +} +``` + +### Response (Error) +```json +{ + "jsonrpc": "2.0", + "error": { + "code": , + "message": "" + }, + "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` (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` (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>` (List of [x, y, z] points)* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ca7609b --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..23a1079 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..7d95af4 --- /dev/null +++ b/config.yaml @@ -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] diff --git a/include/cloud_point_rpc/config.hpp b/include/cloud_point_rpc/config.hpp new file mode 100644 index 0000000..21f2ada --- /dev/null +++ b/include/cloud_point_rpc/config.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace cloud_point_rpc { + +struct ServerConfig { + std::string ip; + int port; +}; + +struct TestData { + std::vector intrinsic_params; + std::vector extrinsic_params; + std::vector> 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("127.0.0.1"); + c.server.port = config["server"]["port"].as(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>(); + c.test_data.extrinsic_params = config["test_data"]["extrinsic_params"].as>(); + + // 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>()); + } + } 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 diff --git a/include/cloud_point_rpc/rpc_client.hpp b/include/cloud_point_rpc/rpc_client.hpp new file mode 100644 index 0000000..9d5be3a --- /dev/null +++ b/include/cloud_point_rpc/rpc_client.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +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 get_intrinsic_params() { + return call_method>("get-intrinsic-params"); + } + + [[nodiscard]] std::vector get_extrinsic_params() { + return call_method>("get-extrinsic-params"); + } + + [[nodiscard]] std::vector> get_cloud_point() { + return call_method>>("get-cloud-point"); + } + + ~RpcClient() { + // Socket closes automatically + } + + private: + template + 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()); + } + + return response["result"].get(); + } + + asio::io_context io_context_; + asio::ip::tcp::socket socket_; + int id_counter_ = 0; +}; + +} // namespace cloud_point_rpc diff --git a/include/cloud_point_rpc/rpc_server.hpp b/include/cloud_point_rpc/rpc_server.hpp new file mode 100644 index 0000000..88b9d5f --- /dev/null +++ b/include/cloud_point_rpc/rpc_server.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +namespace cloud_point_rpc { + +class RpcServer { + public: + using Handler = std::function; + + void register_method(const std::string& name, Handler handler); + [[nodiscard]] std::string process(const std::string& request_str); + + private: + std::map handlers_; +}; + +} // namespace cloud_point_rpc diff --git a/include/cloud_point_rpc/service.hpp b/include/cloud_point_rpc/service.hpp new file mode 100644 index 0000000..cd213e7 --- /dev/null +++ b/include/cloud_point_rpc/service.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include "cloud_point_rpc/config.hpp" + +namespace cloud_point_rpc { + +class Service { + public: + explicit Service(const TestData& data = {}); + + [[nodiscard]] std::vector get_intrinsic_params() const; + [[nodiscard]] std::vector get_extrinsic_params() const; + [[nodiscard]] std::vector> get_cloud_point() const; + + private: + TestData data_; +}; + +} // namespace cloud_point_rpc diff --git a/include/cloud_point_rpc/tcp_server.hpp b/include/cloud_point_rpc/tcp_server.hpp new file mode 100644 index 0000000..9b66b61 --- /dev/null +++ b/include/cloud_point_rpc/tcp_server.hpp @@ -0,0 +1,129 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace cloud_point_rpc { + +class TcpServer { + public: + using RequestProcessor = std::function; + + 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(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 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 running_; + std::jthread accept_thread_; +}; + +} // namespace cloud_point_rpc diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..0c3802a --- /dev/null +++ b/meson.build @@ -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') diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..bab2e46 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,33 @@ +#include +#include +#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; +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..2a3cbfe --- /dev/null +++ b/src/meson.build @@ -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) diff --git a/src/rpc_server.cpp b/src/rpc_server.cpp new file mode 100644 index 0000000..4282d3f --- /dev/null +++ b/src/rpc_server.cpp @@ -0,0 +1,61 @@ +#include "cloud_point_rpc/rpc_server.hpp" +#include + +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 diff --git a/src/server_main.cpp b/src/server_main.cpp new file mode 100644 index 0000000..0cf1bbf --- /dev/null +++ b/src/server_main.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#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; +} diff --git a/src/service.cpp b/src/service.cpp new file mode 100644 index 0000000..c976ddf --- /dev/null +++ b/src/service.cpp @@ -0,0 +1,38 @@ +#include "cloud_point_rpc/service.hpp" + +namespace cloud_point_rpc { + +Service::Service(const TestData& data) : data_(data) {} + +std::vector 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 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> 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 diff --git a/subprojects/asio.wrap b/subprojects/asio.wrap new file mode 100644 index 0000000..23f913a --- /dev/null +++ b/subprojects/asio.wrap @@ -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 diff --git a/subprojects/glog.wrap b/subprojects/glog.wrap new file mode 100644 index 0000000..8881630 --- /dev/null +++ b/subprojects/glog.wrap @@ -0,0 +1,7 @@ +[wrap-git] +url = https://github.com/google/glog.git +revision = v0.6.0 +depth = 1 + +[provide] +glog = glog_dep diff --git a/subprojects/gtest.wrap b/subprojects/gtest.wrap new file mode 100644 index 0000000..9902a4f --- /dev/null +++ b/subprojects/gtest.wrap @@ -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 diff --git a/subprojects/nlohmann_json.wrap b/subprojects/nlohmann_json.wrap new file mode 100644 index 0000000..e3b6f6f --- /dev/null +++ b/subprojects/nlohmann_json.wrap @@ -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 diff --git a/subprojects/yaml-cpp.wrap b/subprojects/yaml-cpp.wrap new file mode 100644 index 0000000..d80dc7a --- /dev/null +++ b/subprojects/yaml-cpp.wrap @@ -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 diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..8cc36bd --- /dev/null +++ b/tests/meson.build @@ -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) diff --git a/tests/test_integration.cpp b/tests/test_integration.cpp new file mode 100644 index 0000000..b21c960 --- /dev/null +++ b/tests/test_integration.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include + +#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 + +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(config_.test_data); + rpc_server_ = std::make_unique(); + + rpc_server_->register_method("get-intrinsic-params", [&](const nlohmann::json&) { + return service_->get_intrinsic_params(); + }); + + // Start Server Thread + tcp_server_ = std::make_unique(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_; + std::unique_ptr rpc_server_; + std::unique_ptr 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); +} diff --git a/tests/test_rpc.cpp b/tests/test_rpc.cpp new file mode 100644 index 0000000..b687fe3 --- /dev/null +++ b/tests/test_rpc.cpp @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include "cloud_point_rpc/rpc_server.hpp" +#include "cloud_point_rpc/service.hpp" +#include + +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>(); + 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); +}