From 28f1819cf9b254c6f9b8e168542cf0fe67ed748d Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 20 Apr 2026 17:30:39 +0800 Subject: [PATCH 1/5] ci(coverage): gate changed-line coverage Use diff-cover changed-line coverage as the changed-code gate, keep the overall coverage delta gate at -0.1%, and keep Jacoco reports as summary-only snapshots. --- .github/workflows/pr-build.yml | 102 ++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8ef800e15ff..1ba06a870dd 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -279,6 +279,60 @@ jobs: echo "base_xmls=$BASE_XMLS" >> "$GITHUB_OUTPUT" echo "pr_xmls=$PR_XMLS" >> "$GITHUB_OUTPUT" + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Changed-line coverage (diff-cover) + id: diff-cover + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + set -euo pipefail + pip install --quiet 'diff-cover==9.2.0' + + # Ensure the base branch ref is available locally for diff-cover. + git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" + + PR_XMLS=$(find coverage/pr -name "jacocoTestReport.xml" | sort) + if [ -z "$PR_XMLS" ]; then + echo "No PR jacoco XML reports found, skipping diff-cover." + exit 0 + fi + + SRC_ROOTS=$(find . -type d -path '*/src/main/java' \ + -not -path './coverage/*' -not -path './.git/*' | sort) + + set +e + diff-cover $PR_XMLS \ + --compare-branch="origin/${BASE_REF}" \ + --src-roots $SRC_ROOTS \ + --fail-under=0 \ + --json-report=diff-cover.json \ + --markdown-report=diff-cover.md + DIFF_RC=$? + set -e + + CHANGED_LINE_COVERAGE=$(jq -r '.total_percent_covered // empty' diff-cover.json) + if [ -z "$CHANGED_LINE_COVERAGE" ]; then + echo "Unable to parse changed-line coverage from diff-cover.json." + exit 1 + fi + echo "changed_line_coverage=${CHANGED_LINE_COVERAGE}" >> "$GITHUB_OUTPUT" + + { + echo "### Changed-line Coverage (diff-cover)" + echo "" + if [ -f diff-cover.md ] && [ -s diff-cover.md ]; then + cat diff-cover.md + else + echo "_diff-cover produced no report._" + fi + } >> "$GITHUB_STEP_SUMMARY" + + echo "diff-cover exit code: ${DIFF_RC}" + - name: Aggregate base coverage id: jacoco-base uses: madrapps/jacoco-report@v1.7.2 @@ -288,6 +342,7 @@ jobs: min-coverage-overall: 0 min-coverage-changed-files: 0 skip-if-no-changes: true + comment-type: summary title: '## Base Coverage Snapshot' update-comment: false @@ -300,6 +355,7 @@ jobs: min-coverage-overall: 0 min-coverage-changed-files: 0 skip-if-no-changes: true + comment-type: summary title: '## PR Code Coverage Report' update-comment: false @@ -307,7 +363,7 @@ jobs: env: BASE_OVERALL_RAW: ${{ steps.jacoco-base.outputs.coverage-overall }} PR_OVERALL_RAW: ${{ steps.jacoco-pr.outputs.coverage-overall }} - PR_CHANGED_RAW: ${{ steps.jacoco-pr.outputs.coverage-changed-files }} + CHANGED_LINE_RAW: ${{ steps.diff-cover.outputs.changed_line_coverage }} run: | set -euo pipefail @@ -329,7 +385,7 @@ jobs: # 1) Parse metrics from jacoco-report outputs BASE_OVERALL="$(sanitize "$BASE_OVERALL_RAW")" PR_OVERALL="$(sanitize "$PR_OVERALL_RAW")" - PR_CHANGED="$(sanitize "$PR_CHANGED_RAW")" + CHANGED_LINE="$(sanitize "$CHANGED_LINE_RAW")" if ! is_number "$BASE_OVERALL" || ! is_number "$PR_OVERALL"; then echo "Failed to parse coverage values: base='${BASE_OVERALL}', pr='${PR_OVERALL}'." @@ -340,19 +396,16 @@ jobs: DELTA=$(awk -v pr="$PR_OVERALL" -v base="$BASE_OVERALL" 'BEGIN { printf "%.4f", pr - base }') DELTA_OK=$(compare_float "${DELTA} >= ${MAX_DROP}") - CHANGED_STATUS="SKIPPED (no changed coverage value)" - CHANGED_OK=1 - if [ -n "$PR_CHANGED" ] && [ "$PR_CHANGED" != "NaN" ]; then - if ! is_number "$PR_CHANGED"; then - echo "Failed to parse changed-files coverage: changed='${PR_CHANGED}'." - exit 1 - fi - CHANGED_OK=$(compare_float "${PR_CHANGED} > ${MIN_CHANGED}") - if [ "$CHANGED_OK" -eq 1 ]; then - CHANGED_STATUS="PASS (> ${MIN_CHANGED}%)" - else - CHANGED_STATUS="FAIL (<= ${MIN_CHANGED}%)" - fi + if [ -z "$CHANGED_LINE" ] || [ "$CHANGED_LINE" = "NaN" ] || ! is_number "$CHANGED_LINE"; then + echo "Failed to parse changed-line coverage: changed-line='${CHANGED_LINE}'." + exit 1 + fi + + CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}") + if [ "$CHANGED_LINE_OK" -eq 1 ]; then + CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)" + else + CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)" fi # 3) Output base metrics (always visible in logs + step summary) @@ -362,11 +415,11 @@ jobs: fi METRICS_TEXT=$(cat <> "$GITHUB_STEP_SUMMARY" @@ -391,14 +444,9 @@ jobs: exit 1 fi - if [ -z "$PR_CHANGED" ] || [ "$PR_CHANGED" = "NaN" ]; then - echo "No changed-files coverage value detected, skip changed-files gate." - exit 0 - fi - - if [ "$CHANGED_OK" -ne 1 ]; then - echo "Coverage gate failed: changed files coverage must be > 60%." - echo "changed=${PR_CHANGED}%" + if [ "$CHANGED_LINE_OK" -ne 1 ]; then + echo "Coverage gate failed: changed-line coverage must be > 60%." + echo "changed-line=${CHANGED_LINE}%" exit 1 fi From ece6e0930fa57ecad17c047c3f66a582522dac01 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Fri, 24 Apr 2026 11:56:44 +0800 Subject: [PATCH 2/5] ci(coverage): harden diff-cover step and handle non-Java PRs - Drop the unreachable PR_XMLS empty-check; collect-xml already exits 1 when jacoco reports are missing, and the previous `exit 0` would have left changed_line_coverage unset and triggered a misleading "Failed to parse changed-line coverage" in the enforcement step. - Parse total_num_lines from diff-cover.json: when a PR has no changed Java source lines (workflow-only, docs-only, proto-only), emit the sentinel NA and treat it as SKIPPED in the enforcement step so the gate does not wrongly fail. - Display NA without a trailing %% in the step summary and logs. Addresses review comments from CodeRabbit and cubic on PR #23. --- .github/workflows/pr-build.yml | 48 ++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 1ba06a870dd..5c71aba493d 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -296,11 +296,6 @@ jobs: git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" PR_XMLS=$(find coverage/pr -name "jacocoTestReport.xml" | sort) - if [ -z "$PR_XMLS" ]; then - echo "No PR jacoco XML reports found, skipping diff-cover." - exit 0 - fi - SRC_ROOTS=$(find . -type d -path '*/src/main/java' \ -not -path './coverage/*' -not -path './.git/*' | sort) @@ -314,12 +309,18 @@ jobs: DIFF_RC=$? set -e - CHANGED_LINE_COVERAGE=$(jq -r '.total_percent_covered // empty' diff-cover.json) - if [ -z "$CHANGED_LINE_COVERAGE" ]; then - echo "Unable to parse changed-line coverage from diff-cover.json." - exit 1 + TOTAL_NUM_LINES=$(jq -r '.total_num_lines // 0' diff-cover.json) + if [ "${TOTAL_NUM_LINES}" = "0" ]; then + echo "No changed Java source lines; skipping changed-line gate." + echo "changed_line_coverage=NA" >> "$GITHUB_OUTPUT" + else + CHANGED_LINE_COVERAGE=$(jq -r '.total_percent_covered // empty' diff-cover.json) + if [ -z "$CHANGED_LINE_COVERAGE" ]; then + echo "Unable to parse changed-line coverage from diff-cover.json." + exit 1 + fi + echo "changed_line_coverage=${CHANGED_LINE_COVERAGE}" >> "$GITHUB_OUTPUT" fi - echo "changed_line_coverage=${CHANGED_LINE_COVERAGE}" >> "$GITHUB_OUTPUT" { echo "### Changed-line Coverage (diff-cover)" @@ -396,16 +397,19 @@ jobs: DELTA=$(awk -v pr="$PR_OVERALL" -v base="$BASE_OVERALL" 'BEGIN { printf "%.4f", pr - base }') DELTA_OK=$(compare_float "${DELTA} >= ${MAX_DROP}") - if [ -z "$CHANGED_LINE" ] || [ "$CHANGED_LINE" = "NaN" ] || ! is_number "$CHANGED_LINE"; then + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_OK=1 + CHANGED_LINE_STATUS="SKIPPED (no changed Java source lines)" + elif [ -z "$CHANGED_LINE" ] || [ "$CHANGED_LINE" = "NaN" ] || ! is_number "$CHANGED_LINE"; then echo "Failed to parse changed-line coverage: changed-line='${CHANGED_LINE}'." exit 1 - fi - - CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}") - if [ "$CHANGED_LINE_OK" -eq 1 ]; then - CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)" else - CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)" + CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}") + if [ "$CHANGED_LINE_OK" -eq 1 ]; then + CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)" + else + CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)" + fi fi # 3) Output base metrics (always visible in logs + step summary) @@ -414,8 +418,14 @@ jobs: OVERALL_STATUS="FAIL (< ${MAX_DROP}%)" fi + if [ "$CHANGED_LINE" = "NA" ]; then + CHANGED_LINE_DISPLAY="NA" + else + CHANGED_LINE_DISPLAY="${CHANGED_LINE}%" + fi + METRICS_TEXT=$(cat < Date: Fri, 24 Apr 2026 18:10:24 +0800 Subject: [PATCH 3/5] test(common): add uncovered helper to force gate fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intentional test: add a public utility method to DecodeUtil without any unit test so `diff-cover` reports changed-line coverage below 60% and the Coverage Gate exercises its FAIL path (Acceptance Gate G1.3 — Java PR with coverage below threshold should FAIL). The new method has ~10 executable lines and is not called from production code or tests, so the new lines appear as 0% covered. Revert before merging. --- .../java/org/tron/common/utils/DecodeUtil.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/src/main/java/org/tron/common/utils/DecodeUtil.java b/common/src/main/java/org/tron/common/utils/DecodeUtil.java index 769268da2b9..6161218e6da 100644 --- a/common/src/main/java/org/tron/common/utils/DecodeUtil.java +++ b/common/src/main/java/org/tron/common/utils/DecodeUtil.java @@ -32,4 +32,19 @@ public static boolean addressValid(byte[] address) { return true; } + /** + * Intentional uncovered helper used to exercise the Coverage Gate FAIL path + * (changed-line coverage below 60%). No unit test is added on purpose. + * Revert before merging. + */ + public static int hexLengthForAddress(int byteLength) { + if (byteLength < 0) { + throw new IllegalArgumentException("byteLength must be non-negative"); + } + if (byteLength == 0) { + return 0; + } + return byteLength * 2; + } + } From 8b8a2d244f571a4e1126f84202eab67badfb2d5b Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 27 Apr 2026 10:35:38 +0800 Subject: [PATCH 4/5] test(common): cover DecodeUtil.addressValid branches Add DecodeUtilTest covering all four code paths of addressValid: - null input - empty byte array - wrong-length payload (too short / too long) - wrong address prefix - canonical valid address `addressPreFixByte` is a static mutable field; the test class saves and restores it in @BeforeClass / @AfterClass to avoid polluting sibling tests that may run in the same JVM fork. --- .../org/tron/common/utils/DecodeUtilTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 framework/src/test/java/org/tron/common/utils/DecodeUtilTest.java diff --git a/framework/src/test/java/org/tron/common/utils/DecodeUtilTest.java b/framework/src/test/java/org/tron/common/utils/DecodeUtilTest.java new file mode 100644 index 00000000000..f2e10a4e5c7 --- /dev/null +++ b/framework/src/test/java/org/tron/common/utils/DecodeUtilTest.java @@ -0,0 +1,58 @@ +package org.tron.common.utils; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.tron.core.Constant; + +public class DecodeUtilTest { + + private static byte savedPrefixByte; + + @BeforeClass + public static void saveAddressPrefix() { + savedPrefixByte = DecodeUtil.addressPreFixByte; + DecodeUtil.addressPreFixByte = Constant.ADD_PRE_FIX_BYTE_MAINNET; + } + + @AfterClass + public static void restoreAddressPrefix() { + DecodeUtil.addressPreFixByte = savedPrefixByte; + } + + @Test + public void addressValidRejectsNull() { + Assert.assertFalse(DecodeUtil.addressValid(null)); + } + + @Test + public void addressValidRejectsEmptyArray() { + Assert.assertFalse(DecodeUtil.addressValid(new byte[0])); + } + + @Test + public void addressValidRejectsWrongLength() { + byte[] tooShort = new byte[DecodeUtil.ADDRESS_SIZE / 2 - 1]; + tooShort[0] = Constant.ADD_PRE_FIX_BYTE_MAINNET; + Assert.assertFalse(DecodeUtil.addressValid(tooShort)); + + byte[] tooLong = new byte[DecodeUtil.ADDRESS_SIZE / 2 + 1]; + tooLong[0] = Constant.ADD_PRE_FIX_BYTE_MAINNET; + Assert.assertFalse(DecodeUtil.addressValid(tooLong)); + } + + @Test + public void addressValidRejectsWrongPrefix() { + byte[] address = new byte[DecodeUtil.ADDRESS_SIZE / 2]; + address[0] = (byte) (Constant.ADD_PRE_FIX_BYTE_MAINNET + 1); + Assert.assertFalse(DecodeUtil.addressValid(address)); + } + + @Test + public void addressValidAcceptsCanonicalAddress() { + byte[] address = new byte[DecodeUtil.ADDRESS_SIZE / 2]; + address[0] = Constant.ADD_PRE_FIX_BYTE_MAINNET; + Assert.assertTrue(DecodeUtil.addressValid(address)); + } +} From 6258af8d61b7097368a236c8890c749b81129910 Mon Sep 17 00:00:00 2001 From: bladehan1 Date: Mon, 27 Apr 2026 14:05:26 +0800 Subject: [PATCH 5/5] test(common): add docs change to verify mixed-PR gate scope Append an HTML comment to README.md so this branch becomes a mixed Java + docs PR. Used to verify Acceptance Gate G1.4: diff-cover should still report only the Java src/main/java changed lines (currently 5 lines from hexLengthForAddress) and ignore the docs addition. Revert before merging. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 575409b3a96..c5b6b315990 100644 --- a/README.md +++ b/README.md @@ -220,3 +220,5 @@ Thank you for considering to help out with the source code! If you'd like to con # License java-tron is released under the [LGPLv3 license](https://github.com/tronprotocol/java-tron/blob/master/LICENSE). + +