RATIS-1985. Calculate code coverage (#1000)

diff --git a/.github/workflows/post-commit.yml b/.github/workflows/post-commit.yml
index 677171e..9978caa 100644
--- a/.github/workflows/post-commit.yml
+++ b/.github/workflows/post-commit.yml
@@ -16,6 +16,8 @@
 on:
   - push
   - pull_request
+env:
+  WITH_COVERAGE: true
 jobs:
   build:
     runs-on: ubuntu-20.04
@@ -38,6 +40,14 @@
           java-version: 8
       - name: Run a full build
         run: ./dev-support/checks/build.sh -Prelease assembly:single
+      - name: Store binaries for tests
+        uses: actions/upload-artifact@v3
+        with:
+          name: ratis-bin
+          path: |
+            ratis-assembly/target/apache-ratis-*.tar.gz
+            !ratis-assembly/target/apache-ratis-*-src.tar.gz
+          retention-days: 1
       - name: Store source tarball for compilation
         uses: actions/upload-artifact@v3
         with:
@@ -140,6 +150,11 @@
             key: maven-repo-${{ hashFiles('**/pom.xml') }}
             restore-keys: |
               maven-repo-
+        - name: Setup java
+          uses: actions/setup-java@v3
+          with:
+            distribution: 'temurin'
+            java-version: 8
         - name: Run tests
           run: ./dev-support/checks/unit.sh -P${{ matrix.profile }}-tests
         - name: Summary of failures
@@ -202,8 +217,10 @@
           with:
             name: findbugs
             path: target/findbugs
-  sonar:
-    name: sonar
+  coverage:
+    needs:
+      - build
+      - unit
     runs-on: ubuntu-20.04
     if: (github.repository == 'apache/ratis' || github.repository == 'apache/incubator-ratis') && github.event_name != 'pull_request'
     steps:
@@ -225,8 +242,24 @@
           with:
             distribution: 'temurin'
             java-version: 17
+        - name: Download artifacts
+          uses: actions/download-artifact@v3
+          with:
+            path: target/artifacts
+        - name: Untar binaries
+          run: |
+            mkdir -p ratis-assembly/target
+            tar xzvf target/artifacts/ratis-bin/apache-ratis*.tar.gz -C ratis-assembly/target
+        - name: Calculate combined coverage
+          run: ./dev-support/checks/coverage.sh
         - name: Upload coverage to Sonar
           run: ./dev-support/checks/sonar.sh
           env:
             SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}
             GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        - name: Archive build results
+          uses: actions/upload-artifact@v3
+          if: always()
+          with:
+            name: ${{ github.job }}
+            path: target/${{ github.job }}
diff --git a/dev-support/checks/build.sh b/dev-support/checks/build.sh
index 4a71ba6..6add1ae 100755
--- a/dev-support/checks/build.sh
+++ b/dev-support/checks/build.sh
@@ -18,6 +18,14 @@
 
 source "${DIR}/../find_maven.sh"
 
+: ${WITH_COVERAGE:="false"}
+
+MAVEN_OPTIONS='-V -B -Dmaven.javadoc.skip=true -DskipTests --no-transfer-progress'
+
+if [[ "${WITH_COVERAGE}" != "true" ]]; then
+  MAVEN_OPTIONS="${MAVEN_OPTIONS} -Djacoco.skip"
+fi
+
 export MAVEN_OPTS="-Xmx4096m"
-${MVN} -V -B -Dmaven.javadoc.skip=true -DskipTests --no-transfer-progress clean install "$@"
+${MVN} ${MAVEN_OPTIONS} clean install "$@"
 exit $?
diff --git a/dev-support/checks/coverage.sh b/dev-support/checks/coverage.sh
new file mode 100755
index 0000000..a2fab9b
--- /dev/null
+++ b/dev-support/checks/coverage.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This script merges combined jacoco files (output of unit.sh)
+# and generates a report in HTML and XML formats
+
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+cd "$DIR/../.." || exit 1
+
+source "${DIR}/../find_maven.sh"
+
+REPORT_DIR="$DIR/../../target/coverage"
+
+mkdir -p "$REPORT_DIR"
+
+JACOCO_VERSION=$(${MVN} help:evaluate -Dexpression=jacoco.version -q -DforceStdout)
+
+#Install jacoco cli
+${MVN} --non-recursive --no-transfer-progress \
+  org.apache.maven.plugins:maven-dependency-plugin:3.6.1:copy \
+  -Dartifact=org.jacoco:org.jacoco.cli:${JACOCO_VERSION}:jar:nodeps
+
+jacoco() {
+  java -jar target/dependency/org.jacoco.cli-${JACOCO_VERSION}-nodeps.jar "$@"
+}
+
+# merge all jacoco-combined.exec files
+jacoco merge $(find target -name jacoco-combined.exec) --destfile "$REPORT_DIR/jacoco-all.exec"
+
+rm -fr target/coverage-classes target/coverage-sources
+mkdir -p target/coverage-classes target/coverage-sources
+
+# unzip all classes from the build
+find ratis-assembly/target/apache-ratis* -name 'ratis-*.jar' \
+  | grep -v -E 'examples|proto|test|thirdparty' \
+  | xargs -n1 unzip -o -q -d target/coverage-classes
+
+# copy all sources
+for d in $(find ratis* -type d -name java); do
+  cp -r "$d"/org target/coverage-sources/
+done
+
+# generate the reports
+jacoco report "$REPORT_DIR/jacoco-all.exec" \
+  --sourcefiles target/coverage-sources \
+  --classfiles target/coverage-classes \
+  --html "$REPORT_DIR/all" \
+  --xml "$REPORT_DIR/all.xml"
diff --git a/dev-support/checks/findbugs.sh b/dev-support/checks/findbugs.sh
index b6e51db..17c669b 100755
--- a/dev-support/checks/findbugs.sh
+++ b/dev-support/checks/findbugs.sh
@@ -18,6 +18,8 @@
 
 source "${DIR}/../find_maven.sh"
 
