diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/System/IConsole.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/System/IConsole.cs index 6951af5697..3f4de402fd 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/System/IConsole.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/System/IConsole.cs @@ -26,6 +26,18 @@ internal interface IConsole [UnsupportedOSPlatform("tvos")] int BufferWidth { get; } + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + int WindowHeight { get; } + + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + int WindowWidth { get; } + bool IsOutputRedirected { get; } [UnsupportedOSPlatform("android")] diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/System/SystemConsole.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/System/SystemConsole.cs index 39500ecafa..f26bf08ef1 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/System/SystemConsole.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/System/SystemConsole.cs @@ -26,6 +26,24 @@ internal sealed class SystemConsole : IConsole [UnsupportedOSPlatform("tvos")] public int BufferWidth => Console.BufferWidth; + /// + /// Gets the height of the console window area. + /// + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public int WindowHeight => Console.WindowHeight; + + /// + /// Gets the width of the console window area. + /// + [UnsupportedOSPlatform("android")] + [UnsupportedOSPlatform("browser")] + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("tvos")] + public int WindowWidth => Console.WindowWidth; + /// /// Gets a value indicating whether output has been redirected from the standard output stream. /// diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs index 7460235e79..ee9587e09d 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/AnsiTerminal.cs @@ -56,12 +56,12 @@ public AnsiTerminal(IConsole console) public int Width => _console.IsOutputRedirected || OperatingSystem.IsBrowser() || OperatingSystem.IsAndroid() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() ? int.MaxValue - : _console.BufferWidth; + : _console.WindowWidth; public int Height => _console.IsOutputRedirected || OperatingSystem.IsBrowser() || OperatingSystem.IsAndroid() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() ? int.MaxValue - : _console.BufferHeight; + : _console.WindowHeight; public void Append(char value) { diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleTerminalBase.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleTerminalBase.cs index 5cf26f9dbb..a3a1214e8e 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleTerminalBase.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/SimpleTerminalBase.cs @@ -17,12 +17,12 @@ public SimpleTerminal(IConsole console) public int Width => Console.IsOutputRedirected || OperatingSystem.IsBrowser() || OperatingSystem.IsAndroid() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() ? int.MaxValue - : Console.BufferWidth; + : Console.WindowWidth; public int Height => Console.IsOutputRedirected || OperatingSystem.IsBrowser() || OperatingSystem.IsAndroid() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() ? int.MaxValue - : Console.BufferHeight; + : Console.WindowHeight; protected IConsole Console { get; } diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs index d8dc222a9c..f3081c81dd 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/OutputDevice/Terminal/TerminalTestReporterTests.cs @@ -458,11 +458,11 @@ public void AnsiTerminal_OutputProgressFrameIsCorrect() Error output Oh no! ␛[m - [␛[32m✓1␛[m/␛[31mx0␛[m/␛[33m↓0␛[m] assembly.dll (net8.0|x64)␛[2147483640G(1m 31s) - SkippedTest1␛[2147483640G(1m 31s) - InProgressTest1␛[2147483640G(1m 31s) - InProgressTest2␛[2147483643G(31s) - InProgressTest3␛[2147483644G(1s) + [␛[32m✓1␛[m/␛[31mx0␛[m/␛[33m↓0␛[m] assembly.dll (net8.0|x64)␛[242G(1m 31s) + SkippedTest1␛[242G(1m 31s) + InProgressTest1␛[242G(1m 31s) + InProgressTest2␛[245G(31s) + InProgressTest3␛[246G(1s) ␛[7F ␛[J␛[33mskipped␛[m SkippedTest1 ␛[90m(10s 000ms)␛[m ␛[90m Standard output @@ -470,10 +470,10 @@ Oh no! Error output Oh no! ␛[m - [␛[32m✓1␛[m/␛[31mx0␛[m/␛[33m↓1␛[m] assembly.dll (net8.0|x64)␛[2147483640G(1m 31s) - InProgressTest1␛[2147483640G(1m 31s) - InProgressTest2␛[2147483643G(31s) - InProgressTest3␛[2147483644G(1s) + [␛[32m✓1␛[m/␛[31mx0␛[m/␛[33m↓1␛[m] assembly.dll (net8.0|x64)␛[242G(1m 31s) + InProgressTest1␛[242G(1m 31s) + InProgressTest2␛[245G(31s) + InProgressTest3␛[246G(1s) """; @@ -556,7 +556,11 @@ internal class StringBuilderConsole : IConsole public int BufferHeight => int.MaxValue; - public int BufferWidth => int.MinValue; + public int BufferWidth => int.MaxValue; + + public int WindowHeight => int.MaxValue; + + public int WindowWidth => int.MaxValue; public bool IsOutputRedirected => false; @@ -903,4 +907,178 @@ public void TerminalTestReporter_WhenInDiscoveryMode_ShouldIncrementDiscoveredTe // Assert - should contain information about 2 tests discovered Assert.IsTrue(output.Contains('2') || output.Contains("TestMethod1"), "Output should contain information about discovered tests"); } + + [TestMethod] + public void SimpleTerminal_UsesWindowWidthNotBufferWidth() + { + // Arrange - Create a console where BufferWidth and WindowWidth are different + var console = new TestConsoleWithDifferentBufferAndWindowWidth + { + BufferWidth = 4096, + WindowWidth = 120, + }; + + var terminal = new NonAnsiTerminal(console); + + // Assert - Width should use WindowWidth, not BufferWidth + Assert.AreEqual(120, terminal.Width); + } + + [TestMethod] + public void AnsiTerminal_UsesWindowWidthNotBufferWidth() + { + // Arrange - Create a console where BufferWidth and WindowWidth are different + var console = new TestConsoleWithDifferentBufferAndWindowWidth + { + BufferWidth = 4096, + WindowWidth = 120, + }; + + var terminal = new AnsiTerminal(console); + + // Assert - Width should use WindowWidth, not BufferWidth + Assert.AreEqual(120, terminal.Width); + } + + /// + /// Reproduces the bug from issue #7240: when Console.BufferWidth > Console.WindowWidth, + /// the ANSI cursor positioning places timings off-screen because it was using BufferWidth + /// (capped to 250) instead of WindowWidth. + /// + /// Before fix: cursor goes to column 242 (= MaxColumn 250 - 8), off-screen for 120-col window. + /// After fix: cursor goes to column 112 (= WindowWidth 120 - 8), visible in window. + /// + [TestMethod] + public void AnsiTerminal_ProgressFrame_UseWindowWidthForCursorPositioning_WhenBufferWidthIsLarger() + { + string targetFramework = "net8.0"; + string architecture = "x64"; + string assembly = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? @"C:\work\assembly.dll" : "/mnt/work/assembly.dll"; + + // Console with BufferWidth=4096 but WindowWidth=120, mimicking the bug scenario. + var stringBuilderConsole = new StringBuilderConsoleWithCustomWidths(bufferWidth: 4096, windowWidth: 120); + var stopwatchFactory = new StopwatchFactory(); + var terminalReporter = new TerminalTestReporter(assembly, targetFramework, architecture, stringBuilderConsole, new CTRLPlusCCancellationTokenSource(), new TerminalTestReporterOptions + { + ShowPassedTests = () => true, + AnsiMode = AnsiMode.ForceAnsi, + ShowActiveTests = true, + ShowProgress = () => true, + }) + { + CreateStopwatch = stopwatchFactory.CreateStopwatch, + }; + + var startHandle = new AutoResetEvent(initialState: false); + var stopHandle = new AutoResetEvent(initialState: false); + + terminalReporter.OnProgressStartUpdate += (sender, args) => startHandle.WaitOne(); + terminalReporter.OnProgressStopUpdate += (sender, args) => stopHandle.Set(); + + DateTimeOffset startTime = DateTimeOffset.MinValue; + terminalReporter.TestExecutionStarted(startTime, 1, isDiscovery: false); + terminalReporter.AssemblyRunStarted(); + + terminalReporter.TestInProgress(testNodeUid: "Test1", displayName: "Test1"); + stopwatchFactory.AddTime(TimeSpan.FromMinutes(1) + TimeSpan.FromSeconds(31)); + + terminalReporter.TestCompleted(testNodeUid: "Test1", "Test1", TestOutcome.Passed, TimeSpan.FromSeconds(10), + informativeMessage: null, errorMessage: null, exception: null, expected: null, actual: null, standardOutput: null, errorOutput: null); + + string output = stringBuilderConsole.Output; + startHandle.Set(); + stopHandle.WaitOne(); + + string escapedOutput = ShowEscape(output)!; + + // With WindowWidth=120, cursor for "(1m 31s)" (8 chars) should be at column 120-8=112. + // Before the fix, BufferWidth=4096 was capped to MaxColumn=250, giving column 250-8=242. + Assert.Contains("␛[112G(1m 31s)", escapedOutput, + "Cursor should be positioned at column 112 (WindowWidth=120 minus duration length), not at 242 (MaxColumn=250 minus duration length)"); + Assert.DoesNotContain("␛[242G", escapedOutput, + "Cursor must NOT be positioned at column 242 which would happen if BufferWidth (4096, capped to 250) was used instead of WindowWidth"); + } + + internal class TestConsoleWithDifferentBufferAndWindowWidth : IConsole + { + public int BufferHeight { get; set; } = 300; + + public int BufferWidth { get; set; } = 4096; + + public int WindowHeight { get; set; } = 30; + + public int WindowWidth { get; set; } = 120; + + public bool IsOutputRedirected => false; + + public event ConsoleCancelEventHandler? CancelKeyPress = (sender, e) => { }; + + public void Clear() => throw new NotImplementedException(); + + public ConsoleColor GetForegroundColor() => ConsoleColor.White; + + public void SetForegroundColor(ConsoleColor color) + { + } + + public void Write(string? value) + { + } + + public void Write(char value) + { + } + + public void WriteLine() + { + } + + public void WriteLine(string? value) + { + } + } + + /// + /// A StringBuilderConsole variant that captures output and allows custom Buffer/Window dimensions. + /// + internal sealed class StringBuilderConsoleWithCustomWidths : IConsole + { + private readonly StringBuilder _output = new(); + + public StringBuilderConsoleWithCustomWidths(int bufferWidth, int windowWidth) + { + BufferWidth = bufferWidth; + WindowWidth = windowWidth; + } + + public int BufferHeight => int.MaxValue; + + public int BufferWidth { get; } + + public int WindowHeight => int.MaxValue; + + public int WindowWidth { get; } + + public bool IsOutputRedirected => false; + + public string Output => _output.ToString(); + + public event ConsoleCancelEventHandler? CancelKeyPress = (sender, e) => { }; + + public void Clear() => throw new NotImplementedException(); + + public ConsoleColor GetForegroundColor() => ConsoleColor.White; + + public void SetForegroundColor(ConsoleColor color) + { + } + + public void Write(string? value) => _output.Append(value); + + public void Write(char value) => _output.Append(value); + + public void WriteLine() => _output.AppendLine(); + + public void WriteLine(string? value) => _output.AppendLine(value); + } }