using ScadaLink.Commons.Interfaces.Services; namespace ScadaLink.Commons.Tests.Interfaces.Services; /// /// Tests for , in particular the Commons-021 /// thread-safe lazy parse of Response. The pre-fix implementation used /// two mutable fields (_response/_responseParsed) with no /// synchronization, so concurrent readers could each construct a fresh /// DynamicJsonElement and one would overwrite the other. The fix moves /// the parse onto a Lazy<dynamic?> with /// LazyThreadSafetyMode.ExecutionAndPublication (the default), which /// guarantees one parse and one shared result for all readers. /// public class ExternalCallResultTests { [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); } [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); } /// /// Commons-021: 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. /// [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]); } } }