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

Macros lead to wrong coverage reports #22247

Open
samuele-buro-sonarsource opened this issue Dec 19, 2024 · 1 comment
Open

Macros lead to wrong coverage reports #22247

samuele-buro-sonarsource opened this issue Dec 19, 2024 · 1 comment
Assignees
Labels
area:coverage Code coverage, see https://dotty.epfl.ch/docs/internals/coverage.html area:metaprogramming:other Issues tied to metaprogramming/macros not covered by the other labels. itype:bug

Comments

@samuele-buro-sonarsource

Compiler version

MWE based on 3.5.1.

Notes

  • We also managed to reproduce it on 3.6.2.
  • The issue also seems unrelated to the build plugin of choice as it appears with both sbt-scoverage and gradle-scoverage (reproducer uses the former).

Minimized code

An MWE can be found here. The reproducer is minimal but requires interaction between multiple files.

Code overview

The code is organized as follows:

src/main/scala
└── org
    └── example
        ├── App.scala
        ├── NumUtils.scala
        ├── greeting
        │   └── Greeting.scala
        └── macros
            └── Macros.scala

The key points are:

  • Greeting.scala defines the logic to greet users (greet: Option[String] => String);
  • Macros.scala defines a macro to decorate strings at compile time (decorate: String => String);
  • App.scala greets an undefined user and decorates a string using the macro (println(decorate(greet(None))));
  • NumUtils.scala defines a simple function isEven, which is completely unrelated to the rest of the code.

Output

The coverage report fails to include Greeting.scala, Macros.scala, and NumUtils.scala. Moreover, it shows 100%
coverage for App.scala even though it should be 0% (main method is never called in the tests).

Expectation

We expect to get an equivalent report as if macros were converted to ordinary methods. As explained in the README,

To make sure that macros are the cause of the issue, simply make the decorate macro into a regular method:

def decorate(s: String): String = s">>> $s <<<"

and re-run tests and report generation. Coverage is now correctly computed.

@samuele-buro-sonarsource samuele-buro-sonarsource added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels Dec 19, 2024
@Gedochao Gedochao added area:coverage Code coverage, see https://dotty.epfl.ch/docs/internals/coverage.html area:metaprogramming:other Issues tied to metaprogramming/macros not covered by the other labels. and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels Dec 20, 2024
@jchyb
Copy link
Contributor

jchyb commented Dec 20, 2024

Minimised for scalac:

// MacroDef.scala
object App:
  def main(args: Array[String]): Unit =
    println(Macros.decorate(Greeting.greet()))
// MacroRun.scala
import scala.quoted.{Expr, Quotes}

object Macros:
  inline def decorate(inline s: String): String = ${ decorateQuotes('s) }
  def decorateQuotes(s: Expr[String])(using Quotes): Expr[String] = '{ ">>> " + $s + " <<<" }

object Greeting:
  def greet() = "hello"

Running with scalac MacroDef.scala MacroCall.scala --coverage-out:. generates scoverage.coverage, where only possible calls to MacroRun.scala are recorded.:

# Coverage data, format version: 3.0
# Statement data:
# - id
# - source path
# - package name
# - class name
# - class type (Class, Object or Trait)
# - full class name
# - method name
# - start offset
# - end offset
# - line number
# - symbol name
# - tree name
# - is branch
# - invocations count
# - is ignored
# - description (can be multi-line)
# '' sign
# ------------------------------------------
0
MacroDef.scala
<empty>
App
Object
<empty>.App
main
70
112
4
println
Apply
false
0
false
println(Macros.decorate(Greeting.greet()))

1
MacroDef.scala
<empty>
App
Object
<empty>.App
main
78
111
4
+
Apply
false
0
false
Macros.decorate(Greeting.greet())

2
MacroDef.scala
<empty>
App
Object
<empty>.App
main
78
111
4
+
Apply
false
0
false
Macros.decorate(Greeting.greet())

3
MacroDef.scala
<empty>
App
Object
<empty>.App
main
94
110
4
greet
Apply
false
0
false
Greeting.greet()

4
MacroDef.scala
<empty>
App
Object
<empty>.App
main
28
36
3
main
DefDef
false
0
false
def main


What seems to happen is that our coverage compiler phase is not correctly handling file suspension mechanism caused by having macros and calls to them in the same compilation:

  • First both files are being compiled, after running into a macro call, the compiler suspends the file with macro call.
  • The file with macro def gets compiled, and has a scoverage.coverage file generated (with indexes 0-4)
  • The file with a macro call gets compiled, also has a scoverage.coverage file generated (with indexes 0-4), incorrectly overwriting the previous file
  • Not only do we lose information about indexes from the previously compiled files, those previously compiled classifies point to the wrong indexes

A workaround while we try to fix this is to have the macro calls and macro defs in separate projects

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:coverage Code coverage, see https://dotty.epfl.ch/docs/internals/coverage.html area:metaprogramming:other Issues tied to metaprogramming/macros not covered by the other labels. itype:bug
Projects
None yet
Development

No branches or pull requests

3 participants