[cli] added cli for testing capabilities

Gemini 3 pro + opencode
This commit is contained in:
Artur Mukhamadiev 2026-01-26 00:24:43 +03:00
parent cbe56d9193
commit 54c169a845
8 changed files with 336 additions and 44 deletions

145
AGENTS.md Normal file
View File

@ -0,0 +1,145 @@
# Cloud Point RPC Agent Guide
This repository contains a C++20 implementation of a JSON RPC protocol for communicating with a Unity Scene.
Agents working on this codebase must adhere to the following guidelines and conventions.
## 1. Build, Lint, and Test
The project uses the **Meson** build system.
### Build Commands
- **Setup Build Directory:**
```bash
meson setup build
```
- **Compile:**
```bash
meson compile -C build
```
- **Clean:**
```bash
meson compile --clean -C build
```
### Testing
- **Run All Tests:**
```bash
meson test -C build
```
- **Run Specific Test:**
To run a single test suite or case, use the test name defined in `meson.build`:
```bash
meson test -C build <test_name>
```
*Example: `meson test -C build unit_tests`*
- **Verbose Output:**
```bash
meson test -C build -v
```
### Linting & Formatting
- **Format Code:**
Use `clang-format` with the project's configuration (assumed Google style if not present).
```bash
ninja -C build clang-format
# OR manual execution
find src include tests -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
```
- **Static Analysis:**
If configured, run clang-tidy:
```bash
ninja -C build clang-tidy
```
## 2. Code Style & Conventions
Adhere strictly to **Modern C++20** standards.
### General Guidelines
- **Standard:** C++20. Use concepts, ranges, and smart pointers. Avoid raw `new`/`delete`.
- **Memory Management:** Use `std::unique_ptr` and `std::shared_ptr`.
- **Const Correctness:** Use `const` (and `constexpr`/`consteval`) whenever possible.
- **Includes:** Use absolute paths for project headers (e.g., `#include "rpc/server.hpp"`).
- **Auto:** Use `auto` when the type is obvious from the right-hand side or is a complex template type.
### 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).
- **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).
### Project Structure
- `include/cloud_point_rpc/`: Public header files.
- `src/`: Implementation files.
- `tests/`: Unit and integration tests.
- `subprojects/`: Meson wrap files for dependencies.
- `meson.build`: Build configuration.
### Error Handling
- Use **Exceptions** (`std::runtime_error`, `std::invalid_argument`) for invariant violations and logical errors.
- Use **`std::optional`** or **`std::expected`** for recoverable runtime errors.
- **JSON RPC Errors:** Ensure all RPC handlers catch exceptions and return valid JSON-RPC error objects containing `code`, `message`, and `data`.
### Documentation
- Use **Doxygen** style comments for public APIs.
- Document thread safety guarantees.
### Example Class
```cpp
namespace cloud_point_rpc {
/// @brief Manages camera parameters.
class CameraController {
public:
struct Config {
int width;
int height;
};
explicit CameraController(const Config& config);
/// @brief Retrieves current intrinsic parameters.
/// @return A 3x3 matrix.
[[nodiscard]] std::vector<double> get_intrinsic_params() const;
private:
Config config_;
mutable std::mutex mutex_;
std::vector<double> cached_intrinsics_;
};
} // namespace cloud_point_rpc
```
### Implementation Details
- **JSON Library:** Use `nlohmann/json` (likely via `subprojects/nlohmann_json.wrap`).
- **Concurrency:** Use `std::jthread` (auto-joining) over `std::thread`.
- **RPC Methods:**
- Implement handlers for: `get-cloud-point`, `get-intrinsic-params`, `get-extrinsic-params`.
- Ensure thread safety if the RPC server is multi-threaded.
## 3. Workflow & Git
### Commit Messages
- Use conventional commits format: `<type>(<scope>): <subject>`
- Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`.
- Example: `feat(rpc): add handler for get-cloud-point`
### Pull Requests
- Ensure `meson test -C build` passes before requesting review.
- Keep PRs small and focused on a single logical change.
## 4. Cursor & Copilot Rules
*(No specific rules found in .cursor/rules/ or .github/copilot-instructions.md.)*
- **Context:** Always read related header files before modifying source files.
- **Verification:** Write unit tests for new features in `tests/`.
- **Refactoring:** When refactoring, ensure existing behavior is preserved via tests.
- **Dependencies:** Do not introduce new dependencies without updating `meson.build` and `subprojects/`.

View File

@ -0,0 +1,19 @@
#pragma once
#include <iostream>
#include <string>
namespace cloud_point_rpc {
/**
* @brief Runs the CLI client.
*
* @param input Input stream (usually std::cin)
* @param output Output stream (usually std::cout)
* @param ip Server IP
* @param port Server Port
* @return int exit code
*/
int run_cli(std::istream& input, std::ostream& output, const std::string& ip, int port);
} // namespace cloud_point_rpc

View File

@ -25,30 +25,25 @@ class RpcClient {
}
[[nodiscard]] std::vector<double> get_intrinsic_params() {
return call_method<std::vector<double>>("get-intrinsic-params");
return call("get-intrinsic-params")["result"].get<std::vector<double>>();
}
[[nodiscard]] std::vector<double> get_extrinsic_params() {
return call_method<std::vector<double>>("get-extrinsic-params");
return call("get-extrinsic-params")["result"].get<std::vector<double>>();
}
[[nodiscard]] std::vector<std::vector<double>> get_cloud_point() {
return call_method<std::vector<std::vector<double>>>("get-cloud-point");
return call("get-cloud-point")["result"].get<std::vector<std::vector<double>>>();
}
~RpcClient() {
// Socket closes automatically
}
private:
template<typename T>
T call_method(const std::string& method) {
[[nodiscard]] nlohmann::json call(const std::string& method, const nlohmann::json& params = nlohmann::json::object()) {
using json = nlohmann::json;
// Create Request
json request = {
{"jsonrpc", "2.0"},
{"method", method},
{"params", params},
{"id", ++id_counter_}
};
std::string request_str = request.dump();
@ -59,19 +54,26 @@ class RpcClient {
// Read Response
LOG(INFO) << "Client reading response...";
char buffer[4096];
size_t len = socket_.read_some(asio::buffer(buffer, 4096));
LOG(INFO) << "Client read " << len << " bytes";
std::vector<char> buffer(65536);
asio::error_code ec;
size_t len = socket_.read_some(asio::buffer(buffer), ec);
if (ec) throw std::system_error(ec);
json response = json::parse(std::string(buffer, len));
LOG(INFO) << "Client read " << len << " bytes";
json response = json::parse(std::string(buffer.data(), len));
if (response.contains("error")) {
throw std::runtime_error(response["error"]["message"].get<std::string>());
}
return response["result"].get<T>();
return response;
}
~RpcClient() {
// Socket closes automatically
}
private:
asio::io_context io_context_;
asio::ip::tcp::socket socket_;
int id_counter_ = 0;

58
src/cli.cpp Normal file
View File

@ -0,0 +1,58 @@
#include "cloud_point_rpc/cli.hpp"
#include "cloud_point_rpc/rpc_client.hpp"
#include <glog/logging.h>
#include <iomanip>
namespace cloud_point_rpc {
void print_menu(std::ostream& output) {
output << "\n=== Cloud Point RPC CLI ===" << std::endl;
output << "1. get-intrinsic-params" << std::endl;
output << "2. get-extrinsic-params" << std::endl;
output << "3. get-cloud-point" << std::endl;
output << "0. Exit" << std::endl;
output << "Select an option: ";
}
int run_cli(std::istream& input, std::ostream& output, const std::string& ip, int port) {
try {
RpcClient client;
client.connect(ip, port);
output << "Connected to " << ip << ":" << port << std::endl;
std::string choice;
while (true) {
print_menu(output);
if (!(input >> choice)) break;
if (choice == "0") break;
std::string method;
if (choice == "1") {
method = "get-intrinsic-params";
} else if (choice == "2") {
method = "get-extrinsic-params";
} else if (choice == "3") {
method = "get-cloud-point";
} else {
output << "Invalid option: " << choice << std::endl;
continue;
}
try {
auto response = client.call(method);
output << "\nResponse:\n" << response.dump(4) << std::endl;
} catch (const std::exception& e) {
output << "\nRPC Error: " << e.what() << std::endl;
}
}
} catch (const std::exception& e) {
LOG(ERROR) << "CLI Error: " << e.what();
output << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
} // namespace cloud_point_rpc

View File

@ -1,33 +1,32 @@
#include <iostream>
#include <string>
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/service.hpp"
#include <fstream>
#include "cloud_point_rpc/cli.hpp"
#include "cloud_point_rpc/config.hpp"
#include <glog/logging.h>
int main() {
cloud_point_rpc::Service service;
cloud_point_rpc::RpcServer server;
int main(int argc, char* argv[]) {
google::InitGoogleLogging(argv[0]);
FLAGS_logtostderr = 1;
// Bind service methods to RPC handlers
server.register_method("get-intrinsic-params", [&](const nlohmann::json&) {
return service.get_intrinsic_params();
});
std::string config_path = "config.yaml";
if (argc > 1) {
config_path = argv[1];
}
server.register_method("get-extrinsic-params", [&](const nlohmann::json&) {
return service.get_extrinsic_params();
});
// Check if config file exists
std::ifstream f(config_path.c_str());
if (!f.good()) {
std::cerr << "Config file not found: " << config_path << std::endl;
std::cerr << "Please create config.yaml or provide path as argument." << std::endl;
return 1;
}
f.close();
server.register_method("get-cloud-point", [&](const nlohmann::json&) {
return service.get_cloud_point();
});
std::cout << "Cloud Point RPC Server initialized." << std::endl;
std::cout << "Enter JSON-RPC requests (Ctrl+D to exit):" << std::endl;
std::string line;
while (std::getline(std::cin, line)) {
if (line.empty()) continue;
std::cout << server.process(line) << std::endl;
}
return 0;
try {
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;
return 1;
}
}

View File

@ -1,6 +1,7 @@
cloud_point_rpc_sources = files(
'rpc_server.cpp',
'service.cpp'
'service.cpp',
'cli.cpp'
)
libcloud_point_rpc = static_library('cloud_point_rpc',

View File

@ -1,6 +1,7 @@
test_sources = files(
'test_rpc.cpp',
'test_integration.cpp'
'test_integration.cpp',
'test_cli.cpp'
)
test_exe = executable('unit_tests',

67
tests/test_cli.cpp Normal file
View File

@ -0,0 +1,67 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <thread>
#include <chrono>
#include <sstream>
#include <iostream>
#include <future>
#include "cloud_point_rpc/tcp_server.hpp"
#include "cloud_point_rpc/rpc_server.hpp"
#include "cloud_point_rpc/cli.hpp"
using namespace cloud_point_rpc;
class CliTest : public ::testing::Test {
protected:
void SetUp() override {
server_ip = "127.0.0.1";
server_port = 9096;
rpc_server = std::make_unique<RpcServer>();
rpc_server->register_method("hello", [](const nlohmann::json& params) {
return "world";
});
tcp_server = std::make_unique<TcpServer>(server_ip, server_port, [this](const std::string& req) {
return rpc_server->process(req);
});
tcp_server->start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
void TearDown() override {
tcp_server->stop();
}
std::string server_ip;
int server_port;
std::unique_ptr<RpcServer> rpc_server;
std::unique_ptr<TcpServer> tcp_server;
};
TEST_F(CliTest, SendsInputToServerAndReceivesResponse) {
std::stringstream input;
std::stringstream output;
// Select option 1 (get-intrinsic-params) then 0 (exit)
// First we need to make sure the rpc_server has the method registered.
// Our SetUp registers "hello", let's register "get-intrinsic-params" too.
rpc_server->register_method("get-intrinsic-params", [](const nlohmann::json&) {
return std::vector<double>{1.0, 2.0, 3.0};
});
input << "1" << std::endl;
input << "0" << std::endl;
int result = run_cli(input, output, server_ip, server_port);
EXPECT_EQ(result, 0);
std::string response = output.str();
// Use more flexible check because of pretty printing
EXPECT_THAT(response, ::testing::HasSubstr("\"result\": ["));
EXPECT_THAT(response, ::testing::HasSubstr("1.0"));
EXPECT_THAT(response, ::testing::HasSubstr("2.0"));
EXPECT_THAT(response, ::testing::HasSubstr("3.0"));
}