Skip to content

ADR-21: Rust 매크로 기반 테스트 감지 ​

🇺🇸 English Version

날짜작성자레포
2025-12-27@specvitalcore

컨텍스트 ​

문제 상황 ​

SpecVital Core의 Rust 파서가 표준 #[test] 속성 함수만 감지. 성숙한 Rust 코드베이스에서 일반적인 매크로 기반 패턴으로 정의된 테스트 미감지.

발견 경위 ​

BurntSushi/ripgrep 리포지토리 검증 결과 상당한 탐지 격차 확인:

지표값
실제 테스트 수 (cargo test)1,111개
파서 탐지 결과436개
차이-675 (61% 미탐지)

근본 원인 분석으로 두 가지 미탐지 테스트 범주 식별:

범주 1: 이름 기반 매크로 (~330개 테스트)

이름에 "test"가 포함된 테스트 함수 생성 매크로:

rust
// ripgrep/tests/integration.rs
rgtest!(f1, |dir: Dir, mut cmd: TestCommand| {
    // 테스트 본문
});

범주 2: 정의 기반 매크로 (~264개 테스트)

내부적으로 #[test]로 확장되는 정의를 가진 매크로:

rust
// 동일 파일 내 정의
macro_rules! syntax {
    ($name:ident, $re:expr, $hay:expr) => {
        #[test]
        fn $name() {
            // ...
        }
    };
}

// 사용
syntax!(test_literal, r"foo", "foo");

기술적 제약 ​

  1. 단일 파일 파싱 제약 (ADR-14): Core 파서가 크로스 파일 의존성 해석 없이 파일별 독립 작동
  2. Tree-sitter 한계: AST 파싱이 구조 제공하지만 매크로 확장 불가
  3. Rust 매크로 유형:
    • 선언적 매크로 (macro_rules!): 동일 파일 내 정의 분석 가능
    • 절차적 속성 매크로 (rstest, test_case): 외부 크레이트 구현, 컴파일러 확장 없이 불투명

전략적 필요성 ​

61% 탐지 실패는 Rust 생태계에 대한 플랫폼 신뢰도 훼손. 솔루션은 아키텍처 제약 내에서 정확도 개선 균형 필요.

결정 ​

매크로 이름과 동일 파일 매크로 정의를 모두 분석하는 2단계 매크로 기반 테스트 감지 전략 구현.

1단계: 이름 기반 휴리스틱 ​

매크로 이름에 "test"가 포함된 매크로 호출 탐지 (대소문자 무시):

rust
// 탐지됨: 매크로 이름에 "test" 포함
rgtest!(test_name, |...| { ... });
test_case!(name, input, expected);

2단계: 정의 분석 ​

동일 파일 내 macro_rules! 정의에서 매크로 본문에 #[test] 속성 확장 포함 여부 분석:

rust
// 1단계: 매크로 정의 수집
macro_rules! syntax {  // <- 정의 발견
    ($name:ident, $re:expr, $hay:expr) => {
        #[test]  // <- #[test]로 확장
        fn $name() { ... }
    };
}

// 2단계: 테스트 생성 매크로 호출 카운트
syntax!(test_one, ...);   // <- 테스트로 카운트
syntax!(test_two, ...);   // <- 테스트로 카운트

구현 ​

pkg/parser/strategies/cargotest/definition.go에서 2패스 AST 분석:

go
func parseRustAST(root *sitter.Node, source []byte) []domain.TestSuite {
    // 패스 1: #[test] 생성 매크로 정의 수집
    macroRegistry := collectTestMacroDefinitions(root, source)

    // 패스 2: 레지스트리 + 이름 휴리스틱으로 모든 노드 순회
    var tests []domain.Test
    walkTree(root, func(node *sitter.Node) {
        switch node.Type() {
        case "function_item":
            if hasTestAttribute(node) {
                tests = append(tests, extractAttributeTest(node))
            }
        case "macro_invocation":
            if isTestMacro(node, macroRegistry) {
                tests = append(tests, extractMacroTest(node))
            }
        }
    })
    return tests
}

주요 함수:

  • collectTestMacroDefinitions(): 테스트 생성 매크로 레지스트리 구축을 위한 첫 번째 패스
  • tokenTreeHasTestAttribute(): macro_rules! 본문에서 #[test] 재귀 검색
  • isTestMacro(): 레지스트리 먼저 확인, 이름 휴리스틱으로 폴백

외부 매크로 폴백 ​

외부 크레이트의 절차적 속성 매크로(rstest, test_case)는 크레이트 해석 없이 구현 분석 불가하므로 이름 기반 휴리스틱 폴백 사용.

고려한 옵션 ​

옵션 A: 2단계 탐지 (선택됨) ​

