1294.94 Halt 동작 검증 테스트
1. Halt 검증의 범위
Halt 동작 검증 테스트는 제어 노드가 정지될 때 발생하는 일련의 상태 전이와 자원 해제를 검증한다. 검증 대상은 다음과 같다: 부모로부터의 Halt 전파가 RUNNING 자식에게 정확히 도달하는지, Halt 이후 제어 노드의 내부 상태가 올바르게 초기화되는지, 자식 노드의 onHalted() 콜백이 적시에 호출되는지, 그리고 Halt 이후 재실행 시 깨끗한 상태에서 시작하는지이다(Faconti, 2022).
2. 기본 Halt 전파 검증
2.1 SequenceNode의 Halt 전파
TEST(SequenceHalt, PropagatesHaltToRunningChild) {
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
// action2가 RUNNING 상태
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
EXPECT_EQ(asyncAction2->status(), BT::NodeStatus::RUNNING);
// Sequence에 Halt 호출
seq.halt();
// 검증: asyncAction2의 onHalted() 호출
EXPECT_EQ(asyncAction2->haltCount(), 1);
// 검증: 모든 노드가 IDLE로 전환
EXPECT_EQ(seq.status(), BT::NodeStatus::IDLE);
EXPECT_EQ(action1->status(), BT::NodeStatus::IDLE);
EXPECT_EQ(asyncAction2->status(), BT::NodeStatus::IDLE);
}
2.2 FallbackNode의 Halt 전파
TEST(FallbackHalt, PropagatesHaltToRunningChild) {
action1->setReturn(BT::NodeStatus::FAILURE);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
EXPECT_EQ(fb.executeTick(), BT::NodeStatus::RUNNING);
fb.halt();
EXPECT_EQ(asyncAction2->haltCount(), 1);
EXPECT_EQ(fb.status(), BT::NodeStatus::IDLE);
EXPECT_EQ(asyncAction2->status(), BT::NodeStatus::IDLE);
}
2.3 비 RUNNING 자식에 대한 Halt 미전파
TEST(SequenceHalt, DoesNotHaltIdleChildren) {
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
seq.executeTick(); // action1: IDLE(완료), action2: RUNNING, action3: IDLE(미실행)
seq.halt();
// action3는 IDLE 상태이므로 halt가 호출되지 않음
EXPECT_EQ(asyncAction3->haltCount(), 0);
// action1도 이미 완료되어 IDLE이므로 halt 불필요
}
ControlNode::halt()는 RUNNING 상태인 자식에 대해서만 Halt를 호출한다. IDLE 상태인 자식에 대해서는 Halt를 호출하지 않는다.
3. 인덱스 초기화 검증
3.1 SequenceNode의 current_child_idx_ 초기화
TEST(SequenceHalt, ResetsChildIndex) {
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
// Tick 1: idx=1 (action2에서 RUNNING)
seq.executeTick();
// Halt: idx가 0으로 초기화
seq.halt();
// 재실행: action1부터 시작해야 함
action1->resetTickCount();
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::SUCCESS);
action3->setReturn(BT::NodeStatus::SUCCESS);
seq.executeTick();
EXPECT_EQ(action1->tickCount(), 1); // action1 재실행 확인
}
3.2 FallbackNode의 current_child_idx_ 초기화
TEST(FallbackHalt, ResetsChildIndex) {
action1->setReturn(BT::NodeStatus::FAILURE);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
fb.executeTick(); // idx=1
fb.halt(); // idx=0
action1->resetTickCount();
action1->setReturn(BT::NodeStatus::SUCCESS);
fb.executeTick();
EXPECT_EQ(action1->tickCount(), 1); // action1부터 재시작
}
4. ReactiveSequence의 Halt 검증
4.1 조건 FAILURE에 의한 자동 Halt
TEST(ReactiveSequenceHalt, ConditionFailureTriggerHalt) {
condition->setReturn(BT::NodeStatus::SUCCESS);
asyncAction->setReturnOnStart(BT::NodeStatus::RUNNING);
// Tick 1: condition S, action R → RUNNING
rseq.executeTick();
EXPECT_EQ(asyncAction->haltCount(), 0);
// Tick 2: condition F → FAILURE (action 자동 Halt)
condition->setReturn(BT::NodeStatus::FAILURE);
rseq.executeTick();
// resetChildren()에 의해 action Halt
EXPECT_EQ(asyncAction->haltCount(), 1);
}
ReactiveSequence에서 앞쪽 조건이 FAILURE를 반환하면, resetChildren()에 의해 RUNNING이었던 뒤쪽 행동 노드가 Halt된다.
4.2 RUNNING 자식 이후 자식의 선택적 Halt
TEST(ReactiveSequenceHalt, HaltsOnlyAfterRunning) {
condition->setReturn(BT::NodeStatus::SUCCESS);
asyncAction1->setReturnOnStart(BT::NodeStatus::RUNNING);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
// Tick 1: condition S, action1 S, action2 R → RUNNING
asyncAction1->setReturnOnStart(BT::NodeStatus::SUCCESS);
rseq.executeTick();
// Tick 2: condition S, action1 R → RUNNING
// action2는 haltChild()에 의해 Halt
asyncAction1->setReturnOnStart(BT::NodeStatus::RUNNING);
rseq.executeTick();
EXPECT_EQ(asyncAction2->haltCount(), 1); // action2 Halt
}
ReactiveSequence의 tick() 구현에서 RUNNING을 반환한 자식 이후의 모든 자식에 대해 haltChild()가 호출된다.
5. ReactiveFallback의 Halt 검증
5.1 상위 대안 SUCCESS에 의한 Halt
TEST(ReactiveFallbackHalt, SuccessTriggerHalt) {
action1->setReturn(BT::NodeStatus::FAILURE);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
// Tick 1: action1 F, action2 R → RUNNING
rfb.executeTick();
// Tick 2: action1 S → SUCCESS (action2 Halt)
action1->setReturn(BT::NodeStatus::SUCCESS);
rfb.executeTick();
EXPECT_EQ(asyncAction2->haltCount(), 1);
}
5.2 상위 RUNNING에 의한 하위 Halt
TEST(ReactiveFallbackHalt, RunningTriggerHaltOfLater) {
asyncAction1->setReturnOnStart(BT::NodeStatus::FAILURE);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
// Tick 1: action1 F, action2 R → RUNNING
rfb.executeTick();
// Tick 2: action1 R → RUNNING (action2 Halt)
asyncAction1->setReturnOnStart(BT::NodeStatus::RUNNING);
rfb.executeTick();
EXPECT_EQ(asyncAction2->haltCount(), 1);
}
ReactiveFallback에서 상위 대안이 RUNNING을 반환하면, 하위 RUNNING 대안이 haltChild()에 의해 정지된다.
6. 중첩 트리에서의 Halt 전파 검증
6.1 Sequence 내 Fallback의 Halt 전파
TEST(NestedHalt, SequenceHaltPropagatesThrough Fallback) {
// Sequence → Fallback → asyncAction
action1->setReturn(BT::NodeStatus::SUCCESS);
fallbackAction1->setReturn(BT::NodeStatus::FAILURE);
asyncFallbackAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
// asyncFallbackAction2가 RUNNING
seq.executeTick();
EXPECT_EQ(seq.status(), BT::NodeStatus::RUNNING);
// Sequence Halt → Fallback Halt → asyncFallbackAction2 Halt
seq.halt();
EXPECT_EQ(asyncFallbackAction2->haltCount(), 1);
EXPECT_EQ(asyncFallbackAction2->status(), BT::NodeStatus::IDLE);
}
6.2 다단계 중첩의 Halt 전파
TEST(NestedHalt, DeepNestingHaltPropagation) {
// outerSeq → innerFb → innerSeq → asyncAction
// asyncAction이 RUNNING 상태에서 outerSeq Halt
outerSeq.executeTick();
EXPECT_EQ(asyncAction->status(), BT::NodeStatus::RUNNING);
outerSeq.halt();
// 3단계를 거쳐 asyncAction까지 Halt 전파
EXPECT_EQ(asyncAction->haltCount(), 1);
EXPECT_EQ(innerSeq.status(), BT::NodeStatus::IDLE);
EXPECT_EQ(innerFb.status(), BT::NodeStatus::IDLE);
EXPECT_EQ(outerSeq.status(), BT::NodeStatus::IDLE);
}
Halt 전파는 트리의 깊이에 관계없이 RUNNING 상태인 모든 노드에 도달해야 한다.
7. Halt 후 재실행 검증
7.1 깨끗한 상태에서의 재시작
TEST(HaltAndRestart, CleanRestart) {
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
// 첫 번째 실행 사이클
seq.executeTick();
seq.halt();
// 모든 상태 초기화 확인
EXPECT_EQ(seq.status(), BT::NodeStatus::IDLE);
// 두 번째 실행 사이클: 처음부터 시작
action1->resetTickCount();
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::SUCCESS);
action3->setReturn(BT::NodeStatus::SUCCESS);
auto result = seq.executeTick();
EXPECT_EQ(result, BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 1); // action1 재실행
}
7.2 반복 Halt-재실행 안정성
TEST(HaltAndRestart, RepeatedHaltRestart) {
for (int cycle = 0; cycle < 10; cycle++) {
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
seq.halt();
EXPECT_EQ(seq.status(), BT::NodeStatus::IDLE);
}
// 10회 반복 후에도 정상 동작
EXPECT_EQ(asyncAction2->haltCount(), 10);
// 최종 정상 실행
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::SUCCESS);
action3->setReturn(BT::NodeStatus::SUCCESS);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::SUCCESS);
}
반복적인 Halt-재실행 사이클 이후에도 내부 상태 누적이나 리소스 누수 없이 정상 동작하는지 검증한다.
8. onHalted() 콜백의 검증
8.1 콜백 호출 타이밍
TEST(OnHaltedCallback, CalledBeforeStatusChange) {
BT::NodeStatus status_during_halt = BT::NodeStatus::IDLE;
asyncAction->setOnHaltedCallback([&]() {
status_during_halt = asyncAction->status();
});
asyncAction->setReturnOnStart(BT::NodeStatus::RUNNING);
seq.executeTick();
seq.halt();
// onHalted()가 호출될 때 노드는 아직 RUNNING 상태
EXPECT_EQ(status_during_halt, BT::NodeStatus::RUNNING);
// halt 완료 후 IDLE로 전환
EXPECT_EQ(asyncAction->status(), BT::NodeStatus::IDLE);
}
8.2 리소스 해제 검증
TEST(OnHaltedCallback, ResourceCleanup) {
bool resource_released = false;
asyncAction->setOnHaltedCallback([&]() {
resource_released = true;
});
asyncAction->setReturnOnStart(BT::NodeStatus::RUNNING);
seq.executeTick();
EXPECT_FALSE(resource_released);
seq.halt();
EXPECT_TRUE(resource_released);
}
9. Halt 검증 시 주의 사항
-
이중 Halt 방지: IDLE 상태인 노드에
halt()를 호출해도 오류가 발생하지 않아야 한다.ControlNode::halt()는 RUNNING 자식에 대해서만 Halt를 전파하므로, IDLE 상태에서의 Halt 호출은 무해해야 한다. -
Halt 순서 의존성: 다수의 RUNNING 자식이 있는 경우, Halt가 호출되는 순서가 중요할 수 있다.
ControlNode::halt()는children_nodes_벡터를 순회하므로 인덱스 순서대로 Halt가 호출된다. -
비동기 정지의 동기성:
onHalted()콜백은 동기적으로 호출되어야 하며, 콜백 내에서의 비동기 작업은 완료될 때까지 차단해야 한다. 그렇지 않으면 Halt 이후에도 자원이 사용 중인 상태가 될 수 있다(Faconti, 2022).
참고 문헌
- Colledanchise, M., & Ogren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. CRC Press.
- Faconti, D. (2022). BehaviorTree.CPP documentation and API reference. https://www.behaviortree.dev/