cmvr-es/cmvr-es/service/grpc/tests/grpc_arm_service_test.cpp
xtkuang f4be2ffaaa feat(safety): unify device admission and recovery
Add the DeviceManager-owned safety coordinator, shared sensor/control policies, command ledger, service guards, generalized StopAll, and RecoverSafetyState. Preserve device-side hardware checks and AUBO hardware E-stop release reconciliation while keeping software E-stop independently latched.
2026-08-17 08:34:44 +08:00

1237 lines
42 KiB
C++

#include "service/grpc/include/grpc_arm_service.h"
#include <chrono>
#include <cstdio>
#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <google/protobuf/descriptor.h>
#include <grpcpp/grpcpp.h>
#include <gtest/gtest.h>
#include <unistd.h>
#include "cmvr/config/device_manager_config/device_manager_config.pb.h"
#include "manager/control_authority/include/control_authority_manager.h"
#include "manager/device_manager/include/device_manager.h"
#include "service/grpc/include/grpc_error_logging_interceptor.h"
#include "service/stop_all/include/stop_all_admission_gate.h"
namespace cmvr::service {
namespace {
class JsonCommandRobotArm final : public device::RobotArm {
public:
explicit JsonCommandRobotArm(std::string id)
{
id_ = std::move(id);
}
std::string typeName() const override { return "JsonCommandRobotArm"; }
bool executeJsonCommand(const std::string& request_json,
std::string& response_json) override
{
++execute_calls;
last_request_json = request_json;
response_json = next_response_json;
return next_success;
}
device::RobotModel getRobotModel() const override { return {}; }
std::size_t getDof() const override { return 0U; }
device::ArmState getRobotState() const override { return {}; }
device::JointGroupState getJointState() const override { return {}; }
device::CartesianPose getTcpPose(
device::FrameType = device::FrameType::Base) const override
{
return {};
}
device::RobotMode getRobotMode() const override
{
return device::RobotMode::Unknown;
}
device::SafetyMode getSafetyMode() const override
{
return device::SafetyMode::Unknown;
}
device::ControlMode getControlMode() const override
{
return device::ControlMode::None;
}
device::Result torqueOn() override { return torqueOn({}); }
device::Result torqueOn(
const std::function<bool()>& cancellation_requested) override
{
std::unique_lock lock(motion_mutex_);
++torque_on_calls_;
last_torque_on_had_cancellation_ =
static_cast<bool>(cancellation_requested);
if (!block_next_torque_on_) {
return device::Result::success();
}
block_next_torque_on_ = false;
blocking_torque_on_started_ = true;
torque_on_started_cv_.notify_all();
while (!release_blocking_torque_on_) {
lock.unlock();
const bool cancelled =
cancellation_requested && cancellation_requested();
lock.lock();
if (cancelled) {
last_torque_on_cancellation_requested_ = true;
if (fail_next_torque_on_cancellation_) {
fail_next_torque_on_cancellation_ = false;
return device::Result::failure(
device::ArmErrorCode::CommandFailed,
"simulated torqueOn safety termination failure");
}
return device::Result::failure(
device::ArmErrorCode::CommandRejected,
"simulated torqueOn cancellation");
}
torque_on_release_cv_.wait_for(
lock, std::chrono::milliseconds(5));
}
return device::Result::success();
}
device::Result torqueOff() override
{
std::lock_guard lock(motion_mutex_);
++torque_off_calls_;
return device::Result::success();
}
device::Result calibrateZeroQ(const std::string&) override
{
return device::Result::success();
}
device::Result emergencyStop() override
{
return device::Result::success();
}
device::Result protectiveStop() override
{
return device::Result::success();
}
device::Result setSpeedScaling(double) override
{
return device::Result::success();
}
double getSpeedScaling() const override { return 1.0; }
bool isProtectiveStopped() const override { return false; }
bool isEmergencyStopped() const override { return false; }
bool isFault() const override { return false; }
device::Result moveJ(const device::JointPositionCommand&,
const device::MotionOptions& options) override
{
return enterMotion("moveJ", move_j_calls_, options);
}
device::Result speedJ(const device::JointVelocityCommand&,
double,
double) override
{
return device::Result::success();
}
device::Result stopJ(double) override
{
return device::Result::success();
}
device::Result moveL(
const device::CartesianPose&,
const device::MotionOptions& options,
device::FrameType = device::FrameType::Base) override
{
return enterMotion("moveL", move_l_calls_, options);
}
device::Result speedL(
const device::CartesianVelocity&,
double,
double,
device::FrameType = device::FrameType::Base) override
{
return device::Result::success();
}
device::Result stopL(std::optional<double> = std::nullopt) override
{
return device::Result::success();
}
device::Result stopMotion() override
{
std::unique_lock lock(motion_mutex_);
++stop_motion_calls_;
if (block_next_stop_) {
block_next_stop_ = false;
blocking_stop_started_ = true;
stop_started_cv_.notify_all();
stop_release_cv_.wait(
lock,
[this]() { return release_blocking_stop_; });
}
if (throw_next_stop_) {
throw_next_stop_ = false;
throw std::runtime_error("simulated stopMotion exception");
}
if (fail_next_stop_) {
fail_next_stop_ = false;
return device::Result::failure(
device::ArmErrorCode::CommandFailed,
"simulated stopMotion failure");
}
return device::Result::success();
}
void blockNextMotion()
{
std::lock_guard lock(motion_mutex_);
block_next_motion_ = true;
blocking_motion_started_ = false;
release_blocking_motion_ = false;
blocking_motion_name_.clear();
}
void blockNextTorqueOn()
{
std::lock_guard lock(motion_mutex_);
block_next_torque_on_ = true;
blocking_torque_on_started_ = false;
release_blocking_torque_on_ = false;
last_torque_on_cancellation_requested_ = false;
}
void failNextTorqueOnCancellation()
{
std::lock_guard lock(motion_mutex_);
fail_next_torque_on_cancellation_ = true;
}
bool waitForBlockingTorqueOn(
const std::chrono::milliseconds timeout)
{
std::unique_lock lock(motion_mutex_);
return torque_on_started_cv_.wait_for(
lock,
timeout,
[this]() { return blocking_torque_on_started_; });
}
void releaseBlockingTorqueOn()
{
{
std::lock_guard lock(motion_mutex_);
release_blocking_torque_on_ = true;
}
torque_on_release_cv_.notify_all();
}
bool waitForBlockingMotion(
const std::string& operation,
const std::chrono::milliseconds timeout)
{
std::unique_lock lock(motion_mutex_);
return motion_started_cv_.wait_for(
lock,
timeout,
[this, &operation]() {
return blocking_motion_started_ &&
blocking_motion_name_ == operation;
});
}
void releaseBlockingMotion()
{
{
std::lock_guard lock(motion_mutex_);
release_blocking_motion_ = true;
}
motion_release_cv_.notify_all();
}
void blockNextStopMotion()
{
std::lock_guard lock(motion_mutex_);
block_next_stop_ = true;
blocking_stop_started_ = false;
release_blocking_stop_ = false;
}
void failNextStopMotion()
{
std::lock_guard lock(motion_mutex_);
fail_next_stop_ = true;
}
void throwNextStopMotion()
{
std::lock_guard lock(motion_mutex_);
throw_next_stop_ = true;
}
bool waitForBlockingStop(const std::chrono::milliseconds timeout)
{
std::unique_lock lock(motion_mutex_);
return stop_started_cv_.wait_for(
lock,
timeout,
[this]() { return blocking_stop_started_; });
}
void releaseBlockingStop()
{
{
std::lock_guard lock(motion_mutex_);
release_blocking_stop_ = true;
}
stop_release_cv_.notify_all();
}
int moveJCalls() const
{
std::lock_guard lock(motion_mutex_);
return move_j_calls_;
}
int moveLCalls() const
{
std::lock_guard lock(motion_mutex_);
return move_l_calls_;
}
int stopMotionCalls() const
{
std::lock_guard lock(motion_mutex_);
return stop_motion_calls_;
}
int torqueOffCalls() const
{
std::lock_guard lock(motion_mutex_);
return torque_off_calls_;
}
int torqueOnCalls() const
{
std::lock_guard lock(motion_mutex_);
return torque_on_calls_;
}
bool lastTorqueOnHadCancellation() const
{
std::lock_guard lock(motion_mutex_);
return last_torque_on_had_cancellation_;
}
bool lastTorqueOnCancellationRequested() const
{
std::lock_guard lock(motion_mutex_);
return last_torque_on_cancellation_requested_;
}
bool lastMotionHadCancellation() const
{
std::lock_guard lock(motion_mutex_);
return last_motion_had_cancellation_;
}
bool lastMotionCancellationRequested() const
{
std::lock_guard lock(motion_mutex_);
return last_motion_cancellation_requested_;
}
device::Result startServoMode(const device::ServoOptions&) override
{
return device::Result::success();
}
device::Result servoJ(const device::JointPositionCommand&) override
{
return device::Result::success();
}
device::Result servoL(
const device::CartesianPose&,
device::FrameType = device::FrameType::Base) override
{
return device::Result::success();
}
device::Result servoSpeedJ(const device::JointVelocityCommand&) override
{
return device::Result::success();
}
device::Result servoSpeedL(
const device::CartesianVelocity&,
device::FrameType = device::FrameType::Base) override
{
return device::Result::success();
}
device::Result stopServoMode() override
{
return device::Result::success();
}
device::Result connect(const std::string&, int) override
{
return device::Result::success();
}
device::Result disconnect() override
{
return device::Result::success();
}
bool isConnected() const override { return true; }
device::Result powerOn() override { return device::Result::success(); }
device::Result powerOff() override { return device::Result::success(); }
device::Result brakeRelease() override
{
return device::Result::success();
}
device::Result shutdown() override
{
return device::Result::success();
}
device::Result clearFault() override
{
return device::Result::success();
}
device::Result unlockProtectiveStop() override
{
return device::Result::success();
}
device::Result loadProgram(const std::string&) override
{
return device::Result::success();
}
device::Result playProgram() override
{
return device::Result::success();
}
device::Result pauseProgram() override
{
return device::Result::success();
}
device::Result stopProgram() override
{
return device::Result::success();
}
std::vector<double> ik(const std::string&,
const std::string&,
const device::CartesianPose&) override
{
return {};
}
std::shared_ptr<cmvr::IKSolver> kinematicsSolver() const override
{
return nullptr;
}
device::CartesianPose fk(const std::string&,
const std::string&) override
{
return {};
}
device::CartesianPose fk(bool = true) override { return {}; }
device::CartesianVelocity getSpeedLCommandTwistBase() const override
{
return {};
}
bool busy() const override { return false; }
int execute_calls{0};
bool next_success{true};
std::string next_response_json;
std::string last_request_json;
private:
device::Result enterMotion(
const char* operation,
int& call_count,
const device::MotionOptions& options)
{
std::unique_lock lock(motion_mutex_);
++call_count;
last_motion_had_cancellation_ =
static_cast<bool>(options.cancellation_requested);
last_motion_cancellation_requested_ =
last_motion_had_cancellation_ &&
options.cancellation_requested();
if (!block_next_motion_) {
return device::Result::success();
}
block_next_motion_ = false;
blocking_motion_started_ = true;
blocking_motion_name_ = operation;
motion_started_cv_.notify_all();
motion_release_cv_.wait(
lock,
[this]() { return release_blocking_motion_; });
return device::Result::success();
}
mutable std::mutex motion_mutex_;
std::condition_variable motion_started_cv_;
std::condition_variable motion_release_cv_;
std::condition_variable stop_started_cv_;
std::condition_variable stop_release_cv_;
std::condition_variable torque_on_started_cv_;
std::condition_variable torque_on_release_cv_;
bool block_next_motion_{false};
bool blocking_motion_started_{false};
bool release_blocking_motion_{false};
bool block_next_stop_{false};
bool blocking_stop_started_{false};
bool release_blocking_stop_{false};
bool fail_next_stop_{false};
bool throw_next_stop_{false};
bool block_next_torque_on_{false};
bool blocking_torque_on_started_{false};
bool release_blocking_torque_on_{false};
bool fail_next_torque_on_cancellation_{false};
std::string blocking_motion_name_;
int move_j_calls_{0};
int move_l_calls_{0};
int stop_motion_calls_{0};
int torque_off_calls_{0};
int torque_on_calls_{0};
bool last_torque_on_had_cancellation_{false};
bool last_torque_on_cancellation_requested_{false};
bool last_motion_had_cancellation_{false};
bool last_motion_cancellation_requested_{false};
};
class JsonCommandNonArmDevice final : public device::AbstractDevice {
public:
explicit JsonCommandNonArmDevice(std::string id)
: AbstractDevice(std::move(id))
{
}
std::string typeName() const override { return "JsonCommandNonArmDevice"; }
bool executeJsonCommand(const std::string&,
std::string& response_json) override
{
++execute_calls;
response_json = R"({"success":true})";
return true;
}
int execute_calls{0};
};
class GrpcArmServiceTest : public ::testing::Test {
protected:
void SetUp() override
{
control::ControlAuthorityManager::instance().clear();
globalStopAllAdmissionGate().clearForTesting();
device::DeviceManager::destroyInstance();
config::DeviceManagerConfig config;
auto& manager = device::DeviceManager::getInstance(config);
left_arm_ = std::make_shared<JsonCommandRobotArm>("left_arm");
aubo_arm_ = std::make_shared<JsonCommandRobotArm>("aubo_arm");
non_arm_ = std::make_shared<JsonCommandNonArmDevice>("camera");
manager.registerDevice(left_arm_);
manager.registerDevice(aubo_arm_);
manager.registerDevice(non_arm_);
service_ = std::make_unique<gRPCArmServiceImpl>();
}
void TearDown() override
{
service_.reset();
non_arm_.reset();
aubo_arm_.reset();
left_arm_.reset();
device::DeviceManager::destroyInstance();
control::ControlAuthorityManager::instance().clear();
globalStopAllAdmissionGate().clearForTesting();
}
grpc::Status execute(const std::string& device_id,
const std::string& request_json,
api::JsonDeviceCommand_Feedback& response)
{
api::JsonDeviceCommand_Request request;
request.mutable_header()->set_device_id(device_id);
request.set_request_json(request_json);
grpc::ServerContext context;
return service_->ExecuteJsonCommand(&context, &request, &response);
}
struct MoveOutcome {
grpc::Status status;
bool response_success{false};
std::string response_error;
};
MoveOutcome moveJ(const std::string& device_id)
{
api::MoveJ_Request request;
request.mutable_header()->set_device_id(device_id);
request.mutable_target()->add_position(0.1);
api::MoveJ_Response response;
grpc::ServerContext context;
auto status = service_->moveJ(&context, &request, &response);
return {
std::move(status),
response.header().success(),
response.header().error_message()};
}
grpc::Status identifiedMoveJ(
const std::string& command_id,
const double position,
api::MoveJ_Response& response)
{
api::MoveJ_Request request;
auto* header = request.mutable_header();
header->set_device_id("aubo_arm");
header->set_command_id(command_id);
header->set_expected_service_instance_id(
device::DeviceManager::getInstance()
.safetyCoordinator()
.serviceInstanceId());
header->set_valid_for_ms(1000);
request.mutable_target()->add_position(position);
grpc::ServerContext context;
return service_->moveJ(&context, &request, &response);
}
MoveOutcome moveL(const std::string& device_id)
{
api::MoveL_Request request;
request.mutable_header()->set_device_id(device_id);
request.mutable_target()->set_x(0.1);
api::MoveL_Response response;
grpc::ServerContext context;
auto status = service_->moveL(&context, &request, &response);
return {
std::move(status),
response.header().success(),
response.header().error_message()};
}
grpc::Status stopMotion(
const std::string& device_id,
api::CommandHeader_Feedback& response)
{
api::CommandHeader_Request request;
request.set_device_id(device_id);
grpc::ServerContext context;
return service_->stopMotion(&context, &request, &response);
}
grpc::Status torqueOff(
const std::string& device_id,
api::CommandHeader_Feedback& response)
{
api::CommandHeader_Request request;
request.set_device_id(device_id);
grpc::ServerContext context;
return service_->torqueOff(&context, &request, &response);
}
MoveOutcome torqueOn(const std::string& device_id)
{
api::CommandHeader_Request request;
request.set_device_id(device_id);
api::CommandHeader_Feedback response;
grpc::ServerContext context;
auto status = service_->torqueOn(&context, &request, &response);
return {
std::move(status),
response.success(),
response.error_message()};
}
std::shared_ptr<JsonCommandRobotArm> left_arm_;
std::shared_ptr<JsonCommandRobotArm> aubo_arm_;
std::shared_ptr<JsonCommandNonArmDevice> non_arm_;
std::unique_ptr<gRPCArmServiceImpl> service_;
};
TEST(GrpcArmServiceDescriptorTest,
ExecuteJsonCommandBelongsOnlyToArmService)
{
const auto* pool = google::protobuf::DescriptorPool::generated_pool();
const auto* arm_service =
pool->FindServiceByName("cmvr.api.ArmService");
const auto* system_service =
pool->FindServiceByName("cmvr.api.SystemService");
ASSERT_NE(arm_service, nullptr);
ASSERT_NE(system_service, nullptr);
EXPECT_NE(arm_service->FindMethodByName("ExecuteJsonCommand"), nullptr);
EXPECT_EQ(system_service->FindMethodByName("ExecuteJsonCommand"), nullptr);
}
TEST_F(GrpcArmServiceTest, RoutesByHeaderDeviceIdAndForwardsSuccessfulJson)
{
const std::string request_json =
R"({"command":"cabinet_io","operation":"get_di","index":0})";
const std::string response_json =
R"({"success":true,"operation":"get_di","index":0,"value":false})";
aubo_arm_->next_response_json = response_json;
api::JsonDeviceCommand_Feedback response;
const auto status = execute("aubo_arm", request_json, response);
ASSERT_TRUE(status.ok()) << status.error_message();
EXPECT_TRUE(response.header().success());
EXPECT_TRUE(response.header().error_message().empty());
EXPECT_TRUE(response.header().has_timestamp());
EXPECT_GT(response.header().timestamp().seconds(), 0);
EXPECT_EQ(response.response_json(), response_json);
EXPECT_EQ(aubo_arm_->execute_calls, 1);
EXPECT_EQ(aubo_arm_->last_request_json, request_json);
EXPECT_EQ(left_arm_->execute_calls, 0);
}
TEST_F(GrpcArmServiceTest, ForwardsDeviceJsonFailureWithLegacyGrpcOkSemantics)
{
const std::string response_json =
R"({"success":false,"error_code":"not_connected"})";
aubo_arm_->next_success = false;
aubo_arm_->next_response_json = response_json;
api::JsonDeviceCommand_Feedback response;
const auto status = execute(
"aubo_arm",
R"({"command":"cabinet_io","operation":"get_do","index":0})",
response);
ASSERT_TRUE(status.ok()) << status.error_message();
EXPECT_FALSE(response.header().success());
EXPECT_EQ(response.header().error_message(), response_json);
EXPECT_TRUE(response.header().has_timestamp());
EXPECT_GT(response.header().timestamp().seconds(), 0);
EXPECT_EQ(response.response_json(), response_json);
EXPECT_EQ(aubo_arm_->execute_calls, 1);
EXPECT_EQ(left_arm_->execute_calls, 0);
}
TEST_F(GrpcArmServiceTest,
MissingOrNonArmIdReturnsBusinessFailureWithoutBackendDispatch)
{
api::JsonDeviceCommand_Feedback non_arm_response;
const auto non_arm_status = execute(
"camera", R"({"command":"cabinet_io"})", non_arm_response);
ASSERT_TRUE(non_arm_status.ok()) << non_arm_status.error_message();
EXPECT_FALSE(non_arm_response.header().success());
EXPECT_EQ(non_arm_response.header().error_message(),
"Device not found: camera");
EXPECT_TRUE(non_arm_response.header().has_timestamp());
EXPECT_TRUE(non_arm_response.response_json().empty());
EXPECT_EQ(non_arm_->execute_calls, 0);
EXPECT_EQ(aubo_arm_->execute_calls, 0);
EXPECT_EQ(left_arm_->execute_calls, 0);
api::JsonDeviceCommand_Feedback missing_response;
const auto missing_status = execute(
"missing_arm", R"({"command":"cabinet_io"})", missing_response);
ASSERT_TRUE(missing_status.ok()) << missing_status.error_message();
EXPECT_FALSE(missing_response.header().success());
EXPECT_EQ(missing_response.header().error_message(),
"Device not found: missing_arm");
EXPECT_TRUE(missing_response.header().has_timestamp());
EXPECT_TRUE(missing_response.response_json().empty());
EXPECT_EQ(non_arm_->execute_calls, 0);
EXPECT_EQ(aubo_arm_->execute_calls, 0);
EXPECT_EQ(left_arm_->execute_calls, 0);
}
TEST_F(GrpcArmServiceTest,
StopMotionRevokesBlockedMoveJLeaseBeforeMoveLReturns)
{
aubo_arm_->blockNextMotion();
auto blocked_move = std::async(
std::launch::async,
[this]() { return moveJ("aubo_arm"); });
const bool move_started = aubo_arm_->waitForBlockingMotion(
"moveJ", std::chrono::seconds(2));
MoveOutcome conflict;
MoveOutcome during_stop;
api::CommandHeader_Feedback torque_off_response;
grpc::Status torque_off_status;
api::CommandHeader_Feedback stop_response;
grpc::Status stop_status;
MoveOutcome before_retired_handler_release;
bool stop_started = false;
std::future<grpc::Status> blocked_stop;
std::future<grpc::Status> torque_off;
if (move_started) {
conflict = moveL("aubo_arm");
aubo_arm_->blockNextStopMotion();
blocked_stop = std::async(
std::launch::async,
[this, &stop_response]() {
return stopMotion("aubo_arm", stop_response);
});
stop_started = aubo_arm_->waitForBlockingStop(
std::chrono::seconds(2));
if (stop_started) {
torque_off = std::async(
std::launch::async,
[this, &torque_off_response]() {
return torqueOff("aubo_arm", torque_off_response);
});
during_stop = moveL("aubo_arm");
}
aubo_arm_->releaseBlockingStop();
before_retired_handler_release = moveL("aubo_arm");
}
// Keep the original RPC active until after the replacement MoveL has
// attempted to acquire control. This models a driver whose stopped motion
// takes time to unwind and guards the lease hand-off itself.
aubo_arm_->releaseBlockingMotion();
const auto original_move = blocked_move.get();
if (blocked_stop.valid()) {
stop_status = blocked_stop.get();
}
if (torque_off.valid()) {
torque_off_status = torque_off.get();
}
const auto resumed_move = moveL("aubo_arm");
ASSERT_TRUE(move_started);
ASSERT_TRUE(stop_started);
EXPECT_EQ(conflict.status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(conflict.response_error, conflict.status.error_message());
EXPECT_EQ(during_stop.status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(during_stop.response_error,
during_stop.status.error_message());
EXPECT_TRUE(torque_off_status.ok())
<< torque_off_status.error_message();
EXPECT_TRUE(torque_off_response.success())
<< torque_off_response.error_message();
EXPECT_TRUE(stop_status.ok()) << stop_status.error_message();
EXPECT_TRUE(stop_response.success())
<< stop_response.error_message();
EXPECT_EQ(
before_retired_handler_release.status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_TRUE(resumed_move.status.ok())
<< resumed_move.status.error_message();
EXPECT_TRUE(resumed_move.response_success)
<< resumed_move.response_error;
EXPECT_TRUE(original_move.status.ok())
<< original_move.status.error_message();
EXPECT_TRUE(original_move.response_success)
<< original_move.response_error;
EXPECT_EQ(aubo_arm_->moveJCalls(), 1);
EXPECT_EQ(aubo_arm_->moveLCalls(), 1);
EXPECT_EQ(aubo_arm_->stopMotionCalls(), 2);
EXPECT_EQ(aubo_arm_->torqueOffCalls(), 2);
}
TEST_F(GrpcArmServiceTest, MoveBindsLeaseRevocationCancellation)
{
const auto outcome = moveJ("aubo_arm");
ASSERT_TRUE(outcome.status.ok())
<< outcome.status.error_message();
EXPECT_TRUE(outcome.response_success) << outcome.response_error;
EXPECT_TRUE(aubo_arm_->lastMotionHadCancellation());
EXPECT_FALSE(aubo_arm_->lastMotionCancellationRequested());
}
TEST_F(GrpcArmServiceTest,
IdenticalCommandIdReplaysCachedResultWithoutRedispatch)
{
api::MoveJ_Response first;
api::MoveJ_Response retry;
const auto first_status = identifiedMoveJ(
"arm-movej-idempotency-1", 0.1, first);
const auto retry_status = identifiedMoveJ(
"arm-movej-idempotency-1", 0.1, retry);
ASSERT_TRUE(first_status.ok()) << first_status.error_message();
ASSERT_TRUE(retry_status.ok()) << retry_status.error_message();
EXPECT_TRUE(first.header().success());
EXPECT_TRUE(retry.header().success());
EXPECT_EQ(
retry.header().execution_state(),
api::COMMAND_EXECUTION_STATE_COMPLETED);
EXPECT_EQ(retry.header().command_id(), "arm-movej-idempotency-1");
EXPECT_EQ(aubo_arm_->moveJCalls(), 1);
}
TEST_F(GrpcArmServiceTest,
ReusedCommandIdWithDifferentPayloadIsRejectedWithoutRedispatch)
{
api::MoveJ_Response first;
api::MoveJ_Response conflict;
const auto first_status = identifiedMoveJ(
"arm-movej-conflict-1", 0.1, first);
const auto conflict_status = identifiedMoveJ(
"arm-movej-conflict-1", 0.2, conflict);
ASSERT_TRUE(first_status.ok()) << first_status.error_message();
EXPECT_EQ(
conflict_status.error_code(), grpc::StatusCode::ALREADY_EXISTS);
EXPECT_FALSE(conflict.header().success());
EXPECT_EQ(
conflict.header().reason_code(),
api::COMMAND_REASON_CODE_COMMAND_ID_CONFLICT);
EXPECT_EQ(aubo_arm_->moveJCalls(), 1);
}
TEST_F(GrpcArmServiceTest,
StopAllCancelsInFlightTorqueOnWithoutReportingSuccess)
{
aubo_arm_->blockNextTorqueOn();
auto blocked_torque_on = std::async(
std::launch::async,
[this]() { return torqueOn("aubo_arm"); });
const bool torque_on_started = aubo_arm_->waitForBlockingTorqueOn(
std::chrono::seconds(2));
auto& admission = globalStopAllAdmissionGate();
StopAllAdmissionGate::StopAllTicket stop_all_ticket;
if (torque_on_started) {
stop_all_ticket = admission.beginStopAll();
}
const bool cancelled_promptly =
blocked_torque_on.wait_for(std::chrono::seconds(2)) ==
std::future_status::ready;
if (!cancelled_promptly) {
aubo_arm_->releaseBlockingTorqueOn();
}
const auto outcome = blocked_torque_on.get();
ASSERT_TRUE(torque_on_started);
ASSERT_TRUE(stop_all_ticket.valid());
EXPECT_TRUE(cancelled_promptly);
EXPECT_EQ(outcome.status.error_code(), grpc::StatusCode::UNAVAILABLE);
EXPECT_FALSE(outcome.response_success);
EXPECT_EQ(outcome.response_error, outcome.status.error_message());
EXPECT_NE(outcome.response_error.find("StopAll"), std::string::npos);
EXPECT_EQ(aubo_arm_->torqueOnCalls(), 1);
EXPECT_TRUE(aubo_arm_->lastTorqueOnHadCancellation());
EXPECT_TRUE(aubo_arm_->lastTorqueOnCancellationRequested());
EXPECT_TRUE(admission.finishStopAll(stop_all_ticket, true));
}
TEST_F(GrpcArmServiceTest,
StopAllPreservesTorqueOnSafetyTerminationFailure)
{
aubo_arm_->blockNextTorqueOn();
aubo_arm_->failNextTorqueOnCancellation();
auto blocked_torque_on = std::async(
std::launch::async,
[this]() { return torqueOn("aubo_arm"); });
const bool torque_on_started = aubo_arm_->waitForBlockingTorqueOn(
std::chrono::seconds(2));
auto& admission = globalStopAllAdmissionGate();
StopAllAdmissionGate::StopAllTicket stop_all_ticket;
if (torque_on_started) {
stop_all_ticket = admission.beginStopAll();
}
const bool completed_promptly =
blocked_torque_on.wait_for(std::chrono::seconds(2)) ==
std::future_status::ready;
if (!completed_promptly) {
aubo_arm_->releaseBlockingTorqueOn();
}
const auto outcome = blocked_torque_on.get();
ASSERT_TRUE(torque_on_started);
ASSERT_TRUE(stop_all_ticket.valid());
EXPECT_TRUE(completed_promptly);
EXPECT_EQ(outcome.status.error_code(), grpc::StatusCode::INTERNAL);
EXPECT_FALSE(outcome.response_success);
EXPECT_EQ(
outcome.response_error,
"simulated torqueOn safety termination failure");
EXPECT_EQ(outcome.status.error_message(), outcome.response_error);
EXPECT_TRUE(aubo_arm_->lastTorqueOnCancellationRequested());
EXPECT_FALSE(admission.finishStopAll(stop_all_ticket, false));
}
TEST_F(GrpcArmServiceTest,
ClientCancellationPreservesTorqueOnSafetyTerminationFailure)
{
std::mutex records_mutex;
std::condition_variable records_changed;
std::vector<GrpcFailureRecord> records;
grpc::ServerBuilder builder;
const std::string socket_path =
"/tmp/cmvr_grpc_arm_service_test_" +
std::to_string(static_cast<long long>(::getpid())) + ".sock";
std::remove(socket_path.c_str());
const std::string address = "unix:" + socket_path;
builder.AddListeningPort(
address,
grpc::InsecureServerCredentials());
builder.RegisterService(service_.get());
std::vector<std::unique_ptr<
grpc::experimental::ServerInterceptorFactoryInterface>> factories;
factories.emplace_back(makeGrpcErrorLoggingInterceptorFactory(
[&records, &records_mutex, &records_changed](
const GrpcFailureRecord& record) {
{
std::lock_guard lock(records_mutex);
records.push_back(record);
}
records_changed.notify_all();
}));
builder.experimental().SetInterceptorCreators(std::move(factories));
auto server = builder.BuildAndStart();
ASSERT_NE(server, nullptr);
const auto channel = grpc::CreateChannel(
address,
grpc::InsecureChannelCredentials());
auto stub = api::ArmService::NewStub(channel);
grpc::ClientContext client_context;
client_context.set_deadline(
std::chrono::system_clock::now() + std::chrono::seconds(3));
api::CommandHeader_Request request;
request.set_device_id("aubo_arm");
api::CommandHeader_Feedback response;
grpc::Status client_status;
aubo_arm_->blockNextTorqueOn();
aubo_arm_->failNextTorqueOnCancellation();
std::thread client_call([&]() {
client_status = stub->torqueOn(
&client_context, request, &response);
});
const bool torque_on_started = aubo_arm_->waitForBlockingTorqueOn(
std::chrono::seconds(2));
client_context.TryCancel();
client_call.join();
bool failure_recorded = false;
{
std::unique_lock lock(records_mutex);
failure_recorded = records_changed.wait_for(
lock,
std::chrono::seconds(2),
[&records]() { return !records.empty(); });
}
if (!failure_recorded) {
aubo_arm_->releaseBlockingTorqueOn();
}
server->Shutdown();
server->Wait();
std::remove(socket_path.c_str());
ASSERT_TRUE(torque_on_started);
EXPECT_EQ(client_status.error_code(), grpc::StatusCode::CANCELLED);
ASSERT_TRUE(failure_recorded);
ASSERT_EQ(records.size(), 1U);
EXPECT_EQ(records.front().kind, GrpcFailureKind::GRPC_STATUS);
EXPECT_EQ(records.front().status_code, grpc::StatusCode::INTERNAL);
EXPECT_EQ(records.front().severity, GrpcFailureSeverity::ERROR);
EXPECT_EQ(
records.front().detail,
"simulated torqueOn safety termination failure");
EXPECT_TRUE(aubo_arm_->lastTorqueOnCancellationRequested());
}
TEST_F(GrpcArmServiceTest,
StopAllGateRejectsMutatingCommandsButAllowsReadsAndStops)
{
auto& admission = globalStopAllAdmissionGate();
const auto ticket = admission.beginStopAll();
ASSERT_TRUE(ticket.valid());
const auto rejected_move = moveJ("aubo_arm");
api::JsonDeviceCommand_Feedback json_response;
const auto json_status = execute(
"aubo_arm", R"({"command":"cabinet_io"})", json_response);
api::JointRequest state_request;
state_request.mutable_header()->set_device_id("aubo_arm");
api::JointResponse state_response;
grpc::ServerContext state_context;
const auto state_status = service_->getJointState(
&state_context, &state_request, &state_response);
api::CommandHeader_Feedback stop_response;
const auto stop_status = stopMotion("aubo_arm", stop_response);
EXPECT_EQ(
rejected_move.status.error_code(),
grpc::StatusCode::UNAVAILABLE);
EXPECT_FALSE(rejected_move.response_success);
EXPECT_EQ(aubo_arm_->moveJCalls(), 0);
EXPECT_EQ(json_status.error_code(), grpc::StatusCode::UNAVAILABLE);
EXPECT_FALSE(json_response.header().success());
EXPECT_EQ(aubo_arm_->execute_calls, 0);
EXPECT_TRUE(state_status.ok()) << state_status.error_message();
EXPECT_TRUE(state_response.header().success());
EXPECT_TRUE(stop_status.ok()) << stop_status.error_message();
EXPECT_TRUE(stop_response.success())
<< stop_response.error_message();
EXPECT_EQ(aubo_arm_->stopMotionCalls(), 2);
EXPECT_TRUE(admission.finishStopAll(ticket, true));
const auto resumed_move = moveJ("aubo_arm");
EXPECT_TRUE(resumed_move.status.ok())
<< resumed_move.status.error_message();
EXPECT_TRUE(resumed_move.response_success)
<< resumed_move.response_error;
EXPECT_EQ(aubo_arm_->moveJCalls(), 1);
}
TEST_F(GrpcArmServiceTest,
StopMotionRevokesBlockedMoveLLeaseBeforeMoveJReturns)
{
aubo_arm_->blockNextMotion();
auto blocked_move = std::async(
std::launch::async,
[this]() { return moveL("aubo_arm"); });
const bool move_started = aubo_arm_->waitForBlockingMotion(
"moveL", std::chrono::seconds(2));
MoveOutcome conflict;
api::CommandHeader_Feedback stop_response;
grpc::Status stop_status;
MoveOutcome before_retired_handler_release;
if (move_started) {
conflict = moveJ("aubo_arm");
auto stop = std::async(
std::launch::async,
[this, &stop_response]() {
return stopMotion("aubo_arm", stop_response);
});
before_retired_handler_release = moveJ("aubo_arm");
aubo_arm_->releaseBlockingMotion();
stop_status = stop.get();
}
aubo_arm_->releaseBlockingMotion();
const auto original_move = blocked_move.get();
const auto resumed_move = moveJ("aubo_arm");
ASSERT_TRUE(move_started);
EXPECT_EQ(conflict.status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(conflict.response_error, conflict.status.error_message());
EXPECT_TRUE(stop_status.ok()) << stop_status.error_message();
EXPECT_TRUE(stop_response.success())
<< stop_response.error_message();
EXPECT_EQ(
before_retired_handler_release.status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_TRUE(resumed_move.status.ok())
<< resumed_move.status.error_message();
EXPECT_TRUE(resumed_move.response_success)
<< resumed_move.response_error;
EXPECT_TRUE(original_move.status.ok())
<< original_move.status.error_message();
EXPECT_TRUE(original_move.response_success)
<< original_move.response_error;
EXPECT_EQ(aubo_arm_->moveJCalls(), 1);
EXPECT_EQ(aubo_arm_->moveLCalls(), 1);
EXPECT_EQ(aubo_arm_->stopMotionCalls(), 2);
}
TEST_F(GrpcArmServiceTest, StopMotionFailureRetainsSafetyBarrier)
{
auto& authority = control::ControlAuthorityManager::instance();
const auto action_lease = authority.tryAcquire(
"aubo_arm", "action-queue:test", std::chrono::hours(1));
ASSERT_TRUE(action_lease.acquired) << action_lease.detail;
aubo_arm_->failNextStopMotion();
api::CommandHeader_Feedback stop_response;
const auto stop_status = stopMotion("aubo_arm", stop_response);
const auto rejected_move = moveJ("aubo_arm");
EXPECT_EQ(stop_status.error_code(), grpc::StatusCode::INTERNAL);
EXPECT_FALSE(stop_response.success());
EXPECT_EQ(
stop_response.reason_code(),
api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
EXPECT_FALSE(authority.validate(action_lease.token));
EXPECT_TRUE(authority.isLeased("aubo_arm"));
EXPECT_EQ(
rejected_move.status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(aubo_arm_->moveJCalls(), 0);
EXPECT_EQ(aubo_arm_->stopMotionCalls(), 1);
authority.release(action_lease.token);
const auto recovery = authority.preemptAcquire(
"aubo_arm", "confirmed-stop-recovery", std::chrono::hours(1));
ASSERT_TRUE(recovery.acquired) << recovery.detail;
ASSERT_TRUE(authority.waitForPreemptedRelease(
recovery.token, std::chrono::milliseconds::zero()));
ASSERT_TRUE(authority.recoverRetiredSafetyHolders(recovery.token));
authority.release(recovery.token);
const auto recovered = authority.tryAcquire(
"aubo_arm", "move-after-recovery", std::chrono::hours(1));
EXPECT_TRUE(recovered.acquired) << recovered.detail;
authority.release(recovered.token);
}
TEST_F(GrpcArmServiceTest, StopMotionExceptionRetainsSafetyBarrier)
{
auto& authority = control::ControlAuthorityManager::instance();
const auto action_lease = authority.tryAcquire(
"aubo_arm", "action-queue:test", std::chrono::hours(1));
ASSERT_TRUE(action_lease.acquired) << action_lease.detail;
aubo_arm_->throwNextStopMotion();
api::CommandHeader_Feedback stop_response;
const auto stop_status = stopMotion("aubo_arm", stop_response);
const auto rejected_move = moveL("aubo_arm");
EXPECT_EQ(
stop_status.error_code(), grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_FALSE(stop_response.success());
EXPECT_EQ(
stop_response.reason_code(),
api::COMMAND_REASON_CODE_STOP_UNCONFIRMED);
EXPECT_FALSE(authority.validate(action_lease.token));
EXPECT_TRUE(authority.isLeased("aubo_arm"));
EXPECT_EQ(
rejected_move.status.error_code(),
grpc::StatusCode::FAILED_PRECONDITION);
EXPECT_EQ(aubo_arm_->moveLCalls(), 0);
EXPECT_EQ(aubo_arm_->stopMotionCalls(), 1);
authority.release(action_lease.token);
const auto recovery = authority.preemptAcquire(
"aubo_arm", "confirmed-exception-recovery", std::chrono::hours(1));
ASSERT_TRUE(recovery.acquired) << recovery.detail;
ASSERT_TRUE(authority.waitForPreemptedRelease(
recovery.token, std::chrono::milliseconds::zero()));
ASSERT_TRUE(authority.recoverRetiredSafetyHolders(recovery.token));
authority.release(recovery.token);
}
} // namespace
} // namespace cmvr::service