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

code coverage support #184

Open
johnynek opened this issue Apr 25, 2017 · 66 comments
Open

code coverage support #184

johnynek opened this issue Apr 25, 2017 · 66 comments
Labels

Comments

@johnynek
Copy link
Member

johnynek commented Apr 25, 2017

This would look like using:

https://github.com/scoverage/scalac-scoverage-plugin

to get the scala test rules to emit code coverage reports, then aggregating them across the repo.

It is related to bazelbuild/bazel#1118 but I don't think it is actually a blocker. We may just be able to produce coverage output of each test rule, then an aspect to walk all the rules and aggregate a total.

@softprops
Copy link
Contributor

Has anyone started working on this? This is going to become a priority from my team soon. We're working on replacing our reliance on sbt in our jenkins pipeline with bazel. One part of that is figuring out the test report and codecov ( scoverage ) stories.

@ittaiz
Copy link
Member

ittaiz commented May 5, 2017 via email

@softprops
Copy link
Contributor

👍

I was looking through the machinery that ties scoverage into sbt, and it looks like it may be possible to to just opt into code coverage for a regular scala_test by threading through a few additional arguments flags: namely: -Xplugin:path/to/plugin and -P:scoverage:dataDir:path/to/scoverage-data

@softprops
Copy link
Contributor

opt in meaning maybe this is possible today without any additional changes and when the coverage story in bazel settles something like this could then just become a conveniences of being instrumented by default with running bazel cover

@johnynek
Copy link
Member Author

johnynek commented May 5, 2017 via email

@softprops
Copy link
Contributor

My guess is it'll be the same as the test_filter flag story. When x is present in the env ( or args ) do y. My guess about where the conventional place to write reports would be customizable with rule dependent defaults, like junit test reports

@ittaiz
Copy link
Member

ittaiz commented May 5, 2017 via email

@softprops
Copy link
Contributor

softprops commented May 5, 2017

@johnynek do you know ( specifically for scalac ) if there needs to be anything special done for scalac plugins to set up a special kind of classpath separate form the classpath of the files being compiled?

I've now optimistically convinced myself this should be more straightforward that I originally thought so I couldn't help trying out a POC to close out my friday on a happy note.

my WORKSPACE

# other stuff ...

filegroup(
    name = "scoverage_plugin",
    srcs = ["sbt-plugins/scalac-scoverage-plugin_2.11-1.2.0.jar"],
    visibility = ["//visibility:public"],
)

java_import(
    name = "sbt-plugins",
    jars=glob([
    "sbt-plugins/scalac-scoverage*.jar"],
    exclude = [
            "sbt-plugins/scala-library*jar",
            "sbt-plugins/scala-xml*jar"
    ]),
    visibility = ["//visibility:public"],
)

my BUILD

scala_test(
    name = "foo",
    size = "small", 
    srcs = glob(
        [
            "src/test/scala/**/*.scala",
            "src/test/java/**/*.java",
        ],
    ),
    jvm_flags = [
        "-Dfile.encoding=UTF-8",
    ],
    plugins = ["@meetuplib//:scoverage_plugin"], # the scoverage scalac plugin jar
    scalacopts = [
      "-P:scoverage:dataDir:/path/to/target/scovtest/scoverage-data",
      "-Yrangepos"
    ],
   deps = [
      # other stuff....
        "@meetuplib//:sbt-plugins" # puts scoverage deps on my classpath
    ],
)

when I run bazel test -s --progress_report_interval 1 --test_output streamed --strategy=Scalac=standalone //foo:* I get a class not found error on scala.xml.NamespaceBinding. Scala xml is a dependency of scoverage but I've already confirmed it's on my classpath. I checked my foo-test_worker_input file to be sure.

[info] Instrumentation completed [50790 statements]
error: java.lang.NoClassDefFoundError: scala/xml/NamespaceBinding
	at scoverage.ScoverageInstrumentationComponent$$anon$1.run(plugin.scala:121)
	at scala.tools.nsc.Global$Run.compileUnitsInternal(Global.scala:1501)
	at scala.tools.nsc.Global$Run.compileUnits(Global.scala:1486)
	at scala.tools.nsc.Global$Run.compileSources(Global.scala:1481)
	at scala.tools.nsc.Global$Run.compile(Global.scala:1582)
	at scala.tools.nsc.Driver.doCompile(Driver.scala:32)
	at scala.tools.nsc.MainClass.doCompile(Main.scala:23)
	at scala.tools.nsc.Driver.process(Driver.scala:51)
	at io.bazel.rulesscala.scalac.ScalacProcessor.compileScalaSources(ScalacProcessor.java:207)
	at io.bazel.rulesscala.scalac.ScalacProcessor.processRequest(ScalacProcessor.java:77)
	at io.bazel.rulesscala.worker.GenericWorker.run(GenericWorker.java:125)
	at io.bazel.rulesscala.scalac.ScalaCInvoker.main(ScalaCInvoker.java:42)
Caused by: java.lang.ClassNotFoundException: scala.xml.NamespaceBinding
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 12 more

cat bazel-out/local-fastbuild/bin/foo/foo-test_worker_input
Classpath: # includes external/scala/lib/scala-xml_2.11-1.0.4.jar among otherthings
EnableIjar: False
Files: # bunch of sources
IjarCmdPath:
IjarOutput:
JarOutput: bazel-out/local-fastbuild/bin/foo/foo-test.jar
JavacOpts:
JavacPath: external/local_jdk/bin/javac
JavaFiles:  # bunch of sources
JvmFlags: -J-Dfile.encoding=UTF-8
Manifest: bazel-out/local-fastbuild/bin/foo/foo-test_MANIFEST.MF
Plugins: external/meetuplib/sbt-plugins/scalac-scoverage-plugin_2.11-1.2.0.jar
PrintCompileTime: False
ResourceDests: /log4j.properties,/malformed-manifest.json,/test-velocity.properties,/valid-manifest.json
ResourceJars:
ResourceSrcs: # bunch of sources
ResourceStripPrefix:
ScalacOpts: -P:scoverage:dataDir:/path/to/target/scovtest/scoverage-data,-Yrangepos
SourceJars:

The only thing that turned up in a lazy google search was this issue that popped on up the intellij mailing list. I didn't quite get the solution in that case or how it may be part of the problem in mine.

@sdtwigg
Copy link
Contributor

sdtwigg commented May 5, 2017

I was actually speaking with ahirreddy@ about this the other day. I think the plugin pulls classes from the bootclasspath. Was eventually planning to verify this and fixup the rules to also setup the bootclasspath properly. Edit: You just need to add the dependencies as other (fake) -Xplugin. This makes a more structured deploy_jar construction tempting.

