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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ reference ambiguous.
| `OnStart(Action action)` | `TrayAppBuilder` | What to run when the tool starts, in both modes. |
| `OnStop(Action action)` | `TrayAppBuilder` | What to run on the way out, in both modes. |
| `RefreshOn(Action<Action> subscribe)` | `TrayAppBuilder` | Hands the tool a delegate that refreshes the menu. |
| `Preferences(IPersistenceProvider<string> provider, string key, TimeSpan? debounce)` | `TrayAppBuilder` | Remembers persisted toggles between runs. |
| `Preferences(IPersistenceProvider<string> provider, string key, TimeSpan? debounce)` | `TrayAppBuilder` | Remembers persisted toggles between runs. `debounce` defaults to half a second; a negative value, `Timeout.InfiniteTimeSpan` or more than about 49.7 days throws `ArgumentOutOfRangeException`. |
| `RunAsync(string[] args)` | `Task<int>` | Parses the command line and runs the tool. 0 success, 1 the platform refused, 2 the command line was wrong. |

### `TrayMenu`
Expand Down
44 changes: 44 additions & 0 deletions TrayApp.Test/TrayAppBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,50 @@
Assert.IsNull(app.IsActive);
}

[TestMethod]
[DataRow(-1000.0)]
[DataRow(-1.0)]
[DataRow(60.0 * 24 * 60 * 60 * 1000)]
public void Preferences_WithADebounceATimerCannotWaitFor_Throws(double milliseconds)
{
InMemoryPersistenceProvider<string> provider = new();
TrayAppBuilder builder = TrayAppBuilder.Create("demo");

ArgumentOutOfRangeException exception = Assert.ThrowsExactly<ArgumentOutOfRangeException>(
() => builder.Preferences(provider, debounce: TimeSpan.FromMilliseconds(milliseconds)));
Assert.AreEqual("debounce", exception.ParamName);
}

[TestMethod]
public void Preferences_WithAnInfiniteDebounce_Throws()
{
InMemoryPersistenceProvider<string> provider = new();

_ = Assert.ThrowsExactly<ArgumentOutOfRangeException>(
() => TrayAppBuilder.Create("demo").Preferences(provider, debounce: System.Threading.Timeout.InfiniteTimeSpan));
}

[TestMethod]
public void Preferences_WithZeroOrNoDebounce_IsAccepted()
{
InMemoryPersistenceProvider<string> provider = new();

Assert.AreEqual(TimeSpan.Zero, TrayAppBuilder.Create("demo").Preferences(provider, debounce: TimeSpan.Zero).Build().PreferenceDebounce);
Assert.AreEqual(TimeSpan.FromMilliseconds(500), TrayAppBuilder.Create("demo").Preferences(provider).Build().PreferenceDebounce);
}

[TestMethod]
public void Preferences_WithTheLongestDebounce_IsAcceptedAndATimerTakesIt()

Check warning on line 164 in TrayApp.Test/TrayAppBuilderTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Add at least one assertion to this test case.

Check warning on line 164 in TrayApp.Test/TrayAppBuilderTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Add at least one assertion to this test case.

Check warning on line 164 in TrayApp.Test/TrayAppBuilderTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Add at least one assertion to this test case.

Check warning on line 164 in TrayApp.Test/TrayAppBuilderTests.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Add at least one assertion to this test case.

Check failure on line 164 in TrayApp.Test/TrayAppBuilderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_TrayApp&issues=AaEgDq5Qx7WqIdKgVD3s&open=AaEgDq5Qx7WqIdKgVD3s&pullRequest=44
{
InMemoryPersistenceProvider<string> provider = new();
TrayAppDefinition app = TrayAppBuilder.Create("demo")
.Preferences(provider, debounce: TrayAppBuilder.MaxPreferenceDebounce)
.Build();

using DebouncedPreferenceStore store = new(provider, "tray", app.PreferenceDebounce);
store.Set("a", true);
}

[TestMethod]
public async Task LoadPreferences_RestoresOnlyThePersistedToggles()
{
Expand Down
26 changes: 25 additions & 1 deletion TrayApp/TrayAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public sealed class TrayAppBuilder

private static readonly TimeSpan DefaultPreferenceDebounce = TimeSpan.FromMilliseconds(500);

/// <summary>
/// The longest debounce a <see cref="System.Threading.Timer"/> due time can hold, about 49.7 days.
/// </summary>
internal static readonly TimeSpan MaxPreferenceDebounce = TimeSpan.FromMilliseconds(uint.MaxValue - 1.0);

private readonly string name;
private readonly List<TrayMenuItem> items = [];
private readonly List<TrayToggleItem> toggles = [];
Expand Down Expand Up @@ -338,9 +343,16 @@ public TrayAppBuilder RefreshOn(Action<Action> subscribe)
/// <c>ktsu.Essentials.FileSystemProviders.Native</c> in the tool, and hand the result in here.
/// </param>
/// <param name="key">The key the state is stored under. The default suits a provider already namespaced by application.</param>
/// <param name="debounce">How long a burst of changes is coalesced for, or <see langword="null"/> for half a second.</param>
/// <param name="debounce">
/// How long a burst of changes is coalesced for, from zero up to about 49.7 days, or <see langword="null"/>
/// for half a second.
/// </param>
/// <returns>The builder, for chaining.</returns>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> or <paramref name="key"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="debounce"/> is negative, including <see cref="System.Threading.Timeout.InfiniteTimeSpan"/>,
/// or longer than a timer can wait.
/// </exception>
/// <remarks>
/// A tool that never calls this does no I/O at all and needs no provider package: persistence is a
/// dependency of the tool head, never of this library.
Expand All @@ -350,6 +362,18 @@ public TrayAppBuilder Preferences(IPersistenceProvider<string> provider, string
Ensure.NotNull(provider);
Ensure.NotNull(key);

// The debounce is only handed to a timer on the first click of a persisted toggle, from inside the
// tray's click handler, so a value the timer refuses would surface there as a crash far from its
// cause. Infinite is refused too: it would mean "never write until exit", and an exit by SIGTERM
// does not flush.
if (debounce is TimeSpan value && (value < TimeSpan.Zero || value > MaxPreferenceDebounce))
{
throw new ArgumentOutOfRangeException(
nameof(debounce),
value,
"The preference debounce must be between zero and about 49.7 days.");
}

persistence = provider;
preferenceKey = key;
preferenceDebounce = debounce ?? DefaultPreferenceDebounce;
Expand Down
Loading