From 6cb3357d80cf6dcaec3366adba7d61fa93a5418b Mon Sep 17 00:00:00 2001 From: phdoerfler Date: Fri, 16 Oct 2026 16:19:59 +0200 Subject: [PATCH] bugfix: Keep coverage data in the class directory on sbt 2 Fixes #696 --- .github/workflows/ci.yml | 6 ++++++ README.md | 6 +++++- .../scoverage/ScoverageSbtPluginCompat.scala | 7 +++++++ .../scoverage/ScoverageSbtPluginCompat.scala | 21 ++++++++++++++++++- .../scala/scoverage/ScoverageSbtPlugin.scala | 2 +- .../scoverage/cached-compile/build.sbt | 16 ++++++++++++++ .../cached-compile/project/plugins.sbt | 9 ++++++++ .../cached-compile/src/main/scala/Calc.scala | 3 +++ .../src/test/scala/CalcSuite.scala | 3 +++ src/sbt-test/scoverage/cached-compile/test | 14 +++++++++++++ 10 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/sbt-test/scoverage/cached-compile/build.sbt create mode 100644 src/sbt-test/scoverage/cached-compile/project/plugins.sbt create mode 100644 src/sbt-test/scoverage/cached-compile/src/main/scala/Calc.scala create mode 100644 src/sbt-test/scoverage/cached-compile/src/test/scala/CalcSuite.scala create mode 100644 src/sbt-test/scoverage/cached-compile/test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c4dacf..6afb9d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,12 @@ jobs: if: "${{ matrix.java != 8 }}" run: sbt "++ 3.x" test + # "++ 3.x" selects the sbt 2 plugin build (pluginCrossBuild / sbtVersion), so scripted + # launches the test projects with sbt 2. Only tests that support sbt 2 are listed. + - name: Run scripted tests sbt 2 + if: "${{ matrix.java != 8 }}" + run: sbt "++ 3.x" "scripted scoverage/cached-compile" + formatting: runs-on: ubuntu-latest diff --git a/README.md b/README.md index 1b22e4e..aa6ba7c 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,11 @@ settings will apply to that report. ### Override Location for Coverage Data And Report If desired, one could override the default location for generating the -sbt report and data through setting `coverageDataDir`: +sbt report and data through setting `coverageDataDir`. It defaults to +`crossTarget` on sbt 1 and to `Compile / classDirectory` on sbt 2. On sbt 2, +`compile` is cached, and a cache hit only restores the class directory, so the +scoverage data must live inside it. If you override it on sbt 2, keep it inside +`Compile / classDirectory`. Example in data-dir test: ```scala diff --git a/src/main/scala-2/scoverage/ScoverageSbtPluginCompat.scala b/src/main/scala-2/scoverage/ScoverageSbtPluginCompat.scala index cd20548..b5ea6e1 100644 --- a/src/main/scala-2/scoverage/ScoverageSbtPluginCompat.scala +++ b/src/main/scala-2/scoverage/ScoverageSbtPluginCompat.scala @@ -1,7 +1,14 @@ package scoverage +import sbt.Keys._ +import sbt._ + private[scoverage] object ScoverageSbtPluginCompat { implicit class DefOps(private val self: sbt.Def.type) extends AnyVal { def uncached[A](a: A): A = a } + + /** Default for `coverageDataDir` on sbt 1. */ + val defaultCoverageDataDir: Def.Initialize[File] = + Def.setting(crossTarget.value) } diff --git a/src/main/scala-3/scoverage/ScoverageSbtPluginCompat.scala b/src/main/scala-3/scoverage/ScoverageSbtPluginCompat.scala index 94f3770..1ec353f 100644 --- a/src/main/scala-3/scoverage/ScoverageSbtPluginCompat.scala +++ b/src/main/scala-3/scoverage/ScoverageSbtPluginCompat.scala @@ -1,3 +1,22 @@ package scoverage -private[scoverage] object ScoverageSbtPluginCompat +import sbt.Keys.* +import sbt.* + +private[scoverage] object ScoverageSbtPluginCompat { + + /** Default for `coverageDataDir` on sbt 2. + * + * sbt 2 caches `compile` and, on a cache hit, restores only its declared + * outputs, i.e. the class directory. The scoverage-data directory + * (`scoverage.coverage`, plus the directory the instrumented classes write + * measurements into) is a side effect of compilation. Under `crossTarget` it + * is not restored, so tests fail with a FileNotFoundException or + * coverageReport finds no data. Keeping it inside the class directory makes + * it part of the cached compile output. + * + * See https://github.com/scoverage/sbt-scoverage/issues/696 + */ + val defaultCoverageDataDir: Def.Initialize[File] = + Def.setting((Compile / classDirectory).value) +} diff --git a/src/main/scala/scoverage/ScoverageSbtPlugin.scala b/src/main/scala/scoverage/ScoverageSbtPlugin.scala index 4053709..c9c892a 100644 --- a/src/main/scala/scoverage/ScoverageSbtPlugin.scala +++ b/src/main/scala/scoverage/ScoverageSbtPlugin.scala @@ -71,7 +71,7 @@ object ScoverageSbtPlugin extends AutoPlugin { coverageReport := coverageReport0.value, coverageAggregate := coverageAggregate0.value, coverageAggregate / aggregate := false, - coverageDataDir := crossTarget.value, + coverageDataDir := defaultCoverageDataDir.value, coverageScalacPluginVersion := { scalaVersion.value match { case "2.13.11" => "2.3.0" diff --git a/src/sbt-test/scoverage/cached-compile/build.sbt b/src/sbt-test/scoverage/cached-compile/build.sbt new file mode 100644 index 0000000..31beeb7 --- /dev/null +++ b/src/sbt-test/scoverage/cached-compile/build.sbt @@ -0,0 +1,16 @@ +version := "0.1" +scalaVersion := "3.8.4" +libraryDependencies += "org.scalameta" %% "munit" % "1.1.1" % Test +Test / fork := true +coverageMinimumStmtTotal := 100 +coverageFailOnMinimum := true + +// coverageReport only logs "No coverage data" when the instrumentation file is +// missing, so assert on it explicitly. A command rather than a task: sbt 2 +// caches tasks, and a cached check would not look at the file again. +commands += Command.command("checkCoverageData") { state => + val dir = Project.extract(state).get(coverageDataDir) + val f = dir / "scoverage-data" / "scoverage.coverage" + if (!f.exists) sys.error(s"missing $f") + state +} diff --git a/src/sbt-test/scoverage/cached-compile/project/plugins.sbt b/src/sbt-test/scoverage/cached-compile/project/plugins.sbt new file mode 100644 index 0000000..4361ea4 --- /dev/null +++ b/src/sbt-test/scoverage/cached-compile/project/plugins.sbt @@ -0,0 +1,9 @@ +val pluginVersion = sys.props.getOrElse( + "plugin.version", + throw new RuntimeException( + """|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin + ) +) + +addSbtPlugin("org.scoverage" % "sbt-scoverage" % pluginVersion) diff --git a/src/sbt-test/scoverage/cached-compile/src/main/scala/Calc.scala b/src/sbt-test/scoverage/cached-compile/src/main/scala/Calc.scala new file mode 100644 index 0000000..96106bd --- /dev/null +++ b/src/sbt-test/scoverage/cached-compile/src/main/scala/Calc.scala @@ -0,0 +1,3 @@ +object Calc { + def add(a: Int, b: Int): Int = a + b +} diff --git a/src/sbt-test/scoverage/cached-compile/src/test/scala/CalcSuite.scala b/src/sbt-test/scoverage/cached-compile/src/test/scala/CalcSuite.scala new file mode 100644 index 0000000..baafeaa --- /dev/null +++ b/src/sbt-test/scoverage/cached-compile/src/test/scala/CalcSuite.scala @@ -0,0 +1,3 @@ +class CalcSuite extends munit.FunSuite { + test("add")(assertEquals(Calc.add(1, 2), 3)) +} diff --git a/src/sbt-test/scoverage/cached-compile/test b/src/sbt-test/scoverage/cached-compile/test new file mode 100644 index 0000000..25c785d --- /dev/null +++ b/src/sbt-test/scoverage/cached-compile/test @@ -0,0 +1,14 @@ +# First round compiles with coverage and populates the build cache. +> clean +> coverage +> testOnly CalcSuite +> checkCoverageData +> coverageReport + +# Second round: on sbt 2, compile is restored from the cache after clean. +# The scoverage data must come back with it. +> clean +> coverage +> testOnly CalcSuite +> checkCoverageData +> coverageReport