Skip to content

@W-22952929: [Android] Stabilize flaky test testNullsInSelectedLoginServer#2926

Merged
JohnsonEricAtSalesforce merged 1 commit into
forcedotcom:devfrom
JohnsonEricAtSalesforce:feature/W-22952929_android-stabilize-flaky-test-testnullsinselectedloginserver
Jun 11, 2026
Merged

@W-22952929: [Android] Stabilize flaky test testNullsInSelectedLoginServer#2926
JohnsonEricAtSalesforce merged 1 commit into
forcedotcom:devfrom
JohnsonEricAtSalesforce:feature/W-22952929_android-stabilize-flaky-test-testnullsinselectedloginserver

Conversation

@JohnsonEricAtSalesforce

@JohnsonEricAtSalesforce JohnsonEricAtSalesforce commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a LiveData postValue/getValue race condition in LoginServerManager.getSelectedLoginServer() that caused testNullsInSelectedLoginServer to fail intermittently on Firebase Test Lab under device load.

Related: PR #2922 (stabilized LoginViewModelTest coroutine races — same flaky-test stabilization effort, different root cause)

Root Cause

getSelectedLoginServer() used selectedServer.postValue(...) to update LiveData, then immediately read from selectedServer.getValue() for both its return value and a subsequent setSelectedLoginServer() call. Since postValue is asynchronous (scheduled on the main looper), getValue() returns stale data until the post is dispatched — a window that widens under CI device load.

Three instances of this race existed in the method:

  1. Fallback path (lines 228–233): When the selected server is null/unavailable and the method falls back to the first server in the list, postValue(loginServers.get(0)) is followed by setSelectedLoginServer(selectedServer.getValue()). The getValue() returns null (nothing was ever posted before), so setSelectedLoginServer no-ops (null guard) and nothing gets persisted.

  2. Return value (line 236): return selectedServer.getValue() reads from LiveData instead of returning the locally-computed result. In the fallback path this returns null; in the "valid and available" path it returns the previous server value before postValue lands.

  3. Valid-and-available path (lines 214–219): The method correctly computes selectedLoginServer locally but discards it at the return statement in favor of selectedServer.getValue(), which may still hold the prior value if postValue hasn't dispatched.

Fix

The method now uses the local selectedLoginServer variable as the single source of truth and returns it directly. LiveData is used exclusively as a notification channel for observers — never read back for synchronous logic:

// Before (racy):
if (!loginServers.isEmpty()) {
    selectedServer.postValue(loginServers.get(0));
}
setSelectedLoginServer(selectedServer.getValue());  // reads null
return selectedServer.getValue();                   // returns null

// After (deterministic):
if (!loginServers.isEmpty()) {
    selectedLoginServer = loginServers.get(0);
    selectedServer.postValue(selectedLoginServer);
}
setSelectedLoginServer(selectedLoginServer);        // correct value
return selectedLoginServer;                         // correct value

Test Change

testNullSelectedLoginServer previously mocked the server list SharedPreferences with null name/URL entries (producing an empty getLoginServers() result), yet asserted that getSelectedLoginServer() returns "Production". This only passed because the constructor's initSharedPrefFile() called setSelectedLoginServer(...) which synchronously set selectedServer's LiveData value, and the racy return selectedServer.getValue() happened to read it back.

The mock now stubs the server list with "Production" / "https://login.salesforce.com" — reflecting the post-initialization state where initSharedPrefFile() has populated servers from servers.xml. This matches what real SharedPreferences would contain after the constructor completes and tests the intended scenario: "selected server prefs are null, but the server list is populated, so fall back to the first available server."

Backward Compatibility

  • getSelectedLoginServer() is a public API, but its contract ("returns the selected login server, defaulting to the first available") is unchanged
  • The only behavioral difference: callers now receive the correct value synchronously instead of potentially receiving null or a stale value
  • LiveData observers (selectedServer) continue to receive the same postValue notifications as before

Test Plan

  • testNullsInSelectedLoginServer passes 15+ consecutive runs locally (API 35 emulator)
  • Full LoginServerManagerMockTest class (21 tests) passes 10 consecutive runs with no failures
  • LoginServerManagerTest class (22 tests) passes with no regressions
  • CI passes on Firebase Test Lab

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

…erver (Fix postValue race in getSelectedLoginServer)
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce merged commit 837978a into forcedotcom:dev Jun 11, 2026
5 checks passed
@JohnsonEricAtSalesforce JohnsonEricAtSalesforce deleted the feature/W-22952929_android-stabilize-flaky-test-testnullsinselectedloginserver branch June 11, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants