diff --git a/Results/Entities/helpers/DatabaseMigrationHelper.cs b/Results/Entities/helpers/DatabaseMigrationHelper.cs index 6481ed596..8f301141e 100644 --- a/Results/Entities/helpers/DatabaseMigrationHelper.cs +++ b/Results/Entities/helpers/DatabaseMigrationHelper.cs @@ -1,6 +1,7 @@ using System; using MySql.Data.MySqlClient; using Common; +using log4net; namespace Results.Entities.helpers { @@ -10,6 +11,8 @@ namespace Results.Entities.helpers /// public static class DatabaseMigrationHelper { + private static readonly ILog log = LogManager.GetLogger(typeof(DatabaseMigrationHelper)); + public static void EnsureSchema(DBType dbType, string connectionString) { switch (dbType) @@ -36,9 +39,31 @@ namespace Results.Entities.helpers // EnsureColumnMySql(conn, "WaterMeter", "Q3Channel", "INT NOT NULL DEFAULT 0"); // EnsureColumnMySql(conn, "MeterTestRslt", "Q3Channel", "INT NOT NULL DEFAULT 0"); - EnsureColumnMySql(conn, "MeterTestRslt", "FlipMode", "INT NULL"); - } - } + EnsureColumnMySql(conn, "MeterTestRslt", "PulsesPerKilogram", "DOUBLE NOT NULL DEFAULT 0"); + EnsureMeterTestResultMassColumnsMySql(conn); + 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"); + } + + // These properties were added to the MeterTestRslt NHibernate mapping + // after older customer databases had already been created. Keep them in + // one migration group so a batch insert does not fail one column at a time. + private static void EnsureMeterTestResultMassColumnsMySql(MySqlConnection conn) + { + EnsureColumnMySql(conn, "MeterTestRslt", "MassMeter", "DOUBLE NOT NULL DEFAULT 0"); + EnsureColumnMySql(conn, "MeterTestRslt", "MassRef", "DOUBLE NOT NULL DEFAULT 0"); + EnsureColumnMySql(conn, "MeterTestRslt", "ErrorMass", "DOUBLE NOT NULL DEFAULT 0"); + EnsureColumnMySql(conn, "MeterTestRslt", "PassedMass", "TINYINT(1) NOT NULL DEFAULT 0"); + EnsureColumnMySql(conn, "MeterTestRslt", "QuantityUnits", "VARCHAR(32) NULL"); + } private static void EnsureColumnMySql( MySqlConnection conn, @@ -74,7 +99,9 @@ namespace Results.Entities.helpers alter.Transaction = transaction; alter.CommandText = "ALTER TABLE `" + tableName + "` ADD COLUMN `" + columnName + "` " + columnDefinition; - alter.ExecuteNonQuery(); + alter.ExecuteNonQuery(); + log.WarnFormat("Results DB migration: added {0}.{1} ({2}).", + tableName, columnName, columnDefinition); } } @@ -100,9 +127,28 @@ namespace Results.Entities.helpers // EnsureColumnSQLite(conn, "WaterMeter", "Q3Channel", "INTEGER NOT NULL DEFAULT 0"); // EnsureColumnSQLite(conn, "MeterTestRslt", "Q3Channel", "INTEGER NOT NULL DEFAULT 0"); - EnsureColumnSQLite(conn, "MeterTestRslt", "FlipMode", "INTEGER NULL"); - } - } + EnsureColumnSQLite(conn, "MeterTestRslt", "PulsesPerKilogram", "REAL NOT NULL DEFAULT 0"); + EnsureMeterTestResultMassColumnsSQLite(conn); + 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 EnsureMeterTestResultMassColumnsSQLite(System.Data.SQLite.SQLiteConnection conn) + { + EnsureColumnSQLite(conn, "MeterTestRslt", "MassMeter", "REAL NOT NULL DEFAULT 0"); + EnsureColumnSQLite(conn, "MeterTestRslt", "MassRef", "REAL NOT NULL DEFAULT 0"); + EnsureColumnSQLite(conn, "MeterTestRslt", "ErrorMass", "REAL NOT NULL DEFAULT 0"); + EnsureColumnSQLite(conn, "MeterTestRslt", "PassedMass", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumnSQLite(conn, "MeterTestRslt", "QuantityUnits", "TEXT NULL"); + } private static void EnsureColumnSQLite( System.Data.SQLite.SQLiteConnection conn, @@ -136,7 +182,9 @@ namespace Results.Entities.helpers { alter.CommandText = "ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + columnDefinition; - alter.ExecuteNonQuery(); + alter.ExecuteNonQuery(); + log.WarnFormat("Results DB migration: added {0}.{1} ({2}).", + tableName, columnName, columnDefinition); } } } diff --git a/TBF/ReadMe_BugFix.md b/TBF/ReadMe_BugFix.md index eb9891461..bb265abb5 100644 --- a/TBF/ReadMe_BugFix.md +++ b/TBF/ReadMe_BugFix.md @@ -3,3 +3,14 @@ | Version | Source of change | Target Environment | Title | Description | |------------|----------------------|--------------------------------------------------------------|-----------------------------------------------------|------------------------------------| | 3.9.2149.0 | Michal databse error | HeatMeters, Heat meter sensors, Procedure Dilog, Tab Process | Excanged columns value 'Sensor' and 'Heat meter sensor' | Fix in code ProcedureDlg, row 1851 | +| 3.9.2149.1 | Main TBF | General release | Version iteration | Assembly and file version increment. | +| 3.9.2149.2 | Main TBF | Poseidon register reader | Poseidon pulse and timing handling | Added reference-pulse reading in `Run()` and improved Poseidon timing/task tracking. | +| 3.9.2149.4 | Main TBF | Mass collection / Poseidon start-stop | Mass collection update | Refactored standing-start mass collection, extended Poseidon start/end data-entry configuration and improved dialog/task handling. | +| 3.9.2200.1 | Main TBF | Test infrastructure | TBF test assembly access | Added `InternalsVisibleTo` support for `TBFTests`. | +| 3.9.2201.1 | Main TBF | Poseidon CLI | Poseidon CLI configuration | Added macro descriptions, refined serial-port/CLI argument configuration and extended CLI test coverage. | +| 3.9.2202.1 | Main TBF | Poseidon CLI | CLI release iteration | Assembly and file version increment for the Poseidon CLI workstream. | +| 3.9.2203.1 | Main TBF | Poseidon CLI / smart-meter sequence | CLI test and configuration update | Updated CLI executable test setup, serial-port default responses and smart-meter component-name handling. | +| 3.9.2204.1 | Morrisville | Morrisville / Poseidon CLI | Morrisville CLI diagnostics | Restored lost 2204 versioning, added detailed CLI command/response logging and used a fixed CLI directory for deployment. | +| 3.9.2205.1 | Morrisville | Morrisville / Poseidon CLI / Results DB | Poseidon read diagnostics and results DB migration | Improved Poseidon CLI execution and diagnostics: fixed CLI working directory, exit code/stdout/stderr capture, JSON/NFC/reading validation and culture-independent decimal parsing. Added reader-cycle and fake-CLI coverage. Results DB now creates missing compatibility columns (`FlipMode`, `ExtraDataPath`, `X1`-`X9`) automatically for MySQL and SQLite. | +| 3.9.2206.0 | Morrisville / Main TBF | Poseidon CLI / Results DB | Poseidon start/end rearm and simulation isolation | Re-armed a completed START reader once for END, preventing reused START values or skipped END CLI calls. Added CLI response, dialog prefill and confirmed-value diagnostics. Simulation always runs `C:\TBF\Cli\cmdSleepTest.exe` instead of the configured physical Hat CLI. Added customer-response, decimal separator, non-zero, dialog-transfer, reader-cycle and simulation-selection regression tests. Added compatibility migration for `PulsesPerKilogram`, `MassMeter`, `MassRef`, `ErrorMass`, `PassedMass` and `QuantityUnits`. | +| 3.9.3143.0 | Ally port | Ally / Poseidon CLI / Results DB | Morrisville Poseidon fixes transferred | Ported the applicable Morrisville Poseidon read, simulation, logging, regression-test and results-schema migration fixes to the Ally source branch while retaining its compatible legacy reader configuration and non-blocking UI flow. | diff --git a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs index 6f4d39027..50e35f622 100644 --- a/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs +++ b/TBF/Rig/DataEntry/PoseidonCmd/EntryForm.cs @@ -368,10 +368,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]); + } } } @@ -426,31 +432,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; modelessDialogOpening = false; @@ -459,6 +444,32 @@ namespace TBF.Rig.DataEntry.PoseidonCmd return Event.ModelessFormClosed; /// Form closed } + 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() { diff --git a/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs b/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs index 835cb7537..fbe10e4c6 100644 --- a/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs +++ b/TBF/Rig/DataEntry/PoseidonCmd/TestStartEndForm.cs @@ -147,6 +147,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd private void SetUiBusy(bool busy, long deltaTime = -1) { + log.DebugFormat("Poseidon UI: SetUiBusy busy={0}, deltaTime={1}, enabledTextBoxes={2}", + busy, deltaTime, enabledTextBoxes.Count); // show wait cursor for form and children this.UseWaitCursor = busy; @@ -296,6 +298,8 @@ namespace TBF.Rig.DataEntry.PoseidonCmd BeginInvoke(new Action(() => UpdateValues(stratValue, enableEdit))); return; } + log.DebugFormat("Poseidon UI: UpdateValues on UI thread startValue={0}, enableEdit={1}, deltaTime={2}", + stratValue, enableEdit, deltaTime); for (int i = 0; i < TextBoxesCount; i++) { @@ -320,6 +324,7 @@ namespace TBF.Rig.DataEntry.PoseidonCmd if (enableEdit) { SetUiBusy(false, deltaTime); + log.Debug("Poseidon UI: values applied and dialog released from Loading..."); } // if you also need to enable/disable editing, do it here, // it's now safely on the UI thread. @@ -348,6 +353,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]); } } } @@ -359,6 +366,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/PoseidonReader.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs index 0e3e34a84..d889ee351 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs @@ -25,6 +25,8 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop public class PoseidonReader : ComponentBase, IDevice, IRegReaderDatastream, ISessionDataMngmnt, IOperation, ICommonRegReader { private static readonly ILog log = LogManager.GetLogger(typeof(PoseidonReader)); + // Simulation must never call the CLI configured for the physical Hat. + internal const string SimulatedCliFileName = "cmdSleepTest.exe"; public override string ToString() { return string.Format("{0}({1})", ClassName, Cfg.ToString(-1)); } readonly PoseidonCfg registerReaderCfg; @@ -302,9 +304,17 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop registerReaderCfg.MeterType, SerialPortData.DefaultPoseidonMeterType); } - serialPort = new SerialPortData(string.Format("COM{0}", registerReaderCfg.ComPortNr), - registerReaderCfg.CliFileName, - registerReaderCfg.MeterType); + string cliFileName = GetCliFileNameForMode(DebugLevel, registerReaderCfg.CliFileName); + serialPort = new SerialPortData(string.Format("COM{0}", registerReaderCfg.ComPortNr), + cliFileName, + registerReaderCfg.MeterType); + + 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 @@ -327,6 +337,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); @@ -518,12 +535,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 +552,11 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop Name, _isReadingStart ? "Begin" : "End", data.DeviceId, data.Reading, volume, volumeLi, beginWMState, endWMState); } 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); + } } } @@ -554,6 +577,29 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop CultureInfo.InvariantCulture, out value); } + /// + /// Validates a CLI response and converts its US-gallon reading to the + /// litre value assigned to the START/END dialog. + /// + 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) { failureReason = "CLI returned no JSON data."; return false; } diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonCustomerCliResponseTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonCustomerCliResponseTest.cs new file mode 100644 index 000000000..71f5c6e30 --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonCustomerCliResponseTest.cs @@ -0,0 +1,54 @@ +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 responses. + /// + [TestClass] + public class PoseidonCustomerCliResponseTest + { + // Independently calculated fixtures can differ by insignificant + // IEEE-754 rounding during gallons-to-litres conversion. + 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() + { + AssertDialogValue(Com43InitialResponse, "1000000219", "002780.99", 10527.1923171862); + AssertDialogValue(Com45InitialResponse, "1000000279", "002316.63", 8769.39850116792); + 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); + + 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..93ef2760e --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonDialogTransferTest.cs @@ -0,0 +1,129 @@ +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 + { + 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); + 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); + Assert.AreEqual(dialog.WMStartStateStr[0], FindTextBox(dialog, "startTextBox1").Text); + + ConfirmDialog(dialog); + Assert.IsTrue(dialog.Completed); + Assert.AreEqual(expectedValue, dialog.WMStartState[0], 0.0001); + + var entryForm = CreateEntryFormForTransfer(EntryForm.CurrentOp.ReadDatastream_StartStates); + TransferAcceptedDialogValues(entryForm, dialog); + Assert.AreEqual(expectedValue, entryForm.WMStartState(0), 0.0001); + } + } + + 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); + Assert.AreEqual(expectedValue.ToString(), FindTextBox(dialog, "endTextBox1").Text); + + ConfirmDialog(dialog); + Assert.IsTrue(dialog.Completed); + Assert.AreEqual(expectedValue, dialog.WMEndState[0], 0.0001); + + var entryForm = CreateEntryFormForTransfer(EntryForm.CurrentOp.ReadDatastream_EndStates); + TransferAcceptedDialogValues(entryForm, dialog); + Assert.AreEqual(expectedValue, entryForm.WMEndState(0), 0.0001); + } + } + + 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 method = typeof(EntryForm).GetMethod("StoreAcceptedDialogValues", + BindingFlags.Instance | BindingFlags.NonPublic); + Assert.IsNotNull(method); + method.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 c228200ee..89567b91d 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReadCycleTest.cs @@ -82,6 +82,26 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop } } + [TestMethod] + public void InputJson_NonZeroReading_WithDecimalCommaOrDot_IsParsedCorrectly() + { + foreach (string reading in new[] { "002433,50", "002433.50", "002898,60", "002898.60" }) + { + double value; + Assert.IsTrue(CmdPoseidonReader.TryParseCliReading(reading, out value)); + Assert.IsTrue(value > 0, "The non-zero CLI reading must remain non-zero: " + reading); + } + } + + [TestMethod] + public void SimulationMode_AlwaysUsesCmdSleepTestInsteadOfConfiguredHatCli() + { + Assert.AreEqual("cmdSleepTest.exe", CmdPoseidonReader.GetCliFileNameForMode( + Common.DebugMode.Simulate, "HatCliDemo.exe")); + Assert.AreEqual("HatCliDemo.exe", CmdPoseidonReader.GetCliFileNameForMode( + Common.DebugMode.Normal, "HatCliDemo.exe")); + } + [TestMethod] public void CliReadResponse_RequiresNfcAndCompletedReading() { diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index 8c2f17db1..7dd3ba129 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -136,6 +136,8 @@ + +