Skip to content

Commit 5abf9f0

Browse files
Merge pull request #1467 from gruntwork-io/thread-safe-log
Make `Log` and `Logf` threadsafe
2 parents 40c8075 + 9943f43 commit 5abf9f0

File tree

2 files changed

+61
-0
lines changed

2 files changed

+61
-0
lines changed

modules/logger/logger.go

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"runtime"
99
"strings"
10+
"sync"
1011
gotesting "testing"
1112
"time"
1213

@@ -125,6 +126,8 @@ func Logf(t testing.TestingT, format string, args ...interface{}) {
125126
tt.Helper()
126127
}
127128

129+
mutexStdout.Lock()
130+
defer mutexStdout.Unlock()
128131
DoLog(t, 2, os.Stdout, fmt.Sprintf(format, args...))
129132
}
130133

@@ -136,9 +139,13 @@ func Log(t testing.TestingT, args ...interface{}) {
136139
tt.Helper()
137140
}
138141

142+
mutexStdout.Lock()
143+
defer mutexStdout.Unlock()
139144
DoLog(t, 2, os.Stdout, args...)
140145
}
141146

147+
var mutexStdout sync.Mutex
148+
142149
// DoLog logs the given arguments to the given writer, along with a timestamp and information about what test and file is
143150
// doing the logging.
144151
func DoLog(t testing.TestingT, callDepth int, writer io.Writer, args ...interface{}) {

modules/logger/logger_test.go

+54
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package logger
33
import (
44
"bytes"
55
"fmt"
6+
"io"
7+
"os"
68
"strings"
79
"testing"
810

911
tftesting "github.com/gruntwork-io/terratest/modules/testing"
1012
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
1114
)
1215

1316
func TestDoLog(t *testing.T) {
@@ -52,3 +55,54 @@ func TestCustomLogger(t *testing.T) {
5255
assert.Equal(t, "log output 2", c.logs[1])
5356
assert.Equal(t, "subtest log", c.logs[2])
5457
}
58+
59+
// TestLockedLog make sure that Log and Logf which use stdout are thread-safe
60+
func TestLockedLog(t *testing.T) {
61+
// should not call t.Parallel() since we are modifying os.Stdout
62+
stdout := os.Stdout
63+
t.Cleanup(func() {
64+
os.Stdout = stdout
65+
})
66+
67+
data := []struct {
68+
name string
69+
fn func(*testing.T, string)
70+
}{
71+
{
72+
name: "Log",
73+
fn: func(t *testing.T, s string) {
74+
Log(t, s)
75+
}},
76+
{
77+
name: "Logf",
78+
fn: func(t *testing.T, s string) {
79+
Logf(t, "%s", s)
80+
}},
81+
}
82+
83+
for _, d := range data {
84+
mutexStdout.Lock()
85+
str := "Logging something" + t.Name()
86+
87+
r, w, _ := os.Pipe()
88+
os.Stdout = w
89+
ch := make(chan struct{})
90+
go func() {
91+
d.fn(t, str)
92+
w.Close()
93+
close(ch)
94+
}()
95+
96+
select {
97+
case <-ch:
98+
t.Error("Log should be locked")
99+
default:
100+
}
101+
102+
mutexStdout.Unlock()
103+
b, err := io.ReadAll(r)
104+
require.NoError(t, err, "log should be unlocked")
105+
assert.Contains(t, string(b), str, "should contains logged string")
106+
}
107+
108+
}

0 commit comments

Comments
 (0)