AEGLOSLAB ROS 2 Jazzy 대전

제14부 시스템 설계·품질·운영

14.6 테스트 II — launch_testing·통합·HIL

14.6.1 이 장에서 배우는 것 / 선수 지식

14.5이 부분을 시험했다면, 이 장은 조립된 시스템을 시험한다. 단위 시험이 모두 통과해도 시스템이 동작하지 않는 경우가 많은데, 그 원인이 대개 경계에 있기 때문이다 — 토픽 이름 불일치, QoS 미스매치(1.5.4), 파라미터 미적용(1.9.7), 시작 순서(14.2.3), tf 누락(5.2.5).

이런 것들은 여러 노드를 실제로 실행해야만 드러난다. 그것이 통합 시험이 필요한 이유다.

배우는 것:

  • launch_testing — launch를 시험 하네스로.
  • 통합 시험 설계 — 무엇을 확인하는가.
  • 시뮬 기반 시험 — 동작 수준.
  • bag 기반 회귀 — 실제 데이터로.
  • HIL — 하드웨어를 넣은 시험.
  • 현장 승인 시험 — 배포 전 확인.
  • 시험 전략 — 무엇을 어디서.

선수 지식: 14.5(단위·모의), 14.3·14.4(launch), 10.1~10.6(시뮬), 11.3.8(회귀 데이터셋), 4.10(CI), 6.2.8(mock 하드웨어).

정직: launch_testing의 API·데코레이터 이름은 배포판 의존이다(확인 필요)(→ 167차 확인: Jazzy에서 이 장이 쓴 API(generate_test_description·ReadyToTest·post_shutdown_test·assertExitCodes·assertWaitFor·get_node_names)를 전부 실제로 실행해 확인했다 — 14.6.2·14.6.3 실행 확인(167차) 참고). HIL 구성은 하드웨어와 조직에 따라 크게 다르므로 개념과 판단만 다룬다. 안전 관련 승인 시험은 안전 표준·전문가의 영역이며(14.10), 이 책의 서술을 근거로 삼아서는 안 된다. 이 환경에서 검증하지 않았다(목차 3).

14.6.2 launch_testing

launch로 시스템을 기동하고, 그 위에서 시험을 실행한다.

# test/test_perception_pipeline.py (개념 — 확인 필요: API)(→ 167차 확인: 아래 API 전부 Jazzy에서 실재·동작 확인)
import unittest
import launch, launch_ros, launch_testing
import rclpy
from sensor_msgs.msg import PointCloud2

def generate_test_description():
    driver = launch_ros.actions.Node(
        package='my_pkg', executable='fake_lidar', name='fake_lidar')
    filter_node = launch_ros.actions.Node(
        package='my_pkg', executable='cloud_filter', name='cloud_filter',
        parameters=[{'leaf_size': 0.05}])
    return (
        launch.LaunchDescription([
            driver, filter_node,
            launch_testing.actions.ReadyToTest(),      # ★ 여기부터 시험 시작
        ]),
        {'driver': driver, 'filter': filter_node},     # 시험에서 참조할 프로세스
    )