It might be easier just to build the plugin as a deploy_jar for now though.

@softprops
Copy link
Contributor

@sdtwigg I'm more of a consumer than a producer in this case. I'm using the scoverage plugin and runtime jars from maven central and trying to provide them as a plugin value to a scala_test rule. Can you draw me a rough sketch of what you mean?

@softprops
Copy link
Contributor

softprops commented May 6, 2017

Now I'm thinking there may be something to do with the bootclasspath which may actually be related to an open bug with scala.

One thing I failed to mention above when setting up my test was that initially I didn't have the scalac-scoverage-runtime jar on my classpath which caused a similar issue. After adding that to my scala_tests' deps, that problem was resolved. The next issue I ran into was the xml.NameSpace error.

I tried the change mentioned in the issue by adding the -nobootcp to my scalacopts. Same issue.

I'm a little confused because the identity of this class technically didn't change between scala 2.10 and 2.11 so either way I expected it to be on the classpath one way or another.

@softprops
Copy link
Contributor

I have a little bit more mental energy to this but I'm still stumped on the scala.xml.NameSpace issue. Though I was reminded that in order for scoverage to instrument code it needs to be applied to my scala_library targets ( not my scala_test ) targets which makes things a bit more awkward

@johnynek
Copy link
Member Author

johnynek commented May 8, 2017

we should be able to use a target in the current repo as a plugin. If we can't now, that shouldn't be too much work. Then, we could just make a deploy jar ourselves by linking in the full path.

@sdtwigg
Copy link
Contributor

sdtwigg commented May 8, 2017

Sorry, I never really described what deploy_jar is although seems like you found out but just in case: The deploy_jar is a fat-jar that contains the current compiled target and all its transitive dependencies (both runtime and compile time). It is analogous to the deploy_jar you can get from a java_binary (although not executable if from scala_library).

I realized over the weekend that just directly using a deploy_jar from one of the rules_scala rules probably won't work because I don't think it will put the plugin.xml in the right place. So, would need to take the deploy_jar from the rules and have another action/genrule put the plugin.xml inside that fat jar.

I evntually wanted to demonstrate this by replacing the linter plugin in the tests with a custom rolled one (both to demonstrate how to make plugins and drop a rules_scala dependency). Unfortunately, I am really reluctant to say I have the time to do this :( . Had spent my 'free-work' time over weekend pushing through the bazel changes outlined in: #57 (comment)

PS: Do you think it is a problem that using two plugins might have redundant classes in their fat-jars? (Assuming that the classes are identical duplicates then it seems like it would just be like a 'stylistic' issue versus a functional one.)

@softprops
Copy link
Contributor

How do you make a deploy_jar without a main class, do scalac plugins run with a java main?

Also, I can kind of half confirm the deploy_jar thing works. I cloned the scoverage repo and used sbt-assembly to bake me a fat jar. I referenced that in a java_import and that removed the scala.xml.NamespaceBinding issue.

Next issue is related to sandboxing which is probably just something Im doing wrong. Given the scalacopt for the plugin's data dir ( -P:scoverage:dataDir ), let's call that /path/to/scovtest/scoverage-data/

[info] Instrumentation completed [852 statements]
[info] Wrote instrumentation file [/path/to/scovtest/scoverage-data/scoverage.coverage.xml]
[info] Will write measurement data to [/path/to/scovtest/scoverage-data]
Discovery starting.
*** RUN ABORTED *** (153 milliseconds)
  java.lang.RuntimeException: Unable to load a Suite class that was discovered in the runpath: com.meetup.base.util.StatusesTest
  at org.scalatest.tools.DiscoverySuite$.getSuiteInstance(DiscoverySuite.scala:84)
  at org.scalatest.tools.DiscoverySuite$$anonfun$1.apply(DiscoverySuite.scala:38)
  at org.scalatest.tools.DiscoverySuite$$anonfun$1.apply(DiscoverySuite.scala:37)
  at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:234)
  at scala.collection.TraversableLike$$anonfun$map$1.apply(TraversableLike.scala:234)
  at scala.collection.Iterator$class.foreach(Iterator.scala:893)
  at scala.collection.AbstractIterator.foreach(Iterator.scala:1336)
  at scala.collection.IterableLike$class.foreach(IterableLike.scala:72)
  at scala.collection.AbstractIterable.foreach(Iterable.scala:54)
  at scala.collection.TraversableLike$class.map(TraversableLike.scala:234)
  ...
  Cause: java.io.FileNotFoundException: /path/to/scovtest/scoverage-data/scoverage.measurements.1 (Read-only file system)
  at java.io.FileOutputStream.open0(Native Method)
  at java.io.FileOutputStream.open(FileOutputStream.java:270)
  at java.io.FileOutputStream.<init>(FileOutputStream.java:213)
  at java.io.FileWriter.<init>(FileWriter.java:107)
  at scoverage.Invoker$$anonfun$1.apply(Invoker.scala:42)
  at scoverage.Invoker$$anonfun$1.apply(Invoker.scala:42)
  at scala.collection.concurrent.TrieMap.getOrElseUpdate(TrieMap.scala:901)
  at scoverage.Invoker$.invoked(Invoker.scala:42)
  at com.meetup.base.util.StatusesTest.<init>(StatusesTest.scala:6)
  at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)

This kind of makes sense because sandboxing shouldn't let me write outside. To continue the POC I added --spawn_strategy=standalone

bazel test --spawn_strategy=standalone --verbose_failures  --progress_report_interval 1 --test_output streamed --strategy=Scalac=standalone //base:*

that allows the scalac plugin to generate two files

scoverage.coverage.xml  scoverage.measurements.1

So thats the recording process in a nutshell. Report generation... that's a whole other beast but I just need to confirm that the coverage recordings are all that my jenkins plugin requires. Reports may not be a need to have for me atm.

@softprops
Copy link
Contributor

Unfortunately, I am really reluctant to say I have the time to do this

I'm happy to help but will probably need more direction, I have a decent sense for bazel's architectural model still have some blindspots.

I'm very much in a POC phase right now where Im testing to see if it can work in a timeboxed period of time. The next to think about is probably more relevant to the discussion on bazel core's issue tracker. How should the library rules detect the conditional case for appending the scalac plugin. You typically will only want to do that in some testing mode. What you ship shouldn't have the scoverage instrumentation.

@softprops
Copy link
Contributor

circling back to this. I had a few questions that I think could be answered faster here but perhaps should be asked on the bazel-discuss list.

  • creating a standalone scoverage jar

