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
7 changes: 7 additions & 0 deletions TrayApp.Test/CommandLineParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
27 changes: 27 additions & 0 deletions TrayApp.Test/DurationParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,31 @@
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);

Check warning on line 78 in TrayApp.Test/DurationParserTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.IsLessThanOrEqualTo' instead of 'Assert.IsTrue'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_TrayApp&issues=AaDfbvs8TFIbZ_TpZyfO&open=AaDfbvs8TFIbZ_TpZyfO&pullRequest=15
}

[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));

Check warning on line 96 in TrayApp.Test/DurationParserTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_TrayApp&issues=AaDfbvs8TFIbZ_TpZyfP&open=AaDfbvs8TFIbZ_TpZyfP&pullRequest=15
_ = Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => signal.Wait(DurationParser.MaxDuration + TimeSpan.FromMilliseconds(1)));

Check warning on line 97 in TrayApp.Test/DurationParserTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_TrayApp&issues=AaDfbvs8TFIbZ_TpZyfQ&open=AaDfbvs8TFIbZ_TpZyfQ&pullRequest=15
}
}
8 changes: 7 additions & 1 deletion TrayApp/Cli/CommandLineParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
33 changes: 32 additions & 1 deletion TrayApp/Cli/DurationParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,44 @@ namespace ktsu.TrayApp.Cli;
/// </remarks>
public static class DurationParser
{
/// <summary>
/// The longest duration <see cref="TryParse(string?, out TimeSpan)"/> accepts: <see cref="int.MaxValue"/>
/// milliseconds, a little under 24 days and 21 hours.
/// </summary>
/// <remarks>
/// That is the longest timeout a wait handle or a timer takes. Anything longer parses cleanly and then
/// throws <see cref="ArgumentOutOfRangeException"/> from the wait that enforces it, after the tool has
/// already started.
/// </remarks>
public static readonly TimeSpan MaxDuration = TimeSpan.FromMilliseconds(int.MaxValue);

/// <summary>
/// Parses a duration such as <c>45s</c>, <c>90m</c>, <c>2h</c>, <c>1h30m</c>, or a bare <c>90</c> for minutes.
/// </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 positive duration no longer than <see cref="MaxDuration"/>.
/// </returns>
public static bool TryParse(string? text, out TimeSpan duration)
{
if (!TryParseAnyLength(text, out duration) || duration > MaxDuration)
{
duration = TimeSpan.Zero;
return false;
}

return true;
}

/// <summary>
/// Parses a duration as <see cref="TryParse(string?, out TimeSpan)"/> 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.
/// </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>
internal static bool TryParseAnyLength(string? text, out TimeSpan duration)
{
duration = TimeSpan.Zero;

Expand Down
Loading