diff --git a/.github/workflows/tests_emulator.yml b/.github/workflows/tests_emulator.yml index f9103577f8..829bb5d575 100644 --- a/.github/workflows/tests_emulator.yml +++ b/.github/workflows/tests_emulator.yml @@ -20,6 +20,13 @@ jobs: timeout-minutes: 75 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + TEST_RELEASE_BUILD: true + # could be better, '/home/runner' should be '~/' but neither that nor '$HOME' worked + STOREFILEDIR: /home/runner/src + STOREFILE: android-keystore + STOREPASS: testpass + KEYPASS: testpass + KEYALIAS: nrkeystorealias strategy: fail-fast: false matrix: @@ -33,6 +40,15 @@ jobs: # This is useful for benchmarking, do 0, 1, 2, etc (up to 256 max job-per-matrix limit) for averages iteration: [0] steps: + - name: Test Credential Prep + run: | + echo "KSTOREPWD=$STOREPASS" >> $GITHUB_ENV + echo "KEYPWD=$KEYPASS" >> $GITHUB_ENV + mkdir $STOREFILEDIR + cd $STOREFILEDIR + echo y | keytool -genkeypair -dname "cn=AnkiDroid, ou=ankidroid, o=AnkiDroid, c=US" -alias $KEYALIAS -keypass $KEYPASS -keystore "$STOREFILE" -storepass $STOREPASS -keyalg RSA -validity 20000 + shell: bash + - uses: actions/checkout@v4 with: fetch-depth: 50 @@ -80,7 +96,7 @@ jobs: timeout_minutes: 15 retry_wait_seconds: 60 max_attempts: 3 - command: ./gradlew packagePlayDebug packagePlayDebugAndroidTest --daemon + command: ./gradlew packagePlayRelease packagePlayReleaseAndroidTest --daemon - name: AVD Boot and Snapshot Creation # Only generate a snapshot for saving if we are on main branch with a cache miss @@ -147,6 +163,13 @@ jobs: sleep 5 ./gradlew uninstallAll jacocoAndroidTestReport --daemon + - name: Upload Test Report + uses: actions/upload-artifact@v4 + if: always() + with: + name: ${{ matrix.api-level }}-${{ matrix.arch }}-${{matrix.target}}-${{matrix.first-boot-delay}}-${{matrix.iteration}}-jacocoAndroidTestReport + path: ~/work/Anki-Android/Anki-Android/AnkiDroid/build/reports/jacoco/ + - name: Upload Emulator Log uses: actions/upload-artifact@v4 if: always() diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index 4009a07467..d63590b0f1 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -6,6 +6,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.parcelize) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.keeper) id 'idea' } @@ -15,6 +16,12 @@ repositories { maven { url "https://jitpack.io" } } +keeper { + traceReferences { + // Silence missing definitions + arguments.set(["--map-diagnostics:MissingDefinitionsDiagnostic", "error", "none"]) + } +} idea { module { @@ -42,6 +49,12 @@ android { aidl = true } + if (rootProject.testReleaseBuild) { + testBuildType "release" + } else { + testBuildType "debug" + } + defaultConfig { applicationId "com.ichi2.anki" buildConfigField "Boolean", "CI", (System.getenv("CI") == "true").toString() @@ -77,6 +90,12 @@ android { versionCode=21900107 versionName="2.19alpha7" minSdk libs.versions.minSdk.get().toInteger() + + // Stays until this is in a release: https://github.com/google/desugar_jdk_libs/commit/c01a5446ca13586b801dbba4d83c6821337b3cc2 + if (testReleaseBuild && minSdk < 24) { + minSdk 24 + } + // After #13695: change .tests_emulator.yml targetSdk libs.versions.targetSdk.get().toInteger() testApplicationId "com.ichi2.anki.tests" @@ -129,9 +148,11 @@ android { resValue "string", "applicationId", "${defaultConfig.applicationId}${applicationIdSuffix}" } named('release') { + testCoverageEnabled = testReleaseBuild minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + testProguardFile 'proguard-test-rules.pro' splits.abi.universalApk = universalApkEnabled // Build universal APK for release with `-Duniversal-apk=true` - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release // syntax: assembleRelease -PcustomSuffix="suffix" -PcustomName="New name" @@ -293,7 +314,7 @@ tasks.register('assertNonzeroAndroidTests') { } } afterEvaluate { - tasks.named('connectedPlayDebugAndroidTest').configure { finalizedBy('assertNonzeroAndroidTests') } + tasks.named(androidTestName).configure { finalizedBy('assertNonzeroAndroidTests') } } apply from: "./robolectricDownloader.gradle" diff --git a/AnkiDroid/jacoco.gradle b/AnkiDroid/jacoco.gradle index 81719c3509..c65eb9acf2 100644 --- a/AnkiDroid/jacoco.gradle +++ b/AnkiDroid/jacoco.gradle @@ -92,6 +92,12 @@ def fileFilter = [ '**/*$Result$*.*' ] +def classDir = 'tmp/kotlin-classes/play' +if (rootProject.testReleaseBuild) + classDir += "Release" +else + classDir += "Debug" + // Our merge report task def testReport = tasks.register('jacocoTestReport', JacocoReport) { def htmlOutDir = project.layout.buildDirectory.dir("reports/jacoco/$name/html").get().asFile @@ -105,7 +111,7 @@ def testReport = tasks.register('jacocoTestReport', JacocoReport) { html.destination htmlOutDir } - def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir('tmp/kotlin-classes/playDebug'), excludes: fileFilter) + def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(classDir), excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.from = files([mainSrc]) @@ -117,7 +123,7 @@ def testReport = tasks.register('jacocoTestReport', JacocoReport) { } testReport.configure { dependsOn('testPlayDebugUnitTest') - dependsOn('connectedPlayDebugAndroidTest') + dependsOn(rootProject.androidTestName) } // A unit-test only report task @@ -133,7 +139,7 @@ def unitTestReport = tasks.register('jacocoUnitTestReport', JacocoReport) { html.destination htmlOutDir } - def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir('tmp/kotlin-classes/playDebug'), excludes: fileFilter) + def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(classDir), excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.from = files([mainSrc]) @@ -158,7 +164,7 @@ def androidTestReport = tasks.register('jacocoAndroidTestReport', JacocoReport) html.destination htmlOutDir } - def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir('tmp/kotlin-classes/playDebug'), excludes: fileFilter) + def kotlinClasses = fileTree(dir: project.layout.buildDirectory.dir(classDir), excludes: fileFilter) def mainSrc = "$project.projectDir/src/main/java" sourceDirectories.from = files([mainSrc]) @@ -167,7 +173,8 @@ def androidTestReport = tasks.register('jacocoAndroidTestReport', JacocoReport) '**/*.ec' ]) } -androidTestReport.configure { dependsOn('connectedPlayDebugAndroidTest') } + +androidTestReport.configure { dependsOn(rootProject.androidTestName) } // Issue 16640 - some emulators run, but register zero coverage tasks.register('assertNonzeroAndroidTestCoverage') { diff --git a/AnkiDroid/proguard-rules.pro b/AnkiDroid/proguard-rules.pro index 1e7b12192b..8882b346be 100644 --- a/AnkiDroid/proguard-rules.pro +++ b/AnkiDroid/proguard-rules.pro @@ -1,14 +1,10 @@ # Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:\Program Files (x86)\Android\android-sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# Add any project specific keep options here: - # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: @@ -16,16 +12,16 @@ # public *; #} -# FIXME remove this entire Android 4.2 workaround from 12/3/15 timrae commit for 2.15.x+ -# Samsung Android 4.2 bug workaround -# https://code.google.com/p/android/issues/detail?id=78377 --keepattributes ** --keep class !android.support.v7.view.menu.**,!android.support.design.internal.NavigationMenu,!android.support.design.internal.NavigationMenuPresenter,!android.support.design.internal.NavigationSubMenu,** {*;} -#5806 - Class: ActionBarOverflow --keep public class android.support.v7.internal.view.menu.** { *; } --keep public class androidx.appcompat.view.menu.** { *; } --dontpreverify --dontoptimize --dontshrink --dontwarn ** --dontnote ** +# Uncomment this to preserve the line number information for +# debugging stack traces. +-keepattributes SourceFile,LineNumberTable + +# Used through Reflection +-keep class com.ichi2.anki.**.*Fragment { *; } +-keep class * extends com.google.protobuf.GeneratedMessageLite { *; } +-keep class androidx.core.app.ActivityCompat$* { *; } +-keep class androidx.concurrent.futures.** { *; } + +# Ignore unused packages +-dontwarn javax.naming.** +-dontwarn org.ietf.jgss.** diff --git a/AnkiDroid/proguard-test-rules.pro b/AnkiDroid/proguard-test-rules.pro new file mode 100644 index 0000000000..845baa7937 --- /dev/null +++ b/AnkiDroid/proguard-test-rules.pro @@ -0,0 +1,11 @@ +# These proguard rules are only needed when building +# for the combination of testing and release mode +# Certain androidx frameworks that are test-only have +# issues with proguard / minimization in release mode + +# Used for testing +-keep class kotlin.test.** { *; } +-keep class **.R$layout { (...); ; } + +# Ignore unused packages +-dontwarn com.google.protobuf.GeneratedMessageLite$* \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index cdbef47aa4..5529d621f0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,6 @@ import com.android.build.api.dsl.CommonExtension +import com.android.build.api.extension.impl.AndroidComponentsExtensionImpl +import com.slack.keeper.optInToKeeper import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.internal.jvm.Jvm import org.jetbrains.kotlin.gradle.tasks.KotlinCompile @@ -18,6 +20,11 @@ buildscript { } } } + + dependencies { + // Stays until this is merged: https://github.com/slackhq/keeper/pull/147 + classpath(files("${rootDir.path}/tools/keeper-gradle-plugin.jar")) + } } plugins { @@ -29,6 +36,7 @@ plugins { alias(libs.plugins.kotlin.serialization) apply false alias(libs.plugins.ktlint) apply false alias(libs.plugins.dokka) apply false + alias(libs.plugins.keeper) apply false } val localProperties = java.util.Properties() @@ -64,6 +72,15 @@ subprojects { it.systemProperties["junit.jupiter.execution.parallel.enabled"] = true it.systemProperties["junit.jupiter.execution.parallel.mode.default"] = "concurrent" } + + val androidComponentsExtension = + extensions.findByName("androidComponents") as AndroidComponentsExtensionImpl<*, *, *> + androidComponentsExtension.beforeVariants { builder -> + if (testReleaseBuild && builder.name == "playRelease") + { + builder.optInToKeeper() + } + } } /** @@ -118,6 +135,11 @@ val preDexEnabled by extra("true" == System.getProperty("pre-dex", "true")) // allows for universal APKs to be generated val universalApkEnabled by extra("true" == System.getProperty("universal-apk", "false")) +val testReleaseBuild by extra(System.getenv("TEST_RELEASE_BUILD") == "true") +var androidTestName by extra( + if (testReleaseBuild) "connectedPlayReleaseAndroidTest" else "connectedPlayDebugAndroidTest" +) + val gradleTestMaxParallelForks by extra( if (System.getProperty("os.name") == "Mac OS X") { // macOS reports hardware cores. This is accurate for CI, Intel (halved due to SMT) and Apple Silicon diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4819c1d2b6..7a6038f1d0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -67,6 +67,7 @@ robolectric = "4.12.2" searchpreference = "2.5.1" seismic = "1.0.3" sharedPreferencesMock = "1.2.4" +slackKeeper = "0.16.0" slf4jTimber = "3.1" timber = "5.0.1" triplet = "3.10.0" @@ -177,3 +178,4 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +keeper = { id = "com.slack.keeper", version.ref = "slackKeeper" } diff --git a/tools/keeper-gradle-plugin.jar b/tools/keeper-gradle-plugin.jar new file mode 100644 index 0000000000..93bb5d6937 Binary files /dev/null and b/tools/keeper-gradle-plugin.jar differ