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

Draft: Fix coverage reporting with for comprehensions #17239

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

vijaykramesh
Copy link

This addresses #16634 and all the other sbt-scoverage issues related to it (scoverage/sbt-scoverage#382, scoverage/sbt-scoverage#458)

The main functional change is in InstrumentCoverage, where the tryInstrument for a Select tree should always instrument instead of dropping out to the InstrumentedParts.notCovered call.

I also added a test (see vijaykramesh#1 for some more info) that clearly demonstrates the problem on main.

Without the fix - my new test has all sorts of uncovered code that doesn't make sense:
no_fix_flag_true_codegrid
no_fix_summary

with the fix - the coverage output make sense:
coverage_fix_true_flag

& then also this changed some of the saved coverage output data - I looked at the output before and after to confirm there were some changes but the actual coverage is still correct and as expected:

before fix :
without-fix-java-ident

after fix:
with-fix-java-ident

before fix:
no-fix-inline-def-codegrid

after fix:
with-fix-inline-grid-codegrid

@vijaykramesh vijaykramesh force-pushed the fix_for_comprehension_coverage branch from 0f8e529 to f259dee Compare April 13, 2023 05:02
@vijaykramesh
Copy link
Author

seems like a test failure,

[info] Test dotty.tools.dotc.coverage.CoverageTests.checkCoverageStatements started

  exception while retyping {
  scala.runtime.coverage.Invoker.invoked(21, "/tmp/coverage6283052436488169606")
  _root_.scala.StringContext.apply(["","\\n" : String]).s
} of class Block # -1

  An unhandled exception was thrown in the compiler.
  Please file a crash report here:
  https://github.com/lampepfl/dotty/issues/new/choose

     while compiling: /__w/dotty/dotty/tests/coverage/pos/Enum.scala
        during phase: MegaPhase{elimOpaque, explicitOuter, explicitSelf, interpolators, dropBreaks}
                mode: Mode(ImplicitsEnabled,ReadPositions)
     library version: version 2.13.10
    compiler version: version 3.3.1-RC1-bin-SNAPSHOT-git-8355ac0
            settings: -Xsemanticdb true -Xunchecked-java-output-version 9 -Xverify-signatures true -Ycheck List(all, instrumentCoverage) -Yforce-sbt-phases true -Yno-deep-subtypes true -Yno-double-bindings true -classpath /root/.cache/coursier/v1/https/scala-webapps.epfl.ch/artifactory/central/org/scala-lang/scala-library/2.13.10/scala-library-2.13.10.jar:/__w/dotty/dotty/library/../out/bootstrap/scala3-library-bootstrapped/scala-3.3.1-RC1-bin-SNAPSHOT-nonbootstrapped/scala3-library_3-3.3.1-RC1-bin-SNAPSHOT.jar:out/instrumentCoverage/Enum/pos/Enum -color never -coverage-out /tmp/coverage6283052436488169606 -d out/instrumentCoverage/Enum/pos/Enum -indent true -pagewidth 120 -sourceroot /__w/dotty/dotty/tests/coverage/pos

                tree: x$2
       tree position: /__w/dotty/dotty/tests/coverage/pos/Enum.scala:32:12
           tree type: (x$2 : String)
              symbol: value x$2
   symbol definition: val x$2: String (a Symbol)
Fatal compiler crash when compiling: /__w/dotty/dotty/tests/coverage/pos/Enum.scala:
assertion failed: _root_.scala.StringContext.apply(["","\\n" : String]).s in value x$2 should have been rewritten by phase interpolators
	at scala.runtime.Scala3RunTime$.assertFailed(Scala3RunTime.scala:8)
	at dotty.tools.dotc.transform.localopt.StringInterpolatorOpt.checkPostCondition(StringInterpolatorOpt.scala:35)
	at dotty.tools.dotc.transform.TreeChecker$Checker.typedUnadapted$$anonfun$1(TreeChecker.scala:412)
      symbol package: covtest
       symbol owners:  in method test
           call site: method test in object EnumTypes in module class covtest

  == Source file context for tree position ==

-- Error: /__w/dotty/dotty/tests/coverage/pos/Enum.scala:32:12 ---------------------------------------------------------
32 |    println(s"${list}\n")
            ^^^^^^^^^^^^

investigating this. locally running like sbt "scala3-bootstrapped/testOnly *CoverageTests" they all pass on my branch

@vijaykramesh vijaykramesh force-pushed the fix_for_comprehension_coverage branch from f259dee to 4262be0 Compare April 13, 2023 15:28
@vijaykramesh
Copy link
Author

figured out how to locally use this dotty in another local project (sbt-scoverage-samples, so I can confirm this change addresses the issue at scoverage/sbt-scoverage-samples#28) - BUT then have run into some error when running with coverage on:

[info] org.scoverage.samples.DefaultArgumentsObjectTest *** ABORTED ***
[info]   java.lang.NoSuchMethodError: 'java.lang.String scala.StringContext.s(scala.collection.immutable.Seq)'
[info]   at org.scoverage.samples.DefaultArgumentsObject$.makeGreeting(DefaultArgumentsObject.scala:12)
[info]   at org.scoverage.samples.DefaultArgumentsObjectTest.testFun$proxy1$1(DefaultArgumentsObjectTest.scala:9)
[info]   at org.scoverage.samples.DefaultArgumentsObjectTest.$init$$$anonfun$1(DefaultArgumentsObjectTest.scala:8)
[info]   at org.scalatest.Transformer.apply$$anonfun$1(Transformer.scala:22)
[info]   at org.scalatest.OutcomeOf.outcomeOf(OutcomeOf.scala:85)
[info]   at org.scalatest.OutcomeOf.outcomeOf$(OutcomeOf.scala:31)
[info]   at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
[info]   at org.scalatest.Transformer.apply(Transformer.scala:22)
[info]   at org.scalatest.Transformer.apply(Transformer.scala:21)
[info]   at org.scalatest.flatspec.AnyFlatSpecLike$$anon$5.apply(AnyFlatSpecLike.scala:1697)
[info]   ...
[error] java.lang.NoSuchMethodError: 'java.lang.String scala.StringContext.s(scala.collection.immutable.Seq)'

quite sure it is related to my change, will address this before marking this PR as ready.

@vijaykramesh vijaykramesh changed the title Fix coverage reporting with for comprehensions Draft: Fix coverage reporting with for comprehensions Apr 13, 2023
Copy link
Member

@TheElectronWill TheElectronWill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for trying to tackle this problem!

If the problem is that some Select trees are missing, I think that it would be better to find out why canInstrumentParameterless exclude some necessary trees of for-comprehensions. Then, it can be refactored to let them be instrumented, while still excluding some cases that badly interact with the rest of the compiler.

val transformed = cpy.Select(tree)(transform(tree.qualifier), tree.name)
if canInstrumentParameterless(sym) then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it's not that simple, for two reasons:

  • one critical: some Select trees simply cannot be instrumented, because the later phases of the compiler expect some trees to be removed/transformed in some specific ways that aren't compatible with the coverage instrumentation (e.g. asInstanceOf).
  • one ergonomic: method applications are already handled by tryInstrument(Apply), it would be redundant to instrument the select. For instance for a.f(x), we get an Apply(Select(...), args) where the Select is a.f and the args is x. Here, instrumenting the Apply tree is enough.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks these comments make sense, and indeed as I do more testing I see it's not this simple :)

hopefully will report back after tackling this further.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheElectronWill so I'm able to reproduce the issue neatly locally now, and then use my local dotty build in sbt-scoverage-samples in order to clearly see the coverage issue and how my local changes impact it
Screen Shot 2023-04-18 at 7 58 53 AM

I am able to get the right side of the flatMap/for comprehension to show up in the measurements file but not arbitrarily deep (so the right hand side of a <- List(1) will be green but the right hand side of the next line b <- List(2) won't be.

& also it somewhat seems fundamentally wrong to me, because we are getting a statement for the whole Apply flatMap tree around everything -

 for {\n s <- Future.successful("foo")\n t <- Future.successful("barnum")\n } yield {\n "yield me"\n } 

and that is properly getting added to the measurements file when it is called.

so I actually think the "correct" solution would be to stop these other statements (that are inside the flatMap/for comprehension statement) from showing up entirely. does that make sense to you?

statement list to show you the "double counting"

Screen Shot 2023-04-18 at 8 02 18 AM

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I actually think the "correct" solution would be to stop these other statements (that are inside the flatMap/for comprehension statement) from showing up entirely. does that make sense to you?

But then, how would the coverage check that all of the for-comprehension is executed? The first rhs could fail, and in that case the rest of the for-comprehension must not be 100% covered. For example:

for
  a <- throwException()
  b <- other() // must not be covered
do
  println(b) // must not be covered

@TheElectronWill
Copy link
Member

I've read in vijaykramesh#1 that some Apply trees that come from the for-comprehension are in the report but aren't covered, so the actual issue might be somewhere else 🤔

@vijaykramesh vijaykramesh force-pushed the fix_for_comprehension_coverage branch from 0865aab to 3481f0c Compare April 21, 2023 17:56
@ckipp01 ckipp01 marked this pull request as draft May 9, 2023 17:18
@rolandtritsch
Copy link

@vijaykramesh @TheElectronWill @ckipp01 Seems we can see what the problem is. What is the next step towards fixing it? Can I help?

@longliveenduro
Copy link

Problem is still there in Scala 3.3.0. Any updates on this?

@vijaykramesh
Copy link
Author

I can confirm the problem still persists on latest main (3.3.2-RC1)
Screenshot 2023-07-20 at 10 57 57 AM

@bart-sofomo
Copy link

I can also confirm it is still not working in 3.3.1-RC4.

@smarter
Copy link
Member

smarter commented Aug 10, 2023

So the problem is not specific to for-comprehensions, I can reproduce it with:

def test: Unit =
  List(1)
  List(2).map(a => a)

After phase instrumentCoverage, the argument of List(1) is instrument but the argument of List(2) isn't, because canInstrumentParameterless returns false on map.

@TheElectronWill I'm not sure I understand the logic around using canInstrumentParameterLess. Should it be !sym.info.isParameterless || canInstrumentParameterLess ?

More generally, is it possible to change the percentage of coverage reported to not include stuff we ignore for one reason or another? That would solve the immediate reported in https://contributors.scala-lang.org/t/coverage-broken-why-does-nobody-care/6262v

@javax-swing
Copy link

I've taken a further look and I think the problem is around the handling of lambdas. If I take an example like the below

object Test {
  def main(args: Array[String]): Unit = {
    List(123).map(a => {
      println(123)
      a + a
    })

    List(456).map(a => a)
  }
}

I get the following tasty tree segments.

List(456).map(a => a)

   199:           APPLY(91)
   201:             TYPEAPPLY(43)
   203:               SELECTin(38) 38 [map[Signed Signature(List(1, scala.Function1),scala.collection.immutable.List) @map]]
   206:                 APPLY(32)
   208:                   TYPEAPPLY(12)
   210:                     SELECTin(7) 42 [apply[Signed Signature(List(1, scala.collection.immutable.Seq),java.lang.Object) @apply]]
   213:                       SHAREDtype 135
   216:                       SHAREDtype 141
   219:                     SHAREDtype 145
   222:                   TYPED(16)
   224:                     REPEATED(6)
   226:                       SHAREDtype 145
   229:                       INTconst 123
   232:                     APPLIEDtype(6)
   234:                       SHAREDtype 161
   237:                       SHAREDtype 145
   240:                 SHAREDtype 168
   243:               SHAREDtype 145
   246:             BLOCK(44)
   248:               LAMBDA(3)
   250:                 TERMREFdirect 253
   253:               DEFDEF(37) 47 [$anonfun]
   256:                 PARAM(4) 48 [a]
   259:                   SHAREDtype 145
   262:                 SHAREDtype 145
   265:                 BLOCK(23)
   267:                   APPLY(12)
   269:                     SELECTin(7) 51 [+[Signed Signature(List(scala.Int),scala.Int) @+]]
   272:                       TERMREFdirect 256
   275:                       SHAREDtype 145
   278:                     SHAREDtype 272
   281:                   APPLY(7)
   283:                     TERMREF 54 [println[Signed Signature(List(java.lang.Object),scala.Unit) @println]]
   285:                       SHAREDtype 105
   287:                     INTconst 123
   290:                 SYNTHETIC
   291:                 ARTIFAC
    List(123).map(a => {
      println(123)
      a + a
    })
   121:             APPLY(76)
   123:               TYPEAPPLY(50)
   125:                 SELECTin(45) 38 [map[Signed Signature(List(1, scala.Function1),scala.collection.immutable.List) @map]]
   128:                   APPLY(38)
   130:                     TYPEAPPLY(17)
   132:                       SELECTin(11) 42 [apply[Signed Signature(List(1, scala.collection.immutable.Seq),java.lang.Object) @apply]]
   135:                         TERMREF 34 [List]
   137:                           TERMREF 43 [package]
   139:                             SHAREDtype 58
   141:                         TYPEREF 44 [IterableFactory]
   143:                           TERMREFpkg 31 [scala[Qualified . collection]]
   145:                       TYPEREF 45 [Int]
   147:                         SHAREDtype 58
   149:                     TYPED(17)
   151:                       REPEATED(6)
   153:                         SHAREDtype 145
   156:                         INTconst 456
   159:                       APPLIEDtype(7)
   161:                         TYPEREF 46 [<repeated>]
   163:                           SHAREDtype 58
   165:                         SHAREDtype 145
   168:                   TYPEREF 34 [List]
   170:                     TERMREFpkg 33 [scala[Qualified . collection][Qualified . immutable]]
   172:                 SHAREDtype 145
   175:               BLOCK(22)
   177:                 LAMBDA(3)
   179:                   TERMREFdirect 182
   182:                 DEFDEF(15) 47 [$anonfun]
   185:                   PARAM(4) 48 [a]
   188:                     SHAREDtype 145
   191:                   SHAREDtype 145
   194:                   TERMREFdirect 185
   197:                   SYNTHETIC
   198:                   ARTIFACT

and the below is a snippet from the decompiled class file

   public void main(final String[] args) {
      .MODULE$.invoked(5, "target/cov");
      .MODULE$.invoked(2, "target/cov");
      ArraySeq var2 = scala.runtime.ScalaRunTime..MODULE$.wrapIntArray(new int[]{123});
      .MODULE$.invoked(0, "target/cov");
      ((List)scala.package..MODULE$.List().apply(var2)).map((a) -> {
         .MODULE$.invoked(1, "target/cov");
         scala.Predef..MODULE$.println(BoxesRunTime.boxToInteger(123));
         return a + a;
      });
      .MODULE$.invoked(4, "target/cov");
      ArraySeq var3 = scala.runtime.ScalaRunTime..MODULE$.wrapIntArray(new int[]{456});
      .MODULE$.invoked(3, "target/cov");
      ((List)scala.package..MODULE$.List().apply(var3)).map((a) -> {
         return a;
      });
   }

From InstrumentCoverage, you can see that transformDefDef only transforms the body of the lambda if the DefDef is not SYNTHETIC

I also can't see any handling of LAMBDAs, perhaps that is the key?

I think that if LAMBDA DefDefs made it through that transformBody method, they would probably be instrumented ok

@smarter
Copy link
Member

smarter commented Aug 17, 2023

Good call @javax-swing, I think there's two issues involved here, if I start with:

val x = List(2).map(a => a)

InstrumentCoverages generates code equivalent to:

val x = {
  scala.runtime.coverage.Invoker.invoked(...)
  List(2).map(a => a)
}

if I remove the Artifact | Synthetic check you mentioned, I get:

val x = {
  scala.runtime.coverage.Invoker.invoked(...)
  List(2).map(a =>
    scala.runtime.coverage.Invoker.invoked(...)
    a
  )
}

So the lambda is indeed instrumented but the List(2) prefix is still uninstrumented which can be fixed by the change I mentioned in #17239 (comment) (but it looks like my naive change adds a lot of extraneous unnecessary instrumentation calls)

@javax-swing
Copy link

Nice @smarter

I should probably spend some time trying to build the compiler so that I can play with it too!

I'm guessing the Artifact | Synthetic check is still needed for some macro generated functions, or default value functions. So maybe we need to special case based on siblings (if that's at all possible)

I'll see if I can spend some time this week coming up with some scenarios and expectations.

Perhaps it's better for it to over instrument than under instrument in any case.

What I am curious about is the mismatch between the scoverage report and the tasty modifications. Scoverage must be aware of the statements which haven't been instrumented somehow (as well as ones that can be ignored), so perhaps we can align with that, because everything highlighted in red/green looks like sensible things to be instrumented.

@smarter
Copy link
Member

smarter commented Aug 17, 2023

I should probably spend some time trying to build the compiler so that I can play with it too!

It should work like a regular sbt project but feel free to ask any question on scala-contributors on https://discord.com/invite/scala

I'm guessing the Artifact | Synthetic check is still needed for some macro generated functions, or default value functions. So maybe we need to special case based on siblings (if that's at all possible)

In another part of InstrumentCoverage we use isZeroExtent which excludes tree that don't correspond to something the user wrote (detected because the start and end of their position is equal).

From InstrumentCoverage, you can see that transformDefDef only transforms the body of the lambda if the DefDef is not SYNTHETIC

On a second look I think that's not quite correct actually: if the else if branch is not taken, we still recurse on tree.rhs which will be the body of the lambda, the call to instrumentBody is used for regular defs to highlight the name of the def itself (which is useful for defs with trivial body which don't get any highlighting at all otherwise), it might also be useful for trivial lambdas like a => a, but I don't think that it leads to a lower coverage report.

