From 54c169a845d364438f6e67384183f27afe520306 Mon Sep 17 00:00:00 2001 From: Artur Mukhamadiev Date: Mon, 26 Jan 2026 00:24:43 +0300 Subject: [PATCH] [cli] added cli for testing capabilities Gemini 3 pro + opencode --- AGENTS.md | 145 +++++++++++++++++++++++++ include/cloud_point_rpc/cli.hpp | 19 ++++ include/cloud_point_rpc/rpc_client.hpp | 32 +++--- src/cli.cpp | 58 ++++++++++ src/main.cpp | 53 +++++---- src/meson.build | 3 +- tests/meson.build | 3 +- tests/test_cli.cpp | 67 ++++++++++++ 8 files changed, 336 insertions(+), 44 deletions(-) create mode 100644 AGENTS.md create mode 100644 include/cloud_point_rpc/cli.hpp create mode 100644 src/cli.cpp create mode 100644 tests/test_cli.cpp diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2889a48 --- /dev/null +++ b/AGENTS.md @@ -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 + ``` + *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 get_intrinsic_params() const; + + private: + Config config_; + mutable std::mutex mutex_; + std::vector 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: `(): ` + - 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/`. diff --git a/include/cloud_point_rpc/cli.hpp b/include/cloud_point_rpc/cli.hpp new file mode 100644 index 0000000..614913c --- /dev/null +++ b/include/cloud_point_rpc/cli.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +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 diff --git a/include/cloud_point_rpc/rpc_client.hpp b/include/cloud_point_rpc/rpc_client.hpp index 9d5be3a..0a23508 100644 --- a/include/cloud_point_rpc/rpc_client.hpp +++ b/include/cloud_point_rpc/rpc_client.hpp @@ -25,30 +25,25 @@ class RpcClient { } [[nodiscard]] std::vector get_intrinsic_params() { - return call_method>("get-intrinsic-params"); + return call("get-intrinsic-params")["result"].get>(); } [[nodiscard]] std::vector get_extrinsic_params() { - return call_method>("get-extrinsic-params"); + return call("get-extrinsic-params")["result"].get>(); } [[nodiscard]] std::vector> get_cloud_point() { - return call_method>>("get-cloud-point"); + return call("get-cloud-point")["result"].get>>(); } - ~RpcClient() { - // Socket closes automatically - } - - private: - template - 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 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()); } - return response["result"].get(); + return response; } + ~RpcClient() { + // Socket closes automatically + } + + private: asio::io_context io_context_; asio::ip::tcp::socket socket_; int id_counter_ = 0; diff --git a/src/cli.cpp b/src/cli.cpp new file mode 100644 index 0000000..c7067a8 --- /dev/null +++ b/src/cli.cpp @@ -0,0 +1,58 @@ +#include "cloud_point_rpc/cli.hpp" +#include "cloud_point_rpc/rpc_client.hpp" +#include +#include + +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 diff --git a/src/main.cpp b/src/main.cpp index bab2e46..f96ee19 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,33 +1,32 @@ #include -#include -#include "cloud_point_rpc/rpc_server.hpp" -#include "cloud_point_rpc/service.hpp" +#include +#include "cloud_point_rpc/cli.hpp" +#include "cloud_point_rpc/config.hpp" +#include -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; + } } diff --git a/src/meson.build b/src/meson.build index 2a3cbfe..ba59f69 100644 --- a/src/meson.build +++ b/src/meson.build @@ -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', diff --git a/tests/meson.build b/tests/meson.build index 8cc36bd..fd6ab1c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,6 +1,7 @@ test_sources = files( 'test_rpc.cpp', - 'test_integration.cpp' + 'test_integration.cpp', + 'test_cli.cpp' ) test_exe = executable('unit_tests', diff --git a/tests/test_cli.cpp b/tests/test_cli.cpp new file mode 100644 index 0000000..38540c0 --- /dev/null +++ b/tests/test_cli.cpp @@ -0,0 +1,67 @@ +#include +#include +#include +#include +#include +#include +#include + +#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(); + rpc_server->register_method("hello", [](const nlohmann::json& params) { + return "world"; + }); + + tcp_server = std::make_unique(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 rpc_server; + std::unique_ptr 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{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")); +}