feat: add mtran ROS2 translation service
This commit is contained in:
301
src/mtran/test/test_translator_node.cpp
Normal file
301
src/mtran/test/test_translator_node.cpp
Normal file
@@ -0,0 +1,301 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user