[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() {
|
||||
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
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 <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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
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