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