1294.92 Memory 동작 검증 테스트
1. Memory 동작의 검증 대상
SequenceNode와 FallbackNode는 current_child_idx_ 멤버 변수를 통해 메모리 기반 재진입(Memory) 동작을 구현한다. 이 동작의 핵심은 RUNNING 상태를 반환한 자식의 인덱스를 기억하여, 다음 Tick에서 해당 자식부터 실행을 재개하고 이전에 완료된 자식을 건너뛰는 것이다. Memory 동작 검증 테스트는 이 인덱스 관리의 정확성, 인덱스 초기화의 적시성, 그리고 건너뛰기 동작의 올바름을 검증한다(Faconti, 2022).
2. SequenceNode의 Memory 검증
2.1 기본 재진입 테스트
TEST(SequenceMemory, SkipCompletedChildren) {
// 3개 자식: action1, action2, action3
action1->setReturn(BT::NodeStatus::SUCCESS);
action2->setReturn(BT::NodeStatus::RUNNING);
// Tick 1: action1 S, action2 R → RUNNING
auto result1 = seq.executeTick();
EXPECT_EQ(result1, BT::NodeStatus::RUNNING);
EXPECT_EQ(action1->tickCount(), 1);
EXPECT_EQ(action2->tickCount(), 1);
EXPECT_EQ(action3->tickCount(), 0);
// Tick 2: action2부터 재개 (action1 건너뜀)
action2->setReturn(BT::NodeStatus::SUCCESS);
action3->setReturn(BT::NodeStatus::SUCCESS);
action1->resetTickCount();
action2->resetTickCount();
auto result2 = seq.executeTick();
EXPECT_EQ(result2, BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 0); // 건너뜀!
EXPECT_EQ(action2->tickCount(), 1);
EXPECT_EQ(action3->tickCount(), 1);
}
이 테스트의 핵심 검증 지점은 Tick 2에서 action1->tickCount()가 0인 것이다. SequenceNode의 current_child_idx_가 1을 유지하므로, action1은 재실행되지 않는다.
2.2 다단계 재진입 테스트
TEST(SequenceMemory, MultiStageReentry) {
// Tick 1: action1 R
action1->setReturn(BT::NodeStatus::RUNNING);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
// Tick 2: action1 S, action2 R (idx: 0→1)
action1->setReturn(BT::NodeStatus::SUCCESS);
action2->setReturn(BT::NodeStatus::RUNNING);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
// Tick 3: action2 S, action3 R (idx: 1→2)
action2->setReturn(BT::NodeStatus::SUCCESS);
action3->setReturn(BT::NodeStatus::RUNNING);
action1->resetTickCount();
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
EXPECT_EQ(action1->tickCount(), 0); // Tick 3에서 건너뜀
// Tick 4: action3 S → 전체 SUCCESS (idx: 0으로 초기화)
action3->setReturn(BT::NodeStatus::SUCCESS);
action2->resetTickCount();
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 0); // 여전히 건너뜀
EXPECT_EQ(action2->tickCount(), 0); // 건너뜀
}
current_child_idx_가 0→1→2→0으로 순차 진행하고, 최종 SUCCESS 시 0으로 초기화되는 전체 생애 주기를 검증한다.
2.3 FAILURE에 의한 인덱스 초기화 테스트
TEST(SequenceMemory, FailureResetsIndex) {
// Tick 1: action1 S, action2 R (idx=1)
action1->setReturn(BT::NodeStatus::SUCCESS);
action2->setReturn(BT::NodeStatus::RUNNING);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
// Tick 2: action2 F → FAILURE (idx=0으로 초기화)
action2->setReturn(BT::NodeStatus::FAILURE);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::FAILURE);
// Tick 3: 다시 action1부터 시작
action1->resetTickCount();
action2->setReturn(BT::NodeStatus::SUCCESS);
action3->setReturn(BT::NodeStatus::SUCCESS);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 1); // action1 다시 실행
}
FAILURE가 발생하면 current_child_idx_가 0으로 초기화되어, 다음 실행 사이클에서 첫 번째 자식부터 다시 시작하는 것을 검증한다.
3. FallbackNode의 Memory 검증
3.1 기본 재진입 테스트
TEST(FallbackMemory, SkipFailedChildren) {
// Tick 1: action1 F, action2 R → RUNNING
action1->setReturn(BT::NodeStatus::FAILURE);
action2->setReturn(BT::NodeStatus::RUNNING);
auto result1 = fb.executeTick();
EXPECT_EQ(result1, BT::NodeStatus::RUNNING);
// Tick 2: action2부터 재개 (action1 건너뜀)
action2->setReturn(BT::NodeStatus::SUCCESS);
action1->resetTickCount();
auto result2 = fb.executeTick();
EXPECT_EQ(result2, BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 0); // FAILURE였으므로 건너뜀
}
FallbackNode에서는 FAILURE를 반환한 자식을 건너뛴다. Sequence가 SUCCESS를 건너뛰는 것과 대칭적이다.
3.2 SUCCESS에 의한 인덱스 초기화 테스트
TEST(FallbackMemory, SuccessResetsIndex) {
// Tick 1: action1 F, action2 R (idx=1)
action1->setReturn(BT::NodeStatus::FAILURE);
action2->setReturn(BT::NodeStatus::RUNNING);
EXPECT_EQ(fb.executeTick(), BT::NodeStatus::RUNNING);
// Tick 2: action2 S → SUCCESS (idx=0으로 초기화)
action2->setReturn(BT::NodeStatus::SUCCESS);
EXPECT_EQ(fb.executeTick(), BT::NodeStatus::SUCCESS);
// Tick 3: 다시 action1부터 시작
action1->resetTickCount();
action1->setReturn(BT::NodeStatus::SUCCESS);
EXPECT_EQ(fb.executeTick(), BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 1); // action1 다시 실행
}
3.3 전체 FAILURE에 의한 인덱스 초기화 테스트
TEST(FallbackMemory, AllFailureResetsIndex) {
// Tick 1: action1 F, action2 F, action3 F → FAILURE
action1->setReturn(BT::NodeStatus::FAILURE);
action2->setReturn(BT::NodeStatus::FAILURE);
action3->setReturn(BT::NodeStatus::FAILURE);
EXPECT_EQ(fb.executeTick(), BT::NodeStatus::FAILURE);
// Tick 2: 다시 action1부터 시작
action1->resetTickCount();
action1->setReturn(BT::NodeStatus::SUCCESS);
EXPECT_EQ(fb.executeTick(), BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 1);
}
4. Halt에 의한 인덱스 초기화 검증
4.1 외부 Halt의 인덱스 영향
TEST(SequenceMemory, HaltResetsIndex) {
// Tick 1: action1 S, action2 R (idx=1)
action1->setReturn(BT::NodeStatus::SUCCESS);
action2->setReturn(BT::NodeStatus::RUNNING);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
// 외부에서 Halt 호출
seq.halt();
// Halt 후 다시 Tick: action1부터 시작해야 함
action1->resetTickCount();
action1->setReturn(BT::NodeStatus::SUCCESS);
action2->setReturn(BT::NodeStatus::SUCCESS);
action3->setReturn(BT::NodeStatus::SUCCESS);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::SUCCESS);
EXPECT_EQ(action1->tickCount(), 1); // action1 재실행
}
halt() 메서드는 current_child_idx_를 0으로 초기화한다. 따라서 Halt 이후의 Tick은 항상 첫 번째 자식부터 시작해야 한다.
4.2 Halt 시 RUNNING 자식의 정지 검증
TEST(SequenceMemory, HaltStopsRunningChild) {
// action2가 비동기 노드
action1->setReturn(BT::NodeStatus::SUCCESS);
asyncAction2->setReturnOnStart(BT::NodeStatus::RUNNING);
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
// Halt
seq.halt();
// asyncAction2의 onHalted()가 호출되었는지 검증
EXPECT_EQ(asyncAction2->haltCount(), 1);
// 노드 상태가 IDLE로 복원되었는지 검증
EXPECT_EQ(seq.status(), BT::NodeStatus::IDLE);
}
5. Memory 동작과 Reactive 동작의 비교 검증
5.1 동일 시나리오에서의 차이 검증
// Memory Sequence: 완료된 자식 건너뜀
TEST(MemoryVsReactive, SequenceMemorySkips) {
action1->setReturn(BT::NodeStatus::SUCCESS);
action2->setReturn(BT::NodeStatus::RUNNING);
seq.executeTick(); // idx=1
action1->resetTickCount();
action2->setReturn(BT::NodeStatus::RUNNING);
seq.executeTick();
EXPECT_EQ(action1->tickCount(), 0); // Memory: 건너뜀
}
// Reactive Sequence: 완료된 자식도 재평가
TEST(MemoryVsReactive, ReactiveSequenceReevaluates) {
action1->setReturn(BT::NodeStatus::SUCCESS);
action2->setReturn(BT::NodeStatus::RUNNING);
rseq.executeTick();
action1->resetTickCount();
action2->setReturn(BT::NodeStatus::RUNNING);
rseq.executeTick();
EXPECT_EQ(action1->tickCount(), 1); // Reactive: 재평가
}
동일한 자식 상태 시나리오에서 SequenceNode는 action1을 건너뛰지만 ReactiveSequence는 action1을 재평가한다. 이 차이가 Memory 동작의 본질이다.
6. 장기 실행 시나리오 테스트
6.1 다수 Tick에 걸친 인덱스 추적
TEST(SequenceMemory, LongRunningScenario) {
const int NUM_CHILDREN = 5;
std::vector<MockAsyncAction*> children(NUM_CHILDREN);
// 5개 자식을 가진 Sequence 구성
for (int i = 0; i < NUM_CHILDREN; i++) {
children[i]->setReturnOnStart(BT::NodeStatus::RUNNING);
children[i]->setReturnOnRunning(BT::NodeStatus::RUNNING);
}
// 각 자식이 3 Tick 후 SUCCESS를 반환하는 시나리오
int total_ticks = 0;
for (int i = 0; i < NUM_CHILDREN; i++) {
for (int t = 0; t < 3; t++) {
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::RUNNING);
total_ticks++;
// 현재 자식 이전의 자식들이 실행되지 않았는지 확인
for (int j = 0; j < i; j++) {
// 이전 자식의 tick_count가 증가하지 않음
}
}
children[i]->setReturnOnRunning(BT::NodeStatus::SUCCESS);
}
// 마지막 자식이 SUCCESS → 전체 SUCCESS
EXPECT_EQ(seq.executeTick(), BT::NodeStatus::SUCCESS);
}
7. 인덱스 일관성 검증 패턴
7.1 상태 전이 매트릭스
Memory 동작의 인덱스 변화를 상태 전이 매트릭스로 정리하면 다음과 같다:
| 이벤트 | SequenceNode idx | FallbackNode idx |
|---|---|---|
| 초기 상태(IDLE) | 0 | 0 |
| 자식 i가 RUNNING 반환 | i 유지 | i 유지 |
| 자식 i가 SUCCESS 반환 | i+1로 증가 (Seq 진행) | 0으로 초기화 (조기 종료) |
| 자식 i가 FAILURE 반환 | 0으로 초기화 (조기 종료) | i+1로 증가 (Fb 진행) |
| 모든 자식 완료 | 0으로 초기화 | 0으로 초기화 |
| halt() 호출 | 0으로 초기화 | 0으로 초기화 |
각 테스트 케이스는 이 매트릭스의 한 행 이상을 검증해야 하며, 전체 테스트 스위트가 모든 행을 포괄해야 한다.
8. Memory 검증 시 주의 사항
-
Tick 간 상태 독립성: 테스트에서 모의 노드의 반환 상태를 Tick 사이에 변경할 때, 변경이 제어 노드의 내부 상태에 미치는 영향을 정확히 예측해야 한다. 특히
resetChildren()호출 이후 모든 자식이 IDLE로 전환되므로, 모의 노드의onStart()가 다시 호출될 수 있다. -
비동기 노드와의 상호작용:
StatefulActionNode를 자식으로 사용할 때,onStart()와onRunning()의 호출 시점이 Memory 동작에 의해 달라진다. IDLE에서 첫 Tick은onStart()를, 이후 Tick은onRunning()을 호출하므로, 테스트에서 이 구분을 반영해야 한다. -
resetChildren()과 haltChild()의 차이: SequenceNode의 FAILURE 시
resetChildren()이 호출되어 모든 자식이 초기화된다. 반면 ReactiveSequence에서는haltChild()로 특정 자식만 정지된다. Memory 테스트에서는 이 두 메서드의 효과 차이를 명확히 구분하여 검증해야 한다(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/