I think I found some examples of where I can create the scoverage binary jar here. I think that was suggested above. I'm just double check to make sure that's the right pattern to follow.

  • conditionally appending scoverage jar to scalac plugins

So the way to know the coverage should be enabled or not is documented here. The problem I'm running into is that I need access to the scoverage jar to conditionally append it to this list. What I haven't seem examples of yet in bazel rules is how to dynamically access a dependency from within an implementation. My guess is that there's something fundamentally wrong with me asking how to do that because bazel needs to run though the analysis phase first before I have the context. That said, pointers are welcome!

  • identifying and value for and conditionally appending scalac options for scalac plugin arguments

This is related to the above. I'll need to provide the scoverage enabled compile run -P:scoverage:dataDir:path/to/scoverage-data scalac plugin arg via the scalacopts list. This should be more straight forward but I think I'll need some flavor of make var eval for getting the location of the test-logs directory for the scala_library. I'm not yet sure where the conventional location to put these files but test-logs somehow seems appropriate.

@softprops
Copy link
Contributor

The answer came to me on the train ride home. I can just declare these as private ( _ ) attr dependencies for rules that depend on _compile. The intuition being that rules can have dependencies that they do not always use

@softprops
Copy link
Contributor

I put together an initial prototype that was intended as a pt 1 of the process to have scoverage record starting measurements here One part I'm getting stuck on is the directory that scoverage should write data to

I'm currently using a subdirectory of testlogs

scoverage_data_dir = "bazel-testlogs/{pkg}/{name}/scoverage-data".format(
   pkg=ctx.label.package,
   name=ctx.label.name
)

This doesn't quite work and I'll allude to why

bazel coverage --test_output streamed //test:CalculatorTest
WARNING: Streamed test output requested. All tests will be run locally, without sharding, one at a time.
INFO: Using default value for --instrumentation_filter: "//test".
INFO: Override the above default with --instrumentation_filter
INFO: Found 1 test target...
INFO: From scala //test:Calculator:
[info] Cleaning datadir [bazel-testlogs/test/Calculator/scoverage-data]
[info] Beginning coverage instrumentation
[info] Instrumentation completed [1 statements]
[info] Wrote instrumentation file [bazel-testlogs/test/Calculator/scoverage-data/scoverage.coverage.xml]
[info] Will write measurement data to [bazel-testlogs/test/Calculator/scoverage-data]
INFO: From scala //scala/support:test_reporter:
[info] Cleaning datadir [bazel-testlogs/scala/support/test_reporter/scoverage-data]
[info] Beginning coverage instrumentation
[warn] Could not instrument [EmptyTree$/null]. No pos.
[info] Instrumentation completed [458 statements]
[info] Wrote instrumentation file [bazel-testlogs/scala/support/test_reporter/scoverage-data/scoverage.coverage.xml]
[info] Will write measurement data to [bazel-testlogs/scala/support/test_reporter/scoverage-data]
An exception or error caused a run to abort. This may have been caused by a problematic custom reporter.
java.io.FileNotFoundException: bazel-testlogs/scala/support/test_reporter/scoverage-data/scoverage.measurements.1 (No such file or directory)
	at java.io.FileOutputStream.open0(Native Method)
	at java.io.FileOutputStream.open(FileOutputStream.java:270)
	at java.io.FileOutputStream.<init>(FileOutputStream.java:213)
	at java.io.FileWriter.<init>(FileWriter.java:107)
	at scoverage.Invoker$$anonfun$1.apply(Invoker.scala:42)
	at scoverage.Invoker$$anonfun$1.apply(Invoker.scala:42)
	at scala.collection.concurrent.TrieMap.getOrElseUpdate(TrieMap.scala:901)
	at scoverage.Invoker$.invoked(Invoker.scala:42)
	at io.bazel.rules.scala.JUnitXmlReporter.<init>(JUnitXmlReporter.scala:45)

note: dispite the fact that scoverage says it wrote the initial coverage.xml file it doesn't actually exist

[info] Wrote instrumentation file [bazel-testlogs/test/Calculator/scoverage-data/scoverage.coverage.xml]

An the reason is test logs are attributed to test packages not library packages. Looking for pointers on an alternative approach for picking this directory.

I did notice that the JUnit xml reporters using and env var to know where to record data. I'm not sure if there's a similar feature for coverage writers. I don't think that skylark rules can access env vars if there was.

@softprops
Copy link
Contributor

status update. I updated my branch with a change that partially addresses the coverage dir problem. I realized that the action that writes the coverage data needs to own the process of creating the directory so I ended up threading the coverage dir as a new CompileOption to let the ScalacProcessor create the path. That gets me the ability to create the instrumentation data

ls -al bazel-out/local-fastbuild/bin/test/Calculator-coverage/scoverage.coverage.xml
-rw-r--r-- 1 doug users 523 May 24 18:57 bazel-out/local-fastbuild/bin/test/Calculator-coverage/scoverage.coverage.xml

But I am still unsure how to provide that directory to the test runner which I think I need to write the measurement data to. Let me know what you folks think of my current direction. I'd like to pivot early in the process of I'm going down the wrong direction.

[info] Cleaning datadir [bazel-out/local-fastbuild/bin/test/Calculator-coverage]
[info] Beginning coverage instrumentation
[info] Instrumentation completed [1 statements]
[info] Wrote instrumentation file [bazel-out/local-fastbuild/bin/test/Calculator-coverage/scoverage.coverage.xml]
[info] Will write measurement data to [bazel-out/local-fastbuild/bin/test/Calculator-coverage]
INFO: From scala //scala/support:test_reporter:
[info] Cleaning datadir [bazel-out/local-fastbuild/bin/scala/support/test_reporter-coverage]
[info] Beginning coverage instrumentation
[warn] Could not instrument [EmptyTree$/null]. No pos.
[info] Instrumentation completed [458 statements]
[info] Wrote instrumentation file [bazel-out/local-fastbuild/bin/scala/support/test_reporter-coverage/scoverage.coverage.xml]
[info] Will write measurement data to [bazel-out/local-fastbuild/bin/scala/support/test_reporter-coverage]
An exception or error caused a run to abort. This may have been caused by a problematic custom reporter.
java.io.FileNotFoundException: bazel-out/local-fastbuild/bin/scala/support/test_reporter-coverage/scoverage.measurements.1 (No such file or directory)
	at java.io.FileOutputStream.open0(Native Method)
	at java.io.FileOutputStream.open(FileOutputStream.java:270)
	at java.io.FileOutputStream.<init>(FileOutputStream.java:213)
	at java.io.FileWriter.<init>(FileWriter.java:107)
	at scoverage.Invoker$$anonfun$1.apply(Invoker.scala:42)
	at scoverage.Invoker$$anonfun$1.apply(Invoker.scala:42)
	at scala.collection.concurrent.TrieMap.getOrElseUpdate(TrieMap.scala:901)
	at scoverage.Invoker$.invoked(Invoker.scala:42)

