Compare commits

..

6 Commits

Author SHA1 Message Date
Artur Mukhamadiev
d2146279c4 [tests] remote retrieving of available methods
:Release Notes:
check that get-available-methods correctly called and parsed

:Detailed Notes:
-

:Testing Performed:
weak coverage, basic tests

:QA Notes:
-

:Issues Addressed:
TG-4
2026-03-19 23:08:16 +03:00
Artur Mukhamadiev
4343dd13e7 [rpc] new methods in api
:Release Notes:
added new implicit rpc method:
"get-available-methods"

get_count
get_method_name_by_id
get_method_names

:Detailed Notes:
-

:Testing Performed:
weak coverage

:QA Notes:
-

:Issues Addressed:
TG-4 #done
2026-03-19 22:59:32 +03:00
e3b5be0f66 [docs] consistency across impl and documentation
fully generated by gemini+opencode
2026-03-12 23:23:36 +03:00
2b12a0be9b [opencv] rectification boilerplate
TG-8
2026-03-12 23:14:27 +03:00
f5f4bd4115 [image] connection block between opencv and rpc
Details:
  imageRPC for now assumed to have 4 fields:
  width,height of type int (4 signed bytes)
  type (enum BGR,RGBA,DEPTH)
  and raw vector of data

tests written by opencode + gemini flash

TG-7
2026-03-12 22:13:51 +03:00
9cb3f009ea [deps] added opencv and stdexec
opencv would be used to compile actual processing part
stdexec would be used for RPC module :)

TG-8 #in-progress
2026-03-12 20:47:25 +03:00
48 changed files with 599 additions and 1429 deletions

View File

@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} runs verification of the project
on:
push:
branches:
- "master"
- 'master'
jobs:
Is-Buildable:
@ -22,12 +22,12 @@ jobs:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: "true"
submodules: 'true'
- name: Build project
run: |
cd ${{ gitea.workspace }}
meson setup build
meson compile -C build -j2
meson compile -C build
- name: Unit Test Results
run: |
meson test -C build

3
.gitignore vendored
View File

@ -7,5 +7,6 @@ subprojects/nlohmann_json/
subprojects/packagecache/
subprojects/yaml-cpp-0.8.0
subprojects/base64-0.5.2/
subprojects/stdexec/
subprojects/.*
.venv/
.worktrees/

View File

@ -66,12 +66,12 @@ Adhere strictly to **Modern C++20** standards.
### Naming Conventions
- **Files:** `snake_case.cpp`, `snake_case.hpp`.
- **Classes/Structs:** `PascalCase`.
- **Functions/Methods:** `snake_case` (or `camelCase` if adhering strictly to a specific external library style, but default to snake_case).
- **Functions/Methods:** `snake_case`.
- **Variables:** `snake_case`.
- **Private Members:** `snake_case_` (trailing underscore).
- **Constants:** `kPascalCase` or `ALL_CAPS` for macros (avoid macros).
- **Namespaces:** `snake_case`.
- **Interfaces:** `IPascalCase` (optional, but consistent if used).
- **Constants:** `kPascalCase` or `ALL_CAPS` for macros.
- **Namespaces:** `score` (primary project namespace).
- **Interfaces:** `IPascalCase`.
### Project Structure
- `include/cloud_point_rpc/`: Public header files.
@ -92,7 +92,7 @@ Adhere strictly to **Modern C++20** standards.
### Example Class
```cpp
namespace cloud_point_rpc {
namespace score {
/// @brief Manages camera parameters.
class CameraController {
@ -114,7 +114,7 @@ class CameraController {
std::vector<double> cached_intrinsics_;
};
} // namespace cloud_point_rpc
} // namespace score
```
### Implementation Details

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)*

View File

@ -2,10 +2,19 @@
Communication JSON RPC protocol and implementation with Unity Scene.
## TODO
## Project Structure
- `include/`: Header files for the RPC server, TCP server, and C-API.
- `src/`: Implementation of the RPC logic, networking, and C-API.
- `src/cloud_point/`: OpenCV-based image processing and rectification logic.
- `docs/`: Documentation diagrams and models.
- `subprojects/`: Dependencies managed by Meson.
## Status
- [x] Server implementation with C-API for Unity
- [ ] Client correct implementation with OpenCV
- [x] Basic OpenCV image processing (Rectification, Image wrapper)
- [ ] Full OpenCV client implementation
## API Documentation
@ -17,20 +26,29 @@ The project uses **Meson** build system and **C++20**.
### Dependencies
- Meson, Ninja
- Meson (>= 1.1.0), Ninja
- GCC/Clang (C++20 support)
- Git (for subprojects)
- OpenCV (for cloud point compute)
The following dependencies are managed via Meson subprojects:
- [ASIO](https://think-async.com/Asio/) (Networking)
- [nlohmann/json](https://github.com/nlohmann/json) (JSON serialization)
- [yaml-cpp](https://github.com/jbeder/yaml-cpp) (Configuration loading)
- [glog](https://github.com/google/glog) (Logging)
- [jsonrpccxx](https://github.com/uS-S/jsonrpccxx) (JSON-RPC 2.0 implementation)
- [stdexec](https://github.com/NVIDIA/stdexec) (P2300 Senders/Receivers)
### Build & Run
```bash
git submodule init
git submodule update
meson setup build
meson compile -C build
./build/src/cloud_point_rpc_server config.yaml
```
*Note: You need a `config.yaml` file. See `config.yaml.example` for the required format.*
#### Build on windows
It's assumed that you have `GCC` and `make`/`ninja` installed on your system (and available in `PATH`)
@ -41,9 +59,7 @@ git submodule init
git submodule update
# Next python:
python3 -m venv .\venv
.\venv\Scripts\Activate.ps1
# or
.\venv\bin\Activate.ps1
.\.venv\Scripts\Activate.ps1
pip install meson cmake
meson setup -Ddefault_library=static build
meson compile -C build

11
config.yaml.example Normal file
View File

@ -0,0 +1,11 @@
server:
ip: "127.0.0.1"
port: 8080
test_data:
intrinsic_params: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]
extrinsic_params: [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]
cloud_point:
- [0.1, 0.2, 0.3]
- [1.1, 1.2, 1.3]
- [5.5, 6.6, 7.7]

View File

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

View File

@ -0,0 +1,21 @@
//
// Created by vptyp on 11.03.2026.
//
#pragma once
#include <opencv4/opencv2/opencv.hpp>
namespace score {
class Image {
public:
Image();
explicit Image(const cv::Mat &image);
~Image();
/// @note data_ could be changed through this
[[nodiscard]] cv::Mat get();
protected:
cv::Mat data_;
};
} // namespace score

View File

@ -0,0 +1,40 @@
//
// Created by vptyp on 12.03.2026.
//
#pragma once
#include <cloud_point/image.h>
#include <cloud_point_rpc/imageRpc.h>
namespace score {
class ImageFactory {
public:
/**
* @brief tries to decode available RPC image type to opencv compliant
* @return opencv compliant image type
* @throw runtime_error if type is unknown
*/
static int pixelType(const ImageRPC::Type &type) {
switch (type) {
case ImageRPC::Type::BGR:
return CV_8UC3;
case ImageRPC::Type::RGBA:
return CV_8UC4;
case ImageRPC::Type::DEPTH:
return CV_64FC1;
default:
throw std::runtime_error("Unknown image type");
}
}
/**
* @brief tries to create Image object from ImageRPC
* @throw runtime_error if type is unknown
*/
static Image create(const ImageRPC &image) {
cv::Mat imageMat(image.width, image.height, pixelType(image.type),
const_cast<unsigned char *>(image.data.data()));
return Image{imageMat};
}
};
} // namespace score

View File

@ -0,0 +1,22 @@
#pragma once
#include "opencv2/core/hal/interface.h"
#include "opencv2/core/mat.hpp"
#include <stdexcept>
namespace score {
class CameraMatrixFactory {
public:
/**
* @param rpc < vector of size Width*Height
* @throw runtime_error if size is not Width*Height
*/
template <size_t Width, size_t Height>
static cv::Mat create(std::vector<double> rpc) {
if (rpc.size() != Width * Height)
throw std::runtime_error("Vector size is not Width*Height");
return {Width, Height, CV_64F, rpc.data()};
}
};
} // namespace score

View File

@ -0,0 +1,16 @@
#pragma once
#include "cloud_point/image.h"
#include <opencv2/calib3d.hpp>
namespace score {
class Rectify {
public:
Rectify();
~Rectify();
void perform(Image &image, cv::Mat cameraMatrix,
cv::Mat distCoeffs = cv::Mat::zeros(1, 5, CV_64F));
};
} // namespace score

View File

@ -1,8 +1,8 @@
#pragma once
#include "export.h"
#include <iostream>
#include <string>
#include "export.h"
namespace score {
/**
@ -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 port);
int CRPC_EXPORT run_cli(std::istream &input, std::ostream &output,
const std::string &ip, int port);
} // namespace cloud_point_rpc
} // namespace score

View File

@ -69,4 +69,4 @@ class ConfigLoader {
}
};
} // namespace cloud_point_rpc
} // namespace score

View File

@ -0,0 +1,21 @@
//
// Created by vptyp on 12.03.2026.
//
#pragma once
#include <vector>
namespace score {
struct ImageRPC {
int width{0};
int height{0};
enum class Type {
UNKNOWN,
BGR,
RGBA,
DEPTH,
} type{Type::UNKNOWN};
std::vector<unsigned char> data;
};
} // namespace score

View File

@ -36,4 +36,4 @@ class RpcClient : public jsonrpccxx::JsonRpcClient {
int id{0};
};
} // namespace cloud_point_rpc
} // namespace score

View File

@ -6,17 +6,16 @@
#include <string>
#include <vector>
#include "export.h"
namespace score {
class CRPC_EXPORT IRPCCoder {
class 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 {
class Base64RPCCoder final : public IRPCCoder {
public:
Base64RPCCoder();
~Base64RPCCoder() override;
@ -25,4 +24,4 @@ public:
std::string encode(const std::vector<char>& data) override;
};
}
}

View File

@ -6,7 +6,6 @@
#include <map>
#include <nlohmann/json.hpp>
#include <string>
#include <variant>
extern "C" {
struct rpc_string {
@ -21,16 +20,25 @@ namespace score {
class CRPC_EXPORT RpcServer {
public:
using Handler = std::function<std::variant<nlohmann::json, std::string>(
const nlohmann::json &)>;
using Handler = std::function<nlohmann::json(const nlohmann::json &)>;
using callback_t = rpc_string *(*)(rpc_string *);
public:
/// @note +1 method implicitly added: get-available-methods
RpcServer();
void register_method(const std::string &name, Handler handler);
void register_method(const std::string &name, callback_t handler);
uint64_t get_count() noexcept;
std::span<std::string_view> get_method_names() noexcept;
std::string_view get_method_name_by_id(uint64_t id) noexcept;
///@param request_str json rpc 2.0 formatted string
[[nodiscard]] std::string process(const std::string &request_str);
private:
std::vector<std::string_view> handler_names_;
std::map<std::string, Handler> handlers_;
};

View File

@ -34,4 +34,4 @@ template <NumericType T> T deserialize(const std::vector<uint8_t> &buffer) {
return *reinterpret_cast<const T *>(buffer.data());
}
} // namespace cloud_point_rpc
} // namespace score

View File

@ -1,8 +1,8 @@
#pragma once
#include "cloud_point_rpc/config.hpp"
#include <vector>
#include "export.h"
#include <vector>
namespace score {
class CRPC_EXPORT Service {
@ -17,4 +17,4 @@ class CRPC_EXPORT Service {
TestData data_;
};
} // namespace cloud_point_rpc
} // namespace score

View File

@ -1,11 +1,11 @@
#pragma once
#include "cloud_point_rpc/serialize.hpp"
#include "export.h"
#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 {
/**
* TCPConnector main purpose is to implement jsonrpccxx::IClientConnector Send
@ -43,4 +43,4 @@ class CRPC_EXPORT TCPConnector : public jsonrpccxx::IClientConnector {
asio::ip::tcp::socket socket_;
};
} // namespace cloud_point_rpc
} // namespace score

View File

@ -40,4 +40,4 @@ static inline std::string tcp_read(asio::ip::tcp::socket &socket,
return result;
}
} // namespace cloud_point_rpc
} // namespace score

View File

@ -22,8 +22,7 @@ class CRPC_EXPORT TcpServer {
~TcpServer() {
stop();
std::lock_guard lock(cliThrMtx_);
for (auto &thread : client_threads_ | std::views::keys) {
for (auto &thread : client_threads | std::views::keys) {
thread.join();
}
}
@ -44,9 +43,9 @@ 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) {
std::ranges::remove_if(
client_threads.begin(), client_threads.end(),
[](auto &client_info) {
bool result = false;
if (client_info.second.wait_for(0ms) ==
std::future_status::ready) {
@ -55,27 +54,21 @@ class CRPC_EXPORT TcpServer {
}
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()));
}
client_threads.push_back(
std::make_pair(std::jthread([this, socket, done]() {
handle_client(socket);
done->set_value(true);
}),
done->get_future()));
} catch (const std::system_error &e) {
LOG(INFO) << "Accept exception: " << e.what();
if (running_) {
@ -96,9 +89,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 +107,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";
}
@ -156,9 +145,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::list<std::pair<std::jthread, std::future<bool>>> client_threads;
std::jthread accept_thread_;
};

View File

@ -27,9 +27,10 @@ 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 rpc_string* crpc_get_method_name_by_id(uint64_t id);
CRPC_EXPORT uint64_t crpc_get_methods_count();
CRPC_EXPORT void crpc_add_method(callback_t cb, rpc_string* name);
#ifdef __cplusplus

View File

@ -3,6 +3,7 @@ project('cloud_point_rpc', 'cpp',
default_options : ['warning_level=3', 'cpp_std=c++20'])
# Dependencies
stdexec_dep = dependency('stdexec', fallback: ['stdexec', 'stdexec_dep'])
json_dep = dependency('nlohmann_json', fallback : ['nlohmann_json', 'nlohmann_json_dep'])
thread_dep = dependency('threads')
asio_dep = dependency('asio', fallback : ['asio', 'asio_dep'])
@ -11,18 +12,16 @@ base64_dep = dependency('base64', fallback: ['aklomp-base64', 'base64'])
cmake = import('cmake')
glog_opt = cmake.subproject_options()
glog_opt.add_cmake_defines({
'WITH_GFLAGS': 'OFF',
'WITH_GFLAGS': 'OFF',
'WITH_GTEST': 'OFF',
'CMAKE_POLICY_VERSION_MINIMUM': '3.5'
})
libtype = get_option('default_library')
if libtype == 'static'
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_proj = cmake.subproject('glog', options: glog_opt)
@ -54,4 +53,4 @@ if host_machine.system() == 'windows' and libtype == 'shared'
meson.add_devenv(devenv)
endif
endif

View File

@ -86,4 +86,4 @@ int run_cli(std::istream &input, std::ostream &output, const std::string &ip,
return 0;
}
} // namespace cloud_point_rpc
} // namespace score

18
src/cloud_point/image.cpp Normal file
View File

@ -0,0 +1,18 @@
//
// Created by vptyp on 12.03.2026.
//
#include "cloud_point/image.h"
namespace score {
Image::Image() {
// no work
}
Image::Image(const cv::Mat &image) { this->data_ = image; }
Image::~Image() = default;
cv::Mat Image::get() { return this->data_; }
} // namespace score

View File

@ -0,0 +1,28 @@
opencv_dep = dependency('opencv4',
fallback: ['libopencv', 'libopencv4'],
required: false)
if not opencv_dep.found()
message('\'opencv\' was not found. Try install libopencv-dev or similar package on your system')
message('cloud_point_compute library removed from compilation')
subdir_done()
endif
cloud_point_sources = files(
'image.cpp',
'rectify.cpp'
)
cpc_deps = [ cloud_point_rpc_dep, opencv_dep ]
cloud_point_compute_lib = shared_library('cloud_point_compute',
sources: cloud_point_sources,
include_directories: inc,
dependencies: cpc_deps,
)
cloud_point_compute_dep = declare_dependency(
include_directories: inc,
link_with: cloud_point_compute_lib,
dependencies: cpc_deps
)

View File

@ -0,0 +1,23 @@
#include "opencv2/calib3d.hpp"
#include <cloud_point/rectify.h>
namespace score {
Rectify::Rectify() = default;
Rectify::~Rectify() = default;
/**
* @brief perform rectification operation on providen image
* @param cameraMatrix matrix of intrinsic params of size 3x3
* @param distCoeffs distortion matrix
* @param image reference to image object (changed in-place)
*/
void Rectify::perform(Image &image, cv::Mat cameraMatrix, cv::Mat distCoeffs) {
cv::Mat newmatrix = cv::getOptimalNewCameraMatrix(
cameraMatrix, distCoeffs, {image.get().size[0], image.get().size[1]},
1);
cv::Mat output;
cv::undistort(image.get(), output, cameraMatrix, distCoeffs, newmatrix);
output.copyTo(image.get());
}
} // namespace score

View File

@ -1,79 +1,60 @@
add_project_arguments('-DCRPC_SERVER_API_EXPORT', language: 'cpp')
add_project_arguments('-DCRPC_SERVER_API_EXPORT -pthread', language: 'cpp')
cloud_point_rpc_sources = files(
'rpc_coder.cpp',
'rpc_server.cpp',
'server_api.cpp',
'service.cpp',
'rpc_server.cpp',
'service.cpp',
'server_api.cpp',
'rpc_coder.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',
)
deps = [json_dep, thread_dep, glog_dep, yaml_dep, asio_dep, base64_dep]
libcloud_point_rpc = shared_library('cloud_point_rpc',
cloud_point_rpc_sources,
include_directories : inc,
dependencies : deps,
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],
)
include_directories : inc,
link_with : libcloud_point_rpc,
dependencies : deps)
# Test lib
libcloud_point_rpc_test = shared_library(
'test_cloud_point',
'test_api.cpp',
dependencies: cloud_point_rpc_dep,
install: true,
install_rpath: '$ORIGIN',
)
libcloud_point_rpc_test = shared_library('test_cloud_point',
'test_api.cpp',
dependencies: cloud_point_rpc_dep,
install : true)
cloud_point_rpc_test_dep = declare_dependency(
include_directories: inc,
link_with: libcloud_point_rpc_test,
dependencies: [cloud_point_rpc_dep],
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,
)
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_cli,
dependencies: [cloud_point_rpc_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_cli_dep,
install : true)
# Server executable (TCP)
executable(
'cloud_point_rpc_server',
'server_main.cpp',
dependencies: cloud_point_rpc_dep,
link_args: '-pthread',
install: true,
)
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,
)
subdir('cloud_point')

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,30 +1,27 @@
//
// Created by vptyp on 11.03.2026.
//
#include "cloud_point_rpc/rpc_coder.hpp"
#include "cloud_point_rpc/rpc_coder.h"
#include "libbase64.h"
#include <glog/logging.h>
namespace score {
Base64RPCCoder::Base64RPCCoder() = default;
Base64RPCCoder::Base64RPCCoder() = default;
Base64RPCCoder::~Base64RPCCoder() = default;
/**
* Tries to decode ASCII complained string to the
* Tries to decode ASCII complained string to the raw bytes
* @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");
std::vector<char> Base64RPCCoder::decode(const std::string& encoded) {
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);
base64_decode(encoded.data(), encoded.size(),
result.data(), &result_len, 0);
DLOG(INFO) << "result_len: " << result_len;
result.resize(result_len);
return result;
}
/**
@ -32,15 +29,14 @@ std::vector<char> Base64RPCCoder::decode(const std::string &encoded) {
* @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");
std::string Base64RPCCoder::encode(const std::vector<char>& data) {
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);
std::string result(data.size() / 3 * 4 + 1, 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,7 +1,6 @@
#include "cloud_point_rpc/rpc_server.hpp"
#include "server_api.h"
#include <cstdint>
#include <glog/logging.h>
#include <variant>
using json = nlohmann::json;
namespace score {
@ -14,59 +13,46 @@ 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>>;
RpcServer::RpcServer() {
register_method("get-available-methods", [&](const json&) {
return get_method_names();
});
}
void RpcServer::register_method(const std::string &name, Handler handler) {
handlers_[name] = std::move(handler);
handler_names_.push_back(handlers_.find(name)->first);
}
void RpcServer::register_method(const std::string &name, callback_t handler) {
handlers_[name] = [handler](const nlohmann::json &j)
-> std::variant<nlohmann::json, std::string> {
handlers_[name] = [handler](const nlohmann::json &j) -> nlohmann::json {
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;
tmp.s = j.dump();
rpc_string *res = handler(&tmp);
return {res->s};
};
handler_names_.push_back(handlers_.find(name)->first);
}
std::span<std::string_view> RpcServer::get_method_names() noexcept {
return this->handler_names_;
}
uint64_t RpcServer::get_count() noexcept {
return this->handler_names_.size();
}
std::string_view RpcServer::get_method_name_by_id(uint64_t id) noexcept {
if(id >= handler_names_.size()) {
LOG(ERROR) << __func__ << std::format(": called with id = {} which is bigger, than size={}", id, handler_names_.size());
return {};
}
return handler_names_.at(id);
}
std::string RpcServer::process(const std::string &request_str) {
@ -98,11 +84,8 @@ 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
}

View File

@ -9,56 +9,32 @@
#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();
}
uint64_t crpc_str_get_size(const rpc_string *that) { 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");
google::InitGoogleLogging("CloudPointRPC");
if (config_path == nullptr) {
LOG(ERROR) << "config_path was not provided";
return;
LOG(INFO) << "config_path was not provided";
}
try {
auto config = score::ConfigLoader::load(config_path);
@ -67,7 +43,6 @@ void crpc_init(const char *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();
@ -76,39 +51,24 @@ void crpc_init(const char *config_path) {
}
}
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() {
if (server)
server->join();
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);
}
rpc_string *crpc_get_method_name_by_id(uint64_t id) {
auto value = rpc_server.get_method_name_by_id(id);
return crpc_str_create(value.data(), value.size());
}
uint64_t crpc_get_methods_count() {
return rpc_server.get_count();
}
}

View File

@ -27,4 +27,4 @@ std::vector<std::vector<double>> Service::get_cloud_point() const {
return data_.cloud_point;
}
} // namespace cloud_point_rpc
} // namespace score

View File

@ -4,6 +4,7 @@
#include <condition_variable>
#include <glog/logging.h>
#include <list>
#include <mutex>
#include <queue>
#include <set>
@ -16,7 +17,7 @@ class TestThread {
{{
"jsonrpc": "2.0",
"method": "{}",
"params": {{}},
"params": {{}},
"id": 1
}}
)",
@ -47,8 +48,9 @@ class TestThread {
}
if (state.load() && calls_queue.empty())
cv.wait_for(lock, thr_sleep,
[&] { return stoken.stop_requested(); });
cv.wait_for(lock, thr_sleep, [&] {
return stoken.stop_requested();
});
lock.unlock();
}
@ -65,10 +67,6 @@ class TestThread {
}
}
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)) {
@ -80,10 +78,6 @@ class TestThread {
}
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;
@ -98,10 +92,6 @@ class TestThread {
}
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);
}
@ -148,10 +138,8 @@ class TestThread {
extern "C" {
void crpc_test_init() {
if (!google::IsGoogleLoggingInitialized()) {
if (!google::IsGoogleLoggingInitialized())
google::InitGoogleLogging("TestRPC");
google::LogToStderr();
}
try {
test.start();
} catch (const std::exception &e) {
@ -178,15 +166,9 @@ 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_schedule_call(rpc_string *name) { test.add_queue_call(name->s); }
void crpc_test_auto_call(uint32_t state) {
test.auto_call(static_cast<bool>(state));
}
}
}

7
subprojects/stdexec.wrap Normal file
View File

@ -0,0 +1,7 @@
[wrap-git]
url = https://github.com/NVIDIA/stdexec.git
revision = gtc-2026
depth = 1
[provide]
stdexec = stdexec_dep

View File

@ -1,20 +1,29 @@
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_base64.cpp'
)
test_deps = [cloud_point_rpc_dep, cloud_point_rpc_cli_dep,
cloud_point_rpc_test_dep, json_dep, gtest_dep,
gtest_main_dep, gmock_dep]
if opencv_dep.found()
message('found cloud_point_compute dependency')
test_sources += files(
'test_image.cpp'
)
test_deps += [cloud_point_compute_dep]
else
message('cpc_dep was not found')
endif
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 : test_deps)
test('unit_tests', test_exe)

View File

@ -7,7 +7,7 @@
#include <thread>
#include "cloud_point_rpc/config.hpp"
#include "cloud_point_rpc/rpc_coder.hpp"
#include "cloud_point_rpc/rpc_coder.h"
class Base64Test : public ::testing::Test {
@ -28,4 +28,4 @@ TEST_F(Base64Test, EncodeDecode) {
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

@ -35,7 +35,7 @@ TEST_F(TestCApi, Base) {
task.set_value(installed);
}
DLOG(INFO) << "Go out";
return crpc_str_create("res", sizeof("res") - 1);
return crpc_str_create("res", sizeof("res"));
},
&name);
@ -53,26 +53,24 @@ TEST_F(TestCApi, AddedMultiple) {
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;
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].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);
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>();
@ -113,27 +111,24 @@ TEST_F(TestCApi, ScheduleCall) {
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;
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].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);
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);
};
auto test_idx = [&]<size_t I>() {
using namespace std::chrono_literals;
@ -168,9 +163,7 @@ TEST_F(TestCApi, String) {
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);
auto creation = crpc_str_create("test 2222", sizeof("test 2222"));
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();
}

107
tests/test_image.cpp Normal file
View File

@ -0,0 +1,107 @@
//
// Created by vptyp on 12.03.2026.
//
#include "cloud_point/rectify.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <cloud_point/imageFactory.h>
class ImageTest : public ::testing::Test {
protected:
void SetUp() override {}
void TearDown() override {}
};
TEST_F(ImageTest, DefaultConstructor) {
score::Image image;
cv::Mat mat = image.get();
EXPECT_TRUE(mat.empty());
}
TEST_F(ImageTest, ConstructorWithMat) {
cv::Mat input = cv::Mat::zeros(5, 5, CV_8UC1);
score::Image image(input);
cv::Mat output = image.get();
EXPECT_EQ(output.rows, 5);
EXPECT_EQ(output.cols, 5);
EXPECT_EQ(output.type(), CV_8UC1);
}
TEST_F(ImageTest, PixelTypeMapping) {
EXPECT_EQ(score::ImageFactory::pixelType(score::ImageRPC::Type::BGR),
CV_8UC3);
EXPECT_EQ(score::ImageFactory::pixelType(score::ImageRPC::Type::RGBA),
CV_8UC4);
EXPECT_EQ(score::ImageFactory::pixelType(score::ImageRPC::Type::DEPTH),
CV_64FC1);
EXPECT_THROW(score::ImageFactory::pixelType(score::ImageRPC::Type::UNKNOWN),
std::runtime_error);
}
TEST_F(ImageTest, CreateBGR) {
score::ImageRPC rpc;
rpc.width = 10;
rpc.height = 20;
rpc.type = score::ImageRPC::Type::BGR;
rpc.data.resize(rpc.width * rpc.height * 3, 128);
score::Image image = score::ImageFactory::create(rpc);
cv::Mat mat = image.get();
EXPECT_EQ(mat.rows, 10);
EXPECT_EQ(mat.cols, 20);
EXPECT_EQ(mat.type(), CV_8UC3);
EXPECT_EQ(mat.at<cv::Vec3b>(0, 0)[0], 128);
}
TEST_F(ImageTest, CreateRGBA) {
score::ImageRPC rpc;
rpc.width = 15;
rpc.height = 25;
rpc.type = score::ImageRPC::Type::RGBA;
rpc.data.resize(rpc.width * rpc.height * 4, 255);
score::Image image = score::ImageFactory::create(rpc);
cv::Mat mat = image.get();
EXPECT_EQ(mat.rows, 15);
EXPECT_EQ(mat.cols, 25);
EXPECT_EQ(mat.type(), CV_8UC4);
EXPECT_EQ(mat.at<cv::Vec4b>(0, 0)[0], 255);
}
TEST_F(ImageTest, CreateDepth) {
score::ImageRPC rpc;
rpc.width = 5;
rpc.height = 10;
rpc.type = score::ImageRPC::Type::DEPTH;
rpc.data.resize(rpc.width * rpc.height * sizeof(double));
auto *dataPtr = reinterpret_cast<double *>(rpc.data.data());
for (int i = 0; i < 50; ++i)
dataPtr[i] = static_cast<double>(i);
score::Image image = score::ImageFactory::create(rpc);
cv::Mat mat = image.get();
EXPECT_EQ(mat.rows, 5);
EXPECT_EQ(mat.cols, 10);
EXPECT_EQ(mat.type(), CV_64FC1);
EXPECT_DOUBLE_EQ(mat.at<double>(0, 0), 0.0);
EXPECT_DOUBLE_EQ(mat.at<double>(4, 9), 49.0);
}
TEST_F(ImageTest, RectificationNoThrow) {
score::Rectify rectify;
score::ImageRPC rpc;
rpc.width = 10;
rpc.height = 20;
rpc.type = score::ImageRPC::Type::BGR;
rpc.data.resize(rpc.width * rpc.height * 3, 128);
score::Image image = score::ImageFactory::create(rpc);
double fx = 10.1, fy = 20.2, cx = 30.3, cy = 40.4;
cv::Mat cameraMatrix =
(cv::Mat_<double>(3, 3) << fx, 0, cx, 0, fy, cy, 0, 0, 1);
EXPECT_NO_THROW(rectify.perform(image, cameraMatrix));
}

View File

@ -95,3 +95,11 @@ TEST_F(IntegrationTest, ClientCanConnectAndRetrieveValues) {
TEST_F(IntegrationTest, ClientHandlesConnectionError) {
EXPECT_THROW(TCPConnector connector("127.0.0.1", 9999), std::runtime_error);
}
TEST_F(IntegrationTest, ClientRetrieveRemoteMethods) {
TCPConnector connector(config_.server.ip, config_.server.port);
RpcClient client(connector);
auto res = client.call<std::vector<std::string>>("get-available-methods");
EXPECT_EQ(res.size(), 2);
}

View File

@ -61,3 +61,16 @@ TEST_F(RpcServerTest, InvalidJsonReturnsParseError) {
ASSERT_TRUE(response.contains("error"));
EXPECT_EQ(response["error"]["code"], -32700);
}
TEST_F(RpcServerTest, GetMethod) {
EXPECT_EQ(server.get_count(), 2);
EXPECT_EQ(server.get_method_name_by_id(1), "get-intrinsic-params");
EXPECT_EQ(server.get_method_names()[1], "get-intrinsic-params");
server.register_method("get-test-2", [&](const json&){
return "test";
});
EXPECT_EQ(server.get_count(), 3);
EXPECT_EQ(server.get_method_name_by_id(2), "get-test-2");
EXPECT_EQ(server.get_method_names()[2], "get-test-2");
}

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

@ -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);
}