+: ${WITH_COVERAGE:="false"}
+
 MAVEN_OPTIONS='-B -fae --no-transfer-progress'
 
 if ! type unionBugs >/dev/null 2>&1 || ! type convertXmlToText >/dev/null 2>&1; then
@@ -26,6 +28,10 @@
   exit $?
 fi
 
+if [[ "${WITH_COVERAGE}" != "true" ]]; then
+  MAVEN_OPTIONS="${MAVEN_OPTIONS} -Djacoco.skip"
+fi
+
 #shellcheck disable=SC2086
 ${MVN} ${MAVEN_OPTIONS} test-compile spotbugs:spotbugs
 rc=$?
diff --git a/dev-support/checks/unit.sh b/dev-support/checks/unit.sh
index 0d0c122..f7a4f30 100755
--- a/dev-support/checks/unit.sh
+++ b/dev-support/checks/unit.sh
@@ -23,6 +23,7 @@
 
 : ${FAIL_FAST:="false"}
 : ${ITERATIONS:="1"}
+: ${WITH_COVERAGE:="false"}
 
 declare -i ITERATIONS
 if [[ ${ITERATIONS} -le 0 ]]; then
@@ -33,7 +34,7 @@
 mkdir -p "$REPORT_DIR"
 
 export MAVEN_OPTS="-Xmx4096m"
-MAVEN_OPTIONS='-B --no-transfer-progress'
+MAVEN_OPTIONS='-V -B --no-transfer-progress'
 
 if [[ "${FAIL_FAST}" == "true" ]]; then
   MAVEN_OPTIONS="${MAVEN_OPTIONS} --fail-fast -Dsurefire.skipAfterFailureCount=1"
@@ -41,6 +42,10 @@
   MAVEN_OPTIONS="${MAVEN_OPTIONS} --fail-at-end"
 fi
 
+if [[ "${WITH_COVERAGE}" != "true" ]]; then
+  MAVEN_OPTIONS="${MAVEN_OPTIONS} -Djacoco.skip"
+fi
+
 rc=0
 for i in $(seq 1 ${ITERATIONS}); do
   if [[ ${ITERATIONS} -gt 1 ]]; then
@@ -77,4 +82,9 @@
   fi
 done
 
+if [[ "${WITH_COVERAGE}" == "true" ]]; then
+  # Archive combined jacoco records
+  mvn -B -N jacoco:merge -Djacoco.destFile="${REPORT_DIR}/jacoco-combined.exec"
+fi
+
 exit ${rc}
diff --git a/pom.xml b/pom.xml
index 8841644..6848807 100644
--- a/pom.xml
+++ b/pom.xml
@@ -222,6 +222,7 @@
 
     <slf4j.version>2.0.7</slf4j.version>
     <junit.jupiter.version>5.10.1</junit.jupiter.version>
+    <jacoco.version>0.8.11</jacoco.version>
   </properties>
 
   <dependencyManagement>
@@ -642,7 +643,8 @@
             <reuseForks>false</reuseForks>
             <trimStackTrace>false</trimStackTrace>
             <forkedProcessTimeoutInSeconds>600</forkedProcessTimeoutInSeconds>
-            <argLine>-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError</argLine>
+            <!-- @argLine is filled by jacoco maven plugin. @{} means late evaluation -->
+            <argLine>-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError @{argLine}</argLine>
             <systemPropertyVariables>
               <ratis.log.dir>${project.build.directory}/log</ratis.log.dir>
               <ratis.tmp.dir>${project.build.directory}/tmp</ratis.tmp.dir>
@@ -723,6 +725,12 @@
           <artifactId>maven-bundle-plugin</artifactId>
           <version>${maven-bundle-plugin.version}</version>
         </plugin>
+
+        <plugin>
+          <groupId>org.jacoco</groupId>
+          <artifactId>jacoco-maven-plugin</artifactId>
+          <version>${jacoco.version}</version>
+        </plugin>
       </plugins>
     </pluginManagement>
 
@@ -835,6 +843,32 @@
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <configuration>
+          <fileSets>
+            <fileSet>
+              <directory>${project.basedir}</directory>
+              <includes>
+                <include>**/jacoco.exec</include>
+              </includes>
+              <useDefaultExcludes>false</useDefaultExcludes>
+            </fileSet>
+          </fileSets>
+        </configuration>
+        <executions>
+          <execution>
+            <id>default-prepare-agent</id>
+            <goals>
+              <goal>prepare-agent</goal>
+            </goals>
+            <configuration>
+              <includes>org.apache.ratis.*</includes>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
 
diff --git a/ratis-common/src/main/java/org/apache/ratis/conf/ConfUtils.java b/ratis-common/src/main/java/org/apache/ratis/conf/ConfUtils.java
index a642ebb..c1fb926 100644
--- a/ratis-common/src/main/java/org/apache/ratis/conf/ConfUtils.java
+++ b/ratis-common/src/main/java/org/apache/ratis/conf/ConfUtils.java
@@ -373,7 +373,7 @@
       return;
     }
     final String fieldName = f.getName();
-    if ("LOG".equals(fieldName)) {
+    if ("LOG".equals(fieldName) || "$jacocoData".equals(fieldName)) {
       return;
     }
     if (!"PREFIX".equals(fieldName)) {