fix cli poseidon - take values in cycles + tests

This commit is contained in:
Michal Buzik 2026-09-02 15:40:34 +02:00
parent 234bd39ed5
commit e4b42b5b51
9 changed files with 629 additions and 47 deletions

View File

@ -0,0 +1,158 @@
using System;
using Common;
using log4net;
using MySql.Data.MySqlClient;
namespace Results.Entities.helpers
{
/// <summary>
/// Applies explicit, backwards-compatible results DB migrations before the
/// first NHibernate session factory is created.
/// </summary>
public static class DatabaseMigrationHelper
{
private static readonly ILog log = LogManager.GetLogger(typeof(DatabaseMigrationHelper));
public static void EnsureSchema(DBType dbType, string connectionString)
{
switch (dbType)
{
case DBType.MySql:
EnsureMySqlSchema(connectionString);
break;
case DBType.SQLite:
EnsureSQLiteSchema(connectionString);
break;
}
}
private static void EnsureMySqlSchema(string connectionString)
{
using (var conn = new MySqlConnection(connectionString))
{
conn.Open();
// Added by newer MeterTestRslt mapping; older customer DBs do
// not contain it and otherwise reject the entire batch insert.
EnsureColumnMySql(conn, "MeterTestRslt", "PulsesPerKilogram", "DOUBLE NOT NULL DEFAULT 0");
EnsureColumnMySql(conn, "MeterTestRslt", "FlipMode", "INT NULL");
EnsureColumnMySql(conn, "MeterTestRslt", "ExtraDataPath", "VARCHAR(255) NULL");
EnsureMeterTestResultExtraColumnsMySql(conn);
}
}
private static void EnsureMeterTestResultExtraColumnsMySql(MySqlConnection conn)
{
for (int index = 1; index <= 9; index++)
{
EnsureColumnMySql(conn, "MeterTestRslt", "X" + index, "FLOAT NOT NULL DEFAULT 0");
}
}
private static void EnsureColumnMySql(
MySqlConnection conn,
string tableName,
string columnName,
string columnDefinition)
{
using (var transaction = conn.BeginTransaction())
{
try
{
bool exists;
using (var cmd = conn.CreateCommand())
{
cmd.Transaction = transaction;
cmd.CommandText = @"
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = @tableName
AND COLUMN_NAME = @columnName";
cmd.Parameters.AddWithValue("@tableName", tableName);
cmd.Parameters.AddWithValue("@columnName", columnName);
exists = Convert.ToInt32(cmd.ExecuteScalar()) > 0;
}
if (!exists)
{
using (var alter = conn.CreateCommand())
{
alter.Transaction = transaction;
alter.CommandText = "ALTER TABLE `" + tableName + "` ADD COLUMN `" +
columnName + "` " + columnDefinition;
alter.ExecuteNonQuery();
log.WarnFormat("Results DB migration: added {0}.{1} ({2}).",
tableName, columnName, columnDefinition);
}
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
}
private static void EnsureSQLiteSchema(string databaseFile)
{
using (var conn = new System.Data.SQLite.SQLiteConnection("Data Source=" + databaseFile))
{
conn.Open();
EnsureColumnSQLite(conn, "MeterTestRslt", "PulsesPerKilogram", "REAL NOT NULL DEFAULT 0");
EnsureColumnSQLite(conn, "MeterTestRslt", "FlipMode", "INTEGER NULL");
EnsureColumnSQLite(conn, "MeterTestRslt", "ExtraDataPath", "TEXT NULL");
EnsureMeterTestResultExtraColumnsSQLite(conn);
}
}
private static void EnsureMeterTestResultExtraColumnsSQLite(System.Data.SQLite.SQLiteConnection conn)
{
for (int index = 1; index <= 9; index++)
{
EnsureColumnSQLite(conn, "MeterTestRslt", "X" + index, "REAL NOT NULL DEFAULT 0");
}
}
private static void EnsureColumnSQLite(
System.Data.SQLite.SQLiteConnection conn,
string tableName,
string columnName,
string columnDefinition)
{
bool exists = false;
using (var cmd = conn.CreateCommand())
{
cmd.CommandText = "PRAGMA table_info(" + tableName + ")";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
if (string.Equals(reader["name"].ToString(), columnName,
StringComparison.OrdinalIgnoreCase))
{
exists = true;
break;
}
}
}
}
if (!exists)
{
using (var alter = conn.CreateCommand())
{
alter.CommandText = "ALTER TABLE " + tableName + " ADD COLUMN " +
columnName + " " + columnDefinition;
alter.ExecuteNonQuery();
log.WarnFormat("Results DB migration: added {0}.{1} ({2}).",
tableName, columnName, columnDefinition);
}
}
}
}
}

View File

@ -268,10 +268,16 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
{
dlg.WMStartState[item] = poseidonReader.BeginWMState;
dlg.WMStartStateStr[item] = poseidonReader.BeginWMState.ToString();
log.InfoFormat("Poseidon dialog prefill: phase=Start, WM{0}, reader={1}, value={2}",
item + 1, poseidonReader.Name, dlg.WMStartState[item]);
}
if (currentOp == CurrentOp.ReadDatastream_EndStates)
{
dlg.WMEndState[item] = poseidonReader.EndWMState;
log.InfoFormat("Poseidon dialog prefill: phase=End, WM{0}, reader={1}, value={2}",
item + 1, poseidonReader.Name, dlg.WMEndState[item]);
}
}
}
@ -326,31 +332,10 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
}
}
}
else if (modelessDlg is TestStartEndForm && currentOp == CurrentOp.ReadDatastream_StartStates)
else if (modelessDlg is TestStartEndForm)
{
/// Fixed start test - start
TestStartEndForm dlg = (modelessDlg as TestStartEndForm);
if (dlg != null)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmStartState[i] = dlg.WMStartState[i];
wmStartStateStr[i] = dlg.WMStartStateStr[i];
}
}
}
else if (modelessDlg is TestStartEndForm && currentOp == CurrentOp.ReadDatastream_EndStates)
{
/// Fixed start test - end
TestStartEndForm dlg = (modelessDlg as TestStartEndForm);
if (dlg != null)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmEndState[i] = dlg.WMEndState[i];
}
}
}
StoreAcceptedDialogValues((TestStartEndForm)modelessDlg);
}
resultSaved = true;
modelessDlg = null;
}
@ -358,6 +343,36 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
return Event.ModelessFormClosed; /// Form closed
}
/// <summary>
/// Copies values confirmed by the Poseidon start/end dialog into the
/// data-entry state used later by the result calculation.
/// </summary>
private void StoreAcceptedDialogValues(TestStartEndForm dlg)
{
if (dlg == null)
return;
if (currentOp == CurrentOp.ReadDatastream_StartStates)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmStartState[i] = dlg.WMStartState[i];
wmStartStateStr[i] = dlg.WMStartStateStr[i];
log.InfoFormat("Poseidon dialog accepted: phase=Start, WM{0}, text='{1}', value={2}",
i + 1, wmStartStateStr[i], wmStartState[i]);
}
}
else if (currentOp == CurrentOp.ReadDatastream_EndStates)
{
for (int i = 0; i < dlg.WaterMetersCount; i++)
{
wmEndState[i] = dlg.WMEndState[i];
log.InfoFormat("Poseidon dialog accepted: phase=End, WM{0}, value={1}",
i + 1, wmEndState[i]);
}
}
}
/// <summary>Stop this operation</summary>
public void Stop()
{
@ -447,23 +462,21 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
}
}
var phaseRunner = new PoseidonReadPhaseRunner(
currentOp == CurrentOp.ReadDatastream_StartStates);
while (!finishedReading) //TODO BUMI lock - fuck ?
{
Dictionary<IPoseidonReadOperation, bool> operationsBefore =
new Dictionary<IPoseidonReadOperation, bool>();
foreach (IPoseidonReadOperation poseidonReader in poseidonReaders)
{
operationsBefore.Add(poseidonReader, poseidonReader.IsFinished);
if (poseidonReader.IsNotStarted)
log.DebugFormat("Poseidon read: starting {0} for {1}", currentOp, poseidonReader.Name);
if (poseidonReader.IsNotStarted || poseidonReader.IsFinished)
log.DebugFormat("Poseidon read: arming {0} for {1}, previous state finished={2}",
currentOp, poseidonReader.Name, poseidonReader.IsFinished);
}
bool bAllReadersFinished = PoseidonReadCycle.RunIteration(
poseidonReaders,
currentOp == CurrentOp.ReadDatastream_StartStates);
bool bAllReadersFinished = phaseRunner.RunIteration(poseidonReaders);
foreach (IPoseidonReadOperation poseidonReader in poseidonReaders)
{
if (poseidonReader.IsFinished && !operationsBefore[poseidonReader])
if (poseidonReader.IsFinished)
{
if (poseidonReader.HasError)
log.ErrorFormat("Poseidon read: {0} completed with Error during {1}", poseidonReader.Name, currentOp);

View File

@ -354,6 +354,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
if (endTextBoxes[i].Visible && endTextBoxes[i].Enabled)
{
WMEndState[i] = Utils.ParseUDouble(endTextBoxes[i].Text);
log.InfoFormat("Poseidon dialog OK: phase=End, WM{0}, enteredText='{1}', parsedValue={2}",
i + 1, endTextBoxes[i].Text, WMEndState[i]);
}
}
}
@ -365,6 +367,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd
{
WMStartStateStr[i] = startTextBoxes[i].Text;
WMStartState[i] = Utils.ParseUDouble(startTextBoxes[i].Text);
log.InfoFormat("Poseidon dialog OK: phase=Start, WM{0}, enteredText='{1}', parsedValue={2}",
i + 1, startTextBoxes[i].Text, WMStartState[i]);
}
}
}

