ci: Instrumentation tests coverage in ci.
Some checks failed
Check PR code / BuildAndTestAndCoverage (pull_request) Failing after 14m8s

This commit is contained in:
Amine Bouabdallaoui 2025-03-25 13:36:35 +01:00
parent 1b2e9edc8c
commit cf4a04c897
12 changed files with 99 additions and 9 deletions

View File

@ -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_7a
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:

View File

@ -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()
}
}

View File

@ -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(

View File

@ -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.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@ -10,6 +11,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
@ -31,6 +33,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())
}

View File

@ -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(),
)

View File

@ -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())
}

View File

@ -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())
}

View File

@ -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()

View File

@ -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(waitUntilNotLoading(300000))
}
@Suppress("detekt:UtilityClassWithPublicConstructor")
open class WithANRException {
companion object {

View File

@ -8,9 +8,14 @@ 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.withChild
@ -19,11 +24,15 @@ 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 +53,47 @@ fun withError(
}
}
fun waitUntilNotLoading(millis: Long): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = any(View::class.java)
override fun getDescription(): String = "wait for a specific view is not loading during $millis millis."
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<Root> = isPlatformPopup()
fun withDrawable(

View File

@ -50,6 +50,7 @@ class SourcesActivity :
override fun onResume() {
super.onResume()
CountingIdlingResourceSingleton.increment()
val mLayoutManager = LinearLayoutManager(this)
var items: ArrayList<SelfossModel.SourceDetail>
@ -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()

View File

@ -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()