From a4849bcd8c8f47f79874164101c389a904ec6734 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:32:07 +0000 Subject: [PATCH] test: make debounce cancellation tests deterministic and fail loudly on harness download errors Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- Makefile | 21 ++++++++++----- .../sdk/android/StateDebounceManagerTest.java | 26 +++++++++++++------ 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Makefile b/Makefile index ed9965b7..bd5fc161 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,11 @@ SUPPRESSION_FILE_FDV2=testharness-suppressions-fdv2.txt TEST_HARNESS_PARAMS_V2= -status-timeout 60 TEST_HARNESS_PARAMS_V3= -status-timeout 60 +# The downloader script is version-agnostic -- VERSION selects which harness binary it fetches -- +# so both suites use the copy from the v2 branch, which retries the release download. +TEST_HARNESS_DOWNLOADER_URL=https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh +TEST_HARNESS_DOWNLOADER=build/sdk-test-harness-downloader.sh + build-contract-tests: @cd contract-tests && ../gradlew --no-daemon -s assembleDebug -PdisablePreDex @@ -13,16 +18,18 @@ start-emulator: start-contract-test-service: @scripts/start-test-service.sh +$(TEST_HARNESS_DOWNLOADER): + @mkdir -p $(dir $@) + @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ + --fail -sS -L --retry 5 --retry-delay 2 \ + -o $@ $(TEST_HARNESS_DOWNLOADER_URL) + # Note that only the last version of the tests have the stop-service-at-end flag set, so the contract test service will be stopped after the tests are run. -run-contract-tests: +run-contract-tests: $(TEST_HARNESS_DOWNLOADER) @echo "Running SDK contract test v2..." - @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ - -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ - | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh + @VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh $(TEST_HARNESS_DOWNLOADER) @echo "Running SDK contract test v3..." - @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ - -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.1.0-alpha.6/downloader/run.sh \ - | VERSION=v3.1.0-alpha.6 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh + @VERSION=v3.1.0-alpha.6 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh $(TEST_HARNESS_DOWNLOADER) contract-tests: build-contract-tests start-emulator start-contract-test-service run-contract-tests diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StateDebounceManagerTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StateDebounceManagerTest.java index 3fcf52b0..61c58b8f 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StateDebounceManagerTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/StateDebounceManagerTest.java @@ -144,15 +144,21 @@ public void closePreventsFutureCallbacks() throws InterruptedException { } @Test - public void closeCancelsPendingTimer() throws InterruptedException { + public void closeCancelsPendingTimer() { + // Uses a manually-driven executor instead of Thread.sleep so the test is deterministic: + // sleeping for a fraction of the debounce window and expecting close() to win the race + // is flaky on loaded CI runners, where the short sleep can overshoot the window and let + // the timer fire first. AtomicInteger callCount = new AtomicInteger(0); - StateDebounceManager mgr = createManager(true, true, callCount::incrementAndGet); + ManualTaskExecutor manualExecutor = new ManualTaskExecutor(); + StateDebounceManager mgr = new StateDebounceManager( + true, true, manualExecutor, TEST_DEBOUNCE_MS, callCount::incrementAndGet); mgr.setNetworkAvailable(false); - Thread.sleep(TEST_DEBOUNCE_MS / 3); mgr.close(); - Thread.sleep(TEST_DEBOUNCE_MS * 3); + assertEquals("close should cancel the scheduled timer", 1, manualExecutor.cancelledCount()); + manualExecutor.runPendingTasks(); assertEquals("pending timer should be cancelled on close", 0, callCount.get()); } @@ -341,15 +347,19 @@ public void dedupAllowsGenuineChangeAfterSuppression() throws InterruptedExcepti // ==== reset() (CONNMODE 3.5.6 — identify bypasses debounce) ==== @Test - public void resetCancelsPendingTimer() throws InterruptedException { + public void resetCancelsPendingTimer() { + // Manually-driven executor for the same reason as closeCancelsPendingTimer: reset() must + // win against the pending timer, which a wall-clock sleep cannot guarantee. AtomicInteger callCount = new AtomicInteger(0); - StateDebounceManager mgr = createManager(true, true, callCount::incrementAndGet); + ManualTaskExecutor manualExecutor = new ManualTaskExecutor(); + StateDebounceManager mgr = new StateDebounceManager( + true, true, manualExecutor, TEST_DEBOUNCE_MS, callCount::incrementAndGet); mgr.setNetworkAvailable(false); - Thread.sleep(TEST_DEBOUNCE_MS / 3); mgr.reset(true, true); - Thread.sleep(TEST_DEBOUNCE_MS * 3); + assertEquals("reset should cancel the scheduled timer", 1, manualExecutor.cancelledCount()); + manualExecutor.runPendingTasks(); assertEquals("reset should cancel the pending timer", 0, callCount.get()); mgr.close();