테스트 케이스 리듀서는 과소평가된 디버깅 도구

2 hours ago 2
  • 큰 입력에서 문제를 일으키는 부분을 찾을 때 테스트 케이스 리듀서는 입력을 자동으로 줄여 디버깅을 쉽게 만듦
  • 리듀서는 프로그램, 입력, 흥미성 테스트를 받아 더 짧은 후보 입력이 같은 문제를 재현하는지 반복 확인함
  • 단순한 줄 삭제 리듀서도 /usr/share/dict/words에서 긴 단어 하나를 남기고, C 예제에서는 10초 미만에 78줄을 54줄로 줄임
  • 흥미성 테스트는 과도한 축소, 느린 실행, 무한 실행, 병렬 실행 환경 때문에 정확하고 빠르게 작성해야 함
  • 입력 길이뿐 아니라 오류 발생 빈도나 실행 추적 길이 같은 지표를 흥미성 테스트에 넣으면 비결정적 버그와 큰 추적 로그 디버깅에 도움이 됨

테스트 케이스 축소

  • 큰 입력에서 프로그램이 충돌하고 입력의 어느 부분이 원인인지 모를 때, 입력을 줄이면 문제 원인 파악이 쉬워짐
  • 수동 축소는 텍스트 편집기에서 입력 일부를 삭제한 뒤 같은 충돌이 유지되는지 확인하는 방식임
  • 수동 축소는 사람이 많은 삭제 기회를 놓치기 쉽고, 삭제 후 프로그램이 정상 종료되거나 다른 정상 오류를 낼 수 있음
  • 서로 떨어진 A와 B 부분을 함께 삭제해야 효과가 나는 경우 검색 공간이 크게 늘어남

테스트 케이스 리듀서의 기본 구조

  • 테스트 케이스 리듀서는 프로그램, 입력, 흥미성 테스트를 받아 입력을 더 짧게 만드는 도구임
  • 흥미성 테스트는 축소된 입력이 관심 있는 오류를 재현하면 0을 반환하고, 그렇지 않으면 0이 아닌 값을 반환함
  • 테스트 케이스 리듀서는 95~99% 축소가 흔하며, 디버깅을 훨씬 쉽게 만들 수 있음
  • 리듀서는 입력의 어떤 부분을 제거해야 하는지 의미적으로 이해하지 않아도 작동함
  • 단순 리듀서 예제

    • 예제 프로그램은 파일에서 줄을 읽고 길이가 25보다 큰 줄이 있으면 Word too long을 출력함
    • 흥미성 테스트는 프로그램 출력에 Word too long이 있으면 0, 없으면 1을 반환함
    • 단순 Python 리듀서는 입력을 줄 단위로 읽고, 한 줄을 삭제한 후보 입력을 임시 파일에 쓴 뒤 흥미성 테스트를 실행함
    • 후보 입력이 흥미롭다면 현재 입력을 후보로 바꾸고, 더 이상 줄일 수 없으면 결과를 stdout에 출력함
    • /usr/share/dict/words에 실행한 결과는 antidisestablishmentarianism만 남김

더 강한 리듀서와 Shrink Ray

  • 78줄 C 프로그램 예제는 FAST=0과 FAST=1 설정에서 서로 다른 출력을 내는 문제를 다룸
  • 흥미성 테스트는 두 설정으로 컴파일한 뒤 FAST=0 출력이 0d754a56이고 FAST=1 출력이 그 값과 다를 때만 통과함
  • 단순 리듀서는 10초 미만에 78줄 C 입력을 54줄로 줄여 줄 수 기준 약 30% 축소함
  • 흥미로운 후보를 찾을 때마다 처음 줄부터 다시 삭제하도록 i=0을 추가하면 실행 시간은 거의 10배가 되고 3줄을 더 줄임
  • Shrink Ray는 여러 축소 규칙과 병렬 실행을 제공하며, --no-clang-delta를 붙이면 C에 대한 특수 지식을 쓰지 않음
  • Shrink Ray는 약 15분 후 바이트 기준 60% 넘게 입력을 줄였고, 다른 사례에서는 약 20분 뒤 90% 축소를 찾은 뒤 99%까지 더 줄임
  • Shrink Ray는 표준 주석 문법을 알고 초기에 제거를 시도하며, 정수를 더 작은 값으로 줄이는 시도도 수행함

