Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions TrayApp.Test/CommandLineParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
24 changes: 24 additions & 0 deletions TrayApp.Test/DurationParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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
Expand All @@ -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()
{
Expand Down
26 changes: 23 additions & 3 deletions TrayApp/Cli/DurationParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ public static bool TryParse(string? text, out TimeSpan duration)
/// </summary>
/// <param name="text">The text to read.</param>
/// <param name="duration">Receives the duration when parsing succeeds.</param>
/// <returns><see langword="true"/> when <paramref name="text"/> is a positive duration.</returns>
/// <returns>
/// <see langword="true"/> when <paramref name="text"/> is a duration of at least one tick. A duration too long
/// for <see cref="TimeSpan"/> to hold is returned as <see cref="TimeSpan.MaxValue"/>.
/// </returns>
internal static bool TryParseAnyLength(string? text, out TimeSpan duration)
{
duration = TimeSpan.Zero;
Expand All @@ -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;
}

Expand Down
Loading