coverage: Detect unused local file IDs to avoid an LLVM assertion
This case can't actually happen yet (other than via a testing flag), because currently all of a function's spans must belong to the same file and expansion. But this will be an important edge case when adding expansion region support.
This commit is contained in:
parent
8cd8b23b9e
commit
078144fdfa
8 changed files with 87 additions and 6 deletions
|
|
@ -155,6 +155,20 @@ pub(crate) struct Regions {
|
|||
impl Regions {
|
||||
/// Returns true if none of this structure's tables contain any regions.
|
||||
pub(crate) fn has_no_regions(&self) -> bool {
|
||||
// Every region has a span, so if there are no spans then there are no regions.
|
||||
self.all_cov_spans().next().is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn all_cov_spans(&self) -> impl Iterator<Item = &CoverageSpan> {
|
||||
macro_rules! iter_cov_spans {
|
||||
( $( $regions:expr ),* $(,)? ) => {
|
||||
std::iter::empty()
|
||||
$(
|
||||
.chain( $regions.iter().map(|region| ®ion.cov_span) )
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
let Self {
|
||||
code_regions,
|
||||
expansion_regions,
|
||||
|
|
@ -163,11 +177,13 @@ impl Regions {
|
|||
mcdc_decision_regions,
|
||||
} = self;
|
||||
|
||||
code_regions.is_empty()
|
||||
&& expansion_regions.is_empty()
|
||||
&& branch_regions.is_empty()
|
||||
&& mcdc_branch_regions.is_empty()
|
||||
&& mcdc_decision_regions.is_empty()
|
||||
iter_cov_spans!(
|
||||
code_regions,
|
||||
expansion_regions,
|
||||
branch_regions,
|
||||
mcdc_branch_regions,
|
||||
mcdc_decision_regions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use rustc_abi::Align;
|
|||
use rustc_codegen_ssa::traits::{
|
||||
BaseTypeCodegenMethods as _, ConstCodegenMethods, StaticCodegenMethods,
|
||||
};
|
||||
use rustc_index::IndexVec;
|
||||
use rustc_middle::mir::coverage::{
|
||||
BasicCoverageBlock, CovTerm, CoverageIdsInfo, Expression, FunctionCoverageInfo, Mapping,
|
||||
MappingKind, Op,
|
||||
|
|
@ -125,6 +126,12 @@ fn fill_region_tables<'tcx>(
|
|||
|
||||
let local_file_id = covfun.virtual_file_mapping.push_file(&source_file);
|
||||
|
||||
// If this testing flag is set, add an extra unused entry to the local
|
||||
// file table, to help test the code for detecting unused file IDs.
|
||||
if tcx.sess.coverage_inject_unused_local_file() {
|
||||
covfun.virtual_file_mapping.push_file(&source_file);
|
||||
}
|
||||
|
||||
// In rare cases, _all_ of a function's spans are discarded, and coverage
|
||||
// codegen needs to handle that gracefully to avoid #133606.
|
||||
// It's hard for tests to trigger this organically, so instead we set
|
||||
|
|
@ -177,6 +184,19 @@ fn fill_region_tables<'tcx>(
|
|||
}
|
||||
}
|
||||
|
||||
/// LLVM requires all local file IDs to have at least one mapping region.
|
||||
/// If that's not the case, skip this function, to avoid an assertion failure
|
||||
/// (or worse) in LLVM.
|
||||
fn check_local_file_table(covfun: &CovfunRecord<'_>) -> bool {
|
||||
let mut local_file_id_seen =
|
||||
IndexVec::<u32, _>::from_elem_n(false, covfun.virtual_file_mapping.local_file_table.len());
|
||||
for cov_span in covfun.regions.all_cov_spans() {
|
||||
local_file_id_seen[cov_span.file_id] = true;
|
||||
}
|
||||
|
||||
local_file_id_seen.into_iter().all(|seen| seen)
|
||||
}
|
||||
|
||||
/// Generates the contents of the covfun record for this function, which
|
||||
/// contains the function's coverage mapping data. The record is then stored
|
||||
/// as a global variable in the `__llvm_covfun` section.
|
||||
|
|
@ -185,6 +205,10 @@ pub(crate) fn generate_covfun_record<'tcx>(
|
|||
global_file_table: &GlobalFileTable,
|
||||
covfun: &CovfunRecord<'tcx>,
|
||||
) {
|
||||
if !check_local_file_table(covfun) {
|
||||
return;
|
||||
}
|
||||
|
||||
let &CovfunRecord {
|
||||
mangled_function_name,
|
||||
source_hash,
|
||||
|
|
|
|||
|
|
@ -776,7 +776,8 @@ fn test_unstable_options_tracking_hash() {
|
|||
CoverageOptions {
|
||||
level: CoverageLevel::Mcdc,
|
||||
no_mir_spans: true,
|
||||
discard_all_spans_in_codegen: true
|
||||
discard_all_spans_in_codegen: true,
|
||||
inject_unused_local_file: true,
|
||||
}
|
||||
);
|
||||
tracked!(crate_attr, vec!["abc".to_string()]);
|
||||
|
|
|
|||
|
|
@ -195,6 +195,11 @@ pub struct CoverageOptions {
|
|||
/// regression tests for #133606, because we don't have an easy way to
|
||||
/// reproduce it from actual source code.
|
||||
pub discard_all_spans_in_codegen: bool,
|
||||
|
||||
/// `-Zcoverage-options=inject-unused-local-file`: During codegen, add an
|
||||
/// extra dummy entry to each function's local file table, to exercise the
|
||||
/// code that checks for local file IDs with no mapping regions.
|
||||
pub inject_unused_local_file: bool,
|
||||
}
|
||||
|
||||
/// Controls whether branch coverage or MC/DC coverage is enabled.
|
||||
|
|
|
|||
|
|
@ -1413,6 +1413,7 @@ pub mod parse {
|
|||
"mcdc" => slot.level = CoverageLevel::Mcdc,
|
||||
"no-mir-spans" => slot.no_mir_spans = true,
|
||||
"discard-all-spans-in-codegen" => slot.discard_all_spans_in_codegen = true,
|
||||
"inject-unused-local-file" => slot.inject_unused_local_file = true,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -371,6 +371,11 @@ impl Session {
|
|||
self.opts.unstable_opts.coverage_options.discard_all_spans_in_codegen
|
||||
}
|
||||
|
||||
/// True if testing flag `-Zcoverage-options=inject-unused-local-file` was passed.
|
||||
pub fn coverage_inject_unused_local_file(&self) -> bool {
|
||||
self.opts.unstable_opts.coverage_options.inject_unused_local_file
|
||||
}
|
||||
|
||||
pub fn is_sanitizer_cfi_enabled(&self) -> bool {
|
||||
self.opts.unstable_opts.sanitizer.contains(SanitizerSet::CFI)
|
||||
}
|
||||
|
|
|
|||
7
tests/coverage/unused-local-file.coverage
Normal file
7
tests/coverage/unused-local-file.coverage
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
LL| |//@ edition: 2021
|
||||
LL| |
|
||||
LL| |// Force this function to be generated in its home crate, so that it ends up
|
||||
LL| |// with normal coverage metadata.
|
||||
LL| |#[inline(never)]
|
||||
LL| 1|pub fn external_function() {}
|
||||
|
||||
22
tests/coverage/unused-local-file.rs
Normal file
22
tests/coverage/unused-local-file.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//! If we give LLVM a local file table for a function, but some of the entries
|
||||
//! in that table have no associated mapping regions, then an assertion failure
|
||||
//! will occur in LLVM. We therefore need to detect and skip any function that
|
||||
//! would trigger that assertion.
|
||||
//!
|
||||
//! To test that this case is handled, even before adding code that could allow
|
||||
//! it to happen organically (for expansion region support), we use a special
|
||||
//! testing-only flag to force it to occur.
|
||||
|
||||
//@ edition: 2024
|
||||
//@ compile-flags: -Zcoverage-options=inject-unused-local-file
|
||||
|
||||
// The `llvm-cov` tool will complain if the test binary ends up having no
|
||||
// coverage metadata at all. To prevent that, we also link to instrumented
|
||||
// code in an auxiliary crate that doesn't have the special flag set.
|
||||
|
||||
//@ aux-build: discard_all_helper.rs
|
||||
extern crate discard_all_helper;
|
||||
|
||||
fn main() {
|
||||
discard_all_helper::external_function();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue