Почему покрытие кода — это не просто галочка в CI/CD?
Представьте: вы написали на Rust модуль для парсинга сложных конфигурационных файлов. Компилятор счастлив, тесты проходят, но спустя месяц продакшн падает с ошибкой на редком краевом случае. Знакомо? Именно здесь на сцену выходит покрытие кода тестами в Rust — метрика, которая показывает, какой процент вашего кода действительно выполняется во время тестов.
Но будем честны: погоня за 100% покрытием — это ловушка. Гораздо важнее понимать, какие строки остались без тестов и почему. В этой статье мы разберем три главных инструмента для Rust: tarpaulin, grcov и встроенный LLVM-инструментарий. Вы узнаете, как превратить сухие проценты в actionable инсайты для рефакторинга.
Зачем разработчику Rust следить за покрытием?
Rust славится своей безопасностью памяти, но это не отменяет логических ошибок. Покрытие кода тестами помогает:
- Обнаружить «мертвый код» — функции или ветки, которые никогда не вызываются.
- Повысить уверенность при рефакторинге — зная, что критические пути покрыты тестами.
- Улучшить документацию — тесты часто служат лучшей документацией, если они покрывают все сценарии.
Главная метрика — line coverage (покрытие строк), но мы также посмотрим на branch coverage (покрытие ветвлений), которое в Rust особенно важно из-за обилия match-выражений.
Инструмент №1: cargo-tarpaulin — простота и скорость
tarpaulin — это самый популярный инструмент для покрытия кода в Rust. Он работает «из коробки» и не требует сложной настройки. Установка:
cargo install cargo-tarpaulin
Запуск базового анализа:
cargo tarpaulin --out Html
Эта команда сгенерирует HTML-отчет в папке tarpaulin-report.html. Откроем его — и увидим цветовую карту вашего проекта: зеленые строки (покрытые тестами), красные (непокрытые) и серые (нерабочие, например, комментарии).
Пример: тестируем простую функцию
Создадим файл src/lib.rs с функцией вычисления факториала:
pub fn factorial(n: u32) -> u32 {
match n {
0 | 1 => 1,
_ => n * factorial(n - 1),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_factorial() {
assert_eq!(factorial(0), 1);
assert_eq!(factorial(1), 1);
assert_eq!(factorial(5), 120);
}
}
Запустим tarpaulin:
cargo tarpaulin --ignore-tests --out Html
Флаг --ignore-tests исключает сам код тестов из отчета. Результат: 100% покрытие для этой функции. Но давайте усложним задачу — добавим обработку ошибок:
pub fn factorial_safe(n: i32) -> Result<u32, String> {
if n < 0 {
return Err("Отрицательное число!".to_string());
}
Ok(factorial(n as u32))
}
Теперь tarpaulin покажет, что ветка Err не покрыта тестами. Добавим тест:
#[test]
fn test_negative() {
assert!(factorial_safe(-5).is_err());
}
После повторного запуска покрытие снова станет 100%.
Продвинутые настройки tarpaulin
Для больших проектов используйте:
--fail-under 80— аварийный выход, если покрытие ниже 80%.--skip-clean— не пересобирать проект, если изменения незначительны.--engine llvm— использовать LLVM-инструментарий (экспериментально, но точнее).
Инструмент №2: grcov — король интеграций
grcov — это более гибкий инструмент, который работает с LLVM-инструментарием. Он особенно полезен, если вы используете CI/CD (GitHub Actions, GitLab CI) и хотите отправлять отчеты в Coveralls или Codecov.
Установка:
cargo install grcov
rustup component add llvm-tools-preview
Запуск в две команды:
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zinstrument-coverage"
cargo build
grcov . --binary-path ./target/debug/ -s . -t html --branch --ignore-not-existing -o ./coverage/
Разберем ключевые флаги:
-Zinstrument-coverage— включает сбор профилирующей информации в LLVM.--branch— включает покрытие ветвлений (очень полезно для match и if).--ignore-not-existing— игнорирует файлы, которые не были скомпилированы.
Сравнение tarpaulin и grcov
Оба инструмента хороши, но есть нюансы:
- Tarpaulin — быстрее, проще, но менее точен для branch coverage.
- Grcov — точнее (использует LLVM), но требует больше настроек и дольше работает.
Для небольших проектов выбирайте tarpaulin. Для корпоративных или open-source — grcov.
Инструмент №3: LLVM-инструментарий — максимальная точность
Если вам нужен полный контроль, используйте встроенные средства LLVM. Этот метод подходит для глубокого анализа, например, для проверки покрытия ассемблерного кода или inline-функций.
Пример запуска:
export RUSTFLAGS="-Cinstrument-coverage"
cargo test
llvm-profdata merge -sparse default.profraw -o default.profdata
llvm-cov show target/debug/your_project -instr-profile=default.profdata -show-line-counts-or-regions -Xdemangler=rustfilt
Результат — детальный построчный отчет в терминале. Этот метод редко используется напрямую, но лежит в основе grcov.
Метрики: что действительно важно?
Не зацикливайтесь на 100%. Вместо этого следите за:
- Покрытием критических путей — например, функции аутентификации или обработки платежей.
- Branch coverage — если в match-выражении 5 веток, а тестами покрыты только 3, это зона риска.
- Δ-покрытие (delta coverage) — как изменилось покрытие