- allow CliRunner to run without an injected logger - keep CLI diagnostics optional using null-safe logging - standardize non-zero process failure text as "exit code <n>" - update Poseidon CLI v2 test expectation to meter type 74
310 lines
10 KiB
C#
310 lines
10 KiB
C#
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 SerialPortData CreateDefaultSerialPortData()
|
|
{
|
|
return new SerialPortData(
|
|
"COM3",
|
|
"cmdSleepTest.exe",
|
|
SerialPortData.MeterProduct.Poseidon,
|
|
SerialPortData.HatType.Mth,
|
|
30,
|
|
TBF.Rig.RegisterReaders.PoseidonCmdStartStop.GetDescription.ToDescription(SerialPortData.MacroTypes.readall));
|
|
}
|
|
|
|
private static string GetCmdSleepTestPath()
|
|
{
|
|
return CreateDefaultSerialPortData().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 void SerialPortData_ForTimeoutTests_ShouldUseVersion2Defaults()
|
|
{
|
|
SerialPortData serialPort = CreateDefaultSerialPortData();
|
|
|
|
string args = serialPort.DefaultArgSettings(SerialPortData.EMeterArg.AllParams);
|
|
|
|
Assert.AreEqual(
|
|
"-p COM3 -m 74 --hat mth --timeout 30 --operation readall",
|
|
args);
|
|
}
|
|
|
|
[TestMethod]
|
|
public void SerialPortData_ForTimeoutTests_ShouldStillSupportOldMeterType()
|
|
{
|
|
SerialPortData serialPort = new SerialPortData(
|
|
"COM3",
|
|
"cmdSleepTest.exe",
|
|
74);
|
|
|
|
string args = serialPort.DefaultArgSettings(SerialPortData.EMeterArg.AllParams);
|
|
|
|
Assert.AreEqual(
|
|
"-p COM3 -m 74 --hat mth --timeout 10 --operation readall",
|
|
args);
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task Timeout_ShouldBeDetected_ForLongRunningTask()
|
|
{
|
|
string exePath = GetCmdSleepTestPath();
|
|
EnsureExeExists(exePath);
|
|
|
|
var cliRunner = new CliRunner(false);
|
|
|
|
cliRunner.AddRunAndCaptureJsonAsync<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>;
|
|
|
|
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<JsonDataFromPoseidon>(exePath, "sleep=50");
|
|
cliRunner.AddRunAndCaptureJsonAsync<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>;
|
|
|
|
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<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>(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<JsonDataFromPoseidon>(
|
|
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<JsonDataFromPoseidon>(
|
|
exePath,
|
|
"sleep=50 invalidjson=true");
|
|
|
|
try
|
|
{
|
|
cliRunner.WaitAll();
|
|
}
|
|
catch
|
|
{
|
|
// Let assertions inspect states.
|
|
}
|
|
|
|
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<JsonDataFromPoseidon>;
|
|
|
|
Assert.IsNotNull(task);
|
|
Assert.AreEqual(TaskStatus.RanToCompletion, task.Status);
|
|
|
|
Assert.IsNull(task.Result, "Invalid JSON must not create a default reading object.");
|
|
}
|
|
|
|
[TestMethod]
|
|
public async Task CliProcess_ExitErrorWithoutJson_RecordsDiagnosticsAndReturnsNoReading()
|
|
{
|
|
string cmdExe = System.Environment.GetEnvironmentVariable("ComSpec") ?? @"C:\\Windows\\System32\\cmd.exe";
|
|
var cliRunner = new CliRunner(false);
|
|
var info = new CliTaskInfo { Name = "missing-dependency-simulation" };
|
|
|
|
JsonDataFromPoseidon result = await cliRunner.RunAndCaptureJsonAsync<JsonDataFromPoseidon>(
|
|
cmdExe,
|
|
"/d /c \"echo Could not load file or assembly 'log4net' 1>&2 & exit /b 17\"",
|
|
info);
|
|
|
|
Assert.IsNull(result, "A failed CLI process must not create a default zero reading.");
|
|
Assert.AreEqual(17, info.ExitCode);
|
|
StringAssert.Contains(info.FailureReason, "exit code 17");
|
|
StringAssert.Contains(info.StandardError, "log4net");
|
|
}
|
|
}
|
|
}
|