-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmock.go
298 lines (250 loc) · 6.78 KB
/
mock.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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package bintest
import (
"bytes"
"errors"
"fmt"
"io"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
const (
InfiniteTimes = -1
)
// TestingT is an interface for *testing.T
type TestingT interface {
Logf(format string, args ...interface{})
Errorf(format string, args ...interface{})
}
// Mock provides a wrapper around a Proxy for testing
type Mock struct {
sync.Mutex
// Name of the binary
Name string
// Path to the bintest binary
Path string
// Actual invocations that occurred
invocations []Invocation
// The executions expected of the binary
expected ExpectationSet
// A list of middleware functions to call before invocation
before []func(i Invocation) error
// Whether to ignore unexpected calls
ignoreUnexpected bool
// The related proxy
proxy *Proxy
// A command to passthrough execution to
passthroughPath string
}
// NewMock builds a new Mock, or an error if the bintest fails to compile
func NewMock(path string) (*Mock, error) {
m := &Mock{}
proxy, err := CompileProxy(path)
if err != nil {
return nil, err
}
m.Name = strings.TrimSuffix(filepath.Base(proxy.Path), `.exe`)
m.Path = proxy.Path
m.proxy = proxy
go func() {
for call := range m.proxy.Ch {
m.invoke(call)
}
}()
return m, nil
}
func NewMockFromTestMain(path string) (*Mock, error) {
m := &Mock{}
proxy, err := LinkTestBinaryAsProxy(path)
if err != nil {
return nil, err
}
m.Name = filepath.Base(proxy.Path)
m.Path = proxy.Path
m.proxy = proxy
go func() {
for call := range m.proxy.Ch {
m.invoke(call)
}
}()
return m, nil
}
func (m *Mock) invoke(call *Call) {
m.Lock()
defer m.Unlock()
debugf("Handling invocation for %s %v", m.Name, call.Args[1:])
var invocation = Invocation{
Args: call.Args[1:],
Env: call.Env,
Dir: call.Dir,
}
// Before we execute any invocations, run the before funcs
for _, beforeFunc := range m.before {
if err := beforeFunc(invocation); err != nil {
fmt.Fprintf(call.Stderr, "\033[31m🚨 Error: %v\033[0m\n", err)
call.Exit(1)
return
}
}
result := m.expected.ForArguments(call.Args[1:]...)
expected, err := result.Match()
if err != nil {
debugf("No match found for expectation: %v", err)
m.invocations = append(m.invocations, invocation)
if m.ignoreUnexpected {
debugf("Exiting silently, ignoreUnexpected is set")
call.Exit(0)
} else if err == ErrNoExpectationsMatch {
fmt.Fprintf(call.Stderr, "\033[31m🚨 Error: %s\033[0m\n", result.ClosestMatch().Explain())
call.Exit(1)
} else {
fmt.Fprintf(call.Stderr, "\033[31m🚨 Error: %v\033[0m\n", err)
call.Exit(1)
}
return
}
debugf("Found expectation: %s", expected)
invocation.Expectation = expected
if expected.stdin != nil {
// read all of stdin
buf, err := io.ReadAll(call.Stdin)
if err != nil {
fmt.Fprintf(call.Stderr, "\033[31m🚨 Error reading stdin: %v\033[0m\n", err)
call.Exit(1)
}
// copy to Expectation
expected.readStdin = make([]byte, len(buf))
copy(expected.readStdin, buf)
// restore original stdin
call.Stdin = io.NopCloser(bytes.NewReader(buf))
}
if m.passthroughPath != "" {
call.PassthroughWithTimeout(m.passthroughPath, time.Second*10)
} else if expected.passthroughPath != "" {
call.PassthroughWithTimeout(expected.passthroughPath, time.Second*10)
} else if expected.callFunc != nil {
expected.callFunc(call)
} else {
_, _ = io.Copy(call.Stdout, expected.writeStdout)
_, _ = io.Copy(call.Stderr, expected.writeStderr)
call.Exit(expected.exitCode)
}
debugf("Incrementing total call of expected from %d to %d", expected.totalCalls, expected.totalCalls+1)
expected.totalCalls++
m.invocations = append(m.invocations, invocation)
}
// PassthroughToLocalCommand executes the mock name as a local command (looked up in PATH) and then passes
// the result as the result of the mock. Useful for assertions that commands happen, but where
// you want the command to actually be executed.
func (m *Mock) PassthroughToLocalCommand() *Mock {
m.Lock()
defer m.Unlock()
debugf("[mock] Looking up %s in path", m.Name)
path, err := exec.LookPath(m.Name)
if err != nil {
panic(err)
}
m.passthroughPath = path
return m
}
// IgnoreUnexpectedInvocations allows for invocations without matching call expectations
// to just silently return 0 and no output
func (m *Mock) IgnoreUnexpectedInvocations() *Mock {
m.Lock()
defer m.Unlock()
m.ignoreUnexpected = true
return m
}
// Before adds a middleware that is run before the Invocation is dispatched
func (m *Mock) Before(f func(i Invocation) error) *Mock {
m.Lock()
defer m.Unlock()
if m.before == nil {
m.before = []func(i Invocation) error{f}
} else {
m.before = append(m.before, f)
}
return m
}
// Expect creates an expectation that the mock will be called with the provided args
func (m *Mock) Expect(args ...interface{}) *Expectation {
m.Lock()
defer m.Unlock()
ex := &Expectation{
name: m.Name,
sequence: len(m.expected) + 1,
arguments: Arguments(args),
writeStderr: &bytes.Buffer{},
writeStdout: &bytes.Buffer{},
minCalls: 1,
maxCalls: 1,
passthroughPath: m.passthroughPath,
}
debugf("Creating expectation %s", ex)
m.expected = append(m.expected, ex)
return ex
}
// ExpectAll is a shortcut for adding lots of expectations
func (m *Mock) ExpectAll(argSlices [][]interface{}) {
for _, args := range argSlices {
m.Expect(args...)
}
}
// Check that all assertions are met and that there aren't invocations that don't match expectations
func (m *Mock) Check(t TestingT) bool {
m.Lock()
defer m.Unlock()
if len(m.expected) == 0 {
return true
}
var failedExpectations, unexpectedInvocations int
// first check that everything we expect
for _, expected := range m.expected {
if !expected.Check(t) {
failedExpectations++
}
}
if failedExpectations > 0 {
t.Errorf("Not all expectations were met (%d out of %d)",
len(m.expected)-failedExpectations,
len(m.expected))
}
// next check if we have invocations without expectations
if !m.ignoreUnexpected {
for _, invocation := range m.invocations {
if invocation.Expectation == nil {
t.Logf("Unexpected call to %s %s",
m.Name, FormatStrings(invocation.Args))
unexpectedInvocations++
}
}
if unexpectedInvocations > 0 {
t.Errorf("More invocations than expected (%d vs %d)",
unexpectedInvocations,
len(m.invocations))
}
}
return unexpectedInvocations == 0 && failedExpectations == 0
}
func (m *Mock) CheckAndClose(t TestingT) error {
if err := m.proxy.Close(); err != nil {
return err
}
if !m.Check(t) {
return errors.New("Assertion checks failed")
}
return nil
}
func (m *Mock) Close() error {
debugf("Closing mock")
return m.proxy.Close()
}
// Invocation is a call to the binary
type Invocation struct {
Args []string
Env []string
Dir string
Expectation *Expectation
}