Scoverage must be aware of the statements which haven't been instrumented somehow

I believe that's due to the explicit use of InstrumentedParts.notCovered which maybe we should simply remove (once we're confident we're not over-reporting coverage on certain things).

@TheElectronWill
Copy link
Member

Sorry for my lack of reactivity, I'm currently in the middle of moving. I think you're right, lambdas aren't handled properly!

Overinstrumenting vs Underinstrumenting is a difficult trade-off IMO, because we still want to exclude compiler-generated code from the instrumentation, in particular when instrumenting synthetic code can crash the compiler (it happened!).

@javax-swing
Copy link

@smarter @vijaykramesh

I believe I have found the issue with this PR: #18420 feel free to nab the commit.

It seems that the TypeApply branch was throwing away the nested expression when the expression itself could not be instrumented.

This was problematic because that expr is likely to have been transformed and the entries already added to the scoverage file

After the change code like the following

    List(789).map(a => List(123,456).map(b => b + a))

    List(789).map(a => Future.successful(123).map(b => b + a))

Results in the expected invoke calls, which were otherwise missing

   public void main(final String[] args) {
      .MODULE$.invoked(15, "target/cov");
      .MODULE$.invoked(0, "target/cov");
      .MODULE$.invoked(2, "target/cov");
      List var2 = scala.package..MODULE$.List();
      ArraySeq var3 = scala.runtime.ScalaRunTime..MODULE$.wrapIntArray(new int[]{789});
      .MODULE$.invoked(1, "target/cov");
      ((List)var2.apply(var3)).map((a) -> {
         return this.main$$anonfun$1(BoxesRunTime.unboxToInt(a));
      });
      .MODULE$.invoked(8, "target/cov");
      .MODULE$.invoked(10, "target/cov");
      List var4 = scala.package..MODULE$.List();
      ArraySeq var5 = scala.runtime.ScalaRunTime..MODULE$.wrapIntArray(new int[]{789});
      .MODULE$.invoked(9, "target/cov");
      ((List)var4.apply(var5)).map((a) -> {
         return this.main$$anonfun$2(BoxesRunTime.unboxToInt(a));
      });
   }

   // $FF: synthetic method
   private final List main$$anonfun$1(final int a) {
      .MODULE$.invoked(7, "target/cov");
      .MODULE$.invoked(3, "target/cov");
      .MODULE$.invoked(5, "target/cov");
      List var2 = scala.package..MODULE$.List();
      ArraySeq var3 = scala.runtime.ScalaRunTime..MODULE$.wrapIntArray(new int[]{123, 456});
      .MODULE$.invoked(4, "target/cov");
      return ((List)var2.apply(var3)).map((b) -> {
         .MODULE$.invoked(6, "target/cov");
         return b + a;
      });
   }

   // $FF: synthetic method
   private final Future main$$anonfun$2(final int a) {
      .MODULE$.invoked(14, "target/cov");
      .MODULE$.invoked(12, "target/cov");
      Future var2 = scala.concurrent.Future..MODULE$.successful(BoxesRunTime.boxToInteger(123));
      ExecutionContext var3 = scala.concurrent.ExecutionContext.Implicits..MODULE$.global();
      .MODULE$.invoked(11, "target/cov");
      return var2.map((b) -> {
         .MODULE$.invoked(13, "target/cov");
         return b + a;
      }, var3);
   }

