Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

-Cinstrument-coverage produces invalid coverage mapping for the test harness's entry point #110749

Closed
Zalathar opened this issue Apr 24, 2023 · 7 comments · Fixed by #111042
Closed
Labels
A-code-coverage Area: Source-based code coverage (-Cinstrument-coverage) C-bug Category: This is a bug.

Comments

@Zalathar
Copy link
Contributor

Zalathar commented Apr 24, 2023

Take this simple crate:

// THIS LINE SHOULD NOT BE DUPLICATED

fn main() {}

#[test]
fn my_test() {}

If I try to gather code coverage for cargo test, I end up with the first line mysteriously duplicated as a covered region:

$ cargo llvm-cov test --text
   Compiling cov-playground v0.1.0 (/Users/me/Dev/rust/cov-playground)
    Finished test [unoptimized + debuginfo] target(s) in 0.08s
     Running unittests src/main.rs (target/llvm-cov-target/debug/deps/cov_playground-e5b52a4d8622a05a)

running 1 test
test my_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

    1|      1|// THIS LINE SHOULD NOT BE DUPLICATED// THIS LINE SHOULD NOT BE DUPLICATED
    2|       |
    3|      0|fn main() {}
    4|       |
    5|      1|#[test]
    6|      1|fn my_test() {}

(I'm using cargo-llvm-cov for convenience, but the same thing happens when I use RUSTC_FLAGS="-Cinstrument-coverage" and process the coverage data manually with llvm-profdata and llvm-cov.)

Note that in the report produced by llvm-cov, the first line of the file appears twice. It is marked as having executed once, even though it is a comment.

Instead, the line should only appear once, and its execution count (if any) should be the execution count of the actual code on that line.

Meta

$ rustc --version --verbose
rustc 1.69.0 (84c898d65 2023-04-16)
binary: rustc
commit-hash: 84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc
commit-date: 2023-04-16
host: aarch64-apple-darwin
release: 1.69.0
LLVM version: 15.0.7
$ cargo llvm-cov --version
cargo-llvm-cov 0.5.11
@Zalathar Zalathar added the C-bug Category: This is a bug. label Apr 24, 2023
@Zalathar
Copy link
Contributor Author

I did some initial investigation, and discovered a few things:

  • The coverage mapping that rustc embeds in the test binary contains entries for two different functions named main. One is the actual main that appears in the file, and the other is the main that is injected by the test harness.
  • The mapping for the injected main function spans from (1,0) to (1,1). Coverage mapping regions use 1-based line numbers and column numbers, so a column number of 0 should be impossible.*
  • This unexpected column number confuses the processing code in llvm-cov, and results in line 1 being duplicated in the output.

*: There are special circumstances where it's legal for a region to have 0 for its start column and its end column, but that isn't what's happening here.

@Zalathar Zalathar changed the title -Cinstrument-coverage produces invalid coverage mapping for the test runner's entry point -Cinstrument-coverage produces invalid coverage mapping for the test harness's entry point Apr 24, 2023
@Zalathar
Copy link
Contributor Author

The origin of the bogus (1,0) to (1,1) region seems to be:

  • The test harness entry point is created with a dummy span:

    let def_site = DUMMY_SP.with_def_site_ctxt(expn_id.to_expn_id());

  • When that dummy span is converted into a region, its 0-based start CharPos is decremented in an attempt to make the region non-empty. This decrement overflows to usize::MAX, and a subsequent increment (to make it 1-based) overflows back to 0.

    /// Convert the Span into its file name, start line and column, and end line and column
    fn make_code_region(
    source_map: &SourceMap,
    file_name: Symbol,
    source_file: &Lrc<SourceFile>,
    span: Span,
    body_span: Span,
    ) -> CodeRegion {
    let (start_line, mut start_col) = source_file.lookup_file_pos(span.lo());
    let (end_line, end_col) = if span.hi() == span.lo() {
    let (end_line, mut end_col) = (start_line, start_col);
    // Extend an empty span by one character so the region will be counted.
    let CharPos(char_pos) = start_col;
    if span.hi() == body_span.hi() {
    start_col = CharPos(char_pos - 1);
    } else {
    end_col = CharPos(char_pos + 1);
    }
    (end_line, end_col)
    } else {
    source_file.lookup_file_pos(span.hi())
    };
    let start_line = source_map.doctest_offset_line(&source_file.name, start_line);
    let end_line = source_map.doctest_offset_line(&source_file.name, end_line);
    CodeRegion {
    file_name,
    start_line: start_line as u32,
    start_col: start_col.to_u32() + 1,
    end_line: end_line as u32,
    end_col: end_col.to_u32() + 1,
    }
    }

@Zalathar
Copy link
Contributor Author

Zalathar commented Apr 24, 2023

My first thought was to sidestep the whole issue by adding #[no_coverage] to the test harness's injected main.

However, I couldn’t get that to work, because #[no_coverage] is unstable and can't necessarily be used by the crate it is being injected into.

So I think the fix will involve having the instrumentor detect and skip code with dummy spans.

@Jules-Bertholet
Copy link
Contributor

@rustbot label +A-code-coverage

@rustbot rustbot added the A-code-coverage Area: Source-based code coverage (-Cinstrument-coverage) label Apr 26, 2023
@Zalathar
Copy link
Contributor Author

#97368 fixes the overflow, which gets rid of the duplicate line.

However, the instrumentor still ends up emitting an empty region from (1,1) to (1,1) for the test harness's fn main, so I'd still like to find a way to avoid instrumenting that function.

@Zalathar
Copy link
Contributor Author

I think the empty region now confusing the llvm-cov reporter in more subtle ways, because now I see weird stuff like this:

    1|      1|#![allow(dead_code)]
    2|      1|
    3|      1|fn unused() {}
              ^0
    4|       |
    5|      0|fn main() {}
    6|       |
    7|      1|#[test]
    8|      1|fn my_test() {}

It makes sense that line 1 is “covered” (via the injected fn main), but it's strange that lines 2 and 3 also have non-zero counts.

@Zalathar
Copy link
Contributor Author

It looks like llvm-cov does have special handling for empty regions, but it doesn't properly anticipate the first region being empty, and ends up in a slightly confused state.

https://github.com/rust-lang/llvm-project/blob/ea6fa9c2d43aaf0f11559719eda9b54d356d5416/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp#L590-L603

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-code-coverage Area: Source-based code coverage (-Cinstrument-coverage) C-bug Category: This is a bug.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants