Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a40c38686 | |||
| 9955c1986c | |||
| 15fb311c66 | |||
| 5c5b886360 | |||
| 1eea074051 | |||
| 626cfa64f2 | |||
| 00da1c9f32 | |||
| c5ede14eaf | |||
| 981568f104 | |||
| 638c565702 | |||
| 81f8f709a2 | |||
| 6fea0e2450 | |||
| e881b6b699 | |||
| 2157b25a95 | |||
| f2dfee7a38 | |||
| 5afbf771ca |
@ -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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,6 +6,4 @@ subprojects/googletest-*
|
||||
subprojects/nlohmann_json/
|
||||
subprojects/packagecache/
|
||||
subprojects/yaml-cpp-0.8.0
|
||||
subprojects/base64-0.5.2/
|
||||
.venv/
|
||||
.worktrees/
|
||||
|
||||
37
API.md
37
API.md
@ -2,14 +2,6 @@
|
||||
|
||||
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)
|
||||
|
||||
## General Format
|
||||
|
||||
All requests and responses are JSON objects.
|
||||
@ -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)*
|
||||
|
||||
@ -4,7 +4,7 @@ Communication JSON RPC protocol and implementation with Unity Scene.
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Server implementation with C-API for Unity
|
||||
- [ ] Server implementation with C-API for Unity
|
||||
- [ ] Client correct implementation with OpenCV
|
||||
|
||||
## API Documentation
|
||||
@ -41,9 +41,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
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
server:
|
||||
ip: "127.0.0.1"
|
||||
port: 9095
|
||||
@ -3,7 +3,7 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "export.h"
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
/**
|
||||
* @brief Runs the CLI client.
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
#include <vector>
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
struct ServerConfig {
|
||||
std::string ip;
|
||||
int port{0};
|
||||
int port;
|
||||
};
|
||||
|
||||
struct TestData {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#include <jsonrpccxx/client.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <vector>
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
class RpcClient : public jsonrpccxx::JsonRpcClient {
|
||||
public:
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
//
|
||||
// Created by vptyp on 11.03.2026.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "export.h"
|
||||
namespace score {
|
||||
|
||||
class CRPC_EXPORT IRPCCoder {
|
||||
public:
|
||||
virtual ~IRPCCoder() = default;
|
||||
virtual std::vector<char> decode(const std::string& encoded) = 0;
|
||||
virtual std::string encode(const std::vector<char>& data) = 0;
|
||||
};
|
||||
|
||||
class CRPC_EXPORT Base64RPCCoder final : public IRPCCoder {
|
||||
public:
|
||||
Base64RPCCoder();
|
||||
~Base64RPCCoder() override;
|
||||
|
||||
std::vector<char> decode(const std::string& encoded) override;
|
||||
std::string encode(const std::vector<char>& data) override;
|
||||
};
|
||||
|
||||
}
|
||||
@ -1,12 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "export.h"
|
||||
#include <functional>
|
||||
#include <jsonrpccxx/server.hpp>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include "export.h"
|
||||
extern "C" {
|
||||
|
||||
struct rpc_string {
|
||||
@ -17,12 +16,11 @@ struct rpc_string {
|
||||
};
|
||||
}
|
||||
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
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*);
|
||||
|
||||
void register_method(const std::string &name, Handler handler);
|
||||
@ -34,4 +32,4 @@ class CRPC_EXPORT RpcServer {
|
||||
std::map<std::string, Handler> handlers_;
|
||||
};
|
||||
|
||||
} // namespace score
|
||||
} // namespace cloud_point_rpc
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
template <typename T>
|
||||
concept NumericType = requires(T param) {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include "cloud_point_rpc/config.hpp"
|
||||
#include <vector>
|
||||
#include "export.h"
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
class CRPC_EXPORT Service {
|
||||
public:
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
#include <glog/logging.h>
|
||||
#include <string>
|
||||
#include "export.h"
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
/**
|
||||
* TCPConnector main purpose is to implement jsonrpccxx::IClientConnector Send
|
||||
* method As an internal implementation, TCPConnector adds to the beginning of
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include <asio.hpp>
|
||||
#include <cloud_point_rpc/serialize.hpp>
|
||||
#include <glog/logging.h>
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
static inline std::string tcp_read(asio::ip::tcp::socket &socket,
|
||||
std::string_view prefix) {
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "export.h"
|
||||
#include <asio.hpp>
|
||||
#include <atomic>
|
||||
#include <cloud_point_rpc/tcp_read.hpp>
|
||||
#include <functional>
|
||||
#include <glog/logging.h>
|
||||
#include <list>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
namespace score {
|
||||
#include "export.h"
|
||||
#include <list>
|
||||
#include <ranges>
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
class CRPC_EXPORT TcpServer {
|
||||
public:
|
||||
@ -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,38 +43,27 @@ 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) {
|
||||
if (client_info.second.wait_for(0ms) == std::future_status::ready) {
|
||||
client_info.first.join();
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
try {
|
||||
auto socket = std::make_shared<asio::ip::tcp::socket>(
|
||||
io_context_);
|
||||
{
|
||||
std::lock_guard lock(acceptorMtx_);
|
||||
acceptor_.accept(*socket);
|
||||
}
|
||||
|
||||
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]() {
|
||||
client_threads.push_back(std::make_pair(std::jthread([this, socket, done]() {
|
||||
handle_client(socket);
|
||||
done->set_value(true);
|
||||
}),
|
||||
done->get_future()));
|
||||
}
|
||||
}),done->get_future()));
|
||||
} catch (const std::system_error &e) {
|
||||
LOG(INFO) << "Accept exception: " << e.what();
|
||||
if (running_) {
|
||||
@ -96,9 +84,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 +102,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,10 +140,8 @@ 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_;
|
||||
};
|
||||
|
||||
} // namespace score
|
||||
} // namespace cloud_point_rpc
|
||||
|
||||
@ -27,7 +27,6 @@ CRPC_EXPORT void crpc_str_destroy(rpc_string*);
|
||||
typedef rpc_string*(*callback_t)(rpc_string*);
|
||||
|
||||
CRPC_EXPORT void crpc_init(const char* config_path);
|
||||
CRPC_EXPORT void crpc_init_with_address(const char* ip, int port);
|
||||
CRPC_EXPORT void crpc_deinit();
|
||||
|
||||
CRPC_EXPORT void crpc_add_method(callback_t cb, rpc_string* name);
|
||||
|
||||
@ -6,14 +6,13 @@ project('cloud_point_rpc', 'cpp',
|
||||
json_dep = dependency('nlohmann_json', fallback : ['nlohmann_json', 'nlohmann_json_dep'])
|
||||
thread_dep = dependency('threads')
|
||||
asio_dep = dependency('asio', fallback : ['asio', 'asio_dep'])
|
||||
base64_dep = dependency('base64', fallback: ['aklomp-base64', 'base64'])
|
||||
|
||||
# GLog via CMake fallback
|
||||
cmake = import('cmake')
|
||||
glog_opt = cmake.subproject_options()
|
||||
glog_opt.add_cmake_defines({
|
||||
'WITH_GFLAGS': 'OFF',
|
||||
'WITH_GTEST': 'OFF',
|
||||
'CMAKE_POLICY_VERSION_MINIMUM': '3.5'
|
||||
})
|
||||
|
||||
libtype = get_option('default_library')
|
||||
@ -22,7 +21,6 @@ if libtype == 'static'
|
||||
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)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
#include <glog/logging.h>
|
||||
#include <string>
|
||||
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
void print_menu(std::ostream &output) {
|
||||
output << "\n=== Cloud Point RPC CLI ===" << std::endl;
|
||||
|
||||
@ -24,8 +24,8 @@ int main(int argc, char *argv[]) {
|
||||
f.close();
|
||||
|
||||
try {
|
||||
auto config = score::ConfigLoader::load(config_path);
|
||||
return score::run_cli(std::cin, std::cout, config.server.ip,
|
||||
auto config = cloud_point_rpc::ConfigLoader::load(config_path);
|
||||
return cloud_point_rpc::run_cli(std::cin, std::cout, config.server.ip,
|
||||
config.server.port);
|
||||
} catch (const std::exception &e) {
|
||||
std::cerr << "Failed to start CLI: " << e.what() << std::endl;
|
||||
|
||||
@ -1,79 +1,55 @@
|
||||
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',
|
||||
'server_api.cpp'
|
||||
)
|
||||
|
||||
libcloud_point_rpc = shared_library(
|
||||
'cloud_point_rpc',
|
||||
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',
|
||||
)
|
||||
dependencies : [json_dep, thread_dep, glog_dep, yaml_dep, asio_dep],
|
||||
install : true)
|
||||
|
||||
cloud_point_rpc_dep = declare_dependency(
|
||||
include_directories : inc,
|
||||
link_with : libcloud_point_rpc,
|
||||
dependencies: [json_dep, glog_dep, yaml_dep, asio_dep, base64_dep],
|
||||
)
|
||||
dependencies : [json_dep, glog_dep, yaml_dep, asio_dep])
|
||||
|
||||
# Test lib
|
||||
libcloud_point_rpc_test = shared_library(
|
||||
'test_cloud_point',
|
||||
libcloud_point_rpc_test = shared_library('test_cloud_point',
|
||||
'test_api.cpp',
|
||||
dependencies: cloud_point_rpc_dep,
|
||||
install: true,
|
||||
install_rpath: '$ORIGIN',
|
||||
)
|
||||
install : true)
|
||||
|
||||
cloud_point_rpc_test_dep = declare_dependency(
|
||||
include_directories: inc,
|
||||
link_with: libcloud_point_rpc_test,
|
||||
dependencies: [cloud_point_rpc_dep],
|
||||
dependencies: [cloud_point_rpc_dep]
|
||||
)
|
||||
|
||||
libcloud_point_rpc_cli = shared_library(
|
||||
'libcloud_point_rpc_cli',
|
||||
libcloud_point_rpc_cli = shared_library('libcloud_point_rpc_cli',
|
||||
'cli.cpp',
|
||||
include_directories : inc,
|
||||
dependencies: [cloud_point_rpc_dep],
|
||||
install: true,
|
||||
)
|
||||
dependencies : [json_dep, thread_dep, glog_dep, yaml_dep, asio_dep, 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],
|
||||
dependencies: [cloud_point_rpc_dep]
|
||||
)
|
||||
|
||||
# Client/CLI tool (legacy stdin/stdout)
|
||||
executable(
|
||||
'cloud_point_rpc_cli',
|
||||
[
|
||||
'main.cpp',
|
||||
],
|
||||
executable('cloud_point_rpc_cli',
|
||||
['main.cpp', ],
|
||||
dependencies : cloud_point_rpc_cli_dep,
|
||||
install: true,
|
||||
)
|
||||
install : true)
|
||||
|
||||
# Server executable (TCP)
|
||||
executable(
|
||||
'cloud_point_rpc_server',
|
||||
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,
|
||||
)
|
||||
install : true)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
//
|
||||
// Created by vptyp on 11.03.2026.
|
||||
//
|
||||
#include "cloud_point_rpc/rpc_coder.hpp"
|
||||
|
||||
#include "libbase64.h"
|
||||
#include <glog/logging.h>
|
||||
namespace score {
|
||||
|
||||
Base64RPCCoder::Base64RPCCoder() = default;
|
||||
Base64RPCCoder::~Base64RPCCoder() = default;
|
||||
|
||||
/**
|
||||
* Tries to decode ASCII complained string to the
|
||||
* @param encoded ASCII complained base64 encoded string
|
||||
* @return vector of raw bytes << allocated on encoded.size() / 4 * 3 + 1 size
|
||||
*/
|
||||
std::vector<char> Base64RPCCoder::decode(const std::string &encoded) {
|
||||
if (encoded.length() > (std::numeric_limits<size_t>::max() / 3) * 4)
|
||||
throw std::length_error("Base64 input too large");
|
||||
DLOG(INFO) << "Base64RPCCoder::decode";
|
||||
std::vector<char> result((encoded.length() >> 2) * 3 + 1);
|
||||
size_t result_len = 0;
|
||||
base64_decode(encoded.data(), encoded.size(), result.data(), &result_len,
|
||||
0);
|
||||
DLOG(INFO) << "result_len: " << result_len;
|
||||
result.resize(result_len);
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param data raw byte stream
|
||||
* @return encoded base64 string
|
||||
*/
|
||||
std::string Base64RPCCoder::encode(const std::vector<char> &data) {
|
||||
if (data.size() > (std::numeric_limits<size_t>::max() / 4) * 3)
|
||||
throw std::length_error("raw input is too large");
|
||||
DLOG(INFO) << "Base64RPCCoder::encode";
|
||||
size_t result_len = 0;
|
||||
std::string result((data.size() + 2) / 3 * 4, 0);
|
||||
base64_encode(data.data(), data.size(), result.data(), &result_len, 0);
|
||||
DLOG(INFO) << "result_len: " << result_len;
|
||||
result.resize(result_len);
|
||||
return result;
|
||||
}
|
||||
} // namespace score
|
||||
@ -1,10 +1,8 @@
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include "server_api.h"
|
||||
#include <glog/logging.h>
|
||||
#include <variant>
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
namespace {
|
||||
json create_error(int code, const std::string &message,
|
||||
@ -14,58 +12,21 @@ json create_error(int code, const std::string &message,
|
||||
{"id", id}};
|
||||
}
|
||||
|
||||
struct CreateSuccess {
|
||||
json obj;
|
||||
json id;
|
||||
|
||||
void operator()(const json &result) {
|
||||
obj = {{"jsonrpc", "2.0"}, {"result", result}, {"id", id}};
|
||||
json create_success(const json &result, const json &id) {
|
||||
return {{"jsonrpc", "2.0"}, {"result", result}, {"id", id}};
|
||||
}
|
||||
void operator()(const std::string &result) {
|
||||
obj = {{"jsonrpc", "2.0"}, {"result", result}, {"id", id}};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
template <typename T> struct Deleter {
|
||||
void operator()(T *element) {
|
||||
(void)element;
|
||||
LOG(ERROR) << "Called default deleter";
|
||||
}
|
||||
};
|
||||
|
||||
template <> struct Deleter<rpc_string> {
|
||||
void operator()(rpc_string *element) {
|
||||
if (element) {
|
||||
crpc_str_destroy(element);
|
||||
}
|
||||
}
|
||||
};
|
||||
using rpcStringPtr = std::unique_ptr<rpc_string, Deleter<rpc_string>>;
|
||||
|
||||
void RpcServer::register_method(const std::string &name, Handler handler) {
|
||||
handlers_[name] = std::move(handler);
|
||||
}
|
||||
|
||||
void RpcServer::register_method(const std::string& name, callback_t handler) {
|
||||
handlers_[name] = [handler](const nlohmann::json &j)
|
||||
-> std::variant<nlohmann::json, std::string> {
|
||||
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;
|
||||
rpc_string* res = handler(&tmp);
|
||||
return {res->s};
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,14 +59,11 @@ std::string RpcServer::process(const std::string &request_str) {
|
||||
}
|
||||
|
||||
try {
|
||||
auto result = it->second(params);
|
||||
CreateSuccess visitor;
|
||||
visitor.id = id;
|
||||
std::visit(visitor, result);
|
||||
return visitor.obj.dump();
|
||||
json result = it->second(params);
|
||||
return create_success(result, id).dump();
|
||||
} catch (const std::exception &e) {
|
||||
return create_error(-32000, e.what(), id).dump(); // Server error
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace score
|
||||
} // namespace cloud_point_rpc
|
||||
|
||||
@ -1,74 +1,52 @@
|
||||
#include "server_api.h"
|
||||
#include "cloud_point_rpc/config.hpp"
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include "cloud_point_rpc/tcp_server.hpp"
|
||||
#include <algorithm>
|
||||
#include <glog/logging.h>
|
||||
#include <list>
|
||||
#include "server_api.h"
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <list>
|
||||
|
||||
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;
|
||||
cloud_point_rpc::RpcServer rpc_server;
|
||||
std::unique_ptr<cloud_point_rpc::TcpServer> server = nullptr;
|
||||
|
||||
extern "C" {
|
||||
|
||||
const char* crpc_str_get_data(const rpc_string* that) {
|
||||
if (!that) {
|
||||
LOG(ERROR) << "Tried to get data on nullptr";
|
||||
return nullptr;
|
||||
}
|
||||
return that->s.c_str();
|
||||
}
|
||||
|
||||
uint64_t crpc_str_get_size(const rpc_string* that){
|
||||
if (!that) {
|
||||
LOG(ERROR) << "Tried to get size on nullptr";
|
||||
return 0;
|
||||
}
|
||||
return that->s.size();
|
||||
}
|
||||
|
||||
rpc_string* crpc_str_create(const char* data, uint64_t size){
|
||||
if (!data) {
|
||||
LOG(ERROR) << "Tried to create with nullptr data";
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard lock(gc_mtx);
|
||||
gc.push_back(std::make_unique<rpc_string>(data, size));
|
||||
return gc.back().get();
|
||||
}
|
||||
|
||||
void crpc_str_destroy(rpc_string* that){
|
||||
if (!that) {
|
||||
LOG(ERROR) << "Tried to destroy on nullptr";
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(gc_mtx);
|
||||
auto it = std::ranges::find(gc, that, &std::unique_ptr<rpc_string>::get);
|
||||
if(it != gc.end())
|
||||
gc.erase(it);
|
||||
}
|
||||
|
||||
|
||||
void crpc_init(const char* config_path) {
|
||||
if (!google::IsGoogleLoggingInitialized())
|
||||
google::InitGoogleLogging("CloudPointRPC");
|
||||
if(config_path == nullptr) {
|
||||
LOG(ERROR) << "config_path was not provided";
|
||||
return;
|
||||
LOG(INFO) << "config_path was not provided";
|
||||
}
|
||||
try {
|
||||
auto config = score::ConfigLoader::load(config_path);
|
||||
auto config = cloud_point_rpc::ConfigLoader::load(config_path);
|
||||
LOG(INFO) << "Loaded config from " << config_path;
|
||||
|
||||
server = std::make_unique<score::TcpServer>(
|
||||
config.server.ip, config.server.port,
|
||||
server = std::make_unique<cloud_point_rpc::TcpServer>(config.server.ip, config.server.port,
|
||||
[&](const std::string &request) {
|
||||
std::lock_guard lock(server_mtx);
|
||||
return rpc_server.process(request);
|
||||
return rpc_server.process(
|
||||
request);
|
||||
});
|
||||
server->start();
|
||||
} catch (const std::exception &e) {
|
||||
@ -76,39 +54,15 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,12 +20,12 @@ int main(int argc, char *argv[]) {
|
||||
LOG(INFO) << "Starting Cloud Point RPC Server (Test Mock)...";
|
||||
|
||||
try {
|
||||
auto config = score::ConfigLoader::load(config_path);
|
||||
auto config = cloud_point_rpc::ConfigLoader::load(config_path);
|
||||
LOG(INFO) << "Loaded config from " << config_path;
|
||||
|
||||
// Inject test data into service
|
||||
score::Service service(config.test_data);
|
||||
score::RpcServer rpc_server;
|
||||
cloud_point_rpc::Service service(config.test_data);
|
||||
cloud_point_rpc::RpcServer rpc_server;
|
||||
|
||||
rpc_server.register_method("get-intrinsic-params", [&](const json &) {
|
||||
return service.get_intrinsic_params();
|
||||
@ -39,7 +39,7 @@ int main(int argc, char *argv[]) {
|
||||
return service.get_cloud_point();
|
||||
});
|
||||
|
||||
score::TcpServer server(config.server.ip, config.server.port,
|
||||
cloud_point_rpc::TcpServer server(config.server.ip, config.server.port,
|
||||
[&](const std::string &request) {
|
||||
return rpc_server.process(
|
||||
request);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#include "cloud_point_rpc/service.hpp"
|
||||
|
||||
namespace score {
|
||||
namespace cloud_point_rpc {
|
||||
|
||||
Service::Service(const TestData &data) : data_(data) {}
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
#include <condition_variable>
|
||||
#include <glog/logging.h>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
@ -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);
|
||||
}
|
||||
@ -131,7 +121,7 @@ class TestThread {
|
||||
calls_queue = std::queue<std::string>();
|
||||
methods.clear();
|
||||
state.store(true, std::memory_order_relaxed);
|
||||
server = score::RpcServer();
|
||||
server = cloud_point_rpc::RpcServer();
|
||||
}
|
||||
|
||||
private:
|
||||
@ -141,17 +131,15 @@ class TestThread {
|
||||
std::set<std::string> methods{};
|
||||
std::jthread thr;
|
||||
std::mutex mtx;
|
||||
score::RpcServer server;
|
||||
cloud_point_rpc::RpcServer server;
|
||||
std::chrono::duration<int64_t, std::milli> thr_sleep{50};
|
||||
} test;
|
||||
|
||||
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,13 +166,7 @@ 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));
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
[wrap-file]
|
||||
directory = base64-0.5.2
|
||||
source_url = https://github.com/aklomp/base64/archive/refs/tags/v0.5.2.tar.gz
|
||||
source_filename = base64-0.5.2.tar.gz
|
||||
source_hash = 723a0f9f4cf44cf79e97bcc315ec8f85e52eb104c8882942c3f2fba95acc080d
|
||||
source_fallback_url = https://wrapdb.mesonbuild.com/v2/aklomp-base64_0.5.2-1/get_source/base64-0.5.2.tar.gz
|
||||
patch_filename = aklomp-base64_0.5.2-1_patch.zip
|
||||
patch_url = https://wrapdb.mesonbuild.com/v2/aklomp-base64_0.5.2-1/get_patch
|
||||
patch_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/aklomp-base64_0.5.2-1/aklomp-base64_0.5.2-1_patch.zip
|
||||
patch_hash = 9805354b8c0333fe0123c10d8c62356ef1d0d67a2689a348d18f73bddc1e2b10
|
||||
wrapdb_version = 0.5.2-1
|
||||
|
||||
[provide]
|
||||
dependency_names = base64
|
||||
@ -1,16 +1,9 @@
|
||||
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_c_api.cpp'
|
||||
)
|
||||
|
||||
test_exe = executable('unit_tests',
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
//
|
||||
// Created by vptyp on 11.03.2026.
|
||||
//
|
||||
#include <chrono>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <thread>
|
||||
|
||||
#include "cloud_point_rpc/config.hpp"
|
||||
#include "cloud_point_rpc/rpc_coder.hpp"
|
||||
|
||||
|
||||
class Base64Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
|
||||
}
|
||||
void TearDown() override {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(Base64Test, EncodeDecode) {
|
||||
std::vector raw{'H', 'e', 'l', 'l', 'o', 'w', '\0'};
|
||||
score::Base64RPCCoder coder;
|
||||
auto encoded = coder.encode(raw);
|
||||
LOG(INFO) << "encoded: " << encoded;
|
||||
auto decoded = coder.decode(encoded);
|
||||
EXPECT_EQ(std::ranges::equal(decoded, raw), true);
|
||||
LOG(INFO) << "done";
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -63,16 +63,14 @@ TEST_F(TestCApi, AddedMultiple) {
|
||||
}
|
||||
|
||||
auto register_idx = [&]<size_t I>() {
|
||||
crpc_test_add_method(
|
||||
+[](rpc_string *) -> rpc_string * {
|
||||
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);
|
||||
}, &called[I].second);
|
||||
};
|
||||
|
||||
register_idx.template operator()<0>();
|
||||
@ -123,17 +121,14 @@ TEST_F(TestCApi, ScheduleCall) {
|
||||
}
|
||||
|
||||
auto register_idx = [&]<size_t I>() {
|
||||
crpc_test_add_method(
|
||||
+[](rpc_string *) -> rpc_string * {
|
||||
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);
|
||||
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));
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -9,7 +9,7 @@
|
||||
#include "cloud_point_rpc/rpc_server.hpp"
|
||||
#include "cloud_point_rpc/tcp_server.hpp"
|
||||
|
||||
using namespace score;
|
||||
using namespace cloud_point_rpc;
|
||||
|
||||
class CliTest : public ::testing::Test {
|
||||
public:
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
#include <fstream>
|
||||
|
||||
using namespace score;
|
||||
using namespace cloud_point_rpc;
|
||||
|
||||
class IntegrationTest : public ::testing::Test {
|
||||
protected:
|
||||
@ -69,7 +69,7 @@ class IntegrationTest : public ::testing::Test {
|
||||
std::remove("config.yaml");
|
||||
}
|
||||
|
||||
Config config_{};
|
||||
Config config_;
|
||||
std::unique_ptr<Service> service_;
|
||||
std::unique_ptr<RpcServer> rpc_server_;
|
||||
std::unique_ptr<TcpServer> tcp_server_;
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
#include <vector>
|
||||
|
||||
using json = nlohmann::json;
|
||||
using namespace score;
|
||||
using namespace cloud_point_rpc;
|
||||
|
||||
class RpcServerTest : public ::testing::Test {
|
||||
protected:
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -15,11 +15,11 @@ class TcpTest : public ::testing::Test {
|
||||
|
||||
protected:
|
||||
void SetUp() override {
|
||||
server_ = std::make_unique<score::TcpServer>(
|
||||
server_ = std::make_unique<cloud_point_rpc::TcpServer>(
|
||||
"127.0.0.1", 12345, [this](const std::string &request) {
|
||||
EXPECT_EQ(request, expected_);
|
||||
std::string msg = "Echo: " + request;
|
||||
auto v = score::serialize(msg.length());
|
||||
auto v = cloud_point_rpc::serialize(msg.length());
|
||||
std::string res(v.begin(), v.end());
|
||||
res += msg;
|
||||
return res;
|
||||
@ -33,19 +33,19 @@ class TcpTest : public ::testing::Test {
|
||||
}
|
||||
|
||||
std::string expected_;
|
||||
std::unique_ptr<score::TcpServer> server_;
|
||||
std::unique_ptr<cloud_point_rpc::TcpServer> server_;
|
||||
};
|
||||
|
||||
TEST(SerializeTest, Base) {
|
||||
uint64_t value{123};
|
||||
auto res = score::serialize(value);
|
||||
EXPECT_EQ(value, score::deserialize<uint64_t>(res));
|
||||
auto res = cloud_point_rpc::serialize(value);
|
||||
EXPECT_EQ(value, cloud_point_rpc::deserialize<uint64_t>(res));
|
||||
}
|
||||
|
||||
TEST_F(TcpTest, EchoTest) {
|
||||
constexpr std::string_view msg = "Hello, TCP Server!";
|
||||
ExpectedResponse(msg.data());
|
||||
score::TCPConnector connector("127.0.0.1", 12345);
|
||||
cloud_point_rpc::TCPConnector connector("127.0.0.1", 12345);
|
||||
auto res = connector.Send(msg.data());
|
||||
}
|
||||
|
||||
@ -53,6 +53,6 @@ TEST_F(TcpTest, HugeBuffer) {
|
||||
static constexpr uint64_t w = 1280, h = 720, c = 3;
|
||||
std::string data(w * h * c, 77);
|
||||
ExpectedResponse(data);
|
||||
score::TCPConnector connector("127.0.0.1", 12345);
|
||||
cloud_point_rpc::TCPConnector connector("127.0.0.1", 12345);
|
||||
auto res = connector.Send(data);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user