AbsaOSS / AbsaOSS/pramen

Adopt Java Method Filtration JaCoCo Extension

Open
#740 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Scala
Stars
31
Forks
4
Avg merge
1d 10m
Merged PRs (30d)
4

Description

## Background

We use JaCoCo for instruction-based code coverage measurement with Scala code.
Scala introduces a significant amount of boilerplate which makes final reports inaccurate and inflates method counts with code that has no meaningful value to test.

## Feature

1. **Main:** Adopt the [JaCoCo Method Filter (JMF)](https://github.com/MoranaApps/jacoco-method-filter/blob/master/README.md) plugin to enable filtering on the method level, cleanly excluding Scala-generated boilerplate from coverage reports.
2. **Optional:** Set Up the JaCoCo GitHub Actions Workflow

## Business Value

Code coverage reports will reflect accurate, meaningful numbers that the team can trust and act on. Team can better focus on biggest coverage gaps - per module or inside each module.

## Reference Example of JMF

A pilot implementation has been completed in the [atum-service](https://github.com/AbsaOSS/atum-service) and [Unify](https://github.com/absa-group/AUL) projects. The following files from that project can be used as a reference:
- [`build.sbt`](https://github.com/AbsaOSS/atum-service/blob/master/build.sbt) — sbt plugin setup with `JacocoFilterPlugin` enabled
- [`jmf-rules.txt`](https://github.com/AbsaOSS/atum-service/blob/master/jmf-rules.txt) — example global and project-specific filter rules
- [`.github/workflows/jacoco_report.yml`](https://github.com/AbsaOSS/atum-service/blob/master/.github/workflows/jacoco_report.yml) — workflow running tests with JaCoCo and posting PR coverage comments

## Proposed Solution of JMF

Follow the steps below, skipping any that do not apply to your project.

The full code coverage solution consists of **two** parts:
1. **Project-level JaCoCo setup** — enables local test runs and report generation
2. **GitHub Actions workflow** — runs tests with JaCoCo and posts a PR comment with coverage stats

---

### 1. Remove Existing JaCoCo Setup (if applicable)
_Skip this section if JaCoCo is not already configured in your project._

1. Back up any JaCoCo-related settings for later reference (report titles, formats, file exclusions, thresholds, etc.).
2. Remove the existing JaCoCo configuration from your project.
3. Verify the removal by running `sbt test` (or the Maven equivalent) and confirming it succeeds without JaCoCo.

**Why:** switch from old to new solution is often failing because of sbt or mvn library dependencies. The clean before update is confirmed easy to adopt solution.

---

### 2. Add JaCoCo With Method Filtering

**Hint:** Be ready for customization on your project. Both setup guides are generic.

1. Follow the JMF plugin setup guide for your build tool:
**sbt**: [sbt plugin setup docs](https://github.com/MoranaApps/jacoco-method-filter/blob/master/sbt-plugin/README.md)
- Add to `project/plugins.sbt`:
```scala
addSbtPlugin("io.github.moranaapps" % "jacoco-method-filter-sbt" % "2.2.0")
```
- Enable the plugin in `build.sbt`:
```scala
lazy val myModule = (project in file("my-module"))
.enablePlugins(JacocoFilterPlugin)
```
- Optionally add convenience command aliases:
```scala
addCommandAlias("jacoco", "; jacocoOn; clean; test; jacocoReportAll; jacocoOff")
addCommandAlias("jacocoOn", "; set every jacocoPluginEnabled := true")
addCommandAlias("jacocoOff", "; set every jacocoPluginEnabled := false")
```
**Maven**: [Maven plugin setup docs](https://github.com/MoranaApps/jacoco-method-filter/blob/master/maven-plugin/README.md)
- Add the plugin inside a `code-coverage` profile in `pom.xml` (see the full example in the docs).
- Run: `mvn jacoco-method-filter:init-rules` to generate the default rules file, then `mvn clean verify -Pcode-coverage`.

2. Generate the initial rules file:
- **sbt**: `sbt jmfInitRules` — creates `jmf-rules.txt` in the project root with sensible Scala defaults.
- **Maven**: `mvn jacoco-method-filter:init-rules`
- You can also copy and adapt the template from [`jmf-rules.template.txt`](https://github.com/MoranaApps/jacoco-method-filter/blob/master/jmf-rules.template.txt) or use the [balta example](https://github.com/AbsaOSS/balta/blob/master/jmf-rules.txt) as a reference.

3. _(Optional)_ Enable saving the filter report per module by adding to `build.sbt`:
```scala
jmfReportFile := Some(target.value / "jmf-report.json")
jmfReportFormat := "json" // or "txt" (default) / "csv"
```

4. Run JaCoCo with filtering:
- **sbt**: `sbt jacoco`
- **Maven**: `mvn clean verify -Pcode-coverage`
- You should see JaCoCo reports generated and, optionally, a filter rules report.

5. _(Optional)_ Re-apply your backed-up settings from Step 1 (titles, formats, thresholds, exclusions).

6. Review overridden methods that were filtered by global rules — use **include rules** to rescue them:
- Global rules are designed to filter only Scala boilerplate (e.g., `copy`, `equals`, `hashCode`, `apply`, etc.).
- If project-defined overrides of these methods were also filtered, that is unintended and must be corrected with include rules (prefixed with `+`).
- Run `sbt jmfVerify` (or `mvn compile jacoco-method-filter:verify`) to see a dry-run report of excluded and rescued methods without modifying anything.

---

### 3. Define Project-Specific Filter Rules

**Goal:** identify and filter out all methods where unit test coverage either does not make sense or is explicitly excluded by team rules or decisions (e.g., DAOs, trivial delegates, implicit conversions).

1. _(Optional)_ Temporarily comment out any unmatched global rules shown in the generated report. This reduces noise and makes it easier to focus on writing project-specific rules.

2. Identify all methods that are valid candidates for exclusion. Common categories include:
- Deprecated single-call delegates that simply forward to a replacement method
- One-liner factory wrappers with no logic
- Trivial implicit conversions
- Trivial field accessors generated for `val` members
- DAO/repository methods excluded by team convention

3. Add the identified methods to the `# PROJECT RULES` section of `jmf-rules.txt`. Use the balta [jmf-rules.txt](https://github.com/AbsaOSS/balta/blob/master/jmf-rules.txt) as a reference for rule syntax and documentation conventions.

4. Re-run code coverage (`sbt jacoco` / `mvn clean verify -Pcode-coverage`) after each batch of rule additions. Observe the coverage percentage increasing.
- Check the **unmatched rules** section in the report to detect rules that did not match any method — these may have a typo or may be redundant.

---

### 4. Set Up the JaCoCo GitHub Actions Workflow (if applicable)
_Skip this section if you are not using GitHub Actions for coverage reporting, or if you do not want to use the [MoranaApps/jacoco-report](https://github.com/MoranaApps/jacoco-report) action._

Even though this part is independent of the project-level setup, migration to the [MoranaApps/jacoco-report](https://github.com/MoranaApps/jacoco-report) GitHub Action is strongly recommended. It automates publishing JaCoCo coverage reports as PR comments with threshold enforcement.

Use the Unify [jacoco_report.yml](https://github.com/absa-group/AUL/blob/master/.github/workflows/ci-check-jacoco.yml) as a reference workflow. This example show the advance usage of this gh action - it uses `report-groups` instead of global `paths`.
- The proposed solution can be still in unmerged [PR](https://github.com/absa-group/AUL/pull/4947).

```yaml
env:
# hint: "group thresholds" are in format: 'overall*changed-files-average*per-changed-file'
REPORT_GROUPS: |
- name: bootstrap
paths:
- bootstrap/**/target/jacoco.xml
thresholds: '9*80*60'
- name: data-access-spring-web
paths:
- contexts/data-access/spring-web/**/target/jacoco.xml
thresholds: '0*80*60'
- .... _shortened_: there is more groups

- name: Add JaCoCo Report in PR comments
uses: MoranaApps/jacoco-report@f9dc5ba989ff2b987b4c2db99217b982c466cca8
with:
token: '${{ secrets.GITHUB_TOKEN }}'
global-thresholds: '43*80'
report-thresholds-default: '0*0*0'
skip-unchanged: 'true'
evaluate-unchanged: 'false'
report-groups: ${{ env.REPORT_GROUPS }}
```

For full configuration options and examples, see the [jacoco-report README](https://github.com/MoranaApps/jacoco-report/blob/master/README.md).

---

### 5. Enrich a `.github/copilot-instructions.md` File (Recommended)

_This step is optional but strongly recommended._ A Copilot instructions file teaches GitHub Copilot the project's coverage conventions so that AI-assisted code suggestions are consistent with your JMF setup from the start — preventing both missing filter rules and incorrectly filtered methods.

You can inspire from the balta [.github/copilot-instructions.md](https://github.com/AbsaOSS/balta/blob/master/.github/copilot-instructions.md) at **Coverage filtering (JMF)** section and a **Tooling** note. Suggested content:

````markdown
Tooling
- Compiler warnings treated as errors where configured; coverage ≥ 80% via JMF-enabled JaCoCo (excluding methods listed in `jmf-rules.txt`).

Coverage filtering (JMF)
- Reference: [jacoco-method-filter](https://github.com/MoranaApps/jacoco-method-filter), rules file: `jmf-rules.txt`.

- When a unit test adds value — write one:
- The method has any logic of its own: branching (`if`/`match`), exception handling, non-trivial transformation, config/resource read, or reflection.

- 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.

- Can NOT add JMF rules for methods with branching logic, error handling, or non-trivial transformations.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.