diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml
index 77186125a9c..3277b8a0c17 100644
--- a/.github/workflows/linux-ci.yml
+++ b/.github/workflows/linux-ci.yml
@@ -10,6 +10,7 @@ on:
- master
- release/**
- github-mirror
+ - "servicing-*"
paths:
- "**"
- "*"
@@ -23,6 +24,7 @@ on:
- master
- release/**
- github-mirror
+ - "servicing-*"
- "*-feature"
# Path filters for PRs need to go into the changes job
diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml
index 55d852bb68a..f89ce8caa83 100644
--- a/.github/workflows/macos-ci.yml
+++ b/.github/workflows/macos-ci.yml
@@ -8,6 +8,7 @@ on:
- master
- release/**
- github-mirror
+ - "servicing-*"
paths:
- "**"
- "*"
@@ -21,6 +22,7 @@ on:
- master
- release/**
- github-mirror
+ - "servicing-*"
- "*-feature"
# Path filters for PRs need to go into the changes job
diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml
index 8a57b8b9726..42347d2e12f 100644
--- a/.github/workflows/windows-ci.yml
+++ b/.github/workflows/windows-ci.yml
@@ -6,6 +6,7 @@ on:
- master
- release/**
- github-mirror
+ - "servicing-*"
paths:
- "**"
- "*"
@@ -20,6 +21,7 @@ on:
- master
- release/**
- github-mirror
+ - "servicing-*"
- "*-feature"
# Path filters for PRs need to go into the changes job
diff --git a/.pipelines/templates/release-prep-for-ev2.yml b/.pipelines/templates/release-prep-for-ev2.yml
index ace2b8f7df4..ec6ea5ec1e9 100644
--- a/.pipelines/templates/release-prep-for-ev2.yml
+++ b/.pipelines/templates/release-prep-for-ev2.yml
@@ -16,6 +16,9 @@ stages:
- input: pipelineArtifact
pipeline: PSPackagesOfficial
artifactName: drop_linux_package_deb
+ - input: pipelineArtifact
+ pipeline: PSPackagesOfficial
+ artifactName: drop_linux_package_deb_arm64
- input: pipelineArtifact
pipeline: PSPackagesOfficial
artifactName: drop_linux_package_rpm
diff --git a/CHANGELOG/README.md b/CHANGELOG/README.md
index c20cd311ff5..2b022e75735 100644
--- a/CHANGELOG/README.md
+++ b/CHANGELOG/README.md
@@ -1,6 +1,8 @@
# Changelogs
- [Current preview changelog](preview.md)
+- [7.6 changelog](7.6.md)
+- [7.5 changelog](7.5.md)
- [7.4 changelog](7.4.md)
- [7.3 changelog](7.3.md)
- [7.2 changelog](7.2.md)
diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewGuidCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewGuidCommand.cs
index 0e466f86d3f..d1d41b312f7 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewGuidCommand.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/NewGuidCommand.cs
@@ -49,7 +49,7 @@ protected override void ProcessRecord()
}
else
{
- guid = Empty.ToBool() ? Guid.Empty : Guid.NewGuid();
+ guid = Empty.ToBool() ? Guid.Empty : Guid.CreateVersion7();
}
WriteObject(guid);
diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs
index 4f51820524c..01cbd4069c4 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ProgressNode.cs
@@ -67,7 +67,7 @@ namespace Microsoft.PowerShell
this.Style = IsMinimalProgressRenderingEnabled()
? RenderStyle.Ansi
- : this.Style = RenderStyle.FullPlus;
+ : RenderStyle.FullPlus;
this.SourceId = sourceId;
}
@@ -111,7 +111,7 @@ namespace Microsoft.PowerShell
RenderMinimal(strCollection, indentation, maxWidth, rawUI);
break;
case RenderStyle.Ansi:
- RenderAnsi(strCollection, indentation, maxWidth);
+ RenderAnsi(strCollection, indentation, maxWidth, rawUI);
break;
case RenderStyle.Invisible:
// do nothing
@@ -368,9 +368,12 @@ internal static bool IsMinimalProgressRenderingEnabled()
///
/// The maximum number of chars that the rendering is allowed to consume.
///
+ ///
+ /// The PSHostRawUserInterface used to gauge string widths in the rendering.
+ ///
private
void
- RenderAnsi(ArrayList strCollection, int indentation, int maxWidth)
+ RenderAnsi(ArrayList strCollection, int indentation, int maxWidth, PSHostRawUserInterface rawUI)
{
string indent = StringUtil.Padding(indentation);
string secRemain = string.Empty;
@@ -389,68 +392,129 @@ internal static bool IsMinimalProgressRenderingEnabled()
// if the activity is really long, only use up to half the width
string activity;
- if (Activity.Length > maxWidth / 2)
+ int activityDisplayCellsWidth = rawUI.LengthInBufferCells(Activity);
+ if (activityDisplayCellsWidth > maxWidth / 2)
{
- activity = Activity.Substring(0, maxWidth / 2) + PSObjectHelper.Ellipsis;
+ activity = StringUtil.TruncateToBufferCellWidth(rawUI, Activity, (maxWidth / 2) - 1) + PSObjectHelper.Ellipsis;
}
else
{
activity = Activity;
}
+ activityDisplayCellsWidth = rawUI.LengthInBufferCells(activity);
+
// 4 is for the extra space and square brackets below and one extra space
- int barWidth = maxWidth - activity.Length - indentation - 4;
+ int barWidth = maxWidth - activityDisplayCellsWidth - indentation - 4;
var sb = new StringBuilder();
- int padding = maxWidth + PSStyle.Instance.Progress.Style.Length + PSStyle.Instance.Reverse.Length + PSStyle.Instance.ReverseOff.Length;
sb.Append(PSStyle.Instance.Reverse);
- int maxStatusLength = barWidth - secRemainLength - 1;
- if (maxStatusLength > 0 && StatusDescription.Length > barWidth - secRemainLength)
+ // Build the status description part
+ int maxStatusWidth = barWidth - secRemainLength;
+ string statusPart = RenderAnsiStatusPart(rawUI, maxStatusWidth, out int statusPartDisplayWidth);
+
+ sb.Append(statusPart);
+
+ // Calculate padding needed
+ int emptyPadLength = barWidth - statusPartDisplayWidth - secRemainLength;
+ if (emptyPadLength > 0)
{
- sb.Append(StatusDescription.AsSpan(0, barWidth - secRemainLength - 1));
- sb.Append(PSObjectHelper.Ellipsis);
+ sb.Append(' ', emptyPadLength);
}
- else
+
+ sb.Append(secRemain);
+
+ // Insert ReverseOff at the correct position for the progress bar
+ RenderAnsiReverseOff(sb, rawUI, statusPart, barWidth);
+
+ strCollection.Add(
+ StringUtil.Format(
+ "{0}{1}{2} [{3}]{4}",
+ indent,
+ PSStyle.Instance.Progress.Style,
+ activity,
+ sb.ToString(),
+ PSStyle.Instance.Reset));
+ }
+
+ ///
+ /// Builds the status-description portion of the Ansi progress bar, truncating it
+ /// with an ellipsis when it would exceed the available status width.
+ ///
+ ///
+ /// The PSHostRawUserInterface used to gauge string widths.
+ ///
+ ///
+ /// The maximum number of buffer cells available for the status description.
+ ///
+ ///
+ /// On return, the width in buffer cells of the produced status part.
+ ///
+ /// The status description, possibly truncated with an ellipsis.
+ private string RenderAnsiStatusPart(PSHostRawUserInterface rawUI, int maxStatusWidth, out int statusPartDisplayWidth)
+ {
+ int statusDisplayWidth = rawUI.LengthInBufferCells(StatusDescription);
+
+ if (maxStatusWidth <= 0 || statusDisplayWidth <= maxStatusWidth)
{
- sb.Append(StatusDescription);
+ statusPartDisplayWidth = statusDisplayWidth;
+ return StatusDescription;
}
- int emptyPadLength = barWidth + PSStyle.Instance.Reverse.Length - sb.Length - secRemainLength;
- if (emptyPadLength > 0)
+ int ellipsisWidth = rawUI.LengthInBufferCells(PSObjectHelper.EllipsisStr);
+ string statusPart = StringUtil.TruncateToBufferCellWidth(rawUI, StatusDescription, maxStatusWidth - ellipsisWidth) + PSObjectHelper.EllipsisStr;
+ statusPartDisplayWidth = rawUI.LengthInBufferCells(statusPart);
+ return statusPart;
+ }
+
+ ///
+ /// Appends the ReverseOff VT sequence to the rendered bar at the buffer-cell position
+ /// that corresponds to the filled portion of the progress bar, respecting character boundaries.
+ ///
+ /// The StringBuilder holding the bar contents built so far.
+ ///
+ /// The PSHostRawUserInterface used to gauge string widths.
+ ///
+ /// The status text at the start of the bar.
+ /// The total width of the progress bar in buffer cells.
+ private void RenderAnsiReverseOff(StringBuilder sb, PSHostRawUserInterface rawUI, string statusPart, int barWidth)
+ {
+ if (PercentComplete < 0 || PercentComplete >= 100 || barWidth <= 0)
{
- sb.Append(string.Empty.PadRight(emptyPadLength));
+ sb.Append(PSStyle.Instance.ReverseOff);
+ return;
}
- sb.Append(secRemain);
+ int barLength = PercentComplete * barWidth / 100;
+ if (barLength >= barWidth)
+ {
+ barLength = barWidth - 1;
+ }
+
+ // Calculate the string position where we need to insert ReverseOff.
+ // We need to find the character position that corresponds to barLength buffer cells.
+ int stringPos = PSStyle.Instance.Reverse.Length;
+ int currentCellCount = 0;
- if (PercentComplete >= 0 && PercentComplete < 100 && barWidth > 0)
+ for (int i = 0; i < statusPart.Length && currentCellCount < barLength; i++)
{
- int barLength = PercentComplete * barWidth / 100;
- if (barLength >= barWidth)
- {
- barLength = barWidth - 1;
- }
+ currentCellCount += rawUI.LengthInBufferCells(statusPart[i]);
+ stringPos++;
+ }
- if (barLength < sb.Length)
- {
- sb.Insert(barLength + PSStyle.Instance.Reverse.Length, PSStyle.Instance.ReverseOff);
- }
+ // Add any padding characters.
+ int remainingCells = barLength - currentCellCount;
+ stringPos += Math.Max(0, remainingCells);
+
+ if (stringPos < sb.Length)
+ {
+ sb.Insert(stringPos, PSStyle.Instance.ReverseOff);
}
else
{
sb.Append(PSStyle.Instance.ReverseOff);
}
-
- strCollection.Add(
- StringUtil.Format(
- "{0}{1}{2} [{3}]{4}",
- indent,
- PSStyle.Instance.Progress.Style,
- activity,
- sb.ToString(),
- PSStyle.Instance.Reset)
- .PadRight(padding));
}
///
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/New-Guid.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/New-Guid.Tests.ps1
index 8756fed7726..9b082793413 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/New-Guid.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/New-Guid.Tests.ps1
@@ -43,6 +43,12 @@ Describe "New-Guid" -Tags "CI" {
$observed | Should -Be $guids
}
+ It "Should generate a UUID v7" {
+ $guid = New-Guid
+ # UUID v7 has version nibble '7' at position 14 in the string representation
+ $guid.ToString()[14] | Should -BeExactly '7'
+ }
+
It "Should return different guids with each call" {
$guid1 = New-Guid
$guid2 = New-Guid
diff --git a/test/xUnit/csharp/TestProgressRawUI.cs b/test/xUnit/csharp/TestProgressRawUI.cs
new file mode 100644
index 00000000000..e357aa9cfdc
--- /dev/null
+++ b/test/xUnit/csharp/TestProgressRawUI.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Management.Automation.Host;
+using Microsoft.PowerShell;
+
+namespace PSTests.Parallel
+{
+ ///
+ /// Test helper class that implements PSHostRawUserInterface for progress bar testing.
+ /// Delegates width calculations to ConsoleControl.LengthInBufferCells.
+ ///
+ internal sealed class TestProgressRawUI : PSHostRawUserInterface
+ {
+ public override ConsoleColor ForegroundColor { get; set; }
+
+ public override ConsoleColor BackgroundColor { get; set; }
+
+ public override Coordinates CursorPosition { get; set; }
+
+ public override Coordinates WindowPosition { get; set; }
+
+ public override int CursorSize { get; set; }
+
+ public override Size BufferSize { get; set; }
+
+ public override Size WindowSize { get; set; }
+
+ public override Size MaxWindowSize => new Size(120, 50);
+
+ public override Size MaxPhysicalWindowSize => new Size(120, 50);
+
+ public override string WindowTitle { get; set; }
+
+ public override bool KeyAvailable => false;
+
+ public override BufferCell[,] GetBufferContents(Rectangle rectangle) => throw new NotImplementedException();
+
+ public override void SetBufferContents(Rectangle rectangle, BufferCell fill) => throw new NotImplementedException();
+
+ public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) => throw new NotImplementedException();
+
+ public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) => throw new NotImplementedException();
+
+ public override KeyInfo ReadKey(ReadKeyOptions options) => throw new NotImplementedException();
+
+ public override void FlushInputBuffer() => throw new NotImplementedException();
+
+ public override int LengthInBufferCells(string str) => ConsoleControl.LengthInBufferCells(str, 0, checkEscapeSequences: true);
+
+ public override int LengthInBufferCells(string str, int offset) => ConsoleControl.LengthInBufferCells(str, offset, checkEscapeSequences: true);
+
+ public override int LengthInBufferCells(char c) => ConsoleControl.LengthInBufferCells(c);
+ }
+}
diff --git a/test/xUnit/csharp/test_ProgressNode.cs b/test/xUnit/csharp/test_ProgressNode.cs
new file mode 100644
index 00000000000..e76666d50bf
--- /dev/null
+++ b/test/xUnit/csharp/test_ProgressNode.cs
@@ -0,0 +1,331 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections;
+using System.Management.Automation;
+using Microsoft.PowerShell;
+using Xunit;
+
+namespace PSTests.Parallel
+{
+ ///
+ /// 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.
+ ///
+ public static class ProgressNodeTests
+ {
+ ///
+ /// 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).
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// Verify that progress bar respects maxWidth for Japanese text.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// Verify emoji handling with sufficient text length.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// Test various lengths of double-width text.
+ /// All test cases have sufficient length to detect the bug.
+ ///
+ /// The status message displayed in the progress bar.
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// Verify mixed ASCII and double-width characters.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// Verify long double-width string handling and truncation.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// Verify that truncation respects character boundaries.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}");
+ }
+
+ ///
+ /// Test activityDisplayWidth bug with small maxWidth to amplify error.
+ /// Bug: After truncation, activityDisplayWidth is set to maxWidth/2 but actual width differs.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}]");
+ }
+
+ ///
+ /// Test surrogate pair handling with strategically positioned emojis.
+ /// Bug: Substring(i,1) splits surrogate pairs causing width calculation errors.
+ ///
+ /// The maximum width in buffer cells for the progress bar rendering.
+ [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}]");
+ }
+ }
+}