mirror of
https://github.com/aphrody-labs/PowerShell.git
synced 2026-10-14 13:49:09 +00:00
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Justin Chung <124807742+jshigetomi@users.noreply.github.com>
332 lines
14 KiB
C#
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}]");
|
|
}
|
|
}
|
|
}
|