Files
PowerShell/test/xUnit/csharp/test_ProgressNode.cs

332 lines
14 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections;
using System.Management.Automation;
using Microsoft.PowerShell;
using Xunit;
namespace PSTests.Parallel
{
/// <summary>
/// Tests for ProgressNode rendering with double-width unicode characters.
/// These tests verify the fix for Issue #21293 where progress bars with
/// double-width characters (Japanese, Chinese, Korean, emoji) exceeded maxWidth.
/// </summary>
public static class ProgressNodeTests
{
/// <summary>
/// Verify Issue #21293 scenario - the original bug report.
/// This test reproduces the exact scenario from the issue.
/// Tests with edge cases around standard terminal width (80 columns).
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ProgressBar_OriginalBugScenario_MustNotExceedMaxWidth(int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "My Status", "1/6 次の段階");
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.SecondsRemaining = 120;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Issue #21293 (width {maxWidth}): Progress bar ({actualWidth} cells) exceeds maxWidth ({maxWidth}). Output: {output}");
}
/// <summary>
/// Verify that progress bar respects maxWidth for Japanese text.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ProgressBar_WithDoubleWidthChars_MustNotExceedMaxWidth(int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "Activity", "日本語テキスト");
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Progress bar width {maxWidth}: ({actualWidth}) exceeds maxWidth ({maxWidth}). Output: {output}");
}
/// <summary>
/// Verify emoji handling with sufficient text length.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ProgressBar_WithEmoji_MustNotExceedMaxWidth(int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "Upload", "📁ファイル転送中🔄処理中📊");
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Emoji progress bar width {maxWidth}: ({actualWidth} cells) exceeds maxWidth ({maxWidth}). Output: {output}");
}
/// <summary>
/// Test various lengths of double-width text.
/// All test cases have sufficient length to detect the bug.
/// </summary>
/// <param name="statusText">The status message displayed in the progress bar.</param>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData("あいうえお", 79)]
[InlineData("あいうえお", 80)]
[InlineData("あいうえお", 81)]
[InlineData("データ処理中", 79)]
[InlineData("データ処理中", 80)]
[InlineData("データ処理中", 81)]
[InlineData("ファイルをアップロード中", 79)]
[InlineData("ファイルをアップロード中", 80)]
[InlineData("ファイルをアップロード中", 81)]
public static void ProgressBar_VariousDoubleWidthLengths_MustNotExceedMaxWidth(string statusText, int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "Test", statusText);
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Status '{statusText}' width {maxWidth}: resulted in width {actualWidth} exceeding maxWidth {maxWidth}. Output: {output}");
}
/// <summary>
/// Verify mixed ASCII and double-width characters.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ProgressBar_MixedWidthText_MustNotExceedMaxWidth(int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "Process", "File_日本語_document.txt");
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Mixed-width text width {maxWidth}: resulted in width {actualWidth} exceeding maxWidth {maxWidth}. Output: {output}");
}
/// <summary>
/// Verify long double-width string handling and truncation.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ProgressBar_LongDoubleWidthString_MustNotExceedMaxWidth(int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "処理", "ファイルを処理中です今しばらくお待ちください");
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Long double-width text width {maxWidth}: resulted in width {actualWidth} exceeding maxWidth {maxWidth}. Output: {output}");
}
/// <summary>
/// Verify that truncation respects character boundaries.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ProgressBar_Truncation_MustRespectCharacterBoundaries(int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "Downloading", "ファイル処理中123456789012345678901234567890");
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Truncated progress bar width {maxWidth}: ({actualWidth} cells) exceeds maxWidth ({maxWidth}). Output: {output}");
}
/// <summary>
/// Test for Issue: Double closing bracket when truncating with double-width characters.
/// Verifies that statusPartDisplayWidth is calculated from actual statusPart length,
/// not from estimated value, to prevent progress bar from exceeding maxWidth.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ProgressBar_LongStatusWithDoubleWidth_MustNotExceedMaxWidth(int maxWidth)
{
var rawUI = new TestProgressRawUI();
// Very long status with emojis and mixed width characters that will be truncated
var record = new ProgressRecord(0, "Long Text Test", "🚀🎯⚡💾✅ 絵文字も含めた超長文テストケース with emojis and very long text to test the truncation feature properly");
var node = new ProgressNode(1, record);
node.PercentComplete = 100;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
// The progress bar must not exceed maxWidth
Assert.True(
actualWidth <= maxWidth,
$"Progress bar with long truncated status width {maxWidth}: ({actualWidth} cells) exceeds maxWidth ({maxWidth}). Output: {output}");
// Verify no double closing brackets - count ']' occurrences at end
int closingBracketCount = 0;
for (int i = output.Length - 1; i >= 0 && output[i] == ']'; i--)
{
closingBracketCount++;
}
Assert.True(
closingBracketCount <= 1,
$"Found {closingBracketCount} consecutive closing brackets at end (width {maxWidth}), expected 1. Output: {output}");
}
/// <summary>
/// Test activityDisplayWidth bug with small maxWidth to amplify error.
/// Bug: After truncation, activityDisplayWidth is set to maxWidth/2 but actual width differs.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ExtremeTest_ActivityWidthBug_SmallMaxWidth(int maxWidth)
{
var rawUI = new TestProgressRawUI();
string longActivity = "日本語日本語日本語日本語日本語"; // 20 cells
var record = new ProgressRecord(0, longActivity, "St");
var node = new ProgressNode(1, record);
node.PercentComplete = 50;
node.SecondsRemaining = 30;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"maxWidth {maxWidth} test: width {actualWidth} exceeds {maxWidth}. Output: [{output}]");
}
/// <summary>
/// Test surrogate pair handling with strategically positioned emojis.
/// Bug: Substring(i,1) splits surrogate pairs causing width calculation errors.
/// </summary>
/// <param name="maxWidth">The maximum width in buffer cells for the progress bar rendering.</param>
[Theory]
[InlineData(79)]
[InlineData(80)]
[InlineData(81)]
public static void ExtremeTest_SurrogatePairAtVTBoundary(int maxWidth)
{
var rawUI = new TestProgressRawUI();
var record = new ProgressRecord(0, "A", "X🚀Y🚀Z🚀");
var node = new ProgressNode(1, record);
node.PercentComplete = 33;
node.Style = ProgressNode.RenderStyle.Ansi;
var strCollection = new ArrayList();
node.Render(strCollection, 0, maxWidth, rawUI);
var output = strCollection[0] as string;
int actualWidth = ConsoleControl.LengthInBufferCells(output, 0, checkEscapeSequences: true);
Assert.True(
actualWidth <= maxWidth,
$"Surrogate pair test width {maxWidth}: width {actualWidth} exceeds {maxWidth}. Output: [{output}]");
}
}
}