@johnynek
Copy link
Member Author

I assume there is some env-var similar to test where we are supposed to write the file, and in a particular format, but I really haven't looked. I would google the bazel google group and see the story there.

@softprops
Copy link
Contributor

Following up but still blocked. I posted the bazel discuss list to ask for some direction/input but haven't gotten as much feed back as I've gotten here. Consider this a repost of my current state.

I'm trying to follow along with changes that were brought into 5.0 that may help me get code coverage working for scala.

My current blocker is knowing where to write coverage data to.

If I'm reading this right, this should be the block of code that exposes set of COVERAGE_* environment variables to test runs. I can see that coverage is enabled by inspecting ctx.configuration.coverage_enabled when I run bazel coverage and I believe that I'm setting the right instrumented_files field in the struct scala_library returns, but it's not immediately clear why coverage data may be null, which may be why I'm not seeing those env variables with I run bazel coverage -s

If anyone here has time to take a quick look at what I have so far, below is our diff of rules_scala

meetuparchive/rules_scala@master...scoverage-support

@ittaiz
Copy link
Member

ittaiz commented May 31, 2017 via email

@softprops
Copy link
Contributor

Thanks @ittaiz linking here for reference

@dsilvasc
Copy link

FYI, here's how pants integrates with jacoco for code coverage:
pantsbuild/pants@a643710

@johnynek
Copy link
Member Author

http://www.jacoco.org/jacoco/trunk/doc/agent.html

documentation on using the java agent. Seems like it might not be that much work.

@softprops
Copy link
Contributor

Does jacoco work for scala as well? At Meetup I know some engineers use jacoco specifically for java and scoverage for everything else.

Any new news on this enabling this would be great! I kind of dropped my initial effort of getting scoverage to work with bazel in favor of letting a jenkins job using sbt to generated reports be our answer for coverage as we moved our primary building over to bazel. My original scoverage branch for this scala_rules is likely horrible unmergable with this master branch by now but I still have it here for reference.

@johnynek
Copy link
Member Author

It looks like Jacoco works on byte code unless I’m wrong. So it may work for both java and scala.

@johnynek
Copy link
Member Author

I think maybe the only open issue is converting to lcov format, which I think is what bazel is standardizing on.

@greggdonovan
Copy link
Member

greggdonovan commented Oct 15, 2018

@iirina Any advice on how to proceed with Scala coverage now that --experimental_java_coverage is out and lcov has been standardized on? Does it make sense to wait for the coverage_toolchain work before building Scala coverage or is the experimental_java_coverage flag a sufficient foundation? Thanks!

@iirina
Copy link
Contributor

iirina commented Oct 17, 2018

For Scala the --experimental_java_coverage is enough to add coverage. I am working on a document with instructions on how to add coverage support for JVM languages, it should be ready this week.

@iirina
Copy link
Contributor

iirina commented Oct 17, 2018

@greggdonovan I put together this document with the instructions. I want to write a blog post, but to speed things up a doc should work for now.

@prebeta
Copy link
Contributor

prebeta commented Apr 3, 2019

Is anyone actively working on bazel scala coverage right now?

It seems like @softprops had a partially working solution that would instrument the classes using scoverage, but there are a few blockers regarding collecting the coverage data and the output format for the coverage data.

I'd be interested in picking things up, but it looks like there are a couple options moving forward. We can implement the coverage using Jacoco and potentially plug into Bazel's JacocoTestRunner or we can continue with the Scoverage changes which may require some custom tooling to handle coverage formats.

@dsilvasc
Copy link

dsilvasc commented Apr 3, 2019

@prebeta Which one provides more accurate coverage reports for Scala code?

@prebeta
Copy link
Contributor

prebeta commented Apr 3, 2019

@dsilvasc from my initial investigations, the branch/class coverage numbers are pretty much the same between the two. The issue with mixins skewing coverage results in Jacoco seems has been resolved in 8.0+. However, scoverage is able to give more precise statement coverage in the case of single line conditionals, but I'm not sure if this is even translatable into lcov format. Here's an example of scoverage vs jacoco reports.

Scoverage report: https://i.imgur.com/ShYX29f.png
Detected 7/10 statements covered: https://i.imgur.com/Z4Epyum.png

Jacoco report: https://i.imgur.com/0YnDnxF.png
Detected 7/8 lines covered : https://i.imgur.com/btAdKY5.png

@beala-stripe
Copy link
Contributor

beala-stripe commented Apr 3, 2019

@andyscott added preliminary code coverage support in #692

It's 'preliminary' because it produces lcov coverage traces, but iiuc there were some bazel bugs blocking bazel's built in support for generating coverage reports. You have to run genhtml over the coverage traces yourself. @andyscott could elaborate more.

I did however have some success uploading the coverage traces to codecov.io for a small test project.

@prebeta
Copy link
Contributor

prebeta commented Apr 5, 2019

@beala-stripe Thanks for the pointer! I was doing some testing and it seems to work well. :)

However, I'm hitting an issue when generating coverage using genhtml, the sources listed in coverage.dat files are relative to a scala root directory instead of the project root.

For example, I have this project structure:

├── BUILD
└── src
    ├── main
    │   └── scala
    │       ├── com
    │       │   └── scalasample
    │       │       ├── Calculator.scala
    │       │       └── CalculatorTrait.scala
    └── test
        └── scala
            └── com
                └── scalasample
                    └── CalculatorTest.scala

But when I run bazel coverage :calculator_test --extra_toolchains="@io_bazel_rules_scala//test/coverage:enable_code_coverage_aspect", the source files in coverage.dat look like:

SF:com/scalasample/Calculator.scala
SF:com/scalasample/CalculatorTest.scala
SF:com/scalasample/CalculatorTrait.scala

instead of:

src/main/scala/com/scalasample/Calculator.scala
src/main/scala/com/scalasample/CalculatorTrait.scala
src/test/scala/com/scalasample/CalculatorTest.scala

and it makes genhtml confused when generating the coverage report with sources.

The issue looks very similar to: bazelbuild/bazel#2528, and the solution was to pass in --experimental_java_coverage in order to get coverage to list java source files relative to the workspace directory.

I'm currently on bazel version 0.23.1, and the --experimental_java_coverage still exists. However, I get an error at test runtime when I pass the flag in:

