158 lines
5.1 KiB
C#
158 lines
5.1 KiB
C#
using Xunit;
|
|
using ZB.MOM.WW.LmxProxy.Client.Domain;
|
|
using ZB.MOM.WW.LmxProxy.Client.Tests.Fakes;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Client.Tests;
|
|
|
|
public class StreamingExtensionsTests
|
|
{
|
|
[Fact]
|
|
public async Task ReadStreamAsync_BatchesCorrectly()
|
|
{
|
|
var fake = new FakeLmxProxyClient();
|
|
var addresses = Enumerable.Range(0, 250).Select(i => $"tag{i}").ToList();
|
|
|
|
var results = new List<KeyValuePair<string, Vtq>>();
|
|
await foreach (var kvp in fake.ReadStreamAsync(addresses, batchSize: 100))
|
|
{
|
|
results.Add(kvp);
|
|
}
|
|
|
|
// 250 tags at batchSize=100 => 3 batch calls (100, 100, 50)
|
|
Assert.Equal(3, fake.ReadBatchCalls.Count);
|
|
Assert.Equal(100, fake.ReadBatchCalls[0].Count);
|
|
Assert.Equal(100, fake.ReadBatchCalls[1].Count);
|
|
Assert.Equal(50, fake.ReadBatchCalls[2].Count);
|
|
Assert.Equal(250, results.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReadStreamAsync_RetriesOnError()
|
|
{
|
|
var fake = new FakeLmxProxyClient
|
|
{
|
|
ReadBatchExceptionToThrow = new InvalidOperationException("transient"),
|
|
ReadBatchExceptionCount = 1 // First call throws, second succeeds
|
|
};
|
|
|
|
var addresses = Enumerable.Range(0, 5).Select(i => $"tag{i}").ToList();
|
|
var results = new List<KeyValuePair<string, Vtq>>();
|
|
await foreach (var kvp in fake.ReadStreamAsync(addresses, batchSize: 10))
|
|
{
|
|
results.Add(kvp);
|
|
}
|
|
|
|
// Should retry: first call throws, second succeeds
|
|
Assert.Equal(2, fake.ReadBatchCalls.Count);
|
|
Assert.Equal(5, results.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task WriteStreamAsync_BatchesAndReturnsCount()
|
|
{
|
|
var fake = new FakeLmxProxyClient();
|
|
var values = GenerateWriteValues(250);
|
|
|
|
int total = await fake.WriteStreamAsync(values, batchSize: 100);
|
|
|
|
Assert.Equal(250, total);
|
|
Assert.Equal(3, fake.WriteBatchCalls.Count);
|
|
Assert.Equal(100, fake.WriteBatchCalls[0].Count);
|
|
Assert.Equal(100, fake.WriteBatchCalls[1].Count);
|
|
Assert.Equal(50, fake.WriteBatchCalls[2].Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ProcessInParallelAsync_RespectsMaxConcurrency()
|
|
{
|
|
int maxConcurrency = 2;
|
|
int currentConcurrency = 0;
|
|
int maxObservedConcurrency = 0;
|
|
var lockObj = new object();
|
|
|
|
var source = GenerateAsyncSequence(10);
|
|
|
|
await source.ProcessInParallelAsync(async (item, ct) =>
|
|
{
|
|
int current;
|
|
lock (lockObj)
|
|
{
|
|
currentConcurrency++;
|
|
current = currentConcurrency;
|
|
if (current > maxObservedConcurrency)
|
|
maxObservedConcurrency = current;
|
|
}
|
|
|
|
await Task.Delay(50, ct);
|
|
|
|
lock (lockObj)
|
|
{
|
|
currentConcurrency--;
|
|
}
|
|
}, maxConcurrency: maxConcurrency);
|
|
|
|
Assert.True(maxObservedConcurrency <= maxConcurrency,
|
|
$"Max observed concurrency {maxObservedConcurrency} exceeded limit {maxConcurrency}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SubscribeStreamAsync_YieldsFromChannel()
|
|
{
|
|
var fake = new FakeLmxProxyClient();
|
|
var addresses = new[] { "tag1", "tag2" };
|
|
using var cts = new CancellationTokenSource();
|
|
|
|
var results = new List<(string Tag, Vtq Vtq)>();
|
|
|
|
// Start the subscription stream in a background task
|
|
var streamTask = Task.Run(async () =>
|
|
{
|
|
await foreach (var item in fake.SubscribeStreamAsync(addresses, cts.Token))
|
|
{
|
|
results.Add(item);
|
|
if (results.Count >= 3)
|
|
await cts.CancelAsync();
|
|
}
|
|
});
|
|
|
|
// Wait for subscribe to be called with a polling loop
|
|
for (int i = 0; i < 50 && fake.CapturedOnUpdate is null; i++)
|
|
await Task.Delay(50);
|
|
|
|
// Simulate updates via captured callback
|
|
Assert.NotNull(fake.CapturedOnUpdate);
|
|
fake.CapturedOnUpdate!("tag1", new Vtq(1.0, DateTime.UtcNow, Quality.Good));
|
|
fake.CapturedOnUpdate!("tag2", new Vtq(2.0, DateTime.UtcNow, Quality.Good));
|
|
fake.CapturedOnUpdate!("tag1", new Vtq(3.0, DateTime.UtcNow, Quality.Good));
|
|
|
|
// Wait for stream task to complete (cancelled after 3 items)
|
|
try { await streamTask; }
|
|
catch (OperationCanceledException) { }
|
|
|
|
Assert.Equal(3, results.Count);
|
|
Assert.Equal("tag1", results[0].Tag);
|
|
Assert.Equal("tag2", results[1].Tag);
|
|
Assert.Equal("tag1", results[2].Tag);
|
|
}
|
|
|
|
private static async IAsyncEnumerable<KeyValuePair<string, TypedValue>> GenerateWriteValues(int count)
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
yield return new KeyValuePair<string, TypedValue>(
|
|
$"tag{i}",
|
|
new TypedValue { DoubleValue = i * 1.0 });
|
|
await Task.Yield();
|
|
}
|
|
}
|
|
|
|
private static async IAsyncEnumerable<int> GenerateAsyncSequence(int count)
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
yield return i;
|
|
await Task.Yield();
|
|
}
|
|
}
|
|
}
|