diff --git a/TrayApp.Test/CommandLineParserTests.cs b/TrayApp.Test/CommandLineParserTests.cs index c16d952..6a1fe5a 100644 --- a/TrayApp.Test/CommandLineParserTests.cs +++ b/TrayApp.Test/CommandLineParserTests.cs @@ -67,6 +67,13 @@ public void TryParse_WithAnUnreadableDuration_Fails() Assert.Contains("is not a duration", error, StringComparison.Ordinal); } + [TestMethod] + public void TryParse_WithADurationLongerThanATimerCanWait_FailsWithAUsageError() + { + Assert.IsFalse(CommandLineParser.TryParse(["--for", "25d"], NoOptions, out _, out string error)); + Assert.Contains("--for can be at most 24 days", error, StringComparison.Ordinal); + } + [TestMethod] public void TryParse_WithADurationAndNoValue_Fails() { diff --git a/TrayApp.Test/DurationParserTests.cs b/TrayApp.Test/DurationParserTests.cs index f955f89..f584e9a 100644 --- a/TrayApp.Test/DurationParserTests.cs +++ b/TrayApp.Test/DurationParserTests.cs @@ -69,4 +69,31 @@ public void TryParse_WithAnInvalidDuration_ReturnsFalse(string? text) Assert.IsFalse(DurationParser.TryParse(text, out TimeSpan duration)); Assert.AreEqual(TimeSpan.Zero, duration); } + + [TestMethod] + public void TryParse_AtTheLongestWaitATimerTakes_ReturnsTheDuration() + { + Assert.IsTrue(DurationParser.TryParse("24d", out TimeSpan duration)); + Assert.AreEqual(TimeSpan.FromDays(24), duration); + Assert.IsTrue(duration <= DurationParser.MaxDuration); + } + + [TestMethod] + [DataRow("25d")] + [DataRow("24d21h")] + public void TryParse_LongerThanATimerCanWait_ReturnsFalse(string text) + { + // ManualResetEventSlim.Wait and the tray's timer both throw past int.MaxValue milliseconds, about + // 24.8 days, so a longer duration would start the tool and then crash it. + Assert.IsFalse(DurationParser.TryParse(text, out TimeSpan duration)); + Assert.AreEqual(TimeSpan.Zero, duration); + } + + [TestMethod] + public void MaxDuration_IsTheLongestTimeoutAWaitTakes() + { + using System.Threading.ManualResetEventSlim signal = new(initialState: true); + Assert.IsTrue(signal.Wait(DurationParser.MaxDuration)); + _ = Assert.ThrowsExactly(() => signal.Wait(DurationParser.MaxDuration + TimeSpan.FromMilliseconds(1))); + } } diff --git a/TrayApp/Cli/CommandLineParser.cs b/TrayApp/Cli/CommandLineParser.cs index 2ff5b4c..42526b7 100644 --- a/TrayApp/Cli/CommandLineParser.cs +++ b/TrayApp/Cli/CommandLineParser.cs @@ -127,12 +127,18 @@ private static ReadResult ReadDuration( return ReadResult.Failed; } - if (!DurationParser.TryParse(text, out TimeSpan duration)) + if (!DurationParser.TryParseAnyLength(text, out TimeSpan duration)) { error = $"'{text}' is not a duration. Try 45s, 90m, 2h, or 1h30m."; return ReadResult.Failed; } + if (duration > DurationParser.MaxDuration) + { + error = $"'{text}' is too long: {argument} can be at most 24 days."; + return ReadResult.Failed; + } + state.Duration = duration; return ReadResult.Read; } diff --git a/TrayApp/Cli/DurationParser.cs b/TrayApp/Cli/DurationParser.cs index 14aa8b2..531bf9b 100644 --- a/TrayApp/Cli/DurationParser.cs +++ b/TrayApp/Cli/DurationParser.cs @@ -16,13 +16,44 @@ namespace ktsu.TrayApp.Cli; /// public static class DurationParser { + /// + /// The longest duration accepts: + /// milliseconds, a little under 24 days and 21 hours. + /// + /// + /// That is the longest timeout a wait handle or a timer takes. Anything longer parses cleanly and then + /// throws from the wait that enforces it, after the tool has + /// already started. + /// + public static readonly TimeSpan MaxDuration = TimeSpan.FromMilliseconds(int.MaxValue); + /// /// Parses a duration such as 45s, 90m, 2h, 1h30m, or a bare 90 for minutes. /// /// The text to read. /// Receives the duration when parsing succeeds. - /// when is a positive duration. + /// + /// when is a positive duration no longer than . + /// public static bool TryParse(string? text, out TimeSpan duration) + { + if (!TryParseAnyLength(text, out duration) || duration > MaxDuration) + { + duration = TimeSpan.Zero; + return false; + } + + return true; + } + + /// + /// Parses a duration as does, without the upper bound, so a + /// caller can tell a value that is too long apart from one that is not a duration at all. + /// + /// The text to read. + /// Receives the duration when parsing succeeds. + /// when is a positive duration. + internal static bool TryParseAnyLength(string? text, out TimeSpan duration) { duration = TimeSpan.Zero;