< Summary

Information
Class: AmbientServices.AmbientCancellationTokenSource
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/Helpers/ProgressHelpers.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 63
Uncovered lines: 0
Coverable lines: 63
Total lines: 236
Line coverage: 100%
Branch coverage
92%
Covered branches: 46
Total branches: 50
Branch coverage: 92%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
AlreadyCancelledToken()100%11100%
.ctor(...)100%22100%
.ctor(...)100%22100%
.ctor(...)100%22100%
.ctor(...)100%44100%
ValidatedDelay(...)100%44100%
ValidatedDelay(...)100%11100%
ScheduleCancellation(...)83.33%66100%
handler()100%44100%
get_Token()100%22100%
get_IsCancellationRequested()87.5%88100%
get_Checks()100%11100%
Cancel()100%11100%
Cancel(...)100%22100%
CancelAfter(...)100%22100%
CancelAfter(...)50%22100%
CancelAfterChecks(...)75%44100%
Dispose(...)100%66100%
Dispose()100%11100%

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{
 16    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>
 20    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
 25    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{
 341    private static readonly AmbientService<IAmbientClock> _AmbientClock = Ambient.GetService<IAmbientClock>();
 342    private static readonly CancellationToken _AlreadyCancelled = AlreadyCancelledToken();
 43    private static CancellationToken AlreadyCancelledToken()
 44    {
 345        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
 260    public AmbientCancellationTokenSource(CancellationTokenSource? tokenSource = null)
 61    {
 262        _tokenSource = tokenSource ?? new CancellationTokenSource();
 263    }
 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)
 369        : this(_AmbientClock.Override ?? _AmbientClock.Local, ValidatedDelay(timeout, nameof(timeout)))
 70    {
 371    }
 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)
 277        : this(_AmbientClock.Override ?? _AmbientClock.Local, ValidatedDelay(timeoutMilliseconds, nameof(timeoutMillisec
 78    {
 279    }
 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
 385    public AmbientCancellationTokenSource(IAmbientClock? clock, TimeSpan? timeout = null)
 86    {
 387        if (timeout != null) ValidatedDelay(timeout.Value, nameof(timeout));
 388        _clock = clock;
 389        _tokenSource = new CancellationTokenSource();
 390        if (timeout != null)
 91        {
 392            ScheduleCancellation(timeout.Value);
 93        }
 394    }
 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    {
 3106        if (milliseconds < -1 || milliseconds > int.MaxValue) throw new ArgumentOutOfRangeException(parameterName);
 3107        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    {
 3116        return ValidatedDelay(delay.TotalMilliseconds, parameterName);
 117    }
 118
 119    private void ScheduleCancellation(TimeSpan delay)
 120    {
 3121        double milliseconds = delay.TotalMilliseconds;
 122        // never?  then there is nothing to schedule
 3123        if (milliseconds < 0) return;
 124        // already due?  then cancel right now, because a timer cannot be given a zero interval
 3125        if (milliseconds == 0)
 126        {
 2127            _tokenSource?.Cancel();
 2128            return;
 129        }
 3130        AmbientEventTimer timer = new AmbientEventTimer(delay);
 3131        _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.
 3135            timer.Elapsed -= handler;
 3136            _tokenSource?.Cancel();
 3137            timer.Dispose();
 3138            if (ReferenceEquals(_ambientTimer, timer))
 3139                _ambientTimer = null;
 3140        }
 141
 3142        timer.Elapsed += handler;   // note that the handler will keep the timer and the token source alive until the ev
 3143        timer.Enabled = true;
 3144    }
 145
 146    /// <summary>
 147    /// Gets the <see cref="CancellationToken"/> associated with the source.
 148    /// </summary>
 3149    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        {
 2157            if (_cancelAfterChecks != 0 && Interlocked.Increment(ref _checks) > _cancelAfterChecks) _tokenSource?.Cancel
 2158            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>
 2164    public int Checks => _checks;
 165    /// <summary>
 166    /// Marks the associated token as canceled.
 167    /// </summary>
 2168    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>
 2173    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    {
 2180        TimeSpan delay = ValidatedDelay(millisecondsDelay, nameof(millisecondsDelay));
 2181        if (_ambientTimer != null) _ambientTimer.Dispose();
 2182        ScheduleCancellation(delay);
 2183    }
 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    {
 2190        TimeSpan validatedDelay = ValidatedDelay(delay, nameof(delay));
 2191        if (_ambientTimer != null) _ambientTimer.Dispose();
 2192        ScheduleCancellation(validatedDelay);
 2193    }
 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
 2203        if (_tokenSource?.IsCancellationRequested == true) _tokenSource = new();
 2204        Interlocked.Exchange(ref _checks, 0);
 2205        Interlocked.Exchange(ref _cancelAfterChecks, numberOfChecks);
 2206    }
 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    {
 3215        if (disposing)
 216        {
 3217            _tokenSource?.Dispose();
 3218            _tokenSource = null;
 3219            _ambientTimer?.Dispose();
 3220            _ambientTimer = null;
 221        }
 3222    }
 223    /// <summary>
 224    /// Disposes of this instance.
 225    /// </summary>
 226    public void Dispose()
 227    {
 3228        Dispose(true);
 3229        GC.SuppressFinalize(this);
 3230    }
 231    #endregion
 232}
 233[AttributeUsage(AttributeTargets.All)]
 234internal sealed class ExcludeFromCoverageAttribute : Attribute
 235{
 236}