ScalaCon 2021
Chris Kipp
@ckipp01
sbt:scoverage-test> show Compile / compile / scalacOptions [info] Compile / compile / scalacOptions [info] List( [info] -Xplugin:/my/path/to-plugin/2.0.0-M4/scalac-scoverage-plugin_2.13.6-2.0.0-M4.jar, [info] -P:scoverage:dataDir:/path-to-my-project/target/scala-2.13/scoverage-data, [info] -P:scoverage:sourceRoot:/path-to-my-project/scoverage-test, [info] -P:scoverage:reportTestName [info] )
def isTwo(num: Int): Boolean = if (num == 2) { true } else { false }
We'll illustrate with a very simple piece of code
case i: If => treeCopy.If( i, process(i.cond), instrument(process(i.thenp), i.thenp, branch = true), instrument(process(i.elsep), i.elsep, branch = true) )
The tree would be instrumented here
def isTwo(num: Int): Boolean = if ({ scoverage.Invoker.invoked( 1, "/var/folders/fq/nx_jsnyd6550xp03czx898d40000gn/T/", scoverage.Invoker.invoked$default$3() ); num.==(2) }) { scoverage.Invoker.invoked( 3, "/var/folders/fq/nx_jsnyd6550xp03czx898d40000gn/T/", scoverage.Invoker.invoked$default$3() ); { scoverage.Invoker.invoked( 2, "/var/folders/fq/nx_jsnyd6550xp03czx898d40000gn/T/", scoverage.Invoker.invoked$default$3() ); true } } else { scoverage.Invoker.invoked( 5, "/var/folders/fq/nx_jsnyd6550xp03czx898d40000gn/T/", scoverage.Invoker.invoked$default$3() ); { scoverage.Invoker.invoked( 4, "/var/folders/fq/nx_jsnyd6550xp03czx898d40000gn/T/", scoverage.Invoker.invoked$default$3() ); false } };
1 <-- id a/src/main/scala/mypackage/BasicControlStructures.scala <-- source path a <-- package name BasicControlStructures <-- class name Object <-- class type a.BasicControlStructures <-- full class name isTwo <-- method name 74 <-- start offset 79 <-- end offset 5 <-- line number scala.Int.== <-- symbol name Apply <-- tree name false <-- is branch 0 <-- invocations count false <-- is ignored num.==(2) <-- sign
/a/target/scala-2.13/scoverage-data
1 a.basiccontrolstructuresspec 9 a.basiccontrolstructuresspec 4 a.basiccontrolstructuresspec 8 a.basiccontrolstructuresspec 7 a.basiccontrolstructuresspec
a/target/scala-2.13/scoverage-data
lazy val bin212 = Seq( defaultScala212, "2.12.14", "2.12.13", "2.12.12", "2.12.11", "2.12.10", "2.12.9", "2.12.8" ) lazy val bin213 = Seq( defaultScala213, "2.13.6", "2.13.5", "2.13.4", "2.13.3", "2.13.2", "2.13.1", "2.13.0" )
No need to release every 6 weeks
Easily catch regressions
Have a friendly group of compiler devs looking it over
Plus Quentin Jaquier already has a working POC from like 4 years ago.
https://github.com/Qjaquier/dotty-coverage-tools
Again, Quentin's work on this and the report he wrote is incredibly useful. Most of these examples come straight from Code coverage for Dotty.
http://guillaume.martres.me/code_coverage.pdf
❯ scala3-compiler -Xshow-phases parser typer inlinedPositions sbt-deps extractSemanticDB posttyper prepjsinterop sbt-api SetRootTree pickler inlining postInlining staging pickleQuotes {firstTransform, checkReentrant, elimPackagePrefixes, cookComments, checkStatic, betaReduce, inlineVals, expandSAMs} initChecker {elimRepeated, protectedAccessors, extmethods, uncacheGivenAliases, byNameClosures, hoistSuperArgs, specializeApplyMethods, refchecks, tryCatchPatterns, patternMatcher} {elimOpaque, explicitJSClasses, explicitOuter, explicitSelf, elimByName, stringInterpolatorOpt} {pruneErasedDefs, uninitializedDefs, inlinePatterns, vcInlineMethods, seqLiterals, intercepted, getters, specializeFunctions, liftTry, collectNullableFields, elimOuterSelect, resolveSuper, functionXXLForwarders, paramForwarding, genericTuples, letOverApply, arrayConstructors} erasure {elimErasedValueType, pureStats, vcElideAllocations, arrayApply, addLocalJSFakeNews, elimPolyFunction, tailrec, completeJavaEnums, mixin, lazyVals, memoize, nonLocalReturns, capturedVars} {constructors, instrumentation} {lambdaLift, elimStaticThis, countOuterAccesses} {dropOuterAccessors, checkNoSuperThis, flatten, renameLifted, transformWildcards, moveStatic, expandPrivate, restoreScopes, selectStatic, junitBootstrappers, Collect entry points, collectSuperCalls, repeatableAnnotations} genSJSIR genBCode
❯ scala3-compiler -Xshow-phases ... SetRootTree + coverage pickler inlining ...
These are things that can be fully instrumented as they are.
case tree: Literal => instrument(tree) case tree: New => instrument(tree) case tree: This => instrument(tree) case tree: Super => instrument(tree) case Select(qual, _) if (qual.symbol.exists && qual.symbol.is(JavaDefined)) => case tree: Select => if (tree.qualifier.isInstanceOf[New]) { instrument(tree) } else { ... }
These are things that either their syntax don't allow it or they code may be removed during compilation
case tree: Import => tree case tree: Ident if (isWildcardArg(tree)) => tree
Cases where we don't want to instrument the entire tree
def instrumentCaseDef(tree: CaseDef)(using Context): CaseDef = { cpy.CaseDef(tree)(tree.pat, transform(tree.guard), transform(tree.body)) }
object Main { def ourFunction(a: Int, b: Int, c: Int) = ??? } Main.ourFunction(1, thisThrows(2), someOtherComputation())
Main.ourFunction(1, thisThrows(2), someOtherComputation())
val x0 = Main val x1 = 1 val x2 = thisThrows(2) val x3 = someOtherComputation() x0.ourFunction(x1, x2, x3)
Main.ourFunction(1, thisThrows(2), someOtherComputation())
Reusing some of the same logic from ETA expansion
val x0 = doThing() true || x0
This would show x0 as covered, when it shouldn't be
Compiler Plugin
Invoker
Compiler Plugin
Domain
Serializer
Reporter
Compiler Plugin
Domain
(de)Serializer
Reporter
In the Compiler
Invoker
Invoker
2.12, 2,13, 3