From 521be77362afc8ae6f5a24e5ad586750408925b9 Mon Sep 17 00:00:00 2001 From: Andrey Belonogov Date: Tue, 22 Sep 2026 13:36:59 -0700 Subject: [PATCH] test: stop streaming data sources in StreamingDataSourceTest StreamingDataSourceTest started 16 data sources and never stopped any of them. Each start creates a BackgroundEventSource with its own stream and events threads plus an open connection to the test server, so the class left 34 EventSource threads (and their retry loops) running for the rest of the forked test JVM. Simply calling stop() was not enough: StreamingDataSource.stop() lowers the priority of its shutdown thread via android.os.Process, which is not mocked in unit tests, so the shutdown thread died before closing anything and stop() was silently a no-op. Add an android.os.Process stub alongside the existing android.util.Base64 and android.util.Pair stubs, then track every started data source and stop them all in an @After, waiting for the stop callback so teardown is deterministic. Co-authored-by: Cursor --- .../src/test/java/android/os/Process.java | 19 +++++++ .../sdk/android/StreamingDataSourceTest.java | 56 +++++++++++++------ 2 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/test/java/android/os/Process.java diff --git a/launchdarkly-android-client-sdk/src/test/java/android/os/Process.java b/launchdarkly-android-client-sdk/src/test/java/android/os/Process.java new file mode 100644 index 000000000..221be8dd2 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/test/java/android/os/Process.java @@ -0,0 +1,19 @@ +package android.os; + +// This file exists only to support the unit tests in src/test/java. The issue is that the SDK +// code uses android.os.Process, which only exists in the Android runtime library; but the unit +// tests (as opposed to the instrumented tests in src/androidTest/java) run against the regular +// Java runtime library. Without this class the mockable android.jar is used instead, and every +// method on it throws "Method ... not mocked". +// +// StreamingDataSource.stop() lowers the priority of its shutdown thread before closing the +// EventSource. With the mockable android.jar that call throws, the shutdown thread dies before +// closing anything, and stop() silently becomes a no-op in unit tests. Thread priority has no +// meaningful equivalent off-device, so the stub just does nothing and lets shutdown proceed. + +public class Process { + public static final int THREAD_PRIORITY_BACKGROUND = 10; + + public static void setThreadPriority(int priority) { + } +} diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java index c4652d17c..625d8bfed 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StreamingDataSourceTest.java @@ -18,6 +18,7 @@ import com.launchdarkly.testhelpers.httptest.HttpServer; import com.launchdarkly.testhelpers.httptest.RequestInfo; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -25,6 +26,8 @@ import java.io.IOException; import java.net.URI; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -35,6 +38,7 @@ public class StreamingDataSourceTest { private static final LDContext CONTEXT = LDContext.create("context-key"); private static final String MOBILE_KEY = "test-mobile-key"; private static final String VALID_PUT_JSON = "{\"flag1\":{\"key\":\"flag1\",\"version\":1,\"value\":true}}"; + private static final long STOP_TIMEOUT_MILLIS = 5000; @Rule public Timeout globalTimeout = Timeout.seconds(10); @@ -45,6 +49,7 @@ public class StreamingDataSourceTest { private final MockPlatformState platformState = new MockPlatformState(); private final IEnvironmentReporter environmentReporter = new EnvironmentReporterBuilder().build(); private final SimpleTestTaskExecutor taskExecutor = new SimpleTestTaskExecutor(); + private final List startedDataSources = new ArrayList<>(); private PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData; @Before @@ -52,6 +57,25 @@ public void before() { perEnvironmentData = TestUtil.makeSimplePersistentDataStoreWrapper().perEnvironmentData(MOBILE_KEY); } + @After + public void stopDataSources() throws Exception { + for (DataSource dataSource : startedDataSources) { + AwaitableCallback stopped = new AwaitableCallback<>(); + dataSource.stop(stopped); + stopped.await(STOP_TIMEOUT_MILLIS); + } + startedDataSources.clear(); + } + + // Each started data source owns an EventSource with its own threads and an open connection to + // the test server, so anything we start has to be stopped again in stopDataSources(). + private void startDataSource(StreamingDataSource dataSource, Callback callback) { + if (!startedDataSources.contains(dataSource)) { + startedDataSources.add(dataSource); + } + dataSource.start(callback); + } + private ClientContext makeClientContext(boolean inBackground, Boolean previouslyInBackground) { ClientContext baseClientContext = ClientContextImpl.fromConfig( new LDConfig.Builder(AutoEnvAttributes.Disabled).build(), "", "", perEnvironmentData, null, CONTEXT, @@ -492,7 +516,7 @@ public void startSendsRequestWithBase64ContextInPath() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); assertNotNull(callback.awaitSuccess()); @@ -516,7 +540,7 @@ public void startSendsRequestWithoutContextWhenUsingReport() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, true); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); assertNotNull(callback.awaitSuccess()); @@ -543,7 +567,7 @@ public void startSendsRequestWithReasonsWhenEnabled() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), true, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); assertNotNull(callback.awaitSuccess()); @@ -565,7 +589,7 @@ public void startSendsRequestWithoutReasonsWhenDisabled() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); assertNotNull(callback.awaitSuccess()); @@ -587,7 +611,7 @@ public void startSendsRequestWithReportAndReasons() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), true, true); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); assertNotNull(callback.awaitSuccess()); @@ -609,7 +633,7 @@ public void startWithHttp401ShutsDownSink() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); Throwable error = callback.awaitError(); assertNotNull(error); @@ -633,7 +657,7 @@ public void startWithHttp403ReportsNonRetriableWithoutShutDown() throws Exceptio StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); Throwable error = callback.awaitError(); assertNotNull(error); @@ -651,7 +675,7 @@ public void startWithHttp500ReportsRetriableError() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); Throwable error = callback.awaitError(); assertNotNull(error); @@ -683,7 +707,7 @@ private void assertStatusReportsRetriableError(int status) throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); Throwable error = callback.awaitError(); assertNotNull(error); @@ -709,7 +733,7 @@ public void startWithHttp429ReconnectsAndReceivesStreamData() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); Throwable error = callback.awaitError(); assertNotNull(error); @@ -728,7 +752,7 @@ public void startWithNetworkErrorReportsNetworkFailure() throws Exception { StreamingDataSource sds = makeStreamingDataSource( URI.create("http://localhost:1"), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); Throwable error = callback.awaitError(); assertNotNull(error); @@ -744,13 +768,13 @@ public void startWithHttp401PreventsSubsequentStart() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback1 = new TrackingCallback(); - sds.start(callback1); + startDataSource(sds, callback1); assertNotNull(callback1.awaitError()); // Second start should be a no-op due to connection401Error flag TrackingCallback callback2 = new TrackingCallback(); - sds.start(callback2); + startDataSource(sds, callback2); assertNull("Second start should not produce a callback", callback2.errors.poll(500, TimeUnit.MILLISECONDS)); @@ -772,7 +796,7 @@ public void startReceivesPutEventAndInitsSink() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); Boolean success = callback.awaitSuccess(); assertNotNull(success); @@ -798,7 +822,7 @@ public void startReceivesPatchEventAndUpsertsSink() throws Exception { StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); assertNotNull(callback.awaitSuccess()); dataSourceUpdateSink.expectInit(); @@ -823,7 +847,7 @@ public void startReceivesDeleteEventAndUpsertsDeletedPlaceholder() throws Except StreamingDataSource sds = makeStreamingDataSource( server.getUri(), false, false); TrackingCallback callback = new TrackingCallback(); - sds.start(callback); + startDataSource(sds, callback); assertNotNull(callback.awaitSuccess()); dataSourceUpdateSink.expectInit();