Exception in thread "main" java.lang.IllegalStateException: JACOCO_METADATA_JAR environment variable is not set, and no META-INF/MANIFEST.MF on the classpath has a Coverage-Main-Class attribute.  Cannot determine the name of the main class for the code under test.
	at com.google.testing.coverage.JacocoCoverageRunner.getMainClass(JacocoCoverageRunner.java:267)
	at com.google.testing.coverage.JacocoCoverageRunner.main(JacocoCoverageRunner.java:398)

It looks like the flag has been enabled by default in 0.24.1: bazelbuild/bazel@e64066d
and when I run a coverage on that version the build, the build completes and the tests run, but I get a different error in the test.log afterwards:

...
Discovery starting.
Discovery completed in 42 milliseconds.
Run starting. Expected test count is: 2
CalculatorTest:
- testAddOrSubtract
- testMultiply
Run completed in 119 milliseconds.
Total number of tests run: 2
Suites: completed 2, aborted 0
Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
All tests passed.
java.util.zip.ZipException: error in opening zip file
	at java.util.zip.ZipFile.open(Native Method)
	at java.util.zip.ZipFile.<init>(ZipFile.java:225)
	at java.util.zip.ZipFile.<init>(ZipFile.java:155)
	at java.util.jar.JarFile.<init>(JarFile.java:166)
	at java.util.jar.JarFile.<init>(JarFile.java:130)
	at com.google.testing.coverage.JacocoCoverageRunner.addEntriesToExecPathsSet(JacocoCoverageRunner.java:285)
	at com.google.testing.coverage.JacocoCoverageRunner.createPathsSet(JacocoCoverageRunner.java:270)
	at com.google.testing.coverage.JacocoCoverageRunner.createReport(JacocoCoverageRunner.java:149)
	at com.google.testing.coverage.JacocoCoverageRunner.create(JacocoCoverageRunner.java:142)
	at com.google.testing.coverage.JacocoCoverageRunner$2.run(JacocoCoverageRunner.java:518)
+ TEST_STATUS=1
+ touch /home/tony/.cache/bazel/_bazel_tony/46a620847bc368abb6ab2ca398c067f0/sandbox/linux-sandbox/187/execroot/__main__/bazel-out/k8-fastbuild/testlogs/sandbox/calculator_test/coverage.dat
+ [[ 1 -ne 0 ]]
+ echo --
--
+ echo Coverage runner: Not collecting coverage for failed test.
Coverage runner: Not collecting coverage for failed test.
+ echo The following commands failed with status 1
The following commands failed with status 1
+ echo /home/tony/.cache/bazel/_bazel_tony/46a620847bc368abb6ab2ca398c067f0/sandbox/linux-sandbox/187/execroot/__main__/bazel-out/k8-fastbuild/bin/sandbox/calculator_test.runfiles/__main__/sandbox/calculator_test
/home/tony/.cache/bazel/_bazel_tony/46a620847bc368abb6ab2ca398c067f0/sandbox/linux-sandbox/187/execroot/__main__/bazel-out/k8-fastbuild/bin/sandbox/calculator_test.runfiles/__main__/sandbox/calculator_test
+ exit 1

@andyscott
Copy link
Contributor

@prebeta-- We have the same issue internally at Stripe!

To work around this I've pieced together a Python script that rebuilds .dat files with resolved file paths. At the moment it's got some internal paths hard coded, but I could clean it up and share it publicly if it'd be useful.

@prebeta
Copy link
Contributor

prebeta commented Apr 5, 2019

@prebeta-- We have the same issue internally at Stripe!

To work around this I've pieced together a Python script that rebuilds .dat files with resolved file paths. At the moment it's got some internal paths hard coded, but I could clean it up and share it publicly if it'd be useful.

@andyscott thanks for clarifying :)

I've worked around the pathing issues as well with a similar script, I just wanted to bring attention to the potential issues with newer versions of bazel and having --experimental_java_coverage set by default.

@softprops
Copy link
Contributor

What's the current story on this and are they examples to get started, perhaps for folks coming from the land of sbt?

@gergelyfabian
Copy link
Contributor

gergelyfabian commented Dec 19, 2019

@prebeta-- We have the same issue internally at Stripe!
To work around this I've pieced together a Python script that rebuilds .dat files with resolved file paths. At the moment it's got some internal paths hard coded, but I could clean it up and share it publicly if it'd be useful.

@andyscott thanks for clarifying :)

I've worked around the pathing issues as well with a similar script, I just wanted to bring attention to the potential issues with newer versions of bazel and having --experimental_java_coverage set by default.

Let me summarize the state (as I see it) for December 2019 (Bazel 1.2.0 and rules_scala 26cf9b7).

In the latest versions of Bazel --experimental_java_coverage is removed.
You should install lcov package to have access to genhtml (e.g. on Ubuntu: sudo apt install lcov).