@rolandtritsch
Copy link

I believe I have found the issue with this PR: #18420 feel free to nab the commit.

Hi @javax-swing. Excellent work. What's the next step to get this fixed/merged?

sjrd added a commit that referenced this pull request Aug 25, 2023
Based on looking into the issues that:
#17239 was trying to solve.

It was discovered that `TypeApply` was throwing away parts of the
expression tree, which meant that the `coverage` file contained
references to things that could never be invoked.
@filipwiech
Copy link

Seems like this can be closed now. 👍
BTW @KacperFKorban are there any plans to backport the recent coverage fixes and improvements into the upcoming 3.3.2 LTS, or will those be only available starting with the 3.4.0? 🙂

@KacperFKorban
Copy link
Member

@filipwiech I'm not sure about the changes, they are not the same as the ones already introduced, but they were meant to fix similar things.

Regarding the backporting, I'll forward this question to @Kordyjan

@filipwiech
Copy link

@Kordyjan Sorry to bother you, but I see that the backporting process for the 3.3.2 has already begun. Any insight from the maintenance team regarding the question above? 🙂

@Kordyjan
Copy link
Contributor

Kordyjan commented Dec 7, 2023

@filipwiech
Sorry for not responding. For some unknown reason, I have notifications muted for this PR.
We plan to release all changes to the coverage that are currently part of the main branch in 3.2.2 3.3.2.