class TestPipeline(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        rclpy.init()
        cls.node = rclpy.create_node('test_node')

    @classmethod
    def tearDownClass(cls):
        cls.node.destroy_node()
        rclpy.shutdown()

    def test_filtered_output_published(self):
        received = []
        sub = self.node.create_subscription(
            PointCloud2, '/cloud_filtered', lambda m: received.append(m), 10)
        end = time.time() + 10.0
        while time.time() < end and not received:
            rclpy.spin_once(self.node, timeout_sec=0.1)
        self.assertTrue(received, '필터 출력이 발행되지 않음')

@launch_testing.post_shutdown_test()
class TestExitCodes(unittest.TestCase):
    def test_clean_exit(self, proc_info):
        launch_testing.asserts.assertExitCodes(proc_info)   # 비정상 종료 없음
# CMakeLists.txt
if(BUILD_TESTING)
  find_package(launch_testing_ament_cmake REQUIRED)
  add_launch_test(test/test_perception_pipeline.py)
endif()
기능용도
ReadyToTest기동 완료 시점 표시
프로세스 참조출력·종료 코드 확인
post_shutdown_test종료 후 검증
assertExitCodes비정상 종료 탐지
출력 매칭로그에 특정 문구(확인 필요)(→ 167차 확인: proc_output.assertWaitFor(text, ...)로 실재·동작 — 단 기본값이 stdout이 아니라 stderr인 함정 발견, 아래 실행 확인(167차) 참고)

견해: post_shutdown_test의 종료 코드 확인이 의외로 큰 가치를 준다. 시험이 통과해도 노드가 종료 시 크래시하는 경우가 있는데, 이것을 자동으로 검출한다. "조용한 크래시" 를 발견하는 저렴한 수단이다.

함정: ReadyToTest 시점이 "노드가 준비됨"을 뜻하지 않는다. 프로세스가 기동된 것일 뿐이므로, 시험 안에서 준비 상태를 기다려야 한다(14.2.4). 이것을 빠뜨리면 불안정한 시험이 된다.(→ 168차 확인: 실제로 재현했다 — 아래 실행 확인(167차) 블록의 168차 추가분 참고)

견해: launch_testing은 "구성이 맞는가"를 시험하는 데 가장 적합하다. 토픽 연결·QoS·파라미터·기동 순서처럼 launch 구성에서 오는 문제를 검출한다. 알고리즘 품질은 단위 시험(14.5)이나 시뮬 시험이 맡는다.

실행 확인(167차, Jazzy) — post_shutdown_test+assertExitCodes의 "조용한 크래시" 탐지를 양성·음성 둘 다 실측했다. 장기 실행(무한루프) 노드 둘을 만들어 SIGINT를 보냈다 — ① well_behaved_node(SIGINT를 처리해 정상적으로 sys.exit(0))는 능동 시험 통과 → SIGINT 종료 → assertExitCodes 통과. ② buggy_shutdown_node(SIGINT 핸들러 자체가 RuntimeError를 던짐)는 능동 시험도 통과했지만(정상 동작 중이었으므로) 종료 시점에만 크래시(exit code 1)했고, post_shutdown_test가 이를 정확히 검출했다("Proc target_node-1 exited with code 1") — 위 견해가 그대로 확인됐다. 부수 발견 둘 — (a) assertExitCodes의 기본 allowable_exit_codes는 소스(assert_exit_codes.py)로 직접 확인한 결과 [0]뿐이다(비-Windows) — SIGINT로 종료되는 노드를 기본 인자로 통과시키려면 노드 스스로 SIGINT를 처리해 exit(0)으로 끝나야 한다. (b) 시험 대상 프로세스가 능동 시험 구간 중에 스스로 끝나면(성공이든 실패든) launch_testing은 "Processes under test stopped before tests completed"로 전체 실행을 중단시킨다 — post_shutdown_test는 "능동 시험이 끝날 때까지 계속 실행 중이던 프로세스"를 대상으로 설계된 것이지, 시험 중간에 스스로 끝나는 짧은 프로세스에는 다른 방식이 필요하다.

assertWaitFor(출력 매칭) — 실재하지만 기본값이 stderr라는 실제 함정을 발견했다. 노드가 print(...)(stdout)로 특정 마커 문자열을 출력했는데도 proc_output.assertWaitFor('마커', process=target, timeout=10)가 10초 타임아웃으로 실패했다. io_handler.py 소스를 읽어 원인을 확정했다 — waitFor()의 기본 인자가 stream='stderr'다(stdout이 아님). stream='stdout'을 명시하자 즉시 통과했다(마커가 실제로 출력된 시점(t≈3초)과 정확히 일치). 이는 rclpy의 get_logger()가 기본적으로 stderr에 쓰는 관례와는 맞지만, 일반 print()나 다른 언어의 stdout 로깅과는 조용히 어긋나는 문서화되지 않은 함정이다 — 이 장의 새 함정 카탈로그 후보다.

컨테이너 gap167j(ros:jazzy-ros-base + ros-jazzy-launch-testing + ros-jazzy-launch-testing-ros). 원본: tools/gap_harness/gap167/rj118_test/(test_exit_codes2.py·well_behaved_node.py·buggy_shutdown_node.py·test_output_match.py·assert_exit_codes_source.log·io_handler_source.log).

168차 추가 — ReadyToTest 타이밍 함정을 양성·음성 둘 다 실제로 재현했다. 프로세스는 즉시 기동되지만 실제 "준비"(퍼블리셔 생성+발행 시작)까지 3초 걸리는 slow_starter.py를 만들고, ReadyToTest 직후 곧바로 WAIT_SECONDS만큼만 기다리는 시험을 작성했다. WAIT_SECONDS=1.0(책이 경고하는, 준비 상태를 안 기다리는 패턴)은 정확히 실패했다(AssertionError: [] is not true : 1.0초 안에 메시지를 받지 못함, 1.353초 만에 FAIL). WAIT_SECONDS=5.0(3초 지연을 넘겨 충분히 기다림)은 정확히 통과했다(로그에 "slow_starter NOW actually ready, starting to publish"가 시험 성공 전에 출력됨). 위 함정 문구가 양성·음성 둘 다로 결정적으로 확인됐다. 원본: tools/gap_harness/gap168/readytotest_test/(test_readiness.py·slow_starter.py·run_naive_1s.log·run_proper_5s.log).

14.6.3 통합 시험 설계

무엇을 확인할 것인가.

통합 시험 ① 기동 노드가 다 기동되나 ② 연결 토픽이 흐르나 ③ 규약 tf · 프레임 · 단위 ④ 기능 입력→출력이 맞나 ⑤ 이상 대응 고장 시 거동 ⑥ 종료 정상인가
확인예
① 기동기대 노드 목록 전부 존재
② 연결각 토픽이 기대 주기로 발행
③ 규약tf 트리 완결·프레임 이름·단위(7.1.9)
④ 기능장애물 입력 → 회피 명령 출력
⑤ 이상센서 끊김 → 안전 정지(14.2.6)
⑥ 종료종료 코드·자원 해제
def test_expected_nodes_running(self):
    time.sleep(2.0)                    # 또는 조건 대기
    names = self.node.get_node_names()
    for expected in ['cloud_filter', 'costmap_node', 'controller']:
        self.assertIn(expected, names, f'{expected} 노드 없음')

def test_tf_tree_complete(self):
    buf = tf2_ros.Buffer(); tf2_ros.TransformListener(buf, self.node)
    # base_link → map 이 조회되는가 (5.2.5)
    ok = wait_for_transform(buf, 'map', 'base_link', timeout=10.0)
    self.assertTrue(ok, 'map → base_link 변환 없음')

견해: ③(규약 검증)을 자동화하는 것이 특히 가치 있다. 7.1.9에서 직접 확인하라고 한 항목들 — 프레임 이름, 단위, tf 완결성, 스탬프 유효성 — 을 시험으로 만들면 회귀가 자동으로 검출된다. 사람이 매번 확인하는 것은 지속되지 않는다.

실행 확인(167차, Jazzy) — test_expected_nodes_running 패턴과 tf 트리 완결성 패턴을 실측했다. demo_nodes_cpp의 실제 talker·listener 노드 둘을 launch_testing으로 실행하고 self.node.get_node_names()로 둘 다 발견됨을 확인했다(2 tests, OK). test_tf_tree_complete 패턴 자체는 5.7.8에서 훨씬 자세히(양성·음성 둘 다) 실행 확인했다 — 이 절의 두 함수 모두 위 함정 카탈로그·견해와 정확히 일치하는 실측 결과를 냈다. 원본: tools/gap_harness/gap167/rj118_test/test_node_names.py·run_node_names.log; 5.7.8 실행 확인(167차) 참고.

견해: ⑤(이상 대응)가 가장 소홀히 되면서 가장 중요하다. 정상 경로만 시험하면 고장 시 거동은 현장에서 처음 실행된다. 모의 노드를 강제 종료하고 시스템이 안전하게 반응하는지 확인하는 시험을 넣으라(14.5.7의 연장).

함정: 통합 시험이 느리면 아무도 안 실행한다. 하나에 5분 걸리는 시험이 스무 개면 CI가 무의미해진다. 핵심 소수를 PR 검증에, 전체를 야간에 두는 분업이 실용적이다(4.10.2).

14.6.4 시뮬 기반 시험

동작 수준을 검증한다(10.1.7의 회귀 시험).

시험확인
목표 도달성공률·시간(8.8.5)
충돌 없음시뮬 충돌 이벤트
경로 품질길이·부드러움
복구 동작막혔을 때
조작 성공파지·배치(9.5.8)
성능종단 지연(11.7)
# 개념: 헤드리스 시뮬에서 시나리오 실행 후 판정
def test_reaches_goal_in_corridor(self):
    send_goal(self.node, x=5.0, y=0.0)
    result = wait_for_result(self.node, timeout=60.0)
    self.assertEqual(result.status, SUCCEEDED)
    self.assertLess(elapsed_time(), 40.0)          # 여유 있는 기준
    self.assertEqual(collision_count(), 0)

견해: 판정 기준을 여유 있게 설정하라(4.10.6·11.7.9의 반복). 시뮬은 CI 머신 부하에 따라 RTF가 변하므로(10.2.8), 빠듯한 시간 기준은 오탐을 만든다. "명백한 회귀"를 검출하는 것이 목적이다.

견해: "충돌 0"과 "성공/실패" 같은 이산 판정이 안정적이다. 연속 지표(경로 길이·시간)는 변동이 크지만, 이산 결과는 재현성이 높다. 먼저 이산 판정으로 안정된 시험을 만들고, 연속 지표는 추세 관찰용으로 기록한다.

함정: 시뮬 시험 통과가 실기 동작을 보장하지 않는다(10.6.7). 이 시험의 역할은 "회귀 감지" 이지 "동작 증명"이 아니다. 10.6.5의 신뢰도 표가 이 해석의 근거다.

14.6.5 bag 기반 회귀

실제 데이터로 인지·측위를 검증한다(11.3.8).

아니오 데이터셋 bag 재생 + 알고리즘 실행 출력 수집 기준과 비교 허용 범위? 회귀 보고
대상지표
측위궤적 오차·발산 여부
SLAM지도 품질·폐루프
검출정밀도·재현율
분류정확도
처리 시간프레임당 시간

견해: bag 기반 회귀가 인지 개발의 핵심 인프라다(10.6.6). 알고리즘을 고칠 때마다 전체 데이터셋에 실행해 지표 변화를 보면, 개선인지 국소 최적화인지 구분된다. 이것 없이 인지를 개선하는 것은 감에 의존하는 일이다.

견해: 기준값을 "고정 숫자"가 아니라 "이전 실행 대비"로 두는 것도 방법이다. 절대 기준은 데이터셋이 바뀌면 무의미해지지만, 직전 대비 유의미한 악화는 언제나 신호다. 다만 서서히 악화되는 것을 놓칠 수 있으므로, 절대 하한도 함께 두는 것이 좋다.

함정: bag이 크면 시험이 느리다(11.3.8). 대표 소수를 PR에, 전체를 야간에 두고, 데이터셋 자체를 관리(추가·정리)하는 절차를 두라.

14.6.6 HIL — 하드웨어 인 더 루프

실제 하드웨어의 일부를 시험 루프에 넣는다(10.1.4의 ⑤ 충실도).

실제 제어 소프트웨어 실제 컨트롤러 보드 시뮬레이터 물리 · 센서 모델 또는: 실제 액추에이터 부하 없이
구성시험 대상
SW + 실제 컨트롤러 + 시뮬 물리통신·타이밍·펌웨어
SW + 시뮬 + 실제 센서센서 인터페이스
부하 없는 실제 구동계명령·응답·안전
완전 실기최종
얻는 것설명
실제 통신 타이밍시뮬이 근사 못 하는 것(10.6.2의 ③⑤)
펌웨어 검증실제 코드
인터페이스 규약실제 프로토콜
고장 주입케이블 분리 등

견해: HIL이 메우는 간극은 "실행 환경"이다(10.6.2의 ⑤). 시뮬에서 절대 나타나지 않는 통신 지연·지터·임베디드 성능 문제가 여기서 드러난다. 완전 실기 시험보다 안전하고 반복 가능하다는 점이 가치다.

견해: HIL 구축은 비용이 크므로 목적을 좁히라. "모든 것을 HIL로"가 아니라, 시뮬로 검증 불가하고 실기 시험이 위험·비용이 큰 것에 집중한다 — 통신 프로토콜, 타이밍, 고장 주입.

함정: HIL 장비 자체가 유지보수 대상이 된다. 배선·전원·고정구가 손상되면 시험이 실패하는데 그것이 소프트웨어 문제로 보인다. HIL 자체의 자가 점검을 두는 것이 좋다.

14.6.7 현장 승인 시험

배포 전 실기에서 확인하는 것.

항목확인
버전 확인무엇이 설치됐나(11.1.9)
기동·준비정상 부팅(14.2)
규약 점검tf·프레임·센서(7.1.9)
정지 상태 검증움직이기 전(10.6.8의 3단계)
저속 동작기본 기능
안전 기능비상정지·감시 동작
대표 시나리오실제 작업
반복 시험성공률(9.6.9)
성능지연·주기(11.7)
종료·재시작안정성

견해: 승인 시험을 체크리스트로 만들고 결과를 기록하라. 매번 같은 항목을 같은 방법으로 확인해야 비교가 가능하고, 문제가 생겼을 때 "그때는 통과했는가"를 답할 수 있다. 기록이 없으면 승인이 아니라 인상이다.

견해: 10.6.8의 단계적 신뢰 구축이 곧 승인 시험의 순서다. mock → 시뮬 → 실기 정지 → 저속 → 정상속도 → 반복 → 예외. 이 순서를 지키면 문제를 안전한 단계에서 만난다.

정직: 안전 기능의 승인은 이 책의 범위를 넘는다. 비상정지·안전 감시의 검증 방법과 판정 기준은 적용되는 안전 표준과 전문가의 영역이며(14.10), 여기 나열한 항목은 그것을 대체하지 않는다.

14.6.8 시험 전략

무엇을 어디서 시험할 것인가.

확인 대상시험 층
알고리즘 로직단위(14.5)
수치·경계단위
토픽 연결·QoSlaunch_testing
파라미터 적용launch_testing
tf 규약launch_testing
인지 품질bag 회귀
항법 동작시뮬
고장 대응모의 그래프·시뮬
타이밍·통신HIL
펌웨어HIL
최종 동작실기 승인
커밋 단위 + launch_testing 수 분 PR 병합 야간: bag 회귀 + 시뮬 수십 분~시간 릴리스 후보 HIL + 실기 승인 배포

견해: "빠른 것을 자주, 느린 것을 가끔"이 실용적 배치다. PR마다 실행되는 시험은 몇 분 안에 끝나야 하고, 오래 걸리는 것은 야간·릴리스 시점에 실행한다. 이 배치가 CI의 지속 가능성을 결정한다(4.10.2).

견해: 시험이 없는 영역을 명시적으로 인정하라. 모든 것을 시험할 수는 없으므로, "이 부분은 시험하지 않으며 실기 승인에 의존한다" 를 문서로 남기면 위험을 인지하고 관리할 수 있다. 암묵적 공백이 더 위험하다.

견해: 시험 실패를 방치하지 말라(14.5.8의 반복). 빨간 CI가 일상이 되면 시험 체계 전체가 무의미해진다. 고치거나 명시적으로 비활성화하고, 비활성화한 것은 목록으로 관리한다.

14.6.9 함정 카탈로그

#함정증상대처
1ReadyToTest를 준비 완료로 오해불안정조건 대기
2종료 코드 미확인조용한 크래시post_shutdown_test
3통합 시험이 느림CI 무력화분업 배치
4정상 경로만 시험고장 시 미검증이상 대응 시험
5규약 검증 수동지속 안 됨자동화
6빠듯한 시간 기준오탐여유 있게
7연속 지표로 판정불안정이산 판정 우선
8시뮬 통과를 동작 증명으로과신회귀 감지 목적
9bag 시험이 느림실행 안 됨소수/전체 분업
10절대 기준만·상대만놓침둘 다
11HIL 범위 과다비용 폭증목적 좁히기
12HIL 장비 문제를 SW 문제로오진자가 점검
13승인 시험 미기록비교 불가체크리스트·기록
14안전 승인을 자체 판단위험표준·전문가
15시험 공백 미인지위험 누락명시적 문서화
16실패 방치체계 붕괴즉시 대응
17assertWaitFor가 stdout 출력을 못 찾음(167차 실측 발견)타임아웃 실패기본값이 stream='stderr' — print() 등 stdout 출력은 stream='stdout' 명시

14.6.10 체크포인트

  • launch_testing의 다섯 기능과, post_shutdown_test의 가치는?
  • ReadyToTest의 의미와 흔한 오해는?
  • launch_testing이 가장 적합한 시험 대상은?
  • 통합 시험의 여섯 확인 항목을 말할 수 있는가.
  • 규약 검증 자동화가 특히 가치 있는 이유는?
  • 이상 대응 시험이 소홀해지는 이유와 그 위험은?
  • 시뮬 기반 시험에서 판정 기준을 여유 있게 설정하는 근거는?
  • 이산 판정과 연속 지표의 차이, 그리고 권장 순서는?
  • 시뮬 시험의 역할을 "회귀 감지"로 한정하는 근거는?
  • bag 기반 회귀가 인지 개발의 핵심 인프라인 이유는?
  • 기준값을 절대와 상대 둘 다 두라는 조언의 근거는?
  • HIL의 네 구성과, 메우는 간극은 무엇인가.
  • HIL 범위를 좁히라는 조언과, HIL 자체의 함정은?
  • 현장 승인 시험 열 항목과, 기록의 필요성은?
  • 승인 시험 순서가 10.6.8과 같은 이유는?
  • 시험 층별 배치와, "빠른 것을 자주"의 근거는?
  • 시험 공백을 명시적으로 인정하라는 조언의 이유는?

다음 장 14.7는 시험 너머의 품질 — REP 2004 품질 수준과 의존성 위생을 다룬다.