From 1e5b964a7c929b90b6d96e4d764df10622e0614e Mon Sep 17 00:00:00 2001 From: sladro Date: Tue, 13 Jan 2026 08:28:55 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=84=E8=8C=83=E4=BC=98=E5=8C=964?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/graph_manager.h | 13 ++ include/plugin_loader.h | 5 + include/utils/result.h | 121 +++++++++++ src/graph_manager.cpp | 71 +++++++ src/plugin_loader.cpp | 17 +- tests/CMakeLists.txt | 44 +++- tests/test_config_expand.cpp | 387 +++++++++++++++++++++++++++++++++++ tests/test_result.cpp | 137 +++++++++++++ tests/test_simple_json.cpp | 313 ++++++++++++++++++++++++++++ tests/test_spsc_queue.cpp | 283 +++++++++++++++++++++++++ 10 files changed, 1383 insertions(+), 8 deletions(-) create mode 100644 include/utils/result.h create mode 100644 tests/test_config_expand.cpp create mode 100644 tests/test_result.cpp create mode 100644 tests/test_simple_json.cpp create mode 100644 tests/test_spsc_queue.cpp diff --git a/include/graph_manager.h b/include/graph_manager.h index ccaf864..f18e070 100644 --- a/include/graph_manager.h +++ b/include/graph_manager.h @@ -15,6 +15,7 @@ #include "node.h" #include "plugin_loader.h" +#include "utils/result.h" #include "utils/simple_json.h" namespace rk3588 { @@ -178,28 +179,40 @@ public: ~GraphManager(); static bool LoadConfigFile(const std::string& path, SimpleJson& out, std::string& err); + static Result LoadConfig(const std::string& path); bool Build(const SimpleJson& root_cfg, std::string& err); bool BuildFromFile(const std::string& path, std::string& err); + Status BuildFromConfig(const SimpleJson& root_cfg); + Status BuildFromPath(const std::string& path); + bool StartAll(); void StopAll(); void RequestStop(); void BlockUntilStop(); bool ReloadFromFile(const std::string& path, std::string& err); + Status Reload(const std::string& path); const std::string& ConfigPath() const { return config_path_; } const std::string& LastGoodPath() const { return last_good_path_; } bool RollbackFromLastGood(std::string& err); + Status Rollback(); bool UpdateNodeConfig(const std::string& node_id, const std::optional& graph, const SimpleJson& new_node_cfg, std::string& err); + Status SetNodeConfig(const std::string& node_id, const SimpleJson& new_node_cfg, + const std::optional& graph = std::nullopt); std::vector ListGraphSnapshots(); bool GetGraphSnapshot(const std::string& name, GraphSnapshot& out, std::string& err); + Result GetGraph(const std::string& name); + // If graph is not set: auto-match when unique, otherwise return false with err describing ambiguity. bool GetNodeSnapshot(const std::string& node_id, const std::optional& graph, NodeSnapshot& out, std::string& err); + Result GetNode(const std::string& node_id, + const std::optional& graph = std::nullopt); private: bool running_ = false; diff --git a/include/plugin_loader.h b/include/plugin_loader.h index cd59da3..3415608 100644 --- a/include/plugin_loader.h +++ b/include/plugin_loader.h @@ -5,6 +5,7 @@ #include #include "node.h" +#include "utils/result.h" namespace rk3588 { @@ -19,6 +20,10 @@ public: PluginLoader(PluginLoader&& other) noexcept; PluginLoader& operator=(PluginLoader&& other) noexcept; + // New Result-based API + Result CreateNode(const std::string& type); + + // Legacy API (deprecated, use CreateNode instead) NodePtr Create(const std::string& type, std::string& err); // Switch plugin directory. This will unload any cached plugins. diff --git a/include/utils/result.h b/include/utils/result.h new file mode 100644 index 0000000..31b943c --- /dev/null +++ b/include/utils/result.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include +#include +#include + +namespace rk3588 { + +// Error type for Result +class Error { +public: + Error() = default; + explicit Error(std::string message) : message_(std::move(message)) {} + Error(std::string message, int code) : message_(std::move(message)), code_(code) {} + + const std::string& Message() const { return message_; } + int Code() const { return code_; } + bool Empty() const { return message_.empty(); } + + explicit operator bool() const { return !message_.empty(); } + +private: + std::string message_; + int code_ = 0; +}; + +// Result for functions that return a value or error +template +class Result { +public: + // NOLINTNEXTLINE: implicit conversion is intentional for ergonomic API + Result(T value) noexcept(std::is_nothrow_move_constructible_v) + : data_(std::move(value)) {} + // NOLINTNEXTLINE: implicit conversion is intentional for ergonomic API + Result(Error error) noexcept : data_(std::move(error)) {} + + bool Ok() const noexcept { return std::holds_alternative(data_); } + bool Failed() const noexcept { return !Ok(); } + explicit operator bool() const noexcept { return Ok(); } + + const T& Value() const& { return std::get(data_); } + T& Value() & { return std::get(data_); } + T&& Value() && { return std::get(std::move(data_)); } + + const T& ValueOr(const T& def) const& { + return Ok() ? std::get(data_) : def; + } + + const Error& Err() const { return std::get(data_); } + const std::string& ErrMessage() const { + static const std::string kEmpty; + return Failed() ? std::get(data_).Message() : kEmpty; + } + + // Convenience: extract value, moving it out + T Take() { return std::get(std::move(data_)); } + +private: + std::variant data_; +}; + +// Result specialization for functions that only succeed/fail +template <> +class Result { +public: + Result() noexcept : error_() {} + // NOLINTNEXTLINE: implicit conversion is intentional for ergonomic API + Result(Error error) noexcept : error_(std::move(error)) {} + + static Result Ok() noexcept { return Result(); } + static Result Fail(std::string message) { return Result(Error(std::move(message))); } + static Result Fail(std::string message, int code) { + return Result(Error(std::move(message), code)); + } + + bool IsOk() const noexcept { return error_.Empty(); } + bool Failed() const noexcept { return !IsOk(); } + explicit operator bool() const noexcept { return IsOk(); } + + const Error& Err() const { return error_; } + const std::string& ErrMessage() const { return error_.Message(); } + +private: + Error error_; +}; + +// Type aliases for common patterns +using Status = Result; + +// Helper macros for early return on error +#define RK_TRY(expr) \ + do { \ + auto _result = (expr); \ + if (_result.Failed()) { \ + return _result.Err(); \ + } \ + } while (0) + +#define RK_TRY_ASSIGN(var, expr) \ + auto _result_##var = (expr); \ + if (_result_##var.Failed()) { \ + return _result_##var.Err(); \ + } \ + var = std::move(_result_##var).Take() + +// Helper functions +inline Status OkStatus() { return Status::Ok(); } +inline Status FailStatus(std::string msg) { return Status::Fail(std::move(msg)); } + +template +Result MakeResult(T value) { + return Result(std::move(value)); +} + +template +Result MakeError(std::string message) { + return Result(Error(std::move(message))); +} + +} // namespace rk3588 diff --git a/src/graph_manager.cpp b/src/graph_manager.cpp index 7c95af6..62eb54c 100644 --- a/src/graph_manager.cpp +++ b/src/graph_manager.cpp @@ -1603,4 +1603,75 @@ bool GraphManager::GetNodeSnapshot(const std::string& node_id, const std::option return false; } +// New Result-based API implementations + +Result GraphManager::LoadConfig(const std::string& path) { + SimpleJson out; + std::string err; + if (!LoadConfigFile(path, out, err)) { + return Error(err); + } + return out; +} + +Status GraphManager::BuildFromConfig(const SimpleJson& root_cfg) { + std::string err; + if (!Build(root_cfg, err)) { + return Status::Fail(err); + } + return Status::Ok(); +} + +Status GraphManager::BuildFromPath(const std::string& path) { + std::string err; + if (!BuildFromFile(path, err)) { + return Status::Fail(err); + } + return Status::Ok(); +} + +Status GraphManager::Reload(const std::string& path) { + std::string err; + if (!ReloadFromFile(path, err)) { + return Status::Fail(err); + } + return Status::Ok(); +} + +Status GraphManager::Rollback() { + std::string err; + if (!RollbackFromLastGood(err)) { + return Status::Fail(err); + } + return Status::Ok(); +} + +Status GraphManager::SetNodeConfig(const std::string& node_id, const SimpleJson& new_node_cfg, + const std::optional& graph) { + std::string err; + if (!UpdateNodeConfig(node_id, graph, new_node_cfg, err)) { + return Status::Fail(err); + } + return Status::Ok(); +} + +Result GraphManager::GetGraph(const std::string& name) { + GraphSnapshot out; + std::string err; + if (!GetGraphSnapshot(name, out, err)) { + return Error(err); + } + return out; +} + +Result GraphManager::GetNode(const std::string& node_id, + const std::optional& graph) { + NodeSnapshot out; + std::string err; + if (!GetNodeSnapshot(node_id, graph, out, err)) { + return Error(err); + } + return out; +} + } // namespace rk3588 diff --git a/src/plugin_loader.cpp b/src/plugin_loader.cpp index 6b132aa..1d5d6eb 100644 --- a/src/plugin_loader.cpp +++ b/src/plugin_loader.cpp @@ -145,13 +145,14 @@ bool PluginLoader::LoadPlugin(const std::string& type, PluginHandle& out, std::s return true; } -NodePtr PluginLoader::Create(const std::string& type, std::string& err) { +Result PluginLoader::CreateNode(const std::string& type) { auto it = cache_.find(type); if (it == cache_.end()) { PluginHandle handle; + std::string err; if (!LoadPlugin(type, handle, err)) { LogError("[PluginLoader] Failed to load plugin '" + type + "': " + err); - return NodePtr(nullptr, NodeDeleter{}); + return Error("Failed to load plugin '" + type + "': " + err); } it = cache_.emplace(type, std::move(handle)).first; } @@ -159,10 +160,18 @@ NodePtr PluginLoader::Create(const std::string& type, std::string& err) { PluginHandle& handle = it->second; NodePtr node(handle.create(), NodeDeleter{handle.destroy}); if (!node) { - err = "CreateNode returned null for type " + type; - return NodePtr(nullptr, NodeDeleter{handle.destroy}); + return Error("CreateNode returned null for type " + type); } return node; } +NodePtr PluginLoader::Create(const std::string& type, std::string& err) { + auto result = CreateNode(type); + if (result.Failed()) { + err = result.ErrMessage(); + return NodePtr(nullptr, NodeDeleter{}); + } + return std::move(result).Take(); +} + } // namespace rk3588 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0a8a14b..fcd148a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,13 +1,49 @@ -add_executable(rk3588_tests +include(FetchContent) + +# Fetch Google Test +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 +) +# Prevent overriding the parent project's compiler/linker settings on Windows +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +# Legacy simple tests (keep for backward compatibility) +add_executable(rk3588_tests_legacy test_main.cpp ${CMAKE_SOURCE_DIR}/src/utils/config_expand.cpp ) -target_include_directories(rk3588_tests PRIVATE +target_include_directories(rk3588_tests_legacy PRIVATE ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/third_party ) -target_link_libraries(rk3588_tests PRIVATE project_options Threads::Threads) +target_link_libraries(rk3588_tests_legacy PRIVATE project_options Threads::Threads) -add_test(NAME rk3588_tests COMMAND rk3588_tests) +add_test(NAME rk3588_tests_legacy COMMAND rk3588_tests_legacy) + +# New Google Test based tests +add_executable(rk3588_gtests + test_result.cpp + test_spsc_queue.cpp + test_simple_json.cpp + test_config_expand.cpp + ${CMAKE_SOURCE_DIR}/src/utils/config_expand.cpp +) + +target_include_directories(rk3588_gtests PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/third_party +) + +target_link_libraries(rk3588_gtests PRIVATE + project_options + Threads::Threads + GTest::gtest_main +) + +include(GoogleTest) +gtest_discover_tests(rk3588_gtests) diff --git a/tests/test_config_expand.cpp b/tests/test_config_expand.cpp new file mode 100644 index 0000000..5e63fb0 --- /dev/null +++ b/tests/test_config_expand.cpp @@ -0,0 +1,387 @@ +#include + +#include + +#include "utils/config_expand.h" +#include "utils/config_schema.h" +#include "utils/simple_json.h" + +namespace rk3588 { +namespace { + +TEST(ConfigExpandTest, ExpandEmptyConfig) { + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson("{}", root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + EXPECT_TRUE(expanded.IsObject()); +} + +TEST(ConfigExpandTest, PassthroughGraphs) { + const char* json = R"({ + "graphs": [ + { + "name": "test", + "nodes": [{"id": "n1", "type": "input_file", "role": "source"}], + "edges": [] + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + + const SimpleJson* graphs = expanded.Find("graphs"); + ASSERT_NE(graphs, nullptr); + EXPECT_TRUE(graphs->IsArray()); + EXPECT_EQ(graphs->AsArray().size(), 1u); + EXPECT_EQ(graphs->AsArray()[0].ValueOr("name", ""), "test"); +} + +TEST(ConfigExpandTest, ExpandTemplate) { + const char* json = R"({ + "templates": { + "simple": { + "nodes": [ + {"id": "src", "type": "input_file", "role": "source", "path": "${path}"} + ], + "edges": [] + } + }, + "instances": [ + { + "name": "cam1", + "template": "simple", + "params": {"path": "/video/test.mp4"} + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + + const SimpleJson* graphs = expanded.Find("graphs"); + ASSERT_NE(graphs, nullptr); + EXPECT_EQ(graphs->AsArray().size(), 1u); + + const SimpleJson& g = graphs->AsArray()[0]; + EXPECT_EQ(g.ValueOr("name", ""), "cam1"); + + const SimpleJson* nodes = g.Find("nodes"); + ASSERT_NE(nodes, nullptr); + EXPECT_EQ(nodes->AsArray().size(), 1u); + + // Node ID should be prefixed with instance name + EXPECT_EQ(nodes->AsArray()[0].ValueOr("id", ""), "cam1_src"); + // Placeholder should be replaced + EXPECT_EQ(nodes->AsArray()[0].ValueOr("path", ""), "/video/test.mp4"); +} + +TEST(ConfigExpandTest, ExpandMultipleInstances) { + const char* json = R"({ + "templates": { + "t": { + "nodes": [{"id": "n", "type": "test", "role": "source"}], + "edges": [] + } + }, + "instances": [ + {"name": "i1", "template": "t", "params": {}}, + {"name": "i2", "template": "t", "params": {}} + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + + const SimpleJson* graphs = expanded.Find("graphs"); + ASSERT_NE(graphs, nullptr); + EXPECT_EQ(graphs->AsArray().size(), 2u); + EXPECT_EQ(graphs->AsArray()[0].ValueOr("name", ""), "i1"); + EXPECT_EQ(graphs->AsArray()[1].ValueOr("name", ""), "i2"); +} + +TEST(ConfigExpandTest, ExpandWithOverride) { + const char* json = R"({ + "templates": { + "t": { + "nodes": [ + {"id": "n1", "type": "test", "role": "source", "enable": true}, + {"id": "n2", "type": "filter", "role": "filter"} + ], + "edges": [["n1", "n2"]] + } + }, + "instances": [ + { + "name": "inst", + "template": "t", + "params": {}, + "override": { + "nodes": { + "n1": {"enable": false} + } + } + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + + const SimpleJson* graphs = expanded.Find("graphs"); + ASSERT_NE(graphs, nullptr); + + const SimpleJson* nodes = graphs->AsArray()[0].Find("nodes"); + ASSERT_NE(nodes, nullptr); + + // Find the overridden node + bool found = false; + for (const auto& n : nodes->AsArray()) { + if (n.ValueOr("id", "") == "inst_n1") { + found = true; + EXPECT_FALSE(n.ValueOr("enable", true)); + break; + } + } + EXPECT_TRUE(found); +} + +TEST(ConfigExpandTest, EdgeIdPrefixing) { + const char* json = R"({ + "templates": { + "t": { + "nodes": [ + {"id": "src", "type": "source", "role": "source"}, + {"id": "sink", "type": "sink", "role": "sink"} + ], + "edges": [["src", "sink"]] + } + }, + "instances": [ + {"name": "g1", "template": "t", "params": {}} + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + + const SimpleJson* graphs = expanded.Find("graphs"); + ASSERT_NE(graphs, nullptr); + + const SimpleJson* edges = graphs->AsArray()[0].Find("edges"); + ASSERT_NE(edges, nullptr); + EXPECT_EQ(edges->AsArray().size(), 1u); + + // Edge endpoints should be prefixed + const SimpleJson& edge = edges->AsArray()[0]; + EXPECT_TRUE(edge.IsArray()); + EXPECT_EQ(edge.AsArray()[0].AsString(), "g1_src"); + EXPECT_EQ(edge.AsArray()[1].AsString(), "g1_sink"); +} + +TEST(ConfigExpandTest, QueueConfigPreserved) { + const char* json = R"({ + "queue": {"size": 16, "strategy": "drop_newest"}, + "graphs": [ + { + "name": "test", + "nodes": [], + "edges": [] + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + + const SimpleJson* queue = expanded.Find("queue"); + ASSERT_NE(queue, nullptr); + EXPECT_EQ(queue->ValueOr("size", 0), 16); + EXPECT_EQ(queue->ValueOr("strategy", ""), "drop_newest"); +} + +TEST(ConfigExpandTest, GlobalConfigPreserved) { + const char* json = R"({ + "global": {"metrics_port": 8080, "log_level": "debug"}, + "graphs": [] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + + const SimpleJson* global = expanded.Find("global"); + ASSERT_NE(global, nullptr); + EXPECT_EQ(global->ValueOr("metrics_port", 0), 8080); + EXPECT_EQ(global->ValueOr("log_level", ""), "debug"); +} + +TEST(ConfigExpandTest, MissingTemplateError) { + const char* json = R"({ + "templates": {}, + "instances": [ + {"name": "inst", "template": "nonexistent", "params": {}} + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_FALSE(ExpandRootConfig(root, expanded, err)); + EXPECT_FALSE(err.empty()); +} + +// Schema validation tests + +TEST(ConfigSchemaTest, ValidGraphConfig) { + const char* json = R"({ + "graphs": [ + { + "name": "valid", + "nodes": [ + {"id": "n1", "type": "input_file", "role": "source"}, + {"id": "n2", "type": "output", "role": "sink"} + ], + "edges": [["n1", "n2"]] + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + EXPECT_TRUE(ValidateExpandedRootConfig(expanded, err)) << err; +} + +TEST(ConfigSchemaTest, MissingGraphsArray) { + const char* json = R"({})"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + // Should pass since empty graphs is allowed after expand +} + +TEST(ConfigSchemaTest, NodeMissingId) { + const char* json = R"({ + "graphs": [ + { + "name": "test", + "nodes": [{"type": "input", "role": "source"}], + "edges": [] + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + EXPECT_FALSE(ValidateExpandedRootConfig(expanded, err)); +} + +TEST(ConfigSchemaTest, NodeMissingType) { + const char* json = R"({ + "graphs": [ + { + "name": "test", + "nodes": [{"id": "n1", "role": "source"}], + "edges": [] + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + EXPECT_FALSE(ValidateExpandedRootConfig(expanded, err)); +} + +TEST(ConfigSchemaTest, DuplicateNodeId) { + const char* json = R"({ + "graphs": [ + { + "name": "test", + "nodes": [ + {"id": "same", "type": "t1", "role": "source"}, + {"id": "same", "type": "t2", "role": "sink"} + ], + "edges": [] + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + EXPECT_FALSE(ValidateExpandedRootConfig(expanded, err)); +} + +TEST(ConfigSchemaTest, EdgeReferencesUnknownNode) { + const char* json = R"({ + "graphs": [ + { + "name": "test", + "nodes": [{"id": "n1", "type": "t", "role": "source"}], + "edges": [["n1", "unknown"]] + } + ] + })"; + + SimpleJson root; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, root, err)); + + SimpleJson expanded; + EXPECT_TRUE(ExpandRootConfig(root, expanded, err)); + EXPECT_FALSE(ValidateExpandedRootConfig(expanded, err)); +} + +} // namespace +} // namespace rk3588 diff --git a/tests/test_result.cpp b/tests/test_result.cpp new file mode 100644 index 0000000..301889d --- /dev/null +++ b/tests/test_result.cpp @@ -0,0 +1,137 @@ +#include + +#include +#include + +#include "utils/result.h" + +namespace rk3588 { +namespace { + +TEST(ResultTest, VoidResultOk) { + Status s = Status::Ok(); + EXPECT_TRUE(s.IsOk()); + EXPECT_FALSE(s.Failed()); + EXPECT_TRUE(static_cast(s)); + EXPECT_TRUE(s.ErrMessage().empty()); +} + +TEST(ResultTest, VoidResultFail) { + Status s = Status::Fail("something went wrong"); + EXPECT_FALSE(s.IsOk()); + EXPECT_TRUE(s.Failed()); + EXPECT_FALSE(static_cast(s)); + EXPECT_EQ(s.ErrMessage(), "something went wrong"); +} + +TEST(ResultTest, VoidResultFailWithCode) { + Status s = Status::Fail("error", 42); + EXPECT_FALSE(s.IsOk()); + EXPECT_EQ(s.Err().Code(), 42); + EXPECT_EQ(s.ErrMessage(), "error"); +} + +TEST(ResultTest, ValueResultOk) { + Result r = 42; + EXPECT_TRUE(r.Ok()); + EXPECT_FALSE(r.Failed()); + EXPECT_TRUE(static_cast(r)); + EXPECT_EQ(r.Value(), 42); +} + +TEST(ResultTest, ValueResultFail) { + Result r = Error("failed to compute"); + EXPECT_FALSE(r.Ok()); + EXPECT_TRUE(r.Failed()); + EXPECT_FALSE(static_cast(r)); + EXPECT_EQ(r.ErrMessage(), "failed to compute"); +} + +TEST(ResultTest, ValueResultValueOr) { + Result ok = 100; + Result fail = Error("oops"); + + EXPECT_EQ(ok.ValueOr(0), 100); + EXPECT_EQ(fail.ValueOr(0), 0); +} + +TEST(ResultTest, ValueResultTake) { + Result r = std::string("hello"); + EXPECT_TRUE(r.Ok()); + std::string s = std::move(r).Take(); + EXPECT_EQ(s, "hello"); +} + +TEST(ResultTest, StringResult) { + Result ok = std::string("success"); + Result fail = Error("failure"); + + EXPECT_TRUE(ok.Ok()); + EXPECT_EQ(ok.Value(), "success"); + + EXPECT_TRUE(fail.Failed()); + EXPECT_EQ(fail.ErrMessage(), "failure"); +} + +TEST(ResultTest, MakeResultHelper) { + auto r = MakeResult(123); + EXPECT_TRUE(r.Ok()); + EXPECT_EQ(r.Value(), 123); +} + +TEST(ResultTest, MakeErrorHelper) { + auto r = MakeError("test error"); + EXPECT_TRUE(r.Failed()); + EXPECT_EQ(r.ErrMessage(), "test error"); +} + +TEST(ErrorTest, Basic) { + Error e1; + EXPECT_TRUE(e1.Empty()); + EXPECT_FALSE(static_cast(e1)); + + Error e2("message", 100); + EXPECT_FALSE(e2.Empty()); + EXPECT_TRUE(static_cast(e2)); + EXPECT_EQ(e2.Message(), "message"); + EXPECT_EQ(e2.Code(), 100); +} + +TEST(ResultTest, OkStatusHelper) { + Status s = OkStatus(); + EXPECT_TRUE(s.IsOk()); +} + +TEST(ResultTest, FailStatusHelper) { + Status s = FailStatus("test"); + EXPECT_TRUE(s.Failed()); + EXPECT_EQ(s.ErrMessage(), "test"); +} + +// Test with complex types +struct ComplexType { + int x; + std::string s; + bool operator==(const ComplexType& o) const { return x == o.x && s == o.s; } +}; + +TEST(ResultTest, ComplexType) { + Result ok = ComplexType{42, "test"}; + EXPECT_TRUE(ok.Ok()); + EXPECT_EQ(ok.Value().x, 42); + EXPECT_EQ(ok.Value().s, "test"); + + Result fail = Error("complex error"); + EXPECT_TRUE(fail.Failed()); +} + +// Test move semantics +TEST(ResultTest, MoveSemantics) { + Result> r = std::make_unique(42); + EXPECT_TRUE(r.Ok()); + auto ptr = std::move(r).Take(); + EXPECT_EQ(*ptr, 42); +} + +} // namespace +} // namespace rk3588 diff --git a/tests/test_simple_json.cpp b/tests/test_simple_json.cpp new file mode 100644 index 0000000..e5b2569 --- /dev/null +++ b/tests/test_simple_json.cpp @@ -0,0 +1,313 @@ +#include + +#include + +#include "utils/simple_json.h" + +namespace rk3588 { +namespace { + +TEST(SimpleJsonTest, ParseNull) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("null", v, err)); + EXPECT_TRUE(v.IsNull()); +} + +TEST(SimpleJsonTest, ParseBoolTrue) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("true", v, err)); + EXPECT_TRUE(v.IsBool()); + EXPECT_TRUE(v.AsBool()); +} + +TEST(SimpleJsonTest, ParseBoolFalse) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("false", v, err)); + EXPECT_TRUE(v.IsBool()); + EXPECT_FALSE(v.AsBool()); +} + +TEST(SimpleJsonTest, ParseInteger) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("42", v, err)); + EXPECT_TRUE(v.IsNumber()); + EXPECT_EQ(v.AsNumber(), 42.0); + EXPECT_EQ(v.AsInt(), 42); +} + +TEST(SimpleJsonTest, ParseNegativeNumber) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("-123", v, err)); + EXPECT_TRUE(v.IsNumber()); + EXPECT_EQ(v.AsInt(), -123); +} + +TEST(SimpleJsonTest, ParseFloat) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("3.14159", v, err)); + EXPECT_TRUE(v.IsNumber()); + EXPECT_NEAR(v.AsNumber(), 3.14159, 0.00001); +} + +TEST(SimpleJsonTest, ParseScientific) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("1.5e10", v, err)); + EXPECT_TRUE(v.IsNumber()); + EXPECT_NEAR(v.AsNumber(), 1.5e10, 1e6); +} + +TEST(SimpleJsonTest, ParseString) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("\"hello world\"", v, err)); + EXPECT_TRUE(v.IsString()); + EXPECT_EQ(v.AsString(), "hello world"); +} + +TEST(SimpleJsonTest, ParseStringEscapes) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(R"("line1\nline2\ttab\\slash\"quote")", v, err)); + EXPECT_TRUE(v.IsString()); + EXPECT_EQ(v.AsString(), "line1\nline2\ttab\\slash\"quote"); +} + +TEST(SimpleJsonTest, ParseEmptyArray) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("[]", v, err)); + EXPECT_TRUE(v.IsArray()); + EXPECT_TRUE(v.AsArray().empty()); +} + +TEST(SimpleJsonTest, ParseArray) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("[1, 2, 3]", v, err)); + EXPECT_TRUE(v.IsArray()); + EXPECT_EQ(v.AsArray().size(), 3u); + EXPECT_EQ(v.AsArray()[0].AsInt(), 1); + EXPECT_EQ(v.AsArray()[1].AsInt(), 2); + EXPECT_EQ(v.AsArray()[2].AsInt(), 3); +} + +TEST(SimpleJsonTest, ParseMixedArray) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("[1, \"two\", true, null]", v, err)); + EXPECT_TRUE(v.IsArray()); + EXPECT_EQ(v.AsArray().size(), 4u); + EXPECT_TRUE(v.AsArray()[0].IsNumber()); + EXPECT_TRUE(v.AsArray()[1].IsString()); + EXPECT_TRUE(v.AsArray()[2].IsBool()); + EXPECT_TRUE(v.AsArray()[3].IsNull()); +} + +TEST(SimpleJsonTest, ParseEmptyObject) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("{}", v, err)); + EXPECT_TRUE(v.IsObject()); + EXPECT_TRUE(v.AsObject().empty()); +} + +TEST(SimpleJsonTest, ParseObject) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(R"({"name": "test", "value": 42})", v, err)); + EXPECT_TRUE(v.IsObject()); + EXPECT_EQ(v.AsObject().size(), 2u); + EXPECT_EQ(v.ValueOr("name", ""), "test"); + EXPECT_EQ(v.ValueOr("value", 0), 42); +} + +TEST(SimpleJsonTest, ParseNestedObject) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(R"({"outer": {"inner": "value"}})", v, err)); + EXPECT_TRUE(v.IsObject()); + const SimpleJson* outer = v.Find("outer"); + ASSERT_NE(outer, nullptr); + EXPECT_TRUE(outer->IsObject()); + EXPECT_EQ(outer->ValueOr("inner", ""), "value"); +} + +TEST(SimpleJsonTest, ParseNestedArray) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("[[1, 2], [3, 4]]", v, err)); + EXPECT_TRUE(v.IsArray()); + EXPECT_EQ(v.AsArray().size(), 2u); + EXPECT_EQ(v.AsArray()[0].AsArray().size(), 2u); + EXPECT_EQ(v.AsArray()[0].AsArray()[0].AsInt(), 1); +} + +TEST(SimpleJsonTest, FindNonExistent) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(R"({"a": 1})", v, err)); + EXPECT_EQ(v.Find("b"), nullptr); +} + +TEST(SimpleJsonTest, ValueOrDefaults) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(R"({"a": 1})", v, err)); + EXPECT_EQ(v.ValueOr("a", 0), 1); + EXPECT_EQ(v.ValueOr("b", 99), 99); + EXPECT_EQ(v.ValueOr("c", "default"), "default"); + EXPECT_EQ(v.ValueOr("d", true), true); +} + +TEST(SimpleJsonTest, StringToBoolCoercion) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(R"({"a": "true", "b": "false", "c": "yes", "d": "no"})", v, err)); + EXPECT_TRUE(v.ValueOr("a", false)); + EXPECT_FALSE(v.ValueOr("b", true)); + EXPECT_TRUE(v.ValueOr("c", false)); + EXPECT_FALSE(v.ValueOr("d", true)); +} + +TEST(SimpleJsonTest, StringToNumberCoercion) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(R"({"a": "42", "b": "3.14", "c": "not a number"})", v, err)); + EXPECT_EQ(v.ValueOr("a", 0), 42); + EXPECT_NEAR(v.ValueOr("b", 0.0), 3.14, 0.001); + EXPECT_EQ(v.ValueOr("c", -1.0), -1.0); // returns default +} + +TEST(SimpleJsonTest, ParseError_UnterminatedString) { + SimpleJson v; + std::string err; + EXPECT_FALSE(ParseSimpleJson("\"unterminated", v, err)); + EXPECT_FALSE(err.empty()); +} + +TEST(SimpleJsonTest, ParseError_UnterminatedArray) { + SimpleJson v; + std::string err; + EXPECT_FALSE(ParseSimpleJson("[1, 2, 3", v, err)); + EXPECT_FALSE(err.empty()); +} + +TEST(SimpleJsonTest, ParseError_UnterminatedObject) { + SimpleJson v; + std::string err; + EXPECT_FALSE(ParseSimpleJson("{\"a\": 1", v, err)); + EXPECT_FALSE(err.empty()); +} + +TEST(SimpleJsonTest, ParseError_TrailingContent) { + SimpleJson v; + std::string err; + EXPECT_FALSE(ParseSimpleJson("{\"a\": 1} extra", v, err)); + EXPECT_FALSE(err.empty()); +} + +TEST(SimpleJsonTest, ParseError_InvalidValue) { + SimpleJson v; + std::string err; + EXPECT_FALSE(ParseSimpleJson("undefined", v, err)); + EXPECT_FALSE(err.empty()); +} + +TEST(SimpleJsonTest, ParseError_MissingColon) { + SimpleJson v; + std::string err; + EXPECT_FALSE(ParseSimpleJson("{\"a\" 1}", v, err)); + EXPECT_FALSE(err.empty()); +} + +TEST(SimpleJsonTest, ParseWhitespace) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(" \n\t { \"a\" : 1 } \n ", v, err)); + EXPECT_TRUE(v.IsObject()); + EXPECT_EQ(v.ValueOr("a", 0), 1); +} + +TEST(SimpleJsonTest, ComplexDocument) { + const char* json = R"({ + "name": "test_config", + "version": 1.5, + "enabled": true, + "settings": { + "timeout": 30, + "retries": 3 + }, + "items": [ + {"id": 1, "name": "first"}, + {"id": 2, "name": "second"} + ], + "tags": ["a", "b", "c"] + })"; + + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson(json, v, err)); + EXPECT_TRUE(v.IsObject()); + + EXPECT_EQ(v.ValueOr("name", ""), "test_config"); + EXPECT_NEAR(v.ValueOr("version", 0.0), 1.5, 0.01); + EXPECT_TRUE(v.ValueOr("enabled", false)); + + const SimpleJson* settings = v.Find("settings"); + ASSERT_NE(settings, nullptr); + EXPECT_EQ(settings->ValueOr("timeout", 0), 30); + EXPECT_EQ(settings->ValueOr("retries", 0), 3); + + const SimpleJson* items = v.Find("items"); + ASSERT_NE(items, nullptr); + EXPECT_EQ(items->AsArray().size(), 2u); + EXPECT_EQ(items->AsArray()[0].ValueOr("id", 0), 1); + EXPECT_EQ(items->AsArray()[1].ValueOr("name", ""), "second"); + + const SimpleJson* tags = v.Find("tags"); + ASSERT_NE(tags, nullptr); + EXPECT_EQ(tags->AsArray().size(), 3u); +} + +TEST(SimpleJsonTest, AsArrayOnNonArray) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("42", v, err)); + // Should return empty array reference + EXPECT_TRUE(v.AsArray().empty()); +} + +TEST(SimpleJsonTest, AsObjectOnNonObject) { + SimpleJson v; + std::string err; + EXPECT_TRUE(ParseSimpleJson("[1,2,3]", v, err)); + // Should return empty object reference + EXPECT_TRUE(v.AsObject().empty()); +} + +TEST(SimpleJsonTest, ConstructFromValues) { + SimpleJson null_val(nullptr); + EXPECT_TRUE(null_val.IsNull()); + + SimpleJson bool_val(true); + EXPECT_TRUE(bool_val.IsBool()); + EXPECT_TRUE(bool_val.AsBool()); + + SimpleJson num_val(42.0); + EXPECT_TRUE(num_val.IsNumber()); + EXPECT_EQ(num_val.AsNumber(), 42.0); + + SimpleJson str_val(std::string("hello")); + EXPECT_TRUE(str_val.IsString()); + EXPECT_EQ(str_val.AsString(), "hello"); +} + +} // namespace +} // namespace rk3588 diff --git a/tests/test_spsc_queue.cpp b/tests/test_spsc_queue.cpp new file mode 100644 index 0000000..8529fb4 --- /dev/null +++ b/tests/test_spsc_queue.cpp @@ -0,0 +1,283 @@ +#include + +#include +#include +#include +#include + +#include "utils/spsc_queue.h" + +namespace rk3588 { +namespace { + +TEST(SpscQueueTest, BasicPushPop) { + SpscQueue q(4, QueueDropStrategy::DropOldest); + EXPECT_EQ(q.Capacity(), 4u); + EXPECT_EQ(q.Size(), 0u); + + EXPECT_TRUE(q.Push(1)); + EXPECT_TRUE(q.Push(2)); + EXPECT_TRUE(q.Push(3)); + EXPECT_EQ(q.Size(), 3u); + + int v = 0; + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 1); + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 2); + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 3); + EXPECT_EQ(q.Size(), 0u); +} + +TEST(SpscQueueTest, TryPopEmpty) { + SpscQueue q(4, QueueDropStrategy::DropOldest); + int v = -1; + EXPECT_FALSE(q.TryPop(v)); + EXPECT_EQ(v, -1); // unchanged +} + +TEST(SpscQueueTest, DropOldestStrategy) { + SpscQueue q(2, QueueDropStrategy::DropOldest); + + EXPECT_TRUE(q.Push(1)); + EXPECT_TRUE(q.Push(2)); + EXPECT_EQ(q.Size(), 2u); + EXPECT_EQ(q.DroppedCount(), 0u); + + // Queue full, should drop oldest (1) + EXPECT_TRUE(q.Push(3)); + EXPECT_EQ(q.DroppedCount(), 1u); + + int v = 0; + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 2); + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 3); +} + +TEST(SpscQueueTest, DropNewestStrategy) { + SpscQueue q(2, QueueDropStrategy::DropNewest); + + EXPECT_TRUE(q.Push(1)); + EXPECT_TRUE(q.Push(2)); + EXPECT_EQ(q.DroppedCount(), 0u); + + // Queue full, should drop newest (the one being pushed) + EXPECT_TRUE(q.Push(3)); + EXPECT_EQ(q.DroppedCount(), 1u); + + int v = 0; + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 1); + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 2); + EXPECT_FALSE(q.TryPop(v)); // 3 was dropped +} + +TEST(SpscQueueTest, BlockingStrategy) { + SpscQueue q(2, QueueDropStrategy::Block); + + EXPECT_TRUE(q.Push(1)); + EXPECT_TRUE(q.Push(2)); + + // Start a thread that will pop after a delay + std::thread popper([&q]() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + int v; + q.TryPop(v); + }); + + // This should block until popper makes space + auto start = std::chrono::steady_clock::now(); + EXPECT_TRUE(q.Push(3)); + auto elapsed = std::chrono::steady_clock::now() - start; + EXPECT_GE(std::chrono::duration_cast(elapsed).count(), 40); + + popper.join(); + + int v; + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 2); + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 3); +} + +TEST(SpscQueueTest, PopWithTimeout) { + SpscQueue q(4, QueueDropStrategy::DropOldest); + + // Pop on empty queue should timeout + int v = -1; + auto start = std::chrono::steady_clock::now(); + bool got = q.Pop(v, std::chrono::milliseconds(50)); + auto elapsed = std::chrono::steady_clock::now() - start; + + EXPECT_FALSE(got); + EXPECT_GE(std::chrono::duration_cast(elapsed).count(), 40); +} + +TEST(SpscQueueTest, PopWithTimeoutSuccess) { + SpscQueue q(4, QueueDropStrategy::DropOldest); + + std::thread pusher([&q]() { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + q.Push(42); + }); + + int v = -1; + bool got = q.Pop(v, std::chrono::milliseconds(200)); + EXPECT_TRUE(got); + EXPECT_EQ(v, 42); + + pusher.join(); +} + +TEST(SpscQueueTest, Stop) { + SpscQueue q(4, QueueDropStrategy::Block); + q.Push(1); + + EXPECT_FALSE(q.IsStopped()); + q.Stop(); + EXPECT_TRUE(q.IsStopped()); + + // Push should fail after stop + EXPECT_FALSE(q.Push(2)); + + // Pop should still work for existing items + int v = 0; + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 1); + + // After draining, pop should fail + EXPECT_FALSE(q.TryPop(v)); +} + +TEST(SpscQueueTest, StopUnblocksWaiters) { + SpscQueue q(4, QueueDropStrategy::Block); + + std::atomic popped{false}; + std::thread waiter([&q, &popped]() { + int v; + q.Pop(v); // Will block + popped = true; + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + EXPECT_FALSE(popped.load()); + + q.Stop(); + + waiter.join(); + EXPECT_TRUE(popped.load()); +} + +TEST(SpscQueueTest, Stats) { + SpscQueue q(2, QueueDropStrategy::DropOldest); + + q.Push(1); + q.Push(2); + q.Push(3); // drops 1 + + int v; + q.TryPop(v); + + auto stats = q.GetStats(); + EXPECT_EQ(stats.capacity, 2u); + EXPECT_EQ(stats.size, 1u); + EXPECT_EQ(stats.pushed, 3u); + EXPECT_EQ(stats.popped, 1u); + EXPECT_EQ(stats.dropped, 1u); + EXPECT_FALSE(stats.stopped); +} + +TEST(SpscQueueTest, TryPopBatch) { + SpscQueue q(10, QueueDropStrategy::DropOldest); + + for (int i = 0; i < 5; ++i) { + q.Push(i); + } + + std::vector batch; + bool got = q.TryPopBatch(batch, 3); + EXPECT_TRUE(got); + EXPECT_EQ(batch.size(), 3u); + EXPECT_EQ(batch[0], 0); + EXPECT_EQ(batch[1], 1); + EXPECT_EQ(batch[2], 2); + + got = q.TryPopBatch(batch, 10); // request more than available + EXPECT_TRUE(got); + EXPECT_EQ(batch.size(), 2u); + EXPECT_EQ(batch[0], 3); + EXPECT_EQ(batch[1], 4); + + got = q.TryPopBatch(batch, 5); // empty queue + EXPECT_FALSE(got); + EXPECT_TRUE(batch.empty()); +} + +TEST(SpscQueueTest, OnDataAvailableCallback) { + SpscQueue q(4, QueueDropStrategy::DropOldest); + + std::atomic callback_count{0}; + q.SetOnDataAvailable([&callback_count]() { callback_count.fetch_add(1); }); + + q.Push(1); // empty -> non-empty, should trigger + EXPECT_EQ(callback_count.load(), 1); + + q.Push(2); // not empty -> still not empty, no trigger + EXPECT_EQ(callback_count.load(), 1); + + int v; + q.TryPop(v); + q.TryPop(v); // now empty + + q.Push(3); // empty -> non-empty, should trigger + EXPECT_EQ(callback_count.load(), 2); +} + +TEST(SpscQueueTest, ConcurrentPushPop) { + SpscQueue q(100, QueueDropStrategy::DropOldest); + constexpr int kNumItems = 10000; + + std::atomic sum{0}; + + std::thread producer([&q]() { + for (int i = 1; i <= kNumItems; ++i) { + q.Push(i); + } + }); + + std::thread consumer([&q, &sum]() { + int received = 0; + while (received < kNumItems) { + int v; + if (q.TryPop(v)) { + sum.fetch_add(v); + ++received; + } else { + std::this_thread::yield(); + } + } + }); + + producer.join(); + consumer.join(); + + // Sum of 1 to kNumItems + int expected = kNumItems * (kNumItems + 1) / 2; + EXPECT_EQ(sum.load(), expected); +} + +TEST(SpscQueueTest, ZeroCapacityBecomesOne) { + SpscQueue q(0, QueueDropStrategy::DropOldest); + EXPECT_EQ(q.Capacity(), 1u); + + EXPECT_TRUE(q.Push(42)); + int v; + EXPECT_TRUE(q.TryPop(v)); + EXPECT_EQ(v, 42); +} + +} // namespace +} // namespace rk3588