To run code coverage with Jacoco for Scala code you can run (you need the extra toolchain as the scala support wasn't enabled by default - please see the comments in #692):

bazel coverage --extra_toolchains="@io_bazel_rules_scala//test/coverage:enable_code_coverage_aspect" //...

It will produce several .dat files for your modules.

Then you've got two issues to solve:

  1. bazel does not summarize the coverage statistics for you
  2. when you try running genhtml for the .dat files it won't have access to the source files

Both can be solved by using an approach taken e.g. by Gerrit project.
See bazelbuild/bazel#2528 for more information, this also seems to be an issue for Java source code. A possible solution is running a script that moves all source files to a new folder and runs genhtml from that place so that genhtml can have access to them. You can check https://gerrit-review.googlesource.com/c/gerrit/+/106471/6/tools/coverage.sh for inspiration. Current version of this script is also in Gerrit source code: https://gerrit.googlesource.com/gerrit/+/master/tools/coverage.sh.

Using a modified version of this script solved the html report problem for me.

@gergelyfabian
Copy link
Contributor

I believe this issue could be closed as Jacoco code coverage for Scala works.

@ittaiz
Copy link
Member

ittaiz commented Dec 20, 2019

Thanks! from your previous post it sounds like it works but in awkward way. Is this bazel's fault, rules_scala's fault or just how coverage business works? (We don't do coverage internally so I don't really know)

@softprops
Copy link
Contributor

I believe this issue could be closed as Jacoco code coverage for Scala works.

If we have a working example can we document it. Some times as passed so things may have changed but this is where we're currently blocked

scala_rules@5261499b0485f33799a1b210796fcdfa720a5344
[email protected]

bazel coverage //...
[snip]
*** RUN ABORTED *** (857 milliseconds)
  java.lang.NoClassDefFoundError: org/jacoco/agent/rt/internal_1f1cc91/Offline
  at com.meetup.base.secret.Secrets.<clinit>(Secrets.java)
  at sun.reflect.GeneratedSerializationConstructorAccessor1.newInstance(Unknown Source)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:45)
  at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:73)
  at org.mockito.internal.creation.instance.ObjenesisInstantiator.newInstance(ObjenesisInstantiator.java:14)
  at org.mockito.internal.creation.cglib.ClassImposterizer.createProxy(ClassImposterizer.java:143)
  at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.java:58)
  at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.java:49)
  at org.mockito.internal.creation.cglib.CglibMockMaker.createMock(CglibMockMaker.java:24)
  at org.mockito.internal.util.MockUtil.createMock(MockUtil.java:33)
  at org.mockito.internal.MockitoCore.mock(MockitoCore.java:59)
  at org.mockito.Mockito.mock(Mockito.java:1285)
  at org.mockito.Mockito.mock(Mockito.java:1163)
  at org.scalatest.mockito.MockitoSugar$class.mock(MockitoSugar.scala:73)
  at com.meetup.base.db.pool.ConnectionPoolTest.mock(ConnectionPoolTest.scala:14)
  at com.meetup.base.db.pool.ConnectionPoolTest$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$mcV$sp$3.apply(ConnectionPoolTest.scala:18)
  at com.meetup.base.db.pool.ConnectionPoolTest$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$mcV$sp$3.apply(ConnectionPoolTest.scala:17)
  at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
  at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
  at org.scalatest.Transformer.apply(Transformer.scala:22)
  at org.scalatest.Transformer.apply(Transformer.scala:20)
  at org.scalatest.FunSpecLike$$anon$1.apply(FunSpecLike.scala:454)
  at org.scalatest.TestSuite$class.withFixture(TestSuite.scala:196)
  at org.scalatest.FunSpec.withFixture(FunSpec.scala:1630)
  at org.scalatest.FunSpecLike$class.invokeWithFixture$1(FunSpecLike.scala:451)
  at org.scalatest.FunSpecLike$$anonfun$runTest$1.apply(FunSpecLike.scala:464)
  at org.scalatest.FunSpecLike$$anonfun$runTest$1.apply(FunSpecLike.scala:464)
  at org.scalatest.SuperEngine.runTestImpl(Engine.scala:289)
  at org.scalatest.FunSpecLike$class.runTest(FunSpecLike.scala:464)
  at org.scalatest.FunSpec.runTest(FunSpec.scala:1630)
  at org.scalatest.FunSpecLike$$anonfun$runTests$1.apply(FunSpecLike.scala:497)
  at org.scalatest.FunSpecLike$$anonfun$runTests$1.apply(FunSpecLike.scala:497)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:396)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
  at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:373)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:410)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
  at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:373)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:410)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
  at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:379)
  at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:461)
  at org.scalatest.FunSpecLike$class.runTests(FunSpecLike.scala:497)
  at org.scalatest.FunSpec.runTests(FunSpec.scala:1630)
  at org.scalatest.Suite$class.run(Suite.scala:1147)
  at org.scalatest.FunSpec.org$scalatest$FunSpecLike$$super$run(FunSpec.scala:1630)
  at org.scalatest.FunSpecLike$$anonfun$run$1.apply(FunSpecLike.scala:501)
  at org.scalatest.FunSpecLike$$anonfun$run$1.apply(FunSpecLike.scala:501)
  at org.scalatest.SuperEngine.runImpl(Engine.scala:521)
  at org.scalatest.FunSpecLike$class.run(FunSpecLike.scala:501)
  at org.scalatest.FunSpec.run(FunSpec.scala:1630)
  at org.scalatest.Suite$class.callExecuteOnSuite$1(Suite.scala:1210)
  at org.scalatest.Suite$$anonfun$runNestedSuites$1.apply(Suite.scala:1257)
  at org.scalatest.Suite$$anonfun$runNestedSuites$1.apply(Suite.scala:1255)
  at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:33)
  at scala.collection.mutable.ArrayOps$ofRef.foreach(ArrayOps.scala:186)
  at org.scalatest.Suite$class.runNestedSuites(Suite.scala:1255)
  at org.scalatest.tools.DiscoverySuite.runNestedSuites(DiscoverySuite.scala:30)
  at org.scalatest.Suite$class.run(Suite.scala:1144)
  at org.scalatest.tools.DiscoverySuite.run(DiscoverySuite.scala:30)
  at org.scalatest.tools.SuiteRunner.run(SuiteRunner.scala:45)
  at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1346)
  at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1340)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.tools.Runner$.doRunRunRunDaDoRunRun(Runner.scala:1340)
  at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1011)
  at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1010)
  at org.scalatest.tools.Runner$.withClassLoaderAndDispatchReporter(Runner.scala:1506)
  at org.scalatest.tools.Runner$.runOptionallyWithPassFailReporter(Runner.scala:1010)
  at org.scalatest.tools.Runner$.main(Runner.scala:827)
  at org.scalatest.tools.Runner.main(Runner.scala)
  at io.bazel.rulesscala.scala_test.Runner.main(Runner.java:34)
  Cause: java.lang.ClassNotFoundException: org.jacoco.agent.rt.internal_1f1cc91.Offline
  at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
  at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
  at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
  at com.meetup.base.secret.Secrets.<clinit>(Secrets.java)
  at sun.reflect.GeneratedSerializationConstructorAccessor1.newInstance(Unknown Source)
  at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
  at org.objenesis.instantiator.sun.SunReflectionFactoryInstantiator.newInstance(SunReflectionFactoryInstantiator.java:45)
  at org.objenesis.ObjenesisBase.newInstance(ObjenesisBase.java:73)
  at org.mockito.internal.creation.instance.ObjenesisInstantiator.newInstance(ObjenesisInstantiator.java:14)
  at org.mockito.internal.creation.cglib.ClassImposterizer.createProxy(ClassImposterizer.java:143)
  at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.java:58)
  at org.mockito.internal.creation.cglib.ClassImposterizer.imposterise(ClassImposterizer.java:49)
  at org.mockito.internal.creation.cglib.CglibMockMaker.createMock(CglibMockMaker.java:24)
  at org.mockito.internal.util.MockUtil.createMock(MockUtil.java:33)
  at org.mockito.internal.MockitoCore.mock(MockitoCore.java:59)
  at org.mockito.Mockito.mock(Mockito.java:1285)
  at org.mockito.Mockito.mock(Mockito.java:1163)
  at org.scalatest.mockito.MockitoSugar$class.mock(MockitoSugar.scala:73)
  at com.meetup.base.db.pool.ConnectionPoolTest.mock(ConnectionPoolTest.scala:14)
  at com.meetup.base.db.pool.ConnectionPoolTest$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$mcV$sp$3.apply(ConnectionPoolTest.scala:18)
  at com.meetup.base.db.pool.ConnectionPoolTest$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$mcV$sp$3.apply(ConnectionPoolTest.scala:17)
  at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
  at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
  at org.scalatest.Transformer.apply(Transformer.scala:22)
  at org.scalatest.Transformer.apply(Transformer.scala:20)
  at org.scalatest.FunSpecLike$$anon$1.apply(FunSpecLike.scala:454)
  at org.scalatest.TestSuite$class.withFixture(TestSuite.scala:196)
  at org.scalatest.FunSpec.withFixture(FunSpec.scala:1630)
  at org.scalatest.FunSpecLike$class.invokeWithFixture$1(FunSpecLike.scala:451)
  at org.scalatest.FunSpecLike$$anonfun$runTest$1.apply(FunSpecLike.scala:464)
  at org.scalatest.FunSpecLike$$anonfun$runTest$1.apply(FunSpecLike.scala:464)
  at org.scalatest.SuperEngine.runTestImpl(Engine.scala:289)
  at org.scalatest.FunSpecLike$class.runTest(FunSpecLike.scala:464)
  at org.scalatest.FunSpec.runTest(FunSpec.scala:1630)
  at org.scalatest.FunSpecLike$$anonfun$runTests$1.apply(FunSpecLike.scala:497)
  at org.scalatest.FunSpecLike$$anonfun$runTests$1.apply(FunSpecLike.scala:497)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:396)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
  at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:373)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:410)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
  at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:373)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:410)
  at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
  at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:379)
  at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:461)
  at org.scalatest.FunSpecLike$class.runTests(FunSpecLike.scala:497)
  at org.scalatest.FunSpec.runTests(FunSpec.scala:1630)
  at org.scalatest.Suite$class.run(Suite.scala:1147)
  at org.scalatest.FunSpec.org$scalatest$FunSpecLike$$super$run(FunSpec.scala:1630)
  at org.scalatest.FunSpecLike$$anonfun$run$1.apply(FunSpecLike.scala:501)
  at org.scalatest.FunSpecLike$$anonfun$run$1.apply(FunSpecLike.scala:501)
  at org.scalatest.SuperEngine.runImpl(Engine.scala:521)
  at org.scalatest.FunSpecLike$class.run(FunSpecLike.scala:501)
  at org.scalatest.FunSpec.run(FunSpec.scala:1630)
  at org.scalatest.Suite$class.callExecuteOnSuite$1(Suite.scala:1210)
  at org.scalatest.Suite$$anonfun$runNestedSuites$1.apply(Suite.scala:1257)
  at org.scalatest.Suite$$anonfun$runNestedSuites$1.apply(Suite.scala:1255)
  at scala.collection.IndexedSeqOptimized$class.foreach(IndexedSeqOptimized.scala:33)
  at scala.collection.mutable.ArrayOps$ofRef.foreach(ArrayOps.scala:186)
  at org.scalatest.Suite$class.runNestedSuites(Suite.scala:1255)
  at org.scalatest.tools.DiscoverySuite.runNestedSuites(DiscoverySuite.scala:30)
  at org.scalatest.Suite$class.run(Suite.scala:1144)
  at org.scalatest.tools.DiscoverySuite.run(DiscoverySuite.scala:30)
  at org.scalatest.tools.SuiteRunner.run(SuiteRunner.scala:45)
  at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1346)
  at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1340)
  at scala.collection.immutable.List.foreach(List.scala:392)
  at org.scalatest.tools.Runner$.doRunRunRunDaDoRunRun(Runner.scala:1340)
  at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1011)
  at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1010)
  at org.scalatest.tools.Runner$.withClassLoaderAndDispatchReporter(Runner.scala:1506)
  at org.scalatest.tools.Runner$.runOptionallyWithPassFailReporter(Runner.scala:1010)
  at org.scalatest.tools.Runner$.main(Runner.scala:827)
  at org.scalatest.tools.Runner.main(Runner.scala)
  at io.bazel.rulesscala.scala_test.Runner.main(Runner.java:34)

