513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
79 lines
3.3 KiB
C#
79 lines
3.3 KiB
C#
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Interfaces.Services;
|
|
|
|
/// <summary>
|
|
/// Tests for <see cref="ExternalCallResult"/>, in particular the
|
|
/// thread-safe lazy parse of <c>Response</c>. The pre-fix implementation used
|
|
/// two mutable fields (<c>_response</c>/<c>_responseParsed</c>) with no
|
|
/// synchronization, so concurrent readers could each construct a fresh
|
|
/// <c>DynamicJsonElement</c> and one would overwrite the other. The fix moves
|
|
/// the parse onto a <c>Lazy<dynamic?></c> with
|
|
/// <c>LazyThreadSafetyMode.ExecutionAndPublication</c> (the default), which
|
|
/// guarantees one parse and one shared result for all readers.
|
|
/// </summary>
|
|
public class ExternalCallResultTests
|
|
{
|
|
/// <summary>Verifies that a null or empty response JSON yields a null response.</summary>
|
|
[Fact]
|
|
public void Response_NullOrEmptyJson_ReturnsNull()
|
|
{
|
|
var withNull = new ExternalCallResult(Success: true, ResponseJson: null, ErrorMessage: null);
|
|
var withEmpty = new ExternalCallResult(Success: true, ResponseJson: string.Empty, ErrorMessage: null);
|
|
|
|
Assert.Null(withNull.Response);
|
|
Assert.Null(withEmpty.Response);
|
|
}
|
|
|
|
/// <summary>Verifies that the response JSON is parsed into a dynamic element.</summary>
|
|
[Fact]
|
|
public void Response_ParsesJsonIntoDynamicElement()
|
|
{
|
|
var result = new ExternalCallResult(Success: true, ResponseJson: "{\"answer\": 42}", ErrorMessage: null);
|
|
|
|
// dynamic property access is the production usage pattern.
|
|
dynamic? response = result.Response;
|
|
Assert.NotNull(response);
|
|
int answer = (int)response!.answer;
|
|
Assert.Equal(42, answer);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Concurrent readers must observe the same parsed instance
|
|
/// (a `Lazy<T>` invariant). Under the pre-fix code two threads could
|
|
/// both produce a fresh `DynamicJsonElement` and one would win the race —
|
|
/// `ReferenceEquals` would then occasionally fail. With the fix every
|
|
/// reader observes the single Lazy-published value, so the assertion
|
|
/// holds for every pair of observers.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Response_ConcurrentReads_ReturnSameInstance()
|
|
{
|
|
// A larger payload makes the parse window wider so the race, if
|
|
// present, is more likely to fire. The same property — single
|
|
// published instance — must hold for any payload, though.
|
|
var json = "{\"items\":[{\"name\":\"a\"},{\"name\":\"b\"},{\"name\":\"c\"}],\"count\":3}";
|
|
var result = new ExternalCallResult(Success: true, ResponseJson: json, ErrorMessage: null);
|
|
|
|
const int observerCount = 64;
|
|
var barrier = new Barrier(observerCount);
|
|
var observed = new object?[observerCount];
|
|
|
|
Parallel.For(0, observerCount, i =>
|
|
{
|
|
// Force all observers to call `Response` at the same instant so
|
|
// they collide on the lazy parse rather than each finding it
|
|
// already-published.
|
|
barrier.SignalAndWait();
|
|
observed[i] = result.Response;
|
|
});
|
|
|
|
var first = observed[0];
|
|
Assert.NotNull(first);
|
|
for (var i = 1; i < observerCount; i++)
|
|
{
|
|
Assert.Same(first, observed[i]);
|
|
}
|
|
}
|
|
}
|