#include #include #include #include #include #include #include #include #include #include "mtran/http_translation_client.hpp" #include "mtran/srv/translate_english_to_chinese.hpp" #include "mtran/translator_node.hpp" using namespace std::chrono_literals; namespace mtran { namespace { class FakeTranslationClient final : public TranslationClient { public: bool health() override { std::lock_guard lock(mutex_); ++health_calls_; return healthy_; } TranslationResult translate(const std::string & text) override { std::lock_guard lock(mutex_); if (text == "Hello.") { ++warmup_calls_; return warmup_result_; } user_inputs_.push_back(text); return user_result_; } void set_healthy(bool healthy) { std::lock_guard lock(mutex_); healthy_ = healthy; } void set_warmup_result(TranslationResult result) { std::lock_guard lock(mutex_); warmup_result_ = std::move(result); } void set_user_result(TranslationResult result) { std::lock_guard lock(mutex_); user_result_ = std::move(result); } void set_all_translation_results(TranslationResult result) { std::lock_guard lock(mutex_); warmup_result_ = result; user_result_ = std::move(result); } std::size_t user_call_count() const { std::lock_guard lock(mutex_); return user_inputs_.size(); } int warmup_call_count() const { std::lock_guard lock(mutex_); return warmup_calls_; } private: mutable std::mutex mutex_; bool healthy_{true}; int health_calls_{0}; int warmup_calls_{0}; TranslationResult warmup_result_{true, "你好。", ClientError::kNone}; TranslationResult user_result_{true, "机器人已完成检查。", ClientError::kNone}; std::vector user_inputs_; }; class TranslatorNodeTest : public ::testing::Test { protected: using Service = mtran::srv::TranslateEnglishToChinese; static void SetUpTestSuite() { if (!rclcpp::ok()) { int argc = 0; rclcpp::init(argc, nullptr); } } static void TearDownTestSuite() { rclcpp::shutdown(); } void SetUp() override { fake_ = std::make_shared(); rclcpp::NodeOptions options; options.parameter_overrides( { rclcpp::Parameter("health_retry_ms", 1), rclcpp::Parameter("startup_timeout_ms", 1000), rclcpp::Parameter("keep_warm_interval_s", 1), rclcpp::Parameter("max_input_characters", 512), }); server_ = std::make_shared(options, fake_); client_node_ = std::make_shared("mtran_test_client"); client_ = client_node_->create_client("/translate_en_to_zh"); executor_.add_node(server_); executor_.add_node(client_node_); } void TearDown() override { executor_.remove_node(client_node_); executor_.remove_node(server_); client_.reset(); client_node_.reset(); server_.reset(); fake_.reset(); } bool spin_until_service(std::chrono::milliseconds timeout = 500ms) { const auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { executor_.spin_some(); if (client_->service_is_ready()) { return true; } std::this_thread::sleep_for(1ms); } return false; } Service::Response::SharedPtr call(const std::string & text) { auto request = std::make_shared(); request->text = text; auto future = client_->async_send_request(request); EXPECT_EQ(executor_.spin_until_future_complete(future, 1s), rclcpp::FutureReturnCode::SUCCESS); return future.get(); } rclcpp::executors::SingleThreadedExecutor executor_; std::shared_ptr fake_; std::shared_ptr server_; rclcpp::Node::SharedPtr client_node_; rclcpp::Client::SharedPtr client_; }; TEST_F(TranslatorNodeTest, AdvertisesServiceOnlyAfterHealthAndWarmup) { fake_->set_healthy(false); executor_.spin_some(); std::this_thread::sleep_for(10ms); executor_.spin_some(); EXPECT_FALSE(client_->service_is_ready()); fake_->set_healthy(true); EXPECT_TRUE(spin_until_service()); EXPECT_GE(fake_->warmup_call_count(), 1); } TEST_F(TranslatorNodeTest, ValidatesEmptyAndLengthBoundariesBeforeCallingClient) { ASSERT_TRUE(spin_until_service()); const auto empty = call(" "); EXPECT_FALSE(empty->success); EXPECT_TRUE(empty->translation.empty()); EXPECT_EQ(empty->error, "INVALID_INPUT"); const auto at_limit = call(std::string(512, 'a')); EXPECT_TRUE(at_limit->success); EXPECT_EQ(at_limit->translation, "机器人已完成检查。"); EXPECT_TRUE(at_limit->error.empty()); const auto over_limit = call(std::string(513, 'a')); EXPECT_FALSE(over_limit->success); EXPECT_TRUE(over_limit->translation.empty()); EXPECT_EQ(over_limit->error, "INPUT_TOO_LONG"); EXPECT_EQ(fake_->user_call_count(), 1U); } TEST_F(TranslatorNodeTest, MapsTranslationClientOutcomesToServiceErrors) { ASSERT_TRUE(spin_until_service()); fake_->set_user_result({false, "", ClientError::kTimeout}); EXPECT_EQ(call("timeout")->error, "TIMEOUT"); fake_->set_user_result({false, "", ClientError::kUpstream}); EXPECT_EQ(call("upstream")->error, "UPSTREAM_ERROR"); fake_->set_user_result({false, "", ClientError::kInvalidResponse}); EXPECT_EQ(call("invalid")->error, "INVALID_RESPONSE"); } TEST_F(TranslatorNodeTest, RecoversAfterUnavailableSidecarIsHealthyAndWarmAgain) { ASSERT_TRUE(spin_until_service()); fake_->set_user_result({false, "", ClientError::kUnavailable}); const auto unavailable = call("first request"); EXPECT_FALSE(unavailable->success); EXPECT_EQ(unavailable->error, "NOT_READY"); fake_->set_healthy(false); fake_->set_user_result({true, "恢复完成", ClientError::kNone}); const auto while_down = call("second request"); EXPECT_FALSE(while_down->success); EXPECT_EQ(while_down->error, "NOT_READY"); const auto warmups_before_recovery = fake_->warmup_call_count(); fake_->set_healthy(true); const auto deadline = std::chrono::steady_clock::now() + 500ms; while (fake_->warmup_call_count() == warmups_before_recovery && std::chrono::steady_clock::now() < deadline) { executor_.spin_some(); std::this_thread::sleep_for(1ms); } const auto recovered = call("third request"); EXPECT_TRUE(recovered->success); EXPECT_EQ(recovered->translation, "恢复完成"); EXPECT_TRUE(recovered->error.empty()); } TEST_F(TranslatorNodeTest, HealthMonitorDetectsRestartBeforeNextUserRequest) { ASSERT_TRUE(spin_until_service()); const auto user_calls_before_outage = fake_->user_call_count(); fake_->set_healthy(false); const auto detection_deadline = std::chrono::steady_clock::now() + 100ms; while (std::chrono::steady_clock::now() < detection_deadline) { executor_.spin_some(); std::this_thread::sleep_for(1ms); } const auto during_outage = call("request during restart"); EXPECT_FALSE(during_outage->success); EXPECT_EQ(during_outage->error, "NOT_READY"); EXPECT_EQ(fake_->user_call_count(), user_calls_before_outage); fake_->set_healthy(true); const auto warmups_before_recovery = fake_->warmup_call_count(); const auto recovery_deadline = std::chrono::steady_clock::now() + 500ms; while (fake_->warmup_call_count() == warmups_before_recovery && std::chrono::steady_clock::now() < recovery_deadline) { executor_.spin_some(); std::this_thread::sleep_for(1ms); } const auto recovered = call("request after restart"); EXPECT_TRUE(recovered->success); EXPECT_EQ(recovered->translation, "机器人已完成检查。"); } TEST_F(TranslatorNodeTest, TransientKeepWarmFailureDoesNotMarkServiceNotReady) { ASSERT_TRUE(spin_until_service()); fake_->set_all_translation_results({false, "", ClientError::kTimeout}); const auto warmups_before_timer = fake_->warmup_call_count(); const auto deadline = std::chrono::steady_clock::now() + 1500ms; while (fake_->warmup_call_count() == warmups_before_timer && std::chrono::steady_clock::now() < deadline) { executor_.spin_some(); std::this_thread::sleep_for(2ms); } ASSERT_GT(fake_->warmup_call_count(), warmups_before_timer); fake_->set_user_result({true, "服务仍然可用", ClientError::kNone}); const auto response = call("request after transient failure"); EXPECT_TRUE(response->success); EXPECT_EQ(response->translation, "服务仍然可用"); EXPECT_TRUE(response->error.empty()); } } // namespace } // namespace mtran