@gergelyfabian
Copy link
Contributor

Thanks! from your previous post it sounds like it works but in awkward way. Is this bazel's fault, rules_scala's fault or just how coverage business works? (We don't do coverage internally so I don't really know)

  1. bazel does not summarize the coverage statistics for you - bazel's or rules_scala's fault, I cannot tell which
  2. when you try running genhtml for the .dat files it won't have access to the source files - this seems to be a general JVM issue - , please check the Gerrit ticket with Bazel I quoted: genhtml can't find Java source files when generating a coverage report bazel#2528

@gergelyfabian
Copy link
Contributor

gergelyfabian commented Jan 2, 2020

I believe this issue could be closed as Jacoco code coverage for Scala works.

If we have a working example can we document it. Some times as passed so things may have changed but this is where we're currently blocked

scala_rules@5261499b0485f33799a1b210796fcdfa720a5344
[email protected]

Your rules_scala version seems to be a bit old (August 2019). I tried with 26cf9b7, that is from November 2019.

Also, I used rules_jvm_external 3.1 for this test to add the JVM dependencies.

@gergelyfabian
Copy link
Contributor

gergelyfabian commented Jan 2, 2020

Here is a working example:

https://github.com/gergelyfabian/bazel-scala-example

Run:

bazel coverage --extra_toolchains="@io_bazel_rules_scala//test/coverage:enable_code_coverage_aspect" //...

EDIT: example was updated with multiple targets generating code coverage and a demonstration how to use genhtml (with a script inspired by Gerrit project).

@ittaiz
Copy link
Member

ittaiz commented Jan 4, 2020 via email

@gergelyfabian
Copy link
Contributor

Maybe this could be a documentation PR?

Sure :) However, please let me know what you mean by that, as I'm a bazel beginner :)
How should I create a documentation PR, and for which component? rules_scala I guess?

@ittaiz
Copy link
Member

ittaiz commented Jan 7, 2020 via email

@d-haxton
Copy link
Contributor

Just a quick update for anyone still paying attention: #1006

