From 24b9320d6ddb24a309a008785418b28782f00f6b Mon Sep 17 00:00:00 2001 From: Amine Date: Sun, 16 Mar 2025 14:27:30 +0100 Subject: [PATCH 1/7] fix: Fixed source deletion test. --- .../apps/readerforselfossv2/android/SourcesActivityTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt index 403b7b1..126a365 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt @@ -71,6 +71,8 @@ class SourcesActivityTest { fun deleteTheCreatedSource() { onView(withText(sourceName)).check(matches(isDisplayed())) onView(withId(R.id.deleteBtn)).perform(click()) + onView(withText(R.string.confirm_delete_title)).check(matches(isDisplayed())) + onView(withId(android.R.id.button1)).perform(click()) onView(withText(sourceName)).check(doesNotExist()) } -- 2.34.1 From 02d503e03ab7a1339b69eb53111a39e3a8ffd31d Mon Sep 17 00:00:00 2001 From: Amine Date: Sun, 16 Mar 2025 15:44:02 +0100 Subject: [PATCH 2/7] chore: comment robolectric tests for now. --- .../android/tests/robolectric/Helpers.kt | 3 +++ .../android/tests/robolectric/LoginActivityTest.kt | 8 +++++++- .../android/tests/robolectric/RobotElectriqueRunner.kt | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/Helpers.kt b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/Helpers.kt index 51ebb26..4de685e 100644 --- a/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/Helpers.kt +++ b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/Helpers.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint") +/* package bou.amine.apps.readerforselfossv2.android.tests.robolectric import android.view.Menu @@ -25,3 +27,4 @@ fun Menu.assertVisible( val item = this.findItem(id) assertTrue(item.isVisible) } +*/ diff --git a/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt index 5eba600..74b1fdd 100644 --- a/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt +++ b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/LoginActivityTest.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint") +/* package bou.amine.apps.readerforselfossv2.android.tests.robolectric import android.widget.Button @@ -57,7 +59,8 @@ class LoginActivityTest { } } - /* @Test + */ +/* @Test fun connect() { Robolectric.buildActivity(LoginActivity::class.java).use { controller -> controller.setup() // Moves the Activity to the RESUMED state @@ -72,4 +75,7 @@ class LoginActivityTest { assertEquals(expectedIntent.component, actual.component) } }*/ +/* + } +*/ diff --git a/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/RobotElectriqueRunner.kt b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/RobotElectriqueRunner.kt index 2f22623..5085fd2 100644 --- a/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/RobotElectriqueRunner.kt +++ b/androidApp/src/test/kotlin/bou/amine/apps/readerforselfossv2/android/tests/robolectric/RobotElectriqueRunner.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint") +/* package bou.amine.apps.readerforselfossv2.android.tests.robolectric import org.robolectric.RobolectricTestRunner @@ -8,3 +10,4 @@ class RobotElectriqueRunner( ) : RobolectricTestRunner(testClass) { override fun buildGlobalConfig(): Config = Config.Builder().setSdk(25, 30, 33).build() } +*/ -- 2.34.1 From 7c65a6331570544ff89fc196519cc2554c7cd904 Mon Sep 17 00:00:00 2001 From: Amine Date: Sun, 16 Mar 2025 15:55:10 +0100 Subject: [PATCH 3/7] ci: Instrumentation tests coverage in ci. --- .gitea/workflows/common_coverage.yml | 65 ++++++ .gitea/workflows/on_pr.yml | 219 ++++++++++++------ androidApp/build.gradle.kts | 98 +++++++- ...ActivityTest.kt => 1-LoginActivityTest.kt} | 31 +-- ...eActivityTest.kt => 2-HomeActivityTest.kt} | 14 +- ...ivityTest.kt => 3-SettingsActivityTest.kt} | 10 +- ...st.kt => 4-SettingsActivityGeneralTest.kt} | 9 +- ...est.kt => 5-SettingsActivityReaderTest.kt} | 21 +- ...st.kt => 6-SettingsActivityOfflineTest.kt} | 21 +- ...tivityTest.kt => 7-SourcesActivityTest.kt} | 12 +- .../readerforselfossv2/android/CommonTests.kt | 118 +++++++++- androidApp/src/debug/AndroidManifest.xml | 87 +++++++ gradle.properties | 1 + .../repository/Repository.kt | 28 ++- 14 files changed, 583 insertions(+), 151 deletions(-) create mode 100644 .gitea/workflows/common_coverage.yml rename androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/{LoginActivityTest.kt => 1-LoginActivityTest.kt} (86%) rename androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/{HomeActivityTest.kt => 2-HomeActivityTest.kt} (93%) rename androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/{SettingsActivityTest.kt => 3-SettingsActivityTest.kt} (90%) rename androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/{SettingsActivityGeneralTest.kt => 4-SettingsActivityGeneralTest.kt} (94%) rename androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/{SettingsActivityReaderTest.kt => 5-SettingsActivityReaderTest.kt} (83%) rename androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/{SettingsActivityOfflineTest.kt => 6-SettingsActivityOfflineTest.kt} (91%) rename androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/{SourcesActivityTest.kt => 7-SourcesActivityTest.kt} (90%) create mode 100644 androidApp/src/debug/AndroidManifest.xml diff --git a/.gitea/workflows/common_coverage.yml b/.gitea/workflows/common_coverage.yml new file mode 100644 index 0000000..c74481c --- /dev/null +++ b/.gitea/workflows/common_coverage.yml @@ -0,0 +1,65 @@ +name: Coverage +on: + workflow_call: + +jobs: + BuildAndTestAndCoverage: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch tags + run: git fetch --tags -p + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + cache: gradle + - uses: gradle/actions/setup-gradle@v3 + - uses: android-actions/setup-android@v3 + - name: Configure gradle... + run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties + - uses: KengoTODA/actions-setup-docker-compose@v1 + with: + version: "2.23.3" + - name: run selfoss + run: | + docker compose -f .gitea/workflows/assets/docker-compose.yml up -d + - name: Set env url + run: | + export SELFOSS_URL=172.17.0.1:8888 + # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 + - name: Kill crashpad_handler processes + if: always() + run: | + pkill -SIGTERM crashpad_handler || true + sleep 5 + pkill -SIGKILL crashpad_handler || true + - name: Tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + script: | + ./gradlew androidApp:connectedAndroidTest + killall -INT crashpad_handler || true + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: failure-espresso + path: build/reports/androidTests/connected/screenshots + retention-days: 2 + overwrite: true + include-hidden-files: true + - uses: actions/upload-artifact@v3 + with: + name: coverage-espresso + path: build/reports/coverage/androidTest/githubConfig/debug/connected + retention-days: 1 + overwrite: true + include-hidden-files: true + - name: Clean + if: always() + run: | + docker compose -f .gitea/workflows/assets/docker-compose.yml stop diff --git a/.gitea/workflows/on_pr.yml b/.gitea/workflows/on_pr.yml index d0d265b..57a1b31 100644 --- a/.gitea/workflows/on_pr.yml +++ b/.gitea/workflows/on_pr.yml @@ -3,89 +3,160 @@ on: pull_request: branches: - master - - chore-crowdin-ci jobs: - Lint: - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: gradle - - name: Install klint - run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ - - name: Install detekt - run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip - - name: Linting... - run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' - - name: Detecting... - run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' - translations: + BuildAndTestAndCoverage: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v4 with: fetch-depth: 0 - - name: "Check translations changes" - id: check-translations-changes - uses: tj-actions/changed-files@v45 + - name: Fetch tags + run: git fetch --tags -p + - uses: actions/setup-java@v4 with: - files: | - androidApp/src/main/res/values/strings.xml - - name: upload translation sources - if: steps.check-api-changes.outputs.any_modified == 'true' - uses: crowdin/github-action@v2 + distribution: 'temurin' + java-version: '17' + - uses: gradle/actions/setup-gradle@v3 + - uses: android-actions/setup-android@v3 + - name: Configure gradle... + run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties + - uses: KengoTODA/actions-setup-docker-compose@v1 with: - config: './.gitea/workflows/assets/crowdin.yml' - upload_sources: true - upload_translations: false - download_translations: false - create_pull_request: false - push_translations: false - env: - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - name: wait - if: steps.check-api-changes.outputs.any_modified == 'true' - run: sleep 10s - - name: download translations - if: steps.check-api-changes.outputs.any_modified == 'true' - uses: crowdin/github-action@v2 - with: - config: './.gitea/workflows/assets/crowdin.yml' - upload_sources: false - upload_translations: false - download_translations: true - create_pull_request: false - push_translations: false - env: - CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - - name: Check for uncommitted changes - if: steps.check-api-changes.outputs.any_modified == 'true' - id: check-changes - uses: mskri/check-uncommitted-changes-action@v1.0.1 - - name: Commit Changes - if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' + version: "2.23.3" + - name: run selfoss run: | - git config --global user.email aminecmi+giteadrone@pm.me - git config --global user.name giteadrone - git add ./androidApp/src/main/res/* - git commit -m "translation: translation files" - - name: Push changes - if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' - uses: appleboy/git-push-action@v1.0.0 + docker compose -f .gitea/workflows/assets/docker-compose.yml up -d + # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 + - name: Kill crashpad_handler processes + if: always() + run: | + pkill -SIGTERM crashpad_handler || true + sleep 5 + pkill -SIGKILL crashpad_handler || true + - name: Change url until I find a better way to do it + run: | + sed -i "s/val defaultUrl = \"http:\/\/10\.0\.2\.2\:8888\"/val defaultUrl = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt + - name: Tests + uses: reactivecircus/android-emulator-runner@v2 with: - author_name: giteadrone - author_email: aminecmi+giteadrone@pm.me - remote: ${{ secrets.REMOTE_URL }} - ssh_key: ${{ secrets.PRIVATE_KEY }} - branch: ${{ github.head_ref || github.ref_name }} - build: - needs: Lint - uses: ./.gitea/workflows/common_build.yml + api-level: 29 + script: | + ./gradlew androidApp:clearScreenshotsTask || true + ./gradlew androidApp:createScreenshotDirectory + adb logcat -G 16M + ./gradlew JacocoDebugCodeCoverage || true + ./gradlew androidApp:fetchScreenshots + adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt + - uses: actions/upload-artifact@v3 + if: always() + with: + name: screenshot-espresso + path: androidApp/build/reports/androidTests/connected/screenshots + retention-days: 2 + overwrite: true + include-hidden-files: true + - uses: actions/upload-artifact@v3 + if: always() + with: + name: result-espresso + path: androidApp/build/reports/androidTests/connected/debug/flavors/githubConfig + retention-days: 1 + overwrite: true + include-hidden-files: true + - uses: actions/upload-artifact@v3 + with: + name: coverage-espresso + path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage + retention-days: 1 + overwrite: true + include-hidden-files: true + - name: Clean + if: always() + run: | + docker compose -f .gitea/workflows/assets/docker-compose.yml stop + +# Lint: +# runs-on: ubuntu-latest +# steps: +# - name: Check out repository code +# uses: actions/checkout@v4 +# - uses: actions/setup-java@v4 +# with: +# distribution: 'temurin' +# java-version: '17' +# cache: gradle +# - name: Install klint +# run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ +# - name: Install detekt +# run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip +# - name: Linting... +# run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' +# - name: Detecting... +# run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' +# translations: +# runs-on: ubuntu-latest +# steps: +# - name: Check out repository code +# uses: actions/checkout@v4 +# with: +# fetch-depth: 0 +# - name: "Check translations changes" +# id: check-translations-changes +# uses: tj-actions/changed-files@v45 +# with: +# files: | +# androidApp/src/main/res/values/strings.xml +# - name: upload translation sources +# if: steps.check-api-changes.outputs.any_modified == 'true' +# uses: crowdin/github-action@v2 +# with: +# config: './.gitea/workflows/assets/crowdin.yml' +# upload_sources: true +# upload_translations: false +# download_translations: false +# create_pull_request: false +# push_translations: false +# env: +# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} +# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} +# - name: wait +# if: steps.check-api-changes.outputs.any_modified == 'true' +# run: sleep 10s +# - name: download translations +# if: steps.check-api-changes.outputs.any_modified == 'true' +# uses: crowdin/github-action@v2 +# with: +# config: './.gitea/workflows/assets/crowdin.yml' +# upload_sources: false +# upload_translations: false +# download_translations: true +# create_pull_request: false +# push_translations: false +# env: +# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} +# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} +# - name: Check for uncommitted changes +# if: steps.check-api-changes.outputs.any_modified == 'true' +# id: check-changes +# uses: mskri/check-uncommitted-changes-action@v1.0.1 +# - name: Commit Changes +# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' +# run: | +# git config --global user.email aminecmi+giteadrone@pm.me +# git config --global user.name giteadrone +# git add ./androidApp/src/main/res/* +# git commit -m "translation: translation files" +# - name: Push changes +# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' +# uses: appleboy/git-push-action@v1.0.0 +# with: +# author_name: giteadrone +# author_email: aminecmi+giteadrone@pm.me +# remote: ${{ secrets.REMOTE_URL }} +# ssh_key: ${{ secrets.PRIVATE_KEY }} +# branch: ${{ github.head_ref || github.ref_name }} +# build: +# needs: Lint +# uses: ./.gitea/workflows/common_build.yml diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 4e65d85..70bf27e 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -10,6 +10,7 @@ plugins { id("com.mikepenz.aboutlibraries.plugin") id("org.jetbrains.kotlinx.kover") id("app.cash.sqldelight") version "2.0.2" + jacoco } fun Project.execWithOutput( @@ -64,6 +65,15 @@ fun versionNameFromGit(): String { return gitVersion() } +val exclusions = + listOf( + "**/R.class", + "**/R\$*.class", + "**/BuildConfig.*", + "**/Manifest*.*", + "**/*Test*.*", + ) + android { compileOptions { isCoreLibraryDesugaringEnabled = true @@ -95,7 +105,7 @@ android { // tests testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArguments["clearPackageData"] = "true" + testInstrumentationRunnerArguments["useTestStorageService"] = "true" } packaging { resources { @@ -109,6 +119,44 @@ android { proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") } getByName("debug") { + isTestCoverageEnabled = true + enableAndroidTestCoverage = true + installation { + installOptions("-g", "-r") + } + + val androidTests = "connectedAndroidTest" + tasks.register("JacocoDebugCodeCoverage") { + // Depend on unit tests and Android tests tasks + dependsOn(listOf(androidTests)) + // Set task grouping and description + group = "Reporting" + description = "Execute UI and unit tests, generate and combine Jacoco coverage report" + // Configure reports to generate both XML and HTML formats + reports { + xml.required.set(true) + html.required.set(true) + } + // Set source directories to the main source directory + sourceDirectories.setFrom(layout.projectDirectory.dir("src/main")) + // Set class directories to compiled Java and Kotlin classes, excluding specified exclusions + classDirectories.setFrom( + files( + fileTree(layout.buildDirectory.dir("intermediates/javac/")) { + exclude(exclusions) + }, + fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/")) { + exclude(exclusions) + }, + ), + ) + // Collect execution data from .exec and .ec files generated during test execution + executionData.setFrom( + files( + fileTree(layout.buildDirectory) { include(listOf("**/*.exec", "**/*.ec")) }, + ), + ) + } } } flavorDimensions.add("build") @@ -121,7 +169,6 @@ android { namespace = "bou.amine.apps.readerforselfossv2.android" testOptions { animationsDisabled = true - execution = "ANDROIDX_TEST_ORCHESTRATOR" unitTests { isIncludeAndroidResources = true } @@ -154,8 +201,8 @@ dependencies { implementation("androidx.multidex:multidex:2.0.1") // About - implementation("com.mikepenz:aboutlibraries-core:10.5.1") - implementation("com.mikepenz:aboutlibraries:10.5.1") + implementation("com.mikepenz:aboutlibraries-core:11.6.3") + implementation("com.mikepenz:aboutlibraries:11.6.3") // Material-ish things implementation("com.ashokvarma.android:bottom-navigation-bar:2.2.0") @@ -197,14 +244,15 @@ dependencies { testImplementation("io.mockk:mockk:1.13.14") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1") - androidTestImplementation("androidx.test:runner:1.6.2") - androidTestImplementation("androidx.test:rules:1.6.1") + androidTestImplementation("androidx.test:runner:1.7.0-alpha01") + androidTestImplementation("androidx.test:rules:1.7.0-alpha01") androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") implementation("androidx.test.espresso:espresso-idling-resource:3.6.1") androidTestImplementation("androidx.test.ext:junit-ktx:1.2.1") - androidTestUtil("androidx.test:orchestrator:1.5.1") + androidTestUtil("androidx.test.services:test-services:1.6.0-alpha02") testImplementation("org.robolectric:robolectric:4.14.1") - testImplementation("androidx.test:core-ktx:1.6.1") + testImplementation("androidx.test:core-ktx:1.7.0-alpha01") + androidTestImplementation("androidx.test.uiautomator:uiautomator:2.3.0") implementation("ch.acra:acra-http:$acraVersion") implementation("ch.acra:acra-toast:$acraVersion") @@ -224,9 +272,16 @@ tasks.withType { ) showStandardStreams = true } + if (this.name == "connectedAndroidTest") { + configure { + isIncludeNoLocationClasses = true + excludes = listOf("jdk.internal.*") + } + } } aboutLibraries { + excludeFields = arrayOf("generated") offlineMode = true fetchRemoteLicense = false fetchRemoteFunding = false @@ -235,3 +290,30 @@ aboutLibraries { duplicationMode = com.mikepenz.aboutlibraries.plugin.DuplicateMode.MERGE duplicationRule = com.mikepenz.aboutlibraries.plugin.DuplicateRule.GROUP } + +val clearScreenshotsTask = + tasks.register("clearScreenshots") { + println("AMINE : clear") + commandLine = listOf("adb", "shell", "rm", "-r", "/storage/emulated/0/Pictures/selfoss_tests/screenshots/*") + } + +val createScreenshotDirectoryTask = + tasks.register("createScreenshotDirectory") { + println("AMINE : create directory") + group = "reporting" + commandLine = listOf("adb", "shell", "mkdir", "-p", "/storage/emulated/0/Pictures/selfoss_tests/screenshots") + } + +tasks.register("fetchScreenshots") { + val reportsDirectory = file("$buildDir/reports/androidTests/connected") + println("AMINE : fetch") + group = "reporting" + executable(android.adbExecutable.toString()) + commandLine = listOf("adb", "pull", "/storage/emulated/0/Pictures/selfoss_tests/screenshots", reportsDirectory.toString()) + + finalizedBy(clearScreenshotsTask) + + doFirst { + reportsDirectory.mkdirs() + } +} diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt similarity index 86% rename from androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt rename to androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt index 30d2938..5352be0 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/LoginActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt @@ -15,13 +15,17 @@ import androidx.test.filters.LargeTest import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.junit.After import org.junit.Before +import org.junit.FixMethodOrder import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") @RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) @LargeTest -class LoginActivityTest { +class `1-LoginActivityTest` : WithANRException() { @get:Rule val activityRule = ActivityScenarioRule(LoginActivity::class.java) @@ -40,7 +44,7 @@ class LoginActivityTest { } @Test - fun viewIsInitialized() { + fun `1-viewIsInitialized`() { onView(withId(R.id.urlView)).check(matches(isDisplayed())) onView(withId(R.id.selfSigned)) .check(matches(isDisplayed())) @@ -57,28 +61,28 @@ class LoginActivityTest { } @Test - fun urlError() { + fun `2-urlError`() { performLogin("10.0.2.2:8888") onView(withId(R.id.urlView)).perform(click()) onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem))) } @Test - fun connectError() { - performLogin("http://10.0.2.2:8889") - onView(withId(R.id.urlView)).perform(click()) - onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos))) - } - - @Test - fun urlSlashError() { + fun `3-urlSlashError`() { performLogin("https://google.fr/toto") onView(withId(R.id.urlView)).perform(click()) onView(withId(R.id.urlView)).check(matches(withError(R.string.login_url_problem))) } @Test - fun multiError() { + fun `4-connectError`() { + performLogin("http://10.0.2.2:8889") + onView(withId(R.id.urlView)).perform(click()) + onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos))) + } + + @Test + fun `5-multiError`() { onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click()) onView(withId(R.id.signInButton)).perform(click()) @@ -86,8 +90,9 @@ class LoginActivityTest { } @Test - fun connect() { + fun `6-connect`() { performLogin() onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) + onView(withText("OK")).perform(click()) } } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt similarity index 93% rename from androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt rename to androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt index 7330144..b5f40ea 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/HomeActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt @@ -15,21 +15,19 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import org.hamcrest.CoreMatchers.not -import org.junit.Before +import org.junit.FixMethodOrder import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") @RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) @LargeTest -class HomeActivityTest { +class `2-HomeActivityTest` : WithANRException() { @get:Rule - val activityRule = ActivityScenarioRule(LoginActivity::class.java) - - @Before - fun init() { - loginAndInitHome() - } + val activityRule = ActivityScenarioRule(HomeActivity::class.java) @Test fun testMenu() { diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt similarity index 90% rename from androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityTest.kt rename to androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt index 3dcc96b..d9a28e9 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt @@ -19,9 +19,11 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @LargeTest -class SettingsActivityTest { +@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") +class `3-SettingsActivityTest` : WithANRException() { @get:Rule - val activityRule = ActivityScenarioRule(LoginActivity::class.java) + val activityRule = ActivityScenarioRule(HomeActivity::class.java) + lateinit var context: Context @Before @@ -29,7 +31,6 @@ class SettingsActivityTest { activityRule.scenario.onActivity { activity -> context = activity.window.context } - loginAndInitHome() openMenu() onView(withText(R.string.title_activity_settings)).perform(click()) } @@ -68,6 +69,9 @@ class SettingsActivityTest { changeAndSaveSetting("", "10") { onView(withText(R.string.pref_api_timeout)).perform(click()) } + changeAndSaveSetting("", "60") { + onView(withText(R.string.pref_api_timeout)).perform(click()) + } } @Test diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityGeneralTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt similarity index 94% rename from androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityGeneralTest.kt rename to androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt index 7aa60df..f26bbc7 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityGeneralTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt @@ -22,19 +22,22 @@ import androidx.test.filters.LargeTest import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.Before +import org.junit.FixMethodOrder import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") @RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) @LargeTest -class SettingsActivityGeneralTest { +class `4-SettingsActivityGeneralTest` : WithANRException() { @get:Rule - val activityRule = ActivityScenarioRule(LoginActivity::class.java) + val activityRule = ActivityScenarioRule(HomeActivity::class.java) @Before fun init() { - loginAndInitHome() openActionBarOverflowOrOptionsMenu( ApplicationProvider.getApplicationContext(), ) diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityReaderTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt similarity index 83% rename from androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityReaderTest.kt rename to androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt index cf4ffb6..3cf615e 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityReaderTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt @@ -1,29 +1,32 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context -import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") @RunWith(AndroidJUnit4::class) @LargeTest -class SettingsActivityReaderTest { +class `5-SettingsActivityReaderTest` : WithANRException() { @get:Rule - val activityRule = ActivityScenarioRule(LoginActivity::class.java) + val activityRule = ActivityScenarioRule(SettingsActivity::class.java) lateinit var context: Context @@ -32,14 +35,14 @@ class SettingsActivityReaderTest { activityRule.scenario.onActivity { activity -> context = activity.window.context } - loginAndInitHome() - openActionBarOverflowOrOptionsMenu( - ApplicationProvider.getApplicationContext(), - ) - onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.pref_header_viewer)).perform(click()) } + @After + fun back() { + onView(isRoot()).perform(ViewActions.pressBack()) + } + @Test fun testReader() { onView(withSettingsCheckboxFrame(R.string.pref_switch_actions_pager_scroll)).check( diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityOfflineTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt similarity index 91% rename from androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityOfflineTest.kt rename to androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt index 729901c..e7a66ae 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SettingsActivityOfflineTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt @@ -1,31 +1,34 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context -import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isEnabled import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") @RunWith(AndroidJUnit4::class) @LargeTest -class SettingsActivityOfflineTest { +class `6-SettingsActivityOfflineTest` : WithANRException() { @get:Rule - val activityRule = ActivityScenarioRule(LoginActivity::class.java) + val activityRule = ActivityScenarioRule(SettingsActivity::class.java) lateinit var context: Context @@ -34,14 +37,14 @@ class SettingsActivityOfflineTest { activityRule.scenario.onActivity { activity -> context = activity.window.context } - loginAndInitHome() - openActionBarOverflowOrOptionsMenu( - ApplicationProvider.getApplicationContext(), - ) - onView(withText(R.string.title_activity_settings)).perform(click()) onView(withText(R.string.pref_header_offline)).perform(click()) } + @After + fun back() { + onView(isRoot()).perform(ViewActions.pressBack()) + } + @Suppress("detekt:LongMethod") @Test fun testOffline() { diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt similarity index 90% rename from androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt rename to androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt index 126a365..f751ab2 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/SourcesActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt @@ -21,11 +21,12 @@ import org.junit.Test import org.junit.runner.RunWith import java.util.UUID +@Suppress("ktlint:standard:class-naming", "detekt:ClassNaming") @RunWith(AndroidJUnit4::class) @LargeTest -class SourcesActivityTest { +class `7-SourcesActivityTest` : WithANRException() { @get:Rule - val activityRule = ActivityScenarioRule(LoginActivity::class.java) + val activityRule = ActivityScenarioRule(HomeActivity::class.java) lateinit var sourceName: String @@ -33,7 +34,6 @@ class SourcesActivityTest { fun init() { sourceName = UUID.randomUUID().toString().substring(0, 15) - loginAndInitHome() goToSources() } @@ -75,10 +75,4 @@ class SourcesActivityTest { onView(withId(android.R.id.button1)).perform(click()) onView(withText(sourceName)).check(doesNotExist()) } - - private fun goToSources() { - openMenu() - onView(withText(R.string.menu_home_sources)) - .perform(click()) - } } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt index 3e7187c..9f19d07 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -1,7 +1,12 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context +import android.graphics.Bitmap +import android.os.Environment.DIRECTORY_PICTURES +import android.os.Environment.getExternalStoragePublicDirectory +import android.util.Log import androidx.annotation.ArrayRes +import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onData import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click @@ -9,29 +14,37 @@ import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.base.DefaultFailureHandler import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isNotChecked import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf import org.hamcrest.Matchers.hasToString +import org.junit.BeforeClass +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.Locale + +// For now, do not move this as it is modified by the integration tests +val defaultUrl = "http://10.0.2.2:8888" fun performLogin(someUrl: String? = null) { + Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl}") onView(withId(R.id.urlView)).perform(click()).perform( typeTextIntoFocusedView( - if (!someUrl.isNullOrEmpty()) someUrl else "http://10.0.2.2:8888", + if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl, ), ) onView(withId(R.id.signInButton)).perform(click()) } -fun loginAndInitHome() { - performLogin() - onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) - onView(withText("OK")).perform(click()) -} - fun changeAndCancelSetting( oldValue: String, newValue: String, @@ -97,6 +110,12 @@ fun testPreferencesFromArray( } } +fun goToSources() { + openMenu() + onView(withText(R.string.menu_home_sources)) + .perform(click()) +} + fun testAddSourceWithUrl( url: String, sourceName: String, @@ -119,3 +138,88 @@ fun testAddSourceWithUrl( .perform(click()) onView(withText(sourceName)).check(matches(isDisplayed())) } + +@Suppress("detekt:UtilityClassWithPublicConstructor") +open class WithANRException { + companion object { + // Running count of the number of Android Not Responding dialogues to prevent endless dismissal. + private var anrCount = 0 + + // `RootViewWithoutFocusException` class is private, need to match the message (instead of using type matching). + private val rootViewWithoutFocusExceptionMsg = + java.lang.String.format( + Locale.ROOT, + "Waited for the root of the view hierarchy to have " + + "window focus and not request layout for 10 seconds. If you specified a non " + + "default root matcher, it may be picking a root that never takes focus. " + + "Root:", + ) + private val otherException = "System Ul isn't responding" + + private fun handleAnrDialogue() { + val device = UiDevice.getInstance(getInstrumentation()) + // If running the device in English Locale + val waitButton = device.findObject(UiSelector().textContains("wait")) + if (waitButton.exists()) waitButton.click() + } + + @JvmStatic + @BeforeClass + fun setUpHandler() { + Espresso.setFailureHandler { error, viewMatcher -> + + takeScreenshot() + if (error.message!!.contains(otherException)) { + handleAnrDialogue() + } else if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) && + anrCount < 20 + ) { + anrCount++ + handleAnrDialogue() + } else { // chain all failures down to the default espresso handler + Log.e("AMINE", "AMINE : ${error.message}") + println("AMINE : ${error.message}") + DefaultFailureHandler(getInstrumentation().targetContext).handle(error, viewMatcher) + } + } + } + } +} + +fun takeScreenshot() { + try { + val bitmap = getInstrumentation().uiAutomation.takeScreenshot() + + val folder = + File( + File( + getExternalStoragePublicDirectory(DIRECTORY_PICTURES), + "selfoss_tests", + ).absolutePath, + "screenshots", + ) + if (!folder.exists()) { + folder.mkdirs() + } + + var out: BufferedOutputStream? = null + val size = folder.list().size + 1 + try { + out = BufferedOutputStream(FileOutputStream(folder.path + "/" + size + ".png")) + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out) + Log.d("Screenshots", "Screenshot taken") + } catch (e: IOException) { + Log.e("Screenshots", "Could not save the screenshot", e) + } finally { + if (out != null) { + try { + out.close() + } catch (e: IOException) { + Log.e("Screenshots", "Could not save the screenshot", e) + } + } + } + } catch (ex: IOException) { + Log.e("Screenshots", "Could not take the screenshot", ex) + } +} diff --git a/androidApp/src/debug/AndroidManifest.xml b/androidApp/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..4c088f3 --- /dev/null +++ b/androidApp/src/debug/AndroidManifest.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties index 033e88c..720ad0a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,3 +27,4 @@ org.gradle.caching=true ignoreGitVersion=false kotlin.native.cacheKind.iosX64=none org.gradle.configureondemand=true +kotlin.jvm.target.validation.mode=IGNORE diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt index 45a2272..d373968 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt @@ -224,16 +224,25 @@ class Repository( appSettingsService.isItemCachingEnabled() || !appSettingsService.isUpdateSourcesEnabled() val shouldFetch = if (!appSettingsService.isUpdateSourcesEnabled()) !fetchedSources else true if (shouldFetch && connectivityService.isNetworkAvailable()) { - val apiSources = api.sourcesDetailed() - if (apiSources.success && apiSources.data != null) { - fetchedSources = true - sources = apiSources.data - if (isDatabaseEnabled) { - resetDBSourcesWithData(sources) - } - } + sources = sourceDetails(isDatabaseEnabled) } else if (isDatabaseEnabled) { sources = getDBSources().map { it.toView() } as ArrayList + if (sources.isEmpty() && !connectivityService.isNetworkAvailable() && !fetchedSources) { + sources = sourceDetails(isDatabaseEnabled) + } + } + return sources + } + + private suspend fun sourceDetails(isDatabaseEnabled: Boolean): ArrayList { + var sources = ArrayList() + val apiSources = api.sourcesDetailed() + if (apiSources.success && apiSources.data != null) { + fetchedSources = true + sources = apiSources.data + if (isDatabaseEnabled) { + resetDBSourcesWithData(sources) + } } return sources } @@ -371,6 +380,7 @@ class Repository( ): Boolean { var response = false if (connectivityService.isNetworkAvailable()) { + fetchedSources = false response = api .createSourceForVersion( title, @@ -392,6 +402,7 @@ class Repository( ): Boolean { var response = false if (connectivityService.isNetworkAvailable()) { + fetchedSources = false response = api.updateSourceForVersion(id, title, url, spout, tags).isSuccess == true } @@ -406,6 +417,7 @@ class Repository( if (connectivityService.isNetworkAvailable()) { val response = api.deleteSource(id) success = response.isSuccess + fetchedSources = false } // We filter on success or if the network isn't available -- 2.34.1 From 1b2e9edc8c8b55ecd9cc64fe70f0227fc6cbe219 Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 25 Mar 2025 00:13:30 +0100 Subject: [PATCH 4/7] chore: better handling of coroutine dispatchers. --- .../android/1-LoginActivityTest.kt | 1 - .../android/HomeActivity.kt | 101 ++++++++++-------- .../android/LoginActivity.kt | 58 +++++----- .../apps/readerforselfossv2/android/MyApp.kt | 2 +- .../android/ReaderActivity.kt | 14 ++- .../android/SourcesActivity.kt | 38 ++++--- .../android/UpsertSourceActivity.kt | 79 ++++++++------ .../repository/Repository.kt | 8 +- .../readerforselfossv2/rest/SelfossApi.kt | 2 +- .../service/ConnectivityService.kt | 2 +- 10 files changed, 171 insertions(+), 134 deletions(-) diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt index 5352be0..2dea4a4 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt @@ -77,7 +77,6 @@ class `1-LoginActivityTest` : WithANRException() { @Test fun `4-connectError`() { performLogin("http://10.0.2.2:8889") - onView(withId(R.id.urlView)).perform(click()) onView(withId(R.id.urlView)).check(matches(withError(R.string.wrong_infos))) } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index c31c857..77df999 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -104,7 +104,7 @@ class HomeActivity : if (appSettingsService.isItemCachingEnabled()) { CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { repository.tryToCacheItemsAndGetNewOnes() CountingIdlingResourceSingleton.decrement() } @@ -120,12 +120,8 @@ class HomeActivity : binding.swipeRefreshLayout.setOnRefreshListener { repository.offlineOverride = false lastFetchDone = false - CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { - getElementsAccordingToTab() - binding.swipeRefreshLayout.isRefreshing = false - CountingIdlingResourceSingleton.decrement() - } + getElementsAccordingToTab() + binding.swipeRefreshLayout.isRefreshing = false } val swipeDirs = @@ -289,7 +285,7 @@ class HomeActivity : handleRecurringTask() CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { repository.handleDBActions() CountingIdlingResourceSingleton.decrement() } @@ -463,8 +459,8 @@ class HomeActivity : itemType: ItemType, ) { CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { - binding.swipeRefreshLayout.isRefreshing = true + binding.swipeRefreshLayout.isRefreshing = true + CoroutineScope(Dispatchers.IO).launch { repository.displayedItems = itemType items = if (appendResults) { @@ -472,8 +468,12 @@ class HomeActivity : } else { repository.getNewerItems() } - binding.swipeRefreshLayout.isRefreshing = false - handleListResult() + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + binding.swipeRefreshLayout.isRefreshing = false + handleListResult() + CountingIdlingResourceSingleton.decrement() + } CountingIdlingResourceSingleton.decrement() } } @@ -613,22 +613,26 @@ class HomeActivity : needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { val updatedRemote = repository.updateRemote() - if (updatedRemote) { - Toast - .makeText( - this@HomeActivity, - R.string.refresh_success_response, - Toast.LENGTH_LONG, - ).show() - } else { - Toast - .makeText( - this@HomeActivity, - R.string.refresh_failer_message, - Toast.LENGTH_SHORT, - ).show() + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + if (updatedRemote) { + Toast + .makeText( + this@HomeActivity, + R.string.refresh_success_response, + Toast.LENGTH_LONG, + ).show() + } else { + Toast + .makeText( + this@HomeActivity, + R.string.refresh_failer_message, + Toast.LENGTH_SHORT, + ).show() + } + CountingIdlingResourceSingleton.decrement() } CountingIdlingResourceSingleton.decrement() } @@ -639,30 +643,33 @@ class HomeActivity : R.id.readAll -> { if (elementsShown == ItemType.UNREAD) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { - binding.swipeRefreshLayout.isRefreshing = true CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { + binding.swipeRefreshLayout.isRefreshing = true + CoroutineScope(Dispatchers.IO).launch { val success = repository.markAllAsRead(items) - if (success) { - Toast - .makeText( - this@HomeActivity, - R.string.all_posts_read, - Toast.LENGTH_SHORT, - ).show() - tabNewBadge.removeBadge() + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + if (success) { + Toast + .makeText( + this@HomeActivity, + R.string.all_posts_read, + Toast.LENGTH_SHORT, + ).show() + tabNewBadge.removeBadge() - getElementsAccordingToTab() - } else { - Toast - .makeText( - this@HomeActivity, - R.string.all_posts_not_read, - Toast.LENGTH_SHORT, - ).show() + getElementsAccordingToTab() + } else { + Toast + .makeText( + this@HomeActivity, + R.string.all_posts_not_read, + Toast.LENGTH_SHORT, + ).show() + } + binding.swipeRefreshLayout.isRefreshing = false + CountingIdlingResourceSingleton.decrement() } - handleListResult() - binding.swipeRefreshLayout.isRefreshing = false CountingIdlingResourceSingleton.decrement() } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index 7aa4a47..1564c63 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -108,7 +108,7 @@ class LoginActivity : private fun goToMain() { CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { repository.updateApiInformation() ACRA.errorReporter.putCustomData( "SELFOSS_API_VERSION", @@ -127,6 +127,9 @@ class LoginActivity : binding.urlView.error = getString(R.string.wrong_infos) binding.loginView.error = getString(R.string.wrong_infos) binding.passwordView.error = getString(R.string.wrong_infos) + binding.urlView.requestFocus() + + showProgress(false) } private fun attemptLogin() { @@ -160,34 +163,41 @@ class LoginActivity : repository.refreshLoginInformation(url, login, password) CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { try { repository.updateApiInformation() + val result = repository.login() + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + if (result) { + val errorFetching = repository.checkIfFetchFails() + if (!errorFetching) { + goToMain() + } else { + preferenceError() + } + } else { + preferenceError() + } + CountingIdlingResourceSingleton.decrement() + } } catch (e: Exception) { - if (e.message?.startsWith("No transformation found") == true) { - Toast - .makeText( - applicationContext, - R.string.application_selfoss_only, - Toast.LENGTH_LONG, - ).show() - preferenceError() - showProgress(false) + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + if (e.message?.startsWith("No transformation found") == true) { + Toast + .makeText( + applicationContext, + R.string.application_selfoss_only, + Toast.LENGTH_LONG, + ).show() + preferenceError() + } + CountingIdlingResourceSingleton.decrement() } + } finally { + CountingIdlingResourceSingleton.decrement() } - val result = repository.login() - if (result) { - val errorFetching = repository.checkIfFetchFails() - if (!errorFetching) { - goToMain() - } else { - preferenceError() - } - } else { - preferenceError() - } - showProgress(false) - CountingIdlingResourceSingleton.decrement() } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt index ee3b85c..b9bb9f2 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt @@ -73,7 +73,7 @@ class MyApp : ), ) - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.Default).launch { connectivityService.networkAvailableProvider.collect { networkAvailable -> val toastMessage = if (networkAvailable) { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt index 35677d2..650e6dc 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt @@ -27,7 +27,7 @@ class ReaderActivity : DIAware { private var currentItem: Int = 0 - private lateinit var toolbarMenu: Menu + private var toolbarMenu: Menu? = null private lateinit var binding: ActivityReaderBinding @@ -90,8 +90,10 @@ class ReaderActivity : } private fun updateStarIcon() { - val isStarred = allItems.getOrNull(currentItem)?.starred ?: false - toolbarMenu.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE) + if (toolbarMenu != null) { + val isStarred = allItems.getOrNull(currentItem)?.starred ?: false + toolbarMenu!!.findItem(R.id.star)?.icon?.setTint(if (isStarred) Color.RED else Color.WHITE) + } } override fun onSaveInstanceState(oldInstanceState: Bundle) { @@ -133,8 +135,10 @@ class ReaderActivity : private fun alignmentMenu() { val showJustify = appSettingsService.getActiveAllignment() == AppSettingsService.ALIGN_LEFT - toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify - toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify + if (toolbarMenu != null) { + toolbarMenu!!.findItem(R.id.align_left).isVisible = !showJustify + toolbarMenu!!.findItem(R.id.align_justify).isVisible = showJustify + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt index 70745e3..a2c8926 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt @@ -58,24 +58,28 @@ class SourcesActivity : binding.recyclerView.layoutManager = mLayoutManager CountingIdlingResourceSingleton.increment() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { val response = repository.getSourcesDetails() - if (response.isNotEmpty()) { - items = response - val mAdapter = - SourcesListAdapter( - this@SourcesActivity, - items, - ) - binding.recyclerView.adapter = mAdapter - mAdapter.notifyDataSetChanged() - } else { - Toast - .makeText( - this@SourcesActivity, - R.string.cant_get_sources, - Toast.LENGTH_SHORT, - ).show() + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + if (response.isNotEmpty()) { + items = response + val mAdapter = + SourcesListAdapter( + this@SourcesActivity, + items, + ) + binding.recyclerView.adapter = mAdapter + mAdapter.notifyDataSetChanged() + } else { + Toast + .makeText( + this@SourcesActivity, + R.string.cant_get_sources, + Toast.LENGTH_SHORT, + ).show() + } + CountingIdlingResourceSingleton.decrement() } CountingIdlingResourceSingleton.decrement() } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt index f952bdb..690eab6 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/UpsertSourceActivity.kt @@ -9,6 +9,7 @@ import android.widget.TextView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import bou.amine.apps.readerforselfossv2.android.databinding.ActivityUpsertSourceBinding +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.model.NetworkUnavailableException import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.repository.Repository @@ -108,36 +109,42 @@ class UpsertSourceActivity : binding.progress.visibility = View.GONE } - CoroutineScope(Dispatchers.Main).launch { + CountingIdlingResourceSingleton.increment() + CoroutineScope(Dispatchers.IO).launch { try { val items = repository.getSpouts() - if (items.isNotEmpty()) { - val itemsStrings = items.map { it.value.name } - for ((key, value) in items) { - spoutsKV[value.name] = key + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + if (items.isNotEmpty()) { + val itemsStrings = items.map { it.value.name } + for ((key, value) in items) { + spoutsKV[value.name] = key + } + + binding.progress.visibility = View.GONE + binding.formContainer.visibility = View.VISIBLE + + val spinnerArrayAdapter = + ArrayAdapter( + this@UpsertSourceActivity, + android.R.layout.simple_spinner_item, + itemsStrings, + ) + spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.spoutsSpinner.adapter = spinnerArrayAdapter + + if (existingSource != null) { + initFields(items) + } + } else { + handleSpoutFailure() } - - binding.progress.visibility = View.GONE - binding.formContainer.visibility = View.VISIBLE - - val spinnerArrayAdapter = - ArrayAdapter( - this@UpsertSourceActivity, - android.R.layout.simple_spinner_item, - itemsStrings, - ) - spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.spoutsSpinner.adapter = spinnerArrayAdapter - - if (existingSource != null) { - initFields(items) - } - } else { - handleSpoutFailure() + CountingIdlingResourceSingleton.decrement() } } catch (e: NetworkUnavailableException) { handleSpoutFailure(networkIssue = true) } + CountingIdlingResourceSingleton.decrement() } } @@ -160,7 +167,8 @@ class UpsertSourceActivity : } else -> { - CoroutineScope(Dispatchers.Main).launch { + CountingIdlingResourceSingleton.increment() + CoroutineScope(Dispatchers.IO).launch { val successfullyAddedSource = if (existingSource != null) { repository.updateSource( @@ -178,16 +186,21 @@ class UpsertSourceActivity : binding.tags.text.toString(), ) } - if (successfullyAddedSource) { - finish() - } else { - Toast - .makeText( - this@UpsertSourceActivity, - R.string.cant_create_source, - Toast.LENGTH_SHORT, - ).show() + CountingIdlingResourceSingleton.increment() + launch(Dispatchers.Main) { + if (successfullyAddedSource) { + finish() + } else { + Toast + .makeText( + this@UpsertSourceActivity, + R.string.cant_create_source, + Toast.LENGTH_SHORT, + ).show() + } + CountingIdlingResourceSingleton.decrement() } + CountingIdlingResourceSingleton.decrement() } } } diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt index d373968..753c7f7 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/Repository.kt @@ -334,7 +334,7 @@ class Repository( _badgeUnread.value -= 1 } - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.Default).launch { updateDBItem(item) } } @@ -345,7 +345,7 @@ class Repository( _badgeUnread.value += 1 } - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.Default).launch { updateDBItem(item) } } @@ -356,7 +356,7 @@ class Repository( _badgeStarred.value += 1 } - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.Default).launch { updateDBItem(item) } } @@ -367,7 +367,7 @@ class Repository( _badgeStarred.value -= 1 } - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.Default).launch { updateDBItem(item) } } diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt index 9bab495..ab212a6 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt @@ -82,7 +82,7 @@ class SelfossApi( } modifyRequest { Napier.i("Will modify", tag = "HttpSend") - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.IO).launch { Napier.i("Will login", tag = "HttpSend") login() Napier.i("Did login", tag = "HttpSend") diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ConnectivityService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ConnectivityService.kt index 4ade188..d83ae51 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ConnectivityService.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ConnectivityService.kt @@ -16,7 +16,7 @@ class ConnectivityService { fun start() { connectivity = Connectivity() connectivity.start() - CoroutineScope(Dispatchers.Main).launch { + CoroutineScope(Dispatchers.Default).launch { connectivity.statusUpdates.collect { status -> when (status) { is Connectivity.Status.Connected -> { -- 2.34.1 From 8a7743a6fbfcdd5c629bfbb69f1a45183ae87a87 Mon Sep 17 00:00:00 2001 From: Amine Date: Tue, 25 Mar 2025 13:36:35 +0100 Subject: [PATCH 5/7] ci: Instrumentation tests coverage in ci. --- .gitea/workflows/on_pr.yml | 10 +-- .../android/1-LoginActivityTest.kt | 1 + .../android/2-HomeActivityTest.kt | 11 +++ .../android/3-SettingsActivityTest.kt | 7 ++ .../android/4-SettingsActivityGeneralTest.kt | 5 ++ .../android/5-SettingsActivityReaderTest.kt | 5 ++ .../android/6-SettingsActivityOfflineTest.kt | 5 ++ .../android/7-SourcesActivityTest.kt | 5 ++ .../readerforselfossv2/android/CommonTests.kt | 6 ++ .../readerforselfossv2/android/Helpers.kt | 90 +++++++++++++++++++ .../android/LoginActivity.kt | 1 + .../android/SourcesActivity.kt | 2 +- .../android/adapters/SourcesListAdapter.kt | 5 ++ .../android/fragments/FilterSheetFragment.kt | 3 + .../android/settings/SettingsActivity.kt | 1 + 15 files changed, 148 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/on_pr.yml b/.gitea/workflows/on_pr.yml index 57a1b31..df46445 100644 --- a/.gitea/workflows/on_pr.yml +++ b/.gitea/workflows/on_pr.yml @@ -28,13 +28,6 @@ jobs: - name: run selfoss run: | docker compose -f .gitea/workflows/assets/docker-compose.yml up -d - # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 - - name: Kill crashpad_handler processes - if: always() - run: | - pkill -SIGTERM crashpad_handler || true - sleep 5 - pkill -SIGKILL crashpad_handler || true - name: Change url until I find a better way to do it run: | sed -i "s/val defaultUrl = \"http:\/\/10\.0\.2\.2\:8888\"/val defaultUrl = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -42,13 +35,14 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 + profile: pixel_2 script: | ./gradlew androidApp:clearScreenshotsTask || true ./gradlew androidApp:createScreenshotDirectory adb logcat -G 16M ./gradlew JacocoDebugCodeCoverage || true ./gradlew androidApp:fetchScreenshots - adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt + adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt - uses: actions/upload-artifact@v3 if: always() with: diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt index 2dea4a4..746d3bb 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/1-LoginActivityTest.kt @@ -93,5 +93,6 @@ class `1-LoginActivityTest` : WithANRException() { performLogin() onView(withText(R.string.gdpr_dialog_title)).check(matches(isDisplayed())) onView(withText("OK")).perform(click()) + checkHomeLoadingDone() } } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt index b5f40ea..14d468e 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/2-HomeActivityTest.kt @@ -1,6 +1,7 @@ package bou.amine.apps.readerforselfossv2.android import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -14,7 +15,9 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.hamcrest.CoreMatchers.not +import org.junit.Before import org.junit.FixMethodOrder import org.junit.Rule import org.junit.Test @@ -29,6 +32,14 @@ class `2-HomeActivityTest` : WithANRException() { @get:Rule val activityRule = ActivityScenarioRule(HomeActivity::class.java) + @Before + fun registerIdlingResource() { + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) + checkHomeLoadingDone() + } + @Test fun testMenu() { onView(withId(R.id.action_search)).check(matches(isDisplayed())).check( diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt index d9a28e9..d3e4464 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/3-SettingsActivityTest.kt @@ -2,14 +2,17 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.isSelected import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.Before @@ -31,6 +34,9 @@ class `3-SettingsActivityTest` : WithANRException() { activityRule.scenario.onActivity { activity -> context = activity.window.context } + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) openMenu() onView(withText(R.string.title_activity_settings)).perform(click()) } @@ -91,6 +97,7 @@ class `3-SettingsActivityTest` : WithANRException() { @Test fun testAbout() { onView(withText(R.string.action_about)).perform(click()) + onView(isRoot()).perform(waitUntilShown("ACRA", 30000)) onView(withText("ACRA")).check(matches(isDisplayed())) } } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt index f26bbc7..7953e93 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/4-SettingsActivityGeneralTest.kt @@ -3,6 +3,7 @@ package bou.amine.apps.readerforselfossv2.android import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.replaceText @@ -19,6 +20,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.Before @@ -38,6 +40,9 @@ class `4-SettingsActivityGeneralTest` : WithANRException() { @Before fun init() { + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) openActionBarOverflowOrOptionsMenu( ApplicationProvider.getApplicationContext(), ) diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt index 3cf615e..0b20fff 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/5-SettingsActivityReaderTest.kt @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -13,6 +14,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.After @@ -35,6 +37,9 @@ class `5-SettingsActivityReaderTest` : WithANRException() { activityRule.scenario.onActivity { activity -> context = activity.window.context } + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) onView(withText(R.string.pref_header_viewer)).perform(click()) } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt index e7a66ae..e785bc3 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/6-SettingsActivityOfflineTest.kt @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android import android.content.Context import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches @@ -15,6 +16,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not import org.junit.After @@ -37,6 +39,9 @@ class `6-SettingsActivityOfflineTest` : WithANRException() { activityRule.scenario.onActivity { activity -> context = activity.window.context } + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) onView(withText(R.string.pref_header_offline)).perform(click()) } diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt index f751ab2..5adbd7c 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/7-SourcesActivityTest.kt @@ -2,6 +2,7 @@ package bou.amine.apps.readerforselfossv2.android import androidx.test.espresso.AmbiguousViewMatcherException import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.swipeDown @@ -14,6 +15,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import org.junit.After import org.junit.Before import org.junit.Rule @@ -32,6 +34,9 @@ class `7-SourcesActivityTest` : WithANRException() { @Before fun init() { + IdlingRegistry + .getInstance() + .register(CountingIdlingResourceSingleton.countingIdlingResource) sourceName = UUID.randomUUID().toString().substring(0, 15) goToSources() diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt index 9f19d07..bd2a635 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -15,6 +15,7 @@ import androidx.test.espresso.action.ViewActions.typeTextIntoFocusedView import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.base.DefaultFailureHandler +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isNotChecked @@ -24,6 +25,7 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.not import org.hamcrest.Matchers.hasToString import org.junit.BeforeClass import java.io.BufferedOutputStream @@ -139,6 +141,10 @@ fun testAddSourceWithUrl( onView(withText(sourceName)).check(matches(isDisplayed())) } +fun checkHomeLoadingDone() { + onView(withId(R.id.swipeRefreshLayout)).inRoot(not(isDialog())).perform(waitForRecyclerViewToStopLoading(300000)) +} + @Suppress("detekt:UtilityClassWithPublicConstructor") open class WithANRException { companion object { diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/Helpers.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/Helpers.kt index 207afcf..b6d57b5 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/Helpers.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/Helpers.kt @@ -8,22 +8,32 @@ import android.widget.RelativeLayout import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.graphics.drawable.toBitmap +import androidx.core.view.isVisible +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.PerformException import androidx.test.espresso.Root +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction import androidx.test.espresso.matcher.RootMatchers.isPlatformPopup import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withClassName import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withResourceName import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.util.HumanReadables +import androidx.test.espresso.util.TreeIterables import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.any import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.Matchers import org.hamcrest.TypeSafeMatcher +import java.util.concurrent.TimeoutException fun withError( @StringRes id: Int, @@ -44,6 +54,86 @@ fun withError( } } +fun waitUntilShown( + viewText: String, + millis: Long, +): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher = isRoot() + + override fun getDescription(): String = "wait for $millis millis, for a specific view with text <$viewText> to be visible." + + override fun perform( + uiController: UiController, + view: View, + ) { + uiController.loopMainThreadUntilIdle() + val startTime = System.currentTimeMillis() + val endTime = startTime + millis + val viewMatcher = withText(viewText) + + do { + for (child in TreeIterables.breadthFirstViewTraversal(view)) { + if (viewMatcher.matches(child) && child.isShown) { + return + } + } + + uiController.loopMainThreadForAtLeast(100) + } while (System.currentTimeMillis() < endTime) + + // timeout happens + throw PerformException + .Builder() + .withActionDescription(this.description) + .withViewDescription(HumanReadables.describe(view)) + .withCause(TimeoutException()) + .build() + } + } +} + +fun waitForRecyclerViewToStopLoading(millis: Long): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher = any(View::class.java) + + override fun getDescription(): String = "wait for $millis millis for the recyclerview to stop loading." + + override fun perform( + uiController: UiController, + view: View?, + ) { + uiController.loopMainThreadUntilIdle() + val startTime = System.currentTimeMillis() + val endTime = startTime + millis + + do { + // either the empty view is displayed + for (child in TreeIterables.breadthFirstViewTraversal(view)) { + // found view with required ID + if (withId(R.id.emptyText).matches(child) && child.isVisible) { + return + } + } + + // or the refresh layout is refreshing + if (view is SwipeRefreshLayout && !view.isRefreshing) { + return + } + uiController.loopMainThreadForAtLeast(100) + } while (System.currentTimeMillis() < endTime) + + // timeout happens + throw PerformException + .Builder() + .withActionDescription(this.description) + .withViewDescription(HumanReadables.describe(view)) + .withCause(TimeoutException()) + .build() + } + } +} + fun isPopupWindow(): Matcher = isPlatformPopup() fun withDrawable( diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index 1564c63..72fb695 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -310,6 +310,7 @@ class LoginActivity : .withAboutSpecial2Description(AppSettingsService.BUG_URL) .withAboutSpecial1("Project Page") .withAboutSpecial1Description(AppSettingsService.SOURCE_URL) + .withShowLoadingProgress(false) .start(this) true } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt index a2c8926..33def0a 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt @@ -50,6 +50,7 @@ class SourcesActivity : override fun onResume() { super.onResume() + CountingIdlingResourceSingleton.increment() val mLayoutManager = LinearLayoutManager(this) var items: ArrayList @@ -57,7 +58,6 @@ class SourcesActivity : binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = mLayoutManager - CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.IO).launch { val response = repository.getSourcesDetails() CountingIdlingResourceSingleton.increment() diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt index 03c4b7e..95ee1ed 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt @@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.UpsertSourceActivity import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.utils.glide.circularDrawable import bou.amine.apps.readerforselfossv2.model.SelfossModel import bou.amine.apps.readerforselfossv2.repository.Repository @@ -104,8 +105,10 @@ class SourcesListAdapter( source: SelfossModel.SourceDetail, position: Int, ) { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.IO).launch { val successfullyDeletedSource = repository.deleteSource(source.id, source.title) + CountingIdlingResourceSingleton.increment() launch(Dispatchers.Main) { if (successfullyDeletedSource) { items.removeAt(position) @@ -119,7 +122,9 @@ class SourcesListAdapter( Toast.LENGTH_SHORT, ).show() } + CountingIdlingResourceSingleton.decrement() } + CountingIdlingResourceSingleton.decrement() } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt index f208f4b..35fd7c7 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/FilterSheetFragment.kt @@ -14,6 +14,7 @@ import android.view.ViewGroup import bou.amine.apps.readerforselfossv2.android.HomeActivity import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.databinding.FilterFragmentBinding +import bou.amine.apps.readerforselfossv2.android.testing.CountingIdlingResourceSingleton import bou.amine.apps.readerforselfossv2.android.utils.acra.sendSilentlyWithAcraWithName import bou.amine.apps.readerforselfossv2.android.utils.glide.imageIntoViewTarget import bou.amine.apps.readerforselfossv2.android.utils.maybeIfContext @@ -59,12 +60,14 @@ class FilterSheetFragment : ) try { + CountingIdlingResourceSingleton.increment() CoroutineScope(Dispatchers.Main).launch { handleTagChips() handleSourceChips() binding.progressBar2.visibility = GONE binding.filterView.visibility = VISIBLE + CountingIdlingResourceSingleton.decrement() } } catch (e: IllegalStateException) { dismiss() diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt index 7d2fb3f..4a2a25b 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt @@ -124,6 +124,7 @@ class SettingsActivity : LibsBuilder() .withAboutIconShown(true) .withAboutVersionShown(true) + .withShowLoadingProgress(false) .start(it) } true -- 2.34.1 From 7374e95b0e0cf6dfc40c1f00397db979b32597b1 Mon Sep 17 00:00:00 2001 From: Amine Date: Wed, 26 Mar 2025 20:20:19 +0100 Subject: [PATCH 6/7] ci: Instrumentation tests coverage in ci. --- .gitea/workflows/common_build.yml | 18 +- .gitea/workflows/common_coverage.yml | 65 ------ .gitea/workflows/on_merge_on_release.yml | 1 - .gitea/workflows/on_pr.yml | 201 ++++++++++-------- .../readerforselfossv2/android/CommonTests.kt | 11 +- .../android/LoginActivity.kt | 1 + .../readerforselfossv2/rest/SelfossApi.kt | 1 + 7 files changed, 120 insertions(+), 178 deletions(-) delete mode 100644 .gitea/workflows/common_coverage.yml diff --git a/.gitea/workflows/common_build.yml b/.gitea/workflows/common_build.yml index 89cc0b5..2959684 100644 --- a/.gitea/workflows/common_build.yml +++ b/.gitea/workflows/common_build.yml @@ -16,6 +16,9 @@ jobs: with: files: | androidApp/src/** + shared/src/commonMain/** + shared/src/androidMain/** + shared/src/commonTest/** - name: Fetch tags if: steps.check-android-changes.outputs.any_modified == 'true' run: git fetch --tags -p @@ -24,7 +27,6 @@ jobs: with: distribution: 'temurin' java-version: '17' - cache: gradle - uses: gradle/actions/setup-gradle@v3 if: steps.check-android-changes.outputs.any_modified == 'true' - uses: android-actions/setup-android@v3 @@ -34,14 +36,7 @@ jobs: run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties - name: Build and test if: steps.check-android-changes.outputs.any_modified == 'true' - run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest # These tests will be done - # TESTS ARE RUN LOCALLY - # - uses: KengoTODA/actions-setup-docker-compose@v1 - # with: - # version: "2.23.3" - # - name: run selfoss - # run: | - # docker compose -f .gitea/workflows/assets/docker-compose.yml up -d + run: ./gradlew build -x testReleaseUnitTest -x testDebugUnitTest -x testGithubConfigReleaseUnitTest -x testGithubConfigDebugUnitTest - name: coverage if: steps.check-android-changes.outputs.any_modified == 'true' run: | @@ -54,8 +49,3 @@ jobs: retention-days: 1 overwrite: true include-hidden-files: true -# TESTS ARE RUN LOCALLY -# - name: Clean -# if: always() -# run: | -# docker compose -f .gitea/workflows/assets/docker-compose.yml stop diff --git a/.gitea/workflows/common_coverage.yml b/.gitea/workflows/common_coverage.yml deleted file mode 100644 index c74481c..0000000 --- a/.gitea/workflows/common_coverage.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Coverage -on: - workflow_call: - -jobs: - BuildAndTestAndCoverage: - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Fetch tags - run: git fetch --tags -p - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '17' - cache: gradle - - uses: gradle/actions/setup-gradle@v3 - - uses: android-actions/setup-android@v3 - - name: Configure gradle... - run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties - - uses: KengoTODA/actions-setup-docker-compose@v1 - with: - version: "2.23.3" - - name: run selfoss - run: | - docker compose -f .gitea/workflows/assets/docker-compose.yml up -d - - name: Set env url - run: | - export SELFOSS_URL=172.17.0.1:8888 - # https://github.com/ReactiveCircus/android-emulator-runner/issues/385 - - name: Kill crashpad_handler processes - if: always() - run: | - pkill -SIGTERM crashpad_handler || true - sleep 5 - pkill -SIGKILL crashpad_handler || true - - name: Tests - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 29 - script: | - ./gradlew androidApp:connectedAndroidTest - killall -INT crashpad_handler || true - - uses: actions/upload-artifact@v3 - if: failure() - with: - name: failure-espresso - path: build/reports/androidTests/connected/screenshots - retention-days: 2 - overwrite: true - include-hidden-files: true - - uses: actions/upload-artifact@v3 - with: - name: coverage-espresso - path: build/reports/coverage/androidTest/githubConfig/debug/connected - retention-days: 1 - overwrite: true - include-hidden-files: true - - name: Clean - if: always() - run: | - docker compose -f .gitea/workflows/assets/docker-compose.yml stop diff --git a/.gitea/workflows/on_merge_on_release.yml b/.gitea/workflows/on_merge_on_release.yml index cbd71d6..2ec912f 100644 --- a/.gitea/workflows/on_merge_on_release.yml +++ b/.gitea/workflows/on_merge_on_release.yml @@ -86,7 +86,6 @@ jobs: with: distribution: 'temurin' java-version: '17' - cache: gradle - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Configure gradle... diff --git a/.gitea/workflows/on_pr.yml b/.gitea/workflows/on_pr.yml index df46445..7935296 100644 --- a/.gitea/workflows/on_pr.yml +++ b/.gitea/workflows/on_pr.yml @@ -5,33 +5,134 @@ on: - master jobs: - BuildAndTestAndCoverage: + Lint: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Install klint + run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ + - name: Install detekt + run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip + - name: Linting... + run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' + - name: Detecting... + run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' + translations: runs-on: ubuntu-latest steps: - name: Check out repository code uses: actions/checkout@v4 with: fetch-depth: 0 + - name: "Check translations changes" + id: check-translations-changes + uses: tj-actions/changed-files@v45 + with: + files: | + androidApp/src/main/res/values/strings.xml + - name: upload translation sources + if: steps.check-api-changes.outputs.any_modified == 'true' + uses: crowdin/github-action@v2 + with: + config: './.gitea/workflows/assets/crowdin.yml' + upload_sources: true + upload_translations: false + download_translations: false + create_pull_request: false + push_translations: false + env: + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + - name: wait + if: steps.check-api-changes.outputs.any_modified == 'true' + run: sleep 10s + - name: download translations + if: steps.check-api-changes.outputs.any_modified == 'true' + uses: crowdin/github-action@v2 + with: + config: './.gitea/workflows/assets/crowdin.yml' + upload_sources: false + upload_translations: false + download_translations: true + create_pull_request: false + push_translations: false + env: + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + - name: Check for uncommitted changes + if: steps.check-api-changes.outputs.any_modified == 'true' + id: check-changes + uses: mskri/check-uncommitted-changes-action@v1.0.1 + - name: Commit Changes + if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' + run: | + git config --global user.email aminecmi+giteadrone@pm.me + git config --global user.name giteadrone + git add ./androidApp/src/main/res/* + git commit -m "translation: translation files" + - name: Push changes + if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' + uses: appleboy/git-push-action@v1.0.0 + with: + author_name: giteadrone + author_email: aminecmi+giteadrone@pm.me + remote: ${{ secrets.REMOTE_URL }} + ssh_key: ${{ secrets.PRIVATE_KEY }} + branch: ${{ github.head_ref || github.ref_name }} + build: + needs: Lint + uses: ./.gitea/workflows/common_build.yml + RunIntegrationTests: + needs: Lint + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: "Check android app changes" + id: check-android-changes + uses: tj-actions/changed-files@v45 + with: + files: | + androidApp/src/** + shared/src/commonMain/** + shared/src/androidMain/** + shared/src/commonTest/** - name: Fetch tags + if: steps.check-android-changes.outputs.any_modified == 'true' run: git fetch --tags -p - uses: actions/setup-java@v4 + if: steps.check-android-changes.outputs.any_modified == 'true' with: distribution: 'temurin' java-version: '17' - uses: gradle/actions/setup-gradle@v3 + if: steps.check-android-changes.outputs.any_modified == 'true' - uses: android-actions/setup-android@v3 + if: steps.check-android-changes.outputs.any_modified == 'true' - name: Configure gradle... + if: steps.check-android-changes.outputs.any_modified == 'true' run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties - uses: KengoTODA/actions-setup-docker-compose@v1 + if: steps.check-android-changes.outputs.any_modified == 'true' with: version: "2.23.3" - name: run selfoss + if: steps.check-android-changes.outputs.any_modified == 'true' run: | docker compose -f .gitea/workflows/assets/docker-compose.yml up -d - name: Change url until I find a better way to do it + if: steps.check-android-changes.outputs.any_modified == 'true' run: | - sed -i "s/val defaultUrl = \"http:\/\/10\.0\.2\.2\:8888\"/val defaultUrl = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt + sed -i "s/const DEFAULT_URL = \"http:\/\/10\.0\.2\.2\:8888\"/const DEFAULT_URL = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt - name: Tests + if: steps.check-android-changes.outputs.any_modified == 'true' uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 @@ -40,11 +141,9 @@ jobs: ./gradlew androidApp:clearScreenshotsTask || true ./gradlew androidApp:createScreenshotDirectory adb logcat -G 16M - ./gradlew JacocoDebugCodeCoverage || true - ./gradlew androidApp:fetchScreenshots - adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt + ./gradlew JacocoDebugCodeCoverage || (./gradlew androidApp:fetchScreenshots && adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt) - uses: actions/upload-artifact@v3 - if: always() + if: steps.check-android-changes.outputs.any_modified == 'true' with: name: screenshot-espresso path: androidApp/build/reports/androidTests/connected/screenshots @@ -52,14 +151,14 @@ jobs: overwrite: true include-hidden-files: true - uses: actions/upload-artifact@v3 - if: always() + if: steps.check-android-changes.outputs.any_modified == 'true' with: - name: result-espresso path: androidApp/build/reports/androidTests/connected/debug/flavors/githubConfig retention-days: 1 overwrite: true include-hidden-files: true - uses: actions/upload-artifact@v3 + if: steps.check-android-changes.outputs.any_modified == 'true' with: name: coverage-espresso path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage @@ -67,90 +166,6 @@ jobs: overwrite: true include-hidden-files: true - name: Clean - if: always() + if: steps.check-android-changes.outputs.any_modified == 'true' || failure() run: | docker compose -f .gitea/workflows/assets/docker-compose.yml stop - -# Lint: -# runs-on: ubuntu-latest -# steps: -# - name: Check out repository code -# uses: actions/checkout@v4 -# - uses: actions/setup-java@v4 -# with: -# distribution: 'temurin' -# java-version: '17' -# cache: gradle -# - name: Install klint -# run: curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.5.0/ktlint && chmod a+x ktlint && mv ktlint /usr/local/bin/ -# - name: Install detekt -# run: curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7.zip && unzip detekt-cli-1.23.7.zip -# - name: Linting... -# run: ktlint 'shared/**/*.kt' 'androidApp/**/*.kt' '!shared/build' -# - name: Detecting... -# run: ./detekt-cli-1.23.7/bin/detekt-cli -c detekt.yml --excludes '**/shared/build/**/*.kt' -# translations: -# runs-on: ubuntu-latest -# steps: -# - name: Check out repository code -# uses: actions/checkout@v4 -# with: -# fetch-depth: 0 -# - name: "Check translations changes" -# id: check-translations-changes -# uses: tj-actions/changed-files@v45 -# with: -# files: | -# androidApp/src/main/res/values/strings.xml -# - name: upload translation sources -# if: steps.check-api-changes.outputs.any_modified == 'true' -# uses: crowdin/github-action@v2 -# with: -# config: './.gitea/workflows/assets/crowdin.yml' -# upload_sources: true -# upload_translations: false -# download_translations: false -# create_pull_request: false -# push_translations: false -# env: -# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} -# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} -# - name: wait -# if: steps.check-api-changes.outputs.any_modified == 'true' -# run: sleep 10s -# - name: download translations -# if: steps.check-api-changes.outputs.any_modified == 'true' -# uses: crowdin/github-action@v2 -# with: -# config: './.gitea/workflows/assets/crowdin.yml' -# upload_sources: false -# upload_translations: false -# download_translations: true -# create_pull_request: false -# push_translations: false -# env: -# CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} -# CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} -# - name: Check for uncommitted changes -# if: steps.check-api-changes.outputs.any_modified == 'true' -# id: check-changes -# uses: mskri/check-uncommitted-changes-action@v1.0.1 -# - name: Commit Changes -# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' -# run: | -# git config --global user.email aminecmi+giteadrone@pm.me -# git config --global user.name giteadrone -# git add ./androidApp/src/main/res/* -# git commit -m "translation: translation files" -# - name: Push changes -# if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' -# uses: appleboy/git-push-action@v1.0.0 -# with: -# author_name: giteadrone -# author_email: aminecmi+giteadrone@pm.me -# remote: ${{ secrets.REMOTE_URL }} -# ssh_key: ${{ secrets.PRIVATE_KEY }} -# branch: ${{ github.head_ref || github.ref_name }} -# build: -# needs: Lint -# uses: ./.gitea/workflows/common_build.yml diff --git a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt index bd2a635..a1c9af9 100644 --- a/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt +++ b/androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt @@ -35,13 +35,13 @@ import java.io.IOException import java.util.Locale // For now, do not move this as it is modified by the integration tests -val defaultUrl = "http://10.0.2.2:8888" +const val DEFAULT_URL = "http://10.0.2.2:8888" fun performLogin(someUrl: String? = null) { - Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl}") + Log.i("AUTOMATION", "The url used will be ${if (!someUrl.isNullOrEmpty()) someUrl else DEFAULT_URL}") onView(withId(R.id.urlView)).perform(click()).perform( typeTextIntoFocusedView( - if (!someUrl.isNullOrEmpty()) someUrl else defaultUrl, + if (!someUrl.isNullOrEmpty()) someUrl else DEFAULT_URL, ), ) onView(withId(R.id.signInButton)).perform(click()) @@ -160,7 +160,7 @@ open class WithANRException { "default root matcher, it may be picking a root that never takes focus. " + "Root:", ) - private val otherException = "System Ul isn't responding" + private const val OTHER_EXCEPTION = "System Ul isn't responding" private fun handleAnrDialogue() { val device = UiDevice.getInstance(getInstrumentation()) @@ -175,7 +175,7 @@ open class WithANRException { Espresso.setFailureHandler { error, viewMatcher -> takeScreenshot() - if (error.message!!.contains(otherException)) { + if (error.message!!.contains(OTHER_EXCEPTION)) { handleAnrDialogue() } else if (error.message!!.contains(rootViewWithoutFocusExceptionMsg) && anrCount < 20 @@ -192,6 +192,7 @@ open class WithANRException { } } +@Suppress("detekt:NestedBlockDepth") fun takeScreenshot() { try { val bitmap = getInstrumentation().uiAutomation.takeScreenshot() diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index 72fb695..c3c9127 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -132,6 +132,7 @@ class LoginActivity : showProgress(false) } + @Suppress("detekt:LongMethod") private fun attemptLogin() { // Reset errors. binding.urlView.error = null diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt index ab212a6..d7f3bc0 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt @@ -32,6 +32,7 @@ import io.ktor.utils.io.charsets.Charsets import io.ktor.utils.io.core.toByteArray import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO import kotlinx.coroutines.launch import kotlinx.serialization.json.Json -- 2.34.1 From 11c0e744dc80b2a66eff1ff47a463cb1f8d47f1f Mon Sep 17 00:00:00 2001 From: Amine Date: Thu, 27 Mar 2025 20:41:52 +0100 Subject: [PATCH 7/7] ci: Instrumentation tests coverage in ci. --- .../{common_build.yml => on_called_build.yml} | 3 +- .gitea/workflows/on_merge_on_release.yml | 4 +- .gitea/workflows/on_pr.yml | 100 ++---------------- .gitea/workflows/on_pr_test.yml | 67 ++++++++++++ .gitea/workflows/on_push.yml | 4 +- 5 files changed, 81 insertions(+), 97 deletions(-) rename .gitea/workflows/{common_build.yml => on_called_build.yml} (96%) create mode 100644 .gitea/workflows/on_pr_test.yml diff --git a/.gitea/workflows/common_build.yml b/.gitea/workflows/on_called_build.yml similarity index 96% rename from .gitea/workflows/common_build.yml rename to .gitea/workflows/on_called_build.yml index 2959684..5ae78c3 100644 --- a/.gitea/workflows/common_build.yml +++ b/.gitea/workflows/on_called_build.yml @@ -6,8 +6,7 @@ jobs: BuildAndTestAndCoverage: runs-on: ubuntu-latest steps: - - name: Check out repository code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: "Check android app changes" diff --git a/.gitea/workflows/on_merge_on_release.yml b/.gitea/workflows/on_merge_on_release.yml index 2ec912f..f97a6e3 100644 --- a/.gitea/workflows/on_merge_on_release.yml +++ b/.gitea/workflows/on_merge_on_release.yml @@ -1,4 +1,4 @@ -name: Create tag +name: Realease on: push: branches: @@ -7,7 +7,7 @@ on: jobs: build: - uses: ./.gitea/workflows/common_build.yml + uses: ./.gitea/workflows/on_called_build.yml createTagAndChangelog: runs-on: ubuntu-latest needs: build diff --git a/.gitea/workflows/on_pr.yml b/.gitea/workflows/on_pr.yml index 7935296..ea033f4 100644 --- a/.gitea/workflows/on_pr.yml +++ b/.gitea/workflows/on_pr.yml @@ -1,11 +1,11 @@ -name: Check PR code +name: PR on: pull_request: branches: - master jobs: - Lint: + PR: runs-on: ubuntu-latest steps: - name: Check out repository code @@ -36,7 +36,7 @@ jobs: files: | androidApp/src/main/res/values/strings.xml - name: upload translation sources - if: steps.check-api-changes.outputs.any_modified == 'true' + if: steps.check-translations-changes.outputs.any_modified == 'true' uses: crowdin/github-action@v2 with: config: './.gitea/workflows/assets/crowdin.yml' @@ -49,10 +49,10 @@ jobs: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - name: wait - if: steps.check-api-changes.outputs.any_modified == 'true' + if: steps.check-translations-changes.outputs.any_modified == 'true' run: sleep 10s - name: download translations - if: steps.check-api-changes.outputs.any_modified == 'true' + if: steps.check-translations-changes.outputs.any_modified == 'true' uses: crowdin/github-action@v2 with: config: './.gitea/workflows/assets/crowdin.yml' @@ -65,18 +65,18 @@ jobs: CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} - name: Check for uncommitted changes - if: steps.check-api-changes.outputs.any_modified == 'true' + if: steps.check-translations-changes.outputs.any_modified == 'true' id: check-changes uses: mskri/check-uncommitted-changes-action@v1.0.1 - name: Commit Changes - if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' + if: steps.check-translations-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' run: | git config --global user.email aminecmi+giteadrone@pm.me git config --global user.name giteadrone git add ./androidApp/src/main/res/* git commit -m "translation: translation files" - name: Push changes - if: steps.check-api-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' + if: steps.check-translations-changes.outputs.any_modified == 'true' && steps.check-changes.outputs.changes != '' uses: appleboy/git-push-action@v1.0.0 with: author_name: giteadrone @@ -86,86 +86,4 @@ jobs: branch: ${{ github.head_ref || github.ref_name }} build: needs: Lint - uses: ./.gitea/workflows/common_build.yml - RunIntegrationTests: - needs: Lint - runs-on: ubuntu-latest - steps: - - name: Check out repository code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: "Check android app changes" - id: check-android-changes - uses: tj-actions/changed-files@v45 - with: - files: | - androidApp/src/** - shared/src/commonMain/** - shared/src/androidMain/** - shared/src/commonTest/** - - name: Fetch tags - if: steps.check-android-changes.outputs.any_modified == 'true' - run: git fetch --tags -p - - uses: actions/setup-java@v4 - if: steps.check-android-changes.outputs.any_modified == 'true' - with: - distribution: 'temurin' - java-version: '17' - - uses: gradle/actions/setup-gradle@v3 - if: steps.check-android-changes.outputs.any_modified == 'true' - - uses: android-actions/setup-android@v3 - if: steps.check-android-changes.outputs.any_modified == 'true' - - name: Configure gradle... - if: steps.check-android-changes.outputs.any_modified == 'true' - run: mkdir -p ~/.gradle && echo "ignoreGitVersion=true" >> ~/.gradle/gradle.properties - - uses: KengoTODA/actions-setup-docker-compose@v1 - if: steps.check-android-changes.outputs.any_modified == 'true' - with: - version: "2.23.3" - - name: run selfoss - if: steps.check-android-changes.outputs.any_modified == 'true' - run: | - docker compose -f .gitea/workflows/assets/docker-compose.yml up -d - - name: Change url until I find a better way to do it - if: steps.check-android-changes.outputs.any_modified == 'true' - run: | - sed -i "s/const DEFAULT_URL = \"http:\/\/10\.0\.2\.2\:8888\"/const DEFAULT_URL = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt - - name: Tests - if: steps.check-android-changes.outputs.any_modified == 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: 29 - profile: pixel_2 - script: | - ./gradlew androidApp:clearScreenshotsTask || true - ./gradlew androidApp:createScreenshotDirectory - adb logcat -G 16M - ./gradlew JacocoDebugCodeCoverage || (./gradlew androidApp:fetchScreenshots && adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt) - - uses: actions/upload-artifact@v3 - if: steps.check-android-changes.outputs.any_modified == 'true' - with: - name: screenshot-espresso - path: androidApp/build/reports/androidTests/connected/screenshots - retention-days: 2 - overwrite: true - include-hidden-files: true - - uses: actions/upload-artifact@v3 - if: steps.check-android-changes.outputs.any_modified == 'true' - with: - path: androidApp/build/reports/androidTests/connected/debug/flavors/githubConfig - retention-days: 1 - overwrite: true - include-hidden-files: true - - uses: actions/upload-artifact@v3 - if: steps.check-android-changes.outputs.any_modified == 'true' - with: - name: coverage-espresso - path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage - retention-days: 1 - overwrite: true - include-hidden-files: true - - name: Clean - if: steps.check-android-changes.outputs.any_modified == 'true' || failure() - run: | - docker compose -f .gitea/workflows/assets/docker-compose.yml stop + uses: ./.gitea/workflows/on_called_build.yml diff --git a/.gitea/workflows/on_pr_test.yml b/.gitea/workflows/on_pr_test.yml new file mode 100644 index 0000000..348ae4d --- /dev/null +++ b/.gitea/workflows/on_pr_test.yml @@ -0,0 +1,67 @@ +name: PR test +on: + pull_request: + branches: + - master + +jobs: + integrationTests: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch tags + run: git fetch --tags -p + - uses: KengoTODA/actions-setup-docker-compose@v1 + with: + version: "2.23.3" + - name: run selfoss + run: | + docker compose -f .gitea/workflows/assets/docker-compose.yml up -d + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - uses: gradle/actions/setup-gradle@v3 + - uses: android-actions/setup-android@v3 + - name: Configure gradle... + run: mkdir -p ~/.gradle && echo "org.gradle.daemon=false\nignoreGitVersion=true" >> ~/.gradle/gradle.properties + - name: Change url until I find a better way to do it + run: | + sed -i "s/const val DEFAULT_URL = \"http:\/\/10\.0\.2\.2\:8888\"/const val DEFAULT_URL = \"http:\/\/172\.17\.0\.1\:8888\"/g" ./androidApp/src/androidTest/kotlin/bou/amine/apps/readerforselfossv2/android/CommonTests.kt + - name: Tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + profile: pixel_2 + script: | + ./gradlew androidApp:clearScreenshotsTask || true + ./gradlew androidApp:createScreenshotDirectory + adb logcat -G 16M + ./gradlew JacocoDebugCodeCoverage || (./gradlew androidApp:fetchScreenshots && adb logcat 'InputReader:S' 'chatty:S' 'audio_hw_generic:S' 'LogApiCalls:D' '*:I' -d > ./androidApp/build/reports/androidTests/connected/screenshots/logs.txt) + - uses: actions/upload-artifact@v3 + with: + name: screenshot-espresso + path: androidApp/build/reports/androidTests/connected/screenshots + retention-days: 2 + overwrite: true + include-hidden-files: true + - uses: actions/upload-artifact@v3 + with: + path: androidApp/build/reports/androidTests/connected/debug/flavors/githubConfig + retention-days: 1 + overwrite: true + include-hidden-files: true + - uses: actions/upload-artifact@v3 + with: + name: coverage-espresso + path: androidApp/build/reports/jacoco/JacocoDebugCodeCoverage + retention-days: 1 + overwrite: true + include-hidden-files: true + - name: Clean + if: always() + run: | + docker compose -f .gitea/workflows/assets/docker-compose.yml stop diff --git a/.gitea/workflows/on_push.yml b/.gitea/workflows/on_push.yml index 101d35b..2bb0746 100644 --- a/.gitea/workflows/on_push.yml +++ b/.gitea/workflows/on_push.yml @@ -1,4 +1,4 @@ -name: Check master code +name: Master on: push: branches: @@ -6,4 +6,4 @@ on: jobs: build: - uses: ./.gitea/workflows/common_build.yml \ No newline at end of file + uses: ./.gitea/workflows/on_called_build.yml -- 2.34.1