File: Internal\ProgressIndicator.cs
Web Access
Project: src\src\vstest\src\vstest.console\vstest.console.csproj (vstest.console)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Globalization;

using Microsoft.VisualStudio.TestPlatform.Utilities;

namespace Microsoft.VisualStudio.TestPlatform.CommandLine.Internal;

/// <summary>
/// Indicates the test run progress
/// </summary>
internal sealed class ProgressIndicator : IProgressIndicator, IDisposable
{
    private readonly object _syncObject = new();
    private readonly ISystemTimersTimer _timer;
    private readonly string _testRunProgressString;

    private int _dotCounter;

    /// <summary>
    /// Used to output to the console
    /// </summary>
    public IOutput ConsoleOutput { get; private set; }

    /// <summary>
    /// Wrapper over the System Console class
    /// </summary>
    public IConsoleHelper ConsoleHelper { get; private set; }

    /// <summary>
    /// True is the indicator is to be displayed
    /// </summary>
    public bool IsRunning { get; private set; }

    public ProgressIndicator(IOutput output, IConsoleHelper consoleHelper, ISystemTimersTimer? timer = null)
    {
        _timer = timer ?? new SystemTimersTimer(1000);
        ConsoleOutput = output;
        ConsoleHelper = consoleHelper;
        _testRunProgressString = string.Format(CultureInfo.CurrentCulture, "{0}...", Resources.Resources.ProgressIndicatorString);
    }

    /// <inheritdoc />
    public void Start()
    {
        lock (_syncObject)
        {
            _timer.Elapsed += Timer_Elapsed;
            _timer.Start();

            // Print the string based on the previous state, that is dotCounter
            // This is required for smooth transition
            ConsoleOutput.Write(_testRunProgressString.Substring(0, _testRunProgressString.Length + _dotCounter - 2), OutputLevel.Information);
            IsRunning = true;
        }
    }

    /// <summary>
    /// Get the current cursor position
    /// Clear the console starting given position
    /// Reset the cursor position back
    /// </summary>
    /// <param name="startPos">the starting position</param>
    private void Clear(int startPos)
    {
        var currentLineCursor = ConsoleHelper.CursorTop;
        ConsoleHelper.SetCursorPosition(startPos, ConsoleHelper.CursorTop);
        ConsoleOutput.Write(new string(' ', ConsoleHelper.WindowWidth - startPos), OutputLevel.Information);
        ConsoleHelper.SetCursorPosition(startPos, currentLineCursor);
    }

    /// <summary>
    /// Sets the isRunning flag to false so that indicator is not shown.
    /// </summary>
    public void Pause()
    {
        lock (_syncObject)
        {
            IsRunning = false;
            Clear(0);
        }
    }

    /// <summary>
    /// Stops the indicator and clears the current line.
    /// </summary>
    public void Stop()
    {
        lock (_syncObject)
        {
            IsRunning = false;
            _timer?.Stop();
            Clear(0);
        }
    }

    private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
    {
        if (IsRunning)
        {
            // If running, prints dot every second.
            ConsoleOutput.Write(".", OutputLevel.Information);
            _dotCounter = ++_dotCounter % 3;

            // When counter reaches 3, that is 3 dots have been printed
            // Clear and start printing again
            if (_dotCounter == 0)
            {
                Clear(ConsoleHelper.CursorLeft - 3);
            }
        }
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}