< Summary

Information
Class: MyNet.Utilities.Progress.Progresser<T>
Assembly: MyNet.Utilities
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Progress/Progresser.cs
Tag: 323_28699572109
Line coverage
100%
Covered lines: 47
Uncovered lines: 0
Coverable lines: 47
Total lines: 158
Line coverage: 100%
Branch coverage
94%
Covered branches: 17
Total branches: 18
Branch coverage: 94.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
Begin(...)100%11100%
Begin(...)100%11100%
Begin(...)100%11100%
StartStep(...)100%11100%
StartStep(...)100%11100%
StartStep(...)100%11100%
Subscribe(...)100%11100%
Unsubscribe(...)100%11100%
Push(...)100%22100%
Pop()100%22100%
Report()100%1010100%
EqualWeights(...)50%22100%
DiscardCurrentSession()100%11100%
CreateRootStep(...)100%11100%
GetCurrentStep()100%22100%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Utilities/Progress/Progresser.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="Progresser.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Concurrent;
 9using System.Collections.Generic;
 10using System.Diagnostics.CodeAnalysis;
 11using System.Linq;
 12
 13namespace MyNet.Utilities.Progress;
 14
 15/// <summary>
 16/// Concrete implementation of <see cref="IProgresser"/> that works with <see cref="ProgressMessage"/>.
 17/// String-based convenience overloads are available via <see cref="ProgresserExtensions"/>.
 18/// </summary>
 19public class Progresser : Progresser<ProgressMessage>, IProgresser;
 20
 21/// <summary>
 22/// Generic implementation of <see cref="IProgresser{T}"/>.
 23/// Manages a stack of active <see cref="ProgressStep{T}"/> instances and
 24/// notifies registered <see cref="IProgress{T}"/> subscribers on every state change.
 25/// </summary>
 26/// <typeparam name="T">The type of messages associated with progress steps.</typeparam>
 27public class Progresser<T> : IProgresser<T>
 28{
 29    // ── Fields ───────────────────────────────────────────────────────────────
 30
 31    /// <summary>Thread-safe set of progress subscribers (value is unused; the key is the subscriber).</summary>
 3332    private readonly ConcurrentDictionary<IProgress<ProgressReport<T>>, byte> _subscribers = new();
 33
 34    /// <summary>LIFO stack of active steps, from root (bottom) to innermost (top).</summary>
 3335    private readonly ConcurrentStack<ProgressStep<T>> _steps = new();
 36
 37    /// <summary>
 38    /// The root step of the current session. Stored separately so that
 39    /// <see cref="Report"/> can access it in O(1) without iterating the stack.
 40    /// Volatile to avoid stale reads across threads.
 41    /// </summary>
 42    private volatile ProgressStep<T>? _rootStep;
 43
 44    // ── IProgresser<T> — Begin ───────────────────────────────────────────────
 45
 46    /// <inheritdoc/>
 47    public IProgressStep<T> Begin(T message, Action? cancelAction = null)
 48    {
 2749        DiscardCurrentSession();
 2750        return CreateRootStep([], message, cancelAction);
 51    }
 52
 53    /// <inheritdoc/>
 54    public IProgressStep<T> Begin(int numberOfSteps, T message, Action? cancelAction = null)
 55    {
 1556        DiscardCurrentSession();
 1557        return CreateRootStep(EqualWeights(numberOfSteps), message, cancelAction);
 58    }
 59
 60    /// <inheritdoc/>
 61    public IProgressStep<T> Begin(IEnumerable<double> subStepWeightings, T message, Action? cancelAction = null)
 62    {
 1263        DiscardCurrentSession();
 1264        return CreateRootStep(subStepWeightings, message, cancelAction);
 65    }
 66
 67    // ── IProgresser<T> — StartStep ───────────────────────────────────────────
 68
 69    /// <inheritdoc/>
 70    public IProgressStep<T> StartStep(T message, bool canCancel = true)
 71    {
 2472        var parent = GetCurrentStep();
 1873        return new ProgressStep<T>(this, parent, parent.AllocateChildSlot(), message, [], null, canCancel);
 74    }
 75
 76    /// <inheritdoc/>
 77    public IProgressStep<T> StartStep(int numberOfSteps, T message, bool canCancel = true)
 78    {
 979        var parent = GetCurrentStep();
 980        return new ProgressStep<T>(this, parent, parent.AllocateChildSlot(), message, EqualWeights(numberOfSteps), null,
 81    }
 82
 83    /// <inheritdoc/>
 84    public IProgressStep<T> StartStep(IEnumerable<double> subStepWeightings, T message, bool canCancel = true)
 85    {
 986        var parent = GetCurrentStep();
 987        return new ProgressStep<T>(this, parent, parent.AllocateChildSlot(), message, subStepWeightings, null, canCancel
 88    }
 89
 90    // ── IProgresser<T> — Subscription ────────────────────────────────────────
 91
 92    /// <inheritdoc/>
 1293    public void Subscribe(IProgress<ProgressReport<T>> progress) => _subscribers.TryAdd(progress, 0);
 94
 95    /// <inheritdoc/>
 396    public void Unsubscribe(IProgress<ProgressReport<T>> progress) => _subscribers.TryRemove(progress, out _);
 97
 98    // ── Internal API (called by ProgressStep<T>) ─────────────────────────────
 99    [SuppressMessage("ReSharper", "NonAtomicCompoundOperator", Justification = "ConcurrentStack is thread-safe for Push/
 100    internal void Push(ProgressStep<T> step)
 101    {
 90102        _steps.Push(step);
 103
 104        // The first step pushed after DiscardCurrentSession() becomes the root.
 90105        _rootStep ??= step;
 90106    }
 107
 108    internal void Pop()
 109    {
 90110        _steps.TryPop(out _);
 90111        if (_steps.IsEmpty)
 54112            _rootStep = null;
 90113    }
 114
 115    internal void Report()
 116    {
 150117        var root = _rootStep;
 177118        if (root is null) return;
 119
 120        // Snapshot the stack to avoid races during enumeration.
 121        // ConcurrentStack enumerates top → bottom (LIFO), so Reverse() gives root → leaf.
 123122        var snapshot = _steps.ToArray();
 123123        var messages = snapshot
 123124            .Reverse()
 123125            .Where(s => s.Message is not null)
 123126            .Select(s => s.Message!)
 123127            .ToList();
 128
 123129        var canCancel = snapshot.All(s => s.CanCancel);
 123130        var report = new ProgressReport<T>(root.Progress, messages, root.CancelAction, canCancel);
 131
 306132        foreach (var subscriber in _subscribers.Keys)
 30133            subscriber.Report(report);
 123134    }
 135
 136    // ── Private helpers ───────────────────────────────────────────────────────
 137    private static IEnumerable<double> EqualWeights(int count)
 24138        => count <= 0
 24139            ? []
 24140            : Enumerable.Repeat(1.0 / count, count);
 141
 142    /// <summary>Clears any ongoing session without triggering dispose logic on individual steps.</summary>
 143    private void DiscardCurrentSession()
 144    {
 54145        _steps.Clear();
 54146        _rootStep = null;
 54147    }
 148
 149    private ProgressStep<T> CreateRootStep(IEnumerable<double> weightings, T message, Action? cancelAction) =>
 54150        new(this, parent: null, parentSlotIndex: -1, message, weightings, cancelAction, canCancel: true);
 151
 152    private ProgressStep<T> GetCurrentStep()
 42153        => _steps.TryPeek(out var top)
 42154            ? top
 42155            : throw new InvalidOperationException(
 42156                "No active progress session. Call Begin() before StartStep().");
 157}
 158