Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions .github/workflows/coverage-active.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
name: Code Coverage

on:
pull_request:
branches: [ "main" ]

jobs:
coverage:
runs-on: ubuntu-latest

permissions:
contents: read
pull-requests: write
checks: write

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'zulu'
cache: gradle

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Create local.properties
run: |
echo "sdk.dir=$ANDROID_HOME" > local.properties
echo "ndk.dir=$ANDROID_HOME/ndk-bundle" >> local.properties

- name: Run unit tests
run: ./gradlew :source:testDebugUnitTest --stacktrace

- name: Generate coverage report
run: ./gradlew :source:jacocoTestReport --stacktrace

- name: Generate root coverage report
run: ./gradlew jacocoRootReport --stacktrace

- name: Debug coverage files
run: |
echo "=== Looking for coverage files ==="
find . -name "*.exec" -type f | head -10
find . -name "*.xml" -path "*/jacoco*" -type f | head -10
echo "=== Checking specific paths ==="
ls -la build/reports/jacoco/jacocoRootReport/ || echo "Root report not found"
ls -la source/build/reports/jacoco/jacocoTestReport/ || echo "Module report not found"

# Show a sample of the XML content if it exists
if [ -f "build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" ]; then
echo "=== Root XML content sample ==="
head -20 build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml
fi

if [ -f "source/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml" ]; then
echo "=== Module XML content sample ==="
head -20 source/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml
fi

- name: Calculate coverage from XML
id: coverage
run: |
# Try to find the coverage XML file
COVERAGE_XML=""
if [ -f "build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml" ]; then
COVERAGE_XML="build/reports/jacoco/jacocoRootReport/jacocoRootReport.xml"
elif [ -f "source/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml" ]; then
COVERAGE_XML="source/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml"
fi

if [ -n "$COVERAGE_XML" ]; then
echo "coverage_file_exists=true" >> $GITHUB_OUTPUT
echo "coverage_xml_path=$COVERAGE_XML" >> $GITHUB_OUTPUT

# Extract coverage percentages using grep and sed
INSTRUCTION_COVERAGE=$(grep -o 'instruction[^>]*covered="[^"]*"[^>]*missed="[^"]*"' "$COVERAGE_XML" | head -1 | sed -E 's/.*covered="([^"]*)".*missed="([^"]*)".*/\1 \2/' | awk '{covered=$1; missed=$2; total=covered+missed; if(total>0) print int(covered*100/total); else print 0}')
BRANCH_COVERAGE=$(grep -o 'branch[^>]*covered="[^"]*"[^>]*missed="[^"]*"' "$COVERAGE_XML" | head -1 | sed -E 's/.*covered="([^"]*)".*missed="([^"]*)".*/\1 \2/' | awk '{covered=$1; missed=$2; total=covered+missed; if(total>0) print int(covered*100/total); else print 0}')
LINE_COVERAGE=$(grep -o 'line[^>]*covered="[^"]*"[^>]*missed="[^"]*"' "$COVERAGE_XML" | head -1 | sed -E 's/.*covered="([^"]*)".*missed="([^"]*)".*/\1 \2/' | awk '{covered=$1; missed=$2; total=covered+missed; if(total>0) print int(covered*100/total); else print 0}')

echo "instruction_coverage=${INSTRUCTION_COVERAGE:-0}" >> $GITHUB_OUTPUT
echo "branch_coverage=${BRANCH_COVERAGE:-0}" >> $GITHUB_OUTPUT
echo "line_coverage=${LINE_COVERAGE:-0}" >> $GITHUB_OUTPUT

echo "Coverage calculated:"
echo "Instructions: ${INSTRUCTION_COVERAGE:-0}%"
echo "Branches: ${BRANCH_COVERAGE:-0}%"
echo "Lines: ${LINE_COVERAGE:-0}%"
else
echo "❌ No coverage XML file found"
echo "coverage_file_exists=false" >> $GITHUB_OUTPUT
echo "instruction_coverage=0" >> $GITHUB_OUTPUT
echo "branch_coverage=0" >> $GITHUB_OUTPUT
echo "line_coverage=0" >> $GITHUB_OUTPUT
fi

- name: Upload coverage reports
uses: actions/upload-artifact@v3
if: steps.coverage.outputs.coverage_file_exists == 'true'
with:
name: jacoco-coverage-reports
path: |
build/reports/jacoco/
source/build/reports/jacoco/

- name: Comment PR with coverage
uses: actions/github-script@v7
if: steps.coverage.outputs.coverage_file_exists == 'true'
with:
script: |
const instructionCoverage = ${{ steps.coverage.outputs.instruction_coverage }};
const branchCoverage = ${{ steps.coverage.outputs.branch_coverage }};
const lineCoverage = ${{ steps.coverage.outputs.line_coverage }};

const coverageComment = `
## 📊 Code Coverage Report

| Metric | Coverage |
|--------|----------|
| Instructions | ${instructionCoverage}% |
| Branches | ${branchCoverage}% |
| Lines | ${lineCoverage}% |

${instructionCoverage >= 30 ? '✅' : '❌'} **Overall Coverage**: ${instructionCoverage}%

---
*Coverage report generated by JaCoCo*
`;

github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: coverageComment
});

- name: Coverage Check
if: steps.coverage.outputs.coverage_file_exists == 'true'
run: |
INSTRUCTION_COVERAGE=${{ steps.coverage.outputs.instruction_coverage }}
MINIMUM_COVERAGE=30

if [ $INSTRUCTION_COVERAGE -lt $MINIMUM_COVERAGE ]; then
echo "❌ Coverage ($INSTRUCTION_COVERAGE%) is below minimum threshold ($MINIMUM_COVERAGE%)"
echo "Please add more tests to improve coverage."
exit 1
else
echo "✅ Coverage ($INSTRUCTION_COVERAGE%) meets minimum threshold ($MINIMUM_COVERAGE%)"
fi
74 changes: 0 additions & 74 deletions JACOCO_USAGE.md

This file was deleted.

32 changes: 30 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,46 @@ tasks.register<JacocoReport>("jacocoRootReport") {
group = "verification"
description = "Generate Jacoco coverage reports for all modules."

// Depend on subproject test tasks and jacoco reports
dependsOn(subprojects.map { it.tasks.withType<Test>() })
dependsOn(subprojects.map { it.tasks.named("jacocoTestReport") })

reports {
xml.required.set(true)
html.required.set(true)
csv.required.set(false)
}

val sourceDirs = subprojects.map { it.file("src/main/java") } + subprojects.map { it.file("src/main/kotlin") }

sourceDirectories.setFrom(sourceDirs)
classDirectories.setFrom(subprojects.map { it.file("build/tmp/kotlin-classes/debug") })
executionData.setFrom(subprojects.map { it.fileTree("build").include("**/*.exec", "**/*.ec") })

// Aggregate class directories from all subprojects
val classDirectories = subprojects.flatMap { subproject ->
listOf(
subproject.file("build/tmp/kotlin-classes/debug"),
subproject.file("build/intermediates/javac/debug/classes")
)
}
this.classDirectories.setFrom(classDirectories)

// Aggregate execution data from all subprojects
val executionDataFiles = subprojects.flatMap { subproject ->
listOf(
subproject.file("build/jacoco/testDebugUnitTest.exec"),
subproject.file("build/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec"),
subproject.file("build/outputs/code_coverage/debugUnitTest/testDebugUnitTest.exec")
)
}.filter { it.exists() }

executionData.setFrom(executionDataFiles)

doFirst {
println("Root JaCoCo Task Configuration:")
println("Source directories: ${sourceDirectories.files}")
println("Class directories: ${classDirectories.map { it.absolutePath }}")
println("Execution data files: ${executionData.files}")
}
}

subprojects {
Expand Down
66 changes: 48 additions & 18 deletions source/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ android {
buildTypes {
debug {
isMinifyEnabled = false
enableUnitTestCoverage = true
enableAndroidTestCoverage = true
isTestCoverageEnabled = true
}
release {
isMinifyEnabled = false
Expand All @@ -33,6 +32,18 @@ android {
}

buildFeatures { buildConfig = true }

testOptions {
unitTests {
isIncludeAndroidResources = true
isReturnDefaultValues = true
all {
it.systemProperty("robolectric.enabledSdks", "34")
it.systemProperty("robolectric.offline", "true")
it.jvmArgs("-noverify")
}
}
}
}

tasks.withType<DokkaTaskPartial>().configureEach {
Expand Down Expand Up @@ -89,45 +100,64 @@ dependencies {
ksp(libs.clerk.automap.processor)
}

// JaCoCo configuration
// Simple JaCoCo configuration for Robolectric tests
tasks.register<JacocoReport>("jacocoTestReport") {
dependsOn("testDebugUnitTest")
group = "verification"
description = "Generate Jacoco coverage reports for the debug build."
description = "Generate Jacoco coverage reports for unit tests"

dependsOn("testDebugUnitTest")

reports {
xml.required.set(true)
html.required.set(true)
csv.required.set(false)
}

val fileFilter = listOf(
"**/R.class",
"**/R\$*.class",
"**/R\$*.class",
"**/BuildConfig.*",
"**/Manifest*.*",
"**/*Test*.*",
"android/**/*.*",
"**/*\$WhenMappings.*",
"**/*\$serializer.*",
"**/*\$\$serializer.*"
"**/*\$\$serializer.*",
"**/*\$Companion.*"
)

val debugTree = fileTree("${buildDir}/tmp/kotlin-classes/debug") {
val javaClasses = fileTree("${layout.buildDirectory.get().asFile}/intermediates/javac/debug/classes") {
exclude(fileFilter)
}

val kotlinClasses = fileTree("${layout.buildDirectory.get().asFile}/tmp/kotlin-classes/debug") {
exclude(fileFilter)
}

val mainSrc = "${project.projectDir}/src/main/java"
val kotlinSrc = "${project.projectDir}/src/main/kotlin"

sourceDirectories.setFrom(files(mainSrc, kotlinSrc))
classDirectories.setFrom(files(debugTree))
executionData.setFrom(fileTree(buildDir) {
include("**/*.exec", "**/*.ec")
})
classDirectories.setFrom(files(javaClasses, kotlinClasses))

sourceDirectories.setFrom(files(
"${project.projectDir}/src/main/java",
"${project.projectDir}/src/main/kotlin"
))

// Look for execution data in multiple locations
val executionDataFiles = files(
"${layout.buildDirectory.get().asFile}/jacoco/testDebugUnitTest.exec",
"${layout.buildDirectory.get().asFile}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec",
"${layout.buildDirectory.get().asFile}/outputs/code_coverage/debugUnitTest/testDebugUnitTest.exec"
).filter { it.exists() }

executionData.setFrom(executionDataFiles)

doFirst {
println("JaCoCo configuration:")
println("Source dirs: ${sourceDirectories.files}")
println("Class dirs: ${classDirectories.files}")
println("Execution data: ${executionData.files}")
}
}

// Ensure jacocoTestReport runs after tests
// Ensure test report runs after check
tasks.named("check") {
dependsOn("jacocoTestReport")
}
Loading
Loading