< Summary

Information
Class: AmbientServices.AmbientProgressService
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/ProgressHelpers.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 3
Uncovered lines: 0
Coverable lines: 3
Total lines: 236
Line coverage: 100%
Branch coverage
75%
Covered branches: 3
Total branches: 4
Branch coverage: 75%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_Progress()100%22100%
get_GlobalProgress()50%22100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/ProgressHelpers.cs

#LineLine coverage
 1using System;
 2using System.Threading;
 3
 4namespace AmbientServices;
 5
 6/// <summary>
 7/// A static class that holds a property used to more conveniently access the ambient <see cref="IAmbientProgress"/>.
 8/// </summary>
 9/// <remarks>
 10/// <pitch>The one-liner way to get the current operation's progress tracker without holding a service reference.</pitch
 11/// <pledge>Returns the calling execution context's progress from the local (or, for <see cref="GlobalProgress"/>, the g
 12/// <plan>A static facade over <c>Ambient.GetService&lt;IAmbientProgressService&gt;()</c> delegating to <see cref="IAmbi
 13/// </remarks>
 14public static class AmbientProgressService
 15{
 216    private static readonly AmbientService<IAmbientProgressService> _Progress = Ambient.GetService<IAmbientProgressServi
 17    /// <summary>
 18    /// Gets the <see cref="IAmbientProgress"/> from the current local (or global) ambient progress service.
 19    /// </summary>
 220    public static IAmbientProgress? Progress => _Progress.Local?.Progress;
 21    /// <summary>
 22    /// Gets the <see cref="IAmbientProgress"/> from the global ambient progress service.
 23    /// </summary>
 24    [ExcludeFromCoverage]   // this can't be fully tested without possibly affecting other tests and their coverage beca
 225    public static IAmbientProgress? GlobalProgress => _Progress.Global?.Progress;
 26}
 27
 28
 29/// <summary>
 30/// A cancellation token source that works with ambient timers in addition to system timers.
 31/// </summary>
 32/// <remarks>
 33/// <pitch>A <see cref="CancellationTokenSource"/> stand-in whose scheduled cancellations follow the ambient clock, so t
 34/// <pledge>
 35/// Which clock schedules timed cancellations is fixed at construction; under a paused clock, a scheduled cancellation f
 36/// </pledge>
 37/// <plan>Wraps a system <see cref="CancellationTokenSource"/> and schedules timed cancellation with a one-shot <see cre
 38/// </remarks>
 39public class AmbientCancellationTokenSource : IDisposable
 40{
 41    private static readonly AmbientService<IAmbientClock> _AmbientClock = Ambient.GetService<IAmbientClock>();
 42    private static readonly CancellationToken _AlreadyCancelled = AlreadyCancelledToken();
 43    private static CancellationToken AlreadyCancelledToken()
 44    {
 45        CancellationTokenSource source = new(); source.Cancel(); return source.Token;
 46    }
 47
 48#pragma warning disable IDE0052 // Remove unread private members    I'd like to keep this around for debugging and just 
 49    private readonly IAmbientClock? _clock;
 50#pragma warning restore IDE0052 // Remove unread private members
 51    private CancellationTokenSource? _tokenSource;      // note that if this is not nullable, you can't tell if the toke
 52    private AmbientEventTimer? _ambientTimer;
 53    private int _cancelAfterChecks;
 54    private int _checks;
 55
 56    /// <summary>
 57    /// Constructs an ambient cancellation token source using a system <see cref="CancellationTokenSource"/>.
 58    /// </summary>
 59    /// <param name="tokenSource">A <see cref="CancellationTokenSource"/> from the system.  If null, makes a cancellatio
 60    public AmbientCancellationTokenSource(CancellationTokenSource? tokenSource = null)
 61    {
 62        _tokenSource = tokenSource ?? new CancellationTokenSource();
 63    }
 64    /// <summary>
 65    /// Constructs an ambient cancellation token source using the ambient clock.
 66    /// </summary>
 67    /// <param name="timeout">A <see cref="TimeSpan"/> indicating how long to wait before timing out.</param>
 68    public AmbientCancellationTokenSource(TimeSpan timeout)
 69        : this(_AmbientClock.Override ?? _AmbientClock.Local, ValidatedDelay(timeout, nameof(timeout)))
 70    {
 71    }
 72    /// <summary>
 73    /// Constructs an ambient cancellation token source using the ambient clock.
 74    /// </summary>
 75    /// <param name="timeoutMilliseconds">The number of milliseconds to wait before timing out.</param>
 76    public AmbientCancellationTokenSource(int timeoutMilliseconds)
 77        : this(_AmbientClock.Override ?? _AmbientClock.Local, ValidatedDelay(timeoutMilliseconds, nameof(timeoutMillisec
 78    {
 79    }
 80    /// <summary>
 81    /// Constructs an ambient cancellation token source using the specified clock.
 82    /// </summary>
 83    /// <param name="clock">The <see cref="IAmbientClock"/> to use for the token source.</param>
 84    /// <param name="timeout">An optional timeout indicating how long before the associated cancellation token should be
 85    public AmbientCancellationTokenSource(IAmbientClock? clock, TimeSpan? timeout = null)
 86    {
 87        if (timeout != null) ValidatedDelay(timeout.Value, nameof(timeout));
 88        _clock = clock;
 89        _tokenSource = new CancellationTokenSource();
 90        if (timeout != null)
 91        {
 92            ScheduleCancellation(timeout.Value);
 93        }
 94    }
 95
 96    /// <summary>
 97    /// Validates a cancellation delay the way <see cref="CancellationTokenSource"/> does and returns it as a <see cref=
 98    /// </summary>
 99    /// <remarks>
 100    /// Note that this deliberately does not defer to the scheduling timer's validation: <see cref="CancellationTokenSou
 101    /// </remarks>
 102    /// <param name="milliseconds">The number of milliseconds to delay, with -1 meaning never.</param>
 103    /// <param name="parameterName">The name of the parameter being validated, for the exception.</param>
 104    private static TimeSpan ValidatedDelay(double milliseconds, string parameterName)
 105    {
 106        if (milliseconds < -1 || milliseconds > int.MaxValue) throw new ArgumentOutOfRangeException(parameterName);
 107        return TimeSpan.FromMilliseconds(milliseconds);
 108    }
 109    /// <summary>
 110    /// Validates a cancellation delay the way <see cref="CancellationTokenSource"/> does and returns it unchanged.
 111    /// </summary>
 112    /// <param name="delay">A <see cref="TimeSpan"/> indicating how long to delay, with -1 milliseconds meaning never.</
 113    /// <param name="parameterName">The name of the parameter being validated, for the exception.</param>
 114    private static TimeSpan ValidatedDelay(TimeSpan delay, string parameterName)
 115    {
 116        return ValidatedDelay(delay.TotalMilliseconds, parameterName);
 117    }
 118
 119    private void ScheduleCancellation(TimeSpan delay)
 120    {
 121        double milliseconds = delay.TotalMilliseconds;
 122        // never?  then there is nothing to schedule
 123        if (milliseconds < 0) return;
 124        // already due?  then cancel right now, because a timer cannot be given a zero interval
 125        if (milliseconds == 0)
 126        {
 127            _tokenSource?.Cancel();
 128            return;
 129        }
 130        AmbientEventTimer timer = new AmbientEventTimer(delay);
 131        _ambientTimer = timer;
 132        void handler(object? source, System.Timers.ElapsedEventArgs e)
 133        {
 134            // Use the captured timer: Dispose() may null _ambientTimer concurrently while SkipAhead raises Elapsed.
 135            timer.Elapsed -= handler;
 136            _tokenSource?.Cancel();
 137            timer.Dispose();
 138            if (ReferenceEquals(_ambientTimer, timer))
 139                _ambientTimer = null;
 140        }
 141
 142        timer.Elapsed += handler;   // note that the handler will keep the timer and the token source alive until the ev
 143        timer.Enabled = true;
 144    }
 145
 146    /// <summary>
 147    /// Gets the <see cref="CancellationToken"/> associated with the source.
 148    /// </summary>
 149    public CancellationToken Token => _tokenSource?.Token ?? _AlreadyCancelled;
 150    /// <summary>
 151    /// Gets whether or not a cancellation has been requested.
 152    /// </summary>
 153    public bool IsCancellationRequested
 154    {
 155        get
 156        {
 157            if (_cancelAfterChecks != 0 && Interlocked.Increment(ref _checks) > _cancelAfterChecks) _tokenSource?.Cancel
 158            return _tokenSource?.IsCancellationRequested ?? true;
 159        }
 160    }
 161    /// <summary>
 162    /// Gets the number of checks that have been made towards cancellation (see <see cref="CancelAfterChecks(int)"/>.
 163    /// </summary>
 164    public int Checks => _checks;
 165    /// <summary>
 166    /// Marks the associated token as canceled.
 167    /// </summary>
 168    public void Cancel() { Cancel(false); }
 169    /// <summary>
 170    /// Marks the associated token as canceled.
 171    /// </summary>
 172    /// <param name="throwOnFirstException">true if exceptions should immediately propagate, otherwise false.</param>
 173    public void Cancel(bool throwOnFirstException) { _tokenSource?.Cancel(throwOnFirstException); }
 174    /// <summary>
 175    /// Schedules a cancellation after the specified time.
 176    /// </summary>
 177    /// <param name="millisecondsDelay">The number of milliseconds to delay before cancelling.</param>
 178    public void CancelAfter(int millisecondsDelay)
 179    {
 180        TimeSpan delay = ValidatedDelay(millisecondsDelay, nameof(millisecondsDelay));
 181        if (_ambientTimer != null) _ambientTimer.Dispose();
 182        ScheduleCancellation(delay);
 183    }
 184    /// <summary>
 185    /// Schedules a cancellation after the specified time.
 186    /// </summary>
 187    /// <param name="delay">A <see cref="TimeSpan"/> indicating how long to delay before cancelling.</param>
 188    public void CancelAfter(TimeSpan delay)
 189    {
 190        TimeSpan validatedDelay = ValidatedDelay(delay, nameof(delay));
 191        if (_ambientTimer != null) _ambientTimer.Dispose();
 192        ScheduleCancellation(validatedDelay);
 193    }
 194    /// <summary>
 195    /// Schedules a cancellation after a certain number of checks to see if the token was canceled.
 196    /// This is useful mainly for aborting processes part way through in order to test error handling and recovery.
 197    /// Leaves any time-delayed cancellation in place.  If the underlying token source has been canceled, a new not-yet-
 198    /// </summary>
 199    /// <param name="numberOfChecks">The number of checks to cancel after.</param>
 200    public void CancelAfterChecks(int numberOfChecks)
 201    {
 202        // already canceled?  create a new underlying cancellation source
 203        if (_tokenSource?.IsCancellationRequested == true) _tokenSource = new();
 204        Interlocked.Exchange(ref _checks, 0);
 205        Interlocked.Exchange(ref _cancelAfterChecks, numberOfChecks);
 206    }
 207
 208    #region IDisposable Support
 209    /// <summary>
 210    /// Implementation of the standard dispose pattern.
 211    /// </summary>
 212    /// <param name="disposing">Whether or not this instance is being disposed, as opposed to finalized.</param>
 213    protected virtual void Dispose(bool disposing)
 214    {
 215        if (disposing)
 216        {
 217            _tokenSource?.Dispose();
 218            _tokenSource = null;
 219            _ambientTimer?.Dispose();
 220            _ambientTimer = null;
 221        }
 222    }
 223    /// <summary>
 224    /// Disposes of this instance.
 225    /// </summary>
 226    public void Dispose()
 227    {
 228        Dispose(true);
 229        GC.SuppressFinalize(this);
 230    }
 231    #endregion
 232}
 233[AttributeUsage(AttributeTargets.All)]
 234internal sealed class ExcludeFromCoverageAttribute : Attribute
 235{
 236}