mirror of
https://github.com/docker/compose.git
synced 2026-08-27 03:45:29 +00:00
fix(progress): measure taskID width in runes, not bytes
Some checks are pending
ci / validate (lint) (push) Waiting to run
ci / validate (validate-docs) (push) Waiting to run
ci / validate (validate-go-mod) (push) Waiting to run
ci / validate (validate-headers) (push) Waiting to run
ci / binary (push) Waiting to run
ci / binary-finalize (push) Blocked by required conditions
ci / bin-image-test (push) Waiting to run
ci / test (push) Waiting to run
ci / e2e (plugin, oldstable) (push) Waiting to run
ci / e2e (standalone, oldstable) (push) Waiting to run
ci / e2e (plugin, stable) (push) Waiting to run
ci / e2e (standalone, stable) (push) Waiting to run
ci / coverage (push) Blocked by required conditions
ci / release (push) Blocked by required conditions
merge / bin-image-prepare (push) Waiting to run
merge / bin-image (push) Blocked by required conditions
merge / module-image (push) Waiting to run
merge / desktop-edge-test (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
Some checks are pending
ci / validate (lint) (push) Waiting to run
ci / validate (validate-docs) (push) Waiting to run
ci / validate (validate-go-mod) (push) Waiting to run
ci / validate (validate-headers) (push) Waiting to run
ci / binary (push) Waiting to run
ci / binary-finalize (push) Blocked by required conditions
ci / bin-image-test (push) Waiting to run
ci / test (push) Waiting to run
ci / e2e (plugin, oldstable) (push) Waiting to run
ci / e2e (standalone, oldstable) (push) Waiting to run
ci / e2e (plugin, stable) (push) Waiting to run
ci / e2e (standalone, stable) (push) Waiting to run
ci / coverage (push) Blocked by required conditions
ci / release (push) Blocked by required conditions
merge / bin-image-prepare (push) Waiting to run
merge / bin-image (push) Blocked by required conditions
merge / module-image (push) Waiting to run
merge / desktop-edge-test (push) Blocked by required conditions
Scorecards supply-chain security / Scorecards analysis (push) Waiting to run
maxBeforeStatusWidth used len(l.taskID) (bytes) while applyPadding used utf8.RuneCountInString (runes). For ASCII task IDs the two agree and no symptom surfaces, but a taskID containing multi-byte UTF-8 chars (CJK, emoji, accented Latin) reported a width larger than its visual columns. computeOverflow then triggered truncation where none was needed, and truncateLongestTaskID's byte-indexed slice could land mid-multibyte sequence, corrupting the displayed string. Align the two measurements on rune count. Signed-off-by: Guillaume Lours <glours@users.noreply.github.com>
This commit is contained in:
parent
1a6212c859
commit
ef3238ab1f
2 changed files with 89 additions and 15 deletions
|
|
@ -449,7 +449,7 @@ func maxBeforeStatusWidth(lines []lineData) int {
|
|||
var maxWidth int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
width := 3 + lenAnsi(l.prefix) + len(l.taskID) + lenAnsi(l.progress)
|
||||
width := 3 + lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
|
||||
if width > maxWidth {
|
||||
maxWidth = width
|
||||
}
|
||||
|
|
@ -478,18 +478,29 @@ func computeOverflow(lines []lineData, maxBeforeStatus, maxStatusLen, timerLen,
|
|||
}
|
||||
|
||||
// truncateProgressSize drops the trailing "X.XMB / Y.YMB" size info from the
|
||||
// first line whose progress still carries it. Returns true if any line was
|
||||
// modified. Used to recover from overflow without abbreviating the taskID.
|
||||
// line currently driving maxBeforeStatusWidth — only that line's shrink can
|
||||
// reduce overflow. Returns true if any line was modified.
|
||||
func truncateProgressSize(lines []lineData) bool {
|
||||
maxIdx := -1
|
||||
var maxWidth int
|
||||
for i := range lines {
|
||||
l := &lines[i]
|
||||
if l.progressSizeBytes > 0 {
|
||||
l.progress = l.progress[:len(l.progress)-l.progressSizeBytes]
|
||||
l.progressSizeBytes = 0
|
||||
return true
|
||||
if l.progressSizeBytes == 0 {
|
||||
continue
|
||||
}
|
||||
w := lenAnsi(l.prefix) + utf8.RuneCountInString(l.taskID) + lenAnsi(l.progress)
|
||||
if maxIdx < 0 || w > maxWidth {
|
||||
maxWidth = w
|
||||
maxIdx = i
|
||||
}
|
||||
}
|
||||
return false
|
||||
if maxIdx < 0 {
|
||||
return false
|
||||
}
|
||||
l := &lines[maxIdx]
|
||||
l.progress = l.progress[:len(l.progress)-l.progressSizeBytes]
|
||||
l.progressSizeBytes = 0
|
||||
return true
|
||||
}
|
||||
|
||||
// truncateDetails tries to truncate the first line's details to reduce overflow.
|
||||
|
|
@ -510,13 +521,14 @@ func truncateDetails(lines []lineData, overflow int) bool {
|
|||
}
|
||||
|
||||
// truncateLongestTaskID truncates the longest taskID to reduce overflow.
|
||||
// Returns true if truncation was performed.
|
||||
// Returns true if truncation was performed. Lengths and slicing are in runes
|
||||
// to avoid emitting invalid UTF-8 when taskID contains multi-byte chars.
|
||||
func truncateLongestTaskID(lines []lineData, overflow, minIDLen int) bool {
|
||||
longestIdx := -1
|
||||
longestLen := minIDLen
|
||||
for i := range lines {
|
||||
if len(lines[i].taskID) > longestLen {
|
||||
longestLen = len(lines[i].taskID)
|
||||
if utf8.RuneCountInString(lines[i].taskID) > longestLen {
|
||||
longestLen = utf8.RuneCountInString(lines[i].taskID)
|
||||
longestIdx = i
|
||||
}
|
||||
}
|
||||
|
|
@ -527,10 +539,9 @@ func truncateLongestTaskID(lines []lineData, overflow, minIDLen int) bool {
|
|||
|
||||
l := &lines[longestIdx]
|
||||
reduction := overflow + 3 // account for "..."
|
||||
newLen := max(len(l.taskID)-reduction, minIDLen-3)
|
||||
if newLen > 0 {
|
||||
l.taskID = l.taskID[:newLen] + "..."
|
||||
}
|
||||
newLen := max(longestLen-reduction, minIDLen-3)
|
||||
runes := []rune(l.taskID)
|
||||
l.taskID = string(runes[:newLen]) + "..."
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,6 +270,69 @@ func TestAdjustLineWidth_TaskIDCorrectlyTruncated(t *testing.T) {
|
|||
assert.Assert(t, strings.HasSuffix(lines[0].taskID, "..."), "truncated taskID should end with ...")
|
||||
}
|
||||
|
||||
// TestAdjustLineWidth_MultiByteTaskIDFits guards against drift between
|
||||
// applyPadding (rune-based) and maxBeforeStatusWidth (formerly byte-based):
|
||||
// a byte-based measurement falsely flags overflow for multi-byte taskIDs.
|
||||
func TestAdjustLineWidth_MultiByteTaskIDFits(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
taskID := "Image 测试测试" // 10 runes, 18 bytes
|
||||
lines := []lineData{{
|
||||
taskID: taskID,
|
||||
status: "Pulling",
|
||||
}}
|
||||
|
||||
// terminalWidth=30 fits in runes (3+10+1+7+1+4 = 26) but not in bytes
|
||||
// (3+18+1+7+1+4 = 34), so a byte-based measurement would truncate.
|
||||
w.adjustLineWidth(lines, 4, 30)
|
||||
|
||||
assert.Equal(t, taskID, lines[0].taskID,
|
||||
"taskID should not be modified when it fits terminal width in runes")
|
||||
}
|
||||
|
||||
// TestTruncateLongestTaskID_PreservesValidUTF8 verifies that when truncation
|
||||
// of a multi-byte UTF-8 taskID is genuinely required, the resulting string
|
||||
// remains valid UTF-8. Byte-indexed slicing can land mid-rune and emit
|
||||
// replacement characters (<28>) into the rendered output.
|
||||
func TestTruncateLongestTaskID_PreservesValidUTF8(t *testing.T) {
|
||||
taskID := "Image 测试测试测试测试" // 14 runes, 30 bytes
|
||||
lines := []lineData{{taskID: taskID}}
|
||||
|
||||
truncateLongestTaskID(lines, 8, 10)
|
||||
|
||||
assert.Assert(t, utf8.ValidString(lines[0].taskID),
|
||||
"truncated taskID must remain valid UTF-8, got %q", lines[0].taskID)
|
||||
assert.Assert(t, strings.HasSuffix(lines[0].taskID, "..."),
|
||||
"truncated taskID should end with ..., got %q", lines[0].taskID)
|
||||
}
|
||||
|
||||
// TestTruncateProgressSize_PicksWidestLine verifies that dropping the size
|
||||
// suffix targets the line currently driving maxBeforeStatusWidth (the only
|
||||
// line whose shrink can reduce overflow), preserving size info on narrower
|
||||
// lines that are not the bottleneck.
|
||||
func TestTruncateProgressSize_PicksWidestLine(t *testing.T) {
|
||||
narrowSuffix := " 5MB / 10MB"
|
||||
wideSuffix := " 50MB / 100MB"
|
||||
lines := []lineData{
|
||||
{
|
||||
taskID: "Image short",
|
||||
progress: " [⣿⣿]" + narrowSuffix,
|
||||
progressSizeBytes: len(narrowSuffix),
|
||||
},
|
||||
{
|
||||
taskID: "Image very-long-named-task",
|
||||
progress: " [⣿⣿⣿⣿⣿⣿⣿⣿]" + wideSuffix,
|
||||
progressSizeBytes: len(wideSuffix),
|
||||
},
|
||||
}
|
||||
|
||||
truncateProgressSize(lines)
|
||||
|
||||
assert.Equal(t, 0, lines[1].progressSizeBytes,
|
||||
"widest line should lose its size suffix first")
|
||||
assert.Equal(t, len(narrowSuffix), lines[0].progressSizeBytes,
|
||||
"narrower line should retain its size suffix")
|
||||
}
|
||||
|
||||
func TestAdjustLineWidth_NoTruncationNeeded(t *testing.T) {
|
||||
w := &ttyWriter{}
|
||||
originalDetails := "short"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue