From d0edcdac07ca70bddddd048de8c84bbbf1728507 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 10 Nov 2026 17:29:05 +0100 Subject: [PATCH 1/2] collection: Robolectric CI speed-up From 11e2d758bc5c97c172d1badc294026d2e3ef22d4 Mon Sep 17 00:00:00 2001 From: Roman Zavarnitsyn Date: Tue, 10 Nov 2026 12:37:37 +0100 Subject: [PATCH 2/2] test(android): Use fewer Robolectric SDK levels in core and replay Robolectric starts a new sandbox for each SDK level a test JVM uses, and downloads one android-all jar per level. Move pinned levels to the nearest shared level that takes the same code branch: 23/25 -> 24, 26 -> 30, 31/34 -> 33. Level 28 stays because ReplaySmokeTest and AndroidConnectionStatusProviderTest behave differently at 30. Core and replay now use 6 levels instead of 11. Locally, core Robolectric tests go from about 82s to 71s. Co-Authored-By: Claude Opus 5.5 --- .../core/ActivityLifecycleIntegrationTest.kt | 4 ++-- .../android/core/AndroidOptionsInitializerTest.kt | 14 +++++++------- .../ApplicationExitInfoCrossIntegrationTest.kt | 2 +- .../android/core/ScreenshotEventProcessorTest.kt | 2 +- .../io/sentry/android/core/SentryAndroidTest.kt | 4 ++-- .../android/core/SessionTrackingIntegrationTest.kt | 2 +- .../android/core/TombstoneIntegrationTest.kt | 2 +- ...droidConnectionStatusProviderConcurrencyTest.kt | 2 +- .../util/AndroidConnectionStatusProviderTest.kt | 2 +- .../util/CellularNetworkTechnologyProviderTest.kt | 2 +- .../core/internal/util/ScreenshotUtilTest.kt | 2 +- .../io/sentry/android/replay/ReplayCacheTest.kt | 2 +- .../sentry/android/replay/ReplayIntegrationTest.kt | 2 +- .../replay/ReplayIntegrationWithRecorderTest.kt | 2 +- 14 files changed, 22 insertions(+), 22 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 27f339aff31..1a88c1bec6d 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -704,8 +704,8 @@ class ActivityLifecycleIntegrationTest { } @Test - @Config(sdk = [Build.VERSION_CODES.M]) - fun `onHeadlessAppStart creates standalone App Start transaction on API 23`() { + @Config(sdk = [Build.VERSION_CODES.N]) + fun `onHeadlessAppStart creates standalone App Start transaction on API 24`() { val sut = fixture.getSut { it.tracesSampleRate = 1.0 it.isEnableStandaloneAppStartTracing = true diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 7c7acf8ef3b..8b5e8a07053 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -370,7 +370,7 @@ class AndroidOptionsInitializerTest { assertTrue(fixture.sentryOptions.transportGate is AndroidTransportGate) } - @Config(sdk = [Build.VERSION_CODES.N_MR1]) + @Config(sdk = [Build.VERSION_CODES.N]) @Test fun `init on API 25 and lower sets SSLSocketFactory with bundled Sentry root CAs`() { fixture.initSut(configureOptions = { dsn = SAAS_DSN }) @@ -378,7 +378,7 @@ class AndroidOptionsInitializerTest { assertIs(fixture.sentryOptions.sslSocketFactory) } - @Config(sdk = [Build.VERSION_CODES.N_MR1]) + @Config(sdk = [Build.VERSION_CODES.N]) @Test fun `init on API 25 and lower does not set SSLSocketFactory for self-hosted DSN`() { fixture.initSut(configureOptions = { dsn = "https://key@sentry.example.com/1" }) @@ -386,7 +386,7 @@ class AndroidOptionsInitializerTest { assertNull(fixture.sentryOptions.sslSocketFactory) } - @Config(sdk = [Build.VERSION_CODES.O]) + @Config(sdk = [30]) @Test fun `init on API 26 and higher does not set SSLSocketFactory`() { fixture.initSut(configureOptions = { dsn = SAAS_DSN }) @@ -394,7 +394,7 @@ class AndroidOptionsInitializerTest { assertNull(fixture.sentryOptions.sslSocketFactory) } - @Config(sdk = [Build.VERSION_CODES.N_MR1]) + @Config(sdk = [Build.VERSION_CODES.N]) @Test fun `init on API 25 and lower keeps user provided SSLSocketFactory`() { val sslSocketFactory = mock() @@ -437,7 +437,7 @@ class AndroidOptionsInitializerTest { ) } - @Config(sdk = [34]) + @Config(sdk = [33]) @Test fun `init below API 35 does not report PerfettoContinuousProfiling integration`() { SentryIntegrationPackageStorage.getInstance().clearStorage() @@ -450,14 +450,14 @@ class AndroidOptionsInitializerTest { ) } - @Config(sdk = [34]) + @Config(sdk = [33]) @Test fun `init below API 35 with enableLegacyProfiling true sets AndroidContinuousProfiler`() { fixture.initSut(configureOptions = { isEnableLegacyProfiling = true }) assertTrue(fixture.sentryOptions.continuousProfiler is AndroidContinuousProfiler) } - @Config(sdk = [34]) + @Config(sdk = [33]) @Test fun `init below API 35 with enableLegacyProfiling false noops profiler`() { fixture.initSut(configureOptions = { isEnableLegacyProfiling = false }) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoCrossIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoCrossIntegrationTest.kt index 8aceb6784e8..cac76c024af 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoCrossIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitInfoCrossIntegrationTest.kt @@ -35,7 +35,7 @@ import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder // Our current Robolectric version (4.15) caps at API 35. All ApplicationExitInfo code paths we // exercise are at API 31 or below, so we use the latter. @RunWith(AndroidJUnit4::class) -@Config(sdk = [31]) +@Config(sdk = [33]) class ApplicationExitInfoCrossIntegrationTest { @get:Rule val tmpDir = TemporaryFolder() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt index 0abb232b807..b27df5f6bf1 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ScreenshotEventProcessorTest.kt @@ -202,7 +202,7 @@ class ScreenshotEventProcessorTest { } @Test - @Config(sdk = [23]) + @Config(sdk = [24]) fun `when screenshot event processor is called from background thread it executes on main thread`() { val sut = fixture.getSut(true) whenever(fixture.threadChecker.isMainThread).thenReturn(false) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt index 725a5a61122..2d618163d65 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryAndroidTest.kt @@ -349,7 +349,7 @@ class SentryAndroidTest { } @Test - @Config(sdk = [26]) + @Config(sdk = [30]) fun `init starts session replay if app is in foreground`() { initSentryWithForegroundImportance(true) { _ -> Shadows.shadowOf(Looper.getMainLooper()).idle() @@ -358,7 +358,7 @@ class SentryAndroidTest { } @Test - @Config(sdk = [26]) + @Config(sdk = [30]) fun `init does not start session replay if the app is in background`() { initSentryWithForegroundImportance(false) { _ -> Shadows.shadowOf(Looper.getMainLooper()).idle() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index ef920e1d7fe..f49dde9414b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -36,7 +36,7 @@ import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(sdk = [31]) +@Config(sdk = [33]) class SessionTrackingIntegrationTest { private lateinit var context: Context diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt index 39a63148a17..ca4c51319aa 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/TombstoneIntegrationTest.kt @@ -26,7 +26,7 @@ import org.robolectric.annotation.Config import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder @RunWith(AndroidJUnit4::class) -@Config(sdk = [31]) +@Config(sdk = [33]) class TombstoneIntegrationTest : ApplicationExitIntegrationTestBase() { override val config = diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderConcurrencyTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderConcurrencyTest.kt index d201d48d111..ed37286026f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderConcurrencyTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderConcurrencyTest.kt @@ -61,7 +61,7 @@ import org.robolectric.annotation.Config * that must not wait for them run on a separate caller thread with a bounded wait. */ @RunWith(AndroidJUnit4::class) -@Config(sdk = [Build.VERSION_CODES.S]) +@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) class AndroidConnectionStatusProviderConcurrencyTest { @AfterTest diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt index 56cf8f4e0ba..cd55902b4e7 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidConnectionStatusProviderTest.kt @@ -258,7 +258,7 @@ class AndroidConnectionStatusProviderTest { /// Lets a test reject work only after the setup that needs a working executor is done. @Test - @Config(sdk = [Build.VERSION_CODES.S]) + @Config(sdk = [Build.VERSION_CODES.TIRAMISU]) fun `When the network callback cannot be registered, the telephony listener is not started`() { // Without ACCESS_NETWORK_STATE the network callback registration fails, and onBackground skips // unregistering while there is no network callback, so the listener must not be started here. diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt index 4bf1d7de427..64d2b3dc281 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/CellularNetworkTechnologyProviderTest.kt @@ -27,7 +27,7 @@ import org.mockito.kotlin.whenever import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(sdk = [Build.VERSION_CODES.S]) +@Config(sdk = [Build.VERSION_CODES.TIRAMISU]) class CellularNetworkTechnologyProviderTest { private class Fixture { val context = mock() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilTest.kt index 5e74042d3d1..c63addd55cc 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/ScreenshotUtilTest.kt @@ -22,7 +22,7 @@ import org.robolectric.Robolectric.buildActivity import org.robolectric.annotation.Config import org.robolectric.shadows.ShadowPixelCopy -@Config(shadows = [ShadowPixelCopy::class], sdk = [26, 33]) +@Config(shadows = [ShadowPixelCopy::class], sdk = [30, 33]) @RunWith(AndroidJUnit4::class) class ScreenshotUtilTest { @Test diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt index aeca0c868d1..8e08b936bd5 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayCacheTest.kt @@ -49,7 +49,7 @@ import org.robolectric.shadows.ShadowBitmapFactory import org.robolectric.shadows.ShadowCloseGuard @RunWith(AndroidJUnit4::class) -@Config(sdk = [26], shadows = [ReplayShadowMediaCodec::class]) +@Config(sdk = [30], shadows = [ReplayShadowMediaCodec::class]) class ReplayCacheTest { @get:Rule val tmpDir = TemporaryFolder() diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt index 70e8426d124..1dad10238f5 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationTest.kt @@ -90,7 +90,7 @@ import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(sdk = [26], shadows = [ReplayShadowMediaCodec::class]) +@Config(sdk = [30], shadows = [ReplayShadowMediaCodec::class]) class ReplayIntegrationTest { @get:Rule val tmpDir = TemporaryFolder() diff --git a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt index 44f1551ede2..873dec18f66 100644 --- a/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt +++ b/sentry-android-replay/src/test/java/io/sentry/android/replay/ReplayIntegrationWithRecorderTest.kt @@ -40,7 +40,7 @@ import org.mockito.kotlin.whenever import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) -@Config(sdk = [26], shadows = [ReplayShadowMediaCodec::class]) +@Config(sdk = [30], shadows = [ReplayShadowMediaCodec::class]) class ReplayIntegrationWithRecorderTest { @get:Rule val tmpDir = TemporaryFolder()