tbf/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReaderChannelTests.cs
Michal Buzik 7e3df77b5b Refactor GenesisSmartReader processing logic and tests for robust handling:
- Replace property getters with concise expressions in `GenesisSmartReader`.
- Implement FIFO and concurrent producer-consumer unit tests for `ProcessingLoop`.
- Add startup flush mechanism for improved input handling on initialization.
- Refactor processing loop with structured start/stop tasks and cancellation tokens.
- Fix thread-safety issues and enhance logic in `Stop` and `StopDataStreamProcessing`.
- Add internal visibility for tests in `TBFTests` via `InternalsVisibleTo`.
- Update assembly version to `3.9.3038.1`.
2026-04-07 10:42:27 +02:00

511 lines
17 KiB
C#

using System;
using System.Linq;
using System.Reflection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.GenesisRegReader.common;
using TBF.Rig.RegisterReaders.GenesisRegReader.implementations;
namespace TBFTests.Rig.RegisterReaders.GenesisRegReader.implementations
{
[TestClass]
public class GenesisSmartReaderChannelTests
{
private GenesisSmartReader _reader;
[TestInitialize]
public void Setup()
{
_reader = new GenesisSmartReader();
}
private static OptoTelegramRaw Rec(
int channel,
double time,
double volume,
OptoTelegramFlags flags = OptoTelegramFlags.OK)
{
return new OptoTelegramRaw
{
iChannel = channel,
TimestampExt = time,
VolumeRawExt = volume,
Flags = flags
};
}
private object InvokePrivate(string methodName, params object[] args)
{
var mi = typeof(GenesisSmartReader).GetMethod(
methodName,
BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(mi, $"Method '{methodName}' was not found.");
return mi.Invoke(_reader, args);
}
private static T GetPrivateField<T>(object instance, string fieldName)
{
var fi = instance.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(fi, $"Field '{fieldName}' was not found.");
return (T)fi.GetValue(instance);
}
private static void SetPrivateField(object instance, string fieldName, object value)
{
var fi = instance.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(fi, $"Field '{fieldName}' was not found.");
fi.SetValue(instance, value);
}
[TestMethod]
public void GroupRecordsPerChannel_Groups_Valid_Records_By_Channel()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(2, 12, 300),
Rec(0, 13, 110),
Rec(1, 14, 220),
Rec(2, 15, 330),
};
var grouped = (OptoTelegramRaw[][])InvokePrivate(
"GroupRecordsPerChannel",
(object)input, 0, input.Length - 1, input.Length);
Assert.AreEqual(3, grouped.Length);
Assert.AreEqual(2, grouped[0].Length);
Assert.AreEqual(2, grouped[1].Length);
Assert.AreEqual(2, grouped[2].Length);
Assert.AreEqual(100, grouped[0][0].VolumeRawExt);
Assert.AreEqual(110, grouped[0][1].VolumeRawExt);
Assert.AreEqual(200, grouped[1][0].VolumeRawExt);
Assert.AreEqual(220, grouped[1][1].VolumeRawExt);
Assert.AreEqual(300, grouped[2][0].VolumeRawExt);
Assert.AreEqual(330, grouped[2][1].VolumeRawExt);
}
[TestMethod]
public void GroupRecordsPerChannel_Ignores_Invalid_Records()
{
var input = new[]
{
Rec(0, 10, 100, OptoTelegramFlags.OK),
Rec(1, 11, 200, OptoTelegramFlags.InvalidTelegram),
Rec(2, 12, 300, OptoTelegramFlags.OK),
Rec(0, 13, 110, OptoTelegramFlags.SyncError),
Rec(1, 14, 220, OptoTelegramFlags.OK),
};
var grouped = (OptoTelegramRaw[][])InvokePrivate(
"GroupRecordsPerChannel",
(object)input, 0, input.Length - 1, input.Length);
Assert.AreEqual(1, grouped[0].Length);
Assert.AreEqual(1, grouped[1].Length);
Assert.AreEqual(1, grouped[2].Length);
Assert.AreEqual(100, grouped[0][0].VolumeRawExt);
Assert.AreEqual(220, grouped[1][0].VolumeRawExt);
Assert.AreEqual(300, grouped[2][0].VolumeRawExt);
}
[TestMethod]
public void GroupRecordsPerChannel_Swaps_Start_And_End_When_Reversed()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(0, 12, 120),
};
var grouped = (OptoTelegramRaw[][])InvokePrivate(
"GroupRecordsPerChannel",
(object)input, 2, 0, input.Length);
Assert.AreEqual(2, grouped[0].Length);
Assert.AreEqual(1, grouped[1].Length);
Assert.AreEqual(0, grouped[2].Length);
}
[TestMethod]
public void GroupRecordsPerChannel_Returns_Empty_For_Invalid_Range()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
};
var grouped = (OptoTelegramRaw[][])InvokePrivate(
"GroupRecordsPerChannel",
(object)input, -1, 1, input.Length);
Assert.AreEqual(3, grouped.Length);
Assert.IsTrue(grouped.All(x => x.Length == 0));
}
[TestMethod]
public void TimeDeltaPerChannel_Computes_First_Last_Deltas()
{
var grouped = new[]
{
new[]
{
Rec(0, 10, 100),
Rec(0, 18, 170),
Rec(0, 15, 120)
},
new[]
{
Rec(1, 20, 500),
Rec(1, 23, 530)
},
new[]
{
Rec(2, 30, 900)
}
};
var result = (ValueTuple<int, double?, double?>[])InvokePrivate(
"TimeDeltaPerChannel",
(object)grouped);
Assert.AreEqual(0, result[0].Item1);
Assert.AreEqual(5, result[0].Item2);
Assert.AreEqual(20, result[0].Item3);
Assert.AreEqual(1, result[1].Item1);
Assert.AreEqual(3, result[1].Item2);
Assert.AreEqual(30, result[1].Item3);
Assert.AreEqual(2, result[2].Item1);
Assert.IsNull(result[2].Item2);
Assert.IsNull(result[2].Item3);
}
[TestMethod]
public void VolumeStartPerChannel_Returns_Last_Seen_Value_Backward_From_Index()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(0, 12, 120),
Rec(2, 13, 300),
Rec(1, 14, 220),
};
var result = (double?[])InvokePrivate(
"VolumeStartPerChannel",
(object)input, input.Length, 4);
Assert.AreEqual(120, result[0]);
Assert.AreEqual(220, result[1]);
Assert.AreEqual(300, result[2]);
}
[TestMethod]
public void TimeStartPerChannel_Returns_Last_Seen_Time_Backward_From_Index()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(0, 12, 120),
Rec(2, 13, 300),
Rec(1, 14, 220),
};
var result = (double?[])InvokePrivate(
"TimeStartPerChannel",
(object)input, input.Length, 4);
Assert.AreEqual(12, result[0]);
Assert.AreEqual(14, result[1]);
Assert.AreEqual(13, result[2]);
}
[TestMethod]
public void VolumeEndPerChannel_Averages_Last_N_Samples_Per_Channel()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(2, 12, 300),
Rec(0, 13, 120),
Rec(1, 14, 220),
Rec(2, 15, 330),
Rec(0, 16, 140),
Rec(1, 17, 240),
Rec(2, 18, 360),
};
var result = (double?[])InvokePrivate(
"VolumeEndPerChannel",
(object)input, input.Length, 8, 2);
Assert.AreEqual((140 + 120) / 2.0, result[0]);
Assert.AreEqual((240 + 220) / 2.0, result[1]);
Assert.AreEqual((360 + 330) / 2.0, result[2]);
}
[TestMethod]
public void TimeEndPerChannel_Averages_Last_N_Samples_Per_Channel()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(2, 12, 300),
Rec(0, 13, 120),
Rec(1, 14, 220),
Rec(2, 15, 330),
Rec(0, 16, 140),
Rec(1, 17, 240),
Rec(2, 18, 360),
};
var result = (double?[])InvokePrivate(
"TimeEndPerChannel",
(object)input, input.Length, 8, 2);
Assert.AreEqual((16 + 13) / 2.0, result[0]);
Assert.AreEqual((17 + 14) / 2.0, result[1]);
Assert.AreEqual((18 + 15) / 2.0, result[2]);
}
[TestMethod]
public void VolumeStartPerChannel_Returns_All_Nulls_For_OutOfRange_Index()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(2, 12, 300),
};
var result = (double?[])InvokePrivate(
"VolumeStartPerChannel",
(object)input, input.Length, -1);
Assert.IsNull(result[0]);
Assert.IsNull(result[1]);
Assert.IsNull(result[2]);
}
[TestMethod]
public void TimeEndPerChannel_Uses_One_Sample_When_SamplesPerChannel_Is_Zero()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(2, 12, 300),
Rec(0, 13, 120),
Rec(1, 14, 220),
Rec(2, 15, 330),
};
var result = (double?[])InvokePrivate(
"TimeEndPerChannel",
(object)input, input.Length, 5, 0);
Assert.AreEqual(13, result[0]);
Assert.AreEqual(14, result[1]);
Assert.AreEqual(15, result[2]);
}
[TestMethod]
public void Precreated_OptoData_Private_Methods_Read_Correct_Values()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(2, 12, 300),
Rec(0, 13, 120),
Rec(1, 14, 220),
Rec(2, 15, 330),
Rec(0, 16, 140),
Rec(1, 17, 240),
Rec(2, 18, 360),
};
SetPrivateField(_reader, "optoData", input);
SetPrivateField(_reader, "optoDataCount", input.Length);
_reader.TestStartTelegramIx = 5;
_reader.TestEndTelegramIx = 8;
Assert.AreEqual(input.Length, GetPrivateField<int>(_reader, "optoDataCount"));
var startVolumes = (double?[])InvokePrivate(
"VolumeStartPerChannel",
(object)input, input.Length, 5);
var endVolumes = (double?[])InvokePrivate(
"VolumeEndPerChannel",
(object)input, input.Length, 8, 2 * GenesisSmartReader.StartEndFilterSamplesCount2 + 1);
var startTimes = (double?[])InvokePrivate(
"TimeStartPerChannel",
(object)input, input.Length, 5);
var endTimes = (double?[])InvokePrivate(
"TimeEndPerChannel",
(object)input, input.Length, 8, 2 * GenesisSmartReader.StartEndFilterSamplesCount2 + 1);
Assert.AreEqual(120, startVolumes[0]);
Assert.AreEqual(220, startVolumes[1]);
Assert.AreEqual(330, startVolumes[2]);
Assert.AreEqual(13, startTimes[0]);
Assert.AreEqual(14, startTimes[1]);
Assert.AreEqual(15, startTimes[2]);
Assert.AreEqual((140 + 120 + 100) / 3.0, endVolumes[0]);
Assert.AreEqual((240 + 220 + 200) / 3.0, endVolumes[1]);
Assert.AreEqual((360 + 330 + 300) / 3.0, endVolumes[2]);
Assert.AreEqual((16 + 13 + 10) / 3.0, endTimes[0]);
Assert.AreEqual((17 + 14 + 11) / 3.0, endTimes[1]);
Assert.AreEqual((18 + 15 + 12) / 3.0, endTimes[2]);
}
[TestMethod]
public void Public_Channel_Properties_Default_To_Zero_When_NoSamples_Is_True()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 11, 200),
Rec(2, 12, 300),
};
SetPrivateField(_reader, "optoData", input);
SetPrivateField(_reader, "optoDataCount", input.Length);
_reader.TestStartTelegramIx = 5;
_reader.TestEndTelegramIx = 8;
Assert.IsTrue(_reader.NoSamples);
Assert.AreEqual(0, _reader.VolumeLtrStartCh1);
Assert.AreEqual(0, _reader.VolumeLtrStartCh2);
Assert.AreEqual(0, _reader.VolumeLtrStartCh3);
Assert.AreEqual(0, _reader.VolumeLtrEndCh1);
Assert.AreEqual(0, _reader.VolumeLtrEndCh2);
Assert.AreEqual(0, _reader.VolumeLtrEndCh3);
Assert.AreEqual(0, _reader.TimestampSecStartCh1);
Assert.AreEqual(0, _reader.TimestampSecStartCh2);
Assert.AreEqual(0, _reader.TimestampSecStartCh3);
Assert.AreEqual(0, _reader.TimestampSecEndCh1);
Assert.AreEqual(0, _reader.TimestampSecEndCh2);
Assert.AreEqual(0, _reader.TimestampSecEndCh3);
}
[TestMethod]
public void Copy_Should_Copy_From_Source_Record()
{
var source = Rec(2, 123.5, 456.7, OptoTelegramFlags.OK_TestEnd);
source.Counter = 77;
source.RefFlow = 9.5f;
source.FlowRaw = 111;
source.VolumeRaw = 222;
source.Impedance = 333;
source.CheckSum = 44;
var target = new OptoTelegramRaw();
target.Copy(source);
Assert.AreEqual(source.iChannel, target.iChannel);
Assert.AreEqual(source.TimestampExt, target.TimestampExt);
Assert.AreEqual(source.VolumeRawExt, target.VolumeRawExt);
Assert.AreEqual(source.Flags, target.Flags);
Assert.AreEqual(source.Counter, target.Counter);
Assert.AreEqual(source.RefFlow, target.RefFlow);
Assert.AreEqual(source.FlowRaw, target.FlowRaw);
Assert.AreEqual(source.VolumeRaw, target.VolumeRaw);
Assert.AreEqual(source.Impedance, target.Impedance);
Assert.AreEqual(source.CheckSum, target.CheckSum);
}
[TestMethod]
public void RecalculateVolumeAndTimeDeltaPerChannel_Returns_Recalculated_Output()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(0, 20, 200),
Rec(1, 10, 500),
Rec(1, 30, 560),
Rec(2, 10, 1000),
Rec(2, 15, 1050),
};
var result = (OptoTelegramRaw[][])InvokePrivate(
"RecalculateVolumeAndTimeDeltaPerChannel",
(object)input,
input.Length,
0,
input.Length - 1);
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Length);
for (int ch = 0; ch < 3; ch++)
{
Assert.IsNotNull(result[ch]);
Assert.AreEqual(2, result[ch].Length);
Assert.IsNotNull(result[ch][0]);
Assert.IsNotNull(result[ch][1]);
Assert.AreEqual(10, result[ch][0].TimestampExt, 0.0001);
Assert.AreEqual(30, result[ch][1].TimestampExt, 0.0001);
}
Assert.AreEqual(100, result[0][0].VolumeRawExt, 0.0001);
Assert.AreEqual(300, result[0][1].VolumeRawExt, 0.0001);
Assert.AreEqual(500, result[1][0].VolumeRawExt, 0.0001);
Assert.AreEqual(560, result[1][1].VolumeRawExt, 0.0001);
Assert.AreEqual(1000, result[2][0].VolumeRawExt, 0.0001);
Assert.AreEqual(1200, result[2][1].VolumeRawExt, 0.0001);
}
[TestMethod]
public void RecalculateVolumeAndTimeDeltaPerChannel_Returns_Null_When_No_Channel_Has_Two_Records()
{
var input = new[]
{
Rec(0, 10, 100),
Rec(1, 20, 200),
Rec(2, 30, 300),
};
var result = (OptoTelegramRaw[][])InvokePrivate(
"RecalculateVolumeAndTimeDeltaPerChannel",
(object)input,
input.Length,
0,
input.Length - 1);
Assert.IsNull(result);
}
}
}