View File

@ -78,4 +78,43 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return allReadersFinished;
}
}
/// <summary>
/// Owns one logical dialog phase. A new phase must arm a reader even when
/// the preceding phase left it in Done/Error; within this phase it is armed
/// only once.
/// </summary>
public sealed class PoseidonReadPhaseRunner
{
private readonly bool readStart;
private readonly HashSet<IPoseidonReadOperation> startedReaders =
new HashSet<IPoseidonReadOperation>();
public PoseidonReadPhaseRunner(bool readStart)
{
this.readStart = readStart;
}
public bool RunIteration(IEnumerable<IPoseidonReadOperation> readers)
{
if (readers == null)
return true;
bool allReadersFinished = true;
foreach (IPoseidonReadOperation reader in readers)
{
if (reader == null)
continue;
if (startedReaders.Add(reader))
reader.Start(readStart);
reader.Run();
if (!reader.IsFinished)
allReadersFinished = false;
}
return allReadersFinished;
}
}
}

View File

@ -25,6 +25,9 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
public class PoseidonReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ICommonRegReader
{
private static readonly ILog log = LogManager.GetLogger(typeof(PoseidonReader));
// The simulator must never open the configured physical Hat CLI. The
// deterministic test CLI returns a valid Poseidon JSON response instead.
internal const string SimulatedCliFileName = "cmdSleepTest.exe";
public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); }
readonly PoseidonCfg registerReaderCfg;
@ -281,8 +284,9 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
if ((DebugLevel == DebugMode.Normal)||(DebugLevel == DebugMode.Simulate))
{
/// Prepare serial port
string cliFileName = GetCliFileNameForMode(DebugLevel, registerReaderCfg.CliFileName);
serialPort = new SerialPortData(string.Format("COM{0}", registerReaderCfg.ComPortNr),
registerReaderCfg.CliFileName,
cliFileName,
registerReaderCfg.MeterType, //registerReaderCfg.MeterType, //MeterProduct.Poseidon
registerReaderCfg.HatType,//HatType.Mth
registerReaderCfg.TimeOut, //timeoutSeconds
@ -290,6 +294,13 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
);
if (DebugLevel == DebugMode.Simulate)
{
log.WarnFormat(
"{0}: simulation mode enabled; overriding configured CLI '{1}' with '{2}'.",
Name, registerReaderCfg.CliFileName, serialPort.SerialPortCmdClientPath);
}
//TODO BUMI prepare serial port - for us do nothing
//serialPort.Open();
//we can check file program if exists
@ -310,6 +321,13 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
}
internal static string GetCliFileNameForMode(DebugMode debugMode, string configuredCliFileName)
{
return debugMode == DebugMode.Simulate
? SimulatedCliFileName
: configuredCliFileName;
}
public void Clear()
{
log.DebugFormat("{0}:Clear()", Name);
@ -516,12 +534,14 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
}
double volume;
if (TryParseCliReading(data.Reading, out volume))
double volumeLi;
string dialogValueFailureReason;
if (TryGetDialogValue(data, out volumeLi, out dialogValueFailureReason))
{
lastCliReadingParsed = true;
lastCliReadSucceeded = true;
double volumeLi = Units.ConvertFrom(Unit.USgal, volume);
double volume;
TryParseCliReading(data.Reading, out volume);
if (_isReadingStart)
beginWMState = volumeLi;
@ -533,7 +553,7 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
}
else
{
lastCliReadFailureReason = "Reading could not be parsed: '" + data.Reading + "'.";
lastCliReadFailureReason = dialogValueFailureReason;
log.ErrorFormat("{0}: cannot parse CLI reading '{1}' using invariant or current culture.",
Name, data.Reading);
}
@ -563,6 +583,29 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return Double.TryParse(normalizedReading, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
}
/// <summary>
/// Converts a validated CLI response to the value written into the
/// start/end dialog. Poseidon reports US gallons; TBF stores litres.
/// </summary>
public static bool TryGetDialogValue(JsonDataFromPoseidon data, out double valueLitres,
out string failureReason)
{
valueLitres = 0;
if (!TryValidateCliReadResponse(data, out failureReason))
return false;
double valueGallons;
if (!TryParseCliReading(data.Reading, out valueGallons))
{
failureReason = "Reading could not be parsed: '" + data.Reading + "'.";
return false;
}
valueLitres = Units.ConvertFrom(Unit.USgal, valueGallons);
failureReason = null;
return true;
}
public static bool TryValidateCliReadResponse(JsonDataFromPoseidon data, out string failureReason)
{
if (data == null)

View File

@ -0,0 +1,62 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
/// <summary>
/// Regression fixtures extracted from PT50 customer CliRunner.txt (2026-09-01).
/// They are non-zero responses returned by HatCliDemo for COM43 and COM45.
/// </summary>
[TestClass]
public class PoseidonCustomerCliResponseTest
{
// The expected fixtures were calculated independently. A 0.1 ml
// tolerance covers insignificant IEEE-754 conversion differences while
// still detecting any meaningful incorrect transfer to the dialog.
private const double DialogValueToleranceLitres = 0.0001;
private const string Com43InitialResponse =
"{\"Reading\":\"002780.99\",\"DeviceId\":\"1000000219\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
private const string Com45InitialResponse =
"{\"Reading\":\"002316.63\",\"DeviceId\":\"1000000279\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
private const string Com43LaterResponse =
"{\"Reading\":\"002898.60\",\"DeviceId\":\"1000000219\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
private const string Com45LaterResponse =
"{\"Reading\":\"002433.50\",\"DeviceId\":\"1000000279\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
[TestMethod]
public void CustomerCliResponses_AreDeserializedAndConvertedToDialogValues()
{
// The first pair is a valid START dialog result; neither value is zero.
AssertDialogValue(Com43InitialResponse, "1000000219", "002780.99", 10527.1923171862);
AssertDialogValue(Com45InitialResponse, "1000000279", "002316.63", 8769.39850116792);
// The later pair must be usable by a new START or END phase, rather
// than TBF retaining the initial START values or displaying zero.
AssertDialogValue(Com43LaterResponse, "1000000219", "002898.60", 10972.3945971024);
AssertDialogValue(Com45LaterResponse, "1000000279", "002433.50", 9211.799576364);
}
private static void AssertDialogValue(string json, string expectedDeviceId,
string expectedReading, double expectedLitres)
{
var cliRunner = new CliRunner(false);
JsonDataFromPoseidon response;
Assert.IsTrue(cliRunner.TryJsonStringDeserialize(json, out response));
Assert.IsNotNull(response);
Assert.AreEqual(expectedDeviceId, response.DeviceId);
Assert.AreEqual(expectedReading, response.Reading);
Assert.IsTrue(response.NfcTagDetected == true);
Assert.IsTrue(response.ReadingComplete == true);
double dialogValue;
string failureReason;
Assert.IsTrue(CmdPoseidonReader.TryGetDialogValue(response, out dialogValue, out failureReason), failureReason);
Assert.AreEqual(expectedLitres, dialogValue, DialogValueToleranceLitres,
"The value assigned to the START/END dialog must come from the CLI response.");
Assert.AreNotEqual(0d, dialogValue);
}
}
}

