using System.CommandLine; using ZB.MOM.WW.ScadaBridge.CLI.Commands; namespace ZB.MOM.WW.ScadaBridge.CLI.Tests.Commands; /// /// #54: template script add/update must expose --min-time-between-runs /// (a duration with ms/s/min units) and --execution-timeout-seconds (an int) /// so the per-script throttle/re-fire interval and the execution-timeout override — /// previously settable only via Transport bundle import — are CLI-authorable. These /// tests pin the option surface and the duration-parsing semantics of /// . /// public class TemplateScriptTimingTests { private static readonly Option Url = new("--url") { Recursive = true }; private static readonly Option Username = new("--username") { Recursive = true }; private static readonly Option Password = new("--password") { Recursive = true }; private static readonly Option Format = CliOptions.CreateFormatOption(); private static Command ScriptGroup() => TemplateCommands.Build(Url, Format, Username, Password) .Subcommands.Single(c => c.Name == "script"); // ---- option surface ---- [Fact] public void ScriptAdd_HasTimingOptions() { var add = ScriptGroup().Subcommands.Single(c => c.Name == "add"); var names = add.Options.Select(o => o.Name).ToArray(); Assert.Contains("--min-time-between-runs", names); Assert.Contains("--execution-timeout-seconds", names); } [Fact] public void ScriptUpdate_HasTimingOptions() { var update = ScriptGroup().Subcommands.Single(c => c.Name == "update"); var names = update.Options.Select(o => o.Name).ToArray(); Assert.Contains("--min-time-between-runs", names); Assert.Contains("--execution-timeout-seconds", names); } // ---- --min-time-between-runs parsing ---- [Fact] public void MinTime_Absent_IsUnsetNoError() { Assert.True(TemplateCommands.TryParseMinTimeBetweenRuns(null, out var d, out var err)); Assert.Null(d); Assert.Null(err); } [Fact] public void MinTime_Blank_IsUnsetNoError() { Assert.True(TemplateCommands.TryParseMinTimeBetweenRuns(" ", out var d, out var err)); Assert.Null(d); Assert.Null(err); } [Theory] [InlineData("500ms", 500)] [InlineData("250MS", 250)] public void MinTime_Milliseconds_Parsed(string value, int expectedMs) { Assert.True(TemplateCommands.TryParseMinTimeBetweenRuns(value, out var d, out var err)); Assert.Null(err); Assert.Equal(TimeSpan.FromMilliseconds(expectedMs), d); } [Theory] [InlineData("5")] // bare number → seconds [InlineData("5s")] [InlineData("5sec")] [InlineData("5SEC")] public void MinTime_Seconds_BareDefaultsToSeconds(string value) { Assert.True(TemplateCommands.TryParseMinTimeBetweenRuns(value, out var d, out var err)); Assert.Null(err); Assert.Equal(TimeSpan.FromSeconds(5), d); } [Theory] [InlineData("2m")] [InlineData("2min")] [InlineData("2MIN")] public void MinTime_Minutes_Parsed(string value) { Assert.True(TemplateCommands.TryParseMinTimeBetweenRuns(value, out var d, out var err)); Assert.Null(err); Assert.Equal(TimeSpan.FromMinutes(2), d); } [Theory] [InlineData("0")] [InlineData("0s")] [InlineData("0min")] public void MinTime_Zero_IsUnset(string value) { // 0 means "no throttle" → null, mirroring DurationInput.Compose's non-positive handling. Assert.True(TemplateCommands.TryParseMinTimeBetweenRuns(value, out var d, out var err)); Assert.Null(err); Assert.Null(d); } [Theory] [InlineData("abc")] [InlineData("5h")] // hours not supported [InlineData("5days")] [InlineData("-3")] [InlineData("1.5s")] // non-integer public void MinTime_Invalid_ReturnsError(string value) { Assert.False(TemplateCommands.TryParseMinTimeBetweenRuns(value, out var d, out var err)); Assert.Null(d); Assert.NotNull(err); } /// /// An absurdly large value (16-digit millisecond count) would overflow long/TimeSpan /// without the overflow guard. The parser must return false cleanly — not throw. /// [Theory] [InlineData("9999999999999999ms")] // 16-digit ms: far exceeds TimeSpan.MaxValue (~922337203685477ms) [InlineData("9999999999999999s")] // 16-digit seconds: even larger when scaled [InlineData("9999999999999999min")] // 16-digit minutes public void MinTime_AbsurdlyLarge_ReturnsFalseWithoutThrowing(string value) { // Must not throw — TryParse contract: never throws, always returns bool. var threw = false; bool result = false; TimeSpan? duration = null; string? error = null; try { result = TemplateCommands.TryParseMinTimeBetweenRuns(value, out duration, out error); } catch { threw = true; } Assert.False(threw, "TryParseMinTimeBetweenRuns must not throw on large input"); Assert.False(result); Assert.Null(duration); Assert.NotNull(error); } }