[cli] added cli for testing capabilities
Gemini 3 pro + opencode
This commit is contained in:
parent
cbe56d9193
commit
54c169a845
145
AGENTS.md
Normal file
145
AGENTS.md
Normal 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/`.
|
||||||
19
include/cloud_point_rpc/cli.hpp
Normal file
19
include/cloud_point_rpc/cli.hpp
Normal 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
|
||||||
@ -25,30 +25,25 @@ class RpcClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::vector<double> get_intrinsic_params() {
|
[[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() {
|
[[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() {
|
[[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() {
|
[[nodiscard]] nlohmann::json call(const std::string& method, const nlohmann::json& params = nlohmann::json::object()) {
|
||||||
// Socket closes automatically
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
template<typename T>
|
|
||||||
T call_method(const std::string& method) {
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
// Create Request
|
// Create Request
|
||||||
json request = {
|
json request = {
|
||||||
{"jsonrpc", "2.0"},
|
{"jsonrpc", "2.0"},
|
||||||
{"method", method},
|
{"method", method},
|
||||||
|
{"params", params},
|
||||||
{"id", ++id_counter_}
|
{"id", ++id_counter_}
|
||||||
};
|
};
|
||||||
std::string request_str = request.dump();
|
std::string request_str = request.dump();
|
||||||
@ -59,19 +54,26 @@ class RpcClient {
|
|||||||
|
|
||||||
// Read Response
|
// Read Response
|
||||||
LOG(INFO) << "Client reading response...";
|
LOG(INFO) << "Client reading response...";
|
||||||
char buffer[4096];
|
std::vector<char> buffer(65536);
|
||||||
size_t len = socket_.read_some(asio::buffer(buffer, 4096));
|
asio::error_code ec;
|
||||||
LOG(INFO) << "Client read " << len << " bytes";
|
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")) {
|
if (response.contains("error")) {
|
||||||
throw std::runtime_error(response["error"]["message"].get<std::string>());
|
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::io_context io_context_;
|
||||||
asio::ip::tcp::socket socket_;
|
asio::ip::tcp::socket socket_;
|
||||||
int id_counter_ = 0;
|
int id_counter_ = 0;
|
||||||
|
|||||||
58
src/cli.cpp
Normal file
58
src/cli.cpp
Normal 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
|
||||||
53
src/main.cpp
53
src/main.cpp
@ -1,33 +1,32 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <fstream>
|
||||||
#include "cloud_point_rpc/rpc_server.hpp"
|
#include "cloud_point_rpc/cli.hpp"
|
||||||
#include "cloud_point_rpc/service.hpp"
|
#include "cloud_point_rpc/config.hpp"
|
||||||
|
#include <glog/logging.h>
|
||||||
|
|
||||||
int main() {
|
int main(int argc, char* argv[]) {
|
||||||
cloud_point_rpc::Service service;
|
google::InitGoogleLogging(argv[0]);
|
||||||
cloud_point_rpc::RpcServer server;
|
FLAGS_logtostderr = 1;
|
||||||
|
|
||||||
// Bind service methods to RPC handlers
|
std::string config_path = "config.yaml";
|
||||||
server.register_method("get-intrinsic-params", [&](const nlohmann::json&) {
|
if (argc > 1) {
|
||||||
return service.get_intrinsic_params();
|
config_path = argv[1];
|
||||||
});
|
}
|
||||||
|
|
||||||
server.register_method("get-extrinsic-params", [&](const nlohmann::json&) {
|
// Check if config file exists
|
||||||
return service.get_extrinsic_params();
|
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&) {
|
try {
|
||||||
return service.get_cloud_point();
|
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::cout << "Cloud Point RPC Server initialized." << std::endl;
|
std::cerr << "Failed to start CLI: " << e.what() << std::endl;
|
||||||
std::cout << "Enter JSON-RPC requests (Ctrl+D to exit):" << std::endl;
|
return 1;
|
||||||
|
}
|
||||||
std::string line;
|
|
||||||
while (std::getline(std::cin, line)) {
|
|
||||||
if (line.empty()) continue;
|
|
||||||
std::cout << server.process(line) << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
cloud_point_rpc_sources = files(
|
cloud_point_rpc_sources = files(
|
||||||
'rpc_server.cpp',
|
'rpc_server.cpp',
|
||||||
'service.cpp'
|
'service.cpp',
|
||||||
|
'cli.cpp'
|
||||||
)
|
)
|
||||||
|
|
||||||
libcloud_point_rpc = static_library('cloud_point_rpc',
|
libcloud_point_rpc = static_library('cloud_point_rpc',
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
test_sources = files(
|
test_sources = files(
|
||||||
'test_rpc.cpp',
|
'test_rpc.cpp',
|
||||||
'test_integration.cpp'
|
'test_integration.cpp',
|
||||||
|
'test_cli.cpp'
|
||||||
)
|
)
|
||||||
|
|
||||||
test_exe = executable('unit_tests',
|
test_exe = executable('unit_tests',
|
||||||
|
|||||||
67
tests/test_cli.cpp
Normal file
67
tests/test_cli.cpp
Normal 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"));
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user