diff --git a/Results/Entities/helpers/DatabaseMigrationHelper.cs b/Results/Entities/helpers/DatabaseMigrationHelper.cs
new file mode 100644
index 000000000..21f7d5ac5
--- /dev/null
+++ b/Results/Entities/helpers/DatabaseMigrationHelper.cs
@@ -0,0 +1,158 @@
+using System;
+using Common;
+using log4net;
+using MySql.Data.MySqlClient;
+
+namespace Results.Entities.helpers
+{
+ ///
+ /// Applies explicit, backwards-compatible results DB migrations before the
+ /// first NHibernate session factory is created.
+ ///
+ 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);
+ }
+ }
+ }
+ }
+}
diff --git a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs
index 9dc95d57d..52e3499e2 100644
--- a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs
+++ b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs
@@ -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
}
+ ///
+ /// Copies values confirmed by the Poseidon start/end dialog into the
+ /// data-entry state used later by the result calculation.
+ ///
+ 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]);
+ }
+ }
+ }
+
/// Stop this operation
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 operationsBefore =
- new Dictionary();
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);
diff --git a/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs b/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs
index f2b2f465b..0162da360 100644
--- a/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs
+++ b/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs
@@ -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]);
}
}
}
diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs
index 0c07a1fe6..89163642a 100644
--- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs
+++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycle.cs
@@ -78,4 +78,43 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop
return allReadersFinished;
}
}
+
+ ///
+ /// 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.
+ ///
+ public sealed class PoseidonReadPhaseRunner
+ {
+ private readonly bool readStart;
+ private readonly HashSet startedReaders =
+ new HashSet();
+
+ public PoseidonReadPhaseRunner(bool readStart)
+ {
+ this.readStart = readStart;
+ }
+
+ public bool RunIteration(IEnumerable 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;
+ }
+ }
}
diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs
index 99aab7f55..2f3238dcc 100644
--- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs
+++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs
@@ -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);
}
+ ///
+ /// Converts a validated CLI response to the value written into the
+ /// start/end dialog. Poseidon reports US gallons; TBF stores litres.
+ ///
+ 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)
diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonCustomerCliResponseTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonCustomerCliResponseTest.cs
new file mode 100644
index 000000000..559bd8327
--- /dev/null
+++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonCustomerCliResponseTest.cs
@@ -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
+{
+ ///
+ /// Regression fixtures extracted from PT50 customer CliRunner.txt (2026-09-01).
+ /// They are non-zero responses returned by HatCliDemo for COM43 and COM45.
+ ///
+ [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);
+ }
+ }
+}
diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonDialogTransferTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonDialogTransferTest.cs
new file mode 100644
index 000000000..a731808ec
--- /dev/null
+++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonDialogTransferTest.cs
@@ -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().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);
+ }
+ }
+}
diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs
index 9aa1ae19b..06d6fa3eb 100644
--- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs
+++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs
@@ -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 { 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 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 readers)
+ {
+ bool completed = runner.RunIteration(readers);
+ LogReaderSummary(scenario + " iteration=" + iteration + " phase=" + phase +
+ " completed=" + completed, readers);
+ return completed;
+ }
+
+ private void LogReaderSummary(string label, IEnumerable 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()
+ .GroupBy(reader => reader.StartCount)
+ .OrderBy(group => group.Key)
+ .Select(group => "starts" + group.Key + "=" + group.Count()));
+
+ string details = readerList.Count <= 10
+ ? string.Join("; ", readerList.OfType().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)
diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj
index b2d12071d..04e85f6e6 100644
--- a/TBFTests/TBFTests.csproj
+++ b/TBFTests/TBFTests.csproj
@@ -108,6 +108,8 @@
+
+