< Summary

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
get_Progress()100%66100%
PushSubProgress(...)100%11100%
PopSubProgress(...)75%1616100%
Pop(...)50%22100%

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
 222    public BasicAmbientProgress()
 23    {
 224        _progress = new AsyncLocal<Progress?>();
 225    }
 26
 27    public IAmbientProgress? Progress
 28    {
 29        get
 30        {
 231            IAmbientProgress? ambientProgress = _progress.Value;
 232            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
 234            if (ambientProgress == null || (topProgress?.Disposed ?? false))
 35            {
 236                topProgress = new Progress(this, null, 0.0f, 1.0f, null, false);
 237                _progress.Value = topProgress;
 238                ambientProgress = topProgress;
 39            }
 40            else
 41            {
 242                ambientProgress = _progress.Value;
 43            }
 244            return ambientProgress;
 45        }
 46    }
 47
 48    public void PushSubProgress(Progress subProgress)
 49    {
 250        _progress.Value = subProgress;
 251    }
 52    public void PopSubProgress(Progress specified)
 53    {
 254        Progress? expected = _progress.Value;
 255        Progress? pop = expected;
 56        // are the specified progress and the one at the top of the stack *not* the same?
 257        if (expected != specified)
 58        {
 59            // walk up from both the expected progress and the specified progress to try to find the others
 260            Progress? tryFindExpected = Progress as Progress;
 261            Progress? specifiedPopperAncestor = specified;
 262            Progress? expectedPopperAncestor = tryFindExpected;
 263            while (specifiedPopperAncestor != null || expectedPopperAncestor != null)
 64            {
 265                specifiedPopperAncestor = specifiedPopperAncestor?.Parent as Progress;
 266                expectedPopperAncestor = expectedPopperAncestor?.Parent as Progress;
 67                // did we find the expected progress up the chain from the specified one (popping the specified progress
 268                if (specifiedPopperAncestor == tryFindExpected)
 69                {
 70                    // we've *already* popped an ancestor of the specified progress, so there's nothing left to pop off 
 271                    pop = null;
 72                    // if we add dispose code below, we could also check to see if things are disposed here
 273                    break;
 74                }
 75                // did we find the specified progress up the stack from the expected progress (popping the specified pro
 276                else if (expectedPopperAncestor == specified)
 77                {
 78                    // pop everything up to the specified progress
 279                    pop = specified;
 80                    // if there were anything to do other than the stack and the progress update (which isn't needed bec
 281                    break;
 82                }
 83                // else just keep walking up the stack
 84            }
 85            // pop all the way up to the specified item (if needed)
 286            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
 288            throw new InvalidOperationException("The SubProgress object stack is corrupt!");
 89        }
 290        Pop(pop);
 291    }
 92    private void Pop(Progress? subProgress)
 93    {
 294        IAmbientProgress? parent = subProgress?.Parent;
 295        _progress.Value = parent as Progress;
 296    }
 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)
 122         : this (progress, null, 0.0f, 1.0f, null, false)
 123    {
 124    }
 125    public Progress(BasicAmbientProgress progressService, IAmbientProgress? parentProgress, float startPortion, float po
 126    {
 127        _tracker = progressService;
 128        _prefix = prefix ?? "";
 129        _currentItem = "";
 130        if (startPortion < 0.0 || startPortion > 1.0) throw new ArgumentOutOfRangeException(nameof(startPortion), "The s
 131        if (portionPart < 0.0 || portionPart > 1.0) throw new ArgumentOutOfRangeException(nameof(portionPart), "The port
 132        if (startPortion + portionPart > 1.0) throw new ArgumentOutOfRangeException(nameof(portionPart), "The sum of the
 133        Parent = parentProgress;
 134        _startPortion = startPortion;
 135        _portionPart = portionPart;
 136        if (_inheritedCancelSource = inheritCancellationSource) // note that this is an ASSIGNMENT in addition to a test
 137        {
 138            AmbientCancellationTokenSource? parentCancellationSource = parentProgress?.CancellationTokenSource;
 139            if (parentCancellationSource != null)
 140            {
 141                CancellationTokenSource = parentCancellationSource;
 142                _ownCancelSource = false;       // this was true, but I believe false is correct because the parent prog
 143            }
 144            else
 145            {
 146                CancellationTokenSource = new AmbientCancellationTokenSource();
 147                _ownCancelSource = true;
 148            }
 149        }
 150        else
 151        {
 152            CancellationTokenSource = new AmbientCancellationTokenSource();
 153            _ownCancelSource = true;
 154        }
 155        progressService.PushSubProgress(this);
 156    }
 157
 158    public void ResetCancellation(TimeSpan timeout)
 159    {
 160        AmbientCancellationTokenSource cancelSource = new(timeout);
 161        // dispose of any previously-held cancellation token source and swap in the new one
 162        if (_ownCancelSource) CancellationTokenSource.Dispose();
 163        _inheritedCancelSource = false;
 164        _ownCancelSource = true;
 165        CancellationTokenSource = cancelSource;
 166    }
 167    public void ResetCancellation(CancellationTokenSource? cancellationTokenSource = null)
 168    {
 169        AmbientCancellationTokenSource? cancelSource = (cancellationTokenSource == null) ? new AmbientCancellationTokenS
 170        // dispose of any previously-held cancellation token source and swap in the new one
 171        if (_ownCancelSource) CancellationTokenSource.Dispose();
 172        _inheritedCancelSource = false;
 173        _ownCancelSource = true;
 174        CancellationTokenSource = cancelSource;
 175    }
 176    public void ThrowIfCancelled()
 177    {
 178        CancellationTokenSource.Token.ThrowIfCancellationRequested();
 179    }
 180    public CancellationToken CancellationToken => CancellationTokenSource.Token;
 181    public AmbientCancellationTokenSource CancellationTokenSource { get; private set; }
 182    public float PortionComplete => _portionComplete;
 183    public string ItemCurrentlyBeingProcessed => _currentItem;
 184    public void Update(float portionComplete, string? itemCurrentlyBeingProcessed = null)
 185    {
 186        if (portionComplete < 0.0 || portionComplete > 1.0) throw new ArgumentOutOfRangeException(nameof(portionComplete
 187        Interlocked.Exchange(ref _portionComplete, portionComplete);
 188        if (itemCurrentlyBeingProcessed != null) Interlocked.Exchange(ref _currentItem, itemCurrentlyBeingProcessed);
 189        // have we been canceled?
 190        if ((!_inheritedCancelSource || Parent == null) && CancellationTokenSource.IsCancellationRequested) Cancellation
 191        Parent?.Update(_startPortion + _portionPart * portionComplete, itemCurrentlyBeingProcessed == null ? null : _pre
 192    }
 193    public IDisposable TrackPart(float startPortion, float portionPart, string? prefix = null, bool inheritCancellationT
 194    {
 195        string partPrefix = _prefix + prefix;
 196        Progress ret = new(_tracker, this, startPortion, portionPart, partPrefix, inheritCancellationTokenSource);
 197        ret.Update(0.0f);
 198        return ret;
 199    }
 200    public void Dispose()
 201    {
 202        if (!Disposed)
 203        {
 204            try
 205            {
 206                // make sure the parent knows we're done, but prevent throwing a cancellation exception during disposal
 207                Update(1.0f);
 208            }
 209            catch (OperationCanceledException) { }  // ignore these in Dispose!
 210            _tracker.PopSubProgress(this);
 211            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
 214            if (_ownCancelSource) CancellationTokenSource.Dispose();   // note that this will cancel any associated toke
 215        }
 216    }
 217    internal bool Disposed { get; private set; }
 218    internal IAmbientProgress? Parent { get; }
 219}