Skip to content

Coverage instrumentation loses information in macro-generated code #123567

Open
@narpfel

Description

@narpfel

I tried this code (using thiserror v1.0.58):

use thiserror::Error;

#[derive(Error, Debug)]
pub enum E {
    #[error("{}", if *.0 { "a" } else { "b" })]
    X(bool)
}

#[cfg(test)]
mod tests {
    use super::E;

    #[test]
    fn test() {
        assert_eq!(E::X(true).to_string(), "a");
    }
}

I expected to see this happen: When running the test with cargo llvm-cov --text, the else arm should be marked as uncovered while the then arm should be shown as covered.

Instead, this happened: The code in the #[error] attribute does not have any coverage data. When run with cargo llvm-cov --text, this is the result:

$ cargo llvm-cov --text
[...]
    1|       |use thiserror::Error;
    2|       |
    3|      1|#[derive(Error, Debug)]
    4|       |pub enum E {
    5|       |    #[error("{}", if *.0 { "a" } else { "b" })]
    6|       |    X(bool)
    7|       |}
    8|       |
    9|       |#[cfg(test)]
   10|       |mod tests {
   11|       |    use super::E;
   12|       |
   13|       |    #[test]
   14|      1|    fn test() {
   15|      1|        assert_eq!(E::X(true).to_string(), "a");
   16|      1|    }
   17|       |}

Note that the proc-macro generated code is shown as covering line 3.

If the code is wrapped in a function (either a normal function or a lambda), the coverage information is correct:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum E {
    #[error("{}", (|| if *.0 { "a" } else { "b" })())]
    X(bool)
}

#[cfg(test)]
mod tests {
    use super::E;

    #[test]
    fn test() {
        assert_eq!(E::X(true).to_string(), "a");
    }
}

will lead to

cargo llvm-cov --text
[...]
    1|       |use thiserror::Error;
    2|       |
    3|      1|#[derive(Error, Debug)]
    4|       |pub enum E {
    5|      1|    #[error("{}", (|| if *.0 { "a" } else { "b" })())]
                                                          ^0
    6|       |    X(bool)
    7|       |}
    8|       |
    9|       |#[cfg(test)]
   10|       |mod tests {
   11|       |    use super::E;
   12|       |
   13|       |    #[test]
   14|      1|    fn test() {
   15|      1|        assert_eq!(E::X(true).to_string(), "a");
   16|      1|    }
   17|       |}

Note that now "b" is shown as uncovered.

I initially encountered this while writing a proc-macro similar to thiserror that generates error messages based on enum definitions. To work around this issue, I wrapped the code that I wanted to have coverage information in IIFEs inside the proc-macro.

Meta

rustc --version --verbose:

$ rustc --version --verbose
rustc 1.79.0-nightly (9d79cd5f7 2024-04-05)
binary: rustc
commit-hash: 9d79cd5f79e75bd0d2083260271307ce9acd9081
commit-date: 2024-04-05
host: x86_64-unknown-linux-gnu
release: 1.79.0-nightly
LLVM version: 18.1.2
$ cargo llvm-cov --version
cargo-llvm-cov 0.6.9
$ grep 'name = "thiserror"' Cargo.lock -A1
name = "thiserror"
version = "1.0.58"

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-code-coverageArea: Source-based code coverage (-Cinstrument-coverage)A-proc-macrosArea: Procedural macrosC-bugCategory: This is a bug.E-needs-mcveCall for participation: This issue has a repro, but needs a Minimal Complete and Verifiable ExampleT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions