-
Notifications
You must be signed in to change notification settings - Fork 9.5k
/
testing.go
191 lines (175 loc) · 6.2 KB
/
testing.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package terminal
import (
"fmt"
"io"
"os"
"strings"
"sync"
"testing"
)
// StreamsForTesting is a helper for test code that is aiming to test functions
// that interact with the input and output streams.
//
// This particular function is for the simple case of a function that only
// produces output: the returned input stream is connected to the system's
// "null device", as if a user had run Terraform with I/O redirection like
// </dev/null on Unix. It also configures the output as a pipe rather than
// as a terminal, and so can't be used to test whether code is able to adapt
// to different terminal widths.
//
// The return values are a Streams object ready to pass into a function under
// test, and a callback function for the test itself to call afterwards
// in order to obtain any characters that were written to the streams. Once
// you call the close function, the Streams object becomes invalid and must
// not be used anymore. Any caller of this function _must_ call close before
// its test concludes, even if it doesn't intend to check the output, or else
// it will leak resources.
//
// Since this function is for testing only, for convenience it will react to
// any setup errors by logging a message to the given testing.T object and
// then failing the test, preventing any later code from running.
func StreamsForTesting(t *testing.T) (streams *Streams, close func(*testing.T) *TestOutput) {
stdinR, err := os.Open(os.DevNull)
if err != nil {
t.Fatalf("failed to open /dev/null to represent stdin: %s", err)
}
// (Although we only have StreamsForTesting right now, it seems plausible
// that we'll want some other similar helpers for more complicated
// situations, such as codepaths that need to read from Stdin or
// tests for whether a function responds properly to terminal width.
// In that case, we'd probably want to factor out the core guts of this
// which set up the pipe *os.File values and the goroutines, but then
// let each caller produce its own Streams wrapping around those. For
// now though, it's simpler to just have this whole implementation together
// in one function.)
// Our idea of streams is only a very thin wrapper around OS-level file
// descriptors, so in order to produce a realistic implementation for
// the code under test while still allowing us to capture the output
// we'll OS-level pipes and concurrently copy anything we read from
// them into the output object.
outp := &TestOutput{}
var lock sync.Mutex // hold while appending to outp
stdoutR, stdoutW, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create stdout pipe: %s", err)
}
stderrR, stderrW, err := os.Pipe()
if err != nil {
t.Fatalf("failed to create stderr pipe: %s", err)
}
var wg sync.WaitGroup // for waiting until our goroutines have exited
// We need an extra goroutine for each of the pipes so we can block
// on reading both of them alongside the caller hopefully writing to
// the write sides.
wg.Add(2)
consume := func(r *os.File, isErr bool) {
var buf [1024]byte
for {
n, err := r.Read(buf[:])
if err != nil {
if err != io.EOF {
// We aren't allowed to write to the testing.T from
// a different goroutine than it was created on, but
// encountering other errors would be weird here anyway
// so we'll just panic. (If we were to just ignore this
// and then drop out of the loop then we might deadlock
// anyone still trying to write to the write end.)
panic(fmt.Sprintf("failed to read from pipe: %s", err))
}
break
}
lock.Lock()
outp.parts = append(outp.parts, testOutputPart{
isErr: isErr,
bytes: append(([]byte)(nil), buf[:n]...), // copy so we can reuse the buffer
})
lock.Unlock()
}
wg.Done()
}
go consume(stdoutR, false)
go consume(stderrR, true)
close = func(t *testing.T) *TestOutput {
err := stdinR.Close()
if err != nil {
t.Errorf("failed to close stdin handle: %s", err)
}
// We'll close both of the writer streams now, which should in turn
// cause both of the "consume" goroutines above to terminate by
// encountering io.EOF.
err = stdoutW.Close()
if err != nil {
t.Errorf("failed to close stdout pipe: %s", err)
}
err = stderrW.Close()
if err != nil {
t.Errorf("failed to close stderr pipe: %s", err)
}
// The above error cases still allow this to complete and thus
// potentially allow the test to report its own result, but will
// ensure that the test doesn't pass while also leaking resources.
// Wait for the stream-copying goroutines to finish anything they
// are working on before we return, or else we might miss some
// late-arriving writes.
wg.Wait()
return outp
}
return &Streams{
Stdout: &OutputStream{
File: stdoutW,
},
Stderr: &OutputStream{
File: stderrW,
},
Stdin: &InputStream{
File: stdinR,
},
}, close
}
// TestOutput is a type used to return the results from the various stream
// testing helpers. It encapsulates any captured writes to the output and
// error streams, and has methods to consume that data in some different ways
// to allow for a few different styles of testing.
type TestOutput struct {
parts []testOutputPart
}
type testOutputPart struct {
// isErr is true if this part was written to the error stream, or false
// if it was written to the output stream.
isErr bool
// bytes are the raw bytes that were written
bytes []byte
}
// All returns the output written to both the Stdout and Stderr streams,
// interleaved together in the order of writing in a single string.
func (o TestOutput) All() string {
buf := &strings.Builder{}
for _, part := range o.parts {
buf.Write(part.bytes)
}
return buf.String()
}
// Stdout returns the output written to just the Stdout stream, ignoring
// anything that was written to the Stderr stream.
func (o TestOutput) Stdout() string {
buf := &strings.Builder{}
for _, part := range o.parts {
if part.isErr {
continue
}
buf.Write(part.bytes)
}
return buf.String()
}
// Stderr returns the output written to just the Stderr stream, ignoring
// anything that was written to the Stdout stream.
func (o TestOutput) Stderr() string {
buf := &strings.Builder{}
for _, part := range o.parts {
if !part.isErr {
continue
}
buf.Write(part.bytes)
}
return buf.String()
}