| 
1 | 1 | package color_test  | 
2 | 2 | 
 
  | 
3 | 3 | import (  | 
 | 4 | +	"bytes"  | 
4 | 5 | 	"fmt"  | 
5 | 6 | 	"strings"  | 
 | 7 | +	"sync"  | 
6 | 8 | 	"testing"  | 
7 | 9 | 
 
  | 
8 | 10 | 	"github.com/gookit/color"  | 
@@ -49,3 +51,64 @@ foo <bg=lightGreen;fg=black>two</> four  | 
49 | 51 | 	color.Print(test2)  | 
50 | 52 | 	color.Print(test3)  | 
51 | 53 | }  | 
 | 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 | +}  | 
0 commit comments