diff --git a/src/CuteUtils.Tests/CuteUtils.Tests.csproj b/src/CuteUtils.Tests/CuteUtils.Tests.csproj
index 95dadc8..e625376 100644
--- a/src/CuteUtils.Tests/CuteUtils.Tests.csproj
+++ b/src/CuteUtils.Tests/CuteUtils.Tests.csproj
@@ -10,10 +10,13 @@
-
-
-
-
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/src/CuteUtils.Tests/Misc/SafeRetryTests.cs b/src/CuteUtils.Tests/Misc/SafeRetryTests.cs
new file mode 100644
index 0000000..dcc597c
--- /dev/null
+++ b/src/CuteUtils.Tests/Misc/SafeRetryTests.cs
@@ -0,0 +1,222 @@
+using CuteUtils.Misc;
+
+namespace CuteUtils.Tests.Misc;
+
+[TestClass]
+public class SafeRetryTests
+{
+ [TestMethod]
+ public async Task RetryAsyncT_SucceedsFirstTry()
+ {
+ int callCount = 0;
+ int result = await SafeRetry.RetryAsync(async () =>
+ {
+ callCount++;
+ await Task.Delay(10);
+ return 42;
+ });
+ Assert.AreEqual(1, callCount);
+ Assert.AreEqual(42, result);
+ }
+
+ [TestMethod]
+ public async Task RetryAsyncT_RetriesAndSucceeds()
+ {
+ int callCount = 0;
+ int result = await SafeRetry.RetryAsync(async () =>
+ {
+ callCount++;
+ if (callCount < 3)
+ {
+ throw new InvalidOperationException();
+ }
+
+ await Task.Delay(10);
+ return 99;
+ }, maxRetries: 5, delay: TimeSpan.FromMilliseconds(1));
+ Assert.AreEqual(3, callCount);
+ Assert.AreEqual(99, result);
+ }
+
+ [TestMethod]
+ public async Task RetryAsyncT_ThrowsAfterMaxRetries()
+ {
+ int callCount = 0;
+ _ = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ _ = await SafeRetry.RetryAsync(() =>
+ {
+ callCount++;
+ throw new InvalidOperationException("fail");
+ }, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
+ });
+ Assert.AreEqual(2, callCount);
+ }
+
+ [TestMethod]
+ public async Task RetryAsync_SucceedsFirstTry()
+ {
+ int callCount = 0;
+ await SafeRetry.RetryAsync(async () =>
+ {
+ callCount++;
+ await Task.Delay(10);
+ });
+ Assert.AreEqual(1, callCount);
+ }
+
+ [TestMethod]
+ public async Task RetryAsync_RetriesAndSucceeds()
+ {
+ int callCount = 0;
+ await SafeRetry.RetryAsync(async () =>
+ {
+ callCount++;
+ if (callCount < 2)
+ {
+ throw new Exception();
+ }
+
+ await Task.Delay(10);
+ }, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
+ Assert.AreEqual(2, callCount);
+ }
+
+ [TestMethod]
+ public async Task RetryAsync_ThrowsAfterMaxRetries()
+ {
+ int callCount = 0;
+ _ = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ await SafeRetry.RetryAsync(() =>
+ {
+ callCount++;
+ throw new Exception();
+ }, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
+ });
+ Assert.AreEqual(2, callCount);
+ }
+
+ [TestMethod]
+ public void RetryT_SucceedsFirstTry()
+ {
+ int callCount = 0;
+ int result = SafeRetry.Retry(() =>
+ {
+ callCount++;
+ return 7;
+ });
+ Assert.AreEqual(1, callCount);
+ Assert.AreEqual(7, result);
+ }
+
+ [TestMethod]
+ public void RetryT_RetriesAndSucceeds()
+ {
+ int callCount = 0;
+ int result = SafeRetry.Retry(() =>
+ {
+ callCount++;
+ if (callCount < 4)
+ {
+ throw new Exception();
+ }
+
+ return 123;
+ }, maxRetries: 5, delay: TimeSpan.FromMilliseconds(1));
+ Assert.AreEqual(4, callCount);
+ Assert.AreEqual(123, result);
+ }
+
+ [TestMethod]
+ public void RetryT_ThrowsAfterMaxRetries()
+ {
+ int callCount = 0;
+ _ = Assert.ThrowsException(() =>
+ {
+ _ = SafeRetry.Retry(() =>
+ {
+ callCount++;
+ throw new Exception();
+ }, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
+ });
+ Assert.AreEqual(3, callCount);
+ }
+
+ [TestMethod]
+ public void Retry_SucceedsFirstTry()
+ {
+ int callCount = 0;
+ SafeRetry.Retry(() =>
+ {
+ callCount++;
+ });
+ Assert.AreEqual(1, callCount);
+ }
+
+ [TestMethod]
+ public void Retry_RetriesAndSucceeds()
+ {
+ int callCount = 0;
+ SafeRetry.Retry(() =>
+ {
+ callCount++;
+ if (callCount < 2)
+ {
+ throw new Exception();
+ }
+ }, maxRetries: 3, delay: TimeSpan.FromMilliseconds(1));
+ Assert.AreEqual(2, callCount);
+ }
+
+ [TestMethod]
+ public void Retry_ThrowsAfterMaxRetries()
+ {
+ int callCount = 0;
+ _ = Assert.ThrowsException(() =>
+ {
+ SafeRetry.Retry(() =>
+ {
+ callCount++;
+ throw new Exception();
+ }, maxRetries: 2, delay: TimeSpan.FromMilliseconds(1));
+ });
+ Assert.AreEqual(2, callCount);
+ }
+
+ [TestMethod]
+ public async Task RetryAsyncT_ThrowsArgumentNullException()
+ {
+ _ = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ _ = await SafeRetry.RetryAsync(null!);
+ });
+ }
+
+ [TestMethod]
+ public async Task RetryAsync_ThrowsArgumentNullException()
+ {
+ _ = await Assert.ThrowsExceptionAsync(async () =>
+ {
+ await SafeRetry.RetryAsync(null!);
+ });
+ }
+
+ [TestMethod]
+ public void RetryT_ThrowsArgumentNullException()
+ {
+ _ = Assert.ThrowsException(() =>
+ {
+ _ = SafeRetry.Retry(null!);
+ });
+ }
+
+ [TestMethod]
+ public void Retry_ThrowsArgumentNullException()
+ {
+ _ = Assert.ThrowsException(() =>
+ {
+ SafeRetry.Retry(null!);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/CuteUtils.Tests/Misc/WaitTests.cs b/src/CuteUtils.Tests/Misc/WaitTests.cs
new file mode 100644
index 0000000..efd14fe
--- /dev/null
+++ b/src/CuteUtils.Tests/Misc/WaitTests.cs
@@ -0,0 +1,137 @@
+using CuteUtils.Misc;
+
+namespace CuteUtils.Tests.Misc;
+
+[TestClass]
+public class WaitTests
+{
+ [TestMethod]
+ public async Task UntilAsync_ConditionMet_CompletesSuccessfully()
+ {
+ bool flag = false;
+ _ = Task.Run(() =>
+ {
+ Thread.Sleep(200);
+ flag = true;
+ });
+ await Wait.UntilAsync(() => flag, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(2));
+ Assert.IsTrue(flag);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(TimeoutException))]
+ public async Task UntilAsync_ConditionNotMet_ThrowsTimeoutException()
+ {
+ await Wait.UntilAsync(() => false, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(200));
+ }
+
+ [TestMethod]
+ public void Until_ConditionMet_CompletesSuccessfully()
+ {
+ bool flag = false;
+ _ = Task.Run(() =>
+ {
+ Thread.Sleep(200);
+ flag = true;
+ });
+ Wait.Until(() => flag, TimeSpan.FromMilliseconds(50), TimeSpan.FromSeconds(2));
+ Assert.IsTrue(flag);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(TimeoutException))]
+ public void Until_ConditionNotMet_ThrowsTimeoutException()
+ {
+ Wait.Until(() => false, TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(200));
+ }
+
+ [TestMethod]
+ public async Task WaitForEventAsync_EventRaised_CompletesSuccessfully()
+ {
+ void subscribe(Action handler)
+ {
+ _ = Task.Run(() =>
+ {
+ Thread.Sleep(200);
+ handler();
+ });
+ }
+ await Wait.WaitForEventAsync(subscribe, TimeSpan.FromSeconds(2));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(TimeoutException))]
+ public async Task WaitForEventAsync_EventNotRaised_ThrowsTimeoutException()
+ {
+ void subscribe(Action handler) { }
+ await Wait.WaitForEventAsync(subscribe, TimeSpan.FromMilliseconds(200));
+ }
+
+ [TestMethod]
+ public async Task WaitForEventAsyncT_EventRaised_CompletesWithValue()
+ {
+ void subscribe(Action handler)
+ {
+ _ = Task.Run(() =>
+ {
+ Thread.Sleep(200);
+ handler(42);
+ });
+ }
+ int result = await Wait.WaitForEventAsync((Action>)subscribe, TimeSpan.FromSeconds(2));
+ Assert.AreEqual(42, result);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(TimeoutException))]
+ public async Task WaitForEventAsyncT_EventNotRaised_ThrowsTimeoutException()
+ {
+ void subscribe(Action handler) { }
+ _ = await Wait.WaitForEventAsync((Action>)subscribe, TimeSpan.FromMilliseconds(200));
+ }
+
+ [TestMethod]
+ public void WaitForEvent_EventRaised_CompletesSuccessfully()
+ {
+ void subscribe(Action handler)
+ {
+ _ = Task.Run(() =>
+ {
+ Thread.Sleep(200);
+ handler();
+ });
+ }
+ Wait.WaitForEvent(subscribe, TimeSpan.FromSeconds(2));
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(TimeoutException))]
+ public void WaitForEvent_EventNotRaised_ThrowsTimeoutException()
+ {
+ void subscribe(Action handler) { }
+ Wait.WaitForEvent(subscribe, TimeSpan.FromMilliseconds(200));
+ }
+
+ [TestMethod]
+ public void WaitForEventT_EventRaised_CompletesWithValue()
+ {
+ void subscribe(Action handler)
+ {
+ _ = Task.Run(() =>
+ {
+ Thread.Sleep(200);
+ handler("hello");
+ });
+ }
+ string result = Wait.WaitForEvent((Action>)subscribe, TimeSpan.FromSeconds(2));
+ Assert.AreEqual("hello", result);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(TimeoutException))]
+ public void WaitForEventT_EventNotRaised_ThrowsTimeoutException()
+ {
+ void subscribe(Action handler) { }
+ _ = Wait.WaitForEvent((Action>)subscribe, TimeSpan.FromMilliseconds(200));
+ }
+}
\ No newline at end of file
diff --git a/src/CuteUtils/CuteUtils.csproj b/src/CuteUtils/CuteUtils.csproj
index fe76f91..a6dbae5 100644
--- a/src/CuteUtils/CuteUtils.csproj
+++ b/src/CuteUtils/CuteUtils.csproj
@@ -29,7 +29,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -44,7 +44,7 @@
-->
$([System.IO.Path]::GetTempPath())
- 1.0.0.0
+ 1.1.0.0
A "cute" utility library for C#
https://github.com/Stone-Red-Code/CuteUtils
Utility, Helper
diff --git a/src/CuteUtils/Misc/SafeRetry.cs b/src/CuteUtils/Misc/SafeRetry.cs
new file mode 100644
index 0000000..d99fd3a
--- /dev/null
+++ b/src/CuteUtils/Misc/SafeRetry.cs
@@ -0,0 +1,130 @@
+namespace CuteUtils.Misc;
+
+///
+/// Provides methods for safely retrying actions and functions with optional delay and retry count.
+///
+public static class SafeRetry
+{
+ ///
+ /// Retries an asynchronous function returning a value, up to a specified number of times, with an optional delay between attempts.
+ ///
+ /// The type of the result returned by the function.
+ /// The asynchronous function to execute.
+ /// The maximum number of retry attempts. Default is 3.
+ /// The delay between retry attempts. Default is 1 second.
+ /// The result of the function if successful.
+ /// Thrown if is null.
+ /// Thrown if all retry attempts fail.
+ public static async Task RetryAsync(Func> action, int maxRetries = 3, TimeSpan? delay = null)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ delay ??= TimeSpan.FromSeconds(1);
+ Exception? lastException = null;
+ for (int i = 0; i < maxRetries; i++)
+ {
+ try
+ {
+ return await action();
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+ await Task.Delay(delay.Value);
+ }
+ }
+ throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
+ }
+
+ ///
+ /// Retries an asynchronous action up to a specified number of times, with an optional delay between attempts.
+ ///
+ /// The asynchronous action to execute.
+ /// The maximum number of retry attempts. Default is 3.
+ /// The delay between retry attempts. Default is 1 second.
+ /// A task representing the asynchronous operation.
+ /// Thrown if is null.
+ /// Thrown if all retry attempts fail.
+ public static async Task RetryAsync(Func action, int maxRetries = 3, TimeSpan? delay = null)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ delay ??= TimeSpan.FromSeconds(1);
+ Exception? lastException = null;
+ for (int i = 0; i < maxRetries; i++)
+ {
+ try
+ {
+ await action();
+ return;
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+ await Task.Delay(delay.Value);
+ }
+ }
+ throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
+ }
+
+ ///
+ /// Retries a synchronous function returning a value, up to a specified number of times, with an optional delay between attempts.
+ ///
+ /// The type of the result returned by the function.
+ /// The function to execute.
+ /// The maximum number of retry attempts. Default is 3.
+ /// The delay between retry attempts. Default is 1 second.
+ /// The result of the function if successful.
+ /// Thrown if is null.
+ /// Thrown if all retry attempts fail.
+ public static T Retry(Func action, int maxRetries = 3, TimeSpan? delay = null)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ delay ??= TimeSpan.FromSeconds(1);
+ Exception? lastException = null;
+ for (int i = 0; i < maxRetries; i++)
+ {
+ try
+ {
+ return action();
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+ System.Threading.Thread.Sleep(delay.Value);
+ }
+ }
+ throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
+ }
+
+ ///
+ /// Retries a synchronous action up to a specified number of times, with an optional delay between attempts.
+ ///
+ /// The action to execute.
+ /// The maximum number of retry attempts. Default is 3.
+ /// The delay between retry attempts. Default is 1 second.
+ /// Thrown if is null.
+ /// Thrown if all retry attempts fail.
+ public static void Retry(Action action, int maxRetries = 3, TimeSpan? delay = null)
+ {
+ ArgumentNullException.ThrowIfNull(action);
+
+ delay ??= TimeSpan.FromSeconds(1);
+ Exception? lastException = null;
+ for (int i = 0; i < maxRetries; i++)
+ {
+ try
+ {
+ action();
+ return;
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+ System.Threading.Thread.Sleep(delay.Value);
+ }
+ }
+ throw new InvalidOperationException($"Failed after {maxRetries} attempts.", lastException);
+ }
+}
\ No newline at end of file
diff --git a/src/CuteUtils/StringExtentions.cs b/src/CuteUtils/Misc/StringExtentions.cs
similarity index 99%
rename from src/CuteUtils/StringExtentions.cs
rename to src/CuteUtils/Misc/StringExtentions.cs
index ca82ea2..1c9d0dc 100644
--- a/src/CuteUtils/StringExtentions.cs
+++ b/src/CuteUtils/Misc/StringExtentions.cs
@@ -1,7 +1,7 @@
using System.Globalization;
using System.Text;
-namespace CuteUtils;
+namespace CuteUtils.Misc;
///
/// Extensions
diff --git a/src/CuteUtils/Misc/WaitUntil.cs b/src/CuteUtils/Misc/WaitUntil.cs
new file mode 100644
index 0000000..470fc06
--- /dev/null
+++ b/src/CuteUtils/Misc/WaitUntil.cs
@@ -0,0 +1,181 @@
+namespace CuteUtils.Misc;
+
+///
+/// Provides utility methods for waiting on conditions or events with optional timeouts and intervals.
+///
+public static class Wait
+{
+ ///
+ /// Asynchronously waits until the specified condition is met or the timeout is reached.
+ ///
+ /// A function that returns true when the wait should end.
+ /// The interval to check the condition. Defaults to 100ms.
+ /// The maximum time to wait. Defaults to infinite.
+ /// Thrown if the condition is not met within the timeout.
+ public static async Task UntilAsync(Func condition, TimeSpan? interval = null, TimeSpan? timeout = null)
+ {
+ timeout ??= TimeSpan.MaxValue;
+ interval ??= TimeSpan.FromMilliseconds(100);
+ DateTime endTime = DateTime.UtcNow.Add(timeout.Value);
+ while (!condition() && DateTime.UtcNow < endTime)
+ {
+ await Task.Delay(interval.Value);
+ }
+ if (!condition())
+ {
+ throw new TimeoutException("The condition was not met within the specified timeout.");
+ }
+ }
+
+ ///
+ /// Synchronously waits until the specified condition is met or the timeout is reached.
+ ///
+ /// A function that returns true when the wait should end.
+ /// The interval to check the condition. Defaults to 100ms.
+ /// The maximum time to wait. Defaults to infinite.
+ /// Thrown if the condition is not met within the timeout.
+ public static void Until(Func condition, TimeSpan? interval = null, TimeSpan? timeout = null)
+ {
+ timeout ??= TimeSpan.MaxValue;
+ interval ??= TimeSpan.FromMilliseconds(100);
+ DateTime endTime = DateTime.UtcNow.Add(timeout.Value);
+ while (!condition() && DateTime.UtcNow < endTime)
+ {
+ System.Threading.Thread.Sleep(interval.Value);
+ }
+ if (!condition())
+ {
+ throw new TimeoutException("The condition was not met within the specified timeout.");
+ }
+ }
+
+ ///
+ /// Asynchronously waits for an event to be raised or the timeout to be reached.
+ ///
+ /// An action that subscribes a handler to the event.
+ /// The maximum time to wait. Defaults to infinite.
+ /// Thrown if the event is not raised within the timeout.
+ public static async Task WaitForEventAsync(Action subscribe, TimeSpan? timeout = null)
+ {
+ timeout ??= TimeSpan.MaxValue;
+ TaskCompletionSource tcs = new TaskCompletionSource();
+ void Handler()
+ {
+ _ = tcs.TrySetResult();
+ }
+
+ subscribe(Handler);
+
+ using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
+ using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
+ {
+ try
+ {
+ await tcs.Task.ConfigureAwait(false);
+ }
+ catch (TaskCanceledException)
+ {
+ throw new TimeoutException("The event was not raised within the specified timeout.");
+ }
+ }
+ }
+
+ ///
+ /// Asynchronously waits for an event to be raised or the timeout to be reached.
+ ///
+ /// An action that subscribes a handler to the event.
+ /// The maximum time to wait. Defaults to infinite.
+ /// The type of the event argument.
+ /// A task that completes with the event argument when the event is raised.
+ /// Thrown if the event is not raised within the timeout.
+ public static async Task WaitForEventAsync(Action> subscribe, TimeSpan? timeout = null)
+ {
+ timeout ??= TimeSpan.MaxValue;
+
+ TaskCompletionSource tcs = new TaskCompletionSource();
+
+ void Handler(T arg)
+ {
+ _ = tcs.TrySetResult(arg);
+ }
+
+ subscribe(Handler);
+
+ using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
+ using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
+ {
+ try
+ {
+ return await tcs.Task;
+ }
+ catch (TaskCanceledException)
+ {
+ throw new TimeoutException("The event was not raised within the specified timeout.");
+ }
+ }
+ }
+
+ ///
+ /// Synchronously waits for an event to be raised or the timeout to be reached.
+ ///
+ /// An action that subscribes a handler to the event.
+ /// The maximum time to wait. Defaults to infinite.
+ /// Thrown if the event is not raised within the timeout.
+ public static void WaitForEvent(Action subscribe, TimeSpan? timeout = null)
+ {
+ timeout ??= TimeSpan.MaxValue;
+ TaskCompletionSource tcs = new TaskCompletionSource();
+ void Handler()
+ {
+ _ = tcs.TrySetResult();
+ }
+ subscribe(Handler);
+ using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
+ using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
+ {
+ try
+ {
+ tcs.Task.Wait();
+ }
+ catch (AggregateException ex) when (ex.InnerException is TaskCanceledException)
+ {
+ throw new TimeoutException("The event was not raised within the specified timeout.", ex);
+ }
+ }
+ }
+
+ ///
+ /// Synchronously waits for an event to be raised or the timeout to be reached.
+ ///
+ /// An action that subscribes a handler to the event.
+ /// The maximum time to wait. Defaults to infinite.
+ /// The type of the event argument.
+ /// The event argument when the event is raised.
+ /// Thrown if the event is not raised within the timeout.
+ public static T WaitForEvent(Action> subscribe, TimeSpan? timeout = null)
+ {
+ timeout ??= TimeSpan.MaxValue;
+ TaskCompletionSource tcs = new TaskCompletionSource();
+ void Handler(T arg)
+ {
+ _ = tcs.TrySetResult(arg);
+ }
+ subscribe(Handler);
+ using CancellationTokenSource cts = new CancellationTokenSource(timeout.Value);
+ using (cts.Token.Register(() => tcs.TrySetCanceled(cts.Token)))
+ {
+ try
+ {
+ return tcs.Task.GetAwaiter().GetResult();
+ }
+ catch (AggregateException ex) when (ex.InnerException is TaskCanceledException)
+ {
+ throw new TimeoutException("The event was not raised within the specified timeout.", ex);
+ }
+ catch (TaskCanceledException ex)
+ {
+ throw new TimeoutException("The event was not raised within the specified timeout.", ex);
+ }
+ }
+ }
+}
\ No newline at end of file