@longliveenduro
Copy link

@Kordyjan I hope you mean 3.3.2 ;-)

@filipwiech
Copy link

No problem @Kordyjan, thanks for the update, sounds great! 👍

@monisha5540
Copy link

@filipwiech Sorry for not responding. For some unknown reason, I have notifications muted for this PR. We plan to release all changes to the coverage that are currently part of the main branch in 3.2.2 3.3.2.

Thanks for the info @Kordyjan , any ETA for Scala 3.3.2 release date?

@Kordyjan
Copy link
Contributor

Kordyjan commented Dec 14, 2023

any ETA for Scala 3.3.2 release date?

I wanted to release 3.3.2-RC1 today. It will probably be on Maven Central tomorrow. The stable release will happen around the last week of January.

@monisha5540
Copy link

any ETA for Scala 3.3.2 release date?

I wanted to release 3.3.2-RC1 today. It will probably be on Maven Central tomorrow. The stable release will happen around the last week of January.

Hi @Kordyjan , Could you please provide the ETA for 3.3.2 stable release.

@Kordyjan
Copy link
Contributor

Kordyjan commented Feb 2, 2024

Could you please provide the ETA for 3.3.2 stable release.

February 14th is the likely date. The delay was caused by the regression that was found outside of the Open Community Build projects and CVE in one of scaladoc dependencies (not exploitable, but better to be safe than sorry).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet