diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8ef800e15ff..5c71aba493d 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -279,6 +279,61 @@ 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) + 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 + + 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 (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 +343,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 +356,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 +364,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 +386,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,18 +397,18 @@ 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}%)" + 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 + else + CHANGED_LINE_OK=$(compare_float "${CHANGED_LINE} > ${MIN_CHANGED}") + if [ "$CHANGED_LINE_OK" -eq 1 ]; then + CHANGED_LINE_STATUS="PASS (> ${MIN_CHANGED}%)" else - CHANGED_STATUS="FAIL (<= ${MIN_CHANGED}%)" + CHANGED_LINE_STATUS="FAIL (<= ${MIN_CHANGED}%)" fi fi @@ -361,12 +418,18 @@ 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 <> "$GITHUB_STEP_SUMMARY" @@ -391,14 +454,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 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). + + 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; + } + } 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)); + } +}