tbf/TBFTests/Rig/RegisterReaders/GenesisRegReader/implementations/GenesisSmartReader_Q3Test.cs
Michal Buzik 5a8478e782 Refactor GenesisSmartReader Q3 calibration tests and logic:
- Expand Q3 calibration tests with diverse scenarios in `GenesisSmartReader_Q3Test`.
- Introduce channel-specific validation for calibration tests.
- Adjust methods for Q3 calibration in `GenesisSmartReader` to support per-channel factors.
- Add utilities for preparing simulation data and refinements for test consistency.
- Update `CliRunner` with improved task management, timeout handling, and logging enhancements.
2026-04-17 16:05:16 +02:00

427 lines
15 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 GenesisSmartReader_Q3Test
{
private static readonly double[] ValidInitFactors = { 15625.0, 15625.0, 15625.0 };
[TestMethod]
public void GetQ3Calibration_ShouldRemainInvalid_WhenRawDataIsNull()
{
var sut = new GenesisSmartReader();
InitializeReaderForQ3Test(sut);
SetPrivateField(sut, "_rawStartEndByChannel", null);
var valid = new bool[3];
var calib = new double[3];
sut.GetQ3Calibration(
refVolume: 1000.0,
refTime: 120.0,
initCalibFactor: ValidInitFactors,
ref valid,
ref calib);
Assert.IsFalse(sut.Q3CalibValid);
CollectionAssert.AreEqual(new[] { false, false, false }, valid);
CollectionAssert.AreEqual(new[] { 0.0, 0.0, 0.0 }, calib);
}
[TestMethod]
public void GetQ3Calibration_ShouldRemainInvalid_WhenNoSamplesIsTrue()
{
var sut = new GenesisSmartReader();
InitializeReaderForQ3Test(sut);
SetPrivateField(sut, "_rawStartEndByChannel", CreateMinimalStartEndData());
Assert.IsTrue(sut.NoSamples, "Expected NoSamples to be true for this test.");
var valid = new bool[3];
var calib = new double[3];
sut.GetQ3Calibration(
refVolume: 1000.0,
refTime: 120.0,
initCalibFactor: ValidInitFactors,
ref valid,
ref calib);
Assert.IsFalse(sut.Q3CalibValid);
CollectionAssert.AreEqual(new[] { false, false, false }, valid);
CollectionAssert.AreEqual(new[] { 0.0, 0.0, 0.0 }, calib);
}
[TestMethod]
public void GetQ3Calibration_ShouldRemainInvalid_WhenRefVolumeIsZero()
{
var sut = new GenesisSmartReader();
InvokePrivateByName(sut, "PrepareCalculatedChannelDataForTest", true);
Assert.IsFalse(sut.NoSamples);
var valid = new bool[3];
var calib = new double[3];
sut.GetQ3Calibration(
refVolume: 0.0,
refTime: 120.0,
initCalibFactor: ValidInitFactors,
ref valid,
ref calib);
Assert.IsFalse(sut.Q3CalibValid);
CollectionAssert.AreEqual(new[] { false, false, false }, valid);
CollectionAssert.AreEqual(new[] { 0.0, 0.0, 0.0 }, calib);
}
[TestMethod]
public void GetQ3Calibration_ShouldRemainInvalid_WhenRefTimeIsZero()
{
var sut = new GenesisSmartReader();
InvokePrivateByName(sut, "PrepareCalculatedChannelDataForTest", true);
Assert.IsFalse(sut.NoSamples);
var valid = new bool[3];
var calib = new double[3];
sut.GetQ3Calibration(
refVolume: 1000.0,
refTime: 0.0,
initCalibFactor: ValidInitFactors,
ref valid,
ref calib);
Assert.IsFalse(sut.Q3CalibValid);
CollectionAssert.AreEqual(new[] { false, false, false }, valid);
CollectionAssert.AreEqual(new[] { 0.0, 0.0, 0.0 }, calib);
}
[TestMethod]
public void GetQ3Calibration_ShouldMarkInvalid_WhenInitFactorsAreZero()
{
var sut = new GenesisSmartReader();
InvokePrivateByName(sut, "PrepareCalculatedChannelDataForTest", true);
Assert.IsFalse(sut.NoSamples);
var init = new[] { 0.0, 0.0, 0.0 };
var valid = new bool[3];
var calib = new double[3];
sut.GetQ3Calibration(
refVolume: 1000.0,
refTime: 120.0,
initCalibFactor: init,
ref valid,
ref calib);
Assert.IsFalse(sut.Q3CalibValid);
CollectionAssert.AreEqual(new[] { false, false, false }, valid);
CollectionAssert.AreEqual(new[] { 0.0, 0.0, 0.0 }, calib);
}
[TestMethod]
public void CalculateQ3Calibration_ShouldPopulatePerChannelValues_WithPreparedSimulationData()
{
var sut = new GenesisSmartReader();
InvokePrivateByName(sut, "PrepareCalculatedChannelDataForTest", true);
InvokePrivateByName(sut, "SetQ3Calibration", new object[] { ValidInitFactors });
Assert.IsFalse(sut.NoSamples);
sut.CalculateQ3Calibration(200.0, 120.0);
Assert.IsNotNull(sut.Q3CalibValue);
Assert.AreEqual(3, sut.Q3CalibValue.Length);
Assert.IsTrue(sut.Q3CalibValue.All(v => !Double.IsNaN(v)));
Assert.IsTrue(sut.Q3CalibValue.All(v => v >= 0.0));
}
[TestMethod]
public void GetQ3Calibration_ShouldCalculateExpectedValues_ForKnownInput()
{
var sut = new GenesisSmartReader();
var raw = CreateKnownStartEndData(
// start volume / end volume / start time / end time
(100.0, 150.0, 10.0, 20.0), // dV = 50, dT = 10
(105.0, 165.0, 10.0, 22.0), // dV = 60, dT = 12
(110.0, 180.0, 10.0, 25.0) // dV = 70, dT = 15
);
SetPrivateField(sut, "_rawStartEndByChannel", raw);
SetPrivateField(sut, "_recalculatedStartEndByChannel", raw); // important for NoSamples == false
SetPrivateField(sut, "optoDataCount", 2);
sut.TestStartTelegramIx = 0;
sut.TestEndTelegramIx = 1;
Assert.IsFalse(sut.NoSamples, "Expected prepared data to produce valid samples.");
var init = new[] { 15625.0, 15625.0, 15625.0 };
var valid = new bool[3];
var calib = new double[3];
sut.GetQ3Calibration(
refVolume: 200.0,
refTime: 120.0,
initCalibFactor: init,
ref valid,
ref calib);
double expectedCh1 = (200.0 / 600.0) * 15625.0;
double expectedCh2 = (200.0 / 600.0) * 15625.0;
double expectedCh3 = (200.0 / 560.0) * 15625.0;
Assert.AreEqual(expectedCh1, calib[0], 0.0001);
Assert.AreEqual(expectedCh2, calib[1], 0.0001);
Assert.AreEqual(expectedCh3, calib[2], 0.0001);
Assert.IsFalse(valid[0]);
Assert.IsFalse(valid[1]);
Assert.IsFalse(valid[2]);
}
[TestMethod]
public void GetQ3Calibration_ShouldMarkChannelValid_WhenDifferenceIsWithinFivePercent()
{
var sut = new GenesisSmartReader();
// We want recalculatedDeltaVolume == refVolume, so calculated factor == initial factor.
// refVolume = 200, refTime = 120
// choose dT = 10, dV = 16.6666666666667 => coef = 12 => recalculated dV = 200
var raw = CreateKnownStartEndData(
(100.0, 116.6666666666667, 10.0, 20.0),
(200.0, 216.6666666666667, 10.0, 20.0),
(300.0, 316.6666666666667, 10.0, 20.0)
);
SetPrivateField(sut, "_rawStartEndByChannel", raw);
SetPrivateField(sut, "_recalculatedStartEndByChannel", raw);
SetPrivateField(sut, "optoDataCount", 2);
sut.TestStartTelegramIx = 0;
sut.TestEndTelegramIx = 1;
Assert.IsFalse(sut.NoSamples, "Expected prepared data to produce valid samples.");
var init = new[] { 15625.0, 15625.0, 15625.0 };
var valid = new bool[3];
var calib = new double[3];
sut.GetQ3Calibration(
refVolume: 200.0,
refTime: 120.0,
initCalibFactor: init,
ref valid,
ref calib);
Assert.IsTrue(valid[0], $"Ch1 invalid, value={calib[0]}");
Assert.IsTrue(valid[1], $"Ch2 invalid, value={calib[1]}");
Assert.IsTrue(valid[2], $"Ch3 invalid, value={calib[2]}");
Assert.AreEqual(15625.0, calib[0], 0.001);
Assert.AreEqual(15625.0, calib[1], 0.001);
Assert.AreEqual(15625.0, calib[2], 0.001);
}
[TestMethod]
public void CalculateQ3Calibration_ShouldComputeExpectedChannelValues_FromSimulationData()
{
var sut = new GenesisSmartReader();
InvokePrivateByName(sut, "PrepareCalculatedChannelDataForTest", true);
InvokePrivateByName(sut, "SetQ3Calibration", new object[] { ValidInitFactors });
sut.CalculateQ3Calibration(200.0, 120.0);
// initial values remain exposed through Q3CalibValue
Assert.AreEqual(15625.0, sut.Q3CalibValue[0], 0.000001);
Assert.AreEqual(15625.0, sut.Q3CalibValue[1], 0.000001);
Assert.AreEqual(15625.0, sut.Q3CalibValue[2], 0.000001);
// calculated values
Assert.AreEqual(5208.33333333333, sut.Q3Calib_Ch1Value, 0.000001);
Assert.AreEqual(5008.01282051282, sut.Q3Calib_Ch2Value, 0.000001);
Assert.AreEqual(4822.53086419753, sut.Q3Calib_Ch3Value, 0.000001);
}
[TestMethod]
public void CalculateQ3Calibration_ShouldKeepInitialArray_AndUpdateCalculatedChannelProperties()
{
var sut = new GenesisSmartReader();
InvokePrivateByName(sut, "PrepareCalculatedChannelDataForTest", true);
InvokePrivateByName(sut, "SetQ3Calibration", new object[] { ValidInitFactors });
sut.CalculateQ3Calibration(200.0, 120.0);
// Q3CalibValue currently exposes INITIAL calibration values, not calculated ones
Assert.AreEqual(15625.0, sut.Q3CalibValue[0], 0.000001);
Assert.AreEqual(15625.0, sut.Q3CalibValue[1], 0.000001);
Assert.AreEqual(15625.0, sut.Q3CalibValue[2], 0.000001);
// Per-channel public properties expose calculated values
Assert.IsTrue(sut.Q3Calib_Ch1Value > 0.0);
Assert.IsTrue(sut.Q3Calib_Ch2Value > 0.0);
Assert.IsTrue(sut.Q3Calib_Ch3Value > 0.0);
Assert.AreNotEqual(sut.Q3CalibValue[0], sut.Q3Calib_Ch1Value, 0.000001);
Assert.AreNotEqual(sut.Q3CalibValue[1], sut.Q3Calib_Ch2Value, 0.000001);
Assert.AreNotEqual(sut.Q3CalibValue[2], sut.Q3Calib_Ch3Value, 0.000001);
}
[TestMethod]
public void PrepareCalculatedChannelDataSimulationForTest_ShouldPrepareUsableData()
{
var sut = new GenesisSmartReader();
InvokePrivateByName(sut, "PrepareCalculatedChannelDataSimulationForTest");
Assert.IsFalse(sut.NoSamples);
Assert.IsTrue(sut.VolumeLtrStart >= 0.0);
Assert.IsTrue(sut.VolumeLtrEnd >= 0.0);
Assert.IsTrue(sut.TimestampSecEnd >= sut.TimestampSecStart);
}
private static void InitializeReaderForQ3Test(GenesisSmartReader reader)
{
SetPrivateField(reader, "_rawStartEndByChannel", null);
SetPrivateField(reader, "_recalculatedStartEndByChannel", null);
SetPrivateField(reader, "optoDataCount", 0);
reader.TestStartTelegramIx = -1;
reader.TestEndTelegramIx = -1;
}
private static OptoTelegramRaw[][] CreateMinimalStartEndData()
{
return new[]
{
new[]
{
CreateRecord(0.0, 0.0, 1),
CreateRecord(15625.0, 120.0, 1)
},
new[]
{
CreateRecord(0.0, 0.0, 2),
CreateRecord(15625.0, 120.0, 2)
},
new[]
{
CreateRecord(0.0, 0.0, 3),
CreateRecord(15625.0, 120.0, 3)
}
};
}
private static OptoTelegramRaw[][] CreateKnownStartEndData(
(double startVolume, double endVolume, double startTime, double endTime) ch1,
(double startVolume, double endVolume, double startTime, double endTime) ch2,
(double startVolume, double endVolume, double startTime, double endTime) ch3)
{
return new[]
{
new[]
{
CreateRecord(ch1.startVolume, ch1.startTime, 1),
CreateRecord(ch1.endVolume, ch1.endTime, 1)
},
new[]
{
CreateRecord(ch2.startVolume, ch2.startTime, 2),
CreateRecord(ch2.endVolume, ch2.endTime, 2)
},
new[]
{
CreateRecord(ch3.startVolume, ch3.startTime, 3),
CreateRecord(ch3.endVolume, ch3.endTime, 3)
}
};
}
private static OptoTelegramRaw CreateRecord(double volumeRawExt, double timestampExt, int channel)
{
return new OptoTelegramRaw
{
VolumeRawExt = volumeRawExt,
TimestampExt = timestampExt,
iChannel = channel,
Flags = OptoTelegramFlags.OK
};
}
private static object InvokePrivateByName(object target, string methodName, params object[] args)
{
Type type = target.GetType();
while (type != null)
{
var candidates = type.GetMethods(
BindingFlags.Instance |
BindingFlags.Static |
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.DeclaredOnly)
.Where(m => m.Name == methodName)
.ToList();
foreach (var method in candidates)
{
var parameters = method.GetParameters();
if (parameters.Length != (args?.Length ?? 0))
continue;
try
{
return method.Invoke(target, args);
}
catch (ArgumentException)
{
}
}
type = type.BaseType;
}
Assert.Fail($"Method '{methodName}' not found or no matching overload accepted the provided arguments.");
return null;
}
private static void SetPrivateField(object target, string fieldName, object value)
{
Type type = target.GetType();
while (type != null)
{
var field = type.GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly);
if (field != null)
{
field.SetValue(target, value);
return;
}
type = type.BaseType;
}
Assert.Fail($"Field '{fieldName}' not found.");
}
}
}