Skip to content

Commit b9dc9a0

Browse files
committed
test(profiling): Add PerfettoContinuousProfilerTests
Verify that onRateLimitChanged stops the profiler, resets profiler/chunk IDs, and logs the expected warning. Run with: ./gradlew :sentry-android-core:testDebugUnitTest --tests "io.sentry.android.core.PerfettoContinuousProfilerTest"
1 parent 5c6f659 commit b9dc9a0

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package io.sentry.android.core
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import androidx.test.core.app.ApplicationProvider
6+
import androidx.test.ext.junit.runners.AndroidJUnit4
7+
import io.sentry.DataCategory
8+
import io.sentry.ILogger
9+
import io.sentry.IScopes
10+
import io.sentry.ProfileLifecycle
11+
import io.sentry.Sentry
12+
import io.sentry.SentryLevel
13+
import io.sentry.TracesSampler
14+
import io.sentry.protocol.SentryId
15+
import io.sentry.test.DeferredExecutorService
16+
import io.sentry.transport.RateLimiter
17+
import kotlin.test.AfterTest
18+
import kotlin.test.BeforeTest
19+
import kotlin.test.Test
20+
import kotlin.test.assertEquals
21+
import kotlin.test.assertFalse
22+
import kotlin.test.assertTrue
23+
import org.junit.runner.RunWith
24+
import org.mockito.Mockito.mockStatic
25+
import org.mockito.kotlin.any
26+
import org.mockito.kotlin.eq
27+
import org.mockito.kotlin.mock
28+
import org.mockito.kotlin.spy
29+
import org.mockito.kotlin.verify
30+
import org.mockito.kotlin.whenever
31+
32+
@RunWith(AndroidJUnit4::class)
33+
class PerfettoContinuousProfilerTest {
34+
private lateinit var context: Context
35+
private val fixture = Fixture()
36+
37+
private class Fixture {
38+
private val mockDsn = "http://key@localhost/proj"
39+
val buildInfo =
40+
mock<BuildInfoProvider> {
41+
whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.VANILLA_ICE_CREAM)
42+
}
43+
val executor = DeferredExecutorService()
44+
val mockedSentry = mockStatic(Sentry::class.java)
45+
val mockLogger = mock<ILogger>()
46+
val mockTracesSampler = mock<TracesSampler>()
47+
val mockPerfettoProfiler = mock<PerfettoProfiler>()
48+
49+
val scopes: IScopes = mock()
50+
51+
val options =
52+
spy(SentryAndroidOptions()).apply {
53+
dsn = mockDsn
54+
profilesSampleRate = 1.0
55+
isDebug = true
56+
setLogger(mockLogger)
57+
}
58+
59+
init {
60+
whenever(mockTracesSampler.sampleSessionProfile(any())).thenReturn(true)
61+
whenever(mockPerfettoProfiler.start(any())).thenReturn(
62+
AndroidProfiler.ProfileStartData(
63+
System.nanoTime(),
64+
0L,
65+
io.sentry.DateUtils.getCurrentDateTime(),
66+
),
67+
)
68+
}
69+
70+
fun getSut(): PerfettoContinuousProfiler {
71+
options.executorService = executor
72+
whenever(scopes.options).thenReturn(options)
73+
return PerfettoContinuousProfiler(
74+
buildInfo,
75+
mockLogger,
76+
{ options.executorService },
77+
{ mockPerfettoProfiler },
78+
)
79+
}
80+
}
81+
82+
@BeforeTest
83+
fun `set up`() {
84+
context = ApplicationProvider.getApplicationContext()
85+
Sentry.setCurrentScopes(fixture.scopes)
86+
fixture.mockedSentry.`when`<Any> { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes)
87+
}
88+
89+
@AfterTest
90+
fun clear() {
91+
fixture.mockedSentry.close()
92+
}
93+
94+
@Test
95+
fun `profiler stops when rate limited`() {
96+
val profiler = fixture.getSut()
97+
val rateLimiter = mock<RateLimiter>()
98+
whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunkUi)).thenReturn(true)
99+
100+
profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler)
101+
assertTrue(profiler.isRunning)
102+
103+
profiler.onRateLimitChanged(rateLimiter)
104+
assertFalse(profiler.isRunning)
105+
assertEquals(SentryId.EMPTY_ID, profiler.profilerId)
106+
assertEquals(SentryId.EMPTY_ID, profiler.chunkId)
107+
verify(fixture.mockLogger)
108+
.log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler."))
109+
}
110+
111+
@Test
112+
fun `manual profiler can be started again after a full start-stop cycle`() {
113+
// DeferredExecutorService captures scheduled runnables instead of waiting.
114+
// executor.runAll() fires them immediately, simulating the 60s chunk timer elapsing.
115+
val profiler = fixture.getSut()
116+
117+
// Session 1: start profiling, then stop it
118+
profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler)
119+
assertTrue(profiler.isRunning)
120+
profiler.stopProfiler(ProfileLifecycle.MANUAL)
121+
// Simulate the 60s chunk timer firing — stopInternal(restartProfiler=true) runs,
122+
// sees shouldStop=true, and does NOT restart. Profiler stops.
123+
fixture.executor.runAll()
124+
assertFalse(profiler.isRunning)
125+
126+
// Session 2: start profiling again
127+
profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler)
128+
assertTrue(profiler.isRunning)
129+
// Simulate the 60s chunk timer firing — stopInternal(restartProfiler=true) runs.
130+
// shouldStop must have been reset to false by startProfiler, so the profiler
131+
// should restart for the next chunk.
132+
fixture.executor.runAll()
133+
assertTrue(profiler.isRunning, "Profiler should continue running after chunk restart — shouldStop must be reset on start")
134+
}
135+
}

0 commit comments

Comments
 (0)