diff --git a/TrayApp.Test/CommandLineParserTests.cs b/TrayApp.Test/CommandLineParserTests.cs index c685c21..b2b05cc 100644 --- a/TrayApp.Test/CommandLineParserTests.cs +++ b/TrayApp.Test/CommandLineParserTests.cs @@ -68,9 +68,18 @@ public void TryParse_WithAnUnreadableDuration_Fails() } [TestMethod] - public void TryParse_WithADurationLongerThanATimerCanWait_FailsWithAUsageError() + public void TryParse_WithADurationUnderATick_Fails() { - Assert.IsFalse(CommandLineParser.TryParse(["--for", "25d"], NoOptions, out _, out string error)); + Assert.IsFalse(CommandLineParser.TryParse(["--for", "0.00000001s"], NoOptions, out _, out string error)); + Assert.Contains("is not a duration", error, StringComparison.Ordinal); + } + + [TestMethod] + [DataRow("25d")] + [DataRow("20000000d")] + public void TryParse_WithADurationLongerThanATimerCanWait_FailsWithAUsageError(string text) + { + Assert.IsFalse(CommandLineParser.TryParse(["--for", text], NoOptions, out _, out string error)); Assert.Contains("--for can be at most 24 days", error, StringComparison.Ordinal); } diff --git a/TrayApp.Test/DurationParserTests.cs b/TrayApp.Test/DurationParserTests.cs index f584e9a..5357f0d 100644 --- a/TrayApp.Test/DurationParserTests.cs +++ b/TrayApp.Test/DurationParserTests.cs @@ -64,6 +64,8 @@ public void TryParse_WithABareNumberAfterAUnit_ReturnsFalse() [DataRow("5x")] [DataRow("m5")] [DataRow("1e9h")] + [DataRow("0.00000001s")] + [DataRow("0.00000004s")] public void TryParse_WithAnInvalidDuration_ReturnsFalse(string? text) { Assert.IsFalse(DurationParser.TryParse(text, out TimeSpan duration)); @@ -81,6 +83,8 @@ public void TryParse_AtTheLongestWaitATimerTakes_ReturnsTheDuration() [TestMethod] [DataRow("25d")] [DataRow("24d21h")] + [DataRow("20000000d")] + [DataRow("99999999999999999999999999999999999999999999999999999999999999999999h")] public void TryParse_LongerThanATimerCanWait_ReturnsFalse(string text) { // ManualResetEventSlim.Wait and the tray's timer both throw past int.MaxValue milliseconds, about @@ -89,6 +93,26 @@ public void TryParse_LongerThanATimerCanWait_ReturnsFalse(string text) Assert.AreEqual(TimeSpan.Zero, duration); } + [TestMethod] + [DataRow("0.00000001s")] + [DataRow("0.00000004s")] + public void TryParseAnyLength_WithADurationThatRoundsToZero_ReturnsFalse(string text) + { + // The value is positive, but under one tick, so it would be a zero-length run. + Assert.IsFalse(DurationParser.TryParseAnyLength(text, out TimeSpan duration)); + Assert.AreEqual(TimeSpan.Zero, duration); + } + + [TestMethod] + [DataRow("20000000d")] + [DataRow("99999999999999999999999999999999999999999999999999999999999999999999h")] + public void TryParseAnyLength_WithADurationBeyondTimeSpan_ReturnsTheLongestTimeSpan(string text) + { + // It is a duration, just one too long to hold, so the caller reports it as too long. + Assert.IsTrue(DurationParser.TryParseAnyLength(text, out TimeSpan duration)); + Assert.AreEqual(TimeSpan.MaxValue, duration); + } + [TestMethod] public void MaxDuration_IsTheLongestTimeoutAWaitTakes() { diff --git a/TrayApp/Cli/DurationParser.cs b/TrayApp/Cli/DurationParser.cs index 531bf9b..e5824bc 100644 --- a/TrayApp/Cli/DurationParser.cs +++ b/TrayApp/Cli/DurationParser.cs @@ -52,7 +52,10 @@ public static bool TryParse(string? text, out TimeSpan duration) /// /// The text to read. /// Receives the duration when parsing succeeds. - /// when is a positive duration. + /// + /// when is a duration of at least one tick. A duration too long + /// for to hold is returned as . + /// internal static bool TryParseAnyLength(string? text, out TimeSpan duration) { duration = TimeSpan.Zero; @@ -78,12 +81,29 @@ internal static bool TryParseAnyLength(string? text, out TimeSpan duration) sawAnyPart = true; } - if (!sawAnyPart || totalSeconds <= 0 || totalSeconds > TimeSpan.MaxValue.TotalSeconds) + if (!sawAnyPart || totalSeconds <= 0) + { + return false; + } + + // A value TimeSpan cannot hold is still a duration, just one too long for anything to wait out, so it + // is reported as the longest TimeSpan and the caller's upper bound turns it away as too long. + if (totalSeconds >= TimeSpan.MaxValue.TotalSeconds) + { + duration = TimeSpan.MaxValue; + return true; + } + + TimeSpan converted = TimeSpan.FromSeconds(totalSeconds); + + // The check above ran on the double: a value under one tick passes it and then rounds to zero, which + // would start the tool and stop it in the same moment. + if (converted <= TimeSpan.Zero) { return false; } - duration = TimeSpan.FromSeconds(totalSeconds); + duration = converted; return true; }