Skip to content

Commit 6de7584

Browse files
authored
Fix race condition in Theme.Tips method when called concurrently (#109)
* Initial plan * Fix race condition in Theme.Tips method and add test * Fix unit test error in Theme.Tips method - maintain original styling behavior
1 parent c34c15e commit 6de7584

File tree

2 files changed

+68
-3
lines changed

2 files changed

+68
-3
lines changed

issues_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package color_test
22

33
import (
4+
"bytes"
45
"fmt"
56
"strings"
7+
"sync"
68
"testing"
79

810
"github.com/gookit/color"
@@ -49,3 +51,64 @@ foo <bg=lightGreen;fg=black>two</> four
4951
color.Print(test2)
5052
color.Print(test3)
5153
}
54+
55+
// https://github.com/gookit/color/issues/95
56+
func TestIssues_95(t *testing.T) {
57+
// Test for race condition in Theme.Tips when called concurrently
58+
// We use a thread-safe buffer to avoid low-level I/O races
59+
// and focus on testing the application-level race condition
60+
buf := &safeBuffer{Buffer: &bytes.Buffer{}}
61+
color.SetOutput(buf)
62+
defer color.ResetOutput()
63+
64+
const numGoroutines = 20
65+
var wg sync.WaitGroup
66+
wg.Add(numGoroutines * 2)
67+
68+
// Start multiple goroutines calling Tips concurrently
69+
for i := 0; i < numGoroutines; i++ {
70+
go func(id int) {
71+
defer wg.Done()
72+
color.Warn.Tips("warning message %d", id)
73+
}(i)
74+
75+
go func(id int) {
76+
defer wg.Done()
77+
color.Error.Tips("error message %d", id)
78+
}(i)
79+
}
80+
81+
wg.Wait()
82+
output := buf.String()
83+
84+
// Clean the output of ANSI codes to check logical structure
85+
cleanOutput := color.ClearCode(output)
86+
lines := strings.Split(strings.TrimSpace(cleanOutput), "\n")
87+
88+
// Verify that each line is properly formatted (without ANSI codes)
89+
for i, line := range lines {
90+
if line == "" {
91+
continue
92+
}
93+
// Each line should start with either "WARNING:" or "ERROR:"
94+
if !strings.HasPrefix(line, "WARNING:") && !strings.HasPrefix(line, "ERROR:") {
95+
t.Errorf("Line %d is malformed due to race condition: %q", i, line)
96+
}
97+
// Should not contain both prefixes (indicating interleaved output)
98+
if strings.Contains(line, "WARNING:") && strings.Contains(line, "ERROR:") {
99+
t.Errorf("Line %d contains interleaved output: %q", i, line)
100+
}
101+
}
102+
}
103+
104+
// Thread-safe buffer for testing
105+
type safeBuffer struct {
106+
*bytes.Buffer
107+
mu sync.Mutex
108+
}
109+
110+
func (sb *safeBuffer) Write(p []byte) (n int, err error) {
111+
sb.mu.Lock()
112+
defer sb.mu.Unlock()
113+
return sb.Buffer.Write(p)
114+
}

style.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,11 @@ func (t *Theme) Save() {
122122

123123
// Tips use name as title, only apply style for name
124124
func (t *Theme) Tips(format string, a ...any) {
125-
// only apply style for name
126-
t.Print(strings.ToUpper(t.Name) + ": ")
127-
Printf(format+"\n", a...)
125+
// Format the message part first
126+
message := fmt.Sprintf(format, a...)
127+
// Create styled title and combine with unstyled message in a single print operation to avoid race conditions
128+
styledTitle := t.Style.Render(strings.ToUpper(t.Name) + ": ")
129+
Printf("%s%s\n", styledTitle, message)
128130
}
129131

130132
// Prompt use name as title, and apply style for message

0 commit comments

Comments
 (0)