-
Notifications
You must be signed in to change notification settings - Fork 0
/
bdd.go
344 lines (277 loc) · 7.09 KB
/
bdd.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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
// Package bdd is a very simple but expressive BDD-flavoured testing framework
//
// Documentation is incomplete
// Refer to [bdd_test.go](bdd_test.go) for usage
//
// ## Rationale
//
// The most of present Golang BDD-style testing frameworks are:
// 1) too big => hard to maintain
// 2) overcomplicated => same here
// 3) bloated => goes against golang philosophy
// 4) unsupported
// 5) not enough expressive
// 6) provides less control of execution order
//
// This one is:
// 1) dead-simple
// 2) easy to maintain
// 3) more expressive
// 4) better control of test blocks execution order
//
// ## The Goal
//
// 1) Be minimalistic, yet expressive and fully functional
// 2) Stay within 500LOC
// 3) Be easily maintainable
// 4) Be compatible with all the popular ecosystem tools (without sacrificing the above)
//
//
// No matcher library provided; There are plenty of them.
// i.e.: [github.com/stretchr/testify/assert](https://github.com/stretchr/testify/tree/master/assert)
//
package bdd
import (
"encoding/json"
"fmt"
"hash/crc64"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
"unsafe"
"github.com/fatih/color"
)
/***************************************************************
** Printing Helpers
**/
// For debugging purposes; To print objects...
func jsonf(v interface{}) string {
if data, err := json.MarshalIndent(v, "", " "); err != nil {
panic(err)
} else {
return string(data)
}
}
func printService(s *scenario, msg ...interface{}) {
s.t.Helper()
s.t.Log(color.New(color.FgCyan).Sprint("<BDD> ", fmt.Sprint(msg...)))
}
func printScenario(s *scenario, msg ...interface{}) {
s.t.Helper()
s.t.Log(color.New(color.FgBlue).Sprint("<", s.runID, "/S> ", fmt.Sprint(msg...)))
}
func printAct(s *scenario, msg ...interface{}) {
s.t.Helper()
s.t.Log(color.New(color.FgYellow).Sprint("<", s.runID, "/A> ", fmt.Sprint(msg...)))
}
func printBranch(s *scenario, msg ...interface{}) {
s.t.Helper()
s.t.Log(color.New(color.FgHiYellow).Sprint("<", s.runID, "/B> ", fmt.Sprint(msg...)))
}
func printTest(s *scenario, msg ...interface{}) {
s.t.Helper()
var c = color.FgGreen
if s.t.Failed() {
c = color.FgRed
}
s.t.Log(color.New(c).Sprint("<", s.runID, "/T> ", fmt.Sprint(msg...)))
}
/***************************************************************
** Core Helpers
**/
var runCounter int64 = 0
func runID() string { return fmt.Sprint(atomic.AddInt64(&runCounter, 1)) }
var (
scenarios = map[uint64]*scenario{}
scenariosLock sync.RWMutex
)
func testID(t *testing.T) uint64 { return uint64(uintptr(unsafe.Pointer(t))) }
func getScenario(t *testing.T) *scenario {
scenariosLock.RLock()
defer scenariosLock.RUnlock()
return scenarios[testID(t)]
}
func sum(name []byte) string {
return fmt.Sprintf("%x", crc64.Checksum(name, crc64.MakeTable(crc64.ECMA)))
}
func branchID(forkFile string, forkLine uint64, branchName string) string {
return sum(append(
append(
[]byte(sum([]byte(forkFile))),
[]byte{
byte(0xff & forkLine),
byte(0xff & (forkLine >> 8)),
byte(0xff & (forkLine >> 16)),
byte(0xff & (forkLine >> 24)),
byte(0xff & (forkLine >> 32)),
byte(0xff & (forkLine >> 40)),
byte(0xff & (forkLine >> 48)),
byte(0xff & (forkLine >> 56)),
}...,
),
branchName...,
))
}
func addPath(path, node string) string { return fmt.Sprintf("%s/%s", path, node) }
func whereIsTheFork() (file string, line int) {
pc := [1]uintptr{}
runtime.Callers(4, pc[:])
f := runtime.FuncForPC(pc[0])
return f.FileLine(pc[0])
}
/***************************************************************
** Core implementation
**/
type tree struct {
id string
children map[string]*tree
}
type scenario struct {
t *testing.T
runID string
path string
tree map[string]bool
}
func (s *scenario) run(title string, fn ScenarioFunc) {
s.t.Helper()
L:
for {
printService(s, strings.Repeat("•", 64))
s.runID = runID()
s.path = ""
printScenario(s, title)
fn(s.t, s.runID)
{ //> Are there any unvisited paths?
thereIs := false
for _, v := range s.tree {
if !v {
thereIs = true
}
}
if !thereIs {
break L
}
}
}
}
func (s *scenario) visit(id string) bool {
var (
oldPath = s.path
nextPath = addPath(s.path, id)
)
res := map[string]bool{}
for p, v := range s.tree {
if strings.HasPrefix(p, nextPath) {
res[p] = v
}
}
if len(res) == 0 {
s.tree[nextPath] = true
s.path = nextPath
delete(s.tree, oldPath)
return true
}
for _, v := range res {
if !v {
s.tree[nextPath] = true
s.path = nextPath
delete(s.tree, oldPath)
return true
}
}
return false
}
func (s *scenario) blockAct(name string, fn BlockFunc) {
s.t.Helper()
printAct(s, name)
fn()
}
func (s *scenario) blockFork(branches []BranchConstructorFunc) {
s.t.Helper()
var (
oldPath = s.path
visited = false
forkFile, forkLine = whereIsTheFork()
)
for _, branchFn := range branches {
name, fn := branchFn()
// TODO: Check for duplicated names
id := branchID(forkFile, uint64(forkLine), name)
if visited {
s.tree[addPath(oldPath, id)] = false
} else if s.visit(id) {
visited = true
printBranch(s, name)
(func() {
defer func() {
s.path = strings.TrimSuffix(s.path, fmt.Sprintf("/%s", id))
}()
fn()
})()
}
}
}
func (s *scenario) blockTest(what string, fn BlockFunc) {
s.t.Helper()
printTest(s, what)
fn()
}
/***************************************************************
** Public API
**/
type BlockFunc func()
type ScenarioFunc func(t *testing.T, runID string)
// Scenario is a root of your scenario.
// Scenario blocks can not be nested.
func Scenario(t *testing.T, title string, fn ScenarioFunc) {
t.Helper()
scenario := &scenario{
t: t,
tree: map[string]bool{},
}
(func() {
scenariosLock.Lock()
defer scenariosLock.Unlock()
scenarios[testID(t)] = scenario
})()
defer (func() {
scenariosLock.Lock()
defer scenariosLock.Unlock()
delete(scenarios, testID(t))
})()
scenario.run(title, fn)
}
// Act is just a structural block; You use it to make your test structure
// more readable and explanatory
func Act(t *testing.T, name string, fn BlockFunc) {
t.Helper()
getScenario(t).blockAct(name, fn)
}
type BranchConstructorFunc func() (string, BlockFunc)
// Fork comes into a business when you want a separate sub-scenarios in your tests.
// For each Branch in a Fork the framework will reexecute all the blocks
// defined prior to the fork
//
// TODO: Rewrite this shame
func Fork(t *testing.T, branches ...BranchConstructorFunc) {
t.Helper()
getScenario(t).blockFork(branches)
}
// Branch is a branch of a Fork; That is.
// Branch block names should NOT have duplicates within same Fork
func Branch(name string, fn BlockFunc) BranchConstructorFunc {
return func() (string, BlockFunc) {
return name, fn
}
}
// Test is a block where you write your actual test code and run your assertions.
// Remember: you write ALL the logic in Test blocks,
// except for some error-free-by-default initializations
// Test blocks can be nested.
// Test blocks can NOT contain other block types.
func Test(t *testing.T, what string, fn BlockFunc) {
t.Helper()
getScenario(t).blockTest(what, fn)
}