LLVM 소스 기반 코드 커버리지
rustc는 컴파일 시점에 Rust 라이브러리와 바이너리에 추가 명령어와 데이터를 삽입하여 계측하는 명령줄 옵션(-C instrument-coverage)을 통해 상세한 소스 기반 코드 및 테스트 커버리지 분석을 지원합니다.
커버리지 계측은 (MIR 기반 제어 흐름 분석을 바탕으로) 코드 분기 지점에 LLVM 내장 명령어 llvm.instrprof.increment에 대한 호출을 삽입하며, LLVM은 이를 실행 시 정적 카운터를 증가시키는 명령어로 변환합니다. LLVM 커버리지 계측에는 또한 소스 메타데이터를 인코딩하는 커버리지 맵(Coverage Map)이 필요하며, 이 맵은 카운터 ID를 직접 또는 간접적으로 (시작 및 끝 줄과 열을 포함한) 파일 위치에 매핑합니다.
커버리지 계측 여부와 관계없이 Rust 라이브러리는 계측된 바이너리에 링크될 수 있습니다. 프로그램이 실행되고 정상적으로 종료되면, LLVM 라이브러리는 최종 카운터 값을 파일(default.profraw 또는 환경 변수 LLVM_PROFILE_FILE로 설정한 사용자 지정 파일)에 기록합니다.
개발자는 기존 LLVM 커버리지 분석 도구를 사용하여 .profraw 파일을 (그 파일을 생성한 바이너리에서 얻은) 해당 커버리지 맵과 함께 디코딩하고, 분석을 위한 다양한 보고서를 생성합니다. 예를 들면 다음과 같습니다.
자세한 사용법과 예제는 rustc 책에 문서화되어 있습니다.
권장 bootstrap.toml 설정
커버리지 계측 코드를 작업할 때는 보통 [build]에 profiler = true를 설정하여 프로파일러 런타임을 활성화해야 합니다. 이렇게 하면 컴파일러가 계측된 바이너리를 생성할 수 있고, 전체 커버리지 테스트 스위트를 실행할 수 있습니다.
컴파일러와 LLVM에서 디버그 단언을 활성화하는 것을 권장하지만, 필수는 아닙니다.
# Similar to the "compiler" profile, but also enables debug assertions in LLVM.
# These assertions can detect malformed coverage mappings in some cases.
profile = "codegen"
# IMPORTANT: This tells the build system to build the LLVM profiler runtime.
# Without it, the compiler can't produce coverage-instrumented binaries,
# and many of the coverage tests will be skipped.
build.profiler = true
# Enable debug assertions in the compiler.
rust.debug-assertions = true
Rust 심볼 맹글링
-C instrument-coverage는 일관되고 되돌릴 수 있는 이름 맹글링을 보장하기 위해 Rust 심볼 맹글링 v0을 자동으로 활성화합니다(사용자가 rustc를 호출할 때 -C symbol-mangling-version=v0 옵션을 지정한 것과 같습니다). 이렇게 하면 두 가지 중요한 이점이 있습니다.
- LLVM 커버리지 도구는 소스 코드의 일부 변경을 포함하여 여러 번의 실행에 걸쳐 커버리지를 분석할 수 있으므로, 맹글링된 이름은 여러 컴파일에 걸쳐 일관되어야 합니다.
- LLVM 커버리지 보고서는 함수별로 커버리지를 보고할 수 있으며, 제네릭 함수가 여러 가지 타입 치환으로 호출된 경우에는 각 고유 인스턴스화의 커버리지 횟수를 따로 구분하여 보고하기도 합니다.
LLVM 프로파일러 런타임
커버리지 데이터는 실행 가능한 Rust 프로그램을 실행할 때에만 생성됩니다. rustc는 카운터 값을 .profraw 파일에 기록하는 프로그램 훅(예: exit 훅)을 구현한 LLVM 런타임 코드(compiler-rt, 컴파일러 런타임)를 커버리지 계측된 바이너리에 정적으로 링크합니다.
rustc 소스 트리에서 library/profiler_builtins는 LLVM compiler-rt 코드를 Rust 라이브러리 크레이트로 묶습니다. rustc를 빌드할 때 profiler_builtins는 bootstrap.toml에 build.profiler = true가 설정된 경우에만 포함된다는 점에 유의하십시오.
-C instrument-coverage로 컴파일할 때 CStore::postprocess()는 inject_profiler_runtime()을 호출하여 profiler_builtins를 동적으로 로드합니다.
커버리지 계측 테스트
(tests/coverage 테스트 스위트에 대한 compiletest 문서도 참고하십시오.)
MIR의 커버리지 계측은 mir-opt 테스트인 tests/mir-opt/coverage/instrument_coverage.rs로 검증됩니다.
LLVM IR의 커버리지 계측은 coverage-map 모드의 tests/coverage 테스트 스위트로 검증됩니다. 이 테스트들은 테스트 프로그램을 LLVM IR 어셈블리로 컴파일한 다음, src/tools/coverage-dump 도구를 사용하여 최종 바이너리에 포함될 커버리지 매핑을 추출하고 보기 좋게 출력합니다.
커버리지 계측과 커버리지 보고의 종단 간 테스트는 coverage-run 모드의 tests/coverage 테스트 스위트와 tests/coverage-run-rustdoc 테스트 스위트가 수행합니다. 이 테스트들은 커버리지 계측을 적용하여 테스트 프로그램을 컴파일하고 실행한 다음, LLVM 도구를 사용하여 커버리지 데이터를 사람이 읽을 수 있는 커버리지 보고서로 변환합니다.
coverage-run모드의 테스트에는 암묵적으로//@ needs-profiler-runtime지시문이 적용되므로, 프로파일러 런타임이bootstrap.toml에서 활성화되지 않았다면 건너뜁니다.
마지막으로 tests/codegen-llvm/instrument-coverage/testprog.rs 테스트는 간단한 Rust 프로그램을 -C instrument-coverage로 컴파일하고, 컴파일된 프로그램의 LLVM IR을 커버리지가 활성화된 프로그램에 대해 예상되는 LLVM IR 명령어 및 구조화된 데이터와 비교합니다. 여기에는 커버리지 맵 관련 메타데이터와 런타임 카운터를 증가시키는 LLVM 내장 함수 호출에 대한 다양한 검사가 포함됩니다.
coverage, coverage-run-rustdoc, mir-opt 테스트의 예상 결과는 다음 명령을 실행하여 갱신할 수 있습니다:
./x test coverage --bless
./x test coverage-run-rustdoc --bless
./x test tests/mir-opt --bless