forked from zbw/yiliao2026
302 lines
8.6 KiB
C++
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
|