View File

@ -0,0 +1,145 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows.Forms;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TBF.Rig.DataEntry.PoseidonCmd;
using TBF.Rig.RegisterReaders.PoseidonCmdStartStop;
using CmdPoseidonReader = TBF.Rig.RegisterReaders.PoseidonCmdStartStop.PoseidonReader;
namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
{
[TestClass]
public class PoseidonDialogTransferTest
{
// Exact non-zero reading shape returned by HatCliDemo in the PT50 customer log.
private const string CustomerCliResponse =
"{\"Reading\":\"002433.50\",\"DeviceId\":\"1000000279\",\"ProductType\":74,\"ReadingComplete\":true,\"NfcTagDetected\":true,\"CliVersion\":\"2.0.0.2\"}";
[TestMethod]
public void CustomerCliValue_IsPrefilledConfirmedAndTransferred_ForStartAndEnd()
{
Exception failure = null;
var staThread = new Thread(() =>
{
try
{
RunDialogTransferScenario();
}
catch (Exception exception)
{
failure = exception;
}
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
staThread.Join(TimeSpan.FromSeconds(15));
Assert.IsFalse(staThread.IsAlive, "The Poseidon dialog test did not finish.");
if (failure != null)
throw failure;
}
private static void RunDialogTransferScenario()
{
var cliRunner = new CliRunner(false);
JsonDataFromPoseidon response;
Assert.IsTrue(cliRunner.TryJsonStringDeserialize(CustomerCliResponse, out response));
double cliValueLitres;
string failureReason;
Assert.IsTrue(CmdPoseidonReader.TryGetDialogValue(response, out cliValueLitres, out failureReason), failureReason);
Assert.AreNotEqual(0d, cliValueLitres);
VerifyStartDialogTransfer(cliValueLitres);
VerifyEndDialogTransfer(cliValueLitres);
}
private static void VerifyStartDialogTransfer(double expectedValue)
{
using (var dialog = new TestStartEndForm(1, null, new bool[1]))
{
dialog.CreateControl();
dialog.WMStartState[0] = expectedValue;
dialog.WMStartStateStr[0] = expectedValue.ToString();
dialog.UpdateValues(true, true);
TextBox textBox = FindTextBox(dialog, "startTextBox1");
Assert.AreEqual(dialog.WMStartStateStr[0], textBox.Text, "CLI value was not prefilled into the Start dialog.");
ConfirmDialog(dialog);
Assert.IsTrue(dialog.Completed);
Assert.AreEqual(expectedValue, dialog.WMStartState[0], 0.000001, "Start dialog did not parse the confirmed value.");
var entryForm = CreateEntryFormForTransfer(EntryForm.CurrentOp.ReadDatastream_StartStates);
TransferAcceptedDialogValues(entryForm, dialog);
Assert.AreEqual(expectedValue, entryForm.WMStartState(0), 0.000001,
"EntryForm did not receive the Start value confirmed in the dialog.");
}
}
private static void VerifyEndDialogTransfer(double expectedValue)
{
using (var dialog = new TestStartEndForm(1, null, new[] { expectedValue.ToString() }, new bool[1], 0, 0, 0))
{
dialog.CreateControl();
dialog.WMEndState[0] = expectedValue;
dialog.UpdateValues(false, true);
TextBox textBox = FindTextBox(dialog, "endTextBox1");
Assert.AreEqual(expectedValue.ToString(), textBox.Text, "CLI value was not prefilled into the End dialog.");
ConfirmDialog(dialog);
Assert.IsTrue(dialog.Completed);
Assert.AreEqual(expectedValue, dialog.WMEndState[0], 0.000001, "End dialog did not parse the confirmed value.");
var entryForm = CreateEntryFormForTransfer(EntryForm.CurrentOp.ReadDatastream_EndStates);
TransferAcceptedDialogValues(entryForm, dialog);
Assert.AreEqual(expectedValue, entryForm.WMEndState(0), 0.000001,
"EntryForm did not receive the End value confirmed in the dialog.");
}
}
private static TextBox FindTextBox(Control dialog, string name)
{
TextBox textBox = dialog.Controls.Find(name, true).OfType<TextBox>().FirstOrDefault();
Assert.IsNotNull(textBox, "Expected dialog control was not found: " + name);
return textBox;
}
private static void ConfirmDialog(TestStartEndForm dialog)
{
MethodInfo okHandler = typeof(TestStartEndForm).GetMethod("okButton_Click",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(okHandler);
okHandler.Invoke(dialog, new object[] { dialog, EventArgs.Empty });
}
private static EntryForm CreateEntryFormForTransfer(EntryForm.CurrentOp currentOp)
{
var entryForm = new EntryForm();
SetPrivateField(entryForm, "currentOp", currentOp);
SetPrivateField(entryForm, "wmStartState", new double[1]);
SetPrivateField(entryForm, "wmStartStateStr", new string[1]);
SetPrivateField(entryForm, "wmEndState", new double[1]);
return entryForm;
}
private static void TransferAcceptedDialogValues(EntryForm entryForm, TestStartEndForm dialog)
{
MethodInfo transferMethod = typeof(EntryForm).GetMethod("StoreAcceptedDialogValues",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(transferMethod);
transferMethod.Invoke(entryForm, new object[] { dialog });
}
private static void SetPrivateField(object instance, string name, object value)
{
FieldInfo field = instance.GetType().GetField(name,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.IsNotNull(field, "Expected field was not found: " + name);
field.SetValue(instance, value);
}
}
}

View File

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -13,6 +14,8 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
[TestSubject(typeof(PoseidonReadCycle))]
public class PoseidonReadCycleTest
{
public TestContext TestContext { get; set; }
private sealed class FakePoseidonReader : IPoseidonReadOperation
{
public string Name { get; private set; }
@ -80,16 +83,18 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
for (int i = 1; i <= 48; i++)
readers.Add(new FakePoseidonReader("PoseidonPos" + i, 1 + (i % 3)));
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, true));
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, true));
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, true));
Assert.IsTrue(PoseidonReadCycle.RunIteration(readers, true));
TestContext.WriteLine("PoseidonReadCycle | scenario=FullBench | phase=Start | readers=48");
Assert.IsFalse(RunAndLogCycleIteration("FullBench", 1, readers, true));
Assert.IsFalse(RunAndLogCycleIteration("FullBench", 2, readers, true));
Assert.IsFalse(RunAndLogCycleIteration("FullBench", 3, readers, true));
Assert.IsTrue(RunAndLogCycleIteration("FullBench", 4, readers, true));
foreach (FakePoseidonReader reader in readers)
{
Assert.AreEqual(1, reader.StartCount, reader.Name + " must be armed exactly once.");
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
}
LogReaderSummary("FullBench final", readers);
}
[TestMethod]
@ -101,27 +106,64 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
new FakePoseidonReader("PoseidonPos2", 3)
};
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, false));
TestContext.WriteLine("PoseidonReadCycle | scenario=ReducedBench | phase=End | readers=2");
Assert.IsFalse(RunAndLogCycleIteration("ReducedBench", 1, readers, false));
// Pos1 is now Done while Pos2 is still running. This is the state
// which used to relaunch Pos1's CLI process over and over.
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, false));
Assert.IsFalse(RunAndLogCycleIteration("ReducedBench", 2, readers, false));
Assert.AreEqual(1, ((FakePoseidonReader)readers[0]).StartCount);
Assert.IsFalse(PoseidonReadCycle.RunIteration(readers, false));
Assert.IsTrue(PoseidonReadCycle.RunIteration(readers, false));
Assert.IsFalse(RunAndLogCycleIteration("ReducedBench", 3, readers, false));
Assert.IsTrue(RunAndLogCycleIteration("ReducedBench", 4, readers, false));
foreach (FakePoseidonReader reader in readers)
{
Assert.AreEqual(1, reader.StartCount, reader.Name + " must be armed exactly once.");
}
LogReaderSummary("ReducedBench final", readers);
}
[TestMethod]
public void StartThenEnd_PhasesRearmCompletedReadersExactlyOncePerPhase()
{
var reader = new FakePoseidonReader("PoseidonPos1", 1);
var readers = new List<IPoseidonReadOperation> { reader };
var startPhase = new PoseidonReadPhaseRunner(true);
TestContext.WriteLine("PoseidonReadCycle | scenario=StartThenEnd | phase=Start | readers=1");
Assert.IsFalse(RunAndLogPhaseIteration("StartThenEnd", "Start", 1, startPhase, readers));
Assert.IsTrue(RunAndLogPhaseIteration("StartThenEnd", "Start", 2, startPhase, readers));
Assert.AreEqual(1, reader.StartCount);
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
var endPhase = new PoseidonReadPhaseRunner(false);
TestContext.WriteLine("PoseidonReadCycle | scenario=StartThenEnd | phase=End | rearming reader completed by Start");
Assert.IsFalse(RunAndLogPhaseIteration("StartThenEnd", "End", 1, endPhase, readers));
Assert.IsTrue(RunAndLogPhaseIteration("StartThenEnd", "End", 2, endPhase, readers));
Assert.AreEqual(2, reader.StartCount,
"A completed START read must be rearmed once for the END phase.");
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
LogReaderSummary("StartThenEnd final", readers);
}
[TestMethod]
public void InputJson_ZeroReading_WithDecimalCommaOrDot_IsDeserializedAndParsedCorrectly()
{
TestContext.WriteLine("Poseidon JSON parsing | verifies zero is retained for both decimal separators.");
AssertInputJsonReading("00000,0", 0.0);
AssertInputJsonReading("00000.0", 0.0);
}
[TestMethod]
public void InputJson_NonZeroReading_WithDecimalCommaOrDot_IsDeserializedAndParsedCorrectly()
{
TestContext.WriteLine("Poseidon JSON parsing | verifies non-zero readings for both decimal separators.");
AssertInputJsonReading("002433,50", 2433.50);
AssertInputJsonReading("002433.50", 2433.50);
AssertInputJsonReading("002898,60", 2898.60);
AssertInputJsonReading("002898.60", 2898.60);
}
[TestMethod]
public void CliReadResponse_ZeroReading_IsAcceptedOnlyWhenNfcAndReadingAreComplete()
{
@ -135,18 +177,21 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
Assert.IsTrue(CmdPoseidonReader.TryValidateCliReadResponse(validZero, out failureReason));
Assert.IsNull(failureReason);
TestContext.WriteLine("Poseidon CLI validation | zero reading accepted when NFC=true and ReadingComplete=true.");
validZero.ReadingComplete = false;
Assert.IsFalse(CmdPoseidonReader.TryValidateCliReadResponse(validZero, out failureReason));
Assert.AreEqual("ReadingComplete=false.", failureReason);
TestContext.WriteLine("Poseidon CLI validation | rejected response: " + failureReason);
validZero.ReadingComplete = true;
validZero.NfcTagDetected = false;
Assert.IsFalse(CmdPoseidonReader.TryValidateCliReadResponse(validZero, out failureReason));
Assert.AreEqual("NfcTagDetected=false.", failureReason);
TestContext.WriteLine("Poseidon CLI validation | rejected response: " + failureReason);
}
private static void AssertInputJsonReading(string reading, double expectedValue)
private void AssertInputJsonReading(string reading, double expectedValue)
{
string json = "{\"NfcTagDetected\":true,\"ReadingComplete\":true," +
"\"DeviceId\":\"1000000322\",\"Reading\":\"" + reading + "\"}";
@ -165,6 +210,25 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
bool parsed = CmdPoseidonReader.TryParseCliReading(data.Reading, out parsedReading);
Assert.IsTrue(parsed, "CLI Reading must be parsed for '" + reading + "'.");
Assert.AreEqual(expectedValue, parsedReading, 0.000001);
TestContext.WriteLine(string.Format(
"Poseidon JSON parsing | rawReading='{0}' | parsed={1} | expected={2} | deviceId={3}",
reading, parsedReading, expectedValue, data.DeviceId));
}
[TestMethod]
public void SimulationMode_AlwaysUsesCmdSleepTestInsteadOfConfiguredHatCli()
{
string simulatedCli = CmdPoseidonReader.GetCliFileNameForMode(
Common.DebugMode.Simulate, "HatCliDemo.exe");
string normalCli = CmdPoseidonReader.GetCliFileNameForMode(
Common.DebugMode.Normal, "HatCliDemo.exe");
Assert.AreEqual("cmdSleepTest.exe", simulatedCli);
Assert.AreEqual("HatCliDemo.exe", normalCli);
TestContext.WriteLine(
"Poseidon simulation CLI | mode=Simulate | configured=HatCliDemo.exe | selected=" + simulatedCli);
TestContext.WriteLine(
"Poseidon simulation CLI | mode=Normal | configured=HatCliDemo.exe | selected=" + normalCli);
}
[TestMethod]
@ -178,16 +242,68 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop
CmdPoseidonReader reader = CreateReader("PoseidonPos1", 3, cliFile);
reader.SetCurrentOp(CmdPoseidonReader.CurrentPoseidonOp.ReadDataStream_Start);
TestContext.WriteLine("Poseidon cmdSleepTest | reader=PoseidonPos1 | phase=Start | cli=" + cliPath);
for (int iteration = 0; iteration < 300 && !IsTerminal(reader); iteration++)
{
reader.Run();
if (iteration == 0 || iteration % 25 == 0 || IsTerminal(reader))
TestContext.WriteLine(string.Format(
"Poseidon cmdSleepTest | iteration={0} | state={1} | serial='{2}' | begin={3}",
iteration + 1, reader.CurrentOp, reader.SerialNr, reader.BeginWMState));
Thread.Sleep(10);
}
Assert.AreEqual(CmdPoseidonReader.CurrentPoseidonOp.Done, reader.CurrentOp);
Assert.IsFalse(string.IsNullOrEmpty(reader.SerialNr));
Assert.IsTrue(reader.BeginWMState > 0, "The JSON reading from cmdSleepTest must be transferred to BeginWMState.");
TestContext.WriteLine(string.Format(
"Poseidon cmdSleepTest | completed | state={0} | serial='{1}' | begin={2}",
reader.CurrentOp, reader.SerialNr, reader.BeginWMState));
}
private bool RunAndLogCycleIteration(string scenario, int iteration,
IList<IPoseidonReadOperation> readers, bool readStart)
{
bool completed = PoseidonReadCycle.RunIteration(readers, readStart);
LogReaderSummary(scenario + " iteration=" + iteration + " phase=" + (readStart ? "Start" : "End") +
" completed=" + completed, readers);
return completed;
}
private bool RunAndLogPhaseIteration(string scenario, string phase, int iteration,
PoseidonReadPhaseRunner runner, IList<IPoseidonReadOperation> readers)
{
bool completed = runner.RunIteration(readers);
LogReaderSummary(scenario + " iteration=" + iteration + " phase=" + phase +
" completed=" + completed, readers);
return completed;
}
private void LogReaderSummary(string label, IEnumerable<IPoseidonReadOperation> readers)
{
var readerList = readers.ToList();
int notStartedCount = readerList.Count(reader => reader.IsNotStarted);
int errorCount = readerList.Count(reader => reader.HasError);
int doneCount = readerList.Count(reader => reader.IsFinished && !reader.HasError);
int runningCount = readerList.Count - notStartedCount - errorCount - doneCount;
string stateCounts = string.Format("NotStarted={0}, Running={1}, Done={2}, Error={3}",
notStartedCount, runningCount, doneCount, errorCount);
string startCounts = string.Join(", ", readerList
.OfType<FakePoseidonReader>()
.GroupBy(reader => reader.StartCount)
.OrderBy(group => group.Key)
.Select(group => "starts" + group.Key + "=" + group.Count()));
string details = readerList.Count <= 10
? string.Join("; ", readerList.OfType<FakePoseidonReader>().Select(reader =>
string.Format("{0}[state={1}, starts={2}, runs={3}]",
reader.Name, reader.CurrentOp, reader.StartCount, reader.RunCount)))
: "details=omitted for full bench";
TestContext.WriteLine(string.Format(
"PoseidonReadCycle | {0} | readerCount={1} | states={2} | {3} | {4}",
label, readerList.Count, stateCounts, startCounts, details));
}
internal static CmdPoseidonReader CreateReader(string name, int comPort, string cliFile)

View File

@ -108,6 +108,8 @@
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\CliRunnerTimeoutUnitTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReaderTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonReadCycleTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonCustomerCliResponseTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonDialogTransferTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonCmdStartStop\PoseidonSingleMeterIntegrationTest.cs" />
<Compile Include="Rig\TestMethods\iPerlCommunication\IPerlCommunicationFormIntegrationTest.cs" />
<Compile Include="Rig\RegisterReaders\PoseidonReader\UniHeadTestCtrlTest.cs" />