private string _errorText;
private int _maxRow = -1;
- private readonly bool _useAnsi;
private int _consoleHeight = -1;
private int _consoleWidth = -1;
+ private IConsole _console;
- public ConsoleWriter(bool useAnsi)
+ public ConsoleWriter(IConsole console)
{
- _useAnsi = useAnsi;
+ _console = console;
}
public void Initialize()
AssignRowsAndInitializeDisplay();
}
- private void SetCursorPosition(int col, int row)
- {
- if (_useAnsi)
- {
- Console.Write($"\u001b[{row + 1 - _topRow};{col + 1}H");
- }
- else
- {
- Console.SetCursorPosition(col, row);
- }
- }
-
- private void Clear()
- {
- if (_useAnsi)
- {
- Console.Write($"\u001b[H\u001b[J");
- }
- else
- {
- Console.Clear();
- }
- }
private void UpdateStatus()
{
- SetCursorPosition(0, _statusRow);
- Console.Write($" Status: {GetStatus()}{new string(' ', 40)}"); // Write enough blanks to clear previous status.
+ _console.SetCursorPosition(0, _statusRow);
+ _console.Write($" Status: {GetStatus()}{new string(' ', 40)}"); // Write enough blanks to clear previous status.
}
private string GetStatus() => !_initialized ? "Waiting for initial payload..." : (_paused ? "Paused" : "Running");
/// <summary>Clears display and writes out category and counter name layout.</summary>
public void AssignRowsAndInitializeDisplay()
{
- Clear();
+ _console.Clear();
// clear row data on all counters
foreach (ObservedProvider provider in _providers.Values)
}
}
- _consoleWidth = Console.WindowWidth;
- _consoleHeight = Console.WindowHeight;
+ _consoleWidth = _console.WindowWidth;
+ _consoleHeight = _console.WindowHeight;
_maxNameLength = Math.Max(Math.Min(80, _consoleWidth) - (CounterValueLength + Indent + 1), 0); // Truncate the name to prevent line wrapping as long as the console width is >= CounterValueLength + Indent + 1 characters
- int row = Console.CursorTop;
+ int row = _console.CursorTop;
_topRow = row;
string instructions = "Press p to pause, r to resume, q to quit.";
- Console.WriteLine((instructions.Length < _consoleWidth) ? instructions : instructions.Substring(0, _consoleWidth)); row++;
- Console.WriteLine($" Status: {GetStatus()}"); _statusRow = row++;
+ _console.WriteLine((instructions.Length < _consoleWidth) ? instructions : instructions.Substring(0, _consoleWidth)); row++;
+ _console.WriteLine($" Status: {GetStatus()}"); _statusRow = row++;
if (_errorText != null)
{
- Console.WriteLine(_errorText);
+ _console.WriteLine(_errorText);
row += GetLineWrappedLines(_errorText);
}
if (lineOutput != null)
{
- Console.Write(lineOutput);
+ _console.Write(lineOutput);
}
if (row < _consoleHeight + _topRow - 1) // prevents screen from scrolling due to newline on last line of console
{
- Console.WriteLine();
+ _console.WriteLine();
}
if (counterRow != null)
redraw = true;
}
- if (Console.WindowWidth != _consoleWidth || Console.WindowHeight != _consoleHeight)
+ if (_console.WindowWidth != _consoleWidth || _console.WindowHeight != _consoleHeight)
{
redraw = true;
}
{
return;
}
- SetCursorPosition(Indent + _maxNameLength + 1, row);
- Console.Write(FormatValue(payload.Value));
+ _console.SetCursorPosition(Indent + _maxNameLength + 1, row);
+ _console.Write(FormatValue(payload.Value));
}
}
}
}
- private static int GetLineWrappedLines(string text)
+ private int GetLineWrappedLines(string text)
{
string[] lines = text.Split(Environment.NewLine);
int lineCount = lines.Length;
- int width = Console.BufferWidth;
+ int width = _console.BufferWidth;
foreach (string line in lines)
{
lineCount += (int)Math.Floor(((float)line.Length) / width);
if (row > -1)
{
- SetCursorPosition(0, row);
- Console.WriteLine();
+ _console.SetCursorPosition(0, row);
+ _console.WriteLine();
}
}
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Diagnostics.Monitoring.EventPipe;
+using Microsoft.Diagnostics.Tools.Counters.Exporters;
+using Xunit;
+
+namespace DotnetCounters.UnitTests
+{
+ public class ConsoleExporterTests
+ {
+ [Fact]
+ public void DisplayWaitingMessage()
+ {
+ MockConsole console = new MockConsole(60, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Waiting for initial payload...");
+ }
+
+ [Fact]
+ public void DisplayEventCounter()
+ {
+ MockConsole console = new MockConsole(60, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 12), false);
+
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 12");
+ }
+
+ [Fact]
+ public void DisplayIncrementingEventCounter()
+ {
+ MockConsole console = new MockConsole(60, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false);
+
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " Allocation Rate (B / 1 sec) 1,731");
+ }
+
+ [Fact]
+ public void DisplayMultipleProviders()
+ {
+ MockConsole console = new MockConsole(60, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false);
+ exporter.CounterPayloadReceived(CreateEventCounter("Provider2", "CounterXyz", "Doodads", 0.076), false);
+
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " Allocation Rate (B / 1 sec) 1,731",
+ "[Provider2]",
+ " CounterXyz (Doodads) 0.076");
+ }
+
+ [Fact]
+ public void UpdateCounters()
+ {
+ MockConsole console = new MockConsole(60, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ // update 1
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 12), false);
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 12",
+ " Allocation Rate (B / 1 sec) 1,731");
+
+ // update 2
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 7), false);
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 123456), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 7",
+ " Allocation Rate (B / 1 sec) 123,456");
+ }
+
+ [Fact]
+ public void PauseAndUnpause()
+ {
+ MockConsole console = new MockConsole(60, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ // update 1
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 12), false);
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 12",
+ " Allocation Rate (B / 1 sec) 1,731");
+
+ // pause
+ exporter.ToggleStatus(true);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Paused",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 12",
+ " Allocation Rate (B / 1 sec) 1,731");
+
+ // update 2, still paused
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 7), true);
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 123456), true);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Paused",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 12",
+ " Allocation Rate (B / 1 sec) 1,731");
+
+ // unpause doesn't automatically update values (maybe it should??)
+ exporter.ToggleStatus(false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 12",
+ " Allocation Rate (B / 1 sec) 1,731");
+
+
+ // update 3 will change the values
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 1), false);
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 2), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 1",
+ " Allocation Rate (B / 1 sec) 2");
+ }
+
+ [Fact]
+ public void AlignValues()
+ {
+ MockConsole console = new MockConsole(60, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 0.1), false);
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false);
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "BigCounter", "nanoseconds", 602341234567890123.0), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 0.1",
+ " Allocation Rate (B / 1 sec) 1,731",
+ " BigCounter (nanoseconds) 6.0234e+17");
+ }
+
+ [Fact]
+ public void NameColumnWidthAdjusts()
+ {
+ MockConsole console = new MockConsole(50, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "% Time in GC since last GC", "%", 0.1), false);
+ exporter.CounterPayloadReceived(CreateIncrementingEventCounter("System.Runtime", "Allocation Rate", "B", 1731), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " % Time in GC since last GC (%) 0.1",
+ " Allocation Rate (B / 1 sec) 1,731");
+ }
+
+ [Fact]
+ public void LongNamesAreTruncated()
+ {
+ MockConsole console = new MockConsole(50, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ exporter.CounterPayloadReceived(CreateEventCounter("System.Runtime", "ThisCounterHasAVeryLongNameThatDoesNotFit", "%", 0.1), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[System.Runtime]",
+ " ThisCounterHasAVeryLongNameTha 0.1");
+ }
+
+ [Fact]
+ public void MultiDimensionalCountersAreListed()
+ {
+ MockConsole console = new MockConsole(50, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue", 87), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[Provider1]",
+ " Counter1 ({widget} / 1 sec)",
+ " color=blue 87",
+ " color=red 0.1",
+ " Counter2 ({widget} / 1 sec)",
+ " size=1 14",
+ " temp=hot 160");
+ }
+
+ [Fact]
+ public void LongMultidimensionalTagsAreTruncated()
+ {
+ MockConsole console = new MockConsole(50, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue,LongNameTag=ThisDoesNotFit,AnotherOne=Hi", 87), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[Provider1]",
+ " Counter1 ({widget} / 1 sec)",
+ " color=blue,LongNameTag=Thi 87",
+ " color=red 0.1",
+ " Counter2 ({widget} / 1 sec)",
+ " size=1 14",
+ " temp=hot 160");
+ }
+
+ [Fact]
+ public void CountersAreTruncatedBeyondScreenHeight()
+ {
+ MockConsole console = new MockConsole(50, 6);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue", 87), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false);
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "",
+ "[Provider1]",
+ " Counter1 ({widget} / 1 sec)",
+ " color=blue 87");
+ }
+
+ [Fact]
+ public void ErrorStatusIsDisplayed()
+ {
+ MockConsole console = new MockConsole(50, 40);
+ ConsoleWriter exporter = new ConsoleWriter(console);
+ exporter.Initialize();
+
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=red", 0.1), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter1", "{widget}", "color=blue", 87), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "size=1", 14), false);
+ exporter.CounterPayloadReceived(CreateMeterCounter("Provider1", "Counter2", "{widget}", "temp=hot", 160), false);
+ exporter.SetErrorText("Uh-oh, a bad thing happened");
+
+ console.AssertLinesEqual("Press p to pause, r to resume, q to quit.",
+ " Status: Running",
+ "Uh-oh, a bad thing happened",
+ "",
+ "[Provider1]",
+ " Counter1 ({widget} / 1 sec)",
+ " color=blue 87",
+ " color=red 0.1",
+ " Counter2 ({widget} / 1 sec)",
+ " size=1 14",
+ " temp=hot 160");
+ }
+
+
+
+ private static CounterPayload CreateEventCounter(string provider, string displayName, string unit, double value)
+ {
+ return new EventCounterPayload(DateTime.MinValue, provider, displayName, displayName, unit, value, CounterType.Metric, 0, 0, "");
+ }
+
+ private static CounterPayload CreateIncrementingEventCounter(string provider, string displayName, string unit, double value)
+ {
+ return new EventCounterPayload(DateTime.MinValue, provider, displayName, displayName, unit, value, CounterType.Rate, 0, 1, "");
+ }
+
+ private static CounterPayload CreateMeterCounter(string meterName, string instrumentName, string unit, string tags, double value)
+ {
+ return new RatePayload(meterName, instrumentName, instrumentName, unit, tags, value, 1, DateTime.MinValue);
+ }
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using Microsoft.Diagnostics.Tools.Counters.Exporters;
+using Xunit;
+
+namespace DotnetCounters.UnitTests
+{
+ internal class MockConsole : IConsole
+ {
+ char[][] _chars;
+
+ int _cursorLeft;
+
+ public MockConsole(int width, int height)
+ {
+ WindowWidth = BufferWidth = width;
+ WindowHeight = height;
+ Clear();
+ }
+ public int WindowHeight { get; init; }
+
+ public int WindowWidth { get; init; }
+
+ public bool CursorVisible { get => throw new NotSupportedException(); set => throw new NotImplementedException(); }
+
+ public int CursorTop { get; private set; }
+
+ public int BufferWidth { get; private set; }
+
+ public void Clear()
+ {
+ _chars = new char[WindowHeight][];
+ for(int i = 0; i < WindowHeight; i++)
+ {
+ _chars[i] = new char[WindowWidth];
+ for(int j = 0; j < WindowWidth; j++)
+ {
+ _chars[i][j] = ' ';
+ }
+ }
+ CursorTop = 0;
+ _cursorLeft = 0;
+ }
+ public void SetCursorPosition(int col, int row)
+ {
+ CursorTop = row;
+ _cursorLeft = col;
+ }
+ public void Write(string text)
+ {
+ for(int textPos = 0; textPos < text.Length; )
+ {
+ // This attempts to mirror the behavior of System.Console
+ // if the console width is X then it is possible to write X characters and still have the console
+ // report you are on the same line. If the X+1'th character isn't a newline then the console automatically
+ // wraps and writes that character at the beginning of the next line leaving the cursor at index 1. If
+ // the X+1'th character is a newline then the cursor moves to the next line at index 0.
+ Debug.Assert(_cursorLeft <= WindowWidth);
+ if(text.AsSpan(textPos).StartsWith(Environment.NewLine))
+ {
+ textPos += Environment.NewLine.Length;
+ _cursorLeft = 0;
+ CursorTop++;
+ }
+ else
+ {
+ if (_cursorLeft == WindowWidth)
+ {
+ _cursorLeft = 0;
+ CursorTop++;
+ }
+ // make sure we are writing inside the legal buffer area, if not we'll hit the exception below
+ if (CursorTop < WindowHeight)
+ {
+ _chars[CursorTop][_cursorLeft] = text[textPos];
+ textPos++;
+ _cursorLeft++;
+ }
+ }
+ if (CursorTop >= WindowHeight)
+ {
+ // For now we assume that no test case intentionally scrolls the buffer. If we want to have tests that
+ // scroll the buffer by design then update this implementation.
+ throw new Exception("Writing beyond the end of the console buffer would have caused text to scroll.");
+ }
+ }
+ }
+ public void WriteLine(string text)
+ {
+ Write(text);
+ Write(Environment.NewLine);
+ }
+ public void WriteLine() => Write(Environment.NewLine);
+
+ public string GetLineText(int row) => new string(_chars[row]).TrimEnd();
+
+ public string[] Lines
+ {
+ get
+ {
+ string[] lines = new string[WindowHeight];
+ for(int i = 0; i < WindowHeight; i++)
+ {
+ lines[i] = GetLineText(i);
+ }
+ return lines;
+ }
+ }
+
+ public void AssertLinesEqual(params string[] expectedLines) => AssertLinesEqual(0, expectedLines);
+
+ public void AssertLinesEqual(int startLine, params string[] expectedLines)
+ {
+ for(int i = 0; i < expectedLines.Length; i++)
+ {
+ string actualLine = GetLineText(startLine+i);
+ string expectedLine = expectedLines[i];
+ if(actualLine != expectedLine)
+ {
+ Assert.Fail("MockConsole output did not match expected output." + Environment.NewLine +
+ $"Expected line {startLine + i,2}: {expectedLine}" + Environment.NewLine +
+ $"Actual line : {actualLine}");
+ }
+
+ }
+ }
+ }
+}