tbf/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderThreadedReadTests.cs
Michal Buzik 0b73871518 Refactor GenesisSmartReader and add advanced test utilities:
- Introduce `ForceProcessingEnabled`, `InstallSuccessfulProcessingOverride`, and `EnqueueIncomingLine` helpers to streamline test setup.
- Add new test suite `GenesisSmartReaderStopTests` to validate `Stop` behavior for different processing states.
- Enhance `GenesisSmartReader` state management with a dedicated `Reset` method and improved `Stop` logic.
- Improve buffer initialization in `ProcessOptoLine` to address null-value edge cases.
- Update assembly version to `3.9.3043.1`.
2026-04-08 13:51:55 +02:00

389 lines
14 KiB
C#

using System;
using System.Reflection;
using System.Threading;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.GenesisRegReader;
using TBF.Rig.RegisterReaders.GenesisRegReader.common;
using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations
{
[TestClass]
public class GenesisSmartReaderThreadedReadTests
{
private const int ChannelCount = 3;
private static GenesisCfg CreateCfg()
{
var cfg = new GenesisCfg(null);
cfg.OptoComPortNr = 7;
cfg.RfidComPortNr = 8;
return cfg;
}
private static void SetPrivateField(object target, string fieldName, object value)
{
var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null)
throw new MissingFieldException(target.GetType().FullName, fieldName);
field.SetValue(target, value);
}
private static T GetPrivateField<T>(object target, string fieldName)
{
var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null)
throw new MissingFieldException(target.GetType().FullName, fieldName);
return (T)field.GetValue(target);
}
private static bool WaitUntil(Func<bool> condition, int timeoutMs = 1500, int pollMs = 20)
{
var start = Environment.TickCount;
while (Environment.TickCount - start < timeoutMs)
{
if (condition())
return true;
Thread.Sleep(pollMs);
}
return condition();
}
private static void InitializeThreeChannelState(GenesisSmartReader reader)
{
SetPrivateField(reader, "volumeRawExtLast", new double[ChannelCount]);
SetPrivateField(reader, "timestampExtLast", new double[ChannelCount]);
SetPrivateField(reader, "lastVolumeRaw", new double[ChannelCount]);
SetPrivateField(reader, "lastTimestamp", new double[ChannelCount]);
SetPrivateField(reader, "volumeLtr", new[] { 0.0, 0.0, 0.0 });
SetPrivateField(reader, "volumeLtr0", new[] { 0.0, 0.0, 0.0 });
SetPrivateField(reader, "timestampSec", new[] { 0.0, 0.0, 0.0 });
SetPrivateField(reader, "timestampSec0", new[] { 0.0, 0.0, 0.0 });
}
private static FieldInfo GetAnyField(object target, string fieldName)
{
var field = target.GetType().GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (field == null)
throw new MissingFieldException(target.GetType().FullName, fieldName);
return field;
}
private static void SetField(object target, string fieldName, object value)
{
GetAnyField(target, fieldName).SetValue(target, value);
}
private static T GetField<T>(object target, string fieldName)
{
return (T)GetAnyField(target, fieldName).GetValue(target);
}
private static void WaitForStartupFlushToFinish(GenesisSmartReader reader, int timeoutMs = 3000)
{
bool finished = WaitUntil(() =>
{
return !GetPrivateField<bool>(reader, "_startupFlushActive");
}, timeoutMs);
Assert.IsTrue(finished, "Startup flush did not finish in time.");
}
private static void ForceProcessingEnabled(GenesisSmartReader reader)
{
SetField(reader, "_startupFlushActive", false);
SetField(reader, "_stopQueueData", false);
SetField(reader, "dataStreamState", DataStreamState.ProcessAndSave);
SetField(reader, "startDataProcessing", true);
var signal = GetField<AutoResetEvent>(reader, "_queueSignal");
signal.Set();
}
private static void EnqueueIncomingLine(GenesisSmartReader reader, string line)
{
var queue = GetField<System.Collections.Concurrent.ConcurrentQueue<(DateTime Timestamp, string Line)>>(
reader, "_receivedLines");
var signal = GetField<AutoResetEvent>(reader, "_queueSignal");
queue.Enqueue((DateTime.UtcNow, line));
signal.Set();
}
private static void InstallSuccessfulProcessingOverride(GenesisSmartReader reader)
{
Func<string, bool> handler = line =>
{
var optoData = GetField<OptoTelegramRaw[]>(reader, "optoData");
int count = GetField<int>(reader, "optoDataCount");
optoData[count].Counter = count;
optoData[count].SetFlags(OptoTelegramFlags.OK);
SetField(reader, "optoDataCount", count + 1);
return true;
};
SetField(reader, "TestProcessLineOverride", handler);
}
[TestMethod]
public void Start_ShouldOpenPort_AndStartBackgroundReadLoop()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
reader.Start();
Assert.IsTrue(fake.IsOpen);
Assert.IsTrue(fake.OpenCalls >= 1);
var readLoopTask = GetPrivateField<System.Threading.Tasks.Task>(reader, "_readLoopTask");
Assert.IsNotNull(readLoopTask);
}
[TestMethod]
public void ProcessingLoop_ShouldConsumeQueuedLine()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
ForceProcessingEnabled(reader);
InstallSuccessfulProcessingOverride(reader);
EnqueueIncomingLine(reader, "TEST_LINE_1");
bool processed = WaitUntil(() =>
{
int optoDataCount = GetField<int>(reader, "optoDataCount");
return optoDataCount == 1;
}, 3000);
Assert.IsTrue(processed, "Expected processing loop to consume queued line.");
int finalCount = GetField<int>(reader, "optoDataCount");
Assert.AreEqual(1, finalCount);
}
[TestMethod]
public void ProcessingLoop_ShouldConsumeQueuedLine_AndInsertTelegram()
{
var fake = new FakeSerialDriver();
fake.Encoding = System.Text.Encoding.ASCII;
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
ForceProcessingEnabled(reader);
EnqueueIncomingLine(reader,
"@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD");
bool processed = WaitUntil(() =>
{
int optoDataCount = GetPrivateField<int>(reader, "optoDataCount");
return optoDataCount >= 1;
}, 3000);
Assert.IsTrue(processed, "Expected processing loop to consume queued line.");
int finalCount = GetPrivateField<int>(reader, "optoDataCount");
var optoData = GetPrivateField<OptoTelegramRaw[]>(reader, "optoData");
Assert.AreEqual(1, finalCount);
Assert.AreEqual(1, optoData[0].IChannel());
}
[TestMethod]
public void ProcessingLoop_ShouldConsumeAllQueuedLines()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
ForceProcessingEnabled(reader);
InstallSuccessfulProcessingOverride(reader);
EnqueueIncomingLine(reader, "TEST_LINE_1");
EnqueueIncomingLine(reader, "TEST_LINE_2");
EnqueueIncomingLine(reader, "TEST_LINE_3");
bool processed = WaitUntil(() =>
{
int optoDataCount = GetField<int>(reader, "optoDataCount");
return optoDataCount == 3;
}, 3000);
Assert.IsTrue(processed, "Expected all queued lines to be processed.");
int finalCount = GetField<int>(reader, "optoDataCount");
var queueAfter = GetField<System.Collections.Concurrent.ConcurrentQueue<(DateTime Timestamp, string Line)>>(
reader, "_receivedLines");
Assert.AreEqual(3, finalCount);
Assert.IsTrue(queueAfter.IsEmpty);
}
[TestMethod]
public void IncomingLines_ShouldBeProcessedByBackgroundProcessingLoop()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
WaitForStartupFlushToFinish(reader);
fake.EnqueueLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E");
bool processed = WaitUntil(() =>
{
int optoDataCount = GetPrivateField<int>(reader, "optoDataCount");
return optoDataCount >= 1;
}, 3000);
Assert.IsTrue(processed, "Expected background processing loop to process line.");
}
[TestMethod]
public void StopDataStreamProcessing_ShouldStopReadAndProcessingLoops()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
reader.StopDataStreamProcessing();
var readLoopTask = GetPrivateField<System.Threading.Tasks.Task>(reader, "_readLoopTask");
var readLoopCts = GetPrivateField<CancellationTokenSource>(reader, "_readLoopCts");
var processLoopTask = GetPrivateField<System.Threading.Tasks.Task>(reader, "_processLoopTask");
var processLoopCts = GetPrivateField<CancellationTokenSource>(reader, "_processLoopCts");
Assert.IsNull(readLoopTask);
Assert.IsNull(readLoopCts);
Assert.IsNull(processLoopTask);
Assert.IsNull(processLoopCts);
}
[TestMethod]
public void IncomingLine_AfterStop_ShouldNotBeProcessed()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
reader.StopDataStreamProcessing();
int before = GetPrivateField<int>(reader, "optoDataCount");
fake.EnqueueLine("@h 1 0 0A1F59C4 00017A43 00115C45 72E1596B 00000400 00001998 7D91B652 7D4F37E6 000191E6 0C 062E4A9C 5331");
Thread.Sleep(300);
int after = GetPrivateField<int>(reader, "optoDataCount");
Assert.AreEqual(before, after, "No new telegram should be processed after stop.");
}
[TestMethod]
public void InvalidLine_ShouldNotCrashProcessingLoop()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
WaitForStartupFlushToFinish(reader);
fake.EnqueueLine("INVALID_LINE_THAT_WILL_NOT_PARSE");
fake.EnqueueLine("@h 3 0 0A1DF1D5 00017EA8 00118037 741F80F3 00000400 00001998 7E58A62E 7CC2C606 000191E6 0C 062E5BAE D40E");
bool processed = WaitUntil(() =>
{
int optoDataCount = GetPrivateField<int>(reader, "optoDataCount");
return optoDataCount >= 1;
}, 3000);
Assert.IsTrue(processed, "Processing loop should survive invalid input and continue.");
}
[TestMethod]
public void StartStop_StartAgain_ShouldStillProcessData()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
InitializeThreeChannelState(reader);
reader.Start();
reader.StopDataStreamProcessing();
reader.Start();
WaitForStartupFlushToFinish(reader);
fake.EnqueueLine("@h 2 0 0A1B1FE8 00017B7F 00116B56 72B77427 00000400 0000199A 7DC09688 7B570006 000191E6 0C 062E5326 95BD");
bool processed = WaitUntil(() =>
{
int optoDataCount = GetPrivateField<int>(reader, "optoDataCount");
return optoDataCount >= 1;
}, 3000);
Assert.IsTrue(processed, "Reader should still process data after restart.");
}
[TestMethod]
public void Start_ShouldOpenPort_AndStartBothBackgroundLoops()
{
var fake = new FakeSerialDriver();
var reader = new GenesisSmartReader(CreateCfg(), () => fake);
reader.Initialize();
reader.Start();
Assert.IsTrue(fake.IsOpen);
Assert.IsTrue(fake.OpenCalls >= 1);
var readLoopTask = GetPrivateField<System.Threading.Tasks.Task>(reader, "_readLoopTask");
var processLoopTask = GetPrivateField<System.Threading.Tasks.Task>(reader, "_processLoopTask");
Assert.IsNotNull(readLoopTask);
Assert.IsNotNull(processLoopTask);
}
}
}