Skip to content

Commit 59616e3

Browse files
test: make FileDataSource auto-update tests more robust against timing issues (#192)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Related issues** Fixes flaky test `ModifiedFileIsReloadedEvenIfOneFileIsMissingIfSkipMissingPathsIsSet` observed in CI: [test run logs](https://productionresultssa1.blob.core.windows.net/actions-results/13f4e4cd-7974-47b0-b829-de269e609d1f/workflow-job-run-843a6286-f650-51c4-b0dc-5c1e9e4974ff/logs/job/job-logs.txt) **Describe the solution you've provided** Added a new `AssertHelpers.ExpectJsonValue<T>()` helper that loops on `ExpectValue()` until the received value's JSON matches the expected JSON (or times out). This preserves the original `AssertJsonEqual` assertions while handling race conditions from file watcher events. The helper uses `JsonTestValue` parameters to match the return type of `DataSetAsJson()` and the expected signature of `AssertJsonEqual()`. Tests modified in `FileDataSourceTest.cs`: - `ModifiedFileIsReloadedEvenIfOneFileIsMissingIfSkipMissingPathsIsSet` - `IfFlagsAreBadAtStartTimeAutoUpdateCanStillLoadGoodDataLater` Also fixed a separate flaky test in `PollingDataSourceTest.cs`: - `SuccessfulRequestCausesDataToBeStoredAndDataSourceInitializedMetadata` - added `initTask.Wait()` before checking `Initialized` flag to avoid race condition **Describe alternatives you've considered** - Adding `Thread.Sleep()` delays - rejected as this would make tests slower and still potentially flaky - Using `ExpectPredicate()` with custom predicates - rejected per review feedback to preserve original `AssertJsonEqual` assertions for clarity - Modifying the application code - rejected per guidelines to focus on fixing test code rather than application code **Additional context** - Link to Devin run: https://app.devin.ai/sessions/151f389666f447c78d9990e42bc57b48 - Requested by: [email protected] **Human review checklist** - [ ] Verify `ExpectJsonValue` correctly handles the timeout and doesn't silently swallow real failures (it catches `XunitException` to skip intermediate events) - [ ] Confirm the 30-second timeout is appropriate (matches existing patterns in the codebase) - [ ] Verify the original assertions are preserved and we're testing the same behavior - [ ] Note: The `AssertJsonEqual` call after `ExpectJsonValue` is intentionally redundant for readability per review feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent a50515e commit 59616e3

File tree

3 files changed

+58
-6
lines changed

3 files changed

+58
-6
lines changed

pkgs/sdk/server/test/AssertHelpers.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,5 +78,40 @@ public static void ExpectPredicate<T>(EventSink<T> sink, Predicate<T> predicate,
7878
return;
7979
}
8080
}
81+
82+
/// <summary>
83+
/// Expect that the given sink will receive a value that matches the expected JSON within the specified timeout.
84+
/// This handles race conditions where multiple events may be received (e.g., from file watcher triggers)
85+
/// by continuing to check events until one matches or the timeout expires.
86+
/// </summary>
87+
/// <param name="sink">the sink to check events from</param>
88+
/// <param name="expectedJson">the expected JSON value</param>
89+
/// <param name="actualJsonFunc">function to convert the received value to JSON for comparison</param>
90+
/// <param name="timeout">the overall timeout</param>
91+
/// <typeparam name="T">the type of the sink</typeparam>
92+
/// <returns>the value that matched the expected JSON</returns>
93+
public static T ExpectJsonValue<T>(EventSink<T> sink, JsonTestValue expectedJson, Func<T, JsonTestValue> actualJsonFunc,
94+
TimeSpan timeout)
95+
{
96+
var deadline = DateTime.UtcNow + timeout;
97+
98+
while (true)
99+
{
100+
var remaining = deadline - DateTime.UtcNow;
101+
Assert.True(remaining > TimeSpan.Zero, "Timed out waiting for expected JSON value");
102+
103+
var candidate = sink.ExpectValue(remaining);
104+
105+
try
106+
{
107+
AssertJsonEqual(expectedJson, actualJsonFunc(candidate));
108+
return candidate;
109+
}
110+
catch (XunitException)
111+
{
112+
// This was an intermediate event that doesn't match; keep waiting for the correct one
113+
}
114+
}
115+
}
81116
}
82117
}

pkgs/sdk/server/test/Internal/DataSources/FileDataSourceTest.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,14 @@ public void ModifiedFileIsReloadedEvenIfOneFileIsMissingIfSkipMissingPathsIsSet(
270270

271271
file1.SetContentFromPath(TestUtils.TestFilePath("segment-only.json"));
272272

273-
var newData = _updateSink.Inits.ExpectValue(TimeSpan.FromSeconds(5));
273+
// Use ExpectJsonValue to handle potential race conditions where the file watcher
274+
// may trigger multiple reload events. This keeps checking events until one matches
275+
// the expected JSON or the timeout expires.
276+
var newData = AssertHelpers.ExpectJsonValue(
277+
_updateSink.Inits,
278+
DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)),
279+
DataSetAsJson,
280+
TimeSpan.FromSeconds(30));
274281

275282
AssertJsonEqual(DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)), DataSetAsJson(newData));
276283
}
@@ -291,11 +298,18 @@ public void IfFlagsAreBadAtStartTimeAutoUpdateCanStillLoadGoodDataLater()
291298

292299
file.SetContentFromPath(TestUtils.TestFilePath("segment-only.json"));
293300

294-
var newData = _updateSink.Inits.ExpectValue(TimeSpan.FromSeconds(5));
295-
296-
AssertJsonEqual(DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)), DataSetAsJson(newData));
301+
// Use ExpectJsonValue to handle potential race conditions where the file watcher
302+
// may trigger multiple reload events. This keeps checking events until one matches
303+
// the expected JSON or the timeout expires.
297304
// Note that the expected version is 2 because we increment the version on each
298305
// *attempt* to load the files, not on each successful load.
306+
var newData = AssertHelpers.ExpectJsonValue(
307+
_updateSink.Inits,
308+
DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)),
309+
DataSetAsJson,
310+
TimeSpan.FromSeconds(30));
311+
312+
AssertJsonEqual(DataSetAsJson(ExpectedDataSetForSegmentOnlyFile(2)), DataSetAsJson(newData));
299313
}
300314
}
301315
}

pkgs/sdk/server/test/Internal/DataSources/PollingDataSourceTest.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ public void SuccessfulRequestCausesDataToBeStoredAndDataSourceInitializedMetadat
9696
// the http server as it isn't implemented in this package.
9797
Assert.NotEmpty(receivedData.Item2);
9898

99-
Assert.True(dataSource.Initialized);
99+
// Wait for initialization to complete before checking Initialized flag
100+
// to avoid race condition where data is received but flag not yet set
101+
bool completed = initTask.Wait(TimeSpan.FromSeconds(1));
102+
Assert.True(completed);
100103

101-
Assert.True(initTask.IsCompleted);
104+
Assert.True(dataSource.Initialized);
102105
Assert.False(initTask.IsFaulted);
103106
}
104107
}

0 commit comments

Comments
 (0)