diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f45832..9f4ac86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,11 +26,13 @@ jobs: - uses: actions/checkout@v7 with: submodules: true - fetch-depth: 0 + fetch-depth: 0 + - name: Make script executable run: chmod +x ./patch_submodule.sh - name: Run patch submodule script run: ./patch_submodule.sh + - name: set up secrets run: | echo "${{ secrets.RELEASE_KEYSTORE }}" > keystore.asc @@ -49,14 +51,32 @@ jobs: with: java-version: 21 distribution: "temurin" - cache: 'gradle' + - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 + # Scoped to :app so the upstream KeePassDX submodule tests do not gate this build. + - name: Run unit tests + run: ./gradlew :app:testDebugUnitTest + - name: Build APK run: | VERSION_CODE=$(git rev-list --count HEAD) - ./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE + VERSION_BASE=$(git describe --tags --abbrev=0 | sed 's/^v//') + VERSION_DEV=$(git rev-list --count $(git describe --tags --abbrev=0)..HEAD) + if [ $VERSION_DEV -gt 0 ]; then + VERSION_NAME="${VERSION_BASE}.${VERSION_DEV}" + else + VERSION_NAME="${VERSION_BASE}" + fi + ./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME + + - name: Upload APK + uses: actions/upload-artifact@v7 + with: + path: app/build/outputs/apk/signedRelease/app-signedRelease.apk + compression-level: 0 + archive: false - name: Release uses: softprops/action-gh-release@v3 @@ -70,4 +90,3 @@ jobs: else echo "true" fi - diff --git a/app/build.gradle b/app/build.gradle index a2776dc..cf903e6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,20 +1,8 @@ -def getCommitCount() { - try { - def stdout = new ByteArrayOutputStream() - exec { - commandLine 'git', 'rev-list', '--count', 'HEAD' - standardOutput = stdout - } - return stdout.toString().trim().toInteger() - } catch (ignored) { - return 1 - } -} - plugins { id 'com.android.application' id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.20' id 'org.jetbrains.kotlin.plugin.compose' version '2.4.20' + id 'jacoco' } android { @@ -24,12 +12,15 @@ android { defaultConfig { applicationId 'net.helcel.fidelity' - versionName "1.3b" + // CI derives both from git (see .github/workflows/build.yml); the fallbacks are for local builds. + versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1.3c" versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1 buildConfigField("String", "APP_NAME", "\"Keepass Fidelity\"") manifestPlaceholders["APP_NAME"] = "Keepass Fidelity" minSdk = 28 targetSdk = 37 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } signingConfigs { @@ -55,6 +46,7 @@ android { debuggable true initWith(buildTypes.release) signingConfig signingConfigs.debug + enableUnitTestCoverage true } release { minifyEnabled true @@ -92,6 +84,25 @@ android { composeOptions { kotlinCompilerExtensionVersion = "2.2.20" } + + testOptions { + unitTests { + includeAndroidResources = true + all { + maxParallelForks = 2 + forkEvery = 50 + maxHeapSize = "1024m" + // Robolectric loads classes through its own sandbox classloader, which leaves them + // without a code-source location. Without this JaCoCo skips them entirely and the + // report only sees the handful of plain JVM classes. + jacoco { + includeNoLocationClasses = true + excludes = ["jdk.internal.*"] + } + } + } + } + kotlin { jvmToolchain(21) } @@ -137,4 +148,36 @@ dependencies { implementation 'joda-time:joda-time:2.14.3' implementation 'org.joda:joda-convert:3.0.1' + testImplementation 'junit:junit:4.13.2' + testImplementation 'io.mockk:mockk:1.14.11' + testImplementation 'org.robolectric:robolectric:4.16.1' + testImplementation 'androidx.test:core:1.7.0' + testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.11.0' + testImplementation 'androidx.compose.ui:ui-test-junit4' + + androidTestImplementation platform('androidx.compose:compose-bom:2026.09.00') + androidTestImplementation 'androidx.test.ext:junit:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + androidTestImplementation 'io.mockk:mockk-android:1.14.11' + debugImplementation 'androidx.compose.ui:ui-test-manifest' +} + +tasks.register('jacocoTestReport', JacocoReport) { + dependsOn 'testDebugUnitTest' + + reports { + xml.required = true + html.required = true + } + + def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*'] + def debugTree = fileTree(dir: "${project.layout.buildDirectory.asFile.get()}/intermediates/built_in_kotlinc/debug/compileDebugKotlin/classes", excludes: fileFilter) + def mainSrc = "${project.projectDir}/src/main/java" + + sourceDirectories.from = files([mainSrc]) + classDirectories.from = files([debugTree]) + executionData.from = fileTree(dir: project.layout.buildDirectory.asFile.get(), includes: [ + 'jacoco/testDebugUnitTest.exec', 'outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec' + ]) } diff --git a/build.gradle b/build.gradle index 3d719c9..10e4d19 100644 --- a/build.gradle +++ b/build.gradle @@ -7,8 +7,8 @@ buildscript { } plugins { - id 'com.android.application' version '9.4.0' apply false - id 'com.android.library' version '9.4.0' apply false - id 'org.jetbrains.kotlin.android' version '2.4.20' apply false - id 'com.autonomousapps.dependency-analysis' version '3.19.2' apply true + id 'com.android.application' version '9.3.2' apply false + id 'com.android.library' version '9.3.2' apply false + id 'org.jetbrains.kotlin.android' version '2.3.21' apply false + id 'com.autonomousapps.dependency-analysis' version '3.13.0' apply true } \ No newline at end of file