From c4b61d885bef4a05de24a61ce1cab211e4042bfc Mon Sep 17 00:00:00 2001 From: Michal Buzik Date: Mon, 20 Apr 2026 10:57:32 +0200 Subject: [PATCH] Add task state tracking and timeout handling to `CliRunner`, update project files and tests - Introduced `CliTaskState` and `CliTaskInfo` classes for enhanced tracking of task states. - Added timeout handling logic to `CliRunner` with methods like `CancelUndoneTasksAsTimedOut` and state refresh. - Updated `TBF.csproj` to include new classes `CliTaskState` and `CliTaskInfo`. - Refactored test methods in `CliRunnerTest` and introduced new timeout-related tests in `CliRunnerTimeoutTest` and `CliRunnerTimeoutUnitTest`. - Standardized code formatting and added minor improvements to logging and task lifecycle management. --- .../PoseidonCmdStartStop/CliRunner.cs | 471 +++++++++++++----- .../PoseidonCmdStartStop/CliTaskInfo.cs | 25 + .../PoseidonCmdStartStop/CliTaskState.cs | 13 + .../PoseidonCmdStartStop/PoseidonReader.cs | 138 +++-- TBF/TBF.csproj | 2 + .../PoseidonCmdStartStop/CliRunnerTest.cs | 176 ++++--- .../CliRunnerTimeoutTest.cs | 249 +++++++++ .../CliRunnerTimeoutUnitTest.cs | 76 +++ TBFTests/TBFTests.csproj | 2 + 9 files changed, 912 insertions(+), 240 deletions(-) create mode 100644 TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs create mode 100644 TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskState.cs create mode 100644 TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs create mode 100644 TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutUnitTest.cs diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs index 045dbeac8..fe4c7c1c9 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunner.cs @@ -13,46 +13,29 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop { public class CliRunner { - //static readonly ILog log = LogManager.GetLogger(typeof(CliRunner)); private readonly ILog log; - private List taskPool = new List(); - private long startTime; - private long incommingTime; - - public List TaskPool { get { return taskPool; } } - public void AddTask(Task task) { taskPool.Add(task); } - public void WaitAll() { Task.WaitAll(taskPool.ToArray()); } - public void Clear() { taskPool.Clear(); } - public void CancelAll() { Task.WhenAll(taskPool).ContinueWith(t => { }); } + private readonly List taskPool = new List(); + private long startTimeMs; + private long incommingTimeMs; - public void StartAll() + public List TaskPool { - taskPool.ForEach(t => t.Start()); - } - - public bool AreTasksDone() - { - return taskPool.All(task => task.IsCompleted); + get { return taskPool; } } public long StartTime { - get => startTime; + get { return startTimeMs; } } public long IncommingTime { - get => incommingTime; + get { return incommingTimeMs; } } - public bool TimeOutReceived(long timeout) - { - return (DateTime.Now.Ticks - startTime) > timeout; - } - public CliRunner(bool isCliLogging) { - startTime = DateTime.Now.Ticks; + ResetStartTime(); if (isCliLogging) { @@ -60,88 +43,180 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop { log = new ScopedLoggerFactory().CreateLogger( @"C:\TBF\Logs\CliRunner.txt", - 10, // maxFileSizeMB - 7, // maxBackups + 10, + 7, log4net.Core.Level.Debug, - true, // zipRolledFiles - true, // singleZipPerDay - TimeSpan.FromMinutes(2) // zipScanInterval + true, + true, + TimeSpan.FromMinutes(2) ); } catch (Exception ex) { - // Fallback to console or handle gracefully Console.WriteLine($"Failed to initialize logger: {ex.Message}"); } } } - /// - /// - /// - /// - /// - /// - public void AddSendAsync(SerialPortData data, SerialPortData.EMeterArg eMeterArg) + public void ResetStartTime() { - var task = SendAsync(data.SerialPortCmdClientPath, data.DefaultArgSettings(eMeterArg)); - taskPool.Add(task); + startTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } - - public void AddSendAsync(string fileName, string args) - { - var task = SendAsync(fileName, args); - taskPool.Add(task); - } - - - public async Task SendAsync(string fileName, string args, CancellationToken ct = default) - { - var psi = new ProcessStartInfo - { - FileName = fileName, - Arguments = args, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - using var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; - process.Start(); - - Task stdOutTask = process.StandardOutput.ReadToEndAsync(); - Task stdErrTask = process.StandardError.ReadToEndAsync(); - - try + public void Clear() + { + foreach (var item in taskPool) { - await Task.WhenAll(stdOutTask, stdErrTask, process.WaitForExitAsync(ct)); - } - catch (OperationCanceledException) - { - if (!process.HasExited) + try + { + item?.Cts?.Dispose(); + } + catch { - process.Kill(); } - throw; + try + { + if (item?.Process != null) + { + item.Process.Dispose(); + } + } + catch + { + } } - //comming answer from serial port - good place for time stamp - incommingTime = DateTime.Now.Ticks; - string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? ""); - log?.Debug(allOutput); - return allOutput; + taskPool.Clear(); } - - public void AddRunAndCaptureJsonAsync(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new() + public void WaitAll() { - var task = RunAndCaptureJsonAsync(data.SerialPortCmdClientPath, data.DefaultArgSettings(eMeterArg)); - taskPool.Add(task); + var tasks = taskPool + .Where(t => t != null && t.Task != null) + .Select(t => t.Task) + .ToArray(); + + if (tasks.Length == 0) + return; + + try + { + Task.WaitAll(tasks); + } + catch (AggregateException) + { + RefreshTaskStates(); + } } - - public async Task RunAndCaptureJsonAsync(string fileName, string args, CancellationToken ct = default) where T : new() + + public bool AreTasksDone() + { + RefreshTaskStates(); + return taskPool.Count > 0 && taskPool.All(t => t.State != CliTaskState.Running); + } + + public bool TimeOutReceived(long timeoutMs) + { + long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + return (nowMs - startTimeMs) > timeoutMs; + } + + public void RefreshTaskStates() + { + foreach (var item in taskPool) + { + if (item == null || item.Task == null) + continue; + + if (item.State == CliTaskState.TimedOut) + continue; + + if (!item.Task.IsCompleted) + { + item.State = CliTaskState.Running; + } + else if (item.Task.IsCanceled) + { + item.State = CliTaskState.Canceled; + } + else if (item.Task.IsFaulted) + { + item.State = CliTaskState.Faulted; + } + else + { + item.State = CliTaskState.Completed; + } + } + } + + public void CancelUndoneTasksAsTimedOut() + { + foreach (var item in taskPool) + { + if (item == null || item.Task == null) + continue; + + if (item.Task.IsCompleted) + { + if (item.Task.IsCanceled) + item.State = CliTaskState.Canceled; + else if (item.Task.IsFaulted) + item.State = CliTaskState.Faulted; + else + item.State = CliTaskState.Completed; + + continue; + } + + item.State = CliTaskState.TimedOut; + + try + { + item.Cts?.Cancel(); + } + catch (Exception ex) + { + log?.Warn("Cancel token failed", ex); + } + + try + { + if (item.Process != null && !item.Process.HasExited) + { + item.Process.Kill(); + } + } + catch (Exception ex) + { + log?.Warn("Kill process failed", ex); + } + } + } + + public void AddSendAsync(SerialPortData data, SerialPortData.EMeterArg eMeterArg) + { + AddSendAsync(data.SerialPortCmdClientPath, data.DefaultArgSettings(eMeterArg)); + } + + public void AddSendAsync(string fileName, string args) + { + ResetStartTime(); + + var cts = new CancellationTokenSource(); + var info = new CliTaskInfo + { + Cts = cts, + Name = "SendAsync" + }; + + var task = SendAsync(fileName, args, info, cts.Token); + info.Task = task; + taskPool.Add(info); + } + + public async Task SendAsync(string fileName, string args, CliTaskInfo info, CancellationToken ct = default) { var psi = new ProcessStartInfo { @@ -153,80 +228,210 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop CreateNoWindow = true }; - using var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; - process.Start(); + using (var process = new Process { StartInfo = psi, EnableRaisingEvents = true }) + { + info.Process = process; - var stdoutTask = process.StandardOutput.ReadToEndAsync(); - var stderrTask = process.StandardError.ReadToEndAsync(); + try + { + process.Start(); - await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct)); + Task stdOutTask = process.StandardOutput.ReadToEndAsync(); + Task stdErrTask = process.StandardError.ReadToEndAsync(); - string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? ""); - log?.Debug(allOutput); + try + { + await Task.WhenAll(stdOutTask, stdErrTask, process.WaitForExitAsync(ct)); + } + catch (OperationCanceledException) + { + if (info.State != CliTaskState.TimedOut) + info.State = CliTaskState.Canceled; - string json = ExtractJson(allOutput); - if (TryJsonStringDeserialize(json, out T result)) return result; - return default; + if (!process.HasExited) + { + process.Kill(); + } + + throw; + } + + incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + string allOutput = (stdOutTask.Result ?? "") + (stdErrTask.Result ?? ""); + log?.Debug(allOutput); + + info.State = CliTaskState.Completed; + return allOutput; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + info.State = CliTaskState.Faulted; + log?.Error("SendAsync failed", ex); + throw; + } + finally + { + info.Process = null; + } + } } - public bool TryJsonStringDeserialize(string json, out T runAndCaptureJsonAsync) where T : new() + public void AddRunAndCaptureJsonAsync(SerialPortData data, SerialPortData.EMeterArg eMeterArg) where T : new() + { + AddRunAndCaptureJsonAsync( + data.SerialPortCmdClientPath, + data.DefaultArgSettings(eMeterArg)); + } + + public void AddRunAndCaptureJsonAsync(string fileName, string args) where T : new() + { + ResetStartTime(); + + var cts = new CancellationTokenSource(); + var info = new CliTaskInfo + { + Cts = cts, + Name = $"RunAndCaptureJsonAsync<{typeof(T).Name}>" + }; + + var task = RunAndCaptureJsonAsync(fileName, args, info, cts.Token); + + info.Task = task; + taskPool.Add(info); + } + + public async Task RunAndCaptureJsonAsync(string fileName, string args, CliTaskInfo info, CancellationToken ct = default) where T : new() + { + var psi = new ProcessStartInfo + { + FileName = fileName, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (var process = new Process { StartInfo = psi, EnableRaisingEvents = true }) + { + info.Process = process; + + try + { + process.Start(); + + var stdoutTask = process.StandardOutput.ReadToEndAsync(); + var stderrTask = process.StandardError.ReadToEndAsync(); + + try + { + await Task.WhenAll(stdoutTask, stderrTask, process.WaitForExitAsync(ct)); + } + catch (OperationCanceledException) + { + if (info.State != CliTaskState.TimedOut) + info.State = CliTaskState.Canceled; + + if (!process.HasExited) + { + process.Kill(); + } + + throw; + } + + incommingTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + + string allOutput = (stdoutTask.Result ?? "") + (stderrTask.Result ?? ""); + log?.Debug(allOutput); + + string json = ExtractJson(allOutput); + T result; + if (TryJsonStringDeserialize(json, out result)) + { + info.State = CliTaskState.Completed; + return result; + } + + info.State = CliTaskState.Completed; + return default(T); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + info.State = CliTaskState.Faulted; + log?.Error("RunAndCaptureJsonAsync failed", ex); + throw; + } + finally + { + info.Process = null; + } + } + } + + public bool TryJsonStringDeserialize(string json, out T value) where T : new() { if (json != null) { try { - runAndCaptureJsonAsync = JsonConvert.DeserializeObject(json); + value = JsonConvert.DeserializeObject(json); return true; } catch (Exception ex) { - log.Debug(ex.Message); - runAndCaptureJsonAsync = TryConvert(json); + log?.Debug(ex.Message); + value = TryConvert(json); return true; } } - runAndCaptureJsonAsync = default; + value = default(T); return false; } - private static T TryConvert(string json) where T : new() { - - T obj = new T(); + T obj = new T(); - try + try + { + JObject jObject = JObject.Parse(json); + + foreach (PropertyInfo prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { - JObject jObject = JObject.Parse(json); + if (!prop.CanWrite) + continue; - foreach (PropertyInfo prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + JToken token; + if (jObject.TryGetValue(prop.Name, StringComparison.OrdinalIgnoreCase, out token)) { - if (!prop.CanWrite) continue; - - JToken token; - if (jObject.TryGetValue(prop.Name, StringComparison.OrdinalIgnoreCase, out token)) + try + { + object value = token.ToObject(prop.PropertyType); + prop.SetValue(obj, value); + } + catch { - try - { - object value = token.ToObject(prop.PropertyType); - prop.SetValue(obj, value); - } - catch - { - // leave default if conversion fails - } } - // else → keep default value } } - catch (Exception ex) - { - Console.WriteLine($"TryConvert failed: {ex.Message}"); - } + } + catch (Exception ex) + { + Console.WriteLine($"TryConvert failed: {ex.Message}"); + } - return obj; - + return obj; } public string ExtractJson(string text) @@ -241,6 +446,20 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop return null; } + + internal void AddTaskForTest(Task task, string name = "TestTask", CancellationTokenSource cts = null) + { + taskPool.Add(new CliTaskInfo + { + Task = task, + Cts = cts, + Name = name, + State = task.IsCompleted + ? (task.IsCanceled ? CliTaskState.Canceled : + task.IsFaulted ? CliTaskState.Faulted : + CliTaskState.Completed) + : CliTaskState.Running + }); + } } - } \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs new file mode 100644 index 000000000..aabb6ec79 --- /dev/null +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskInfo.cs @@ -0,0 +1,25 @@ +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop +{ + public class CliTaskInfo + { + public Task Task { get; set; } + public CancellationTokenSource Cts { get; set; } + public Process Process { get; set; } + public CliTaskState State { get; set; } = CliTaskState.Running; + public string Name { get; set; } + + public bool UseResult + { + get + { + return State == CliTaskState.Completed + && Task != null + && Task.Status == TaskStatus.RanToCompletion; + } + } + } +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskState.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskState.cs new file mode 100644 index 000000000..3cd761926 --- /dev/null +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/CliTaskState.cs @@ -0,0 +1,13 @@ + +namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop +{ + public enum CliTaskState + { + Running, + Completed, + TimedOut, + Canceled, + Faulted + } + +} \ No newline at end of file diff --git a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs index 437cb113b..9cd4702d7 100644 --- a/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs +++ b/TBF/Rig/RegisterReaders/PoseidonCmdStartStop/PoseidonReader.cs @@ -330,10 +330,12 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop /// for debug purposes what time will consume answer /// private long startTimeInMilis = -1, fullTimeInMilis = -1; + /// 30 seconds private static long SafetyTimeOut = 30 * 1000; private long incommingTime = -1; + private bool _lastOpTimedOut; + - /// 30 seconds public long DeltaTime { get{return fullTimeInMilis;}} public long IncommingTime { get{return incommingTime;}} @@ -350,108 +352,146 @@ namespace TBF.Rig.RegisterReaders.PoseidonCmdStartStop if (_currentOp == CurrentPoseidonOp.SendStartDataStream) { + CliRunner.Clear(); + _lastOpTimedOut = false; CliRunner.AddSendAsync(serialPort, SerialPortData.EMeterArg.AllParams); - return Event.Busy; + _currentOp = CurrentPoseidonOp.SendStartDataStream_Runing; + return Event.Busy; } else if (_currentOp == CurrentPoseidonOp.SendStartDataStream_Runing) { - if (CliRunner.AreTasksDone() - || CliRunner.TimeOutReceived(SafetyTimeOut)) + if (CliRunner.AreTasksDone()) { _currentOp = CurrentPoseidonOp.SendStartDataStream_Done; } - return Event.Busy; + else if (CliRunner.TimeOutReceived(SafetyTimeOut)) + { + _lastOpTimedOut = true; + CliRunner.CancelUndoneTasksAsTimedOut(); + _currentOp = CurrentPoseidonOp.SendStartDataStream_Done; + } + + return Event.Busy; } else if (_currentOp == CurrentPoseidonOp.SendStartDataStream_Done) { - var firstTask = CliRunner.TaskPool.FindLast(t => t is Task); + var firstTaskInfo = CliRunner.TaskPool + .FindLast(t => t.Task is Task && t.UseResult); - - if (firstTask != null && firstTask is Task) + if (firstTaskInfo != null) { - string data = null; - data = (firstTask as Task).Result; + var task = (Task)firstTaskInfo.Task; + string data = task.Result; - if (data != null && TryGetDeviceId(data, out wmSerialNr)) + if (!string.IsNullOrEmpty(data)) { - + TryGetDeviceId(data, out wmSerialNr); } } + else if (_lastOpTimedOut) + { + log.Warn( + $"PoseidonReader {Name}: SendStartDataStream timed out, no completed result will be used."); + } - _currentOp = CurrentPoseidonOp.Done; + CliRunner.Clear(); + _currentOp = _lastOpTimedOut ? CurrentPoseidonOp.Error : CurrentPoseidonOp.Done; return Event.Done; } - else if (_currentOp == CurrentPoseidonOp.ReadDataStream_Start + else if (_currentOp == CurrentPoseidonOp.ReadDataStream_Start || _currentOp == CurrentPoseidonOp.ReadDataStream_End) { - startTimeInMilis = DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond; + startTimeInMilis = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); incommingTime = -1; _isReadingStart = (_currentOp == CurrentPoseidonOp.ReadDataStream_Start); - CliRunner.AddRunAndCaptureJsonAsync(serialPort, SerialPortData.EMeterArg.AllParams); + + CliRunner.Clear(); + _lastOpTimedOut = false; + CliRunner.AddRunAndCaptureJsonAsync(serialPort, + SerialPortData.EMeterArg.AllParams); _currentOp = CurrentPoseidonOp.ReadDatastream_Running; return Event.Busy; - }else if (_currentOp == CurrentPoseidonOp.ReadDatastream_Running) + } + else if (_currentOp == CurrentPoseidonOp.ReadDatastream_Running) { - if (CliRunner.AreTasksDone() - || CliRunner.TimeOutReceived(SafetyTimeOut)) + if (CliRunner.AreTasksDone()) { _currentOp = CurrentPoseidonOp.ReadDatastream_Done; - //set time stamp - end of reading incommingTime = CliRunner.IncommingTime; } + else if (CliRunner.TimeOutReceived(SafetyTimeOut)) + { + _lastOpTimedOut = true; + CliRunner.CancelUndoneTasksAsTimedOut(); + _currentOp = CurrentPoseidonOp.ReadDatastream_Done; + incommingTime = CliRunner.IncommingTime; + } + return Event.Busy; } else if (_currentOp == CurrentPoseidonOp.ReadDatastream_Done) { - fullTimeInMilis = (DateTime.Now.Ticks / TimeSpan.TicksPerMillisecond) - startTimeInMilis; - log.Debug($"PoseidonReader.Run() Name= {Cfg.Name} fullTimeInMilis = {fullTimeInMilis} "); - JsonDataFromPoseidon data = null; - var first = CliRunner.TaskPool.FindLast(t => t is Task); - if (first != null && first is Task) - { - data = (first as Task).Result; + fullTimeInMilis = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTimeInMilis; + log.Debug($"PoseidonReader.Run() Name= {Cfg.Name} fullTimeInMilis = {fullTimeInMilis}"); - //GET serial number + JsonDataFromPoseidon data = null; + + var firstTaskInfo = CliRunner.TaskPool + .FindLast(t => t.Task is Task && t.UseResult); + + if (firstTaskInfo != null) + { + var task = (Task)firstTaskInfo.Task; + data = task.Result; + } + else + { + if (_lastOpTimedOut) + { + log.Warn($"PoseidonReader {Name}: ReadDatastream timed out, no completed result available."); + } + else + { + log.Warn("No completed JsonDataFromPoseidon task available."); + } + } + + if (data != null) + { if (string.IsNullOrEmpty(wmSerialNr)) { try { - wmSerialNr = data?.DeviceId ?? wmSerialNr; + wmSerialNr = data.DeviceId ?? wmSerialNr; } catch (Exception e) { - log.Error("Serial Nr - parse error!"); + log.Error("Serial Nr - parse error!", e); } } - //GET volume - if (data != null && Double.TryParse(data.Reading, out double Volume)) + double volume; + if (Double.TryParse(data.Reading, out volume)) { - double VolumeLi = Units.ConvertFrom(Unit.USgal, Volume); - //double VolumeM3 = Units.ConvertTo(Unit.m3, VolumeLi); - if (_isReadingStart) - { - //volume - beginWMState = VolumeLi; - } - else - { - //volume - endWMState = VolumeLi; - } - } + double volumeLi = Units.ConvertFrom(Unit.USgal, volume); + if (_isReadingStart) + beginWMState = volumeLi; + else + endWMState = volumeLi; + } } - - //Finish reading and loop - _currentOp = CurrentPoseidonOp.Done; + + CliRunner.Clear(); + _currentOp = _lastOpTimedOut ? CurrentPoseidonOp.Error : CurrentPoseidonOp.Done; return Event.Done; } + return Event.None; - return Event.None; } + /// Stop this operation public void Stop() { diff --git a/TBF/TBF.csproj b/TBF/TBF.csproj index cfa9ba9b2..cbaf2f9b6 100644 --- a/TBF/TBF.csproj +++ b/TBF/TBF.csproj @@ -1321,6 +1321,8 @@ RRCfgCtrl.cs + + diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs index df1635ed5..b65f1a0b7 100644 --- a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTest.cs @@ -12,35 +12,36 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop [TestSubject(typeof(CliRunner))] public class CliRunnerTest { - [TestMethod] public void ExtractJson_Test() { - String allOutput = " {\n \"DeviceId\": \"1000000267\"\n}\n"; + string allOutput = " {\n \"DeviceId\": \"1000000267\"\n}\n"; CliRunner cliRunner = new CliRunner(false); string json = cliRunner.ExtractJson(allOutput); - - Assert.IsTrue(!string.IsNullOrEmpty(json)); + + Assert.IsFalse(string.IsNullOrEmpty(json)); } - + [TestMethod] public void ExtractJson_Test2() { - String allOutput = "{\n \"NfcTagDetected\": true,\n \"ProductType\": 74,\n \"ProductTypeVersion\": \"B1.0.13\",\n \"DeviceId\": \"1000000267\",\n \"Reading\": \"0003292.1\",\n \"Reading_Totalizer\": \"000329218\",\n \"Reading_Digits\": \"8\",\n \"Reading_Shift\": \"-1\",\n \"Reading_Resolution\": \"-2\",\n \"Reading_Units\": \"2\",\n \"Reading_FlowDirection\": \"3\",\n \"Reading_FlowRate\": \"0\",\n \"CalibrationFactor\": \"3197\",\n \"ReadingComplete\": true,\n \"MeterState\": \"0x02\",\n \"OpticalDataMode\": \"0x00\",\n \"SpreadSpectrumParameters\": \"Disabled: 0xTrue\",\n \"BuildInformation\": \"\"\n}\n"; + string allOutput = "{\n \"NfcTagDetected\": true,\n \"ProductType\": 74,\n \"ProductTypeVersion\": \"B1.0.13\",\n \"DeviceId\": \"1000000267\",\n \"Reading\": \"0003292.1\",\n \"Reading_Totalizer\": \"000329218\",\n \"Reading_Digits\": \"8\",\n \"Reading_Shift\": \"-1\",\n \"Reading_Resolution\": \"-2\",\n \"Reading_Units\": \"2\",\n \"Reading_FlowDirection\": \"3\",\n \"Reading_FlowRate\": \"0\",\n \"CalibrationFactor\": \"3197\",\n \"ReadingComplete\": true,\n \"MeterState\": \"0x02\",\n \"OpticalDataMode\": \"0x00\",\n \"SpreadSpectrumParameters\": \"Disabled: 0xTrue\",\n \"BuildInformation\": \"\"\n}\n"; CliRunner cliRunner = new CliRunner(false); string json = cliRunner.ExtractJson(allOutput); - - Assert.IsTrue(!string.IsNullOrEmpty(json)); + + Assert.IsFalse(string.IsNullOrEmpty(json)); } [TestMethod] public void TryJsonStringDeserialize_Test() { - String allOutput = "{\n \"NfcTagDetected\": true,\n \"ProductType\": 74,\n \"ProductTypeVersion\": \"B1.0.13\",\n \"DeviceId\": \"1000000267\",\n \"Reading\": \"0003292.1\",\n \"Reading_Totalizer\": \"000329218\",\n \"Reading_Digits\": \"8\",\n \"Reading_Shift\": \"-1\",\n \"Reading_Resolution\": \"-2\",\n \"Reading_Units\": \"2\",\n \"Reading_FlowDirection\": \"3\",\n \"Reading_FlowRate\": \"0\",\n \"CalibrationFactor\": \"3197\",\n \"ReadingComplete\": true,\n \"MeterState\": \"0x02\",\n \"OpticalDataMode\": \"0x00\",\n \"SpreadSpectrumParameters\": \"Disabled: 0xTrue\",\n \"BuildInformation\": \"\"\n}\n"; + string allOutput = "{\n \"NfcTagDetected\": true,\n \"ProductType\": 74,\n \"ProductTypeVersion\": \"B1.0.13\",\n \"DeviceId\": \"1000000267\",\n \"Reading\": \"0003292.1\",\n \"Reading_Totalizer\": \"000329218\",\n \"Reading_Digits\": \"8\",\n \"Reading_Shift\": \"-1\",\n \"Reading_Resolution\": \"-2\",\n \"Reading_Units\": \"2\",\n \"Reading_FlowDirection\": \"3\",\n \"Reading_FlowRate\": \"0\",\n \"CalibrationFactor\": \"3197\",\n \"ReadingComplete\": true,\n \"MeterState\": \"0x02\",\n \"OpticalDataMode\": \"0x00\",\n \"SpreadSpectrumParameters\": \"Disabled: 0xTrue\",\n \"BuildInformation\": \"\"\n}\n"; + CliRunner cliRunner = new CliRunner(false); string json = cliRunner.ExtractJson(allOutput); - var ok = cliRunner.TryJsonStringDeserialize(json, out var dto); + bool ok = cliRunner.TryJsonStringDeserialize(json, out var dto); + Assert.IsTrue(ok, "Deserialization failed"); Assert.IsNotNull(dto); Assert.AreEqual("1000000267", dto.DeviceId); @@ -53,120 +54,165 @@ namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop [TestMethod] public void RunMultipleTimesTestProgram_CheckParalelWork() { - SerialPortData serialPort = new SerialPortData("COM3","cmdSleepTest.exe",74); - - // Check if the executable exists in current directory + SerialPortData serialPort = new SerialPortData("COM3", "cmdSleepTest.exe", 74); + if (!System.IO.File.Exists(serialPort.SerialPortCmdClientPath)) { - Assert.Inconclusive($"Test executable '{serialPort.SerialPortCmdClientPath}' not found. Please ensure cmdSleepTest.exe exists in the test directory."); + Assert.Inconclusive( + $"Test executable '{serialPort.SerialPortCmdClientPath}' not found. Please ensure cmdSleepTest.exe exists in the test directory."); return; } + CliRunner cliRunner = new CliRunner(false); - + CliRunner cliRunnerOne = new CliRunner(false); - DateTime StartTime = DateTime.Now; - cliRunnerOne.AddRunAndCaptureJsonAsync(serialPort, SerialPortData.EMeterArg.AllParams); + DateTime startTime = DateTime.Now; + + cliRunnerOne.AddRunAndCaptureJsonAsync( + serialPort, + SerialPortData.EMeterArg.AllParams); + cliRunnerOne.WaitAll(); - TimeSpan OneEndTime = DateTime.Now - StartTime; - - StartTime = DateTime.Now; - Console.WriteLine($"Start Time Loop: {StartTime.ToString("yyyy-MM-dd HH:mm:ss")}"); - + + TimeSpan oneEndTime = DateTime.Now - startTime; + + startTime = DateTime.Now; + Console.WriteLine($"Start Time Loop: {startTime:yyyy-MM-dd HH:mm:ss}"); + for (int i = 0; i < 20; i++) { - cliRunner.AddRunAndCaptureJsonAsync(serialPort, SerialPortData.EMeterArg.AllParams); + cliRunner.AddRunAndCaptureJsonAsync( + serialPort, + SerialPortData.EMeterArg.AllParams); } + cliRunner.WaitAll(); - DateTime EndTime = DateTime.Now; - Console.WriteLine($"End Time Loop: {EndTime.ToString("yyyy-MM-dd HH:mm:ss")}"); - TimeSpan delta = EndTime - StartTime; - Console.WriteLine($"Delta Time One process {OneEndTime.Hours:D2}:{OneEndTime.Minutes:D2}:{OneEndTime.Seconds:D2}.{OneEndTime.Milliseconds:D3} " + - $"vs Loop: {delta.Hours:D2}:{delta.Minutes:D2}:{delta.Seconds:D2}.{delta.Milliseconds:D3}"); + DateTime endTime = DateTime.Now; + Console.WriteLine($"End Time Loop: {endTime:yyyy-MM-dd HH:mm:ss}"); + + TimeSpan delta = endTime - startTime; + Console.WriteLine( + $"Delta Time One process {oneEndTime.Hours:D2}:{oneEndTime.Minutes:D2}:{oneEndTime.Seconds:D2}.{oneEndTime.Milliseconds:D3} " + + $"vs Loop: {delta.Hours:D2}:{delta.Minutes:D2}:{delta.Seconds:D2}.{delta.Milliseconds:D3}"); Console.WriteLine($"Total tasks: {cliRunner.TaskPool.Count}"); - - - foreach (Task task in cliRunner.TaskPool) + + foreach (CliTaskInfo taskInfo in cliRunner.TaskPool) { - if (task != null) + Assert.IsNotNull(taskInfo); + Assert.IsNotNull(taskInfo.Task); + + if (taskInfo.UseResult) { + var task = taskInfo.Task as Task; + Assert.IsNotNull(task, "Expected Task."); + JsonDataFromPoseidon jsonDataFromPoseidon = task.Result; - Console.WriteLine($"Result: {jsonDataFromPoseidon.DeviceId}"); + Console.WriteLine($"Result: {jsonDataFromPoseidon?.DeviceId}"); Assert.IsNotNull(jsonDataFromPoseidon); } + else + { + Assert.Fail($"Task did not complete successfully. State={taskInfo.State}, Status={taskInfo.Task.Status}"); + } } - - // Check that the total time is greater than the sum of the individual times - Assert.IsTrue((cliRunner.TaskPool.Count*OneEndTime.Ticks) > delta.Ticks); - + + Assert.IsTrue((cliRunner.TaskPool.Count * oneEndTime.Ticks) > delta.Ticks); } - + [TestMethod] public async Task RunMultipleTimesTestProgram_CheckParalelWorkCumulative() { - SerialPortData serialPort = new SerialPortData("COM3","cmdSleepTest.exe",74); - - // Check if the executable exists in current directory + SerialPortData serialPort = new SerialPortData("COM3", "cmdSleepTest.exe", 74); + if (!System.IO.File.Exists(serialPort.SerialPortCmdClientPath)) { - Assert.Inconclusive($"Test executable '{serialPort.SerialPortCmdClientPath}' not found. Please ensure cmdSleepTest.exe exists in the test directory."); + Assert.Inconclusive( + $"Test executable '{serialPort.SerialPortCmdClientPath}' not found. Please ensure cmdSleepTest.exe exists in the test directory."); return; } List cliRunnerList = new List(); - + for (int i = 0; i < 20; i++) + { cliRunnerList.Add(new CliRunner(false)); - + } + CliRunner cliRunnerOne = new CliRunner(false); - DateTime StartTime = DateTime.Now; - cliRunnerOne.AddRunAndCaptureJsonAsync(serialPort, SerialPortData.EMeterArg.AllParams); + DateTime startTime = DateTime.Now; + + cliRunnerOne.AddRunAndCaptureJsonAsync( + serialPort, + SerialPortData.EMeterArg.AllParams); + cliRunnerOne.WaitAll(); - TimeSpan OneEndTime = DateTime.Now - StartTime; - - StartTime = DateTime.Now; - Console.WriteLine($"Start Time Loop: {StartTime.ToString("yyyy-MM-dd HH:mm:ss")}"); + + TimeSpan oneEndTime = DateTime.Now - startTime; + + startTime = DateTime.Now; + Console.WriteLine($"Start Time Loop: {startTime:yyyy-MM-dd HH:mm:ss}"); for (int iCliRunner = 0; iCliRunner < cliRunnerList.Count; iCliRunner++) { - cliRunnerList[iCliRunner].AddRunAndCaptureJsonAsync(serialPort, + cliRunnerList[iCliRunner].AddRunAndCaptureJsonAsync( + serialPort, SerialPortData.EMeterArg.AllParams); for (int i = 0; i < 20; i++) { - cliRunnerList[iCliRunner].AddRunAndCaptureJsonAsync(serialPort, + cliRunnerList[iCliRunner].AddRunAndCaptureJsonAsync( + serialPort, SerialPortData.EMeterArg.AllParams); } } - // Wait for ALL tasks from ALL runners - var allTasks = cliRunnerList.SelectMany(r => r.TaskPool).ToArray(); + Task[] allTasks = cliRunnerList + .SelectMany(r => r.TaskPool) + .Where(ti => ti != null && ti.Task != null) + .Select(ti => ti.Task) + .ToArray(); + await Task.WhenAll(allTasks); - DateTime EndTime = DateTime.Now; - Console.WriteLine($"End Time Loop: {EndTime.ToString("yyyy-MM-dd HH:mm:ss")}"); - TimeSpan delta = EndTime - StartTime; - Console.WriteLine($"Delta Time One process {OneEndTime.Hours:D2}:{OneEndTime.Minutes:D2}:{OneEndTime.Seconds:D2}.{OneEndTime.Milliseconds:D3} " + - $"vs Loop: {delta.Hours:D2}:{delta.Minutes:D2}:{delta.Seconds:D2}.{delta.Milliseconds:D3}"); + DateTime endTime = DateTime.Now; + Console.WriteLine($"End Time Loop: {endTime:yyyy-MM-dd HH:mm:ss}"); + + TimeSpan delta = endTime - startTime; + Console.WriteLine( + $"Delta Time One process {oneEndTime.Hours:D2}:{oneEndTime.Minutes:D2}:{oneEndTime.Seconds:D2}.{oneEndTime.Milliseconds:D3} " + + $"vs Loop: {delta.Hours:D2}:{delta.Minutes:D2}:{delta.Seconds:D2}.{delta.Milliseconds:D3}"); Console.WriteLine($"Total tasks: {cliRunnerList.Sum(runner => runner.TaskPool.Count)}"); - + for (int iCliRunner = 0; iCliRunner < cliRunnerList.Count; iCliRunner++) { string results = $"iCliRunner: {iCliRunner}"; - foreach (Task task in cliRunnerList[iCliRunner].TaskPool) + + foreach (CliTaskInfo taskInfo in cliRunnerList[iCliRunner].TaskPool) { - if (task != null) + Assert.IsNotNull(taskInfo); + Assert.IsNotNull(taskInfo.Task); + + if (taskInfo.UseResult) { + var task = taskInfo.Task as Task; + Assert.IsNotNull(task, "Expected Task."); + JsonDataFromPoseidon jsonDataFromPoseidon = task.Result; - results += ($" ID: {jsonDataFromPoseidon.DeviceId}"); + results += $" ID: {jsonDataFromPoseidon?.DeviceId}"; Assert.IsNotNull(jsonDataFromPoseidon); } + else + { + Assert.Fail( + $"Task did not complete successfully. Runner={iCliRunner}, State={taskInfo.State}, Status={taskInfo.Task.Status}"); + } } + Console.WriteLine(results); } - // Check that the total time is greater than the sum of the individual times - Assert.IsTrue((cliRunnerList.Count*OneEndTime.Ticks) > delta.Ticks); + Assert.IsTrue((cliRunnerList.Count * oneEndTime.Ticks) > delta.Ticks); } } } \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs new file mode 100644 index 000000000..66a0a303a --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutTest.cs @@ -0,0 +1,249 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.PoseidonCmdStartStop; + +namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop +{ + [TestClass] + [TestSubject(typeof(CliRunner))] + public class CliRunnerTimeoutTest + { + private static string GetCmdSleepTestPath() + { + var serialPort = new SerialPortData("COM3", "cmdSleepTest.exe", 74); + return serialPort.SerialPortCmdClientPath; + } + + private static void EnsureExeExists(string exePath) + { + if (!File.Exists(exePath)) + { + Assert.Inconclusive( + $"Test executable '{exePath}' not found. " + + "Please ensure cmdSleepTest.exe exists in the test directory."); + } + } + + [TestMethod] + public async Task Timeout_ShouldBeDetected_ForLongRunningTask() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=5000"); + + await Task.Delay(200); + + bool timedOut = cliRunner.TimeOutReceived(100); + + Assert.IsTrue(timedOut, "Timeout should have been detected."); + } + + [TestMethod] + public async Task CancelUndoneTasksAsTimedOut_ShouldMarkRunningTaskAsTimedOut() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=5000"); + + await Task.Delay(150); + + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + Assert.AreEqual(1, cliRunner.TaskPool.Count); + + var taskInfo = cliRunner.TaskPool.Single(); + + Assert.IsNotNull(taskInfo); + Assert.AreEqual(CliTaskState.TimedOut, taskInfo.State); + Assert.IsFalse(taskInfo.UseResult, "Timed out task must not be used."); + } + + [TestMethod] + public void CancelUndoneTasksAsTimedOut_ShouldKeepCompletedTaskCompleted() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=50"); + + try + { + cliRunner.WaitAll(); + } + catch + { + // let assertions inspect states + } + + cliRunner.RefreshTaskStates(); + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + Assert.AreEqual(1, cliRunner.TaskPool.Count); + + var taskInfo = cliRunner.TaskPool.Single(); + + Assert.IsNotNull(taskInfo); + Assert.AreEqual(CliTaskState.Completed, taskInfo.State); + Assert.IsTrue(taskInfo.UseResult, "Completed task should remain usable."); + + var task = taskInfo.Task as Task; + Assert.IsNotNull(task); + Assert.AreEqual(TaskStatus.RanToCompletion, task.Status); + Assert.IsNotNull(task.Result); + Assert.IsFalse(string.IsNullOrEmpty(task.Result.DeviceId)); + } + + [TestMethod] + public async Task CancelUndoneTasksAsTimedOut_ShouldKeepDoneTask_AndMarkUndoneTask() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=50"); + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=5000"); + + await Task.Delay(250); + + cliRunner.RefreshTaskStates(); + + Assert.AreEqual(2, cliRunner.TaskPool.Count); + Assert.AreEqual(1, cliRunner.TaskPool.Count(t => t.State == CliTaskState.Completed), + "Exactly one fast task should be completed before timeout handling."); + + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + var doneTask = cliRunner.TaskPool.Single(t => t.State == CliTaskState.Completed); + var timedOutTask = cliRunner.TaskPool.Single(t => t.State == CliTaskState.TimedOut); + + Assert.IsTrue(doneTask.UseResult); + Assert.IsFalse(timedOutTask.UseResult); + + var completed = doneTask.Task as Task; + Assert.IsNotNull(completed); + Assert.AreEqual(TaskStatus.RanToCompletion, completed.Status); + Assert.IsNotNull(completed.Result); + } + + [TestMethod] + public async Task AreTasksDone_ShouldReturnTrue_AfterTimeoutCancellation() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=5000"); + + await Task.Delay(150); + + Assert.IsFalse(cliRunner.AreTasksDone(), "Task should still be running before timeout cancellation."); + + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + Assert.IsTrue(cliRunner.AreTasksDone(), + "After timed out tasks are marked, runner should report no running tasks."); + } + + [TestMethod] + public async Task TimedOutTask_ShouldNotHaveUseResult() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=5000"); + + await Task.Delay(150); + + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + var taskInfo = cliRunner.TaskPool.Single(); + + Assert.AreEqual(CliTaskState.TimedOut, taskInfo.State); + Assert.IsFalse(taskInfo.UseResult); + } + + [TestMethod] + public async Task MultipleSlowTasks_ShouldAllBeMarkedTimedOut() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + for (int i = 0; i < 5; i++) + { + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=5000"); + } + + await Task.Delay(200); + + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + Assert.AreEqual(5, cliRunner.TaskPool.Count); + Assert.AreEqual(5, cliRunner.TaskPool.Count(t => t.State == CliTaskState.TimedOut)); + Assert.AreEqual(0, cliRunner.TaskPool.Count(t => t.UseResult)); + } + + [TestMethod] + public void FastTask_WithInvalidJson_ShouldFinishButNotProduceUsefulResult() + { + string exePath = GetCmdSleepTestPath(); + EnsureExeExists(exePath); + + var cliRunner = new CliRunner(false); + + cliRunner.AddRunAndCaptureJsonAsync(exePath, "sleep=50 invalidjson=true"); + + try + { + cliRunner.WaitAll(); + } + catch + { + } + + cliRunner.RefreshTaskStates(); + + Assert.AreEqual(1, cliRunner.TaskPool.Count); + + var taskInfo = cliRunner.TaskPool.Single(); + Assert.AreEqual(CliTaskState.Completed, taskInfo.State); + + var task = taskInfo.Task as Task; + Assert.IsNotNull(task); + Assert.AreEqual(TaskStatus.RanToCompletion, task.Status); + + // depending on your TryJsonStringDeserialize fallback, + // result may be null or an empty/default object + // so avoid asserting UseResult=false here + // instead assert no valid device id + if (task.Result != null) + { + Assert.IsTrue(string.IsNullOrEmpty(task.Result.DeviceId), + "Invalid JSON should not produce a valid DeviceId."); + } + } + } +} \ No newline at end of file diff --git a/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutUnitTest.cs b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutUnitTest.cs new file mode 100644 index 000000000..79ab85aea --- /dev/null +++ b/TBFTests/Rig/RegisterReaders/PoseidonCmdStartStop/CliRunnerTimeoutUnitTest.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using TBF.Rig.RegisterReaders.PoseidonCmdStartStop; + +namespace TBFTests.Rig.RegisterReaders.PoseidonCmdStartStop +{ + [TestClass] + [TestSubject(typeof(CliRunner))] + public class CliRunnerTimeoutUnitTest + { + [TestMethod] + public async Task CancelUndoneTasksAsTimedOut_ShouldMarkOnlyRunningTasks() + { + var cliRunner = new CliRunner(false); + + var completedTask = Task.FromResult("done"); + + var cts = new CancellationTokenSource(); + var runningTask = Task.Delay(TimeSpan.FromSeconds(30), cts.Token); + + cliRunner.AddTaskForTest(completedTask, "completed"); + cliRunner.AddTaskForTest(runningTask, "running", cts); + + await Task.Delay(50); + + cliRunner.RefreshTaskStates(); + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + Assert.AreEqual(2, cliRunner.TaskPool.Count); + + var completed = cliRunner.TaskPool.First(t => t.Name == "completed"); + var timedOut = cliRunner.TaskPool.First(t => t.Name == "running"); + + Assert.AreEqual(CliTaskState.Completed, completed.State); + Assert.IsTrue(completed.UseResult); + + Assert.AreEqual(CliTaskState.TimedOut, timedOut.State); + Assert.IsFalse(timedOut.UseResult); + } + + [TestMethod] + public void TimeOutReceived_ShouldReturnTrue_WhenElapsedExceedsTimeout() + { + var cliRunner = new CliRunner(false); + + Thread.Sleep(30); + + Assert.IsTrue(cliRunner.TimeOutReceived(1)); + } + + [TestMethod] + public async Task AreTasksDone_ShouldReturnTrue_WhenTimedOutTasksAreMarked() + { + var cliRunner = new CliRunner(false); + + var cts = new CancellationTokenSource(); + var runningTask = Task.Delay(TimeSpan.FromSeconds(30), cts.Token); + + cliRunner.AddTaskForTest(runningTask, "running", cts); + + await Task.Delay(50); + + Assert.IsFalse(cliRunner.AreTasksDone()); + + cliRunner.CancelUndoneTasksAsTimedOut(); + cliRunner.RefreshTaskStates(); + + Assert.IsTrue(cliRunner.AreTasksDone()); + } + } +} \ No newline at end of file diff --git a/TBFTests/TBFTests.csproj b/TBFTests/TBFTests.csproj index f3d666950..2c59a2925 100644 --- a/TBFTests/TBFTests.csproj +++ b/TBFTests/TBFTests.csproj @@ -104,6 +104,8 @@ + +