From 2fa57f18be43a57459a9656a5f740c802e1a75a0 Mon Sep 17 00:00:00 2001 From: "Tobias.Mikula" Date: Fri, 5 Jun 2026 09:38:01 +0200 Subject: [PATCH 1/3] Removing JaCoCo state. --- .github/workflows/ci-check-jacoco.yml | 61 ---- build.sbt | 1 - jmf-rules.txt | 162 ---------- project/FilteredJacocoAgentPlugin.scala | 373 ------------------------ project/JacocoBaseKeysPlugin.scala | 28 -- 5 files changed, 625 deletions(-) delete mode 100644 .github/workflows/ci-check-jacoco.yml delete mode 100644 jmf-rules.txt delete mode 100644 project/FilteredJacocoAgentPlugin.scala delete mode 100644 project/JacocoBaseKeysPlugin.scala diff --git a/.github/workflows/ci-check-jacoco.yml b/.github/workflows/ci-check-jacoco.yml deleted file mode 100644 index 9fa8841..0000000 --- a/.github/workflows/ci-check-jacoco.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: JaCoCo Report - -on: - pull_request: - branches: [ master ] - types: [ opened, edited, synchronize, reopened ] - -env: - scalaLong: 2.13.11 - scalaShort: "2.13" - coverage-overall: 80.0 - coverage-changed-files: 80.0 - coverage-per-changed-file: 0.0 - check-overall-coverages: true - -jobs: - jacoco-report: - name: JaCoCo Report - runs-on: ubuntu-latest - services: - dynamodb-local: - image: "amazon/dynamodb-local:latest" - ports: - - "8000:8000" - - steps: - - name: Checkout code - id: code-checkout - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Setup JVM and SBT - id: jvm-setup - uses: coursier/setup-action@v1.3.5 - with: - jvm: corretto:21.0.2.13.1 - apps: sbt - - - name: Build and run tests with test coverage - id: jacoco-run - continue-on-error: true - run: sbt jacoco - env: - AWS_REGION: "ignored" - AWS_ACCESS_KEY_ID: "ignored" - AWS_SECRET_ACCESS_KEY: "ignored" - - - name: Publish JaCoCo Report in PR comments - id: jacoco - uses: MoranaApps/jacoco-report@v2 - with: - token: '${{ secrets.GITHUB_TOKEN }}' - paths: | - **/target/**/jacoco/report/jacoco.xml - sensitivity: "detail" - comment-mode: 'single' - min-coverage-overall: ${{ env.coverage-overall }} - min-coverage-changed-files: ${{ env.coverage-changed-files }} - min-coverage-per-changed-file: ${{ env.coverage-per-changed-file }} - skip-unchanged: false diff --git a/build.sbt b/build.sbt index 796b8c8..c6ed8b8 100644 --- a/build.sbt +++ b/build.sbt @@ -58,4 +58,3 @@ lazy val root = (project in file(".")) ) .enablePlugins(AutomateHeaderPlugin) .enablePlugins(AssemblyPlugin) - .enablePlugins(FilteredJacocoAgentPlugin) diff --git a/jmf-rules.txt b/jmf-rules.txt deleted file mode 100644 index 949a90e..0000000 --- a/jmf-rules.txt +++ /dev/null @@ -1,162 +0,0 @@ -# jacoco-method-filter — Default Rules & HowTo (Scala) -# [jmf:1.0.0] -# -# This file defines which methods should be annotated as *Generated so JaCoCo ignores them. -# One rule per line. -# -# ───────────────────────────────────────────────────────────────────────────── -# HOW TO USE (quick) -# 1) Replace YOUR.PACKAGE.ROOT with your project’s package root (e.g., com.example.app). -# 2) Start with the CONSERVATIVE section only. -# 3) If clean, enable STANDARD. Use AGGRESSIVE only inside DTO/auto‑generated packages. -# 4) Keep rules narrow (by package), prefer flags (synthetic/bridge) for compiler artifacts, -# and add `id:` labels so logs are easy to read. -# -# ───────────────────────────────────────────────────────────────────────────── -# ALLOWED SYNTAX (cheat sheet) -# -# General form: -# #() [FLAGS and PREDICATES...] -# -# FQCN_glob (dot form; $ allowed for inner classes): -# Examples: *.model.*, com.example.*, * -# -# method_glob (glob on method name): -# Examples: copy | $anonfun$* | get* | *_$eq -# -# descriptor_glob (JVM descriptor in (args)ret). You may omit it entirely. -# • Omitting descriptor ⇒ treated as "(*)*" (any args, any return). -# • Short/empty forms "", "()", "(*)" normalize to "(*)*". -# Examples: -# (I)I # takes int, returns int -# (Ljava/lang/String;)V # takes String, returns void -# () or (*) or omitted # any args, any return -# -# FLAGS (optional) — space or comma separated: -# public | protected | private | synthetic | bridge | static | abstract -# -# PREDICATES (optional): -# ret: # match return type only (e.g., ret:V, ret:I, ret:Lcom/example/*;) -# id: # identifier shown in logs/reports -# name-contains: # method name must contain -# name-starts: # method name must start with -# name-ends: # method name must end with -# -# Notes -# - Always use dot-form (com.example.Foo) for class names. -# - Comments (# …) and blank lines are ignored. -# -# ───────────────────────────────────────────────────────────────────────────── -# QUICK EXAMPLES -# -# Simple wildcards -# *#*(*) -# → Match EVERY method in EVERY class (any package). Useful only for diagnostics. -# "(*)" normalizes to "(*)*" ⇒ any args, any return. -# *.dto.*#*(*) -# → Match every method on any class under any package segment named "dto". -# Good when you treat DTOs as generated/boilerplate. - -# Scala case class helpers -# *.model.*#copy(*) -# → Matches Scala case-class `copy` methods under `*.model.*`. -# Hides boilerplate clones with any parameter list and any return. -# *.model.*#productArity() -# → Matches zero-arg `productArity` (case-class/Product API). -# *.model.*#productElement(*) -# → Matches `productElement(int)` (or any descriptor form) on case classes. -# *.model.*#productPrefix() -# → Matches `productPrefix()`; returns the case class' constructor name. - -# Companion objects and defaults -# *.model.*$*#apply(*) -# → Matches companion `apply` factories under `*.model.*` (any args). -# BE CAREFUL: can hide real factory logic; keep the package scope narrow. -# *.model.*$*#unapply(*) -# → Matches extractor `unapply` methods in companions under `*.model.*`. -# *#*$default$*(*) -# → Matches Scala-generated default-argument helpers everywhere. -# Safe to keep enabled; they’re compiler-synthesized. - -# Anonymous / synthetic / bridge -# *#$anonfun$* -# → Matches any method whose name contains `$anonfun$` (Scala lambdas). -# Consider adding `synthetic` and/or a package scope in real configs. -# *#*(*):synthetic # any synthetic -# → Matches ANY method marked `synthetic` (compiler-generated). -# Powerful; scope by package to avoid hiding intentional glue code. -# *#*(*):bridge # any bridge -# → Matches Java generic bridge methods the compiler inserts. -# Usually safe globally, but scoping is still recommended. - -# Setters / fluent APIs -# *.dto.*#*_$eq(*) -# → Matches Scala var setters in DTO packages (e.g., `name_=(...)`). -# Good for excluding trivial field writes. -# *.builder.*#with*(*) -# → Matches builder-style fluent setters (`withXxx(...)`) in builder pkgs. -# Treats chainable configuration as boilerplate. -# *.client.*#with*(*) ret:Lcom/api/client/* -# → Like above but ONLY when the return type matches your client package. -# The `ret:` predicate protects real logic that returns other types. - -# Return-type constraints -# *.jobs.*#*(*):ret:V -# → Any method under `*.jobs.*` returning `void` (`V`). Often orchestration. -# *.math.*#*(*):ret:I -# → Any method under `*.math.*` returning primitive int (`I`). -# *.model.*#*(*):ret:Lcom/example/model/* -# → Any method under `*.model.*` that returns a type in `com.example.model`. -# Handy when the *return type* uniquely identifies boilerplate. - -# ───────────────────────────────────────────────────────────────────────────── -# GLOBALS RULES -# ───────────────────────────────────────────────────────────────────────────── -# ** all case class boilerplate - -# Scala case class helpers -*#canEqual(*) id:case-canequal -*#equals(*) id:case-equals -*#apply(*) id:case-apply -*#unapply(*) id:case-unapply -*#hashCode(*) id:case-hashcode -*#copy(*) id:case-copy -*#copy$default$*(*) id:case-copy-defaults -*#productElement() id:case-prod-element -*#productArity() id:case-prod-arity -*#productPrefix() id:case-prod-prefix -*#productIterator() id:case-prod-iterator -*#tupled() id:case-tupled -*#curried() id:case-curried -*#toString() id:case-tostring -*#name() id:case-name -*#groups() id:case-groups -*#optionalAttributes() id:case-optionalAttributes - -# Companion objects, constructors, and static definitions -*$#(*) id:gen-ctor # constructors -*$#() id:gen-clinit # static initializer blocks - -# Companion objects and defaults -*$*#apply(*) id:comp-apply -*$*#unapply(*) id:comp-unapply -*$*#toString(*) id:comp-tostring -*$*#readResolve(*) id:comp-readresolve - -# anonymous class created by a macro expansion -*$macro$*#$anonfun$inst$macro$* id:macro-inst -*$macro$*#inst$macro$* id:macro-inst - -# lambda -*#* synthetic name-contains:$anonfun$ id:scala-anonfun - -# ───────────────────────────────────────────────────────────────────────────── -# PROJECT RULES -# ───────────────────────────────────────────────────────────────────────────── - -# Options for method filtering: -# *.api.http*#* -# *.config*#* -# za.co.absa.statusboard.Main*#* -# za.co.absa.statusboard.repository.DynamoDb*#* -# za.co.absa.statusboard.utils.DynamoDb*#* diff --git a/project/FilteredJacocoAgentPlugin.scala b/project/FilteredJacocoAgentPlugin.scala deleted file mode 100644 index 1ce69fb..0000000 --- a/project/FilteredJacocoAgentPlugin.scala +++ /dev/null @@ -1,373 +0,0 @@ -// JacocoBaseKeysPlugin.scala | last modified in v1.0.0 + local changes - -import JacocoBaseKeysPlugin.autoImport.* -import sbt.* -import sbt.Keys.* - -/** - * JacocoAgentPlugin (no aggregation/merge) - * --------------------------------------- - * - Attaches JaCoCo agent to forked JVMs per module (Test + optional IntegrationTest) - * - Writes per-module .exec files (no merging) - * - Generates per-module reports - * - Provides root helpers: jacocoCleanAll / jacocoReportAll that just iterate modules (no merge) - */ -object FilteredJacocoAgentPlugin extends AutoPlugin { - object autoImport { - val jacocoVersion = settingKey[String]("JaCoCo version") - val jacocoExecFile = settingKey[File]("Per-module JaCoCo .exec file (Test)") - val jacocoItExecFile = settingKey[File]("Per-module JaCoCo .exec file (IntegrationTest)") - val jacocoReportDir = settingKey[File]("Per-module report directory") - val jacocoIncludes = settingKey[Seq[String]]("Include patterns (JaCoCo syntax)") - val jacocoExcludes = settingKey[Seq[String]]("Exclude patterns (JaCoCo syntax)") - val jacocoAppend = settingKey[Boolean]("Append to existing .exec instead of overwrite (default: false)") - val jacocoFailOnMissingExec = - settingKey[Boolean]("Fail jacocoReport if .exec is missing (default: false – warn & skip)") - - val jacocoReportName = settingKey[String]("Title used for JaCoCo HTML report") - - // Root-only helpers (NO MERGE): just run per-module tasks across aggregated projects - val jacocoCleanAll = taskKey[Unit]("Run jacocoClean in all aggregated modules (no merge)") - val jacocoReportAll = taskKey[Unit]("Run jacocoReport in all aggregated modules (no merge)") - - val jacocoSetUserDirToBuildRoot = settingKey[Boolean]("Mimic non-forked runs by setting -Duser.dir to the build root for forked tests") - - val jmfCoreVersion = settingKey[String]("JMF core library version") - val Jmf = config("jmf").hide - val jmfRewrite = taskKey[File]("Rewrite compiled classes using JMF tool; returns output dir") - val jmfOutDir = settingKey[File]("JMF output base dir") - val jmfRulesFile = settingKey[File]("JMF rules file") - val jmfCliMain = settingKey[String]("Main class of the JMF CLI") - val jmfDryRun = settingKey[Boolean]("Dry-run rewriter") - val jmfEnabled = settingKey[Boolean]("Enable JMF rewriting") - val jmfPrepareForTests = taskKey[Unit]("Run JMF rewrite when enabled (no self-ref to test)") - } - import autoImport.* - - override def requires = JacocoBaseKeysPlugin - override def trigger = noTrigger - - // ---- helper: all aggregated descendants (BFS), excluding the root itself - private def aggregatedDescendants(e: Extracted, root: ProjectRef): Vector[ProjectRef] = { - val s = e.structure - val seen = scala.collection.mutable.LinkedHashSet[ProjectRef](root) - val queue = scala.collection.mutable.Queue[ProjectRef](root) - while (queue.nonEmpty) { - val ref = queue.dequeue() - val kids = Project.getProject(ref, s).toList.flatMap(_.aggregate) - kids.foreach { k => if (!seen(k)) { seen += k; queue.enqueue(k) } } - } - seen.toVector.tail // drop root - } - - // ---- helper: only those that set jacocoPluginEnabled := true - private def enabledUnder(state: State): Vector[ProjectRef] = { - val e = Project.extract(state) - val here = e.currentRef - val all = aggregatedDescendants(e, here) // children only (no root) - all.filter { ref => - e.getOpt((ref / jacocoPluginEnabled): SettingKey[Boolean]).getOrElse(false) - } - } - - lazy val Jmf = config("jmf").extend(Compile) - - // ---- commands - private lazy val jacocoCleanAllCmd = Command.command("jacocoCleanAll") { state => - val targets = enabledUnder(state) - if (targets.isEmpty) { println("[jacoco] nothing to clean (no enabled modules under this aggregate)."); state } - else targets.foldLeft(state) { (st, ref) => Command.process(s"${ref.project}/jacocoClean", st) } - } - - private lazy val jacocoReportAllCmd = Command.command("jacocoReportAll") { state => - val e = Project.extract(state) - val current = e.currentRef - // your existing helper (enabled projects under current aggregate) - val under = enabledUnder(state) - - // Also include current project if enabled - val selfEnabled = - e.getOpt(current / jacocoPluginEnabled).getOrElse(false) - - val targets = (if (selfEnabled) current +: under else under).distinct - - if (targets.isEmpty) { - println("[jacoco] nothing to report (no enabled modules here)."); - state - } else { - targets.foldLeft(state) { (st, ref) => - Command.process(s"${ref.project}/jacocoReport", st) - } - } - } - - // ---- global defaults so keys exist everywhere (safe no-ops on projects without the plugin) - override def buildSettings: Seq[Def.Setting[_]] = Seq( - jacocoPluginEnabled := false, // overridden to true in projects that enable the plugin - // register commands + a convenient alias like sbt-jacoco had - commands ++= Seq(jacocoCleanAllCmd, jacocoReportAllCmd) - ) - - private def findOnCp(cp: Seq[Attributed[File]])(p: File => Boolean): Option[File] = - cp.map(_.data).find(p) - - private def agentJar(cp: Seq[Attributed[File]]): File = { - val files = cp.map(_.data) - files.find(f => f.getName.startsWith("org.jacoco.agent-") && f.getName.contains("-runtime")) - .orElse(files.find(f => f.getName.contains("jacoco") && f.getName.contains("agent") && f.getName.contains("runtime"))) - .orElse(files.find(f => f.getName.startsWith("org.jacoco.agent-") && f.getName.endsWith(".jar"))) // last resort - .getOrElse(sys.error("JaCoCo runtime agent JAR not found on Test / dependencyClasspath")) - } - - private def cliJar(cp: Seq[Attributed[File]]): File = { - val files = cp.map(_.data) - files.find(f => f.getName.startsWith("org.jacoco.cli-") && f.getName.contains("nodeps")) - .orElse(files.find(_.getName.startsWith("org.jacoco.cli-"))) // fallback, but we won't use it - .getOrElse(sys.error("org.jacoco.cli (nodeps) JAR not found on Test / dependencyClasspath")) - } - - private val defaultIncludes = Seq("**") - private val defaultExcludes = Seq("scala.*", "java.*", "sun.*", "jdk.*") - - override def projectSettings: Seq[Setting[_]] = Seq( - jacocoPluginEnabled := false, - - // ---- coordinates - jacocoVersion := "0.8.12", - jmfCoreVersion := "1.0.0", - libraryDependencies ++= Seq( - // pull the agent with the runtime classifier (this is the actual -javaagent jar) - ("org.jacoco" % "org.jacoco.agent" % jacocoVersion.value % Test).classifier("runtime"), - ("org.jacoco" % "org.jacoco.cli" % jacocoVersion.value % Test).classifier("nodeps"), - "io.github.moranaapps" %% "jacoco-method-filter-core" % jmfCoreVersion.value % Jmf.name, - ), - jacocoSetUserDirToBuildRoot := true, - - // ---- defaults - jacocoExecFile := target.value / "jacoco" / "jacoco.exec", - jacocoReportDir := target.value / "jacoco" / "report", - jacocoIncludes := defaultIncludes, - jacocoExcludes := defaultExcludes, - jacocoAppend := false, - jacocoFailOnMissingExec := false, - - jacocoReportName := { - val moduleId = thisProject.value.id // or: thisProjectRef.value.project - s"Report: $moduleId - scala:${scalaVersion.value}" - }, - - // --- JMF tool wiring - ivyConfigurations += Jmf, - - jmfOutDir := target.value / "jmf", - jmfRulesFile:= (ThisBuild / baseDirectory).value / "jmf-rules.txt", - jmfCliMain := "io.moranaapps.jacocomethodfilter.CoverageRewriter", - jmfDryRun := false, - jmfEnabled := true, - - // the rewrite task (your code, lightly cleaned) - jmfRewrite := { - // --- hoist all .value lookups BEFORE conditionals --- - // ensure classes exist (safe to always do; test would compile anyway) - val _ = (Compile / compile).value - - val rules = jmfRulesFile.value - val upd = (Jmf / update).value // hoisted - val log = streams.value.log - val outRoot = jmfOutDir.value - val mainCls = jmfCliMain.value - val dryRun = jmfDryRun.value - val workDir = baseDirectory.value - val classesIn = (Compile / classDirectory).value - val rulesFile = jmfRulesFile.value - val enabled = jacocoPluginEnabled.value - - // Compile classpath (scala-stdlib, scopt, your module classes, etc.) - val compileCp: Seq[File] = Attributed.data((Compile / fullClasspath).value) - - // Jmf-resolved jars (your jacoco-method-filter-core, etc.) - val jmfJars: Seq[File] = (Jmf / update).value.matching(artifactFilter(`type` = "jar")).distinct - - // Final runtime CP - val cp: Seq[File] = (compileCp ++ jmfJars :+ (Compile / classDirectory).value).distinct - val cpStr = cp.distinct.map(_.getAbsolutePath).mkString(java.io.File.pathSeparator) - - val javaBin = { - val h = sys.props.get("java.home").getOrElse("") - if (h.nonEmpty) new java.io.File(new java.io.File(h, "bin"), "java").getAbsolutePath else "java" - } - // ---------------------------------------------------- - - if (!enabled) classesIn - else if (!classesIn.exists) { - log.warn(s"[jmf] compiled classes dir not found, skipping: ${classesIn.getAbsolutePath}"); classesIn - } else { - val hasClasses = (classesIn ** sbt.GlobFilter("*.class")).get.nonEmpty - if (!hasClasses) { log.warn(s"[jmf] no .class files under ${classesIn.getAbsolutePath}; skipping."); classesIn } - else if (!rules.exists) { log.warn(s"[jmf] rules file missing: ${rules.getAbsolutePath}; skipping."); classesIn } - else { - val outDir = jmfOutDir.value / "classes-filtered" - IO.delete(outDir); IO.createDirectory(outDir) - - log.info("[jmf] runtime CP:\n" + cp.map(f => s" - ${f.getAbsolutePath}").mkString("\n")) - - val args = Seq( - javaBin, "-cp", cpStr, jmfCliMain.value, - "--in", classesIn.getAbsolutePath, - "--out", outDir.getAbsolutePath, - "--rules", rules.getAbsolutePath - ) ++ (if (jmfDryRun.value) Seq("--dry-run") else Seq()) - - log.info(s"[jmf] rewrite: ${args.mkString(" ")}") - val code = scala.sys.process.Process(args, workDir).! - if (code != 0) sys.error(s"[jmf] rewriter failed ($code)") - outDir - } - } - }, - - // 1) preparatory task (already defined earlier) - jmfPrepareForTests := Def.taskDyn { - if (jmfEnabled.value) Def.task { jmfRewrite.value; () } - else Def.task { () } - }.value, - - Test / fullClasspath := Def.taskDyn { - // Gather the usual ingredients - val testOut = (Test / classDirectory).value // test classes dir - val mainOut = (Compile / classDirectory).value // original main classes dir - val deps = (Test / internalDependencyClasspath).value - val ext = (Test / externalDependencyClasspath).value - val unmanaged = (Test / unmanagedClasspath).value - val scalaJars = (Test / scalaInstance).value.allJars.map(Attributed.blank(_)).toVector - val resources = (Test / resourceDirectories).value.map(Attributed.blank) - - def build(rewrittenOpt: Option[File]) = Def.task { - val rewrittenDifferent = rewrittenOpt.filter(_ != mainOut) - val prefix = rewrittenDifferent.toVector.map(Attributed.blank) :+ Attributed.blank(testOut) - val rest = (deps ++ ext ++ scalaJars ++ resources ++ unmanaged) - .filterNot(a => a.data == mainOut || a.data == testOut || rewrittenDifferent.exists(_ == a.data)) - (prefix ++ rest :+ Attributed.blank(mainOut)) - } - - if (jacocoPluginEnabled.value) build(Some(jmfRewrite.value)) - else build(None) - }.value, - - // ---- fork so -javaagent is applied - Test / fork := true, - - // Attach agent for Test - Test / forkOptions := { - val fo0 = (Test / forkOptions).value - val rootDir = (LocalRootProject / baseDirectory).value - val baseFO = fo0.withWorkingDirectory(rootDir) // keep tests running from repo root - - // pre-compute values (avoids sbt linter warning about .value inside if) - val cp = (Test / dependencyClasspath).value - val agent = agentJar(cp) - val dest = jacocoExecFile.value.getAbsolutePath - val inc = jacocoIncludes.value.mkString(":") - val exc = jacocoExcludes.value.mkString(":") - val append = if (jacocoAppend.value) "true" else "false" - val agentOpt = - s"-javaagent:${agent.getAbsolutePath}=destfile=$dest,append=$append,output=file,includes=$inc,excludes=$exc,inclbootstrapclasses=false,jmx=false" - - val log = streams.value.log - log.info(s"[jacoco] setting fork working dir to: $rootDir") - - if (jacocoPluginEnabled.value) { - log.info(s"[jacoco] agent jar: ${agent.getName} (enabled)") - baseFO.withRunJVMOptions(baseFO.runJVMOptions :+ agentOpt) - } else { - log.info("[jacoco] disabled (jacocoPluginEnabled=false); NOT adding -javaagent") - baseFO - } - }, - - // Print one sanity line per test fork - Test / testOptions += Tests.Setup { () => - val status = - try { - val rt = Class.forName("org.jacoco.agent.rt.RT") - val m = rt.getMethod("getAgent") - m.invoke(null) // throws if not attached - "attached" - } catch { - case _: ClassNotFoundException => "rt-jar-not-on-classpath" - case _: Throwable => "present-but-not-attached" - } - println(s"[jacoco] agent status: $status; user.dir=" + System.getProperty("user.dir")) - }, - - - // ---- per-module clean - jacocoClean := { - val log = streams.value.log - val outDir = target.value / "jacoco" - IO.delete(outDir) - IO.createDirectory(outDir) - IO.delete(jmfOutDir.value) - - // remove sbt-jacoco leftovers if they ever existed - val instrDir = (Test / crossTarget).value / "jacoco" / "instrumented-classes" - if (instrDir.exists) { - log.info(s"[jacoco] removing sbt-jacoco leftovers: ${instrDir.getAbsolutePath}") - IO.delete(instrDir) - } - log.info(s"[jacoco] cleaned: ${outDir.getAbsolutePath}") - }, - - // ---- per-module report (only this module, no merge) - jacocoReport := { - val log = streams.value.log - val execFile = jacocoExecFile.value - val reportDir = jacocoReportDir.value - IO.createDirectory(reportDir) - - // PRE-compute (avoid linter warnings) - val moduleName = name.value - val baseDir = baseDirectory.value - val failOnMiss = jacocoFailOnMissingExec.value - val cp = (Test / dependencyClasspath).value - val cli = cliJar(cp) - val title = jacocoReportName.value - - // Class dirs (filter to existing) - val classesIn = (Compile / classDirectory).value - val filteredDir = jmfOutDir.value / "classes-filtered" - val mainClasses = - if (jacocoPluginEnabled.value && filteredDir.exists) filteredDir - else classesIn - - val classDirs = Seq(mainClasses).filter(_.exists) - - // Source dirs: unmanaged + managed (filter to existing) - val unmanagedSrc = (Compile / unmanagedSourceDirectories).value - val managedSrc = (Compile / managedSourceDirectories).value - val srcDirs = (unmanagedSrc ++ managedSrc).filter(_.exists) - - if (!execFile.exists) { - val msg = s"[jacoco] .exec not found for $moduleName: $execFile . Run tests first." - if (failOnMiss) sys.error(msg) else { log.warn(msg); reportDir } - } else if (classDirs.isEmpty) { - log.warn(s"[jacoco] no class dirs for $moduleName; skipping report.") - reportDir - } else { - // repeat flags per path - val args = Seq("java","-jar", cli.getAbsolutePath, "report", execFile.getAbsolutePath) ++ - classDirs.flatMap(d => Seq("--classfiles", d.getAbsolutePath)) ++ - srcDirs .flatMap(d => Seq("--sourcefiles", d.getAbsolutePath)) ++ - Seq("--name", title, - "--html", reportDir.getAbsolutePath, - "--xml", (reportDir / "jacoco.xml").getAbsolutePath, - "--csv", (reportDir / "jacoco.csv").getAbsolutePath) - - val exit = scala.sys.process.Process(args, baseDir).! - if (exit != 0) sys.error("JaCoCo report generation failed") - log.info(s"[jacoco] per-module HTML: ${reportDir / "index.html"}") - reportDir - } - } - ) -} diff --git a/project/JacocoBaseKeysPlugin.scala b/project/JacocoBaseKeysPlugin.scala deleted file mode 100644 index 0032121..0000000 --- a/project/JacocoBaseKeysPlugin.scala +++ /dev/null @@ -1,28 +0,0 @@ -// JacocoBaseKeysPlugin.scala | last modified in v1.0.0 - -import sbt._ -import sbt.Keys._ - -object JacocoBaseKeysPlugin extends AutoPlugin { - object autoImport { - val jacocoPluginEnabled = settingKey[Boolean]("Marker for JaCoCo plugin participation") - val jacocoClean = taskKey[Unit]("Clean JaCoCo outputs") - val jacocoReport = taskKey[File]("Generate per-module JaCoCo report") - } - import autoImport._ - - // apply to every project (project scope → target.value etc. are valid) - override def trigger = allRequirements - override def requires = plugins.JvmPlugin - - override def projectSettings: Seq[Def.Setting[_]] = Seq( - jacocoPluginEnabled := false, // default: not participating - jacocoClean := { streams.value.log.debug("[jacoco] not enabled here; clean no-op.") }, - jacocoReport := { - val d = target.value / "jacoco" / "report" // safe placeholder dir - IO.createDirectory(d) - streams.value.log.debug("[jacoco] not enabled here; report no-op.") - d - } - ) -} From 5d634dd52760979deffc257745e68d33b5517cba Mon Sep 17 00:00:00 2001 From: "Tobias.Mikula" Date: Tue, 9 Jun 2026 14:43:44 +0200 Subject: [PATCH 2/3] Bump of the JaCoCo version --- .github/copilot-instructions.md | 26 +++++- .github/workflows/ci-check-jacoco.yml | 97 ++++++++++++++++++++ build.sbt | 3 + jmf-rules.txt | 126 ++++++++++++++++++++++++++ project/build.properties | 2 +- project/plugins.sbt | 1 + 6 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/ci-check-jacoco.yml create mode 100644 jmf-rules.txt diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 1113756..ff8f8e7 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -18,6 +18,7 @@ StatusBoard is a real-time monitoring platform for tracking service health statu - **Database**: AWS DynamoDB for persistence - **Testing**: ZIO Test framework - **Code Coverage**: Jacoco +- Compiler warnings treated as errors where configured; coverage ≥ 80% via JMF-enabled JaCoCo (excluding methods listed in `jmf-rules.txt`). ### Frontend (TypeScript) - **Framework**: Angular 19 @@ -166,6 +167,29 @@ The checker system uses a polymorphic design: - E2E tests: `npm run e2e` (Cypress via `cypress.json`) - Test coverage: `npm run test:coverage` +## Coverage Filtering (JMF) + +### When a unit test adds value — write one +- The method has any logic of its own. + +### When to add to `jmf-rules.txt` instead of writing a unit test +- The body is a single call with no own logic: it forwards to another overload, calls its non-deprecated replacement, returns a field, or wraps a constructor with no transformation. +- **Litmus test**: "Does this method have any logic of its own?" — No → add a JMF rule instead of a test. + +### Global rule collision check (CRITICAL) +- When adding any new method, check whether its name matches a pattern in the `# GLOBAL RULES` section of `jmf-rules.txt`. +- If a method name matches a global rule AND the method contains domain logic: immediately add an INCLUDE rescue rule (`+FQCN#method(*)`) in the `# INCLUDE RULES` section of `jmf-rules.txt`. +- High-risk method names (most common collisions): `apply()`, `toString()`, `equals()`, `copy()`, `name()`, `groups()`, `optionalAttributes()`. See the `# GLOBAL RULES` section of `jmf-rules.txt` for the full list. +- Rationale: broad global rules are designed for compiler-generated boilerplate and can silently suppress coverage for domain methods. INCLUDE rules rescue specific methods from broad exclusions. +- Example: if adding `def apply(id: String): Record`, add `+*Record$#apply(*) id:keep-record-factory` to the `# INCLUDE RULES` section to rescue it from the `*$*#apply(*)` global rule. + +### JMF drift check (review rule) +- When modifying a method that already appears in `jmf-rules.txt`, verify its body still qualifies for exclusion. +- If own logic has been added since the rule was created, remove the JMF rule and write a unit test instead. + +### Cannot add JMF rules for +- Methods with branching logic, error handling, or non-trivial transformations — write a unit test instead. + ## Configuration - Main config: `config. conf` (HOCON format) - Environment-specific configurations for different deployments @@ -279,4 +303,4 @@ npm run sync-version # Sync VERSION file to package.json ## Additional Resources - [Wiki - REST API](https://github.com/AbsaOSS/StatusBoard/wiki/REST-API) - [Wiki - Architecture](https://github.com/AbsaOSS/StatusBoard/wiki/Architecture) -- [Wiki - Supported Checkers](https://github.com/AbsaOSS/StatusBoard/wiki/Supported-checkers) \ No newline at end of file +- [Wiki - Supported Checkers](https://github.com/AbsaOSS/StatusBoard/wiki/Supported-checkers) diff --git a/.github/workflows/ci-check-jacoco.yml b/.github/workflows/ci-check-jacoco.yml new file mode 100644 index 0000000..f632b97 --- /dev/null +++ b/.github/workflows/ci-check-jacoco.yml @@ -0,0 +1,97 @@ +name: CI Check JaCoCo code-coverage + +on: + pull_request: + branches: [ master ] + +env: + scalaLong: 2.13.11 + scalaShort: "2.13" + REPORT_GROUPS: | + - name: statusboard + paths: + - '**/target/**/jacoco/report/jacoco.xml' + thresholds: '77*80*60' + +jobs: + detect: + name: Detect Changed Files + runs-on: ubuntu-latest + timeout-minutes: 2 + outputs: + scala_changed: ${{ steps.filter.outputs.scala }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + with: + persist-credentials: false + fetch-depth: 0 + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d + id: filter + with: + token: "" + filters: | + scala: + - '**/*.scala' + - '!project/**' + + build-test-and-measure: + name: Build, Test and Measure + needs: detect + if: needs.detect.outputs.scala_changed == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + services: + dynamodb-local: + image: "amazon/dynamodb-local:latest" + ports: + - "8000:8000" + + steps: + - name: Checkout code + id: code-checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + with: + persist-credentials: false + + - name: Setup JVM and SBT + id: jvm-setup + uses: coursier/setup-action@fd1707a76b027efdfb66ca79318b4d29b72e5a02 + with: + jvm: corretto:21.0.2.13.1 + apps: sbt + + - name: Build and run tests with test coverage + id: jacoco-run + continue-on-error: true + run: sbt jacoco + env: + AWS_REGION: "ignored" + AWS_ACCESS_KEY_ID: "ignored" + AWS_SECRET_ACCESS_KEY: "ignored" + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: '3.14' + + - name: Check coverage thresholds and add reports in PR comments + id: jacoco + uses: MoranaApps/jacoco-report@69351d88d18f7697c416e1bc2020ed05606d8120 + with: + token: '${{ secrets.GITHUB_TOKEN }}' + global-thresholds: '77*80' + report-thresholds-default: '0*0*0' + skip-unchanged: 'true' + evaluate-unchanged: 'false' + report-groups: ${{ env.REPORT_GROUPS }} + + noop: + name: No Operation + needs: detect + if: needs.detect.outputs.scala_changed != 'true' + runs-on: ubuntu-latest + steps: + - run: echo "No changes in src/**/*.scala — passing." diff --git a/build.sbt b/build.sbt index c6ed8b8..cd74b8f 100644 --- a/build.sbt +++ b/build.sbt @@ -55,6 +55,9 @@ lazy val root = (project in file(".")) Test / parallelExecution := false, (assembly / test) := {}, publish := {}, + jmfReportFile := Some(target.value / "jmf-report.json"), + jmfReportFormat := "json", ) .enablePlugins(AutomateHeaderPlugin) .enablePlugins(AssemblyPlugin) + .enablePlugins(JacocoFilterPlugin) diff --git a/jmf-rules.txt b/jmf-rules.txt new file mode 100644 index 0000000..e2a67f2 --- /dev/null +++ b/jmf-rules.txt @@ -0,0 +1,126 @@ +# jacoco-method-filter — Rules Template (Scala / sbt) +# [jmf:1.0.0] +# +# Syntax reference, pitfalls, examples, and workflows: docs/rules-reference.md +# +# ───────────────────────────────────────────────────────────────────────────── +# HOW TO USE +# ───────────────────────────────────────────────────────────────────────────── +# +# 1) Review the GLOBAL RULES below — they cover compiler-generated boilerplate. +# 2) Add project-specific patterns in the PROJECT RULES section. +# 3) Keep rules narrow; add id: labels so logs are readable. +# Every rule must have an id: