< Summary

Information
Class: AmbientServices.Progress
Assembly: AmbientServices
File(s): /home/runner/work/AmbientServices/AmbientServices/AmbientServices/DefaultImplementation/BasicAmbientProgress.cs
Tag: 332_35464845198
Line coverage
100%
Covered lines: 58
Uncovered lines: 0
Coverable lines: 58
Total lines: 219
Line coverage: 100%
Branch coverage
100%
Covered branches: 44
Total branches: 44
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.ctor(...)100%1818100%
ResetCancellation(...)100%22100%
ResetCancellation(...)100%44100%
ThrowIfCancelled()100%11100%
get_CancellationToken()100%11100%
get_PortionComplete()100%11100%
get_ItemCurrentlyBeingProcessed()100%11100%
Update(...)100%1616100%
TrackPart(...)100%11100%
Dispose()100%44100%

File(s)

/home/runner/work/AmbientServices/AmbientServices/AmbientServices/DefaultImplementation/BasicAmbientProgress.cs

#LineLine coverage
 1using System;
 2using System.Threading;
 3
 4namespace AmbientServices;
 5
 6/// <summary>
 7/// A basic default implementation of <see cref="IAmbientProgressService"/> that tracks a stack of progress scopes for e
 8/// </summary>
 9/// <remarks>
 10/// <pitch>The zero-configuration, in-process progress service used unless overridden.  Progress state lives entirely in
 11/// <pledge><see cref="IAmbientProgressService"/></pledge>
 12/// <pledge>Out-of-order disposal of part scopes is detected rather than silently tolerated: the service makes a best-ef
 13/// <plan>
 14/// Holds the innermost <see cref="AmbientServices.Progress"/> for each execution context in an <see cref="AsyncLocal{T}
 15/// </plan>
 16/// </remarks>
 17[DefaultAmbientService]
 18internal class BasicAmbientProgress : IAmbientProgressService
 19{
 20    private readonly AsyncLocal<Progress?> _progress;
 21
 22    public BasicAmbientProgress()
 23    {
 24        _progress = new AsyncLocal<Progress?>();
 25    }
 26
 27    public IAmbientProgress? Progress
 28    {
 29        get
 30        {
 31            IAmbientProgress? ambientProgress = _progress.Value;
 32            Progress? topProgress = ambientProgress as Progress;
 33            // no progress in this context yet, or the context has been disposed (the docs say not to do that, but we ha
 34            if (ambientProgress == null || (topProgress?.Disposed ?? false))
 35            {
 36                topProgress = new Progress(this, null, 0.0f, 1.0f, null, false);
 37                _progress.Value = topProgress;
 38                ambientProgress = topProgress;
 39            }
 40            else
 41            {
 42                ambientProgress = _progress.Value;
 43            }
 44            return ambientProgress;
 45        }
 46    }
 47
 48    public void PushSubProgress(Progress subProgress)
 49    {
 50        _progress.Value = subProgress;
 51    }
 52    public void PopSubProgress(Progress specified)
 53    {
 54        Progress? expected = _progress.Value;
 55        Progress? pop = expected;
 56        // are the specified progress and the one at the top of the stack *not* the same?
 57        if (expected != specified)
 58        {
 59            // walk up from both the expected progress and the specified progress to try to find the others
 60            Progress? tryFindExpected = Progress as Progress;
 61            Progress? specifiedPopperAncestor = specified;
 62            Progress? expectedPopperAncestor = tryFindExpected;
 63            while (specifiedPopperAncestor != null || expectedPopperAncestor != null)
 64            {
 65                specifiedPopperAncestor = specifiedPopperAncestor?.Parent as Progress;
 66                expectedPopperAncestor = expectedPopperAncestor?.Parent as Progress;
 67                // did we find the expected progress up the chain from the specified one (popping the specified progress
 68                if (specifiedPopperAncestor == tryFindExpected)
 69                {
 70                    // we've *already* popped an ancestor of the specified progress, so there's nothing left to pop off 
 71                    pop = null;
 72                    // if we add dispose code below, we could also check to see if things are disposed here
 73                    break;
 74                }
 75                // did we find the specified progress up the stack from the expected progress (popping the specified pro
 76                else if (expectedPopperAncestor == specified)
 77                {
 78                    // pop everything up to the specified progress
 79                    pop = specified;
 80                    // if there were anything to do other than the stack and the progress update (which isn't needed bec
 81                    break;
 82                }
 83                // else just keep walking up the stack
 84            }
 85            // pop all the way up to the specified item (if needed)
 86            if (pop != null) Pop(pop);
 87            // the best we can do at this point is to just pop like normal and hope that the correct number of pops occu
 88            throw new InvalidOperationException("The SubProgress object stack is corrupt!");
 89        }
 90        Pop(pop);
 91    }
 92    private void Pop(Progress? subProgress)
 93    {
 94        IAmbientProgress? parent = subProgress?.Parent;
 95        _progress.Value = parent as Progress;
 96    }
 97}
 98
 99/// <summary>
 100/// The <see cref="IAmbientProgress"/> realization used by <see cref="BasicAmbientProgress"/>: one node in the per-execu
 101/// </summary>
 102/// <remarks>
 103/// <pitch>Tracks one part of an operation — its portion complete, the item being processed, and its cancellation — prop
 104/// <pledge><see cref="IAmbientProgress"/></pledge>
 105/// <pledge>Also <see cref="IDisposable"/>: disposal reports the part complete (1.0) to the parent — swallowing any pend
 106/// <plan>
 107/// Stores the parent, the start portion and portion span it occupies within the parent, and an item-name prefix, all fi
 108/// </plan>
 109/// </remarks>
 110internal class Progress : IAmbientProgress, IDisposable
 111{
 112    private readonly BasicAmbientProgress _tracker;
 113    private readonly string _prefix;
 114    private bool _inheritedCancelSource;                        // if we inherited the cancel source, there is no need t
 115    private bool _ownCancelSource;
 116    private readonly float _startPortion;
 117    private readonly float _portionPart;
 118    private float _portionComplete;
 119    private string _currentItem;
 120
 121    public Progress(BasicAmbientProgress progress)
 2122         : this (progress, null, 0.0f, 1.0f, null, false)
 123    {
 2124    }
 2125    public Progress(BasicAmbientProgress progressService, IAmbientProgress? parentProgress, float startPortion, float po
 126    {
 2127        _tracker = progressService;
 2128        _prefix = prefix ?? "";
 2129        _currentItem = "";
 2130        if (startPortion < 0.0 || startPortion > 1.0) throw new ArgumentOutOfRangeException(nameof(startPortion), "The s
 2131        if (portionPart < 0.0 || portionPart > 1.0) throw new ArgumentOutOfRangeException(nameof(portionPart), "The port
 2132        if (startPortion + portionPart > 1.0) throw new ArgumentOutOfRangeException(nameof(portionPart), "The sum of the
 2133        Parent = parentProgress;
 2134        _startPortion = startPortion;
 2135        _portionPart = portionPart;
 2136        if (_inheritedCancelSource = inheritCancellationSource) // note that this is an ASSIGNMENT in addition to a test
 137        {
 2138            AmbientCancellationTokenSource? parentCancellationSource = parentProgress?.CancellationTokenSource;
 2139            if (parentCancellationSource != null)
 140            {
 2141                CancellationTokenSource = parentCancellationSource;
 2142                _ownCancelSource = false;       // this was true, but I believe false is correct because the parent prog
 143            }
 144            else
 145            {
 2146                CancellationTokenSource = new AmbientCancellationTokenSource();
 2147                _ownCancelSource = true;
 148            }
 149        }
 150        else
 151        {
 2152            CancellationTokenSource = new AmbientCancellationTokenSource();
 2153            _ownCancelSource = true;
 154        }
 2155        progressService.PushSubProgress(this);
 2156    }
 157
 158    public void ResetCancellation(TimeSpan timeout)
 159    {
 2160        AmbientCancellationTokenSource cancelSource = new(timeout);
 161        // dispose of any previously-held cancellation token source and swap in the new one
 2162        if (_ownCancelSource) CancellationTokenSource.Dispose();
 2163        _inheritedCancelSource = false;
 2164        _ownCancelSource = true;
 2165        CancellationTokenSource = cancelSource;
 2166    }
 167    public void ResetCancellation(CancellationTokenSource? cancellationTokenSource = null)
 168    {
 2169        AmbientCancellationTokenSource? cancelSource = (cancellationTokenSource == null) ? new AmbientCancellationTokenS
 170        // dispose of any previously-held cancellation token source and swap in the new one
 2171        if (_ownCancelSource) CancellationTokenSource.Dispose();
 2172        _inheritedCancelSource = false;
 2173        _ownCancelSource = true;
 2174        CancellationTokenSource = cancelSource;
 2175    }
 176    public void ThrowIfCancelled()
 177    {
 2178        CancellationTokenSource.Token.ThrowIfCancellationRequested();
 2179    }
 2180    public CancellationToken CancellationToken => CancellationTokenSource.Token;
 181    public AmbientCancellationTokenSource CancellationTokenSource { get; private set; }
 2182    public float PortionComplete => _portionComplete;
 2183    public string ItemCurrentlyBeingProcessed => _currentItem;
 184    public void Update(float portionComplete, string? itemCurrentlyBeingProcessed = null)
 185    {
 2186        if (portionComplete < 0.0 || portionComplete > 1.0) throw new ArgumentOutOfRangeException(nameof(portionComplete
 2187        Interlocked.Exchange(ref _portionComplete, portionComplete);
 2188        if (itemCurrentlyBeingProcessed != null) Interlocked.Exchange(ref _currentItem, itemCurrentlyBeingProcessed);
 189        // have we been canceled?
 2190        if ((!_inheritedCancelSource || Parent == null) && CancellationTokenSource.IsCancellationRequested) Cancellation
 2191        Parent?.Update(_startPortion + _portionPart * portionComplete, itemCurrentlyBeingProcessed == null ? null : _pre
 2192    }
 193    public IDisposable TrackPart(float startPortion, float portionPart, string? prefix = null, bool inheritCancellationT
 194    {
 2195        string partPrefix = _prefix + prefix;
 2196        Progress ret = new(_tracker, this, startPortion, portionPart, partPrefix, inheritCancellationTokenSource);
 2197        ret.Update(0.0f);
 2198        return ret;
 199    }
 200    public void Dispose()
 201    {
 2202        if (!Disposed)
 203        {
 204            try
 205            {
 206                // make sure the parent knows we're done, but prevent throwing a cancellation exception during disposal
 2207                Update(1.0f);
 2208            }
 2209            catch (OperationCanceledException) { }  // ignore these in Dispose!
 2210            _tracker.PopSubProgress(this);
 2211            Disposed = true;   // mark that we're disposed to help us make some kind of attempt to recover from progress
 212
 213            // only dispose of the cancel source if we own it
 2214            if (_ownCancelSource) CancellationTokenSource.Dispose();   // note that this will cancel any associated toke
 215        }
 2216    }
 217    internal bool Disposed { get; private set; }
 218    internal IAmbientProgress? Parent { get; }
 219}