이름 기반 휴리스틱과 동일 파일 macro_rules! 정의 분석 결합.

장점:

  • 일반적 패턴에 높은 정확도: 명명 규칙 및 정의 기반 매크로 모두 커버
  • 단일 파일 제약 유지: 크로스 파일 해석 불필요
  • 결정론적: 동일 파일 분석은 완전히 결정론적
  • 합리적 외부 매크로 처리: 이름 휴리스틱으로 rstest, test_case 호출 포착

단점:

  • 정의 분석 범위 제한: 동일 파일의 macro_rules!에서만 작동
  • 휴리스틱이 엣지 케이스 놓칠 수 있음: 이름에 "test" 없고 다른 파일에 정의된 매크로
  • 2패스 오버헤드: 두 번의 AST 순회 필요

옵션 B: 이름 전용 휴리스틱 ​

매크로 이름에 "test"가 있는 매크로 호출만 탐지.

장점:

  • 간단한 구현: 단일 패스, 직관적 패턴 매칭
  • 모든 매크로 유형에 적용: 이름 휴리스틱이 선언적 및 절차적 매크로 모두에 적용

단점:

  • 정의 기반 매크로 미탐지: syntax!, matches! 등 미탐지 (ripgrep에서 ~264개 테스트)
  • 높은 미탐지율: 매크로 기반 테스트의 약 50%만 포착
  • 명명 규칙 의존: 프로젝트가 "test" 명명 규칙을 따른다고 가정

옵션 C: 컴파일러를 통한 전체 매크로 확장 ​

rustc 또는 cargo expand를 호출하여 완전히 확장된 소스 코드 확보.

장점:

  • 100% 정확도: 컴파일러 확장이 모든 매크로 유형 올바르게 처리
  • 휴리스틱 불필요: 컴파일러로부터 실측값

단점:

  • 컴파일 필요: 의존성 해석, 크레이트 다운로드, 빌드 필요
  • 성능 영향: 정적 파싱의 밀리초 대비 크레이트당 수초~수분
  • 환경 의존성: Rust 툴체인 필요, 불완전한 프로젝트에서 실패 가능
  • 정적 분석 원칙 위반 (ADR-01): 정적에서 동적 분석으로 전환

옵션 D: 외부 크레이트 해석 ​

외부 파일 및 크레이트에서 macro_rules! 정의를 해석하기 위한 의존성 그래프 구축.

장점:

  • 옵션 A보다 높은 커버리지: 동일 크레이트 내 다른 파일에 정의된 매크로 포착

단점:

  • 단일 파일 제약 위반 (ADR-14): 다중 파일 조정 필요
  • 복잡성 폭발: 모듈 해석, use 문, 재내보내기
  • 상당한 아키텍처 변경: 현재 파일별 접근 방식과 근본적으로 다름

결과 ​

긍정적 ​

  1. 극적인 정확도 개선

    • ripgrep: 436 → ~1,030 탐지 테스트 (61% 미탐지 → ~7% 미탐지)
    • 실제 Rust 테스트 패턴 대부분 커버
  2. 아키텍처 보존

    • 단일 파일 파싱 제약 유지
    • 외부 의존성 또는 컴파일 불필요
    • ADR-14 경계 결정과 일관성
  3. 점진적 개선

    • 두 단계를 독립적으로 활성화/조정 가능
    • 이름 휴리스틱이 기준선 제공; 정의 분석이 정밀도 추가
  4. 프레임워크 독립적

    • 커스텀 프로젝트 매크로(ripgrep의 rgtest!)와 작동
    • 외부 프레임워크(rstest, test_case)도 이름 휴리스틱으로 작동

부정적 ​

  1. 동일 파일 범위 제한

    • 별도 파일의 macro_rules! 정의 분석 불가
    • 중앙화된 테스트 유틸리티가 있는 프로젝트에서 격차 가능
    • 완화책: 이름 휴리스틱이 일반적 패턴에 폴백 제공
  2. 절차적 매크로 불투명성

    • rstest, test_case 확장 로직 분석 불가
    • 탐지에 명명 규칙 의존
    • 완화책: 이러한 프레임워크는 "test" 명명 규칙 준수; 미탐지 드뭄
  3. 2패스 성능 비용

    • 정의 수집을 위한 추가 AST 순회
    • 완화책: 오버헤드 최소 (Rust 파일에서 ~10-20%)

탐지 커버리지 매트릭스 ​

매크로 유형동일 파일 정의다른 파일외부 크레이트
이름에 "test" 포함탐지됨탐지됨탐지됨
#[test]로 확장탐지됨미탐지미탐지
둘 다 아님미탐지미탐지미탐지

참조 ​

Open-source test coverage insights