fix cli poseidon - take values in cycles + tests
This commit is contained in:
parent
234bd39ed5
commit
e4b42b5b51
158
Results/Entities/helpers/DatabaseMigrationHelper.cs
Normal file
158
Results/Entities/helpers/DatabaseMigrationHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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" />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user