Files
yiliao2026/src/mtran/test/test_translator_node.cpp

302 lines
8.6 KiB
C++

#include <gtest/gtest.h>
#include <rclcpp/rclcpp.hpp>
#include <chrono>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#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<std::mutex> lock(mutex_);
++health_calls_;
return healthy_;
}
TranslationResult translate(const std::string & text) override
{
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex_);
healthy_ = healthy;
}
void set_warmup_result(TranslationResult result)
{
std::lock_guard<std::mutex> lock(mutex_);
warmup_result_ = std::move(result);
}
void set_user_result(TranslationResult result)
{
std::lock_guard<std::mutex> lock(mutex_);
user_result_ = std::move(result);
}
void set_all_translation_results(TranslationResult result)
{
std::lock_guard<std::mutex> lock(mutex_);
warmup_result_ = result;
user_result_ = std::move(result);
}
std::size_t user_call_count() const
{
std::lock_guard<std::mutex> lock(mutex_);
return user_inputs_.size();
}
int warmup_call_count() const
{
std::lock_guard<std::mutex> 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<std::string> 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<FakeTranslationClient>();
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<TranslatorNode>(options, fake_);
client_node_ = std::make_shared<rclcpp::Node>("mtran_test_client");
client_ = client_node_->create_client<Service>("/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<Service::Request>();
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<FakeTranslationClient> fake_;
std::shared_ptr<TranslatorNode> server_;
rclcpp::Node::SharedPtr client_node_;
rclcpp::Client<Service>::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