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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .sauce/sentry-uitest-android-ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,6 @@ suites:
- name: ".*"
platformVersion: "15"

- name: "Android 14 Ui test (api 34)"
testOptions:
clearPackageData: true
useTestOrchestrator: true
devices:
- name: ".*"
platformVersion: "14"

- name: "Android 13 Ui test (api 33)"
testOptions:
clearPackageData: true
useTestOrchestrator: true
devices:
- name: ".*"
platformVersion: "13"

# Controls what artifacts to fetch when the suite on Sauce Cloud has finished.
artifacts:
download:
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
apollo = "2.5.9"
androidxLifecycle = "2.2.0"
androidxNavigation = "2.4.2"
androidxTestCore = "1.6.1"
androidxTestCore = "1.7.0"
androidxCompose = "1.6.3"
composeCompiler = "1.5.14"
coroutines = "1.6.1"
espresso = "3.5.0"
espresso = "3.7.0"
feign = "11.6"
jacoco = "0.8.7"
jackson = "2.18.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -181,26 +182,30 @@ private void registerReceiver(
}
}

private void unregisterReceiver() {
@SuppressWarnings("Convert2MethodRef") // older AGP versions do not support method references
private void scheduleUnregisterReceiver() {
if (options == null) {
return;
}

options
.getExecutorService()
.submit(
() -> {
final @Nullable SystemEventsBroadcastReceiver receiverRef;
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
isStopped = true;
receiverRef = receiver;
receiver = null;
}
try {
options.getExecutorService().submit(() -> unregisterReceiver());
} catch (RejectedExecutionException e) {
unregisterReceiver();
}
}

if (receiverRef != null) {
context.unregisterReceiver(receiverRef);
}
});
private void unregisterReceiver() {
final @Nullable SystemEventsBroadcastReceiver receiverRef;
try (final @NotNull ISentryLifecycleToken ignored = receiverLock.acquire()) {
isStopped = true;
receiverRef = receiver;
receiver = null;
}

if (receiverRef != null) {
context.unregisterReceiver(receiverRef);
}
}

@Override
Expand All @@ -215,7 +220,7 @@ public void close() throws IOException {
}

AppState.getInstance().removeAppStateListener(this);
unregisterReceiver();
scheduleUnregisterReceiver();

if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "SystemEventsBreadcrumbsIntegration removed.");
Expand Down Expand Up @@ -264,7 +269,7 @@ public void onForeground() {

@Override
public void onBackground() {
unregisterReceiver();
scheduleUnregisterReceiver();
}

final class SystemEventsBroadcastReceiver extends BroadcastReceiver {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnitRunner
import io.sentry.JsonSerializer
import io.sentry.ProfilingTraceData
import io.sentry.Sentry
import io.sentry.Sentry.OptionsConfiguration
import io.sentry.SentryEnvelope
import io.sentry.SentryEvent
import io.sentry.SentryItemType
import io.sentry.SentryOptions
import io.sentry.android.core.SentryAndroid
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.protocol.SentryTransaction
import io.sentry.test.applyTestOptions
import io.sentry.test.initForTest
import io.sentry.uitest.android.mockservers.MockRelay
Expand Down Expand Up @@ -43,9 +50,9 @@ abstract class BaseUiTest {
/** Mock relay server that receives all envelopes sent during the test. */
protected val relay = MockRelay(false, relayIdlingResource)

private fun disableDontKeepActivities() {
private fun runCommand(cmd: String) {
val automation = InstrumentationRegistry.getInstrumentation().uiAutomation
val pfd = automation.executeShellCommand("settings put global always_finish_activities 0")
val pfd = automation.executeShellCommand(cmd)
try {
FileInputStream(pfd.fileDescriptor).readBytes()
} catch (e: Throwable) {
Expand All @@ -54,6 +61,16 @@ abstract class BaseUiTest {
pfd.close()
}

private fun disableDontKeepActivities() {
runCommand("settings put global always_finish_activities 0")
}

fun disableSystemAnimations() {
runCommand("settings put global window_animation_scale 0")
runCommand("settings put global transition_animation_scale 0")
runCommand("settings put global animator_duration_scale 0")
}

@BeforeTest
fun baseSetUp() {
runner = InstrumentationRegistry.getInstrumentation() as AndroidJUnitRunner
Expand All @@ -63,6 +80,7 @@ abstract class BaseUiTest {
relay.start()
mockDsn = relay.createMockDsn()
disableDontKeepActivities()
disableSystemAnimations()
}

@AfterTest
Expand Down Expand Up @@ -126,3 +144,40 @@ fun initForTest(
optionsConfiguration.configure(it)
}
}

/**
* Function used to describe the content of the envelope to print in the logs. For debugging
* purposes only.
*/
internal fun SentryEnvelope.describeForTest(): String {
var descr = ""
items.forEach { item ->
when (item.header.type) {
SentryItemType.Event -> {
val deserialized =
JsonSerializer(SentryOptions())
.deserialize(item.data.inputStream().reader(), SentryEvent::class.java)!!
descr +=
"Event (${deserialized.eventId}) - message: ${deserialized.message!!.formatted} -- "
}
SentryItemType.Transaction -> {
val deserialized =
JsonSerializer(SentryOptions())
.deserialize(item.data.inputStream().reader(), SentryTransaction::class.java)!!
descr +=
"Transaction (${deserialized.eventId}) - transaction: ${deserialized.transaction} - spans: ${deserialized.spans.joinToString { "${it.op} ${it.description}" }} -- "
}
SentryItemType.Profile -> {
val deserialized =
JsonSerializer(SentryOptions())
.deserialize(item.data.inputStream().reader(), ProfilingTraceData::class.java)!!
descr +=
"Profile (${deserialized.profileId}) - transactionName: ${deserialized.transactionName} -- "
}
else -> {
descr += "${item.header.type} -- "
}
}
}
return "*** Envelope: $descr ***"
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ class EnvelopeTests : BaseUiTest() {

relayIdlingResource.increment()
IdlingRegistry.getInstance().register(ProfilingSampleActivity.scrollingIdlingResource)
Thread.sleep(1000)
val transaction = Sentry.startTransaction("profiledTransaction", "test1")
val sampleScenario = launchActivity<ProfilingSampleActivity>()
swipeList(1)
Expand Down Expand Up @@ -171,15 +170,17 @@ class EnvelopeTests : BaseUiTest() {
values.last().relativeStartNs.toLong() <= maxTimestampAllowed,
"Last measurement value for '$name' is outside bounds (was: ${values.last().relativeStartNs.toLong()}ns, max: ${maxTimestampAllowed}ns",
)
}

// Timestamps of measurements should differ at least 10 milliseconds from each other
(1 until values.size).forEach { i ->
assertTrue(
values[i].relativeStartNs.toLong() >=
values[i - 1].relativeStartNs.toLong() + TimeUnit.MILLISECONDS.toNanos(10),
"Measurement value timestamp for '$name' does not differ at least 10ms",
)
// Timestamps of measurements should differ at least 10 milliseconds from each other
(1 until values.size).forEach { i ->
val measurementTimestampDiff =
values[i].relativeStartNs.toLong() - values[i - 1].relativeStartNs.toLong()

assertTrue(
measurementTimestampDiff >= TimeUnit.MILLISECONDS.toNanos(10),
"Measurement value timestamp for '$name' should differ at least 10ms, but was $measurementTimestampDiff",
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ class SdkInitTests : BaseUiTest() {
}
val transaction = Sentry.startTransaction("e2etests", "testInit")
val sampleScenario = launchActivity<EmptyActivity>()

initSentry(true) {
it.tracesSampleRate = 1.0
it.profilesSampleRate = 1.0
// We use the same executorService before and after closing the SDK
it.executorService = options.executorService
it.isDebug = true
}

relayIdlingResource.increment()
relayIdlingResource.increment()
relayIdlingResource.increment()
transaction.finish()
Expand All @@ -79,9 +82,19 @@ class SdkInitTests : BaseUiTest() {
it.assertNoOtherItems()
assertEquals("e2etests", transactionItem.transaction)
}
}

relay.assert {
findEnvelope {
assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction ==
"EmptyActivity"
}
.assert {
val transactionItem: SentryTransaction = it.assertTransaction()
// Transaction-based Profiling is already in e2etests transaction, so it won't run again
// here
it.assertNoOtherItems()
assertEquals("EmptyActivity", transactionItem.transaction)
}

findEnvelope {
assertEnvelopeTransaction(it.items.toList(), AndroidLogger()).transaction == "e2etests2"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.Sentry
import io.sentry.SentryEvent
import io.sentry.SentryFeedbackOptions.SentryFeedbackCallback
import io.sentry.SentryItemType
import io.sentry.SentryOptions
import io.sentry.android.core.AndroidLogger
import io.sentry.android.core.R
Expand Down Expand Up @@ -471,6 +472,9 @@ class UserFeedbackUiTest : BaseUiTest() {
// because it would block the espresso interactions (button click)
it.feedbackOptions.onSubmitSuccess = SentryFeedbackCallback {
relayIdlingResource.increment()
if (enableReplay) {
relayIdlingResource.increment()
}
}
// Let's capture a replay, so we can check the replayId in the feedback
if (enableReplay) {
Expand Down Expand Up @@ -511,6 +515,10 @@ class UserFeedbackUiTest : BaseUiTest() {
)
}
}
if (enableReplay) {
findEnvelope { it.items.first().header.type == SentryItemType.ReplayVideo }
}
assertNoOtherEnvelopes()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.sentry.uitest.android.mockservers

import androidx.test.espresso.idling.CountingIdlingResource
import io.sentry.uitest.android.describeForTest
import io.sentry.uitest.android.waitUntilIdle
import kotlin.test.assertNotNull
import okhttp3.mockwebserver.Dispatcher
Expand Down Expand Up @@ -118,7 +119,19 @@ class MockRelay(
/** Wait to receive all requests (if [waitForRequests] is true) and run the [assertion]. */
fun assert(assertion: RelayAsserter.() -> Unit) {
if (waitForRequests) {
waitUntilIdle()
try {
waitUntilIdle()
} catch (e: Exception) {
if (unassertedEnvelopes.isNotEmpty()) {
throw AssertionError(
"There was a total of ${unassertedEnvelopes.size} envelopes: " +
unassertedEnvelopes.joinToString { it.envelope!!.describeForTest() },
e,
)
} else {
throw e
}
}
}
assertion(RelayAsserter(unassertedEnvelopes))
}
Expand Down
Loading
Loading