This should completely remove the need to run any scripts to fix file paths. You should just be able to run genhtml from your directory and passing in the coverage.dat (although it will add noise to your code, but that's a different conversations 😂)

@gergelyfabian
Copy link
Contributor

Just a quick update for anyone still paying attention: #1006

This should completely remove the need to run any scripts to fix file paths. You should just be able to run genhtml from your directory and passing in the coverage.dat (although it will add noise to your code, but that's a different conversations )

After merging #1006 to master indeed code coverage support in rules_scala become a lot better. I confirm that for my usecase indeed simply running genhtml works without any path fixes.

@ittaiz
Copy link
Member

ittaiz commented Feb 28, 2020

@gergelyfabian great! Wdyt about sending a PR to the readme of what one should do to get coverage?
Also this is only for scalatest Targets, right?
I think junit/specs2 should be low cost but AFAIU they aren’t currently supported

@gergelyfabian
Copy link
Contributor

Here is the PR for documentation on coverage: #1017

I only tested with scalatest.

@tanishiking
Copy link
Contributor

tanishiking commented Mar 21, 2023

Regarding scoverage integration: FYI, as commented in

The scala side should be easy. What I don't know is how to plug in to
bazel's expectations (if they are even standard). Like where should we
write the coverage files to? What should the format be?
#184 (comment)

the scala side isn't hard, we can create an extra instrumented version of build target, then it writes to the location we specified. like this https://github.com/tanishiking/bazel-playground/blob/main/18-scala-scoverage

The thing is "how to plug in to bazel's expectations (if they are even standard)", and maybe now we have good references

sluongng commented on May 11, 2021
Good references should be from rules_go:
bazel-contrib/rules_go#140 (and some PRs mentioned here)
https://docs.google.com/document/d/1-ZWHF-Q-qCKf19ik-t33ie58BkNurrYYzKR4OLtcilY/edit
Generally Bazel's coverage is not yet complete.
If your code coverage tooling does not follow the Java's convention, you gona have a bad time.
bazelbuild/rules_rust#690 (comment)

@tanishiking
Copy link
Contributor

tanishiking commented Mar 27, 2023

Hey, I've been trying to get rules_scala to integrate well with scoverage, and I've found that it's not possible with the current scoverage implementation 😞
I'll leave some insights I gained during my struggle.

https://github.com/tanishiking/bazel-playground/tree/main/18-scala-scoverage

How scoverage-scalac-plugin works?

While JaCoCo instrument the compiled bytecode, Scoverage instrument against the source code (Scala AST) in the very early stages of the compilation phases.

To instrument the given scala code, we'll provde the following scalacopts

-Xplugin:/path/to/plugin/scalac-scoverage-plugin_1.4.11.jar # for scoverage >2, we have to add some more deps
-P:scoverage:dataDir:/path/to/project/output
-P:scoverage:sourceRoot:/path-to/project/root
  • At compile time, scoverage-scalac-plugin will instrument the source code, and create a scoverage.coverage to dataDir specified in scalacopts.
  • At runtime (test), instrumented code will call Invoker#invoked, which will write a file something called scoverage.measurements into dataDir specified in scalacopts.
  • Finally, CoverageAggregator will aggregate all scoverage.coverage + scoverage.measurements files in dataDirs, and generate HTML or XML coverage report.

Watch @ckipp01's video for more details 👍
https://www.youtube.com/watch?v=SIkNgemGmYQ

Naive integration

So, I tried to make PoC scoverage + rules_scala works here

https://github.com/tanishiking/bazel-playground/tree/main/18-scala-scoverage

  • In this project, we have instrumented version of targets such as //src/main/scala/mypackage:mypackage.instrumented.
    • This target is instrumented by scoverage with wrapped scala_library rule and we specify -P:scoverage:dataDir:/tmp/scoverage-data/src_main_scala_mypackage_mypackage.instrumented.
  • And tests depends on //src/main/scala/mypackage:mypackage.instrumented.
  • When we build //src/main/scala/mypackage:mypackage.instrumented it will write scoverage.coverage file into specified dataDir
  • And when we run test it will write scoverage.measurements into specified dataDir.
  • Once we ran tests, we can aggregate coverage information by running a script that wraps CoverageAggregator

Even though it doesn't follow "bazel way" (it generates coverage report by bazel test, tests have to depends on instrumented targets, and it writes things into somewhere outside output base), it somehow "works".

However, there're plenty of problems with this implementation, of course.

(1) scoverage.coverage won't be cached.

Since we specify -P:scoverage:dataDir:/tmp/... and we don't register them as outputs, they will of course not be cached.
As a result, when the build is cached, scoverage.coverage won't be available and we won't be able to do any coverage aggregation. (Actually, the dataDir won't be created and invokers will fail to write measurements).

We have to make scoverage.coverage to be cached.

Suppose we're trying to add/replace a phase using customized_phase.

  • Replace phase_scalacopts and add "-P:scoverage:dataDir": + ctx.outputs.scoverage to scalacopts.
    • It didn't work, scoverage will write to somewhere in bazel-workers/worker-x-scalac at compile time (scoverage.coverage), and it tries to write from path relative to exec root at runtime (scoverage.measurements). It seems like we have to provide absolute path to dataDir.
  • Can we specify the absolute path of exec root in -P:scoverage:dataDir?
    • I couldn't find a way to get exec root in absolute path from .bzl files. (because it depends on machines, I guess)
  • Is it possible to ctx.declare_directory("/tmp/.....")?
    • No, it fails with Error in declare_directory: the output directory '/tmp/scoverage-data/...' is not under package directory '...' for target '...'
  • Write to /tmp directory and copy it into somewhere under bazel-out?
    • Copy sources have to be an instance of TreeArtifact or File object with bazel-skylib's copy_directory_action and ctx.actions.run_shell
    • As far as I know, we have to run ctx.declare_directory to retrieve the TreeArtifact represents the /tmp/... directory, but we can't because of the above reason.

That being said, it seems like we may want to scoverage-scalac-plugin to write scoverage.coverage information into output JAR file together?

(2) scoverage.measurements won't be cached.

Since scoverage.measurements also not registered as rule's output, they won't be cached too.
As a result, if a test is cached, the coverage data also won't be available.

The problem is scoverage-scalac-plugin will write measurements files into dataDir specified in target under test, tests can't know where the measurements files will be written.

(3) How can we make it work with bazel coverage ???

How/when should we instrument the code?

For JaCoCo, Bazel will instrument the compiled bytecode and create .uninstrumented class file, and normal class files.

https://docs.google.com/document/d/1mQLQa2uMIgVcwTE-hwaKgwQ5upExIRH5p9lHFyxhj4g/edit#heading=h.sfpf1dphkfqb

However, tools that instrument against source code such as scoverage and cargo-tarpaulin for Rust making two (instrumented and uninstrumented) compiled artifact means we have to run compilation twice, which seems not acceptable, especially for Scala (which isn't compile that fast).

@alexmtrmd
Copy link

+1

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

No branches or pull requests