흥미성 테스트 작성의 어려움

  • 테스트 케이스 리듀서는 흥미성 테스트를 문자 그대로 따르므로, 테스트가 잘못 통과하면 원하는 지점보다 더 줄어드는 과도한 축소가 발생함
  • Shrink Ray는 흥미성 테스트가 빈 입력을 받아들이는지 명시적으로 검사하며, 이런 상황은 자주 발생할 수 있음
  • C 예제에서 단순히 두 출력이 다른지만 확인하면 중요하지 않거나 오해를 부를 수 있는 출력 차이가 흥미로운 입력으로 분류될 수 있음
  • test "$slow_out" = "0d754a56" 검사는 느린 버전이 실제로 기대한 동작을 하는지 확인해 과도한 축소 가능성을 낮춤
  • 속도와 타임아웃

    • 흥미성 테스트가 빠르면 리듀서가 초당 수백 번 실행할 수 있음
    • 중간 크기 예제도 수십만 번의 축소 시도로 이어질 수 있어 흥미성 테스트 최적화가 전체 시간에 큰 영향을 줌
    • 자동 코어 덤프 생성을 끄는 방식으로 흥미성 테스트를 약 3배 빠르게 만든 사례가 있음
    • 리듀서는 i-=1 같은 줄을 제거해 종료되는 프로그램을 무한 실행 프로그램으로 바꿀 수 있음
    • 프로그램이 0.1초에 실행되는데 타임아웃을 60초로 잡으면 전체 축소가 큰 폭으로 느려짐
    • 빠른 프로그램은 timeout을 1~2초로 올림 처리하고, 그 외에는 초기 실행 시간의 약 1.5~2배로 잡는 방식이 사용됨
  • 병렬 실행

    • Shrink Ray 같은 리듀서는 흥미성 테스트를 병렬로 실행함
    • Shrink Ray는 각 흥미성 테스트를 임시 디렉터리에서 실행하고 해당 디렉터리를 자동 정리함
    • 임시 디렉터리만으로 충분하지 않은 경우도 있으며, 필요한 조치는 사례별로 달라짐

흥미성 테스트로 결정성 유도

  • 예제 조각은 len([])==0 때문에 0으로 나누기 오류를 만들지만, random.random() < 0.33 조건 때문에 약 3분의 1 실행에서만 문제가 발생함
  • 비결정적 버그는 오류가 무작위로 나타났다 사라져 가설 검증을 더 어렵고 오래 걸리게 만듦
  • 리듀서가 random.random() 호출을 제거하거나 조건식을 바꾸면 비결정적 오류가 결정적 오류로 바뀔 수 있음
  • 실제 비결정성은 입력의 여러 부분이 불리하게 상호작용하는 경우가 많아 제거가 어려울 수 있음
  • 테스트 케이스 리듀서는 입력 길이를 “더 좋음”의 대리 지표로 쓰는 언덕 오르기 알고리듬처럼 동작함
  • 언덕 오르기 접근은 지역 최적점에 갇히기 쉽고, 짧은 입력이 항상 오류 탐색에 더 좋은 것은 아님
  • 반복 실행 방식

    • 비결정적 버그를 다룰 때 흥미성 테스트가 입력을 여러 번 실행하고, 관심 오류가 한 번 이상 발생하면 입력을 받아들이는 방식이 사용됨
    • 이 방식은 오류 발생 빈도를 높이는 데 도움이 될 수 있음
    • 한 번 이상 발생하면 통과하는 테스트는 비결정적 입력도 받아들이므로, 축소가 진행되며 비결정성이 오히려 커질 수 있음
    • 더 엄격한 방식은 n번 반복 모두에서 오류가 발생할 때만 입력을 받아들이는 테스트임
    • 엄격한 테스트는 초기 입력이 통과할 확률이 낮아 Shrink Ray를 시작하기 어렵고, 예제의 3회 반복 조건에서는 초기 통과 확률이 3.6%임
    • 실용적 우회는 먼저 “n회 중 1회 이상 오류” 테스트로 시작하고, 오류가 더 자주 발생하는 축소 입력을 얻으면 “n회 연속 오류” 테스트로 바꾸는 방식임

전역 카운터와 다른 목표 지표

  • 수동 개입은 강력하지만 Shrink Ray를 지켜봐야 하고 개입 시점을 놓치기 쉬움
  • 입력 길이가 아닌 다른 속성으로 리듀서를 유도하려면 단일 흥미성 테스트 안에서 해당 속성을 강제할 수 있음
  • yk 디버깅에서는 입력 길이보다 실행 추적 길이, 즉 프로그램이 실행한 명령 수에 가까운 값이 더 중요하게 쓰임
  • YKD_LOG="$t:jit-asm" 출력은 텍스트 추적 IR과 기계어 명령을 파일에 쓰며, 짧은 jit-asm 출력이 디버깅을 쉽게 만듦
  • wc -l은 로그 파일 줄 수를 세어 추적 길이에 가까운 대리 지표로 쓰임
  • 흥미성 테스트는 현재 추적 줄 수가 이전 최저 줄 수보다 크면 입력을 흥미롭지 않은 것으로 처리하고, 최저값은 /tmp/global_best에 저장함
  • 이 방식은 병렬 축소에서 안전하지 않고 리듀서 호출 방식에 대한 가정을 포함하지만, 버리는 짧은 스크립트로는 감수 가능한 불완전성으로 취급됨
  • yk 세그폴트 사례에서 일반 축소는 40K줄 추적을 남겼지만, 이 기법은 더 큰 축소 입력 대신 10.1K줄 추적을 만들었고 30분 안에 근본 버그를 파악하게 함

핵심 정리

  • 테스트 케이스 리듀서는 컴파일러 작성자에게만 유용한 도구가 아니며, 비컴파일러 문제에도 쓰일 수 있음
  • 입력 길이를 줄이는 기본 목적 외에도 오류 빈도, 벽시계 시간, 비결정성 수준, 추적 길이 같은 속성을 흥미성 테스트로 유도할 수 있음
  • 흥미성 테스트의 정확도, 실행 속도, 타임아웃, 병렬 실행 안전성이 리듀서의 실제 효과를 좌우함
  • 리듀서는 입력과 프로그램의 의미를 거의 이해하지 않아도, 문제를 더 작은 형태로 유지해 디버깅 생산성을 높일 수 있음
Read Entire Article