This repository was archived by the owner on Jun 18, 2026. It is now read-only.
docs: refresh stale counts in README and ARCHITECTURE #721
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Code Coverage | |
| on: | |
| push: | |
| branches: [main, master] | |
| pull_request: | |
| branches: [main, master] | |
| permissions: | |
| contents: read | |
| jobs: | |
| coverage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v6 | |
| - name: Set up JDK 17 | |
| uses: actions/setup-java@v5 | |
| with: | |
| java-version: 17 | |
| distribution: temurin | |
| - name: Download test dependencies | |
| run: | | |
| mkdir -p Gvisual/lib/test | |
| # JUnit 4 + Hamcrest | |
| curl -sL -o Gvisual/lib/test/junit-4.13.2.jar \ | |
| https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar | |
| curl -sL -o Gvisual/lib/test/hamcrest-core-1.3.jar \ | |
| https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar | |
| # JaCoCo agent + CLI for coverage instrumentation and reporting | |
| curl -sL -o /tmp/jacoco.zip \ | |
| https://repo1.maven.org/maven2/org/jacoco/jacoco/0.8.12/jacoco-0.8.12.zip | |
| unzip -o /tmp/jacoco.zip -d /tmp/jacoco | |
| cp /tmp/jacoco/lib/jacocoagent.jar Gvisual/lib/test/jacocoagent.jar | |
| cp /tmp/jacoco/lib/jacococli.jar Gvisual/lib/test/jacococli.jar | |
| - name: Compile source | |
| working-directory: Gvisual | |
| run: | | |
| mkdir -p build/classes | |
| find src -name '*.java' > sources.txt | |
| javac -source 8 -target 8 \ | |
| -cp "$(find lib -name '*.jar' -not -path 'lib/test/*' | tr '\n' ':')" \ | |
| -d build/classes \ | |
| @sources.txt | |
| - name: Compile tests | |
| working-directory: Gvisual | |
| run: | | |
| mkdir -p build/test/classes | |
| find test -name '*.java' > test-sources.txt | |
| javac -source 8 -target 8 \ | |
| -cp "build/classes:$(find lib -name '*.jar' | tr '\n' ':')" \ | |
| -d build/test/classes \ | |
| @test-sources.txt | |
| - name: Discover test classes | |
| id: discover-tests | |
| working-directory: Gvisual | |
| run: | | |
| # Auto-discover all *Test classes from compiled test output | |
| TEST_CLASSES=$(find build/test/classes -name '*Test.class' \ | |
| | sed 's|build/test/classes/||;s|\.class$||;s|/|.|g' \ | |
| | sort) | |
| echo "Found $(echo "$TEST_CLASSES" | wc -l) test classes" | |
| echo "$TEST_CLASSES" | |
| # Store as space-separated for JUnitCore | |
| echo "classes=$(echo $TEST_CLASSES | tr '\n' ' ')" >> $GITHUB_OUTPUT | |
| - name: Run tests with JaCoCo coverage | |
| working-directory: Gvisual | |
| run: | | |
| java \ | |
| -javaagent:lib/test/jacocoagent.jar=destfile=build/jacoco.exec,includes=gvisual.*:app.* \ | |
| -cp "build/classes:build/test/classes:$(find lib -name '*.jar' | tr '\n' ':')" \ | |
| org.junit.runner.JUnitCore \ | |
| ${{ steps.discover-tests.outputs.classes }} | |
| - name: Generate coverage report | |
| working-directory: Gvisual | |
| run: | | |
| mkdir -p build/coverage | |
| # Generate HTML report | |
| java -jar lib/test/jacococli.jar report build/jacoco.exec \ | |
| --classfiles build/classes \ | |
| --sourcefiles src \ | |
| --html build/coverage/html \ | |
| --xml build/coverage/coverage.xml \ | |
| --csv build/coverage/coverage.csv \ | |
| --name "GraphVisual Coverage Report" | |
| - name: Coverage summary | |
| working-directory: Gvisual | |
| run: | | |
| echo "## 📊 Code Coverage Report" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [ ! -f build/coverage/coverage.csv ]; then | |
| echo "_No coverage.csv produced._" >> $GITHUB_STEP_SUMMARY | |
| exit 0 | |
| fi | |
| # First pass: aggregate totals across all classes. | |
| overall_missed=0 | |
| overall_covered=0 | |
| branch_missed_t=0 | |
| branch_covered_t=0 | |
| line_missed_t=0 | |
| line_covered_t=0 | |
| while IFS=',' read -r group package class inst_missed inst_covered branch_missed branch_covered line_missed line_covered rest; do | |
| overall_missed=$((overall_missed + inst_missed)) | |
| overall_covered=$((overall_covered + inst_covered)) | |
| branch_missed_t=$((branch_missed_t + branch_missed)) | |
| branch_covered_t=$((branch_covered_t + branch_covered)) | |
| line_missed_t=$((line_missed_t + line_missed)) | |
| line_covered_t=$((line_covered_t + line_covered)) | |
| done < <(tail -n +2 build/coverage/coverage.csv) | |
| inst_total=$((overall_missed + overall_covered)) | |
| branch_total=$((branch_missed_t + branch_covered_t)) | |
| line_total=$((line_missed_t + line_covered_t)) | |
| inst_pct=0; [ "$inst_total" -gt 0 ] && inst_pct=$((overall_covered * 100 / inst_total)) | |
| branch_pct=0; [ "$branch_total" -gt 0 ] && branch_pct=$((branch_covered_t * 100 / branch_total)) | |
| line_pct=0; [ "$line_total" -gt 0 ] && line_pct=$((line_covered_t * 100 / line_total)) | |
| echo "**Overall instruction coverage: ${inst_pct}%** (${overall_covered}/${inst_total})" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Metric | Covered | Total | % |" >> $GITHUB_STEP_SUMMARY | |
| echo "|--------|---------|-------|---|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Instructions | ${overall_covered} | ${inst_total} | ${inst_pct}% |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Branches | ${branch_covered_t} | ${branch_total} | ${branch_pct}% |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Lines | ${line_covered_t} | ${line_total} | ${line_pct}% |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "<details><summary>Per-class breakdown</summary>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Package | Class | Instruction Coverage |" >> $GITHUB_STEP_SUMMARY | |
| echo "|---------|-------|---------------------|" >> $GITHUB_STEP_SUMMARY | |
| tail -n +2 build/coverage/coverage.csv | while IFS=',' read -r group package class inst_missed inst_covered rest; do | |
| total=$((inst_missed + inst_covered)) | |
| if [ "$total" -gt 0 ]; then | |
| pct=$((inst_covered * 100 / total)) | |
| echo "| $package | $class | ${pct}% (${inst_covered}/${total}) |" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| done | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "</details>" >> $GITHUB_STEP_SUMMARY | |
| - name: Upload coverage report | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: coverage-report | |
| path: Gvisual/build/coverage/ | |
| retention-days: 30 | |
| - name: Enforce coverage threshold | |
| working-directory: Gvisual | |
| run: | | |
| if [ -f build/coverage/coverage.csv ]; then | |
| # Calculate overall instruction coverage | |
| total_covered=0 | |
| total_missed=0 | |
| while IFS=',' read -r group package class inst_missed inst_covered rest; do | |
| total_missed=$((total_missed + inst_missed)) | |
| total_covered=$((total_covered + inst_covered)) | |
| done < <(tail -n +2 build/coverage/coverage.csv) | |
| total=$((total_missed + total_covered)) | |
| if [ "$total" -gt 0 ]; then | |
| pct=$((total_covered * 100 / total)) | |
| echo "Overall instruction coverage: ${pct}%" | |
| THRESHOLD=50 | |
| if [ "$pct" -lt "$THRESHOLD" ]; then | |
| echo "::error::Coverage ${pct}% is below threshold ${THRESHOLD}%" | |
| exit 1 | |
| fi | |
| echo "✅ Coverage ${pct}% meets threshold ${THRESHOLD}%" | |
| fi | |
| fi | |
| - name: Comment coverage on PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const csvPath = 'Gvisual/build/coverage/coverage.csv'; | |
| if (!fs.existsSync(csvPath)) return; | |
| const lines = fs.readFileSync(csvPath, 'utf8').trim().split('\n').slice(1); | |
| let totalCovered = 0, totalMissed = 0; | |
| const rows = []; | |
| for (const line of lines) { | |
| const cols = line.split(','); | |
| const pkg = cols[1], cls = cols[2]; | |
| const missed = parseInt(cols[3]), covered = parseInt(cols[4]); | |
| const total = missed + covered; | |
| const pct = total > 0 ? Math.round(covered * 100 / total) : 0; | |
| totalMissed += missed; | |
| totalCovered += covered; | |
| rows.push(`| ${pkg} | ${cls} | ${pct}% (${covered}/${total}) |`); | |
| } | |
| const overallTotal = totalMissed + totalCovered; | |
| const overallPct = overallTotal > 0 ? Math.round(totalCovered * 100 / overallTotal) : 0; | |
| const body = [ | |
| '## 📊 Code Coverage Report', | |
| '', | |
| `**Overall: ${overallPct}%** (${totalCovered}/${overallTotal} instructions)`, | |
| '', | |
| '| Package | Class | Coverage |', | |
| '|---------|-------|----------|', | |
| ...rows, | |
| ].join('\n'); | |
| // Find and update existing comment or create new | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes('📊 Code Coverage Report')); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| files: Gvisual/build/coverage/coverage.xml | |
| fail_ci_if_error: false | |
| continue-on-error: true |