Codebase list golang-gomega / 0e57ecff-9a98-4d9a-8caa-90875aa9ad27/main
[ Arnaud Rebillout ] [ Debian Janitor ] New upstream snapshot. Debian Janitor 2 years ago
97 changed file(s) with 8158 addition(s) and 2100 deletion(s). Raw diff Collapse all Expand all
0 github: [onsi]
0 version: 2
1 updates:
2 - package-ecosystem: gomod
3 directory: "/"
4 schedule:
5 interval: daily
6 time: '01:00'
7 open-pull-requests-limit: 5
8 - package-ecosystem: github-actions
9 directory: "/"
10 schedule:
11 interval: daily
12 time: '01:00'
13 open-pull-requests-limit: 5
0 # For most projects, this workflow file will not need changing; you simply need
1 # to commit it to your repository.
2 #
3 # You may wish to alter this file to override the set of languages analyzed,
4 # or to provide custom queries or build logic.
5 #
6 # ******** NOTE ********
7 # We have attempted to detect the languages in your repository. Please check
8 # the `language` matrix defined below to confirm you have the correct set of
9 # supported CodeQL languages.
10 #
11 name: "CodeQL"
12
13 on:
14 push:
15 branches: [ master ]
16 pull_request:
17 # The branches below must be a subset of the branches above
18 branches: [ master ]
19 schedule:
20 - cron: '39 17 * * 3'
21
22 jobs:
23 analyze:
24 name: Analyze
25 runs-on: ubuntu-latest
26
27 strategy:
28 fail-fast: false
29 matrix:
30 language: [ 'go' ]
31 # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
32 # Learn more:
33 # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
34
35 steps:
36 - name: Checkout repository
37 uses: actions/checkout@v2
38
39 # Initializes the CodeQL tools for scanning.
40 - name: Initialize CodeQL
41 uses: github/codeql-action/init@v1
42 with:
43 languages: ${{ matrix.language }}
44 # If you wish to specify custom queries, you can do so here or in a config file.
45 # By default, queries listed here will override any specified in a config file.
46 # Prefix the list here with "+" to use these queries and those in the config file.
47 # queries: ./path/to/local/query, your-org/your-repo/queries@main
48
49 # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
50 # If this step fails, then you should remove it and run the build manually (see below)
51 - name: Autobuild
52 uses: github/codeql-action/autobuild@v1
53
54 # ℹ️ Command-line programs to run using the OS shell.
55 # 📚 https://git.io/JvXDl
56
57 # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
58 # and modify them (or add more) to build your code if your project
59 # uses a compiled language
60
61 #- run: |
62 # make bootstrap
63 # make release
64
65 - name: Perform CodeQL Analysis
66 uses: github/codeql-action/analyze@v1
0 name: test
1
2 on: [push, pull_request]
3
4 jobs:
5 test:
6 runs-on: ubuntu-latest
7 strategy:
8 matrix:
9 version: [ '1.16', '1.17' ]
10 name: Go ${{ matrix.version }}
11 steps:
12 - uses: actions/setup-go@v2
13 with:
14 go-version: ${{ matrix.version }}
15 - uses: actions/checkout@v2
16 - uses: actions/cache@v2
17 with:
18 path: |
19 ~/.cache/go-build
20 ~/go/pkg/mod
21 key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
22 restore-keys: |
23 ${{ runner.os }}-go-
24 - run: make ginkgo
25 lint:
26 runs-on: ubuntu-latest
27 steps:
28 - uses: actions/setup-go@v2
29 with:
30 go-version: '1.16'
31 - uses: actions/checkout@v2
32 - uses: actions/cache@v2
33 with:
34 path: |
35 ~/.cache/go-build
36 ~/go/pkg/mod
37 key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
38 restore-keys: |
39 ${{ runner.os }}-go-
40 - run: make download
41 - run: make fmt
42 - run: make vet
00 language: go
1 arch:
2 - amd64
3 - ppc64le
14
25 go:
3 - 1.14.x
6 - gotip
7 - 1.16.x
48 - 1.15.x
5 - gotip
69
710 env:
811 - GO111MODULE=on
912
10 install:
11 - go get -v ./...
12 - go build ./...
13 - go get github.com/onsi/ginkgo
14 - go install github.com/onsi/ginkgo/ginkgo
13 install: skip
1514
16 script: make test
15 script:
16 - go mod tidy && git diff --exit-code go.mod go.sum
17 - make test
0 ## 1.17.0
1
2 ### Features
3 - Add HaveField matcher [3a26311]
4 - add Error() assertions on the final error value of multi-return values (#480) [2f96943]
5 - separate out offsets and timeouts (#478) [18a4723]
6 - fix transformation error reporting (#479) [e001fab]
7 - allow transform functions to report errors (#472) [bf93408]
8
9 ### Fixes
10 Stop using deprecated ioutil package (#467) [07f405d]
11
12 ## 1.16.0
13
14 ### Features
15 - feat: HaveHTTPStatus multiple expected values (#465) [aa69f1b]
16 - feat: HaveHTTPHeaderWithValue() matcher (#463) [dd83a96]
17 - feat: HaveHTTPBody matcher (#462) [504e1f2]
18 - feat: formatter for HTTP responses (#461) [e5b3157]
19
20 ## 1.15.0
21
22 ### Fixes
23 The previous version (1.14.0) introduced a change to allow `Eventually` and `Consistently` to support functions that make assertions. This was accomplished by overriding the global fail handler when running the callbacks passed to `Eventually/Consistently` in order to capture any resulting errors. Issue #457 uncovered a flaw with this approach: when multiple `Eventually`s are running concurrently they race when overriding the singleton global fail handler.
24
25 1.15.0 resolves this by requiring users who want to make assertions in `Eventually/Consistently` call backs to explicitly pass in a function that takes a `Gomega` as an argument. The passed-in `Gomega` instance can be used to make assertions. Any failures will cause `Eventually` to retry the callback. This cleaner interface avoids the issue of swapping out globals but comes at the cost of changing the contract introduced in v1.14.0. As such 1.15.0 introduces a breaking change with respect to 1.14.0 - however we expect that adoption of this feature in 1.14.0 remains limited.
26
27 In addition, 1.15.0 cleans up some of Gomega's internals. Most users shouldn't notice any differences stemming from the refactoring that was made.
28
29 ## 1.14.0
30
31 ### Features
32 - gmeasure.SamplingConfig now suppers a MinSamplingInterval [e94dbca]
33 - Eventually and Consistently support functions that make assertions [2f04e6e]
34 - Eventually and Consistently now allow their passed-in functions to make assertions.
35 These assertions must pass or the function is considered to have failed and is retried.
36 - Eventually and Consistently can now take functions with no return values. These implicitly return nil
37 if they contain no failed assertion. Otherwise they return an error wrapping the first assertion failure. This allows
38 these functions to be used with the Succeed() matcher.
39 - Introduce InterceptGomegaFailure - an analogue to InterceptGomegaFailures - that captures the first assertion failure
40 and halts execution in its passed-in callback.
41
42 ### Fixes
43 - Call Verify GHTTPWithGomega receiver funcs (#454) [496e6fd]
44 - Build a binary with an expected name (#446) [7356360]
45
46 ## 1.13.0
47
48 ### Features
49 - gmeasure provides BETA support for benchmarking (#447) [8f2dfbf]
50 - Set consistently and eventually defaults on init (#443) [12eb778]
51
52 ## 1.12.0
53
54 ### Features
55 - Add Satisfy() matcher (#437) [c548f31]
56 - tweak truncation message [3360b8c]
57 - Add format.GomegaStringer (#427) [cc80b6f]
58 - Add Clear() method to gbytes.Buffer [c3c0920]
59
60 ### Fixes
61 - Fix error message in BeNumericallyMatcher (#432) [09c074a]
62 - Bump github.com/onsi/ginkgo from 1.12.1 to 1.16.2 (#442) [e5f6ea0]
63 - Bump github.com/golang/protobuf from 1.4.3 to 1.5.2 (#431) [adae3bf]
64 - Bump golang.org/x/net (#441) [3275b35]
65
66 ## 1.11.0
67
68 ### Features
69 - feature: add index to gstruct element func (#419) [334e00d]
70 - feat(gexec) Add CompileTest functions. Close #410 (#411) [47c613f]
71
72 ### Fixes
73 - Check more carefully for nils in WithTransform (#423) [3c60a15]
74 - fix: typo in Makefile [b82522a]
75 - Allow WithTransform function to accept a nil value (#422) [b75d2f2]
76 - fix: print value type for interface{} containers (#409) [f08e2dc]
77 - fix(BeElementOf): consistently flatten expected values [1fa9468]
78
79 ## 1.10.5
80
81 ### Fixes
82 - fix: collections matchers should display type of expectation (#408) [6b4eb5a]
83 - fix(ContainElements): consistently flatten expected values [073b880]
84 - fix(ConsistOf): consistently flatten expected values [7266efe]
85
86 ## 1.10.4
87
88 ### Fixes
89 - update golang net library to more recent version without vulnerability (#406) [817a8b9]
90 - Correct spelling: alloted -> allotted (#403) [0bae715]
91 - fix a panic in MessageWithDiff with long message (#402) [ea06b9b]
92
093 ## 1.10.3
194
295 ### Fixes
0 FROM golang:1.15
0 test:
1 [ -z "`gofmt -s -w -l -e .`" ]
2 go vet
3 ginkgo -p -r --randomizeAllSpecs --failOnPending --randomizeSuites --race
0 ###### Help ###################################################################
41
5 .PHONY: test
2 .DEFAULT_GOAL = help
3
4 .PHONY: help
5
6 help: ## list Makefile targets
7 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
8
9 ###### Targets ################################################################
10
11 test: version download fmt vet ginkgo ## Runs all build, static analysis, and test steps
12
13 download: ## Download dependencies
14 go mod download
15
16 vet: ## Run static code analysis
17 go vet ./...
18
19 ginkgo: ## Run tests using Ginkgo
20 go run github.com/onsi/ginkgo/ginkgo -p -r --randomizeAllSpecs --failOnPending --randomizeSuites --race
21
22 fmt: ## Checks that the code is formatted correcty
23 @@if [ -n "$$(gofmt -s -e -l -d .)" ]; then \
24 echo "gofmt check failed: run 'gofmt -s -e -l -w .'"; \
25 exit 1; \
26 fi
27
28 docker_test: ## Run tests in a container via docker-compose
29 docker-compose build test && docker-compose run --rm test make test
30
31 version: ## Display the version of Go
32 @@go version
00 ![Gomega: Ginkgo's Preferred Matcher Library](http://onsi.github.io/gomega/images/gomega.png)
11
2 [![Build Status](https://travis-ci.org/onsi/gomega.svg?branch=master)](https://travis-ci.org/onsi/gomega)
2 [![test](https://github.com/onsi/gomega/actions/workflows/test.yml/badge.svg)](https://github.com/onsi/gomega/actions/workflows/test.yml)
33
44 Jump straight to the [docs](http://onsi.github.io/gomega/) to learn about Gomega, including a list of [all available matchers](http://onsi.github.io/gomega/#provided-matchers).
55
0 golang-gomega (1.10.3-2) UNRELEASED; urgency=medium
0 golang-gomega (1.17.0+git20211115.1.360db9d-1) UNRELEASED; urgency=medium
11
2 [ Arnaud Rebillout ]
23 * Team upload.
34 * Rename package to golang-github-onsi-gomega-dev.
45 * Update copyright for 'matchers/support/goraph'.
56 * Bump debhelper compat to 13.
67
7 -- Arnaud Rebillout <elboulangero@gmail.com> Wed, 25 Nov 2020 10:15:02 +0700
8 [ Debian Janitor ]
9 * New upstream snapshot.
10
11 -- Arnaud Rebillout <elboulangero@gmail.com> Mon, 15 Nov 2021 23:24:44 -0000
812
913 golang-gomega (1.10.3-1) unstable; urgency=medium
1014
0 version: '3.0'
1
2 services:
3 test:
4 build:
5 dockerfile: Dockerfile
6 context: .
7 working_dir: /app
8 volumes:
9 - ${PWD}:/app
66 package format
77
88 import (
9 "context"
910 "fmt"
1011 "reflect"
1112 "strconv"
1617 // Use MaxDepth to set the maximum recursion depth when printing deeply nested objects
1718 var MaxDepth = uint(10)
1819
20 // MaxLength of the string representation of an object.
21 // If MaxLength is set to 0, the Object will not be truncated.
22 var MaxLength = 4000
23
1924 /*
2025 By default, all objects (even those that implement fmt.Stringer and fmt.GoStringer) are recursively inspected to generate output.
2126
4348 // after the first diff location in a truncated string assertion error message.
4449 var CharactersAroundMismatchToInclude uint = 5
4550
46 // Ctx interface defined here to keep backwards compatibility with go < 1.7
47 // It matches the context.Context interface
48 type Ctx interface {
49 Deadline() (deadline time.Time, ok bool)
50 Done() <-chan struct{}
51 Err() error
52 Value(key interface{}) interface{}
53 }
54
55 var contextType = reflect.TypeOf((*Ctx)(nil)).Elem()
51 var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
5652 var timeType = reflect.TypeOf(time.Time{})
5753
5854 //The default indentation string emitted by the format package
5955 var Indent = " "
6056
6157 var longFormThreshold = 20
58
59 // GomegaStringer allows for custom formating of objects for gomega.
60 type GomegaStringer interface {
61 // GomegaString will be used to custom format an object.
62 // It does not follow UseStringerRepresentation value and will always be called regardless.
63 // It also ignores the MaxLength value.
64 GomegaString() string
65 }
6266
6367 /*
6468 Generates a formatted matcher success/failure message of the form:
104108
105109 tabLength := 4
106110 spaceFromMessageToActual := tabLength + len("<string>: ") - len(message)
107 padding := strings.Repeat(" ", spaceFromMessageToActual+spacesBeforeFormattedMismatch) + "|"
111
112 paddingCount := spaceFromMessageToActual + spacesBeforeFormattedMismatch
113 if paddingCount < 0 {
114 return Message(formattedActual, message, formattedExpected)
115 }
116
117 padding := strings.Repeat(" ", paddingCount) + "|"
108118 return Message(formattedActual, message+padding, formattedExpected)
109119 }
110120
160170 return 0
161171 }
162172
173 const truncateHelpText = `
174 Gomega truncated this representation as it exceeds 'format.MaxLength'.
175 Consider having the object provide a custom 'GomegaStringer' representation
176 or adjust the parameters in Gomega's 'format' package.
177
178 Learn more here: https://onsi.github.io/gomega/#adjusting-output
179 `
180
181 func truncateLongStrings(s string) string {
182 if MaxLength > 0 && len(s) > MaxLength {
183 var sb strings.Builder
184 for i, r := range s {
185 if i < MaxLength {
186 sb.WriteRune(r)
187 continue
188 }
189 break
190 }
191
192 sb.WriteString("...\n")
193 sb.WriteString(truncateHelpText)
194
195 return sb.String()
196 }
197 return s
198 }
199
163200 /*
164201 Pretty prints the passed in object at the passed in indentation level.
165202
174211 func Object(object interface{}, indentation uint) string {
175212 indent := strings.Repeat(Indent, int(indentation))
176213 value := reflect.ValueOf(object)
177 return fmt.Sprintf("%s<%s>: %s", indent, formatType(object), formatValue(value, indentation))
214 return fmt.Sprintf("%s<%s>: %s", indent, formatType(value), formatValue(value, indentation))
178215 }
179216
180217 /*
194231 return result
195232 }
196233
197 func formatType(object interface{}) string {
198 t := reflect.TypeOf(object)
199 if t == nil {
234 func formatType(v reflect.Value) string {
235 switch v.Kind() {
236 case reflect.Invalid:
200237 return "nil"
201 }
202 switch t.Kind() {
203238 case reflect.Chan:
204 v := reflect.ValueOf(object)
205 return fmt.Sprintf("%T | len:%d, cap:%d", object, v.Len(), v.Cap())
239 return fmt.Sprintf("%s | len:%d, cap:%d", v.Type(), v.Len(), v.Cap())
206240 case reflect.Ptr:
207 return fmt.Sprintf("%T | %p", object, object)
241 return fmt.Sprintf("%s | 0x%x", v.Type(), v.Pointer())
208242 case reflect.Slice:
209 v := reflect.ValueOf(object)
210 return fmt.Sprintf("%T | len:%d, cap:%d", object, v.Len(), v.Cap())
243 return fmt.Sprintf("%s | len:%d, cap:%d", v.Type(), v.Len(), v.Cap())
211244 case reflect.Map:
212 v := reflect.ValueOf(object)
213 return fmt.Sprintf("%T | len:%d", object, v.Len())
245 return fmt.Sprintf("%s | len:%d", v.Type(), v.Len())
214246 default:
215 return fmt.Sprintf("%T", object)
247 return fmt.Sprintf("%s", v.Type())
216248 }
217249 }
218250
225257 return "nil"
226258 }
227259
228 if UseStringerRepresentation {
229 if value.CanInterface() {
230 obj := value.Interface()
260 if value.CanInterface() {
261 obj := value.Interface()
262
263 // GomegaStringer will take precedence to other representations and disregards UseStringerRepresentation
264 if x, ok := obj.(GomegaStringer); ok {
265 // do not truncate a user-defined GoMegaString() value
266 return x.GomegaString()
267 }
268
269 if UseStringerRepresentation {
231270 switch x := obj.(type) {
232271 case fmt.GoStringer:
233 return x.GoString()
272 return truncateLongStrings(x.GoString())
234273 case fmt.Stringer:
235 return x.String()
274 return truncateLongStrings(x.String())
236275 }
237276 }
238277 }
263302 case reflect.Ptr:
264303 return formatValue(value.Elem(), indentation)
265304 case reflect.Slice:
266 return formatSlice(value, indentation)
305 return truncateLongStrings(formatSlice(value, indentation))
267306 case reflect.String:
268 return formatString(value.String(), indentation)
307 return truncateLongStrings(formatString(value.String(), indentation))
269308 case reflect.Array:
270 return formatSlice(value, indentation)
309 return truncateLongStrings(formatSlice(value, indentation))
271310 case reflect.Map:
272 return formatMap(value, indentation)
311 return truncateLongStrings(formatMap(value, indentation))
273312 case reflect.Struct:
274313 if value.Type() == timeType && value.CanInterface() {
275314 t, _ := value.Interface().(time.Time)
276315 return t.Format(time.RFC3339Nano)
277316 }
278 return formatStruct(value, indentation)
317 return truncateLongStrings(formatStruct(value, indentation))
279318 case reflect.Interface:
280 return formatValue(value.Elem(), indentation)
319 return formatInterface(value, indentation)
281320 default:
282321 if value.CanInterface() {
283 return fmt.Sprintf("%#v", value.Interface())
284 }
285 return fmt.Sprintf("%#v", value)
322 return truncateLongStrings(fmt.Sprintf("%#v", value.Interface()))
323 }
324 return truncateLongStrings(fmt.Sprintf("%#v", value))
286325 }
287326 }
288327
372411 return fmt.Sprintf("{%s}", strings.Join(result, ", "))
373412 }
374413
414 func formatInterface(v reflect.Value, indentation uint) string {
415 return fmt.Sprintf("<%s>%s", formatType(v.Elem()), formatValue(v.Elem(), indentation))
416 }
417
375418 func isNilValue(a reflect.Value) bool {
376419 switch a.Kind() {
377420 case reflect.Invalid:
00 package format_test
11
22 import (
3 "context"
34 "fmt"
45 "strings"
56 "time"
7273 return "string"
7374 }
7475
75 type ctx struct {
76 }
77
78 func (c *ctx) Deadline() (deadline time.Time, ok bool) {
79 return time.Time{}, false
80 }
81
82 func (c *ctx) Done() <-chan struct{} {
83 return nil
84 }
85
86 func (c *ctx) Err() error {
87 return nil
88 }
89
90 func (c *ctx) Value(key interface{}) interface{} {
91 return nil
76 type gomegaStringer struct {
77 }
78
79 func (g gomegaStringer) GomegaString() string {
80 return "gomegastring"
81 }
82
83 type gomegaStringerLong struct {
84 }
85
86 func (g gomegaStringerLong) GomegaString() string {
87 return strings.Repeat("s", MaxLength*2)
9288 }
9389
9490 var _ = Describe("Format", func() {
112108 for i := range arr {
113109 arr[i] = entriesSwitch
114110 }
115 return "{" + strings.Join(arr, ", ") + "}"
111 return "{\\s*" + strings.Join(arr, ",\\s* ") + ",?\\s*}"
116112 }
117113
118114 Describe("Message", func() {
119115 Context("with only an actual value", func() {
116 BeforeEach(func() {
117 MaxLength = 4000
118 })
119
120120 It("should print out an indented formatted representation of the value and the message", func() {
121121 Expect(Message(3, "to be three.")).Should(Equal("Expected\n <int>: 3\nto be three."))
122 })
123
124 It("should print out an indented formatted representation of the value and the message, and trucate it when too long", func() {
125 tooLong := strings.Repeat("s", MaxLength+1)
126 tooLongResult := strings.Repeat("s", MaxLength) + "...\n" + TruncatedHelpText()
127 Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLongResult + "\nto be truncated"))
128 })
129
130 It("should print out an indented formatted representation of the value and the message, and not trucate it when MaxLength = 0", func() {
131 MaxLength = 0
132 tooLong := strings.Repeat("s", MaxLength+1)
133 Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLong + "\nto be truncated"))
122134 })
123135 })
124136
171183 stringB := "something_else"
172184
173185 Expect(MessageWithDiff(stringA, "to equal", stringB)).Should(Equal(expectedSpecialCharacterFailureMessage))
186 })
187
188 It("handles negative padding length", func() {
189 stringWithB := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
190 stringWithZ := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
191 longMessage := "to equal very long message"
192
193 Expect(MessageWithDiff(stringWithB, longMessage, stringWithZ)).Should(Equal(expectedDiffLongMessage))
174194 })
175195
176196 Context("With truncated diff disabled", func() {
535555 })
536556 })
537557
558 Describe("formatting nested interface{} types", func() {
559 It("should print out the types of the container and value", func() {
560 Expect(Object([]interface{}{"foo"}, 1)).
561 To(match("[]interface {} | len:1, cap:1", `[<string>"foo"]`))
562
563 Expect(Object(map[string]interface{}{"foo": true}, 1)).
564 To(match("map[string]interface {} | len:1", `{"foo": <bool>true}`))
565
566 Expect(Object(struct{ A interface{} }{A: 1}, 1)).
567 To(match("struct { A interface {} }", "{A: <int>1}"))
568
569 v := struct{ A interface{} }{A: struct{ B string }{B: "foo"}}
570 Expect(Object(v, 1)).To(match(`struct { A interface {} }`, `{
571 A: <struct { B string }>{B: "foo"},
572 }`))
573 })
574 })
575
538576 Describe("formatting times", func() {
539577 It("should format time as RFC3339", func() {
540578 t := time.Date(2016, 10, 31, 9, 57, 23, 12345, time.UTC)
583621 byteArrValue: \[17, 20, 32\],
584622 mapValue: %s,
585623 structValue: {Exported: "exported"},
586 interfaceValue: {"a key": 17},
624 interfaceValue: <map\[string\]int \| len:1>{"a key": 17},
587625 }`, s.chanValue, s.funcValue, hashMatchingRegexp(`"a key": 20`, `"b key": 30`))
588626
589627 Expect(Object(s, 1)).Should(matchRegexp(`format_test\.SecretiveStruct`, expected))
599637 outerHash["integer"] = 2
600638 outerHash["map"] = innerHash
601639
602 expected := hashMatchingRegexp(`"integer": 2`, `"map": {"inner": 3}`)
640 expected := hashMatchingRegexp(`"integer": <int>2`, `"map": <map\[string\]int \| len:1>{"inner": 3}`)
603641 Expect(Object(outerHash, 1)).Should(matchRegexp(`map\[string\]interface {} \| len:2`, expected))
604642 })
605643 })
645683 Expect(Object(Stringer{}, 1)).Should(ContainSubstring("<format_test.Stringer>: string"))
646684 })
647685 })
686
687 When("passed a GomegaStringer", func() {
688 It("should use what GomegaString() returns", func() {
689 Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
690 UseStringerRepresentation = false
691 Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
692 })
693
694 It("should use what GomegaString() returns, disregarding MaxLength", func() {
695 Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
696 UseStringerRepresentation = false
697 Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
698 })
699 })
648700 })
649701
650702 Describe("Printing a context.Context field", func() {
651703
652704 type structWithContext struct {
653 Context Ctx
705 Context context.Context
654706 Value string
655707 }
656708
657 context := ctx{}
658 objWithContext := structWithContext{Value: "some-value", Context: &context}
709 objWithContext := structWithContext{Value: "some-value", Context: context.TODO()}
659710
660711 It("Suppresses the content by default", func() {
661712 Expect(Object(objWithContext, 1)).Should(ContainSubstring("<suppressed context>"))
662713 })
663714
664 It("Doesn't supress the context if it's the object being printed", func() {
665 Expect(Object(context, 1)).ShouldNot(MatchRegexp("^.*<suppressed context>$"))
715 It("Doesn't suppress the context if it's the object being printed", func() {
716 Expect(Object(context.TODO(), 1)).ShouldNot(MatchRegexp("^.*<suppressed context>$"))
666717 })
667718
668719 Context("PrintContextObjects is set", func() {
760811 to equal |
761812 <string>: "...z..."
762813 `)
814
815 var expectedDiffLongMessage = strings.TrimSpace(`
816 Expected
817 <string>: "...aaaaabaaaaa..."
818 to equal very long message
819 <string>: "...aaaaazaaaaa..."
820 `)
0 /*
1 Gomega's format test helper package.
2 */
3
4 package format
5
6 // TruncateHelpText returns truncateHelpText.
7 // This function is only accessible during tests.
8 func TruncatedHelpText() string {
9 return truncateHelpText
10 }
105105 b.readCursor += uint64(n)
106106
107107 return n, nil
108 }
109
110 /*
111 Clear clears out the buffer's contents
112 */
113 func (b *Buffer) Clear() error {
114 b.lock.Lock()
115 defer b.lock.Unlock()
116
117 if b.closed {
118 return errors.New("attempt to clear closed buffer")
119 }
120
121 b.contents = []byte{}
122 b.readCursor = 0
123 return nil
108124 }
109125
110126 /*
180180 })
181181 })
182182
183 Describe("clearing the buffer", func() {
184 It("should clear out the contents of the buffer", func() {
185 buffer.Write([]byte("abc"))
186 Expect(buffer).To(Say("ab"))
187 Expect(buffer.Clear()).To(Succeed())
188 Expect(buffer).NotTo(Say("c"))
189 Expect(buffer.Contents()).To(BeEmpty())
190 buffer.Write([]byte("123"))
191 Expect(buffer).To(Say("123"))
192 Expect(buffer.Contents()).To(Equal([]byte("123")))
193 })
194
195 It("should error when the buffer is closed", func() {
196 buffer.Write([]byte("abc"))
197 buffer.Close()
198 err := buffer.Clear()
199 Expect(err).To(HaveOccurred())
200 })
201 })
202
183203 Describe("closing the buffer", func() {
184204 It("should error when further write attempts are made", func() {
185205 _, err := buffer.Write([]byte("abc"))
88 // ErrTimeout is returned by TimeoutCloser, TimeoutReader, and TimeoutWriter when the underlying Closer/Reader/Writer does not return within the specified timeout
99 var ErrTimeout = errors.New("timeout occurred")
1010
11 // TimeoutCloser returns an io.Closer that wraps the passed-in io.Closer. If the underlying Closer fails to close within the alloted timeout ErrTimeout is returned.
11 // TimeoutCloser returns an io.Closer that wraps the passed-in io.Closer. If the underlying Closer fails to close within the allotted timeout ErrTimeout is returned.
1212 func TimeoutCloser(c io.Closer, timeout time.Duration) io.Closer {
1313 return timeoutReaderWriterCloser{c: c, d: timeout}
1414 }
1515
16 // TimeoutReader returns an io.Reader that wraps the passed-in io.Reader. If the underlying Reader fails to read within the alloted timeout ErrTimeout is returned.
16 // TimeoutReader returns an io.Reader that wraps the passed-in io.Reader. If the underlying Reader fails to read within the allotted timeout ErrTimeout is returned.
1717 func TimeoutReader(r io.Reader, timeout time.Duration) io.Reader {
1818 return timeoutReaderWriterCloser{r: r, d: timeout}
1919 }
2020
21 // TimeoutWriter returns an io.Writer that wraps the passed-in io.Writer. If the underlying Writer fails to write within the alloted timeout ErrTimeout is returned.
21 // TimeoutWriter returns an io.Writer that wraps the passed-in io.Writer. If the underlying Writer fails to write within the allotted timeout ErrTimeout is returned.
2222 func TimeoutWriter(w io.Writer, timeout time.Duration) io.Writer {
2323 return timeoutReaderWriterCloser{w: w, d: timeout}
2424 }
0 package main_test
1
2 import "testing"
3
4 func Test(t *testing.T) {
5 t.Log("Hum, it seems okay.")
6 }
55 "errors"
66 "fmt"
77 "go/build"
8 "io/ioutil"
98 "os"
109 "os/exec"
1110 "path"
4544 return doBuild(gopath, packagePath, nil, args...)
4645 }
4746
47 func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
48 executable, err := newExecutablePath(gopath, packagePath)
49 if err != nil {
50 return "", err
51 }
52
53 cmdArgs := append([]string{"build"}, args...)
54 cmdArgs = append(cmdArgs, "-o", executable, packagePath)
55
56 build := exec.Command("go", cmdArgs...)
57 build.Env = replaceGoPath(os.Environ(), gopath)
58 build.Env = append(build.Env, env...)
59
60 output, err := build.CombinedOutput()
61 if err != nil {
62 return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
63 }
64
65 return executable, nil
66 }
67
68 /*
69 CompileTest uses go test to compile the test package at packagePath. The resulting binary is saved off in a temporary directory.
70 A path pointing to this binary is returned.
71
72 CompileTest uses the $GOPATH set in your environment. If $GOPATH is not set and you are using Go 1.8+,
73 it will use the default GOPATH instead. It passes the variadic args on to `go test`.
74 */
75 func CompileTest(packagePath string, args ...string) (compiledPath string, err error) {
76 return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
77 }
78
79 /*
80 GetAndCompileTest is identical to CompileTest but `go get` the package before compiling tests.
81 */
82 func GetAndCompileTest(packagePath string, args ...string) (compiledPath string, err error) {
83 if err := getForTest(build.Default.GOPATH, packagePath, nil); err != nil {
84 return "", err
85 }
86
87 return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
88 }
89
90 /*
91 CompileTestWithEnvironment is identical to CompileTest but allows you to specify env vars to be set at build time.
92 */
93 func CompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
94 return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
95 }
96
97 /*
98 GetAndCompileTestWithEnvironment is identical to GetAndCompileTest but allows you to specify env vars to be set at build time.
99 */
100 func GetAndCompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
101 if err := getForTest(build.Default.GOPATH, packagePath, env); err != nil {
102 return "", err
103 }
104
105 return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
106 }
107
108 /*
109 CompileTestIn is identical to CompileTest but allows you to specify a custom $GOPATH (the first argument).
110 */
111 func CompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
112 return doCompileTest(gopath, packagePath, nil, args...)
113 }
114
115 /*
116 GetAndCompileTestIn is identical to GetAndCompileTest but allows you to specify a custom $GOPATH (the first argument).
117 */
118 func GetAndCompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
119 if err := getForTest(gopath, packagePath, nil); err != nil {
120 return "", err
121 }
122
123 return doCompileTest(gopath, packagePath, nil, args...)
124 }
125
126 func isLocalPackage(packagePath string) bool {
127 return strings.HasPrefix(packagePath, ".")
128 }
129
130 func getForTest(gopath, packagePath string, env []string) error {
131 if isLocalPackage(packagePath) {
132 return nil
133 }
134
135 return doGet(gopath, packagePath, env, "-t")
136 }
137
138 func doGet(gopath, packagePath string, env []string, args ...string) error {
139 args = append(args, packagePath)
140 args = append([]string{"get"}, args...)
141
142 goGet := exec.Command("go", args...)
143 goGet.Dir = gopath
144 goGet.Env = replaceGoPath(os.Environ(), gopath)
145 goGet.Env = append(goGet.Env, env...)
146
147 output, err := goGet.CombinedOutput()
148 if err != nil {
149 return fmt.Errorf("Failed to get %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
150 }
151
152 return nil
153 }
154
155 func doCompileTest(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
156 executable, err := newExecutablePath(gopath, packagePath, ".test")
157 if err != nil {
158 return "", err
159 }
160
161 cmdArgs := append([]string{"test", "-c"}, args...)
162 cmdArgs = append(cmdArgs, "-o", executable, packagePath)
163
164 build := exec.Command("go", cmdArgs...)
165 build.Env = replaceGoPath(os.Environ(), gopath)
166 build.Env = append(build.Env, env...)
167
168 output, err := build.CombinedOutput()
169 if err != nil {
170 return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
171 }
172
173 return executable, nil
174 }
175
48176 func replaceGoPath(environ []string, newGoPath string) []string {
49177 newEnviron := []string{}
50178 for _, v := range environ {
55183 return append(newEnviron, "GOPATH="+newGoPath)
56184 }
57185
58 func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
186 func newExecutablePath(gopath, packagePath string, suffixes ...string) (string, error) {
59187 tmpDir, err := temporaryDirectory()
60188 if err != nil {
61189 return "", err
66194 }
67195
68196 executable := filepath.Join(tmpDir, path.Base(packagePath))
197
69198 if runtime.GOOS == "windows" {
70199 executable += ".exe"
71 }
72
73 cmdArgs := append([]string{"build"}, args...)
74 cmdArgs = append(cmdArgs, "-o", executable, packagePath)
75
76 build := exec.Command("go", cmdArgs...)
77 build.Env = replaceGoPath(os.Environ(), gopath)
78 build.Env = append(build.Env, env...)
79
80 output, err := build.CombinedOutput()
81 if err != nil {
82 return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
83200 }
84201
85202 return executable, nil
103220 mu.Lock()
104221 defer mu.Unlock()
105222 if tmpDir == "" {
106 tmpDir, err = ioutil.TempDir("", "gexec_artifacts")
223 tmpDir, err = os.MkdirTemp("", "gexec_artifacts")
107224 if err != nil {
108225 return "", err
109226 }
110227 }
111228
112 return ioutil.TempDir(tmpDir, "g")
113 }
229 return os.MkdirTemp(tmpDir, "g")
230 }
11
22 import (
33 "fmt"
4 "io/ioutil"
54 "os"
65 "path/filepath"
76
1312 var packagePath = "./_fixture/firefly"
1413
1514 var _ = Describe(".Build", func() {
16 When("there have been previous calls to Build", func() {
17 BeforeEach(func() {
18 _, err := gexec.Build(packagePath)
15 When("there have been previous calls to CompileTest", func() {
16 BeforeEach(func() {
17 _, err := gexec.CompileTest(packagePath)
1918 Expect(err).ShouldNot(HaveOccurred())
2019 })
2120
2322 compiledPath, err := gexec.Build(packagePath)
2423 Expect(err).ShouldNot(HaveOccurred())
2524 Expect(compiledPath).Should(BeAnExistingFile())
25 Expect(filepath.Base(compiledPath)).Should(MatchRegexp(`firefly(|.exe)$`))
2626 })
2727
2828 Context("and CleanupBuildArtifacts has been called", func() {
3131 })
3232
3333 It("compiles the specified package", func() {
34 var err error
35 fireflyPath, err = gexec.Build(packagePath)
34 fireflyPath, err := gexec.Build(packagePath)
35 Expect(err).ShouldNot(HaveOccurred())
36 Expect(fireflyPath).Should(BeAnExistingFile())
37 })
38 })
39 })
40
41 When("there have been previous calls to Build", func() {
42 BeforeEach(func() {
43 _, err := gexec.Build(packagePath)
44 Expect(err).ShouldNot(HaveOccurred())
45 })
46
47 It("compiles the specified package", func() {
48 compiledPath, err := gexec.Build(packagePath)
49 Expect(err).ShouldNot(HaveOccurred())
50 Expect(compiledPath).Should(BeAnExistingFile())
51 })
52
53 Context("and CleanupBuildArtifacts has been called", func() {
54 BeforeEach(func() {
55 gexec.CleanupBuildArtifacts()
56 })
57
58 It("compiles the specified package", func() {
59 fireflyPath, err := gexec.Build(packagePath)
3660 Expect(err).ShouldNot(HaveOccurred())
3761 Expect(fireflyPath).Should(BeAnExistingFile())
3862 })
7397 BeforeEach(func() {
7498 var err error
7599 original = os.Getenv("GOPATH")
76 gopath, err = ioutil.TempDir("", "")
100 gopath, err = os.MkdirTemp("", "")
77101 Expect(err).NotTo(HaveOccurred())
78102 copyFile(filepath.Join("_fixture", "firefly", "main.go"), filepath.Join(gopath, "src", target), "main.go")
79103 Expect(os.Setenv("GOPATH", filepath.Join(os.TempDir(), "emptyFakeGopath"))).To(Succeed())
92116 })
93117
94118 It("appends the gopath env var", func() {
95 _, err := gexec.BuildIn(gopath, target)
96 Expect(err).NotTo(HaveOccurred())
119 compiledPath, err := gexec.BuildIn(gopath, target)
120 Expect(err).NotTo(HaveOccurred())
121 Expect(compiledPath).Should(BeAnExistingFile())
97122 })
98123
99124 It("resets GOPATH to its original value", func() {
103128 })
104129 })
105130
131 var _ = Describe(".CompileTest", func() {
132 Context("a remote package", func() {
133 const remotePackage = "github.com/onsi/ginkgo/types"
134
135 It("compiles the specified test package", func() {
136 compiledPath, err := gexec.GetAndCompileTest(remotePackage)
137 Expect(err).ShouldNot(HaveOccurred())
138 Expect(compiledPath).Should(BeAnExistingFile())
139 })
140 })
141
142 When("there have been previous calls to CompileTest", func() {
143 BeforeEach(func() {
144 _, err := gexec.CompileTest(packagePath)
145 Expect(err).ShouldNot(HaveOccurred())
146 })
147
148 It("compiles the specified test package", func() {
149 compiledPath, err := gexec.CompileTest(packagePath)
150 Expect(err).ShouldNot(HaveOccurred())
151 Expect(compiledPath).Should(BeAnExistingFile())
152 })
153
154 Context("and CleanupBuildArtifacts has been called", func() {
155 BeforeEach(func() {
156 gexec.CleanupBuildArtifacts()
157 })
158
159 It("compiles the specified test package", func() {
160 fireflyTestPath, err := gexec.CompileTest(packagePath)
161 Expect(err).ShouldNot(HaveOccurred())
162 Expect(fireflyTestPath).Should(BeAnExistingFile())
163 })
164 })
165 })
166
167 When("there have been previous calls to Build", func() {
168 BeforeEach(func() {
169 _, err := gexec.Build(packagePath)
170 Expect(err).ShouldNot(HaveOccurred())
171 })
172
173 It("compiles the specified test package", func() {
174 compiledPath, err := gexec.CompileTest(packagePath)
175 Expect(err).ShouldNot(HaveOccurred())
176 Expect(compiledPath).Should(BeAnExistingFile())
177 })
178
179 Context("and CleanupBuildArtifacts has been called", func() {
180 BeforeEach(func() {
181 gexec.CleanupBuildArtifacts()
182 })
183
184 It("compiles the specified test package", func() {
185 fireflyTestPath, err := gexec.CompileTest(packagePath)
186 Expect(err).ShouldNot(HaveOccurred())
187 Expect(fireflyTestPath).Should(BeAnExistingFile())
188 })
189 })
190 })
191 })
192
193 var _ = Describe(".CompileTestWithEnvironment", func() {
194 var err error
195 env := []string{
196 "GOOS=linux",
197 "GOARCH=amd64",
198 }
199
200 Context("a remote package", func() {
201 const remotePackage = "github.com/onsi/ginkgo/types"
202
203 It("compiles the specified test package with the specified env vars", func() {
204 compiledPath, err := gexec.GetAndCompileTestWithEnvironment(remotePackage, env)
205 Expect(err).ShouldNot(HaveOccurred())
206 Expect(compiledPath).Should(BeAnExistingFile())
207 })
208 })
209
210 It("compiles the specified test package with the specified env vars", func() {
211 compiledPath, err := gexec.CompileTestWithEnvironment(packagePath, env)
212 Expect(err).ShouldNot(HaveOccurred())
213 Expect(compiledPath).Should(BeAnExistingFile())
214 })
215
216 It("returns the environment to a good state", func() {
217 _, err = gexec.CompileTestWithEnvironment(packagePath, env)
218 Expect(err).ShouldNot(HaveOccurred())
219 Expect(os.Environ()).ShouldNot(ContainElement("GOOS=linux"))
220 })
221 })
222
223 var _ = Describe(".CompiledTestIn", func() {
224 const (
225 target = "github.com/onsi/gomega/gexec/_fixture/firefly"
226 )
227
228 var (
229 original string
230 gopath string
231 )
232
233 BeforeEach(func() {
234 var err error
235 original = os.Getenv("GOPATH")
236 gopath, err = os.MkdirTemp("", "")
237 Expect(err).NotTo(HaveOccurred())
238 copyFile(filepath.Join("_fixture", "firefly", "main.go"), filepath.Join(gopath, "src", target), "main.go")
239 Expect(os.Setenv("GOPATH", filepath.Join(os.TempDir(), "emptyFakeGopath"))).To(Succeed())
240 Expect(os.Environ()).To(ContainElement(fmt.Sprintf("GOPATH=%s", filepath.Join(os.TempDir(), "emptyFakeGopath"))))
241 })
242
243 AfterEach(func() {
244 if original == "" {
245 Expect(os.Unsetenv("GOPATH")).To(Succeed())
246 } else {
247 Expect(os.Setenv("GOPATH", original)).To(Succeed())
248 }
249 if gopath != "" {
250 os.RemoveAll(gopath)
251 }
252 })
253
254 Context("a remote package", func() {
255 const remotePackage = "github.com/onsi/ginkgo/types"
256
257 It("compiles the specified test package", func() {
258 compiledPath, err := gexec.GetAndCompileTestIn(gopath, remotePackage)
259 Expect(err).ShouldNot(HaveOccurred())
260 Expect(compiledPath).Should(BeAnExistingFile())
261 })
262 })
263
264 It("appends the gopath env var", func() {
265 compiledPath, err := gexec.CompileTestIn(gopath, target)
266 Expect(err).NotTo(HaveOccurred())
267 Expect(compiledPath).Should(BeAnExistingFile())
268 })
269
270 It("resets GOPATH to its original value", func() {
271 _, err := gexec.CompileTestIn(gopath, target)
272 Expect(err).NotTo(HaveOccurred())
273 Expect(os.Getenv("GOPATH")).To(Equal(filepath.Join(os.TempDir(), "emptyFakeGopath")))
274 })
275 })
276
106277 func copyFile(source, directory, basename string) {
107278 Expect(os.MkdirAll(directory, 0755)).To(Succeed())
108 content, err := ioutil.ReadFile(source)
279 content, err := os.ReadFile(source)
109280 Expect(err).NotTo(HaveOccurred())
110 Expect(ioutil.WriteFile(filepath.Join(directory, basename), content, 0644)).To(Succeed())
281 Expect(os.WriteFile(filepath.Join(directory, basename), content, 0644)).To(Succeed())
111282 }
2020 var session *Session
2121
2222 BeforeEach(func() {
23 var err error
23 fireflyPath, err := Build("./_fixture/firefly")
24 Expect(err).ShouldNot(HaveOccurred())
25
2426 command = exec.Command(fireflyPath, "0")
2527 session, err = Start(command, nil, nil)
2628 Expect(err).ShouldNot(HaveOccurred())
77 "testing"
88 )
99
10 var fireflyPath string
11
1210 func TestGexec(t *testing.T) {
13 BeforeSuite(func() {
14 var err error
15 fireflyPath, err = gexec.Build("./_fixture/firefly")
16 Expect(err).ShouldNot(HaveOccurred())
17 })
18
1911 AfterSuite(func() {
2012 gexec.CleanupBuildArtifacts()
2113 })
33
44 import (
55 "io"
6 "io/ioutil"
76 "os/exec"
87 "syscall"
98 "time"
1615 )
1716
1817 var _ = Describe("Session", func() {
19 var command *exec.Cmd
20 var session *Session
21
22 var outWriter, errWriter io.Writer
23
24 BeforeEach(func() {
25 outWriter = nil
26 errWriter = nil
18 Context("firefly binary", func() {
19 var fireflyPath string
20 var command *exec.Cmd
21 var session *Session
22
23 var outWriter, errWriter io.Writer
24
25 BeforeEach(func() {
26 outWriter = nil
27 errWriter = nil
28
29 var err error
30 fireflyPath, err = Build("./_fixture/firefly")
31 Expect(err).ShouldNot(HaveOccurred())
32
33 })
34
35 JustBeforeEach(func() {
36 command = exec.Command(fireflyPath)
37 var err error
38 session, err = Start(command, outWriter, errWriter)
39 Expect(err).ShouldNot(HaveOccurred())
40 })
41
42 Context("running a command", func() {
43 It("should start the process", func() {
44 Expect(command.Process).ShouldNot(BeNil())
45 })
46
47 It("should wrap the process's stdout and stderr with gbytes buffers", func(done Done) {
48 Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
49 Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))
50 defer session.Out.CancelDetects()
51
52 select {
53 case <-session.Out.Detect("Can we maybe vote on the whole murdering people issue"):
54 Eventually(session).Should(Exit(0))
55 case <-session.Out.Detect("I swear by my pretty floral bonnet, I will end you."):
56 Eventually(session).Should(Exit(1))
57 case <-session.Out.Detect("My work's illegal, but at least it's honest."):
58 Eventually(session).Should(Exit(2))
59 }
60
61 close(done)
62 })
63
64 It("should satisfy the gbytes.BufferProvider interface, passing Stdout", func() {
65 Eventually(session).Should(Say("We've done the impossible, and that makes us mighty"))
66 Eventually(session).Should(Exit())
67 })
68 })
69
70 Describe("providing the exit code", func() {
71 It("should provide the app's exit code", func() {
72 Expect(session.ExitCode()).Should(Equal(-1))
73
74 Eventually(session).Should(Exit())
75 Expect(session.ExitCode()).Should(BeNumerically(">=", 0))
76 Expect(session.ExitCode()).Should(BeNumerically("<", 3))
77 })
78 })
79
80 Describe("wait", func() {
81 It("should wait till the command exits", func() {
82 Expect(session.ExitCode()).Should(Equal(-1))
83 Expect(session.Wait().ExitCode()).Should(BeNumerically(">=", 0))
84 Expect(session.Wait().ExitCode()).Should(BeNumerically("<", 3))
85 })
86 })
87
88 Describe("exited", func() {
89 It("should close when the command exits", func() {
90 Eventually(session.Exited).Should(BeClosed())
91 Expect(session.ExitCode()).ShouldNot(Equal(-1))
92 })
93 })
94
95 Describe("kill", func() {
96 It("should kill the command", func() {
97 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
98 Expect(err).ShouldNot(HaveOccurred())
99
100 session.Kill()
101 Eventually(session).Should(Exit(128 + 9))
102 })
103 })
104
105 Describe("interrupt", func() {
106 It("should interrupt the command", func() {
107 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
108 Expect(err).ShouldNot(HaveOccurred())
109
110 session.Interrupt()
111 Eventually(session).Should(Exit(128 + 2))
112 })
113 })
114
115 Describe("terminate", func() {
116 It("should terminate the command", func() {
117 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
118 Expect(err).ShouldNot(HaveOccurred())
119
120 session.Terminate()
121 Eventually(session).Should(Exit(128 + 15))
122 })
123 })
124
125 Describe("signal", func() {
126 It("should send the signal to the command", func() {
127 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
128 Expect(err).ShouldNot(HaveOccurred())
129
130 session.Signal(syscall.SIGABRT)
131 Eventually(session).Should(Exit(128 + 6))
132 })
133
134 It("should ignore sending a signal if the command did not start", func() {
135 session, err := Start(exec.Command("notexisting"), GinkgoWriter, GinkgoWriter)
136 Expect(err).To(HaveOccurred())
137
138 Expect(func() { session.Signal(syscall.SIGUSR1) }).NotTo(Panic())
139 })
140 })
141
142 Context("tracking sessions", func() {
143 BeforeEach(func() {
144 KillAndWait()
145 })
146
147 Describe("kill", func() {
148 It("should kill all the started sessions", func() {
149 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
150 Expect(err).ShouldNot(HaveOccurred())
151
152 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
153 Expect(err).ShouldNot(HaveOccurred())
154
155 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
156 Expect(err).ShouldNot(HaveOccurred())
157
158 Kill()
159
160 Eventually(session1).Should(Exit(128 + 9))
161 Eventually(session2).Should(Exit(128 + 9))
162 Eventually(session3).Should(Exit(128 + 9))
163 })
164
165 It("should not track unstarted sessions", func() {
166 _, err := Start(exec.Command("does not exist", "10000000"), GinkgoWriter, GinkgoWriter)
167 Expect(err).Should(HaveOccurred())
168
169 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
170 Expect(err).ShouldNot(HaveOccurred())
171
172 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
173 Expect(err).ShouldNot(HaveOccurred())
174
175 Kill()
176
177 Eventually(session2).Should(Exit(128 + 9))
178 Eventually(session3).Should(Exit(128 + 9))
179 })
180
181 })
182
183 Describe("killAndWait", func() {
184 It("should kill all the started sessions and wait for them to finish", func() {
185 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
186 Expect(err).ShouldNot(HaveOccurred())
187
188 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
189 Expect(err).ShouldNot(HaveOccurred())
190
191 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
192 Expect(err).ShouldNot(HaveOccurred())
193
194 KillAndWait()
195 Expect(session1).Should(Exit(128+9), "Should have exited")
196 Expect(session2).Should(Exit(128+9), "Should have exited")
197 Expect(session3).Should(Exit(128+9), "Should have exited")
198 })
199 })
200
201 Describe("terminate", func() {
202 It("should terminate all the started sessions", func() {
203 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
204 Expect(err).ShouldNot(HaveOccurred())
205
206 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
207 Expect(err).ShouldNot(HaveOccurred())
208
209 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
210 Expect(err).ShouldNot(HaveOccurred())
211
212 Terminate()
213
214 Eventually(session1).Should(Exit(128 + 15))
215 Eventually(session2).Should(Exit(128 + 15))
216 Eventually(session3).Should(Exit(128 + 15))
217 })
218 })
219
220 Describe("terminateAndWait", func() {
221 It("should terminate all the started sessions, and wait for them to exit", func() {
222 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
223 Expect(err).ShouldNot(HaveOccurred())
224
225 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
226 Expect(err).ShouldNot(HaveOccurred())
227
228 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
229 Expect(err).ShouldNot(HaveOccurred())
230
231 TerminateAndWait()
232
233 Expect(session1).Should(Exit(128+15), "Should have exited")
234 Expect(session2).Should(Exit(128+15), "Should have exited")
235 Expect(session3).Should(Exit(128+15), "Should have exited")
236 })
237 })
238
239 Describe("signal", func() {
240 It("should signal all the started sessions", func() {
241 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
242 Expect(err).ShouldNot(HaveOccurred())
243
244 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
245 Expect(err).ShouldNot(HaveOccurred())
246
247 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
248 Expect(err).ShouldNot(HaveOccurred())
249
250 Signal(syscall.SIGABRT)
251
252 Eventually(session1).Should(Exit(128 + 6))
253 Eventually(session2).Should(Exit(128 + 6))
254 Eventually(session3).Should(Exit(128 + 6))
255 })
256 })
257
258 Describe("interrupt", func() {
259 It("should interrupt all the started sessions, and not wait", func() {
260 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
261 Expect(err).ShouldNot(HaveOccurred())
262
263 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
264 Expect(err).ShouldNot(HaveOccurred())
265
266 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
267 Expect(err).ShouldNot(HaveOccurred())
268
269 Interrupt()
270
271 Eventually(session1).Should(Exit(128 + 2))
272 Eventually(session2).Should(Exit(128 + 2))
273 Eventually(session3).Should(Exit(128 + 2))
274 })
275 })
276 })
277
278 When("the command exits", func() {
279 It("should close the buffers", func() {
280 Eventually(session).Should(Exit())
281
282 Expect(session.Out.Closed()).Should(BeTrue())
283 Expect(session.Err.Closed()).Should(BeTrue())
284
285 Expect(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
286 })
287
288 var So = It
289
290 So("this means that eventually should short circuit", func() {
291 t := time.Now()
292 failures := InterceptGomegaFailures(func() {
293 Eventually(session).Should(Say("blah blah blah blah blah"))
294 })
295 Expect(time.Since(t)).Should(BeNumerically("<=", 500*time.Millisecond))
296 Expect(failures).Should(HaveLen(1))
297 })
298 })
299
300 When("wrapping out and err", func() {
301 var (
302 outWriterBuffer, errWriterBuffer *Buffer
303 )
304
305 BeforeEach(func() {
306 outWriterBuffer = NewBuffer()
307 outWriter = outWriterBuffer
308 errWriterBuffer = NewBuffer()
309 errWriter = errWriterBuffer
310 })
311
312 It("should route to both the provided writers and the gbytes buffers", func() {
313 Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
314 Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))
315
316 Expect(outWriterBuffer.Contents()).Should(ContainSubstring("We've done the impossible, and that makes us mighty"))
317 Expect(errWriterBuffer.Contents()).Should(ContainSubstring("Ah, curse your sudden but inevitable betrayal!"))
318
319 Eventually(session).Should(Exit())
320
321 Expect(outWriterBuffer.Contents()).Should(Equal(session.Out.Contents()))
322 Expect(errWriterBuffer.Contents()).Should(Equal(session.Err.Contents()))
323 })
324
325 When("discarding the output of the command", func() {
326 BeforeEach(func() {
327 outWriter = io.Discard
328 errWriter = io.Discard
329 })
330
331 It("executes succesfuly", func() {
332 Eventually(session).Should(Exit())
333 })
334 })
335 })
27336 })
28337
29 JustBeforeEach(func() {
30 command = exec.Command(fireflyPath)
31 var err error
32 session, err = Start(command, outWriter, errWriter)
33 Expect(err).ShouldNot(HaveOccurred())
34 })
35
36 Context("running a command", func() {
37 It("should start the process", func() {
38 Expect(command.Process).ShouldNot(BeNil())
39 })
40
41 It("should wrap the process's stdout and stderr with gbytes buffers", func(done Done) {
42 Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
43 Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))
44 defer session.Out.CancelDetects()
45
46 select {
47 case <-session.Out.Detect("Can we maybe vote on the whole murdering people issue"):
48 Eventually(session).Should(Exit(0))
49 case <-session.Out.Detect("I swear by my pretty floral bonnet, I will end you."):
50 Eventually(session).Should(Exit(1))
51 case <-session.Out.Detect("My work's illegal, but at least it's honest."):
52 Eventually(session).Should(Exit(2))
53 }
54
55 close(done)
56 })
57
58 It("should satisfy the gbytes.BufferProvider interface, passing Stdout", func() {
59 Eventually(session).Should(Say("We've done the impossible, and that makes us mighty"))
60 Eventually(session).Should(Exit())
61 })
62 })
63
64 Describe("providing the exit code", func() {
65 It("should provide the app's exit code", func() {
66 Expect(session.ExitCode()).Should(Equal(-1))
67
68 Eventually(session).Should(Exit())
69 Expect(session.ExitCode()).Should(BeNumerically(">=", 0))
70 Expect(session.ExitCode()).Should(BeNumerically("<", 3))
71 })
72 })
73
74 Describe("wait", func() {
75 It("should wait till the command exits", func() {
76 Expect(session.ExitCode()).Should(Equal(-1))
77 Expect(session.Wait().ExitCode()).Should(BeNumerically(">=", 0))
78 Expect(session.Wait().ExitCode()).Should(BeNumerically("<", 3))
79 })
80 })
81
82 Describe("exited", func() {
83 It("should close when the command exits", func() {
84 Eventually(session.Exited).Should(BeClosed())
85 Expect(session.ExitCode()).ShouldNot(Equal(-1))
86 })
87 })
88
89 Describe("kill", func() {
90 It("should kill the command", func() {
91 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
338 Context("firefly tests", func() {
339 var fireflyTestPath string
340 var command *exec.Cmd
341 var session *Session
342
343 var outWriter, errWriter io.Writer
344
345 BeforeEach(func() {
346 outWriter = nil
347 errWriter = nil
348
349 var err error
350 fireflyTestPath, err = CompileTest("./_fixture/firefly")
92351 Expect(err).ShouldNot(HaveOccurred())
93
94 session.Kill()
95 Eventually(session).Should(Exit(128 + 9))
96 })
97 })
98
99 Describe("interrupt", func() {
100 It("should interrupt the command", func() {
101 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
352 })
353
354 JustBeforeEach(func() {
355 command = exec.Command(fireflyTestPath)
356 var err error
357 session, err = Start(command, outWriter, errWriter)
102358 Expect(err).ShouldNot(HaveOccurred())
103
104 session.Interrupt()
105 Eventually(session).Should(Exit(128 + 2))
106 })
107 })
108
109 Describe("terminate", func() {
110 It("should terminate the command", func() {
111 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
112 Expect(err).ShouldNot(HaveOccurred())
113
114 session.Terminate()
115 Eventually(session).Should(Exit(128 + 15))
116 })
117 })
118
119 Describe("signal", func() {
120 It("should send the signal to the command", func() {
121 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
122 Expect(err).ShouldNot(HaveOccurred())
123
124 session.Signal(syscall.SIGABRT)
125 Eventually(session).Should(Exit(128 + 6))
126 })
127
128 It("should ignore sending a signal if the command did not start", func() {
129 session, err := Start(exec.Command("notexisting"), GinkgoWriter, GinkgoWriter)
130 Expect(err).To(HaveOccurred())
131
132 Expect(func() { session.Signal(syscall.SIGUSR1) }).NotTo(Panic())
133 })
134 })
135
136 Context("tracking sessions", func() {
137 BeforeEach(func() {
138 KillAndWait()
139 })
140
141 Describe("kill", func() {
142 It("should kill all the started sessions", func() {
143 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
144 Expect(err).ShouldNot(HaveOccurred())
145
146 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
147 Expect(err).ShouldNot(HaveOccurred())
148
149 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
150 Expect(err).ShouldNot(HaveOccurred())
151
152 Kill()
153
154 Eventually(session1).Should(Exit(128 + 9))
155 Eventually(session2).Should(Exit(128 + 9))
156 Eventually(session3).Should(Exit(128 + 9))
157 })
158
159 It("should not track unstarted sessions", func() {
160 _, err := Start(exec.Command("does not exist", "10000000"), GinkgoWriter, GinkgoWriter)
161 Expect(err).Should(HaveOccurred())
162
163 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
164 Expect(err).ShouldNot(HaveOccurred())
165
166 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
167 Expect(err).ShouldNot(HaveOccurred())
168
169 Kill()
170
171 Eventually(session2).Should(Exit(128 + 9))
172 Eventually(session3).Should(Exit(128 + 9))
173 })
174
175 })
176
177 Describe("killAndWait", func() {
178 It("should kill all the started sessions and wait for them to finish", func() {
179 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
180 Expect(err).ShouldNot(HaveOccurred())
181
182 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
183 Expect(err).ShouldNot(HaveOccurred())
184
185 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
186 Expect(err).ShouldNot(HaveOccurred())
187
188 KillAndWait()
189 Expect(session1).Should(Exit(128+9), "Should have exited")
190 Expect(session2).Should(Exit(128+9), "Should have exited")
191 Expect(session3).Should(Exit(128+9), "Should have exited")
192 })
193 })
194
195 Describe("terminate", func() {
196 It("should terminate all the started sessions", func() {
197 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
198 Expect(err).ShouldNot(HaveOccurred())
199
200 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
201 Expect(err).ShouldNot(HaveOccurred())
202
203 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
204 Expect(err).ShouldNot(HaveOccurred())
205
206 Terminate()
207
208 Eventually(session1).Should(Exit(128 + 15))
209 Eventually(session2).Should(Exit(128 + 15))
210 Eventually(session3).Should(Exit(128 + 15))
211 })
212 })
213
214 Describe("terminateAndWait", func() {
215 It("should terminate all the started sessions, and wait for them to exit", func() {
216 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
217 Expect(err).ShouldNot(HaveOccurred())
218
219 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
220 Expect(err).ShouldNot(HaveOccurred())
221
222 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
223 Expect(err).ShouldNot(HaveOccurred())
224
225 TerminateAndWait()
226
227 Expect(session1).Should(Exit(128+15), "Should have exited")
228 Expect(session2).Should(Exit(128+15), "Should have exited")
229 Expect(session3).Should(Exit(128+15), "Should have exited")
230 })
231 })
232
233 Describe("signal", func() {
234 It("should signal all the started sessions", func() {
235 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
236 Expect(err).ShouldNot(HaveOccurred())
237
238 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
239 Expect(err).ShouldNot(HaveOccurred())
240
241 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
242 Expect(err).ShouldNot(HaveOccurred())
243
244 Signal(syscall.SIGABRT)
245
246 Eventually(session1).Should(Exit(128 + 6))
247 Eventually(session2).Should(Exit(128 + 6))
248 Eventually(session3).Should(Exit(128 + 6))
249 })
250 })
251
252 Describe("interrupt", func() {
253 It("should interrupt all the started sessions, and not wait", func() {
254 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
255 Expect(err).ShouldNot(HaveOccurred())
256
257 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
258 Expect(err).ShouldNot(HaveOccurred())
259
260 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
261 Expect(err).ShouldNot(HaveOccurred())
262
263 Interrupt()
264
265 Eventually(session1).Should(Exit(128 + 2))
266 Eventually(session2).Should(Exit(128 + 2))
267 Eventually(session3).Should(Exit(128 + 2))
268 })
269 })
270 })
271
272 When("the command exits", func() {
273 It("should close the buffers", func() {
274 Eventually(session).Should(Exit())
275
276 Expect(session.Out.Closed()).Should(BeTrue())
277 Expect(session.Err.Closed()).Should(BeTrue())
278
279 Expect(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
280 })
281
282 var So = It
283
284 So("this means that eventually should short circuit", func() {
285 t := time.Now()
286 failures := InterceptGomegaFailures(func() {
287 Eventually(session).Should(Say("blah blah blah blah blah"))
288 })
289 Expect(time.Since(t)).Should(BeNumerically("<=", 500*time.Millisecond))
290 Expect(failures).Should(HaveLen(1))
291 })
292 })
293
294 When("wrapping out and err", func() {
295 var (
296 outWriterBuffer, errWriterBuffer *Buffer
297 )
298
299 BeforeEach(func() {
300 outWriterBuffer = NewBuffer()
301 outWriter = outWriterBuffer
302 errWriterBuffer = NewBuffer()
303 errWriter = errWriterBuffer
304 })
305
306 It("should route to both the provided writers and the gbytes buffers", func() {
307 Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
308 Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))
309
310 Expect(outWriterBuffer.Contents()).Should(ContainSubstring("We've done the impossible, and that makes us mighty"))
311 Expect(errWriterBuffer.Contents()).Should(ContainSubstring("Ah, curse your sudden but inevitable betrayal!"))
312
313 Eventually(session).Should(Exit())
314
315 Expect(outWriterBuffer.Contents()).Should(Equal(session.Out.Contents()))
316 Expect(errWriterBuffer.Contents()).Should(Equal(session.Err.Contents()))
317 })
318
319 When("discarding the output of the command", func() {
359 })
360
361 When("wrapping out and err", func() {
362 var (
363 outWriterBuffer, errWriterBuffer *Buffer
364 )
365
320366 BeforeEach(func() {
321 outWriter = ioutil.Discard
322 errWriter = ioutil.Discard
323 })
324
325 It("executes succesfuly", func() {
367 outWriterBuffer = NewBuffer()
368 outWriter = outWriterBuffer
369 errWriterBuffer = NewBuffer()
370 errWriter = errWriterBuffer
371 })
372
373 It("should route to both the provided writers and the gbytes buffers", func() {
374 Eventually(session.Out).Should(Say("PASS"))
375 Eventually(session.Err).Should(Say(""))
376
377 Expect(outWriterBuffer.Contents()).Should(ContainSubstring("PASS"))
378 Expect(errWriterBuffer.Contents()).Should(BeEmpty())
379
326380 Eventually(session).Should(Exit())
381
382 Expect(outWriterBuffer.Contents()).Should(Equal(session.Out.Contents()))
383 Expect(errWriterBuffer.Contents()).Should(Equal(session.Err.Contents()))
384 })
385
386 When("discarding the output of the command", func() {
387 BeforeEach(func() {
388 outWriter = io.Discard
389 errWriter = io.Discard
390 })
391
392 It("executes succesfuly", func() {
393 Eventually(session).Should(Exit())
394 })
327395 })
328396 })
329397 })
55 "encoding/base64"
66 "encoding/json"
77 "fmt"
8 "io/ioutil"
8 "io"
99 "net/http"
1010 "net/url"
1111 "reflect"
108108 //(recall that a `http.Header` is a mapping from string (key) to []string (values))
109109 //It is a convenience wrapper around `VerifyHeader` that allows you to avoid having to create an `http.Header` object.
110110 func (g GHTTPWithGomega) VerifyHeaderKV(key string, values ...string) http.HandlerFunc {
111 return VerifyHeader(http.Header{key: values})
111 return g.VerifyHeader(http.Header{key: values})
112112 }
113113
114114 //VerifyBody returns a handler that verifies that the body of the request matches the passed in byte array.
116116 func (g GHTTPWithGomega) VerifyBody(expectedBody []byte) http.HandlerFunc {
117117 return CombineHandlers(
118118 func(w http.ResponseWriter, req *http.Request) {
119 body, err := ioutil.ReadAll(req.Body)
119 body, err := io.ReadAll(req.Body)
120120 req.Body.Close()
121121 g.gomega.Expect(err).ShouldNot(HaveOccurred())
122122 g.gomega.Expect(body).Should(Equal(expectedBody), "Body Mismatch")
130130 //VerifyJSON also verifies that the request's content type is application/json
131131 func (g GHTTPWithGomega) VerifyJSON(expectedJSON string) http.HandlerFunc {
132132 return CombineHandlers(
133 VerifyMimeType("application/json"),
133 g.VerifyMimeType("application/json"),
134134 func(w http.ResponseWriter, req *http.Request) {
135 body, err := ioutil.ReadAll(req.Body)
135 body, err := io.ReadAll(req.Body)
136136 req.Body.Close()
137137 g.gomega.Expect(err).ShouldNot(HaveOccurred())
138138 g.gomega.Expect(body).Should(MatchJSON(expectedJSON), "JSON Mismatch")
147147 data, err := json.Marshal(object)
148148 g.gomega.Expect(err).ShouldNot(HaveOccurred())
149149 return CombineHandlers(
150 VerifyMimeType("application/json"),
151 VerifyJSON(string(data)),
150 g.VerifyMimeType("application/json"),
151 g.VerifyJSON(string(data)),
152152 )
153153 }
154154
170170 //
171171 //It is a convenience wrapper around `VerifyForm` that lets you avoid having to create a `url.Values` object.
172172 func (g GHTTPWithGomega) VerifyFormKV(key string, values ...string) http.HandlerFunc {
173 return VerifyForm(url.Values{key: values})
173 return g.VerifyForm(url.Values{key: values})
174174 }
175175
176176 //VerifyProtoRepresenting returns a handler that verifies that the body of the request is a valid protobuf
179179 //VerifyProtoRepresenting also verifies that the request's content type is application/x-protobuf
180180 func (g GHTTPWithGomega) VerifyProtoRepresenting(expected proto.Message) http.HandlerFunc {
181181 return CombineHandlers(
182 VerifyContentType("application/x-protobuf"),
182 g.VerifyContentType("application/x-protobuf"),
183183 func(w http.ResponseWriter, req *http.Request) {
184 body, err := ioutil.ReadAll(req.Body)
184 body, err := io.ReadAll(req.Body)
185185 g.gomega.Expect(err).ShouldNot(HaveOccurred())
186186 req.Body.Close()
187187
110110 import (
111111 "fmt"
112112 "io"
113 "io/ioutil"
114113 "net/http"
115114 "net/http/httptest"
116115 "net/http/httputil"
268267 } else {
269268 s.rwMutex.Unlock()
270269 if s.GetAllowUnhandledRequests() {
271 ioutil.ReadAll(req.Body)
270 io.ReadAll(req.Body)
272271 req.Body.Close()
273272 w.WriteHeader(s.GetUnhandledRequestStatusCode())
274273 } else {
22 import (
33 "bytes"
44 "io"
5 "io/ioutil"
65 "net/http"
76 "net/url"
87 "regexp"
5958 Expect(err).ShouldNot(HaveOccurred())
6059 Expect(resp.StatusCode).Should(Equal(200))
6160
62 body, err := ioutil.ReadAll(resp.Body)
61 body, err := io.ReadAll(resp.Body)
6362 resp.Body.Close()
6463 Expect(err).ShouldNot(HaveOccurred())
6564
6968 Expect(err).ShouldNot(HaveOccurred())
7069 Expect(resp.StatusCode).Should(Equal(200))
7170
72 body2, err := ioutil.ReadAll(resp.Body)
71 body2, err := io.ReadAll(resp.Body)
7372 resp.Body.Close()
7473 Expect(err).ShouldNot(HaveOccurred())
7574
101100 Expect(err).ShouldNot(HaveOccurred())
102101 Expect(resp.StatusCode).Should(Equal(http.StatusForbidden))
103102
104 data, err := ioutil.ReadAll(resp.Body)
103 data, err := io.ReadAll(resp.Body)
105104 Expect(err).ShouldNot(HaveOccurred())
106105 Expect(data).Should(BeEmpty())
107106 })
258257 s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) {
259258 // Expect(true).Should(BeFalse()) <-- would be nice to do it this way, but the test just can't be written this way
260259
261 By("We're cheating a bit here -- we're throwing a GINKGO_PANIC which simulates a failed assertion")
262 panic(GINKGO_PANIC)
260 By("We're cheating a bit here -- we're pretending to throw a Ginkgo panic which simulates a failed assertion")
261 panic("defer GinkgoRecover()")
263262 })
264263 })
265264
791790
792791 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
793792
794 body, err := ioutil.ReadAll(resp.Body)
793 body, err := io.ReadAll(resp.Body)
795794 Expect(err).ShouldNot(HaveOccurred())
796795 Expect(body).Should(Equal([]byte("sweet")))
797796
800799
801800 Expect(resp.StatusCode).Should(Equal(http.StatusOK))
802801
803 body, err = ioutil.ReadAll(resp.Body)
802 body, err = io.ReadAll(resp.Body)
804803 Expect(err).ShouldNot(HaveOccurred())
805804 Expect(body).Should(Equal([]byte("sour")))
806805 })
819818 Expect(err).ShouldNot(HaveOccurred())
820819
821820 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
822 Expect(ioutil.ReadAll(resp.Body)).Should(Equal([]byte("sweet")))
821 Expect(io.ReadAll(resp.Body)).Should(Equal([]byte("sweet")))
823822 Expect(resp.Header.Get("X-Custom-Header")).Should(Equal("my header"))
824823 })
825824 })
853852
854853 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
855854
856 body, err := ioutil.ReadAll(resp.Body)
855 body, err := io.ReadAll(resp.Body)
857856 Expect(err).ShouldNot(HaveOccurred())
858857 Expect(body).Should(Equal([]byte("tasty")))
859858
862861
863862 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
864863
865 body, err = ioutil.ReadAll(resp.Body)
864 body, err = io.ReadAll(resp.Body)
866865 Expect(err).ShouldNot(HaveOccurred())
867866 Expect(body).Should(Equal([]byte("treat")))
868867 })
880879
881880 Expect(err).ShouldNot(HaveOccurred())
882881 Expect(resp.StatusCode).Should(Equal(http.StatusOK))
883 body, err := ioutil.ReadAll(resp.Body)
882 body, err := io.ReadAll(resp.Body)
884883 Expect(err).ShouldNot(HaveOccurred())
885884 Expect(body).Should(BeEmpty())
886885
904903
905904 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
906905
907 body, err := ioutil.ReadAll(resp.Body)
906 body, err := io.ReadAll(resp.Body)
908907 Expect(err).ShouldNot(HaveOccurred())
909908 Expect(body).Should(MatchJSON("[1,2,3]"))
910909 })
989988
990989 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
991990
992 body, err := ioutil.ReadAll(resp.Body)
991 body, err := io.ReadAll(resp.Body)
993992 Expect(err).ShouldNot(HaveOccurred())
994993 Expect(body).Should(MatchJSON(`{"Key": "Jim", "Value": "Codes"}`))
995994 })
10701069 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
10711070
10721071 var received protobuf.SimpleMessage
1073 body, err := ioutil.ReadAll(resp.Body)
1072 body, err := io.ReadAll(resp.Body)
10741073 Expect(err).ShouldNot(HaveOccurred())
10751074 err = proto.Unmarshal(body, &received)
10761075 Expect(err).ShouldNot(HaveOccurred())
0 package gmeasure
1
2 import (
3 "crypto/md5"
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 )
9
10 const CACHE_EXT = ".gmeasure-cache"
11
12 /*
13 ExperimentCache provides a director-and-file based cache of experiments
14 */
15 type ExperimentCache struct {
16 Path string
17 }
18
19 /*
20 NewExperimentCache creates and initializes a new cache. Path must point to a directory (if path does not exist, NewExperimentCache will create a directory at path).
21
22 Cached Experiments are stored as separate files in the cache directory - the filename is a hash of the Experiment name. Each file contains two JSON-encoded objects - a CachedExperimentHeader that includes the experiment's name and cache version number, and then the Experiment itself.
23 */
24 func NewExperimentCache(path string) (ExperimentCache, error) {
25 stat, err := os.Stat(path)
26 if os.IsNotExist(err) {
27 err := os.MkdirAll(path, 0777)
28 if err != nil {
29 return ExperimentCache{}, err
30 }
31 } else if !stat.IsDir() {
32 return ExperimentCache{}, fmt.Errorf("%s is not a directory", path)
33 }
34
35 return ExperimentCache{
36 Path: path,
37 }, nil
38 }
39
40 /*
41 CachedExperimentHeader captures the name of the Cached Experiment and its Version
42 */
43 type CachedExperimentHeader struct {
44 Name string
45 Version int
46 }
47
48 func (cache ExperimentCache) hashOf(name string) string {
49 return fmt.Sprintf("%x", md5.Sum([]byte(name)))
50 }
51
52 func (cache ExperimentCache) readHeader(filename string) (CachedExperimentHeader, error) {
53 out := CachedExperimentHeader{}
54 f, err := os.Open(filepath.Join(cache.Path, filename))
55 if err != nil {
56 return out, err
57 }
58 defer f.Close()
59 err = json.NewDecoder(f).Decode(&out)
60 return out, err
61 }
62
63 /*
64 List returns a list of all Cached Experiments found in the cache.
65 */
66 func (cache ExperimentCache) List() ([]CachedExperimentHeader, error) {
67 var out []CachedExperimentHeader
68 entries, err := os.ReadDir(cache.Path)
69 if err != nil {
70 return out, err
71 }
72 for _, entry := range entries {
73 if filepath.Ext(entry.Name()) != CACHE_EXT {
74 continue
75 }
76 header, err := cache.readHeader(entry.Name())
77 if err != nil {
78 return out, err
79 }
80 out = append(out, header)
81 }
82 return out, nil
83 }
84
85 /*
86 Clear empties out the cache - this will delete any and all detected cache files in the cache directory. Use with caution!
87 */
88 func (cache ExperimentCache) Clear() error {
89 entries, err := os.ReadDir(cache.Path)
90 if err != nil {
91 return err
92 }
93 for _, entry := range entries {
94 if filepath.Ext(entry.Name()) != CACHE_EXT {
95 continue
96 }
97 err := os.Remove(filepath.Join(cache.Path, entry.Name()))
98 if err != nil {
99 return err
100 }
101 }
102 return nil
103 }
104
105 /*
106 Load fetches an experiment from the cache. Lookup occurs by name. Load requires that the version numer in the cache is equal to or greater than the passed-in version.
107
108 If an experiment with corresponding name and version >= the passed-in version is found, it is unmarshaled and returned.
109
110 If no experiment is found, or the cached version is smaller than the passed-in version, Load will return nil.
111
112 When paired with Ginkgo you can cache experiments and prevent potentially expensive recomputation with this pattern:
113
114 const EXPERIMENT_VERSION = 1 //bump this to bust the cache and recompute _all_ experiments
115
116 Describe("some experiments", func() {
117 var cache gmeasure.ExperimentCache
118 var experiment *gmeasure.Experiment
119
120 BeforeEach(func() {
121 cache = gmeasure.NewExperimentCache("./gmeasure-cache")
122 name := CurrentSpecReport().LeafNodeText
123 experiment = cache.Load(name, EXPERIMENT_VERSION)
124 if experiment != nil {
125 AddReportEntry(experiment)
126 Skip("cached")
127 }
128 experiment = gmeasure.NewExperiment(name)
129 AddReportEntry(experiment)
130 })
131
132 It("foo runtime", func() {
133 experiment.SampleDuration("runtime", func() {
134 //do stuff
135 }, gmeasure.SamplingConfig{N:100})
136 })
137
138 It("bar runtime", func() {
139 experiment.SampleDuration("runtime", func() {
140 //do stuff
141 }, gmeasure.SamplingConfig{N:100})
142 })
143
144 AfterEach(func() {
145 if !CurrentSpecReport().State.Is(types.SpecStateSkipped) {
146 cache.Save(experiment.Name, EXPERIMENT_VERSION, experiment)
147 }
148 })
149 })
150 */
151 func (cache ExperimentCache) Load(name string, version int) *Experiment {
152 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
153 f, err := os.Open(path)
154 if err != nil {
155 return nil
156 }
157 defer f.Close()
158 dec := json.NewDecoder(f)
159 header := CachedExperimentHeader{}
160 dec.Decode(&header)
161 if header.Version < version {
162 return nil
163 }
164 out := NewExperiment("")
165 err = dec.Decode(out)
166 if err != nil {
167 return nil
168 }
169 return out
170 }
171
172 /*
173 Save stores the passed-in experiment to the cache with the passed-in name and version.
174 */
175 func (cache ExperimentCache) Save(name string, version int, experiment *Experiment) error {
176 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
177 f, err := os.Create(path)
178 if err != nil {
179 return err
180 }
181 defer f.Close()
182 enc := json.NewEncoder(f)
183 err = enc.Encode(CachedExperimentHeader{
184 Name: name,
185 Version: version,
186 })
187 if err != nil {
188 return err
189 }
190 return enc.Encode(experiment)
191 }
192
193 /*
194 Delete removes the experiment with the passed-in name from the cache
195 */
196 func (cache ExperimentCache) Delete(name string) error {
197 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
198 return os.Remove(path)
199 }
0 package gmeasure_test
1
2 import (
3 "fmt"
4 "os"
5
6 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/gomega"
8 "github.com/onsi/gomega/gmeasure"
9 )
10
11 var _ = Describe("Cache", func() {
12 var path string
13 var cache gmeasure.ExperimentCache
14 var e1, e2 *gmeasure.Experiment
15
16 BeforeEach(func() {
17 var err error
18 path = fmt.Sprintf("./cache-%d", GinkgoParallelNode())
19 cache, err = gmeasure.NewExperimentCache(path)
20 Ω(err).ShouldNot(HaveOccurred())
21 e1 = gmeasure.NewExperiment("Experiment-1")
22 e1.RecordValue("foo", 32)
23 e2 = gmeasure.NewExperiment("Experiment-2")
24 e2.RecordValue("bar", 64)
25 })
26
27 AfterEach(func() {
28 Ω(os.RemoveAll(path)).Should(Succeed())
29 })
30
31 Describe("when creating a cache that points to a file", func() {
32 It("errors", func() {
33 f, err := os.Create("cache-temp-file")
34 Ω(err).ShouldNot(HaveOccurred())
35 f.Close()
36 cache, err := gmeasure.NewExperimentCache("cache-temp-file")
37 Ω(err).Should(MatchError("cache-temp-file is not a directory"))
38 Ω(cache).Should(BeZero())
39 Ω(os.RemoveAll("cache-temp-file")).Should(Succeed())
40 })
41 })
42
43 Describe("the happy path", func() {
44 It("can save, load, list, delete, and clear the cache", func() {
45 Ω(cache.Save("e1", 1, e1)).Should(Succeed())
46 Ω(cache.Save("e2", 7, e2)).Should(Succeed())
47
48 Ω(cache.Load("e1", 1)).Should(Equal(e1))
49 Ω(cache.Load("e2", 7)).Should(Equal(e2))
50
51 Ω(cache.List()).Should(ConsistOf(
52 gmeasure.CachedExperimentHeader{"e1", 1},
53 gmeasure.CachedExperimentHeader{"e2", 7},
54 ))
55
56 Ω(cache.Delete("e2")).Should(Succeed())
57 Ω(cache.Load("e1", 1)).Should(Equal(e1))
58 Ω(cache.Load("e2", 7)).Should(BeNil())
59 Ω(cache.List()).Should(ConsistOf(
60 gmeasure.CachedExperimentHeader{"e1", 1},
61 ))
62
63 Ω(cache.Clear()).Should(Succeed())
64 Ω(cache.List()).Should(BeEmpty())
65 Ω(cache.Load("e1", 1)).Should(BeNil())
66 Ω(cache.Load("e2", 7)).Should(BeNil())
67 })
68 })
69
70 Context("with an empty cache", func() {
71 It("should list nothing", func() {
72 Ω(cache.List()).Should(BeEmpty())
73 })
74
75 It("should not error when clearing", func() {
76 Ω(cache.Clear()).Should(Succeed())
77 })
78
79 It("returs nil when loading a non-existing experiment", func() {
80 Ω(cache.Load("floop", 17)).Should(BeNil())
81 })
82 })
83
84 Describe("version management", func() {
85 BeforeEach(func() {
86 Ω(cache.Save("e1", 7, e1)).Should(Succeed())
87 })
88
89 Context("when the cached version is older than the requested version", func() {
90 It("returns nil", func() {
91 Ω(cache.Load("e1", 8)).Should(BeNil())
92 })
93 })
94
95 Context("when the cached version equals the requested version", func() {
96 It("returns the cached version", func() {
97 Ω(cache.Load("e1", 7)).Should(Equal(e1))
98 })
99 })
100
101 Context("when the cached version is newer than the requested version", func() {
102 It("returns the cached version", func() {
103 Ω(cache.Load("e1", 6)).Should(Equal(e1))
104 })
105 })
106 })
107
108 })
0 package gmeasure
1
2 import "encoding/json"
3
4 type enumSupport struct {
5 toString map[uint]string
6 toEnum map[string]uint
7 maxEnum uint
8 }
9
10 func newEnumSupport(toString map[uint]string) enumSupport {
11 toEnum, maxEnum := map[string]uint{}, uint(0)
12 for k, v := range toString {
13 toEnum[v] = k
14 if maxEnum < k {
15 maxEnum = k
16 }
17 }
18 return enumSupport{toString: toString, toEnum: toEnum, maxEnum: maxEnum}
19 }
20
21 func (es enumSupport) String(e uint) string {
22 if e > es.maxEnum {
23 return es.toString[0]
24 }
25 return es.toString[e]
26 }
27
28 func (es enumSupport) UnmarshJSON(b []byte) (uint, error) {
29 var dec string
30 if err := json.Unmarshal(b, &dec); err != nil {
31 return 0, err
32 }
33 out := es.toEnum[dec] // if we miss we get 0 which is what we want anyway
34 return out, nil
35 }
36
37 func (es enumSupport) MarshJSON(e uint) ([]byte, error) {
38 if e == 0 || e > es.maxEnum {
39 return json.Marshal(nil)
40 }
41 return json.Marshal(es.toString[e])
42 }
0 /*
1 Package gomega/gmeasure provides support for benchmarking and measuring code. It is intended as a more robust replacement for Ginkgo V1's Measure nodes.
2
3 gmeasure is organized around the metaphor of an Experiment that can record multiple Measurements. A Measurement is a named collection of data points and gmeasure supports
4 measuring Values (of type float64) and Durations (of type time.Duration).
5
6 Experiments allows the user to record Measurements directly by passing in Values (i.e. float64) or Durations (i.e. time.Duration)
7 or to measure measurements by passing in functions to measure. When measuring functions Experiments take care of timing the duration of functions (for Duration measurements)
8 and/or recording returned values (for Value measurements). Experiments also support sampling functions - when told to sample Experiments will run functions repeatedly
9 and measure and record results. The sampling behavior is configured by passing in a SamplingConfig that can control the maximum number of samples, the maximum duration for sampling (or both)
10 and the number of concurrent samples to take.
11
12 Measurements can be decorated with additional information. This is supported by passing in special typed decorators when recording measurements. These include:
13
14 - Units("any string") - to attach units to a Value Measurement (Duration Measurements always have units of "duration")
15 - Style("any Ginkgo color style string") - to attach styling to a Measurement. This styling is used when rendering console information about the measurement in reports. Color style strings are documented at TODO.
16 - Precision(integer or time.Duration) - to attach precision to a Measurement. This controls how many decimal places to show for Value Measurements and how to round Duration Measurements when rendering them to screen.
17
18 In addition, individual data points in a Measurement can be annotated with an Annotation("any string"). The annotation is associated with the individual data point and is intended to convey additional context about the data point.
19
20 Once measurements are complete, an Experiment can generate a comprehensive report by calling its String() or ColorableString() method.
21
22 Users can also access and analyze the resulting Measurements directly. Use Experiment.Get(NAME) to fetch the Measurement named NAME. This returned struct will have fields containing
23 all the data points and annotations recorded by the experiment. You can subsequently fetch the Measurement.Stats() to get a Stats struct that contains basic statistical information about the
24 Measurement (min, max, median, mean, standard deviation). You can order these Stats objects using RankStats() to identify best/worst performers across multpile experiments or measurements.
25
26 gmeasure also supports caching Experiments via an ExperimentCache. The cache supports storing and retreiving experiments by name and version. This allows you to rerun code without
27 repeating expensive experiments that may not have changed (which can be controlled by the cache version number). It also enables you to compare new experiment runs with older runs to detect
28 variations in performance/behavior.
29
30 When used with Ginkgo, you can emit experiment reports and encode them in test reports easily using Ginkgo V2's support for Report Entries.
31 Simply pass your experiment to AddReportEntry to get a report every time the tests run. You can also use AddReportEntry with Measurements to emit all the captured data
32 and Rankings to emit measurement summaries in rank order.
33
34 Finally, Experiments provide an additional mechanism to measure durations called a Stopwatch. The Stopwatch makes it easy to pepper code with statements that measure elapsed time across
35 different sections of code and can be useful when debugging or evaluating bottlenecks in a given codepath.
36 */
37 package gmeasure
38
39 import (
40 "fmt"
41 "math"
42 "reflect"
43 "sync"
44 "time"
45
46 "github.com/onsi/gomega/gmeasure/table"
47 )
48
49 /*
50 SamplingConfig configures the Sample family of experiment methods.
51 These methods invoke passed-in functions repeatedly to sample and record a given measurement.
52 SamplingConfig is used to control the maximum number of samples or time spent sampling (or both). When both are specified sampling ends as soon as one of the conditions is met.
53 SamplingConfig can also ensure a minimum interval between samples and can enable concurrent sampling.
54 */
55 type SamplingConfig struct {
56 // N - the maximum number of samples to record
57 N int
58 // Duration - the maximum amount of time to spend recording samples
59 Duration time.Duration
60 // MinSamplingInterval - the minimum time that must elapse between samplings. It is an error to specify both MinSamplingInterval and NumParallel.
61 MinSamplingInterval time.Duration
62 // NumParallel - the number of parallel workers to spin up to record samples. It is an error to specify both MinSamplingInterval and NumParallel.
63 NumParallel int
64 }
65
66 // The Units decorator allows you to specify units (an arbitrary string) when recording values. It is ignored when recording durations.
67 //
68 // e := gmeasure.NewExperiment("My Experiment")
69 // e.RecordValue("length", 3.141, gmeasure.Units("inches"))
70 //
71 // Units are only set the first time a value of a given name is recorded. In the example above any subsequent calls to e.RecordValue("length", X) will maintain the "inches" units even if a new set of Units("UNIT") are passed in later.
72 type Units string
73
74 // The Annotation decorator allows you to attach an annotation to a given recorded data-point:
75 //
76 // For example:
77 //
78 // e := gmeasure.NewExperiment("My Experiment")
79 // e.RecordValue("length", 3.141, gmeasure.Annotation("bob"))
80 // e.RecordValue("length", 2.71, gmeasure.Annotation("jane"))
81 //
82 // ...will result in a Measurement named "length" that records two values )[3.141, 2.71]) annotation with (["bob", "jane"])
83 type Annotation string
84
85 // The Style decorator allows you to associate a style with a measurement. This is used to generate colorful console reports using Ginkgo V2's
86 // console formatter. Styles are strings in curly brackets that correspond to a color or style.
87 //
88 // For example:
89 //
90 // e := gmeasure.NewExperiment("My Experiment")
91 // e.RecordValue("length", 3.141, gmeasure.Style("{{blue}}{{bold}}"))
92 // e.RecordValue("length", 2.71)
93 // e.RecordDuration("cooking time", 3 * time.Second, gmeasure.Style("{{red}}{{underline}}"))
94 // e.RecordDuration("cooking time", 2 * time.Second)
95 //
96 // will emit a report with blue bold entries for the length measurement and red underlined entries for the cooking time measurement.
97 //
98 // Units are only set the first time a value or duration of a given name is recorded. In the example above any subsequent calls to e.RecordValue("length", X) will maintain the "{{blue}}{{bold}}" style even if a new Style is passed in later.
99 type Style string
100
101 // The PrecisionBundle decorator controls the rounding of value and duration measurements. See Precision().
102 type PrecisionBundle struct {
103 Duration time.Duration
104 ValueFormat string
105 }
106
107 // Precision() allows you to specify the precision of a value or duration measurement - this precision is used when rendering the measurement to screen.
108 //
109 // To control the precision of Value measurements, pass Precision an integer. This will denote the number of decimal places to render (equivalen to the format string "%.Nf")
110 // To control the precision of Duration measurements, pass Precision a time.Duration. Duration measurements will be rounded oo the nearest time.Duration when rendered.
111 //
112 // For example:
113 //
114 // e := gmeasure.NewExperiment("My Experiment")
115 // e.RecordValue("length", 3.141, gmeasure.Precision(2))
116 // e.RecordValue("length", 2.71)
117 // e.RecordDuration("cooking time", 3214 * time.Millisecond, gmeasure.Precision(100*time.Millisecond))
118 // e.RecordDuration("cooking time", 2623 * time.Millisecond)
119 func Precision(p interface{}) PrecisionBundle {
120 out := DefaultPrecisionBundle
121 switch reflect.TypeOf(p) {
122 case reflect.TypeOf(time.Duration(0)):
123 out.Duration = p.(time.Duration)
124 case reflect.TypeOf(int(0)):
125 out.ValueFormat = fmt.Sprintf("%%.%df", p.(int))
126 default:
127 panic("invalid precision type, must be time.Duration or int")
128 }
129 return out
130 }
131
132 // DefaultPrecisionBundle captures the default precisions for Vale and Duration measurements.
133 var DefaultPrecisionBundle = PrecisionBundle{
134 Duration: 100 * time.Microsecond,
135 ValueFormat: "%.3f",
136 }
137
138 type extractedDecorations struct {
139 annotation Annotation
140 units Units
141 precisionBundle PrecisionBundle
142 style Style
143 }
144
145 func extractDecorations(args []interface{}) extractedDecorations {
146 var out extractedDecorations
147 out.precisionBundle = DefaultPrecisionBundle
148
149 for _, arg := range args {
150 switch reflect.TypeOf(arg) {
151 case reflect.TypeOf(out.annotation):
152 out.annotation = arg.(Annotation)
153 case reflect.TypeOf(out.units):
154 out.units = arg.(Units)
155 case reflect.TypeOf(out.precisionBundle):
156 out.precisionBundle = arg.(PrecisionBundle)
157 case reflect.TypeOf(out.style):
158 out.style = arg.(Style)
159 default:
160 panic(fmt.Sprintf("unrecognized argument %#v", arg))
161 }
162 }
163
164 return out
165 }
166
167 /*
168 Experiment is gmeasure's core data type. You use experiments to record Measurements and generate reports.
169 Experiments are thread-safe and all methods can be called from multiple goroutines.
170 */
171 type Experiment struct {
172 Name string
173
174 // Measurements includes all Measurements recorded by this experiment. You should access them by name via Get() and GetStats()
175 Measurements Measurements
176 lock *sync.Mutex
177 }
178
179 /*
180 NexExperiment creates a new experiment with the passed-in name.
181
182 When using Ginkgo we recommend immediately registering the experiment as a ReportEntry:
183
184 experiment = NewExperiment("My Experiment")
185 AddReportEntry(experiment.Name, experiment)
186
187 this will ensure an experiment report is emitted as part of the test output and exported with any test reports.
188 */
189 func NewExperiment(name string) *Experiment {
190 experiment := &Experiment{
191 Name: name,
192 lock: &sync.Mutex{},
193 }
194 return experiment
195 }
196
197 func (e *Experiment) report(enableStyling bool) string {
198 t := table.NewTable()
199 t.TableStyle.EnableTextStyling = enableStyling
200 t.AppendRow(table.R(
201 table.C("Name"), table.C("N"), table.C("Min"), table.C("Median"), table.C("Mean"), table.C("StdDev"), table.C("Max"),
202 table.Divider("="),
203 "{{bold}}",
204 ))
205
206 for _, measurement := range e.Measurements {
207 r := table.R(measurement.Style)
208 t.AppendRow(r)
209 switch measurement.Type {
210 case MeasurementTypeNote:
211 r.AppendCell(table.C(measurement.Note))
212 case MeasurementTypeValue, MeasurementTypeDuration:
213 name := measurement.Name
214 if measurement.Units != "" {
215 name += " [" + measurement.Units + "]"
216 }
217 r.AppendCell(table.C(name))
218 r.AppendCell(measurement.Stats().cells()...)
219 }
220 }
221
222 out := e.Name + "\n"
223 if enableStyling {
224 out = "{{bold}}" + out + "{{/}}"
225 }
226 out += t.Render()
227 return out
228 }
229
230 /*
231 ColorableString returns a Ginkgo formatted summary of the experiment and all its Measurements.
232 It is called automatically by Ginkgo's reporting infrastructure when the Experiment is registered as a ReportEntry via AddReportEntry.
233 */
234 func (e *Experiment) ColorableString() string {
235 return e.report(true)
236 }
237
238 /*
239 ColorableString returns an unformatted summary of the experiment and all its Measurements.
240 */
241 func (e *Experiment) String() string {
242 return e.report(false)
243 }
244
245 /*
246 RecordNote records a Measurement of type MeasurementTypeNote - this is simply a textual note to annotate the experiment. It will be emitted in any experiment reports.
247
248 RecordNote supports the Style() decoration.
249 */
250 func (e *Experiment) RecordNote(note string, args ...interface{}) {
251 decorations := extractDecorations(args)
252
253 e.lock.Lock()
254 defer e.lock.Unlock()
255 e.Measurements = append(e.Measurements, Measurement{
256 ExperimentName: e.Name,
257 Type: MeasurementTypeNote,
258 Note: note,
259 Style: string(decorations.style),
260 })
261 }
262
263 /*
264 RecordDuration records the passed-in duration on a Duration Measurement with the passed-in name. If the Measurement does not exist it is created.
265
266 RecordDuration supports the Style(), Precision(), and Annotation() decorations.
267 */
268 func (e *Experiment) RecordDuration(name string, duration time.Duration, args ...interface{}) {
269 decorations := extractDecorations(args)
270 e.recordDuration(name, duration, decorations)
271 }
272
273 /*
274 MeasureDuration runs the passed-in callback and times how long it takes to complete. The resulting duration is recorded on a Duration Measurement with the passed-in name. If the Measurement does not exist it is created.
275
276 MeasureDuration supports the Style(), Precision(), and Annotation() decorations.
277 */
278 func (e *Experiment) MeasureDuration(name string, callback func(), args ...interface{}) time.Duration {
279 t := time.Now()
280 callback()
281 duration := time.Since(t)
282 e.RecordDuration(name, duration, args...)
283 return duration
284 }
285
286 /*
287 SampleDuration samples the passed-in callback and times how long it takes to complete each sample.
288 The resulting durations are recorded on a Duration Measurement with the passed-in name. If the Measurement does not exist it is created.
289
290 The callback is given a zero-based index that increments by one between samples. The Sampling is configured via the passed-in SamplingConfig
291
292 SampleDuration supports the Style(), Precision(), and Annotation() decorations. When passed an Annotation() the same annotation is applied to all sample measurements.
293 */
294 func (e *Experiment) SampleDuration(name string, callback func(idx int), samplingConfig SamplingConfig, args ...interface{}) {
295 decorations := extractDecorations(args)
296 e.Sample(func(idx int) {
297 t := time.Now()
298 callback(idx)
299 duration := time.Since(t)
300 e.recordDuration(name, duration, decorations)
301 }, samplingConfig)
302 }
303
304 /*
305 SampleDuration samples the passed-in callback and times how long it takes to complete each sample.
306 The resulting durations are recorded on a Duration Measurement with the passed-in name. If the Measurement does not exist it is created.
307
308 The callback is given a zero-based index that increments by one between samples. The callback must return an Annotation - this annotation is attached to the measured duration.
309
310 The Sampling is configured via the passed-in SamplingConfig
311
312 SampleAnnotatedDuration supports the Style() and Precision() decorations.
313 */
314 func (e *Experiment) SampleAnnotatedDuration(name string, callback func(idx int) Annotation, samplingConfig SamplingConfig, args ...interface{}) {
315 decorations := extractDecorations(args)
316 e.Sample(func(idx int) {
317 t := time.Now()
318 decorations.annotation = callback(idx)
319 duration := time.Since(t)
320 e.recordDuration(name, duration, decorations)
321 }, samplingConfig)
322 }
323
324 func (e *Experiment) recordDuration(name string, duration time.Duration, decorations extractedDecorations) {
325 e.lock.Lock()
326 defer e.lock.Unlock()
327 idx := e.Measurements.IdxWithName(name)
328 if idx == -1 {
329 measurement := Measurement{
330 ExperimentName: e.Name,
331 Type: MeasurementTypeDuration,
332 Name: name,
333 Units: "duration",
334 Durations: []time.Duration{duration},
335 PrecisionBundle: decorations.precisionBundle,
336 Style: string(decorations.style),
337 Annotations: []string{string(decorations.annotation)},
338 }
339 e.Measurements = append(e.Measurements, measurement)
340 } else {
341 if e.Measurements[idx].Type != MeasurementTypeDuration {
342 panic(fmt.Sprintf("attempting to record duration with name '%s'. That name is already in-use for recording values.", name))
343 }
344 e.Measurements[idx].Durations = append(e.Measurements[idx].Durations, duration)
345 e.Measurements[idx].Annotations = append(e.Measurements[idx].Annotations, string(decorations.annotation))
346 }
347 }
348
349 /*
350 NewStopwatch() returns a stopwatch configured to record duration measurements with this experiment.
351 */
352 func (e *Experiment) NewStopwatch() *Stopwatch {
353 return newStopwatch(e)
354 }
355
356 /*
357 RecordValue records the passed-in value on a Value Measurement with the passed-in name. If the Measurement does not exist it is created.
358
359 RecordValue supports the Style(), Units(), Precision(), and Annotation() decorations.
360 */
361 func (e *Experiment) RecordValue(name string, value float64, args ...interface{}) {
362 decorations := extractDecorations(args)
363 e.recordValue(name, value, decorations)
364 }
365
366 /*
367 MeasureValue runs the passed-in callback and records the return value on a Value Measurement with the passed-in name. If the Measurement does not exist it is created.
368
369 MeasureValue supports the Style(), Units(), Precision(), and Annotation() decorations.
370 */
371 func (e *Experiment) MeasureValue(name string, callback func() float64, args ...interface{}) float64 {
372 value := callback()
373 e.RecordValue(name, value, args...)
374 return value
375 }
376
377 /*
378 SampleValue samples the passed-in callback and records the return value on a Value Measurement with the passed-in name. If the Measurement does not exist it is created.
379
380 The callback is given a zero-based index that increments by one between samples. The callback must return a float64. The Sampling is configured via the passed-in SamplingConfig
381
382 SampleValue supports the Style(), Units(), Precision(), and Annotation() decorations. When passed an Annotation() the same annotation is applied to all sample measurements.
383 */
384 func (e *Experiment) SampleValue(name string, callback func(idx int) float64, samplingConfig SamplingConfig, args ...interface{}) {
385 decorations := extractDecorations(args)
386 e.Sample(func(idx int) {
387 value := callback(idx)
388 e.recordValue(name, value, decorations)
389 }, samplingConfig)
390 }
391
392 /*
393 SampleAnnotatedValue samples the passed-in callback and records the return value on a Value Measurement with the passed-in name. If the Measurement does not exist it is created.
394
395 The callback is given a zero-based index that increments by one between samples. The callback must return a float64 and an Annotation - the annotation is attached to the recorded value.
396
397 The Sampling is configured via the passed-in SamplingConfig
398
399 SampleValue supports the Style(), Units(), and Precision() decorations.
400 */
401 func (e *Experiment) SampleAnnotatedValue(name string, callback func(idx int) (float64, Annotation), samplingConfig SamplingConfig, args ...interface{}) {
402 decorations := extractDecorations(args)
403 e.Sample(func(idx int) {
404 var value float64
405 value, decorations.annotation = callback(idx)
406 e.recordValue(name, value, decorations)
407 }, samplingConfig)
408 }
409
410 func (e *Experiment) recordValue(name string, value float64, decorations extractedDecorations) {
411 e.lock.Lock()
412 defer e.lock.Unlock()
413 idx := e.Measurements.IdxWithName(name)
414 if idx == -1 {
415 measurement := Measurement{
416 ExperimentName: e.Name,
417 Type: MeasurementTypeValue,
418 Name: name,
419 Style: string(decorations.style),
420 Units: string(decorations.units),
421 PrecisionBundle: decorations.precisionBundle,
422 Values: []float64{value},
423 Annotations: []string{string(decorations.annotation)},
424 }
425 e.Measurements = append(e.Measurements, measurement)
426 } else {
427 if e.Measurements[idx].Type != MeasurementTypeValue {
428 panic(fmt.Sprintf("attempting to record value with name '%s'. That name is already in-use for recording durations.", name))
429 }
430 e.Measurements[idx].Values = append(e.Measurements[idx].Values, value)
431 e.Measurements[idx].Annotations = append(e.Measurements[idx].Annotations, string(decorations.annotation))
432 }
433 }
434
435 /*
436 Sample samples the passed-in callback repeatedly. The sampling is governed by the passed in SamplingConfig.
437
438 The SamplingConfig can limit the total number of samples and/or the total time spent sampling the callback.
439 The SamplingConfig can also instruct Sample to run with multiple concurrent workers.
440
441 The callback is called with a zero-based index that incerements by one between samples.
442 */
443 func (e *Experiment) Sample(callback func(idx int), samplingConfig SamplingConfig) {
444 if samplingConfig.N == 0 && samplingConfig.Duration == 0 {
445 panic("you must specify at least one of SamplingConfig.N and SamplingConfig.Duration")
446 }
447 if samplingConfig.MinSamplingInterval > 0 && samplingConfig.NumParallel > 1 {
448 panic("you cannot specify both SamplingConfig.MinSamplingInterval and SamplingConfig.NumParallel")
449 }
450 maxTime := time.Now().Add(100000 * time.Hour)
451 if samplingConfig.Duration > 0 {
452 maxTime = time.Now().Add(samplingConfig.Duration)
453 }
454 maxN := math.MaxInt64
455 if samplingConfig.N > 0 {
456 maxN = samplingConfig.N
457 }
458 numParallel := 1
459 if samplingConfig.NumParallel > numParallel {
460 numParallel = samplingConfig.NumParallel
461 }
462 minSamplingInterval := samplingConfig.MinSamplingInterval
463
464 work := make(chan int)
465 if numParallel > 1 {
466 for worker := 0; worker < numParallel; worker++ {
467 go func() {
468 for idx := range work {
469 callback(idx)
470 }
471 }()
472 }
473 }
474
475 idx := 0
476 var avgDt time.Duration
477 for {
478 t := time.Now()
479 if numParallel > 1 {
480 work <- idx
481 } else {
482 callback(idx)
483 }
484 dt := time.Since(t)
485 if numParallel == 1 && dt < minSamplingInterval {
486 time.Sleep(minSamplingInterval - dt)
487 dt = time.Since(t)
488 }
489 if idx >= numParallel {
490 avgDt = (avgDt*time.Duration(idx-numParallel) + dt) / time.Duration(idx-numParallel+1)
491 }
492 idx += 1
493 if idx >= maxN {
494 return
495 }
496 if time.Now().Add(avgDt).After(maxTime) {
497 return
498 }
499 }
500 }
501
502 /*
503 Get returns the Measurement with the associated name. If no Measurement is found a zero Measurement{} is returned.
504 */
505 func (e *Experiment) Get(name string) Measurement {
506 e.lock.Lock()
507 defer e.lock.Unlock()
508 idx := e.Measurements.IdxWithName(name)
509 if idx == -1 {
510 return Measurement{}
511 }
512 return e.Measurements[idx]
513 }
514
515 /*
516 GetStats returns the Stats for the Measurement with the associated name. If no Measurement is found a zero Stats{} is returned.
517
518 experiment.GetStats(name) is equivalent to experiment.Get(name).Stats()
519 */
520 func (e *Experiment) GetStats(name string) Stats {
521 measurement := e.Get(name)
522 e.lock.Lock()
523 defer e.lock.Unlock()
524 return measurement.Stats()
525 }
0 package gmeasure_test
1
2 import (
3 "fmt"
4 "strings"
5 "sync"
6 "time"
7
8 . "github.com/onsi/ginkgo"
9 . "github.com/onsi/gomega"
10
11 "github.com/onsi/gomega/gmeasure"
12 )
13
14 var _ = Describe("Experiment", func() {
15 var e *gmeasure.Experiment
16 BeforeEach(func() {
17 e = gmeasure.NewExperiment("Test Experiment")
18 })
19
20 Describe("Recording Notes", func() {
21 It("creates a note Measurement", func() {
22 e.RecordNote("I'm a note", gmeasure.Style("{{blue}}"))
23 measurement := e.Measurements[0]
24 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeNote))
25 Ω(measurement.ExperimentName).Should(Equal("Test Experiment"))
26 Ω(measurement.Note).Should(Equal("I'm a note"))
27 Ω(measurement.Style).Should(Equal("{{blue}}"))
28 })
29 })
30
31 Describe("Recording Durations", func() {
32 commonMeasurementAssertions := func() gmeasure.Measurement {
33 measurement := e.Get("runtime")
34 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeDuration))
35 Ω(measurement.ExperimentName).Should(Equal("Test Experiment"))
36 Ω(measurement.Name).Should(Equal("runtime"))
37 Ω(measurement.Units).Should(Equal("duration"))
38 Ω(measurement.Style).Should(Equal("{{red}}"))
39 Ω(measurement.PrecisionBundle.Duration).Should(Equal(time.Millisecond))
40 return measurement
41 }
42
43 BeforeEach(func() {
44 e.RecordDuration("runtime", time.Second, gmeasure.Annotation("first"), gmeasure.Style("{{red}}"), gmeasure.Precision(time.Millisecond), gmeasure.Units("ignored"))
45 })
46
47 Describe("RecordDuration", func() {
48 It("generates a measurement and records the passed-in duration along with any relevant decorations", func() {
49 e.RecordDuration("runtime", time.Minute, gmeasure.Annotation("second"))
50 measurement := commonMeasurementAssertions()
51 Ω(measurement.Durations).Should(Equal([]time.Duration{time.Second, time.Minute}))
52 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
53 })
54 })
55
56 Describe("MeasureDuration", func() {
57 It("measure the duration of the passed-in function", func() {
58 e.MeasureDuration("runtime", func() {
59 time.Sleep(200 * time.Millisecond)
60 }, gmeasure.Annotation("second"))
61 measurement := commonMeasurementAssertions()
62 Ω(measurement.Durations[0]).Should(Equal(time.Second))
63 Ω(measurement.Durations[1]).Should(BeNumerically("~", 200*time.Millisecond, 20*time.Millisecond))
64 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
65 })
66 })
67
68 Describe("SampleDuration", func() {
69 It("samples the passed-in function according to SampleConfig and records the measured durations", func() {
70 e.SampleDuration("runtime", func(_ int) {
71 time.Sleep(100 * time.Millisecond)
72 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled"))
73 measurement := commonMeasurementAssertions()
74 Ω(measurement.Durations[0]).Should(Equal(time.Second))
75 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
76 Ω(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
77 Ω(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
78 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"}))
79 })
80 })
81
82 Describe("SampleAnnotatedDuration", func() {
83 It("samples the passed-in function according to SampleConfig and records the measured durations and returned annotations", func() {
84 e.SampleAnnotatedDuration("runtime", func(idx int) gmeasure.Annotation {
85 time.Sleep(100 * time.Millisecond)
86 return gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1))
87 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored"))
88 measurement := commonMeasurementAssertions()
89 Ω(measurement.Durations[0]).Should(Equal(time.Second))
90 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
91 Ω(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
92 Ω(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
93 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"}))
94 })
95 })
96 })
97
98 Describe("Stopwatch Support", func() {
99 It("can generate a new stopwatch tied to the experiment", func() {
100 s := e.NewStopwatch()
101 time.Sleep(50 * time.Millisecond)
102 s.Record("runtime", gmeasure.Annotation("first")).Reset()
103 time.Sleep(100 * time.Millisecond)
104 s.Record("runtime", gmeasure.Annotation("second")).Reset()
105 time.Sleep(150 * time.Millisecond)
106 s.Record("runtime", gmeasure.Annotation("third"))
107 measurement := e.Get("runtime")
108 Ω(measurement.Durations[0]).Should(BeNumerically("~", 50*time.Millisecond, 20*time.Millisecond))
109 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
110 Ω(measurement.Durations[2]).Should(BeNumerically("~", 150*time.Millisecond, 20*time.Millisecond))
111 Ω(measurement.Annotations).Should(Equal([]string{"first", "second", "third"}))
112 })
113 })
114
115 Describe("Recording Values", func() {
116 commonMeasurementAssertions := func() gmeasure.Measurement {
117 measurement := e.Get("sprockets")
118 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeValue))
119 Ω(measurement.ExperimentName).Should(Equal("Test Experiment"))
120 Ω(measurement.Name).Should(Equal("sprockets"))
121 Ω(measurement.Units).Should(Equal("widgets"))
122 Ω(measurement.Style).Should(Equal("{{yellow}}"))
123 Ω(measurement.PrecisionBundle.ValueFormat).Should(Equal("%.0f"))
124 return measurement
125 }
126
127 BeforeEach(func() {
128 e.RecordValue("sprockets", 3.2, gmeasure.Annotation("first"), gmeasure.Style("{{yellow}}"), gmeasure.Precision(0), gmeasure.Units("widgets"))
129 })
130
131 Describe("RecordValue", func() {
132 It("generates a measurement and records the passed-in value along with any relevant decorations", func() {
133 e.RecordValue("sprockets", 17.4, gmeasure.Annotation("second"))
134 measurement := commonMeasurementAssertions()
135 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4}))
136 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
137 })
138 })
139
140 Describe("MeasureValue", func() {
141 It("records the value returned by the passed-in function", func() {
142 e.MeasureValue("sprockets", func() float64 {
143 return 17.4
144 }, gmeasure.Annotation("second"))
145 measurement := commonMeasurementAssertions()
146 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4}))
147 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"}))
148 })
149 })
150
151 Describe("SampleValue", func() {
152 It("samples the passed-in function according to SampleConfig and records the resulting values", func() {
153 e.SampleValue("sprockets", func(idx int) float64 {
154 return 17.4 + float64(idx)
155 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled"))
156 measurement := commonMeasurementAssertions()
157 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4}))
158 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"}))
159 })
160 })
161
162 Describe("SampleAnnotatedValue", func() {
163 It("samples the passed-in function according to SampleConfig and records the returned values and annotations", func() {
164 e.SampleAnnotatedValue("sprockets", func(idx int) (float64, gmeasure.Annotation) {
165 return 17.4 + float64(idx), gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1))
166 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored"))
167 measurement := commonMeasurementAssertions()
168 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4}))
169 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"}))
170 })
171 })
172 })
173
174 Describe("Sampling", func() {
175 var indices []int
176 BeforeEach(func() {
177 indices = []int{}
178 })
179
180 ints := func(n int) []int {
181 out := []int{}
182 for i := 0; i < n; i++ {
183 out = append(out, i)
184 }
185 return out
186 }
187
188 It("calls the function repeatedly passing in an index", func() {
189 e.Sample(func(idx int) {
190 indices = append(indices, idx)
191 }, gmeasure.SamplingConfig{N: 3})
192
193 Ω(indices).Should(Equal(ints(3)))
194 })
195
196 It("can cap the maximum number of samples", func() {
197 e.Sample(func(idx int) {
198 indices = append(indices, idx)
199 }, gmeasure.SamplingConfig{N: 10, Duration: time.Minute})
200
201 Ω(indices).Should(Equal(ints(10)))
202 })
203
204 It("can cap the maximum sample time", func() {
205 e.Sample(func(idx int) {
206 indices = append(indices, idx)
207 time.Sleep(10 * time.Millisecond)
208 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond, MinSamplingInterval: 5 * time.Millisecond})
209
210 Ω(len(indices)).Should(BeNumerically("~", 10, 3))
211 Ω(indices).Should(Equal(ints(len(indices))))
212 })
213
214 It("can ensure a minimum interval between samples", func() {
215 times := map[int]time.Time{}
216 e.Sample(func(idx int) {
217 times[idx] = time.Now()
218 }, gmeasure.SamplingConfig{N: 10, Duration: 200 * time.Millisecond, MinSamplingInterval: 50 * time.Millisecond, NumParallel: 1})
219
220 Ω(len(times)).Should(BeNumerically("~", 4, 2))
221 Ω(times[1]).Should(BeTemporally(">", times[0], 50*time.Millisecond))
222 Ω(times[2]).Should(BeTemporally(">", times[1], 50*time.Millisecond))
223 })
224
225 It("can run samples in parallel", func() {
226 lock := &sync.Mutex{}
227
228 e.Sample(func(idx int) {
229 lock.Lock()
230 indices = append(indices, idx)
231 lock.Unlock()
232 time.Sleep(10 * time.Millisecond)
233 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond, NumParallel: 3})
234
235 lock.Lock()
236 defer lock.Unlock()
237 Ω(len(indices)).Should(BeNumerically("~", 30, 10))
238 Ω(indices).Should(ConsistOf(ints(len(indices))))
239 })
240
241 It("panics if the SamplingConfig does not specify a ceiling", func() {
242 Expect(func() {
243 e.Sample(func(_ int) {}, gmeasure.SamplingConfig{MinSamplingInterval: time.Second})
244 }).To(PanicWith("you must specify at least one of SamplingConfig.N and SamplingConfig.Duration"))
245 })
246
247 It("panics if the SamplingConfig includes both a minimum interval and a directive to run in parallel", func() {
248 Expect(func() {
249 e.Sample(func(_ int) {}, gmeasure.SamplingConfig{N: 10, MinSamplingInterval: time.Second, NumParallel: 2})
250 }).To(PanicWith("you cannot specify both SamplingConfig.MinSamplingInterval and SamplingConfig.NumParallel"))
251 })
252 })
253
254 Describe("recording multiple entries", func() {
255 It("always appends to the correct measurement (by name)", func() {
256 e.RecordDuration("alpha", time.Second)
257 e.RecordDuration("beta", time.Minute)
258 e.RecordValue("gamma", 1)
259 e.RecordValue("delta", 2.71)
260 e.RecordDuration("alpha", 2*time.Second)
261 e.RecordDuration("beta", 2*time.Minute)
262 e.RecordValue("gamma", 2)
263 e.RecordValue("delta", 3.141)
264
265 Ω(e.Measurements).Should(HaveLen(4))
266 Ω(e.Get("alpha").Durations).Should(Equal([]time.Duration{time.Second, 2 * time.Second}))
267 Ω(e.Get("beta").Durations).Should(Equal([]time.Duration{time.Minute, 2 * time.Minute}))
268 Ω(e.Get("gamma").Values).Should(Equal([]float64{1, 2}))
269 Ω(e.Get("delta").Values).Should(Equal([]float64{2.71, 3.141}))
270 })
271
272 It("panics if you incorrectly mix types", func() {
273 e.RecordDuration("runtime", time.Second)
274 Ω(func() {
275 e.RecordValue("runtime", 3.141)
276 }).Should(PanicWith("attempting to record value with name 'runtime'. That name is already in-use for recording durations."))
277
278 e.RecordValue("sprockets", 2)
279 Ω(func() {
280 e.RecordDuration("sprockets", time.Minute)
281 }).Should(PanicWith("attempting to record duration with name 'sprockets'. That name is already in-use for recording values."))
282 })
283 })
284
285 Describe("Decorators", func() {
286 It("uses the default precisions when none is specified", func() {
287 e.RecordValue("sprockets", 2)
288 e.RecordDuration("runtime", time.Minute)
289
290 Ω(e.Get("sprockets").PrecisionBundle.ValueFormat).Should(Equal("%.3f"))
291 Ω(e.Get("runtime").PrecisionBundle.Duration).Should(Equal(100 * time.Microsecond))
292 })
293
294 It("panics if an unsupported type is passed into Precision", func() {
295 Ω(func() {
296 gmeasure.Precision("aardvark")
297 }).Should(PanicWith("invalid precision type, must be time.Duration or int"))
298 })
299
300 It("panics if an unrecognized argumnet is passed in", func() {
301 Ω(func() {
302 e.RecordValue("sprockets", 2, "boom")
303 }).Should(PanicWith(`unrecognized argument "boom"`))
304 })
305 })
306
307 Describe("Getting Measurements", func() {
308 Context("when the Measurement does not exist", func() {
309 It("returns the zero Measurement", func() {
310 Ω(e.Get("not here")).Should(BeZero())
311 })
312 })
313 })
314
315 Describe("Getting Stats", func() {
316 It("returns the Measurement's Stats", func() {
317 e.RecordValue("alpha", 1)
318 e.RecordValue("alpha", 2)
319 e.RecordValue("alpha", 3)
320 Ω(e.GetStats("alpha")).Should(Equal(e.Get("alpha").Stats()))
321 })
322 })
323
324 Describe("Generating Reports", func() {
325 BeforeEach(func() {
326 e.RecordNote("A note")
327 e.RecordValue("sprockets", 7, gmeasure.Units("widgets"), gmeasure.Precision(0), gmeasure.Style("{{yellow}}"), gmeasure.Annotation("sprockets-1"))
328 e.RecordDuration("runtime", time.Second, gmeasure.Precision(100*time.Millisecond), gmeasure.Style("{{red}}"), gmeasure.Annotation("runtime-1"))
329 e.RecordNote("A blue note", gmeasure.Style("{{blue}}"))
330 e.RecordValue("gear ratio", 10.3, gmeasure.Precision(2), gmeasure.Style("{{green}}"), gmeasure.Annotation("ratio-1"))
331
332 e.RecordValue("sprockets", 8, gmeasure.Annotation("sprockets-2"))
333 e.RecordValue("sprockets", 9, gmeasure.Annotation("sprockets-3"))
334
335 e.RecordDuration("runtime", 2*time.Second, gmeasure.Annotation("runtime-2"))
336 e.RecordValue("gear ratio", 13.758, gmeasure.Precision(2), gmeasure.Annotation("ratio-2"))
337 })
338
339 It("emits a nicely formatted table", func() {
340 expected := strings.Join([]string{
341 "Test Experiment",
342 "Name | N | Min | Median | Mean | StdDev | Max ",
343 "=============================================================================",
344 "A note ",
345 "-----------------------------------------------------------------------------",
346 "sprockets [widgets] | 3 | 7 | 8 | 8 | 1 | 9 ",
347 " | | sprockets-1 | | | | sprockets-3",
348 "-----------------------------------------------------------------------------",
349 "runtime [duration] | 2 | 1s | 1.5s | 1.5s | 500ms | 2s ",
350 " | | runtime-1 | | | | runtime-2 ",
351 "-----------------------------------------------------------------------------",
352 "A blue note ",
353 "-----------------------------------------------------------------------------",
354 "gear ratio | 2 | 10.30 | 12.03 | 12.03 | 1.73 | 13.76 ",
355 " | | ratio-1 | | | | ratio-2 ",
356 "",
357 }, "\n")
358 Ω(e.String()).Should(Equal(expected))
359 })
360
361 It("can also emit a styled table", func() {
362 expected := strings.Join([]string{
363 "{{bold}}Test Experiment",
364 "{{/}}{{bold}}Name {{/}} | {{bold}}N{{/}} | {{bold}}Min {{/}} | {{bold}}Median{{/}} | {{bold}}Mean {{/}} | {{bold}}StdDev{{/}} | {{bold}}Max {{/}}",
365 "=============================================================================",
366 "A note ",
367 "-----------------------------------------------------------------------------",
368 "{{yellow}}sprockets [widgets]{{/}} | {{yellow}}3{{/}} | {{yellow}}7 {{/}} | {{yellow}}8 {{/}} | {{yellow}}8 {{/}} | {{yellow}}1 {{/}} | {{yellow}}9 {{/}}",
369 " | | {{yellow}}sprockets-1{{/}} | | | | {{yellow}}sprockets-3{{/}}",
370 "-----------------------------------------------------------------------------",
371 "{{red}}runtime [duration] {{/}} | {{red}}2{{/}} | {{red}}1s {{/}} | {{red}}1.5s {{/}} | {{red}}1.5s {{/}} | {{red}}500ms {{/}} | {{red}}2s {{/}}",
372 " | | {{red}}runtime-1 {{/}} | | | | {{red}}runtime-2 {{/}}",
373 "-----------------------------------------------------------------------------",
374 "{{blue}}A blue note {{/}}",
375 "-----------------------------------------------------------------------------",
376 "{{green}}gear ratio {{/}} | {{green}}2{{/}} | {{green}}10.30 {{/}} | {{green}}12.03 {{/}} | {{green}}12.03{{/}} | {{green}}1.73 {{/}} | {{green}}13.76 {{/}}",
377 " | | {{green}}ratio-1 {{/}} | | | | {{green}}ratio-2 {{/}}",
378 "",
379 }, "\n")
380 Ω(e.ColorableString()).Should(Equal(expected))
381 })
382 })
383 })
0 package gmeasure_test
1
2 import (
3 "testing"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 )
8
9 func TestGmeasure(t *testing.T) {
10 RegisterFailHandler(Fail)
11 RunSpecs(t, "Gmeasure Suite")
12 }
0 package gmeasure
1
2 import (
3 "fmt"
4 "math"
5 "sort"
6 "time"
7
8 "github.com/onsi/gomega/gmeasure/table"
9 )
10
11 type MeasurementType uint
12
13 const (
14 MeasurementTypeInvalid MeasurementType = iota
15 MeasurementTypeNote
16 MeasurementTypeDuration
17 MeasurementTypeValue
18 )
19
20 var letEnumSupport = newEnumSupport(map[uint]string{uint(MeasurementTypeInvalid): "INVALID LOG ENTRY TYPE", uint(MeasurementTypeNote): "Note", uint(MeasurementTypeDuration): "Duration", uint(MeasurementTypeValue): "Value"})
21
22 func (s MeasurementType) String() string { return letEnumSupport.String(uint(s)) }
23 func (s *MeasurementType) UnmarshalJSON(b []byte) error {
24 out, err := letEnumSupport.UnmarshJSON(b)
25 *s = MeasurementType(out)
26 return err
27 }
28 func (s MeasurementType) MarshalJSON() ([]byte, error) { return letEnumSupport.MarshJSON(uint(s)) }
29
30 /*
31 Measurement records all captured data for a given measurement. You generally don't make Measurements directly - but you can fetch them from Experiments using Get().
32
33 When using Ginkgo, you can register Measurements as Report Entries via AddReportEntry. This will emit all the captured data points when Ginkgo generates the report.
34 */
35 type Measurement struct {
36 // Type is the MeasurementType - one of MeasurementTypeNote, MeasurementTypeDuration, or MeasurementTypeValue
37 Type MeasurementType
38
39 // ExperimentName is the name of the experiment that this Measurement is associated with
40 ExperimentName string
41
42 // If Type is MeasurementTypeNote, Note is populated with the note text.
43 Note string
44
45 // If Type is MeasurementTypeDuration or MeasurementTypeValue, Name is the name of the recorded measurement
46 Name string
47
48 // Style captures the styling information (if any) for this Measurement
49 Style string
50
51 // Units capture the units (if any) for this Measurement. Units is set to "duration" if the Type is MeasurementTypeDuration
52 Units string
53
54 // PrecisionBundle captures the precision to use when rendering data for this Measurement.
55 // If Type is MeasurementTypeDuration then PrecisionBundle.Duration is used to round any durations before presentation.
56 // If Type is MeasurementTypeValue then PrecisionBundle.ValueFormat is used to format any values before presentation
57 PrecisionBundle PrecisionBundle
58
59 // If Type is MeasurementTypeDuration, Durations will contain all durations recorded for this measurement
60 Durations []time.Duration
61
62 // If Type is MeasurementTypeValue, Values will contain all float64s recorded for this measurement
63 Values []float64
64
65 // If Type is MeasurementTypeDuration or MeasurementTypeValue then Annotations will include string annotations for all recorded Durations or Values.
66 // If the user does not pass-in an Annotation() decoration for a particular value or duration, the corresponding entry in the Annotations slice will be the empty string ""
67 Annotations []string
68 }
69
70 type Measurements []Measurement
71
72 func (m Measurements) IdxWithName(name string) int {
73 for idx, measurement := range m {
74 if measurement.Name == name {
75 return idx
76 }
77 }
78
79 return -1
80 }
81
82 func (m Measurement) report(enableStyling bool) string {
83 out := ""
84 style := m.Style
85 if !enableStyling {
86 style = ""
87 }
88 switch m.Type {
89 case MeasurementTypeNote:
90 out += fmt.Sprintf("%s - Note\n%s\n", m.ExperimentName, m.Note)
91 if style != "" {
92 out = style + out + "{{/}}"
93 }
94 return out
95 case MeasurementTypeValue, MeasurementTypeDuration:
96 out += fmt.Sprintf("%s - %s", m.ExperimentName, m.Name)
97 if m.Units != "" {
98 out += " [" + m.Units + "]"
99 }
100 if style != "" {
101 out = style + out + "{{/}}"
102 }
103 out += "\n"
104 out += m.Stats().String() + "\n"
105 }
106 t := table.NewTable()
107 t.TableStyle.EnableTextStyling = enableStyling
108 switch m.Type {
109 case MeasurementTypeValue:
110 t.AppendRow(table.R(table.C("Value", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style))
111 for idx := range m.Values {
112 t.AppendRow(table.R(
113 table.C(fmt.Sprintf(m.PrecisionBundle.ValueFormat, m.Values[idx]), table.AlignTypeRight),
114 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft),
115 ))
116 }
117 case MeasurementTypeDuration:
118 t.AppendRow(table.R(table.C("Duration", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style))
119 for idx := range m.Durations {
120 t.AppendRow(table.R(
121 table.C(m.Durations[idx].Round(m.PrecisionBundle.Duration).String(), style, table.AlignTypeRight),
122 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft),
123 ))
124 }
125 }
126 out += t.Render()
127 return out
128 }
129
130 /*
131 ColorableString generates a styled report that includes all the data points for this Measurement.
132 It is called automatically by Ginkgo's reporting infrastructure when the Measurement is registered as a ReportEntry via AddReportEntry.
133 */
134 func (m Measurement) ColorableString() string {
135 return m.report(true)
136 }
137
138 /*
139 String generates an unstyled report that includes all the data points for this Measurement.
140 */
141 func (m Measurement) String() string {
142 return m.report(false)
143 }
144
145 /*
146 Stats returns a Stats struct summarizing the statistic of this measurement
147 */
148 func (m Measurement) Stats() Stats {
149 if m.Type == MeasurementTypeInvalid || m.Type == MeasurementTypeNote {
150 return Stats{}
151 }
152
153 out := Stats{
154 ExperimentName: m.ExperimentName,
155 MeasurementName: m.Name,
156 Style: m.Style,
157 Units: m.Units,
158 PrecisionBundle: m.PrecisionBundle,
159 }
160
161 switch m.Type {
162 case MeasurementTypeValue:
163 out.Type = StatsTypeValue
164 out.N = len(m.Values)
165 if out.N == 0 {
166 return out
167 }
168 indices, sum := make([]int, len(m.Values)), 0.0
169 for idx, v := range m.Values {
170 indices[idx] = idx
171 sum += v
172 }
173 sort.Slice(indices, func(i, j int) bool {
174 return m.Values[indices[i]] < m.Values[indices[j]]
175 })
176 out.ValueBundle = map[Stat]float64{
177 StatMin: m.Values[indices[0]],
178 StatMax: m.Values[indices[out.N-1]],
179 StatMean: sum / float64(out.N),
180 StatStdDev: 0.0,
181 }
182 out.AnnotationBundle = map[Stat]string{
183 StatMin: m.Annotations[indices[0]],
184 StatMax: m.Annotations[indices[out.N-1]],
185 }
186
187 if out.N%2 == 0 {
188 out.ValueBundle[StatMedian] = (m.Values[indices[out.N/2]] + m.Values[indices[out.N/2-1]]) / 2.0
189 } else {
190 out.ValueBundle[StatMedian] = m.Values[indices[(out.N-1)/2]]
191 }
192
193 for _, v := range m.Values {
194 out.ValueBundle[StatStdDev] += (v - out.ValueBundle[StatMean]) * (v - out.ValueBundle[StatMean])
195 }
196 out.ValueBundle[StatStdDev] = math.Sqrt(out.ValueBundle[StatStdDev] / float64(out.N))
197 case MeasurementTypeDuration:
198 out.Type = StatsTypeDuration
199 out.N = len(m.Durations)
200 if out.N == 0 {
201 return out
202 }
203 indices, sum := make([]int, len(m.Durations)), time.Duration(0)
204 for idx, v := range m.Durations {
205 indices[idx] = idx
206 sum += v
207 }
208 sort.Slice(indices, func(i, j int) bool {
209 return m.Durations[indices[i]] < m.Durations[indices[j]]
210 })
211 out.DurationBundle = map[Stat]time.Duration{
212 StatMin: m.Durations[indices[0]],
213 StatMax: m.Durations[indices[out.N-1]],
214 StatMean: sum / time.Duration(out.N),
215 }
216 out.AnnotationBundle = map[Stat]string{
217 StatMin: m.Annotations[indices[0]],
218 StatMax: m.Annotations[indices[out.N-1]],
219 }
220
221 if out.N%2 == 0 {
222 out.DurationBundle[StatMedian] = (m.Durations[indices[out.N/2]] + m.Durations[indices[out.N/2-1]]) / 2
223 } else {
224 out.DurationBundle[StatMedian] = m.Durations[indices[(out.N-1)/2]]
225 }
226 stdDev := 0.0
227 for _, v := range m.Durations {
228 stdDev += float64(v-out.DurationBundle[StatMean]) * float64(v-out.DurationBundle[StatMean])
229 }
230 out.DurationBundle[StatStdDev] = time.Duration(math.Sqrt(stdDev / float64(out.N)))
231 }
232
233 return out
234 }
0 package gmeasure_test
1
2 import (
3 "math"
4 "strings"
5 "time"
6
7 . "github.com/onsi/ginkgo"
8 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/gmeasure"
10 )
11
12 var _ = Describe("Measurement", func() {
13 var e *gmeasure.Experiment
14 var measurement gmeasure.Measurement
15
16 BeforeEach(func() {
17 e = gmeasure.NewExperiment("Test Experiment")
18 })
19
20 Describe("Note Measurement", func() {
21 BeforeEach(func() {
22 e.RecordNote("I'm a red note", gmeasure.Style("{{red}}"))
23 measurement = e.Measurements[0]
24 })
25
26 Describe("Generating Stats", func() {
27 It("returns an empty stats", func() {
28 Ω(measurement.Stats()).Should(BeZero())
29 })
30 })
31
32 Describe("Emitting an unstyled report", func() {
33 It("does not include styling", func() {
34 Ω(measurement.String()).Should(Equal("Test Experiment - Note\nI'm a red note\n"))
35 })
36 })
37
38 Describe("Emitting a styled report", func() {
39 It("does include styling", func() {
40 Ω(measurement.ColorableString()).Should(Equal("{{red}}Test Experiment - Note\nI'm a red note\n{{/}}"))
41 })
42 })
43 })
44
45 Describe("Value Measurement", func() {
46 var min, median, mean, stdDev, max float64
47 BeforeEach(func() {
48 e.RecordValue("flange widths", 7.128, gmeasure.Annotation("A"), gmeasure.Precision(2), gmeasure.Units("inches"), gmeasure.Style("{{blue}}"))
49 e.RecordValue("flange widths", 3.141, gmeasure.Annotation("B"))
50 e.RecordValue("flange widths", 9.28223, gmeasure.Annotation("C"))
51 e.RecordValue("flange widths", 14.249, gmeasure.Annotation("D"))
52 e.RecordValue("flange widths", 8.975, gmeasure.Annotation("E"))
53 measurement = e.Measurements[0]
54 min = 3.141
55 max = 14.249
56 median = 8.975
57 mean = (7.128 + 3.141 + 9.28223 + 14.249 + 8.975) / 5.0
58 stdDev = (7.128-mean)*(7.128-mean) + (3.141-mean)*(3.141-mean) + (9.28223-mean)*(9.28223-mean) + (14.249-mean)*(14.249-mean) + (8.975-mean)*(8.975-mean)
59 stdDev = math.Sqrt(stdDev / 5.0)
60 })
61
62 Describe("Generating Stats", func() {
63 It("generates a correctly configured Stats with correct values", func() {
64 stats := measurement.Stats()
65 Ω(stats.ExperimentName).Should(Equal("Test Experiment"))
66 Ω(stats.MeasurementName).Should(Equal("flange widths"))
67 Ω(stats.Style).Should(Equal("{{blue}}"))
68 Ω(stats.Units).Should(Equal("inches"))
69 Ω(stats.PrecisionBundle.ValueFormat).Should(Equal("%.2f"))
70
71 Ω(stats.ValueBundle[gmeasure.StatMin]).Should(Equal(min))
72 Ω(stats.AnnotationBundle[gmeasure.StatMin]).Should(Equal("B"))
73 Ω(stats.ValueBundle[gmeasure.StatMax]).Should(Equal(max))
74 Ω(stats.AnnotationBundle[gmeasure.StatMax]).Should(Equal("D"))
75 Ω(stats.ValueBundle[gmeasure.StatMedian]).Should(Equal(median))
76 Ω(stats.ValueBundle[gmeasure.StatMean]).Should(Equal(mean))
77 Ω(stats.ValueBundle[gmeasure.StatStdDev]).Should(Equal(stdDev))
78 })
79 })
80
81 Describe("Emitting an unstyled report", func() {
82 It("does not include styling", func() {
83 expected := strings.Join([]string{
84 "Test Experiment - flange widths [inches]",
85 "3.14 < [8.97] | <8.56> ±3.59 < 14.25",
86 "Value | Annotation",
87 "==================",
88 " 7.13 | A ",
89 "------------------",
90 " 3.14 | B ",
91 "------------------",
92 " 9.28 | C ",
93 "------------------",
94 "14.25 | D ",
95 "------------------",
96 " 8.97 | E ",
97 "",
98 }, "\n")
99 Ω(measurement.String()).Should(Equal(expected))
100 })
101 })
102
103 Describe("Emitting a styled report", func() {
104 It("does include styling", func() {
105 expected := strings.Join([]string{
106 "{{blue}}Test Experiment - flange widths [inches]{{/}}",
107 "3.14 < [8.97] | <8.56> ±3.59 < 14.25",
108 "{{blue}}Value{{/}} | {{blue}}Annotation{{/}}",
109 "==================",
110 " 7.13 | {{gray}}A {{/}}",
111 "------------------",
112 " 3.14 | {{gray}}B {{/}}",
113 "------------------",
114 " 9.28 | {{gray}}C {{/}}",
115 "------------------",
116 "14.25 | {{gray}}D {{/}}",
117 "------------------",
118 " 8.97 | {{gray}}E {{/}}",
119 "",
120 }, "\n")
121 Ω(measurement.ColorableString()).Should(Equal(expected))
122 })
123 })
124
125 Describe("Computing medians", func() {
126 Context("with an odd number of values", func() {
127 It("returns the middle element", func() {
128 e.RecordValue("odd", 5)
129 e.RecordValue("odd", 1)
130 e.RecordValue("odd", 2)
131 e.RecordValue("odd", 4)
132 e.RecordValue("odd", 3)
133
134 Ω(e.GetStats("odd").ValueBundle[gmeasure.StatMedian]).Should(Equal(3.0))
135 })
136 })
137
138 Context("when an even number of values", func() {
139 It("returns the mean of the two middle elements", func() {
140 e.RecordValue("even", 1)
141 e.RecordValue("even", 2)
142 e.RecordValue("even", 4)
143 e.RecordValue("even", 3)
144
145 Ω(e.GetStats("even").ValueBundle[gmeasure.StatMedian]).Should(Equal(2.5))
146 })
147 })
148 })
149 })
150
151 Describe("Duration Measurement", func() {
152 var min, median, mean, stdDev, max time.Duration
153 BeforeEach(func() {
154 e.RecordDuration("runtime", 7128*time.Millisecond, gmeasure.Annotation("A"), gmeasure.Precision(time.Millisecond*100), gmeasure.Style("{{blue}}"))
155 e.RecordDuration("runtime", 3141*time.Millisecond, gmeasure.Annotation("B"))
156 e.RecordDuration("runtime", 9282*time.Millisecond, gmeasure.Annotation("C"))
157 e.RecordDuration("runtime", 14249*time.Millisecond, gmeasure.Annotation("D"))
158 e.RecordDuration("runtime", 8975*time.Millisecond, gmeasure.Annotation("E"))
159 measurement = e.Measurements[0]
160 min = 3141 * time.Millisecond
161 max = 14249 * time.Millisecond
162 median = 8975 * time.Millisecond
163 mean = ((7128 + 3141 + 9282 + 14249 + 8975) * time.Millisecond) / 5
164 stdDev = time.Duration(math.Sqrt((float64(7128*time.Millisecond-mean)*float64(7128*time.Millisecond-mean) + float64(3141*time.Millisecond-mean)*float64(3141*time.Millisecond-mean) + float64(9282*time.Millisecond-mean)*float64(9282*time.Millisecond-mean) + float64(14249*time.Millisecond-mean)*float64(14249*time.Millisecond-mean) + float64(8975*time.Millisecond-mean)*float64(8975*time.Millisecond-mean)) / 5.0))
165 })
166
167 Describe("Generating Stats", func() {
168 It("generates a correctly configured Stats with correct values", func() {
169 stats := measurement.Stats()
170 Ω(stats.ExperimentName).Should(Equal("Test Experiment"))
171 Ω(stats.MeasurementName).Should(Equal("runtime"))
172 Ω(stats.Style).Should(Equal("{{blue}}"))
173 Ω(stats.Units).Should(Equal("duration"))
174 Ω(stats.PrecisionBundle.Duration).Should(Equal(time.Millisecond * 100))
175
176 Ω(stats.DurationBundle[gmeasure.StatMin]).Should(Equal(min))
177 Ω(stats.AnnotationBundle[gmeasure.StatMin]).Should(Equal("B"))
178 Ω(stats.DurationBundle[gmeasure.StatMax]).Should(Equal(max))
179 Ω(stats.AnnotationBundle[gmeasure.StatMax]).Should(Equal("D"))
180 Ω(stats.DurationBundle[gmeasure.StatMedian]).Should(Equal(median))
181 Ω(stats.DurationBundle[gmeasure.StatMean]).Should(Equal(mean))
182 Ω(stats.DurationBundle[gmeasure.StatStdDev]).Should(Equal(stdDev))
183 })
184 })
185
186 Describe("Emitting an unstyled report", func() {
187 It("does not include styling", func() {
188 expected := strings.Join([]string{
189 "Test Experiment - runtime [duration]",
190 "3.1s < [9s] | <8.6s> ±3.6s < 14.2s",
191 "Duration | Annotation",
192 "=====================",
193 " 7.1s | A ",
194 "---------------------",
195 " 3.1s | B ",
196 "---------------------",
197 " 9.3s | C ",
198 "---------------------",
199 " 14.2s | D ",
200 "---------------------",
201 " 9s | E ",
202 "",
203 }, "\n")
204 Ω(measurement.String()).Should(Equal(expected))
205 })
206 })
207
208 Describe("Emitting a styled report", func() {
209 It("does include styling", func() {
210 expected := strings.Join([]string{
211 "{{blue}}Test Experiment - runtime [duration]{{/}}",
212 "3.1s < [9s] | <8.6s> ±3.6s < 14.2s",
213 "{{blue}}Duration{{/}} | {{blue}}Annotation{{/}}",
214 "=====================",
215 "{{blue}} 7.1s{{/}} | {{gray}}A {{/}}",
216 "---------------------",
217 "{{blue}} 3.1s{{/}} | {{gray}}B {{/}}",
218 "---------------------",
219 "{{blue}} 9.3s{{/}} | {{gray}}C {{/}}",
220 "---------------------",
221 "{{blue}} 14.2s{{/}} | {{gray}}D {{/}}",
222 "---------------------",
223 "{{blue}} 9s{{/}} | {{gray}}E {{/}}",
224 "",
225 }, "\n")
226 Ω(measurement.ColorableString()).Should(Equal(expected))
227 })
228 })
229
230 Describe("Computing medians", func() {
231 Context("with an odd number of values", func() {
232 It("returns the middle element", func() {
233 e.RecordDuration("odd", 5*time.Second)
234 e.RecordDuration("odd", 1*time.Second)
235 e.RecordDuration("odd", 2*time.Second)
236 e.RecordDuration("odd", 4*time.Second)
237 e.RecordDuration("odd", 3*time.Second)
238
239 Ω(e.GetStats("odd").DurationBundle[gmeasure.StatMedian]).Should(Equal(3 * time.Second))
240 })
241 })
242
243 Context("when an even number of values", func() {
244 It("returns the mean of the two middle elements", func() {
245 e.RecordDuration("even", 1*time.Second)
246 e.RecordDuration("even", 2*time.Second)
247 e.RecordDuration("even", 4*time.Second)
248 e.RecordDuration("even", 3*time.Second)
249
250 Ω(e.GetStats("even").DurationBundle[gmeasure.StatMedian]).Should(Equal(2500 * time.Millisecond))
251 })
252 })
253 })
254 })
255 })
0 package gmeasure
1
2 import (
3 "fmt"
4 "sort"
5
6 "github.com/onsi/gomega/gmeasure/table"
7 )
8
9 /*
10 RankingCriteria is an enum representing the criteria by which Stats should be ranked. The enum names should be self explanatory. e.g. LowerMeanIsBetter means that Stats with lower mean values are considered more beneficial, with the lowest mean being declared the "winner" .
11 */
12 type RankingCriteria uint
13
14 const (
15 LowerMeanIsBetter RankingCriteria = iota
16 HigherMeanIsBetter
17 LowerMedianIsBetter
18 HigherMedianIsBetter
19 LowerMinIsBetter
20 HigherMinIsBetter
21 LowerMaxIsBetter
22 HigherMaxIsBetter
23 )
24
25 var rcEnumSupport = newEnumSupport(map[uint]string{uint(LowerMeanIsBetter): "Lower Mean is Better", uint(HigherMeanIsBetter): "Higher Mean is Better", uint(LowerMedianIsBetter): "Lower Median is Better", uint(HigherMedianIsBetter): "Higher Median is Better", uint(LowerMinIsBetter): "Lower Mins is Better", uint(HigherMinIsBetter): "Higher Min is Better", uint(LowerMaxIsBetter): "Lower Max is Better", uint(HigherMaxIsBetter): "Higher Max is Better"})
26
27 func (s RankingCriteria) String() string { return rcEnumSupport.String(uint(s)) }
28 func (s *RankingCriteria) UnmarshalJSON(b []byte) error {
29 out, err := rcEnumSupport.UnmarshJSON(b)
30 *s = RankingCriteria(out)
31 return err
32 }
33 func (s RankingCriteria) MarshalJSON() ([]byte, error) { return rcEnumSupport.MarshJSON(uint(s)) }
34
35 /*
36 Ranking ranks a set of Stats by a specified RankingCritera. Use RankStats to create a Ranking.
37
38 When using Ginkgo, you can register Rankings as Report Entries via AddReportEntry. This will emit a formatted table representing the Stats in rank-order when Ginkgo generates the report.
39 */
40 type Ranking struct {
41 Criteria RankingCriteria
42 Stats []Stats
43 }
44
45 /*
46 RankStats creates a new ranking of the passed-in stats according to the passed-in criteria.
47 */
48 func RankStats(criteria RankingCriteria, stats ...Stats) Ranking {
49 sort.Slice(stats, func(i int, j int) bool {
50 switch criteria {
51 case LowerMeanIsBetter:
52 return stats[i].FloatFor(StatMean) < stats[j].FloatFor(StatMean)
53 case HigherMeanIsBetter:
54 return stats[i].FloatFor(StatMean) > stats[j].FloatFor(StatMean)
55 case LowerMedianIsBetter:
56 return stats[i].FloatFor(StatMedian) < stats[j].FloatFor(StatMedian)
57 case HigherMedianIsBetter:
58 return stats[i].FloatFor(StatMedian) > stats[j].FloatFor(StatMedian)
59 case LowerMinIsBetter:
60 return stats[i].FloatFor(StatMin) < stats[j].FloatFor(StatMin)
61 case HigherMinIsBetter:
62 return stats[i].FloatFor(StatMin) > stats[j].FloatFor(StatMin)
63 case LowerMaxIsBetter:
64 return stats[i].FloatFor(StatMax) < stats[j].FloatFor(StatMax)
65 case HigherMaxIsBetter:
66 return stats[i].FloatFor(StatMax) > stats[j].FloatFor(StatMax)
67 }
68 return false
69 })
70
71 out := Ranking{
72 Criteria: criteria,
73 Stats: stats,
74 }
75
76 return out
77 }
78
79 /*
80 Winner returns the Stats with the most optimal rank based on the specified ranking criteria. For example, if the RankingCriteria is LowerMaxIsBetter then the Stats with the lowest value or duration for StatMax will be returned as the "winner"
81 */
82 func (c Ranking) Winner() Stats {
83 if len(c.Stats) == 0 {
84 return Stats{}
85 }
86 return c.Stats[0]
87 }
88
89 func (c Ranking) report(enableStyling bool) string {
90 if len(c.Stats) == 0 {
91 return "Empty Ranking"
92 }
93 t := table.NewTable()
94 t.TableStyle.EnableTextStyling = enableStyling
95 t.AppendRow(table.R(
96 table.C("Experiment"), table.C("Name"), table.C("N"), table.C("Min"), table.C("Median"), table.C("Mean"), table.C("StdDev"), table.C("Max"),
97 table.Divider("="),
98 "{{bold}}",
99 ))
100
101 for idx, stats := range c.Stats {
102 name := stats.MeasurementName
103 if stats.Units != "" {
104 name = name + " [" + stats.Units + "]"
105 }
106 experimentName := stats.ExperimentName
107 style := stats.Style
108 if idx == 0 {
109 style = "{{bold}}" + style
110 name += "\n*Winner*"
111 experimentName += "\n*Winner*"
112 }
113 r := table.R(style)
114 t.AppendRow(r)
115 r.AppendCell(table.C(experimentName), table.C(name))
116 r.AppendCell(stats.cells()...)
117
118 }
119 out := fmt.Sprintf("Ranking Criteria: %s\n", c.Criteria)
120 if enableStyling {
121 out = "{{bold}}" + out + "{{/}}"
122 }
123 out += t.Render()
124 return out
125 }
126
127 /*
128 ColorableString generates a styled report that includes a table of the rank-ordered Stats
129 It is called automatically by Ginkgo's reporting infrastructure when the Ranking is registered as a ReportEntry via AddReportEntry.
130 */
131 func (c Ranking) ColorableString() string {
132 return c.report(true)
133 }
134
135 /*
136 String generates an unstyled report that includes a table of the rank-ordered Stats
137 */
138 func (c Ranking) String() string {
139 return c.report(false)
140 }
0 package gmeasure_test
1
2 import (
3 "strings"
4 "time"
5
6 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/extensions/table"
8 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/gmeasure"
10 )
11
12 var _ = Describe("Rank", func() {
13 var A, B, C, D gmeasure.Stats
14
15 Describe("Ranking Values", func() {
16 makeStats := func(name string, min float64, max float64, mean float64, median float64) gmeasure.Stats {
17 return gmeasure.Stats{
18 Type: gmeasure.StatsTypeValue,
19 ExperimentName: "Exp-" + name,
20 MeasurementName: name,
21 N: 100,
22 PrecisionBundle: gmeasure.Precision(2),
23 ValueBundle: map[gmeasure.Stat]float64{
24 gmeasure.StatMin: min,
25 gmeasure.StatMax: max,
26 gmeasure.StatMean: mean,
27 gmeasure.StatMedian: median,
28 gmeasure.StatStdDev: 2.0,
29 },
30 }
31 }
32
33 BeforeEach(func() {
34 A = makeStats("A", 1, 2, 3, 4)
35 B = makeStats("B", 2, 3, 4, 1)
36 C = makeStats("C", 3, 4, 1, 2)
37 D = makeStats("D", 4, 1, 2, 3)
38 })
39
40 DescribeTable("ranking by criteria",
41 func(criteria gmeasure.RankingCriteria, expectedOrder func() []gmeasure.Stats) {
42 ranking := gmeasure.RankStats(criteria, A, B, C, D)
43 expected := expectedOrder()
44 Ω(ranking.Winner()).Should(Equal(expected[0]))
45 Ω(ranking.Stats).Should(Equal(expected))
46 },
47 Entry("entry", gmeasure.LowerMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, D, A, B} }),
48 Entry("entry", gmeasure.HigherMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, A, D, C} }),
49 Entry("entry", gmeasure.LowerMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, C, D, A} }),
50 Entry("entry", gmeasure.HigherMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, D, C, B} }),
51 Entry("entry", gmeasure.LowerMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, B, C, D} }),
52 Entry("entry", gmeasure.HigherMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, C, B, A} }),
53 Entry("entry", gmeasure.LowerMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, A, B, C} }),
54 Entry("entry", gmeasure.HigherMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, B, A, D} }),
55 )
56
57 Describe("Generating Reports", func() {
58 It("can generate an unstyled report", func() {
59 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
60 Ω(ranking.String()).Should(Equal(strings.Join([]string{
61 "Ranking Criteria: Lower Mean is Better",
62 "Experiment | Name | N | Min | Median | Mean | StdDev | Max ",
63 "==================================================================",
64 "Exp-C | C | 100 | 3.00 | 2.00 | 1.00 | 2.00 | 4.00",
65 "*Winner* | *Winner* | | | | | | ",
66 "------------------------------------------------------------------",
67 "Exp-D | D | 100 | 4.00 | 3.00 | 2.00 | 2.00 | 1.00",
68 "------------------------------------------------------------------",
69 "Exp-A | A | 100 | 1.00 | 4.00 | 3.00 | 2.00 | 2.00",
70 "------------------------------------------------------------------",
71 "Exp-B | B | 100 | 2.00 | 1.00 | 4.00 | 2.00 | 3.00",
72 "",
73 }, "\n")))
74 })
75
76 It("can generate a styled report", func() {
77 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
78 Ω(ranking.ColorableString()).Should(Equal(strings.Join([]string{
79 "{{bold}}Ranking Criteria: Lower Mean is Better",
80 "{{/}}{{bold}}Experiment{{/}} | {{bold}}Name {{/}} | {{bold}}N {{/}} | {{bold}}Min {{/}} | {{bold}}Median{{/}} | {{bold}}Mean{{/}} | {{bold}}StdDev{{/}} | {{bold}}Max {{/}}",
81 "==================================================================",
82 "{{bold}}Exp-C {{/}} | {{bold}}C {{/}} | {{bold}}100{{/}} | {{bold}}3.00{{/}} | {{bold}}2.00 {{/}} | {{bold}}1.00{{/}} | {{bold}}2.00 {{/}} | {{bold}}4.00{{/}}",
83 "{{bold}}*Winner* {{/}} | {{bold}}*Winner*{{/}} | | | | | | ",
84 "------------------------------------------------------------------",
85 "Exp-D | D | 100 | 4.00 | 3.00 | 2.00 | 2.00 | 1.00",
86 "------------------------------------------------------------------",
87 "Exp-A | A | 100 | 1.00 | 4.00 | 3.00 | 2.00 | 2.00",
88 "------------------------------------------------------------------",
89 "Exp-B | B | 100 | 2.00 | 1.00 | 4.00 | 2.00 | 3.00",
90 "",
91 }, "\n")))
92 })
93 })
94 })
95
96 Describe("Ranking Durations", func() {
97 makeStats := func(name string, min time.Duration, max time.Duration, mean time.Duration, median time.Duration) gmeasure.Stats {
98 return gmeasure.Stats{
99 Type: gmeasure.StatsTypeDuration,
100 ExperimentName: "Exp-" + name,
101 MeasurementName: name,
102 N: 100,
103 PrecisionBundle: gmeasure.Precision(time.Millisecond * 100),
104 DurationBundle: map[gmeasure.Stat]time.Duration{
105 gmeasure.StatMin: min,
106 gmeasure.StatMax: max,
107 gmeasure.StatMean: mean,
108 gmeasure.StatMedian: median,
109 gmeasure.StatStdDev: 2.0,
110 },
111 }
112 }
113
114 BeforeEach(func() {
115 A = makeStats("A", 1*time.Second, 2*time.Second, 3*time.Second, 4*time.Second)
116 B = makeStats("B", 2*time.Second, 3*time.Second, 4*time.Second, 1*time.Second)
117 C = makeStats("C", 3*time.Second, 4*time.Second, 1*time.Second, 2*time.Second)
118 D = makeStats("D", 4*time.Second, 1*time.Second, 2*time.Second, 3*time.Second)
119 })
120
121 DescribeTable("ranking by criteria",
122 func(criteria gmeasure.RankingCriteria, expectedOrder func() []gmeasure.Stats) {
123 ranking := gmeasure.RankStats(criteria, A, B, C, D)
124 expected := expectedOrder()
125 Ω(ranking.Winner()).Should(Equal(expected[0]))
126 Ω(ranking.Stats).Should(Equal(expected))
127 },
128 Entry("entry", gmeasure.LowerMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, D, A, B} }),
129 Entry("entry", gmeasure.HigherMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, A, D, C} }),
130 Entry("entry", gmeasure.LowerMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, C, D, A} }),
131 Entry("entry", gmeasure.HigherMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, D, C, B} }),
132 Entry("entry", gmeasure.LowerMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, B, C, D} }),
133 Entry("entry", gmeasure.HigherMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, C, B, A} }),
134 Entry("entry", gmeasure.LowerMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, A, B, C} }),
135 Entry("entry", gmeasure.HigherMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, B, A, D} }),
136 )
137
138 Describe("Generating Reports", func() {
139 It("can generate an unstyled report", func() {
140 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
141 Ω(ranking.String()).Should(Equal(strings.Join([]string{
142 "Ranking Criteria: Lower Mean is Better",
143 "Experiment | Name | N | Min | Median | Mean | StdDev | Max",
144 "================================================================",
145 "Exp-C | C | 100 | 3s | 2s | 1s | 0s | 4s ",
146 "*Winner* | *Winner* | | | | | | ",
147 "----------------------------------------------------------------",
148 "Exp-D | D | 100 | 4s | 3s | 2s | 0s | 1s ",
149 "----------------------------------------------------------------",
150 "Exp-A | A | 100 | 1s | 4s | 3s | 0s | 2s ",
151 "----------------------------------------------------------------",
152 "Exp-B | B | 100 | 2s | 1s | 4s | 0s | 3s ",
153 "",
154 }, "\n")))
155 })
156
157 It("can generate a styled report", func() {
158 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
159 Ω(ranking.ColorableString()).Should(Equal(strings.Join([]string{
160 "{{bold}}Ranking Criteria: Lower Mean is Better",
161 "{{/}}{{bold}}Experiment{{/}} | {{bold}}Name {{/}} | {{bold}}N {{/}} | {{bold}}Min{{/}} | {{bold}}Median{{/}} | {{bold}}Mean{{/}} | {{bold}}StdDev{{/}} | {{bold}}Max{{/}}",
162 "================================================================",
163 "{{bold}}Exp-C {{/}} | {{bold}}C {{/}} | {{bold}}100{{/}} | {{bold}}3s {{/}} | {{bold}}2s {{/}} | {{bold}}1s {{/}} | {{bold}}0s {{/}} | {{bold}}4s {{/}}",
164 "{{bold}}*Winner* {{/}} | {{bold}}*Winner*{{/}} | | | | | | ",
165 "----------------------------------------------------------------",
166 "Exp-D | D | 100 | 4s | 3s | 2s | 0s | 1s ",
167 "----------------------------------------------------------------",
168 "Exp-A | A | 100 | 1s | 4s | 3s | 0s | 2s ",
169 "----------------------------------------------------------------",
170 "Exp-B | B | 100 | 2s | 1s | 4s | 0s | 3s ",
171 "",
172 }, "\n")))
173 })
174 })
175 })
176
177 })
0 package gmeasure
1
2 import (
3 "fmt"
4 "time"
5
6 "github.com/onsi/gomega/gmeasure/table"
7 )
8
9 /*
10 Stat is an enum representing the statistics you can request of a Stats struct
11 */
12 type Stat uint
13
14 const (
15 StatInvalid Stat = iota
16 StatMin
17 StatMax
18 StatMean
19 StatMedian
20 StatStdDev
21 )
22
23 var statEnumSupport = newEnumSupport(map[uint]string{uint(StatInvalid): "INVALID STAT", uint(StatMin): "Min", uint(StatMax): "Max", uint(StatMean): "Mean", uint(StatMedian): "Median", uint(StatStdDev): "StdDev"})
24
25 func (s Stat) String() string { return statEnumSupport.String(uint(s)) }
26 func (s *Stat) UnmarshalJSON(b []byte) error {
27 out, err := statEnumSupport.UnmarshJSON(b)
28 *s = Stat(out)
29 return err
30 }
31 func (s Stat) MarshalJSON() ([]byte, error) { return statEnumSupport.MarshJSON(uint(s)) }
32
33 type StatsType uint
34
35 const (
36 StatsTypeInvalid StatsType = iota
37 StatsTypeValue
38 StatsTypeDuration
39 )
40
41 var statsTypeEnumSupport = newEnumSupport(map[uint]string{uint(StatsTypeInvalid): "INVALID STATS TYPE", uint(StatsTypeValue): "StatsTypeValue", uint(StatsTypeDuration): "StatsTypeDuration"})
42
43 func (s StatsType) String() string { return statsTypeEnumSupport.String(uint(s)) }
44 func (s *StatsType) UnmarshalJSON(b []byte) error {
45 out, err := statsTypeEnumSupport.UnmarshJSON(b)
46 *s = StatsType(out)
47 return err
48 }
49 func (s StatsType) MarshalJSON() ([]byte, error) { return statsTypeEnumSupport.MarshJSON(uint(s)) }
50
51 /*
52 Stats records the key statistics for a given measurement. You generally don't make Stats directly - but you can fetch them from Experiments using GetStats() and from Measurements using Stats().
53
54 When using Ginkgo, you can register Measurements as Report Entries via AddReportEntry. This will emit all the captured data points when Ginkgo generates the report.
55 */
56 type Stats struct {
57 // Type is the StatType - one of StatTypeDuration or StatTypeValue
58 Type StatsType
59
60 // ExperimentName is the name of the Experiment that recorded the Measurement from which this Stat is derived
61 ExperimentName string
62
63 // MeasurementName is the name of the Measurement from which this Stat is derived
64 MeasurementName string
65
66 // Units captures the Units of the Measurement from which this Stat is derived
67 Units string
68
69 // Style captures the Style of the Measurement from which this Stat is derived
70 Style string
71
72 // PrecisionBundle captures the precision to use when rendering data for this Measurement.
73 // If Type is StatTypeDuration then PrecisionBundle.Duration is used to round any durations before presentation.
74 // If Type is StatTypeValue then PrecisionBundle.ValueFormat is used to format any values before presentation
75 PrecisionBundle PrecisionBundle
76
77 // N represents the total number of data points in the Meassurement from which this Stat is derived
78 N int
79
80 // If Type is StatTypeValue, ValueBundle will be populated with float64s representing this Stat's statistics
81 ValueBundle map[Stat]float64
82
83 // If Type is StatTypeDuration, DurationBundle will be populated with float64s representing this Stat's statistics
84 DurationBundle map[Stat]time.Duration
85
86 // AnnotationBundle is populated with Annotations corresponding to the data points that can be associated with a Stat.
87 // For example AnnotationBundle[StatMin] will return the Annotation for the data point that has the minimum value/duration.
88 AnnotationBundle map[Stat]string
89 }
90
91 // String returns a minimal summary of the stats of the form "MIN < [MEDIAN] | <MEAN> ±STDDEV < MAX"
92 func (s Stats) String() string {
93 return fmt.Sprintf("%s < [%s] | <%s> ±%s < %s", s.StringFor(StatMin), s.StringFor(StatMedian), s.StringFor(StatMean), s.StringFor(StatStdDev), s.StringFor(StatMax))
94 }
95
96 // ValueFor returns the float64 value for a particular Stat. You should only use this if the Stats has Type StatsTypeValue
97 // For example:
98 //
99 // median := experiment.GetStats("length").ValueFor(gmeasure.StatMedian)
100 //
101 // will return the median data point for the "length" Measurement.
102 func (s Stats) ValueFor(stat Stat) float64 {
103 return s.ValueBundle[stat]
104 }
105
106 // DurationFor returns the time.Duration for a particular Stat. You should only use this if the Stats has Type StatsTypeDuration
107 // For example:
108 //
109 // mean := experiment.GetStats("runtime").ValueFor(gmeasure.StatMean)
110 //
111 // will return the mean duration for the "runtime" Measurement.
112 func (s Stats) DurationFor(stat Stat) time.Duration {
113 return s.DurationBundle[stat]
114 }
115
116 // FloatFor returns a float64 representation of the passed-in Stat.
117 // When Type is StatsTypeValue this is equivalent to s.ValueFor(stat).
118 // When Type is StatsTypeDuration this is equivalent to float64(s.DurationFor(stat))
119 func (s Stats) FloatFor(stat Stat) float64 {
120 switch s.Type {
121 case StatsTypeValue:
122 return s.ValueFor(stat)
123 case StatsTypeDuration:
124 return float64(s.DurationFor(stat))
125 }
126 return 0
127 }
128
129 // StringFor returns a formatted string representation of the passed-in Stat.
130 // The formatting honors the precision directives provided in stats.PrecisionBundle
131 func (s Stats) StringFor(stat Stat) string {
132 switch s.Type {
133 case StatsTypeValue:
134 return fmt.Sprintf(s.PrecisionBundle.ValueFormat, s.ValueFor(stat))
135 case StatsTypeDuration:
136 return s.DurationFor(stat).Round(s.PrecisionBundle.Duration).String()
137 }
138 return ""
139 }
140
141 func (s Stats) cells() []table.Cell {
142 out := []table.Cell{}
143 out = append(out, table.C(fmt.Sprintf("%d", s.N)))
144 for _, stat := range []Stat{StatMin, StatMedian, StatMean, StatStdDev, StatMax} {
145 content := s.StringFor(stat)
146 if s.AnnotationBundle[stat] != "" {
147 content += "\n" + s.AnnotationBundle[stat]
148 }
149 out = append(out, table.C(content))
150 }
151 return out
152 }
0 package gmeasure_test
1
2 import (
3 "time"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/gmeasure"
8 )
9
10 var _ = Describe("Stats", func() {
11 var stats gmeasure.Stats
12
13 Describe("Stats representing values", func() {
14 BeforeEach(func() {
15 stats = gmeasure.Stats{
16 Type: gmeasure.StatsTypeValue,
17 ExperimentName: "My Test Experiment",
18 MeasurementName: "Sprockets",
19 Units: "widgets",
20 N: 100,
21 PrecisionBundle: gmeasure.Precision(2),
22 ValueBundle: map[gmeasure.Stat]float64{
23 gmeasure.StatMin: 17.48992,
24 gmeasure.StatMax: 293.4820,
25 gmeasure.StatMean: 187.3023,
26 gmeasure.StatMedian: 87.2235,
27 gmeasure.StatStdDev: 73.6394,
28 },
29 }
30 })
31
32 Describe("String()", func() {
33 It("returns a one-line summary", func() {
34 Ω(stats.String()).Should(Equal("17.49 < [87.22] | <187.30> ±73.64 < 293.48"))
35 })
36 })
37
38 Describe("ValueFor()", func() {
39 It("returns the value for the requested stat", func() {
40 Ω(stats.ValueFor(gmeasure.StatMin)).Should(Equal(17.48992))
41 Ω(stats.ValueFor(gmeasure.StatMean)).Should(Equal(187.3023))
42 })
43 })
44
45 Describe("FloatFor", func() {
46 It("returns the requested stat as a float", func() {
47 Ω(stats.FloatFor(gmeasure.StatMin)).Should(Equal(17.48992))
48 Ω(stats.FloatFor(gmeasure.StatMean)).Should(Equal(187.3023))
49 })
50 })
51
52 Describe("StringFor", func() {
53 It("returns the requested stat rendered with the configured precision", func() {
54 Ω(stats.StringFor(gmeasure.StatMin)).Should(Equal("17.49"))
55 Ω(stats.StringFor(gmeasure.StatMean)).Should(Equal("187.30"))
56 })
57 })
58 })
59
60 Describe("Stats representing durations", func() {
61 BeforeEach(func() {
62 stats = gmeasure.Stats{
63 Type: gmeasure.StatsTypeDuration,
64 ExperimentName: "My Test Experiment",
65 MeasurementName: "Runtime",
66 N: 100,
67 PrecisionBundle: gmeasure.Precision(time.Millisecond * 100),
68 DurationBundle: map[gmeasure.Stat]time.Duration{
69 gmeasure.StatMin: 17375 * time.Millisecond,
70 gmeasure.StatMax: 890321 * time.Millisecond,
71 gmeasure.StatMean: 328712 * time.Millisecond,
72 gmeasure.StatMedian: 552390 * time.Millisecond,
73 gmeasure.StatStdDev: 186259 * time.Millisecond,
74 },
75 }
76 })
77 Describe("String()", func() {
78 It("returns a one-line summary", func() {
79 Ω(stats.String()).Should(Equal("17.4s < [9m12.4s] | <5m28.7s> ±3m6.3s < 14m50.3s"))
80 })
81 })
82 Describe("DurationFor()", func() {
83 It("returns the duration for the requested stat", func() {
84 Ω(stats.DurationFor(gmeasure.StatMin)).Should(Equal(17375 * time.Millisecond))
85 Ω(stats.DurationFor(gmeasure.StatMean)).Should(Equal(328712 * time.Millisecond))
86 })
87 })
88
89 Describe("FloatFor", func() {
90 It("returns the float64 representation for the requested duration stat", func() {
91 Ω(stats.FloatFor(gmeasure.StatMin)).Should(Equal(float64(17375 * time.Millisecond)))
92 Ω(stats.FloatFor(gmeasure.StatMean)).Should(Equal(float64(328712 * time.Millisecond)))
93 })
94 })
95
96 Describe("StringFor", func() {
97 It("returns the requested stat rendered with the configured precision", func() {
98 Ω(stats.StringFor(gmeasure.StatMin)).Should(Equal("17.4s"))
99 Ω(stats.StringFor(gmeasure.StatMean)).Should(Equal("5m28.7s"))
100 })
101 })
102 })
103 })
0 package gmeasure
1
2 import "time"
3
4 /*
5 Stopwatch provides a convenient abstraction for recording durations. There are two ways to make a Stopwatch:
6
7 You can make a Stopwatch from an Experiment via experiment.NewStopwatch(). This is how you first get a hold of a Stopwatch.
8
9 You can subsequently call stopwatch.NewStopwatch() to get a fresh Stopwatch.
10 This is only necessary if you need to record durations on a different goroutine as a single Stopwatch is not considered thread-safe.
11
12 The Stopwatch starts as soon as it is created. You can Pause() the stopwatch and Reset() it as needed.
13
14 Stopwatches refer back to their parent Experiment. They use this reference to record any measured durations back with the Experiment.
15 */
16 type Stopwatch struct {
17 Experiment *Experiment
18 t time.Time
19 pauseT time.Time
20 pauseDuration time.Duration
21 running bool
22 }
23
24 func newStopwatch(experiment *Experiment) *Stopwatch {
25 return &Stopwatch{
26 Experiment: experiment,
27 t: time.Now(),
28 running: true,
29 }
30 }
31
32 /*
33 NewStopwatch returns a new Stopwatch pointing to the same Experiment as this Stopwatch
34 */
35 func (s *Stopwatch) NewStopwatch() *Stopwatch {
36 return newStopwatch(s.Experiment)
37 }
38
39 /*
40 Record captures the amount of time that has passed since the Stopwatch was created or most recently Reset(). It records the duration on it's associated Experiment in a Measurement with the passed-in name.
41
42 Record takes all the decorators that experiment.RecordDuration takes (e.g. Annotation("...") can be used to annotate this duration)
43
44 Note that Record does not Reset the Stopwatch. It does, however, return the Stopwatch so the following pattern is common:
45
46 stopwatch := experiment.NewStopwatch()
47 // first expensive operation
48 stopwatch.Record("first operation").Reset() //records the duration of the first operation and resets the stopwatch.
49 // second expensive operation
50 stopwatch.Record("second operation").Reset() //records the duration of the second operation and resets the stopwatch.
51
52 omitting the Reset() after the first operation would cause the duration recorded for the second operation to include the time elapsed by both the first _and_ second operations.
53
54 The Stopwatch must be running (i.e. not paused) when Record is called.
55 */
56 func (s *Stopwatch) Record(name string, args ...interface{}) *Stopwatch {
57 if !s.running {
58 panic("stopwatch is not running - call Resume or Reset before calling Record")
59 }
60 duration := time.Since(s.t) - s.pauseDuration
61 s.Experiment.RecordDuration(name, duration, args...)
62 return s
63 }
64
65 /*
66 Reset resets the Stopwatch. Subsequent recorded durations will measure the time elapsed from the moment Reset was called.
67 If the Stopwatch was Paused it is unpaused after calling Reset.
68 */
69 func (s *Stopwatch) Reset() *Stopwatch {
70 s.running = true
71 s.t = time.Now()
72 s.pauseDuration = 0
73 return s
74 }
75
76 /*
77 Pause pauses the Stopwatch. While pasued the Stopwatch does not accumulate elapsed time. This is useful for ignoring expensive operations that are incidental to the behavior you are attempting to characterize.
78 Note: You must call Resume() before you can Record() subsequent measurements.
79
80 For example:
81
82 stopwatch := experiment.NewStopwatch()
83 // first expensive operation
84 stopwatch.Record("first operation").Reset()
85 // second expensive operation - part 1
86 stopwatch.Pause()
87 // something expensive that we don't care about
88 stopwatch.Resume()
89 // second expensive operation - part 2
90 stopwatch.Record("second operation").Reset() // the recorded duration captures the time elapsed during parts 1 and 2 of the second expensive operation, but not the bit in between
91
92
93 The Stopwatch must be running when Pause is called.
94 */
95 func (s *Stopwatch) Pause() *Stopwatch {
96 if !s.running {
97 panic("stopwatch is not running - call Resume or Reset before calling Pause")
98 }
99 s.running = false
100 s.pauseT = time.Now()
101 return s
102 }
103
104 /*
105 Resume resumes a paused Stopwatch. Any time that elapses after Resume is called will be accumulated as elapsed time when a subsequent duration is Recorded.
106
107 The Stopwatch must be Paused when Resume is called
108 */
109 func (s *Stopwatch) Resume() *Stopwatch {
110 if s.running {
111 panic("stopwatch is running - call Pause before calling Resume")
112 }
113 s.running = true
114 s.pauseDuration = s.pauseDuration + time.Since(s.pauseT)
115 return s
116 }
0 package gmeasure_test
1
2 import (
3 "time"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/gmeasure"
8 )
9
10 var _ = Describe("Stopwatch", func() {
11 var e *gmeasure.Experiment
12 var stopwatch *gmeasure.Stopwatch
13
14 BeforeEach(func() {
15 e = gmeasure.NewExperiment("My Test Experiment")
16 stopwatch = e.NewStopwatch()
17 })
18
19 It("records durations", func() {
20 time.Sleep(100 * time.Millisecond)
21 stopwatch.Record("recordings", gmeasure.Annotation("A"))
22 time.Sleep(100 * time.Millisecond)
23 stopwatch.Record("recordings", gmeasure.Annotation("B")).Reset()
24 time.Sleep(100 * time.Millisecond)
25 stopwatch.Record("recordings", gmeasure.Annotation("C")).Reset()
26 time.Sleep(100 * time.Millisecond)
27 stopwatch.Pause()
28 time.Sleep(100 * time.Millisecond)
29 stopwatch.Resume()
30 time.Sleep(100 * time.Millisecond)
31 stopwatch.Pause()
32 time.Sleep(100 * time.Millisecond)
33 stopwatch.Resume()
34 time.Sleep(100 * time.Millisecond)
35 stopwatch.Record("recordings", gmeasure.Annotation("D"))
36 durations := e.Get("recordings").Durations
37 annotations := e.Get("recordings").Annotations
38 Ω(annotations).Should(Equal([]string{"A", "B", "C", "D"}))
39 Ω(durations[0]).Should(BeNumerically("~", 100*time.Millisecond, 50*time.Millisecond))
40 Ω(durations[1]).Should(BeNumerically("~", 200*time.Millisecond, 50*time.Millisecond))
41 Ω(durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 50*time.Millisecond))
42 Ω(durations[3]).Should(BeNumerically("~", 300*time.Millisecond, 50*time.Millisecond))
43
44 })
45
46 It("panics when asked to record but not running", func() {
47 stopwatch.Pause()
48 Ω(func() {
49 stopwatch.Record("A")
50 }).Should(PanicWith("stopwatch is not running - call Resume or Reset before calling Record"))
51 })
52
53 It("panics when paused but not running", func() {
54 stopwatch.Pause()
55 Ω(func() {
56 stopwatch.Pause()
57 }).Should(PanicWith("stopwatch is not running - call Resume or Reset before calling Pause"))
58 })
59
60 It("panics when asked to resume but not paused", func() {
61 Ω(func() {
62 stopwatch.Resume()
63 }).Should(PanicWith("stopwatch is running - call Pause before calling Resume"))
64 })
65 })
0 package table
1
2 // This is a temporary package - Table will move to github.com/onsi/consolable once some more dust settles
3
4 import (
5 "reflect"
6 "strings"
7 "unicode/utf8"
8 )
9
10 type AlignType uint
11
12 const (
13 AlignTypeLeft AlignType = iota
14 AlignTypeCenter
15 AlignTypeRight
16 )
17
18 type Divider string
19
20 type Row struct {
21 Cells []Cell
22 Divider string
23 Style string
24 }
25
26 func R(args ...interface{}) *Row {
27 r := &Row{
28 Divider: "-",
29 }
30 for _, arg := range args {
31 switch reflect.TypeOf(arg) {
32 case reflect.TypeOf(Divider("")):
33 r.Divider = string(arg.(Divider))
34 case reflect.TypeOf(r.Style):
35 r.Style = arg.(string)
36 case reflect.TypeOf(Cell{}):
37 r.Cells = append(r.Cells, arg.(Cell))
38 }
39 }
40 return r
41 }
42
43 func (r *Row) AppendCell(cells ...Cell) *Row {
44 r.Cells = append(r.Cells, cells...)
45 return r
46 }
47
48 func (r *Row) Render(widths []int, totalWidth int, tableStyle TableStyle, isLastRow bool) string {
49 out := ""
50 if len(r.Cells) == 1 {
51 out += strings.Join(r.Cells[0].render(totalWidth, r.Style, tableStyle), "\n") + "\n"
52 } else {
53 if len(r.Cells) != len(widths) {
54 panic("row vs width mismatch")
55 }
56 renderedCells := make([][]string, len(r.Cells))
57 maxHeight := 0
58 for colIdx, cell := range r.Cells {
59 renderedCells[colIdx] = cell.render(widths[colIdx], r.Style, tableStyle)
60 if len(renderedCells[colIdx]) > maxHeight {
61 maxHeight = len(renderedCells[colIdx])
62 }
63 }
64 for colIdx := range r.Cells {
65 for len(renderedCells[colIdx]) < maxHeight {
66 renderedCells[colIdx] = append(renderedCells[colIdx], strings.Repeat(" ", widths[colIdx]))
67 }
68 }
69 border := strings.Repeat(" ", tableStyle.Padding)
70 if tableStyle.VerticalBorders {
71 border += "|" + border
72 }
73 for lineIdx := 0; lineIdx < maxHeight; lineIdx++ {
74 for colIdx := range r.Cells {
75 out += renderedCells[colIdx][lineIdx]
76 if colIdx < len(r.Cells)-1 {
77 out += border
78 }
79 }
80 out += "\n"
81 }
82 }
83 if tableStyle.HorizontalBorders && !isLastRow && r.Divider != "" {
84 out += strings.Repeat(string(r.Divider), totalWidth) + "\n"
85 }
86
87 return out
88 }
89
90 type Cell struct {
91 Contents []string
92 Style string
93 Align AlignType
94 }
95
96 func C(contents string, args ...interface{}) Cell {
97 c := Cell{
98 Contents: strings.Split(contents, "\n"),
99 }
100 for _, arg := range args {
101 switch reflect.TypeOf(arg) {
102 case reflect.TypeOf(c.Style):
103 c.Style = arg.(string)
104 case reflect.TypeOf(c.Align):
105 c.Align = arg.(AlignType)
106 }
107 }
108 return c
109 }
110
111 func (c Cell) Width() (int, int) {
112 w, minW := 0, 0
113 for _, line := range c.Contents {
114 lineWidth := utf8.RuneCountInString(line)
115 if lineWidth > w {
116 w = lineWidth
117 }
118 for _, word := range strings.Split(line, " ") {
119 wordWidth := utf8.RuneCountInString(word)
120 if wordWidth > minW {
121 minW = wordWidth
122 }
123 }
124 }
125 return w, minW
126 }
127
128 func (c Cell) alignLine(line string, width int) string {
129 lineWidth := utf8.RuneCountInString(line)
130 if lineWidth == width {
131 return line
132 }
133 if lineWidth < width {
134 gap := width - lineWidth
135 switch c.Align {
136 case AlignTypeLeft:
137 return line + strings.Repeat(" ", gap)
138 case AlignTypeRight:
139 return strings.Repeat(" ", gap) + line
140 case AlignTypeCenter:
141 leftGap := gap / 2
142 rightGap := gap - leftGap
143 return strings.Repeat(" ", leftGap) + line + strings.Repeat(" ", rightGap)
144 }
145 }
146 return line
147 }
148
149 func (c Cell) splitWordToWidth(word string, width int) []string {
150 out := []string{}
151 n, subWord := 0, ""
152 for _, c := range word {
153 subWord += string(c)
154 n += 1
155 if n == width-1 {
156 out = append(out, subWord+"-")
157 n, subWord = 0, ""
158 }
159 }
160 return out
161 }
162
163 func (c Cell) splitToWidth(line string, width int) []string {
164 lineWidth := utf8.RuneCountInString(line)
165 if lineWidth <= width {
166 return []string{line}
167 }
168
169 outLines := []string{}
170 words := strings.Split(line, " ")
171 outWords := []string{words[0]}
172 length := utf8.RuneCountInString(words[0])
173 if length > width {
174 splitWord := c.splitWordToWidth(words[0], width)
175 lastIdx := len(splitWord) - 1
176 outLines = append(outLines, splitWord[:lastIdx]...)
177 outWords = []string{splitWord[lastIdx]}
178 length = utf8.RuneCountInString(splitWord[lastIdx])
179 }
180
181 for _, word := range words[1:] {
182 wordLength := utf8.RuneCountInString(word)
183 if length+wordLength+1 <= width {
184 length += wordLength + 1
185 outWords = append(outWords, word)
186 continue
187 }
188 outLines = append(outLines, strings.Join(outWords, " "))
189
190 outWords = []string{word}
191 length = wordLength
192 if length > width {
193 splitWord := c.splitWordToWidth(word, width)
194 lastIdx := len(splitWord) - 1
195 outLines = append(outLines, splitWord[:lastIdx]...)
196 outWords = []string{splitWord[lastIdx]}
197 length = utf8.RuneCountInString(splitWord[lastIdx])
198 }
199 }
200 if len(outWords) > 0 {
201 outLines = append(outLines, strings.Join(outWords, " "))
202 }
203
204 return outLines
205 }
206
207 func (c Cell) render(width int, style string, tableStyle TableStyle) []string {
208 out := []string{}
209 for _, line := range c.Contents {
210 out = append(out, c.splitToWidth(line, width)...)
211 }
212 for idx := range out {
213 out[idx] = c.alignLine(out[idx], width)
214 }
215
216 if tableStyle.EnableTextStyling {
217 style = style + c.Style
218 if style != "" {
219 for idx := range out {
220 out[idx] = style + out[idx] + "{{/}}"
221 }
222 }
223 }
224
225 return out
226 }
227
228 type TableStyle struct {
229 Padding int
230 VerticalBorders bool
231 HorizontalBorders bool
232 MaxTableWidth int
233 MaxColWidth int
234 EnableTextStyling bool
235 }
236
237 var DefaultTableStyle = TableStyle{
238 Padding: 1,
239 VerticalBorders: true,
240 HorizontalBorders: true,
241 MaxTableWidth: 120,
242 MaxColWidth: 40,
243 EnableTextStyling: true,
244 }
245
246 type Table struct {
247 Rows []*Row
248
249 TableStyle TableStyle
250 }
251
252 func NewTable() *Table {
253 return &Table{
254 TableStyle: DefaultTableStyle,
255 }
256 }
257
258 func (t *Table) AppendRow(row *Row) *Table {
259 t.Rows = append(t.Rows, row)
260 return t
261 }
262
263 func (t *Table) Render() string {
264 out := ""
265 totalWidth, widths := t.computeWidths()
266 for rowIdx, row := range t.Rows {
267 out += row.Render(widths, totalWidth, t.TableStyle, rowIdx == len(t.Rows)-1)
268 }
269 return out
270 }
271
272 func (t *Table) computeWidths() (int, []int) {
273 nCol := 0
274 for _, row := range t.Rows {
275 if len(row.Cells) > nCol {
276 nCol = len(row.Cells)
277 }
278 }
279
280 // lets compute the contribution to width from the borders + padding
281 borderWidth := t.TableStyle.Padding
282 if t.TableStyle.VerticalBorders {
283 borderWidth += 1 + t.TableStyle.Padding
284 }
285 totalBorderWidth := borderWidth * (nCol - 1)
286
287 // lets compute the width of each column
288 widths := make([]int, nCol)
289 minWidths := make([]int, nCol)
290 for colIdx := range widths {
291 for _, row := range t.Rows {
292 if colIdx >= len(row.Cells) {
293 // ignore rows with fewer columns
294 continue
295 }
296 w, minWid := row.Cells[colIdx].Width()
297 if w > widths[colIdx] {
298 widths[colIdx] = w
299 }
300 if minWid > minWidths[colIdx] {
301 minWidths[colIdx] = minWid
302 }
303 }
304 }
305
306 // do we already fit?
307 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth {
308 // yes! we're done
309 return sum(widths) + totalBorderWidth, widths
310 }
311
312 // clamp the widths and minWidths to MaxColWidth
313 for colIdx := range widths {
314 widths[colIdx] = min(widths[colIdx], t.TableStyle.MaxColWidth)
315 minWidths[colIdx] = min(minWidths[colIdx], t.TableStyle.MaxColWidth)
316 }
317
318 // do we fit now?
319 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth {
320 // yes! we're done
321 return sum(widths) + totalBorderWidth, widths
322 }
323
324 // hmm... still no... can we possibly squeeze the table in without violating minWidths?
325 if sum(minWidths)+totalBorderWidth >= t.TableStyle.MaxTableWidth {
326 // nope - we're just going to have to exceed MaxTableWidth
327 return sum(minWidths) + totalBorderWidth, minWidths
328 }
329
330 // looks like we don't fit yet, but we should be able to fit without violating minWidths
331 // lets start scaling down
332 n := 0
333 for sum(widths)+totalBorderWidth > t.TableStyle.MaxTableWidth {
334 budget := t.TableStyle.MaxTableWidth - totalBorderWidth
335 baseline := sum(widths)
336
337 for colIdx := range widths {
338 widths[colIdx] = max((widths[colIdx]*budget)/baseline, minWidths[colIdx])
339 }
340 n += 1
341 if n > 100 {
342 break // in case we somehow fail to converge
343 }
344 }
345
346 return sum(widths) + totalBorderWidth, widths
347 }
348
349 func sum(s []int) int {
350 out := 0
351 for _, v := range s {
352 out += v
353 }
354 return out
355 }
356
357 func min(a int, b int) int {
358 if a < b {
359 return a
360 }
361 return b
362 }
363
364 func max(a int, b int) int {
365 if a > b {
366 return a
367 }
368 return b
369 }
00 module github.com/onsi/gomega
11
2 go 1.14
2 go 1.16
33
44 require (
5 github.com/golang/protobuf v1.4.2
6 github.com/onsi/ginkgo v1.12.1
7 golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0
8 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
9 gopkg.in/yaml.v2 v2.3.0
5 github.com/golang/protobuf v1.5.2
6 github.com/onsi/ginkgo v1.16.4
7 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
8 gopkg.in/yaml.v2 v2.4.0
109 )
0 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
0 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
2 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
4 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
5 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
6 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
7 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
38 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
49 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
510 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
611 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
712 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
813 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
9 github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
1014 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
15 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
16 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
17 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
1118 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
1219 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
13 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
1420 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
21 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
22 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1523 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
1624 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
17 github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
1825 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
19 github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
26 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
27 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
2028 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
21 github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ=
2229 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
30 github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
31 github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
2332 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
33 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
34 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
35 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
36 github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
37 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
38 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
39 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
40 github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
41 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
2442 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
43 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
44 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
2545 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
26 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
46 golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
47 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
2748 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
2849 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
29 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0=
50 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
3051 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
31 golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0 h1:wBouT66WTYFXdxfVdz9sVWARVd/2vfGcmI45D2gj45M=
32 golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
33 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
52 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
53 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
54 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
3455 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
35 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
56 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
57 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
58 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
3659 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3760 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3861 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
3962 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
40 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA=
63 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4164 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
42 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
4365 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4466 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
67 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
68 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
69 golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
70 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
71 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
72 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
4673 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
47 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
4874 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
75 golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
76 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
4977 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
50 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
78 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
79 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
80 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
81 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
82 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5183 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
84 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
85 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5286 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
5387 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
5488 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
5589 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
5690 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
57 google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
5891 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
92 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
93 google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
94 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
5995 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
6096 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
6197 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
6298 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
6399 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
64100 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
65 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
101 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
66102 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
67 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
68103 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
104 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
105 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
1313 package gomega
1414
1515 import (
16 "errors"
1617 "fmt"
17 "reflect"
1818 "time"
1919
20 "github.com/onsi/gomega/internal/assertion"
21 "github.com/onsi/gomega/internal/asyncassertion"
22 "github.com/onsi/gomega/internal/testingtsupport"
20 "github.com/onsi/gomega/internal"
2321 "github.com/onsi/gomega/types"
2422 )
2523
26 const GOMEGA_VERSION = "1.10.3"
27
28 const nilFailHandlerPanic = `You are trying to make an assertion, but Gomega's fail handler is nil.
24 const GOMEGA_VERSION = "1.17.0"
25
26 const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler.
2927 If you're using Ginkgo then you probably forgot to put your assertion in an It().
3028 Alternatively, you may have forgotten to register a fail handler with RegisterFailHandler() or RegisterTestingT().
3129 Depending on your vendoring solution you may be inadvertently importing gomega and subpackages (e.g. ghhtp, gexec,...) from different locations.
3230 `
3331
34 var globalFailWrapper *types.GomegaFailWrapper
35
36 var defaultEventuallyTimeout = time.Second
37 var defaultEventuallyPollingInterval = 10 * time.Millisecond
38 var defaultConsistentlyDuration = 100 * time.Millisecond
39 var defaultConsistentlyPollingInterval = 10 * time.Millisecond
32 // Gomega describes the essential Gomega DSL. This interface allows libraries
33 // to abstract between the standard package-level function implementations
34 // and alternatives like *WithT.
35 //
36 // The types in the top-level DSL have gotten a bit messy due to earlier depracations that avoid stuttering
37 // and due to an accidental use of a concrete type (*WithT) in an earlier release.
38 //
39 // As of 1.15 both the WithT and Ginkgo variants of Gomega are implemented by the same underlying object
40 // however one (the Ginkgo variant) is exported as an interface (types.Gomega) whereas the other (the withT variant)
41 // is shared as a concrete type (*WithT, which is aliased to *internal.Gomega). 1.15 did not clean this mess up to ensure
42 // that declarations of *WithT in existing code are not broken by the upgrade to 1.15.
43 type Gomega = types.Gomega
44
45 // DefaultGomega supplies the standard package-level implementation
46 var Default = Gomega(internal.NewGomega(internal.FetchDefaultDurationBundle()))
47
48 // NewGomega returns an instance of Gomega wired into the passed-in fail handler.
49 // You generally don't need to use this when using Ginkgo - RegisterFailHandler will wire up the global gomega
50 // However creating a NewGomega with a custom fail handler can be useful in contexts where you want to use Gomega's
51 // rich ecosystem of matchers without causing a test to fail. For example, to aggregate a series of potential failures
52 // or for use in a non-test setting.
53 func NewGomega(fail types.GomegaFailHandler) Gomega {
54 return internal.NewGomega(Default.(*internal.Gomega).DurationBundle).ConfigureWithFailHandler(fail)
55 }
56
57 // WithT wraps a *testing.T and provides `Expect`, `Eventually`, and `Consistently` methods. This allows you to leverage
58 // Gomega's rich ecosystem of matchers in standard `testing` test suites.
59 //
60 // Use `NewWithT` to instantiate a `WithT`
61 //
62 // As of 1.15 both the WithT and Ginkgo variants of Gomega are implemented by the same underlying object
63 // however one (the Ginkgo variant) is exported as an interface (types.Gomega) whereas the other (the withT variant)
64 // is shared as a concrete type (*WithT, which is aliased to *internal.Gomega). 1.15 did not clean this mess up to ensure
65 // that declarations of *WithT in existing code are not broken by the upgrade to 1.15.
66 type WithT = internal.Gomega
67
68 // GomegaWithT is deprecated in favor of gomega.WithT, which does not stutter.
69 type GomegaWithT = WithT
70
71 // NewWithT takes a *testing.T and returngs a `gomega.WithT` allowing you to use `Expect`, `Eventually`, and `Consistently` along with
72 // Gomega's rich ecosystem of matchers in standard `testing` test suits.
73 //
74 // func TestFarmHasCow(t *testing.T) {
75 // g := gomega.NewWithT(t)
76 //
77 // f := farm.New([]string{"Cow", "Horse"})
78 // g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
79 // }
80 func NewWithT(t types.GomegaTestingT) *WithT {
81 return internal.NewGomega(Default.(*internal.Gomega).DurationBundle).ConfigureWithT(t)
82 }
83
84 // NewGomegaWithT is deprecated in favor of gomega.NewWithT, which does not stutter.
85 var NewGomegaWithT = NewWithT
4086
4187 // RegisterFailHandler connects Ginkgo to Gomega. When a matcher fails
4288 // the fail handler passed into RegisterFailHandler is called.
43 func RegisterFailHandler(handler types.GomegaFailHandler) {
44 RegisterFailHandlerWithT(testingtsupport.EmptyTWithHelper{}, handler)
45 }
46
47 // RegisterFailHandlerWithT ensures that the given types.TWithHelper and fail handler
48 // are used globally.
49 func RegisterFailHandlerWithT(t types.TWithHelper, handler types.GomegaFailHandler) {
50 if handler == nil {
51 globalFailWrapper = nil
52 return
53 }
54
55 globalFailWrapper = &types.GomegaFailWrapper{
56 Fail: handler,
57 TWithHelper: t,
58 }
89 func RegisterFailHandler(fail types.GomegaFailHandler) {
90 Default.(*internal.Gomega).ConfigureWithFailHandler(fail)
91 }
92
93 // RegisterFailHandlerWithT is deprecated and will be removed in a future release.
94 // users should use RegisterFailHandler, or RegisterTestingT
95 func RegisterFailHandlerWithT(_ types.GomegaTestingT, fail types.GomegaFailHandler) {
96 fmt.Println("RegisterFailHandlerWithT is deprecated. Please use RegisterFailHandler or RegisterTestingT instead.")
97 Default.(*internal.Gomega).ConfigureWithFailHandler(fail)
5998 }
6099
61100 // RegisterTestingT connects Gomega to Golang's XUnit style
62 // Testing.T tests. It is now deprecated and you should use NewWithT() instead.
63 //
64 // Legacy Documentation:
65 //
66 // You'll need to call this at the top of each XUnit style test:
67 //
68 // func TestFarmHasCow(t *testing.T) {
69 // RegisterTestingT(t)
70 //
71 // f := farm.New([]string{"Cow", "Horse"})
72 // Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
73 // }
74 //
75 // Note that this *testing.T is registered *globally* by Gomega (this is why you don't have to
76 // pass `t` down to the matcher itself). This means that you cannot run the XUnit style tests
77 // in parallel as the global fail handler cannot point to more than one testing.T at a time.
78 //
79 // NewWithT() does not have this limitation
80 //
81 // (As an aside: Ginkgo gets around this limitation by running parallel tests in different *processes*).
101 // Testing.T tests. It is now deprecated and you should use NewWithT() instead to get a fresh instance of Gomega for each test.
82102 func RegisterTestingT(t types.GomegaTestingT) {
83 tWithHelper, hasHelper := t.(types.TWithHelper)
84 if !hasHelper {
85 RegisterFailHandler(testingtsupport.BuildTestingTGomegaFailWrapper(t).Fail)
86 return
87 }
88 RegisterFailHandlerWithT(tWithHelper, testingtsupport.BuildTestingTGomegaFailWrapper(t).Fail)
103 Default.(*internal.Gomega).ConfigureWithT(t)
89104 }
90105
91106 // InterceptGomegaFailures runs a given callback and returns an array of
92107 // failure messages generated by any Gomega assertions within the callback.
93 //
94 // This is accomplished by temporarily replacing the *global* fail handler
95 // with a fail handler that simply annotates failures. The original fail handler
96 // is reset when InterceptGomegaFailures returns.
108 // Exeuction continues after the first failure allowing users to collect all failures
109 // in the callback.
97110 //
98111 // This is most useful when testing custom matchers, but can also be used to check
99112 // on a value using a Gomega assertion without causing a test failure.
100113 func InterceptGomegaFailures(f func()) []string {
101 originalHandler := globalFailWrapper.Fail
114 originalHandler := Default.(*internal.Gomega).Fail
102115 failures := []string{}
103 RegisterFailHandler(func(message string, callerSkip ...int) {
116 Default.(*internal.Gomega).Fail = func(message string, callerSkip ...int) {
104117 failures = append(failures, message)
105 })
118 }
119 defer func() {
120 Default.(*internal.Gomega).Fail = originalHandler
121 }()
106122 f()
107 RegisterFailHandler(originalHandler)
108123 return failures
124 }
125
126 // InterceptGomegaFailure runs a given callback and returns the first
127 // failure message generated by any Gomega assertions within the callback, wrapped in an error.
128 //
129 // The callback ceases execution as soon as the first failed assertion occurs, however Gomega
130 // does not register a failure with the FailHandler registered via RegisterFailHandler - it is up
131 // to the user to decide what to do with the returned error
132 func InterceptGomegaFailure(f func()) (err error) {
133 originalHandler := Default.(*internal.Gomega).Fail
134 Default.(*internal.Gomega).Fail = func(message string, callerSkip ...int) {
135 err = errors.New(message)
136 panic("stop execution")
137 }
138
139 defer func() {
140 Default.(*internal.Gomega).Fail = originalHandler
141 if e := recover(); e != nil {
142 if err == nil {
143 panic(e)
144 }
145 }
146 }()
147
148 f()
149 return err
150 }
151
152 func ensureDefaultGomegaIsConfigured() {
153 if !Default.(*internal.Gomega).IsConfigured() {
154 panic(nilGomegaPanic)
155 }
109156 }
110157
111158 // Ω wraps an actual value allowing assertions to be made on it:
126173 //
127174 // Ω and Expect are identical
128175 func Ω(actual interface{}, extra ...interface{}) Assertion {
129 return ExpectWithOffset(0, actual, extra...)
176 ensureDefaultGomegaIsConfigured()
177 return Default.Ω(actual, extra...)
130178 }
131179
132180 // Expect wraps an actual value allowing assertions to be made on it:
147195 //
148196 // Expect and Ω are identical
149197 func Expect(actual interface{}, extra ...interface{}) Assertion {
150 return ExpectWithOffset(0, actual, extra...)
198 ensureDefaultGomegaIsConfigured()
199 return Default.Expect(actual, extra...)
151200 }
152201
153202 // ExpectWithOffset wraps an actual value allowing assertions to be made on it:
154203 // ExpectWithOffset(1, "foo").To(Equal("foo"))
155204 //
156205 // Unlike `Expect` and `Ω`, `ExpectWithOffset` takes an additional integer argument
157 // that is used to modify the call-stack offset when computing line numbers.
206 // that is used to modify the call-stack offset when computing line numbers. It is
207 // the same as `Expect(...).WithOffset`.
158208 //
159209 // This is most useful in helper functions that make assertions. If you want Gomega's
160210 // error message to refer to the calling line in the test (as opposed to the line in the helper function)
161211 // set the first argument of `ExpectWithOffset` appropriately.
162212 func ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion {
163 if globalFailWrapper == nil {
164 panic(nilFailHandlerPanic)
165 }
166 return assertion.New(actual, globalFailWrapper, offset, extra...)
167 }
168
169 // Eventually wraps an actual value allowing assertions to be made on it.
170 // The assertion is tried periodically until it passes or a timeout occurs.
171 //
172 // Both the timeout and polling interval are configurable as optional arguments:
173 // The first optional argument is the timeout
174 // The second optional argument is the polling interval
175 //
176 // Both intervals can either be specified as time.Duration, parsable duration strings or as floats/integers. In the
177 // last case they are interpreted as seconds.
178 //
179 // If Eventually is passed an actual that is a function taking no arguments and returning at least one value,
180 // then Eventually will call the function periodically and try the matcher against the function's first return value.
181 //
182 // Example:
183 //
184 // Eventually(func() int {
185 // return thingImPolling.Count()
186 // }).Should(BeNumerically(">=", 17))
187 //
188 // Note that this example could be rewritten:
189 //
190 // Eventually(thingImPolling.Count).Should(BeNumerically(">=", 17))
191 //
192 // If the function returns more than one value, then Eventually will pass the first value to the matcher and
193 // assert that all other values are nil/zero.
194 // This allows you to pass Eventually a function that returns a value and an error - a common pattern in Go.
195 //
196 // For example, consider a method that returns a value and an error:
197 // func FetchFromDB() (string, error)
198 //
199 // Then
200 // Eventually(FetchFromDB).Should(Equal("hasselhoff"))
201 //
202 // Will pass only if the the returned error is nil and the returned string passes the matcher.
203 //
204 // Eventually's default timeout is 1 second, and its default polling interval is 10ms
213 ensureDefaultGomegaIsConfigured()
214 return Default.ExpectWithOffset(offset, actual, extra...)
215 }
216
217 /*
218 Eventually enables making assertions on asynchronous behavior.
219
220 Eventually checks that an assertion *eventually* passes. Eventually blocks when called and attempts an assertion periodically until it passes or a timeout occurs. Both the timeout and polling interval are configurable as optional arguments.
221 The first optional argument is the timeout (which defaults to 1s), the second is the polling interval (which defaults to 10ms). Both intervals can be specified as time.Duration, parsable duration strings or floats/integers (in which case they are interpreted as seconds).
222
223 Eventually works with any Gomega compatible matcher and supports making assertions against three categories of actual value:
224
225 **Category 1: Making Eventually assertions on values**
226
227 There are several examples of values that can change over time. These can be passed in to Eventually and will be passed to the matcher repeatedly until a match occurs. For example:
228
229 c := make(chan bool)
230 go DoStuff(c)
231 Eventually(c, "50ms").Should(BeClosed())
232
233 will poll the channel repeatedly until it is closed. In this example `Eventually` will block until either the specified timeout of 50ms has elapsed or the channel is closed, whichever comes first.
234
235 Several Gomega libraries allow you to use Eventually in this way. For example, the gomega/gexec package allows you to block until a *gexec.Session exits successfuly via:
236
237 Eventually(session).Should(gexec.Exit(0))
238
239 And the gomega/gbytes package allows you to monitor a streaming *gbytes.Buffer until a given string is seen:
240
241 Eventually(buffer).Should(gbytes.Say("hello there"))
242
243 In these examples, both `session` and `buffer` are designed to be thread-safe when polled by the `Exit` and `Say` matchers. This is not true in general of most raw values, so while it is tempting to do something like:
244
245 // THIS IS NOT THREAD-SAFE
246 var s *string
247 go mutateStringEventually(s)
248 Eventually(s).Should(Equal("I've changed"))
249
250 this will trigger Go's race detector as the goroutine polling via Eventually will race over the value of s with the goroutine mutating the string. For cases like this you can use channels or introduce your own locking around s by passing Eventually a function.
251
252 **Category 2: Make Eventually assertions on functions**
253
254 Eventually can be passed functions that **take no arguments** and **return at least one value**. When configured this way, Eventually will poll the function repeatedly and pass the first returned value to the matcher.
255
256 For example:
257
258 Eventually(func() int {
259 return client.FetchCount()
260 }).Should(BeNumerically(">=", 17))
261
262 will repeatedly poll client.FetchCount until the BeNumerically matcher is satisfied. (Note that this example could have been written as Eventually(client.FetchCount).Should(BeNumerically(">=", 17)))
263
264 If multple values are returned by the function, Eventually will pass the first value to the matcher and require that all others are zero-valued. This allows you to pass Eventually a function that returns a value and an error - a common patternin Go.
265
266 For example, consider a method that returns a value and an error:
267 func FetchFromDB() (string, error)
268
269 Then
270 Eventually(FetchFromDB).Should(Equal("got it"))
271
272 will pass only if and when the returned error is nil *and* the returned string satisfies the matcher.
273
274 It is important to note that the function passed into Eventually is invoked *synchronously* when polled. Eventually does not (in fact, it cannot) kill the function if it takes longer to return than Eventually's configured timeout. You should design your functions with this in mind.
275
276 **Category 3: Making assertions _in_ the function passed into Eventually**
277
278 When testing complex systems it can be valuable to assert that a _set_ of assertions passes Eventually. Eventually supports this by accepting functions that take a single Gomega argument and return zero or more values.
279
280 Here's an example that makes some asssertions and returns a value and error:
281
282 Eventually(func(g Gomega) (Widget, error) {
283 ids, err := client.FetchIDs()
284 g.Expect(err).NotTo(HaveOccurred())
285 g.Expect(ids).To(ContainElement(1138))
286 return client.FetchWidget(1138)
287 }).Should(Equal(expectedWidget))
288
289 will pass only if all the assertions in the polled function pass and the return value satisfied the matcher.
290
291 Eventually also supports a special case polling function that takes a single Gomega argument and returns no values. Eventually assumes such a function is making assertions and is designed to work with the Succeed matcher to validate that all assertions have passed.
292 For example:
293
294 Eventually(func(g Gomega) {
295 model, err := client.Find(1138)
296 g.Expect(err).NotTo(HaveOccurred())
297 g.Expect(model.Reticulate()).To(Succeed())
298 g.Expect(model.IsReticulated()).To(BeTrue())
299 g.Expect(model.Save()).To(Succeed())
300 }).Should(Succeed())
301
302 will rerun the function until all assertions pass.
303
304 `Eventually` specifying a timeout interval (and an optional polling interval) are
305 the same as `Eventually(...).WithTimeout` or `Eventually(...).WithTimeout(...).WithPolling`.
306 */
205307 func Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion {
206 return EventuallyWithOffset(0, actual, intervals...)
308 ensureDefaultGomegaIsConfigured()
309 return Default.Eventually(actual, intervals...)
207310 }
208311
209312 // EventuallyWithOffset operates like Eventually but takes an additional
210313 // initial argument to indicate an offset in the call stack. This is useful when building helper
211314 // functions that contain matchers. To learn more, read about `ExpectWithOffset`.
315 //
316 // `EventuallyWithOffset` is the same as `Eventually(...).WithOffset`.
317 //
318 // `EventuallyWithOffset` specifying a timeout interval (and an optional polling interval) are
319 // the same as `Eventually(...).WithOffset(...).WithTimeout` or
320 // `Eventually(...).WithOffset(...).WithTimeout(...).WithPolling`.
212321 func EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion {
213 if globalFailWrapper == nil {
214 panic(nilFailHandlerPanic)
215 }
216 timeoutInterval := defaultEventuallyTimeout
217 pollingInterval := defaultEventuallyPollingInterval
218 if len(intervals) > 0 {
219 timeoutInterval = toDuration(intervals[0])
220 }
221 if len(intervals) > 1 {
222 pollingInterval = toDuration(intervals[1])
223 }
224 return asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, actual, globalFailWrapper, timeoutInterval, pollingInterval, offset)
225 }
226
227 // Consistently wraps an actual value allowing assertions to be made on it.
228 // The assertion is tried periodically and is required to pass for a period of time.
229 //
230 // Both the total time and polling interval are configurable as optional arguments:
231 // The first optional argument is the duration that Consistently will run for
232 // The second optional argument is the polling interval
233 //
234 // Both intervals can either be specified as time.Duration, parsable duration strings or as floats/integers. In the
235 // last case they are interpreted as seconds.
236 //
237 // If Consistently is passed an actual that is a function taking no arguments and returning at least one value,
238 // then Consistently will call the function periodically and try the matcher against the function's first return value.
239 //
240 // If the function returns more than one value, then Consistently will pass the first value to the matcher and
241 // assert that all other values are nil/zero.
242 // This allows you to pass Consistently a function that returns a value and an error - a common pattern in Go.
243 //
244 // Consistently is useful in cases where you want to assert that something *does not happen* over a period of time.
245 // For example, you want to assert that a goroutine does *not* send data down a channel. In this case, you could:
246 //
247 // Consistently(channel).ShouldNot(Receive())
248 //
249 // Consistently's default duration is 100ms, and its default polling interval is 10ms
322 ensureDefaultGomegaIsConfigured()
323 return Default.EventuallyWithOffset(offset, actual, intervals...)
324 }
325
326 /*
327 Consistently, like Eventually, enables making assertions on asynchronous behavior.
328
329 Consistently blocks when called for a specified duration. During that duration Consistently repeatedly polls its matcher and ensures that it is satisfied. If the matcher is consistently satisfied, then Consistently will pass. Otherwise Consistently will fail.
330
331 Both the total waiting duration and the polling interval are configurable as optional arguments. The first optional arugment is the duration that Consistently will run for (defaults to 100ms), and the second argument is the polling interval (defaults to 10ms). As with Eventually, these intervals can be passed in as time.Duration, parsable duration strings or an integer or float number of seconds.
332
333 Consistently accepts the same three categories of actual as Eventually, check the Eventually docs to learn more.
334
335 Consistently is useful in cases where you want to assert that something *does not happen* for a period of time. For example, you may want to assert that a goroutine does *not* send data down a channel. In this case you could write:
336
337 Consistently(channel, "200ms").ShouldNot(Receive())
338
339 This will block for 200 milliseconds and repeatedly check the channel and ensure nothing has been received.
340 */
250341 func Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion {
251 return ConsistentlyWithOffset(0, actual, intervals...)
342 ensureDefaultGomegaIsConfigured()
343 return Default.Consistently(actual, intervals...)
252344 }
253345
254346 // ConsistentlyWithOffset operates like Consistently but takes an additional
255347 // initial argument to indicate an offset in the call stack. This is useful when building helper
256348 // functions that contain matchers. To learn more, read about `ExpectWithOffset`.
349 //
350 // `ConsistentlyWithOffset` is the same as `Consistently(...).WithOffset` and
351 // optional `WithTimeout` and `WithPolling`.
257352 func ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion {
258 if globalFailWrapper == nil {
259 panic(nilFailHandlerPanic)
260 }
261 timeoutInterval := defaultConsistentlyDuration
262 pollingInterval := defaultConsistentlyPollingInterval
263 if len(intervals) > 0 {
264 timeoutInterval = toDuration(intervals[0])
265 }
266 if len(intervals) > 1 {
267 pollingInterval = toDuration(intervals[1])
268 }
269 return asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, actual, globalFailWrapper, timeoutInterval, pollingInterval, offset)
353 ensureDefaultGomegaIsConfigured()
354 return Default.ConsistentlyWithOffset(offset, actual, intervals...)
270355 }
271356
272357 // SetDefaultEventuallyTimeout sets the default timeout duration for Eventually. Eventually will repeatedly poll your condition until it succeeds, or until this timeout elapses.
273358 func SetDefaultEventuallyTimeout(t time.Duration) {
274 defaultEventuallyTimeout = t
359 Default.SetDefaultEventuallyTimeout(t)
275360 }
276361
277362 // SetDefaultEventuallyPollingInterval sets the default polling interval for Eventually.
278363 func SetDefaultEventuallyPollingInterval(t time.Duration) {
279 defaultEventuallyPollingInterval = t
364 Default.SetDefaultEventuallyPollingInterval(t)
280365 }
281366
282367 // SetDefaultConsistentlyDuration sets the default duration for Consistently. Consistently will verify that your condition is satisfied for this long.
283368 func SetDefaultConsistentlyDuration(t time.Duration) {
284 defaultConsistentlyDuration = t
369 Default.SetDefaultConsistentlyDuration(t)
285370 }
286371
287372 // SetDefaultConsistentlyPollingInterval sets the default polling interval for Consistently.
288373 func SetDefaultConsistentlyPollingInterval(t time.Duration) {
289 defaultConsistentlyPollingInterval = t
374 Default.SetDefaultConsistentlyPollingInterval(t)
290375 }
291376
292377 // AsyncAssertion is returned by Eventually and Consistently and polls the actual value passed into Eventually against
304389 //
305390 // Eventually(myChannel).Should(Receive(), "Something should have come down the pipe.")
306391 // Consistently(myChannel).ShouldNot(Receive(), func() string { return "Nothing should have come down the pipe." })
307 type AsyncAssertion interface {
308 Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
309 ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
310 }
392 type AsyncAssertion = types.AsyncAssertion
311393
312394 // GomegaAsyncAssertion is deprecated in favor of AsyncAssertion, which does not stutter.
313 type GomegaAsyncAssertion = AsyncAssertion
395 type GomegaAsyncAssertion = types.AsyncAssertion
314396
315397 // Assertion is returned by Ω and Expect and compares the actual value to the matcher
316398 // passed to the Should/ShouldNot and To/ToNot/NotTo methods.
329411 // Example:
330412 //
331413 // Ω(farm.HasCow()).Should(BeTrue(), "Farm %v should have a cow", farm)
332 type Assertion interface {
333 Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
334 ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
335
336 To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
337 ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
338 NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool
339 }
414 type Assertion = types.Assertion
340415
341416 // GomegaAssertion is deprecated in favor of Assertion, which does not stutter.
342 type GomegaAssertion = Assertion
417 type GomegaAssertion = types.Assertion
343418
344419 // OmegaMatcher is deprecated in favor of the better-named and better-organized types.GomegaMatcher but sticks around to support existing code that uses it
345 type OmegaMatcher types.GomegaMatcher
346
347 // WithT wraps a *testing.T and provides `Expect`, `Eventually`, and `Consistently` methods. This allows you to leverage
348 // Gomega's rich ecosystem of matchers in standard `testing` test suites.
349 //
350 // Use `NewWithT` to instantiate a `WithT`
351 type WithT struct {
352 t types.GomegaTestingT
353 }
354
355 // GomegaWithT is deprecated in favor of gomega.WithT, which does not stutter.
356 type GomegaWithT = WithT
357
358 // NewWithT takes a *testing.T and returngs a `gomega.WithT` allowing you to use `Expect`, `Eventually`, and `Consistently` along with
359 // Gomega's rich ecosystem of matchers in standard `testing` test suits.
360 //
361 // func TestFarmHasCow(t *testing.T) {
362 // g := gomega.NewWithT(t)
363 //
364 // f := farm.New([]string{"Cow", "Horse"})
365 // g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
366 // }
367 func NewWithT(t types.GomegaTestingT) *WithT {
368 return &WithT{
369 t: t,
370 }
371 }
372
373 // NewGomegaWithT is deprecated in favor of gomega.NewWithT, which does not stutter.
374 func NewGomegaWithT(t types.GomegaTestingT) *GomegaWithT {
375 return NewWithT(t)
376 }
377
378 // ExpectWithOffset is used to make assertions. See documentation for ExpectWithOffset.
379 func (g *WithT) ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion {
380 return assertion.New(actual, testingtsupport.BuildTestingTGomegaFailWrapper(g.t), offset, extra...)
381 }
382
383 // EventuallyWithOffset is used to make asynchronous assertions. See documentation for EventuallyWithOffset.
384 func (g *WithT) EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion {
385 timeoutInterval := defaultEventuallyTimeout
386 pollingInterval := defaultEventuallyPollingInterval
387 if len(intervals) > 0 {
388 timeoutInterval = toDuration(intervals[0])
389 }
390 if len(intervals) > 1 {
391 pollingInterval = toDuration(intervals[1])
392 }
393 return asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, actual, testingtsupport.BuildTestingTGomegaFailWrapper(g.t), timeoutInterval, pollingInterval, offset)
394 }
395
396 // ConsistentlyWithOffset is used to make asynchronous assertions. See documentation for ConsistentlyWithOffset.
397 func (g *WithT) ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion {
398 timeoutInterval := defaultConsistentlyDuration
399 pollingInterval := defaultConsistentlyPollingInterval
400 if len(intervals) > 0 {
401 timeoutInterval = toDuration(intervals[0])
402 }
403 if len(intervals) > 1 {
404 pollingInterval = toDuration(intervals[1])
405 }
406 return asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, actual, testingtsupport.BuildTestingTGomegaFailWrapper(g.t), timeoutInterval, pollingInterval, offset)
407 }
408
409 // Expect is used to make assertions. See documentation for Expect.
410 func (g *WithT) Expect(actual interface{}, extra ...interface{}) Assertion {
411 return g.ExpectWithOffset(0, actual, extra...)
412 }
413
414 // Eventually is used to make asynchronous assertions. See documentation for Eventually.
415 func (g *WithT) Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion {
416 return g.EventuallyWithOffset(0, actual, intervals...)
417 }
418
419 // Consistently is used to make asynchronous assertions. See documentation for Consistently.
420 func (g *WithT) Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion {
421 return g.ConsistentlyWithOffset(0, actual, intervals...)
422 }
423
424 func toDuration(input interface{}) time.Duration {
425 duration, ok := input.(time.Duration)
426 if ok {
427 return duration
428 }
429
430 value := reflect.ValueOf(input)
431 kind := reflect.TypeOf(input).Kind()
432
433 if reflect.Int <= kind && kind <= reflect.Int64 {
434 return time.Duration(value.Int()) * time.Second
435 } else if reflect.Uint <= kind && kind <= reflect.Uint64 {
436 return time.Duration(value.Uint()) * time.Second
437 } else if reflect.Float32 <= kind && kind <= reflect.Float64 {
438 return time.Duration(value.Float() * float64(time.Second))
439 } else if reflect.String == kind {
440 duration, err := time.ParseDuration(value.String())
441 if err != nil {
442 panic(fmt.Sprintf("%#v is not a valid parsable duration string.", input))
443 }
444 return duration
445 }
446
447 panic(fmt.Sprintf("%v is not a valid interval. Must be time.Duration, parsable duration string or a number.", input))
448 }
449
450 // Gomega describes the essential Gomega DSL. This interface allows libraries
451 // to abstract between the standard package-level function implementations
452 // and alternatives like *WithT.
453 type Gomega interface {
454 Expect(actual interface{}, extra ...interface{}) Assertion
455 Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion
456 Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion
457 }
458
459 type globalFailHandlerGomega struct{}
460
461 // DefaultGomega supplies the standard package-level implementation
462 var Default Gomega = globalFailHandlerGomega{}
463
464 // Expect is used to make assertions. See documentation for Expect.
465 func (globalFailHandlerGomega) Expect(actual interface{}, extra ...interface{}) Assertion {
466 return Expect(actual, extra...)
467 }
468
469 // Eventually is used to make asynchronous assertions. See documentation for Eventually.
470 func (globalFailHandlerGomega) Eventually(actual interface{}, extra ...interface{}) AsyncAssertion {
471 return Eventually(actual, extra...)
472 }
473
474 // Consistently is used to make asynchronous assertions. See documentation for Consistently.
475 func (globalFailHandlerGomega) Consistently(actual interface{}, extra ...interface{}) AsyncAssertion {
476 return Consistently(actual, extra...)
477 }
420 type OmegaMatcher = types.GomegaMatcher
66 "fmt"
77 "reflect"
88 "runtime/debug"
9 "strconv"
910
1011 "github.com/onsi/gomega/format"
1112 errorsutil "github.com/onsi/gomega/gstruct/errors"
2324 // "b": Equal("b"),
2425 // }))
2526 func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher {
27 return &ElementsMatcher{
28 Identifier: identifier,
29 Elements: elements,
30 }
31 }
32
33 //MatchAllElementsWithIndex succeeds if every element of a slice matches the element matcher it maps to
34 //through the id with index function, and every element matcher is matched.
35 // idFn := func(index int, element interface{}) string {
36 // return strconv.Itoa(index)
37 // }
38 //
39 // Expect([]string{"a", "b"}).To(MatchAllElements(idFn, Elements{
40 // "0": Equal("a"),
41 // "1": Equal("b"),
42 // }))
43 func MatchAllElementsWithIndex(identifier IdentifierWithIndex, elements Elements) types.GomegaMatcher {
2644 return &ElementsMatcher{
2745 Identifier: identifier,
2846 Elements: elements,
4664 // "d": Equal("d"),
4765 // }))
4866 func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher {
67 return &ElementsMatcher{
68 Identifier: identifier,
69 Elements: elements,
70 IgnoreExtras: options&IgnoreExtras != 0,
71 IgnoreMissing: options&IgnoreMissing != 0,
72 AllowDuplicates: options&AllowDuplicates != 0,
73 }
74 }
75
76 //MatchElementsWithIndex succeeds if each element of a slice matches the element matcher it maps to
77 //through the id with index function. It can ignore extra elements and/or missing elements.
78 // idFn := func(index int, element interface{}) string {
79 // return strconv.Itoa(index)
80 // }
81 //
82 // Expect([]string{"a", "b", "c"}).To(MatchElements(idFn, IgnoreExtras, Elements{
83 // "0": Equal("a"),
84 // "1": Equal("b"),
85 // }))
86 // Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing, Elements{
87 // "0": Equal("a"),
88 // "1": Equal("b"),
89 // "2": Equal("c"),
90 // "3": Equal("d"),
91 // }))
92 func MatchElementsWithIndex(identifier IdentifierWithIndex, options Options, elements Elements) types.GomegaMatcher {
4993 return &ElementsMatcher{
5094 Identifier: identifier,
5195 Elements: elements,
62106 // Matchers for each element.
63107 Elements Elements
64108 // Function mapping an element to the string key identifying its matcher.
65 Identifier Identifier
109 Identifier Identify
66110
67111 // Whether to ignore extra elements or consider it an error.
68112 IgnoreExtras bool
81125 // Function for identifying (mapping) elements.
82126 type Identifier func(element interface{}) string
83127
128 // Calls the underlying fucntion with the provided params.
129 // Identifier drops the index.
130 func (i Identifier) WithIndexAndElement(index int, element interface{}) string {
131 return i(element)
132 }
133
134 // Uses the index and element to generate an element name
135 type IdentifierWithIndex func(index int, element interface{}) string
136
137 // Calls the underlying fucntion with the provided params.
138 // IdentifierWithIndex uses the index.
139 func (i IdentifierWithIndex) WithIndexAndElement(index int, element interface{}) string {
140 return i(index, element)
141 }
142
143 // Interface for identifing the element
144 type Identify interface {
145 WithIndexAndElement(i int, element interface{}) string
146 }
147
148 // IndexIdentity is a helper function for using an index as
149 // the key in the element map
150 func IndexIdentity(index int, _ interface{}) string {
151 return strconv.Itoa(index)
152 }
153
84154 func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) {
85155 if reflect.TypeOf(actual).Kind() != reflect.Slice {
86156 return false, fmt.Errorf("%v is type %T, expected slice", actual, actual)
105175 elements := map[string]bool{}
106176 for i := 0; i < val.Len(); i++ {
107177 element := val.Index(i).Interface()
108 id := m.Identifier(element)
178 id := m.Identifier.WithIndexAndElement(i, element)
109179 if elements[id] {
110180 if !m.AllowDuplicates {
111181 errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
136136 Expect(nils).Should(m, "should allow an uninitialized slice")
137137 })
138138 })
139
140 Context("with index identifier", func() {
141 allElements := []string{"a", "b"}
142 missingElements := []string{"a"}
143 extraElements := []string{"a", "b", "c"}
144 duplicateElements := []string{"a", "a", "b"}
145 empty := []string{}
146 var nils []string
147
148 It("should use index", func() {
149 m := MatchAllElementsWithIndex(IndexIdentity, Elements{
150 "0": Equal("a"),
151 "1": Equal("b"),
152 })
153 Expect(allElements).Should(m, "should match all elements")
154 Expect(missingElements).ShouldNot(m, "should fail with missing elements")
155 Expect(extraElements).ShouldNot(m, "should fail with extra elements")
156 Expect(duplicateElements).ShouldNot(m, "should fail with duplicate elements")
157 Expect(nils).ShouldNot(m, "should fail with an uninitialized slice")
158
159 m = MatchAllElementsWithIndex(IndexIdentity, Elements{
160 "0": Equal("a"),
161 "1": Equal("fail"),
162 })
163 Expect(allElements).ShouldNot(m, "should run nested matchers")
164
165 m = MatchAllElementsWithIndex(IndexIdentity, Elements{})
166 Expect(empty).Should(m, "should handle empty slices")
167 Expect(allElements).ShouldNot(m, "should handle only empty slices")
168 Expect(nils).Should(m, "should handle nil slices")
169 })
170 })
139171 })
140172
141173 func id(element interface{}) string {
+0
-109
internal/assertion/assertion.go less more
0 package assertion
1
2 import (
3 "fmt"
4 "reflect"
5
6 "github.com/onsi/gomega/types"
7 )
8
9 type Assertion struct {
10 actualInput interface{}
11 failWrapper *types.GomegaFailWrapper
12 offset int
13 extra []interface{}
14 }
15
16 func New(actualInput interface{}, failWrapper *types.GomegaFailWrapper, offset int, extra ...interface{}) *Assertion {
17 return &Assertion{
18 actualInput: actualInput,
19 failWrapper: failWrapper,
20 offset: offset,
21 extra: extra,
22 }
23 }
24
25 func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
26 assertion.failWrapper.TWithHelper.Helper()
27 return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
28 }
29
30 func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
31 assertion.failWrapper.TWithHelper.Helper()
32 return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
33 }
34
35 func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
36 assertion.failWrapper.TWithHelper.Helper()
37 return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
38 }
39
40 func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
41 assertion.failWrapper.TWithHelper.Helper()
42 return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
43 }
44
45 func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
46 assertion.failWrapper.TWithHelper.Helper()
47 return assertion.vetExtras(optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
48 }
49
50 func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) string {
51 switch len(optionalDescription) {
52 case 0:
53 return ""
54 case 1:
55 if describe, ok := optionalDescription[0].(func() string); ok {
56 return describe() + "\n"
57 }
58 }
59 return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
60 }
61
62 func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
63 matches, err := matcher.Match(assertion.actualInput)
64 assertion.failWrapper.TWithHelper.Helper()
65 if err != nil {
66 description := assertion.buildDescription(optionalDescription...)
67 assertion.failWrapper.Fail(description+err.Error(), 2+assertion.offset)
68 return false
69 }
70 if matches != desiredMatch {
71 var message string
72 if desiredMatch {
73 message = matcher.FailureMessage(assertion.actualInput)
74 } else {
75 message = matcher.NegatedFailureMessage(assertion.actualInput)
76 }
77 description := assertion.buildDescription(optionalDescription...)
78 assertion.failWrapper.Fail(description+message, 2+assertion.offset)
79 return false
80 }
81
82 return true
83 }
84
85 func (assertion *Assertion) vetExtras(optionalDescription ...interface{}) bool {
86 success, message := vetExtras(assertion.extra)
87 if success {
88 return true
89 }
90
91 description := assertion.buildDescription(optionalDescription...)
92 assertion.failWrapper.TWithHelper.Helper()
93 assertion.failWrapper.Fail(description+message, 2+assertion.offset)
94 return false
95 }
96
97 func vetExtras(extras []interface{}) (bool, string) {
98 for i, extra := range extras {
99 if extra != nil {
100 zeroValue := reflect.Zero(reflect.TypeOf(extra)).Interface()
101 if !reflect.DeepEqual(zeroValue, extra) {
102 message := fmt.Sprintf("Unexpected non-nil/non-zero extra argument at index %d:\n\t<%T>: %#v", i+1, extra, extra)
103 return false, message
104 }
105 }
106 }
107 return true, ""
108 }
+0
-13
internal/assertion/assertion_suite_test.go less more
0 package assertion_test
1
2 import (
3 . "github.com/onsi/ginkgo"
4 . "github.com/onsi/gomega"
5
6 "testing"
7 )
8
9 func TestAssertion(t *testing.T) {
10 RegisterFailHandler(Fail)
11 RunSpecs(t, "Assertion Suite")
12 }
+0
-277
internal/assertion/assertion_test.go less more
0 package assertion_test
1
2 import (
3 "errors"
4
5 "github.com/onsi/gomega/internal/testingtsupport"
6
7 . "github.com/onsi/ginkgo"
8 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/internal/assertion"
10 "github.com/onsi/gomega/internal/fakematcher"
11 "github.com/onsi/gomega/types"
12 )
13
14 var _ = Describe("Assertion", func() {
15 var (
16 a *assertion.Assertion
17 failureMessage string
18 failureCallerSkip int
19 matcher *fakematcher.FakeMatcher
20 )
21
22 input := "The thing I'm testing"
23
24 var fakeFailWrapper = &types.GomegaFailWrapper{
25 Fail: func(message string, callerSkip ...int) {
26 failureMessage = message
27 if len(callerSkip) == 1 {
28 failureCallerSkip = callerSkip[0]
29 }
30 },
31 TWithHelper: testingtsupport.EmptyTWithHelper{},
32 }
33
34 BeforeEach(func() {
35 matcher = &fakematcher.FakeMatcher{}
36 failureMessage = ""
37 failureCallerSkip = 0
38 a = assertion.New(input, fakeFailWrapper, 1)
39 })
40
41 When("called", func() {
42 It("should pass the provided input value to the matcher", func() {
43 a.Should(matcher)
44
45 Expect(matcher.ReceivedActual).Should(Equal(input))
46 matcher.ReceivedActual = ""
47
48 a.ShouldNot(matcher)
49
50 Expect(matcher.ReceivedActual).Should(Equal(input))
51 matcher.ReceivedActual = ""
52
53 a.To(matcher)
54
55 Expect(matcher.ReceivedActual).Should(Equal(input))
56 matcher.ReceivedActual = ""
57
58 a.ToNot(matcher)
59
60 Expect(matcher.ReceivedActual).Should(Equal(input))
61 matcher.ReceivedActual = ""
62
63 a.NotTo(matcher)
64
65 Expect(matcher.ReceivedActual).Should(Equal(input))
66 })
67 })
68
69 When("the matcher succeeds", func() {
70 BeforeEach(func() {
71 matcher.MatchesToReturn = true
72 matcher.ErrToReturn = nil
73 })
74
75 Context("and a positive assertion is being made", func() {
76 It("should not call the failure callback", func() {
77 a.Should(matcher)
78 Expect(failureMessage).Should(Equal(""))
79 })
80
81 It("should be true", func() {
82 Expect(a.Should(matcher)).Should(BeTrue())
83 })
84 })
85
86 Context("and a negative assertion is being made", func() {
87 It("should call the failure callback", func() {
88 a.ShouldNot(matcher)
89 Expect(failureMessage).Should(Equal("negative: The thing I'm testing"))
90 Expect(failureCallerSkip).Should(Equal(3))
91 })
92
93 It("should be false", func() {
94 Expect(a.ShouldNot(matcher)).Should(BeFalse())
95 })
96 })
97
98 Context("and the optional description is a function", func() {
99 It("should not evaluate that function", func() {
100 evaluated := false
101 a.Should(matcher, func() string {
102 evaluated = true
103 return "A description"
104 })
105 Expect(evaluated).Should(BeFalse())
106 })
107 })
108 })
109
110 When("the matcher fails", func() {
111 BeforeEach(func() {
112 matcher.MatchesToReturn = false
113 matcher.ErrToReturn = nil
114 })
115
116 Context("and a positive assertion is being made", func() {
117 It("should call the failure callback", func() {
118 a.Should(matcher)
119 Expect(failureMessage).Should(Equal("positive: The thing I'm testing"))
120 Expect(failureCallerSkip).Should(Equal(3))
121 })
122
123 It("should be false", func() {
124 Expect(a.Should(matcher)).Should(BeFalse())
125 })
126 })
127
128 Context("and a negative assertion is being made", func() {
129 It("should not call the failure callback", func() {
130 a.ShouldNot(matcher)
131 Expect(failureMessage).Should(Equal(""))
132 })
133
134 It("should be true", func() {
135 Expect(a.ShouldNot(matcher)).Should(BeTrue())
136 })
137 })
138 })
139
140 Context("When reporting a failure", func() {
141 BeforeEach(func() {
142 matcher.MatchesToReturn = false
143 matcher.ErrToReturn = nil
144 })
145
146 Context("and there is an optional description", func() {
147 It("should append the description to the failure message", func() {
148 a.Should(matcher, "A description")
149 Expect(failureMessage).Should(Equal("A description\npositive: The thing I'm testing"))
150 Expect(failureCallerSkip).Should(Equal(3))
151 })
152 })
153
154 Context("and there are multiple arguments to the optional description", func() {
155 It("should append the formatted description to the failure message", func() {
156 a.Should(matcher, "A description of [%d]", 3)
157 Expect(failureMessage).Should(Equal("A description of [3]\npositive: The thing I'm testing"))
158 Expect(failureCallerSkip).Should(Equal(3))
159 })
160 })
161
162 Context("and the optional description is a function", func() {
163 It("should append the description to the failure message", func() {
164 a.Should(matcher, func() string { return "A description" })
165 Expect(failureMessage).Should(Equal("A description\npositive: The thing I'm testing"))
166 Expect(failureCallerSkip).Should(Equal(3))
167 })
168 })
169 })
170
171 Context("When the matcher returns an error", func() {
172 BeforeEach(func() {
173 matcher.ErrToReturn = errors.New("Kaboom!")
174 })
175
176 Context("and a positive assertion is being made", func() {
177 It("should call the failure callback", func() {
178 matcher.MatchesToReturn = true
179 a.Should(matcher)
180 Expect(failureMessage).Should(Equal("Kaboom!"))
181 Expect(failureCallerSkip).Should(Equal(3))
182 })
183 })
184
185 Context("and a negative assertion is being made", func() {
186 It("should call the failure callback", func() {
187 matcher.MatchesToReturn = false
188 a.ShouldNot(matcher)
189 Expect(failureMessage).Should(Equal("Kaboom!"))
190 Expect(failureCallerSkip).Should(Equal(3))
191 })
192 })
193
194 It("should always be false", func() {
195 Expect(a.Should(matcher)).Should(BeFalse())
196 Expect(a.ShouldNot(matcher)).Should(BeFalse())
197 })
198 })
199
200 When("there are extra parameters", func() {
201 It("(a simple example)", func() {
202 Expect(func() (string, int, error) {
203 return "foo", 0, nil
204 }()).Should(Equal("foo"))
205 })
206
207 When("the parameters are all nil or zero", func() {
208 It("should invoke the matcher", func() {
209 matcher.MatchesToReturn = true
210 matcher.ErrToReturn = nil
211
212 var typedNil []string
213 a = assertion.New(input, fakeFailWrapper, 1, 0, nil, typedNil)
214
215 result := a.Should(matcher)
216 Expect(result).Should(BeTrue())
217 Expect(matcher.ReceivedActual).Should(Equal(input))
218
219 Expect(failureMessage).Should(BeZero())
220 })
221 })
222
223 When("any of the parameters are not nil or zero", func() {
224 It("should call the failure callback", func() {
225 matcher.MatchesToReturn = false
226 matcher.ErrToReturn = nil
227
228 a = assertion.New(input, fakeFailWrapper, 1, errors.New("foo"))
229 result := a.Should(matcher)
230 Expect(result).Should(BeFalse())
231 Expect(matcher.ReceivedActual).Should(BeZero(), "The matcher doesn't even get called")
232 Expect(failureMessage).Should(ContainSubstring("foo"))
233 failureMessage = ""
234
235 a = assertion.New(input, fakeFailWrapper, 1, nil, 1)
236 result = a.ShouldNot(matcher)
237 Expect(result).Should(BeFalse())
238 Expect(failureMessage).Should(ContainSubstring("1"))
239 failureMessage = ""
240
241 a = assertion.New(input, fakeFailWrapper, 1, nil, 0, []string{"foo"})
242 result = a.To(matcher)
243 Expect(result).Should(BeFalse())
244 Expect(failureMessage).Should(ContainSubstring("foo"))
245 failureMessage = ""
246
247 a = assertion.New(input, fakeFailWrapper, 1, nil, 0, []string{"foo"})
248 result = a.ToNot(matcher)
249 Expect(result).Should(BeFalse())
250 Expect(failureMessage).Should(ContainSubstring("foo"))
251 failureMessage = ""
252
253 a = assertion.New(input, fakeFailWrapper, 1, nil, 0, []string{"foo"})
254 result = a.NotTo(matcher)
255 Expect(result).Should(BeFalse())
256 Expect(failureMessage).Should(ContainSubstring("foo"))
257 Expect(failureCallerSkip).Should(Equal(3))
258 })
259 })
260 })
261
262 Context("Making an assertion without a registered fail handler", func() {
263 It("should panic", func() {
264 defer func() {
265 e := recover()
266 RegisterFailHandler(Fail)
267 if e == nil {
268 Fail("expected a panic to have occurred")
269 }
270 }()
271
272 RegisterFailHandler(nil)
273 Expect(true).Should(BeTrue())
274 })
275 })
276 })
0 package internal
1
2 import (
3 "fmt"
4 "reflect"
5
6 "github.com/onsi/gomega/types"
7 )
8
9 type Assertion struct {
10 actuals []interface{} // actual value plus all extra values
11 actualIndex int // value to pass to the matcher
12 vet vetinari // the vet to call before calling Gomega matcher
13 offset int
14 g *Gomega
15 }
16
17 // ...obligatory discworld reference, as "vetineer" doesn't sound ... quite right.
18 type vetinari func(assertion *Assertion, optionalDescription ...interface{}) bool
19
20 func NewAssertion(actualInput interface{}, g *Gomega, offset int, extra ...interface{}) *Assertion {
21 return &Assertion{
22 actuals: append([]interface{}{actualInput}, extra...),
23 actualIndex: 0,
24 vet: (*Assertion).vetActuals,
25 offset: offset,
26 g: g,
27 }
28 }
29
30 func (assertion *Assertion) WithOffset(offset int) types.Assertion {
31 assertion.offset = offset
32 return assertion
33 }
34
35 func (assertion *Assertion) Error() types.Assertion {
36 return &Assertion{
37 actuals: assertion.actuals,
38 actualIndex: len(assertion.actuals) - 1,
39 vet: (*Assertion).vetError,
40 offset: assertion.offset,
41 g: assertion.g,
42 }
43 }
44
45 func (assertion *Assertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
46 assertion.g.THelper()
47 return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
48 }
49
50 func (assertion *Assertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
51 assertion.g.THelper()
52 return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
53 }
54
55 func (assertion *Assertion) To(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
56 assertion.g.THelper()
57 return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, true, optionalDescription...)
58 }
59
60 func (assertion *Assertion) ToNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
61 assertion.g.THelper()
62 return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
63 }
64
65 func (assertion *Assertion) NotTo(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
66 assertion.g.THelper()
67 return assertion.vet(assertion, optionalDescription...) && assertion.match(matcher, false, optionalDescription...)
68 }
69
70 func (assertion *Assertion) buildDescription(optionalDescription ...interface{}) string {
71 switch len(optionalDescription) {
72 case 0:
73 return ""
74 case 1:
75 if describe, ok := optionalDescription[0].(func() string); ok {
76 return describe() + "\n"
77 }
78 }
79 return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
80 }
81
82 func (assertion *Assertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
83 actualInput := assertion.actuals[assertion.actualIndex]
84 matches, err := matcher.Match(actualInput)
85 assertion.g.THelper()
86 if err != nil {
87 description := assertion.buildDescription(optionalDescription...)
88 assertion.g.Fail(description+err.Error(), 2+assertion.offset)
89 return false
90 }
91 if matches != desiredMatch {
92 var message string
93 if desiredMatch {
94 message = matcher.FailureMessage(actualInput)
95 } else {
96 message = matcher.NegatedFailureMessage(actualInput)
97 }
98 description := assertion.buildDescription(optionalDescription...)
99 assertion.g.Fail(description+message, 2+assertion.offset)
100 return false
101 }
102
103 return true
104 }
105
106 // vetActuals vets the actual values, with the (optional) exception of a
107 // specific value, such as the first value in case non-error assertions, or the
108 // last value in case of Error()-based assertions.
109 func (assertion *Assertion) vetActuals(optionalDescription ...interface{}) bool {
110 success, message := vetActuals(assertion.actuals, assertion.actualIndex)
111 if success {
112 return true
113 }
114
115 description := assertion.buildDescription(optionalDescription...)
116 assertion.g.THelper()
117 assertion.g.Fail(description+message, 2+assertion.offset)
118 return false
119 }
120
121 // vetError vets the actual values, except for the final error value, in case
122 // the final error value is non-zero. Otherwise, it doesn't vet the actual
123 // values, as these are allowed to take on any values unless there is a non-zero
124 // error value.
125 func (assertion *Assertion) vetError(optionalDescription ...interface{}) bool {
126 if err := assertion.actuals[assertion.actualIndex]; err != nil {
127 // Go error result idiom: all other actual values must be zero values.
128 return assertion.vetActuals(optionalDescription...)
129 }
130 return true
131 }
132
133 // vetActuals vets a slice of actual values, optionally skipping a particular
134 // value slice element, such as the first or last value slice element.
135 func vetActuals(actuals []interface{}, skipIndex int) (bool, string) {
136 for i, actual := range actuals {
137 if i == skipIndex {
138 continue
139 }
140 if actual != nil {
141 zeroValue := reflect.Zero(reflect.TypeOf(actual)).Interface()
142 if !reflect.DeepEqual(zeroValue, actual) {
143 message := fmt.Sprintf("Unexpected non-nil/non-zero argument at index %d:\n\t<%T>: %#v", i, actual, actual)
144 return false, message
145 }
146 }
147 }
148 return true, ""
149 }
0 package internal_test
1
2 import (
3 "errors"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/ginkgo/extensions/table"
7 . "github.com/onsi/gomega"
8 )
9
10 var _ = Describe("Making Synchronous Assertions", func() {
11 var SHOULD_MATCH = true
12 var SHOULD_NOT_MATCH = false
13 var IT_PASSES = true
14 var IT_FAILS = false
15
16 Extras := func(extras ...interface{}) []interface{} {
17 return extras
18 }
19
20 OptionalDescription := func(optionalDescription ...interface{}) []interface{} {
21 return optionalDescription
22 }
23
24 DescribeTable(
25 "the various cases",
26 func(actual interface{}, extras []interface{}, optionalDescription []interface{}, isPositiveAssertion bool, expectedFailureMessage string, expectedReturnValue bool) {
27 if isPositiveAssertion {
28 ig := NewInstrumentedGomega()
29 returnValue := ig.G.Expect(actual, extras...).To(SpecMatch(), optionalDescription...)
30 Expect(returnValue).To(Equal(expectedReturnValue))
31 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
32 if expectedFailureMessage != "" {
33 Expect(ig.FailureSkip).To(Equal([]int{2}))
34 }
35 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).To"))
36
37 ig = NewInstrumentedGomega()
38 returnValue = ig.G.ExpectWithOffset(3, actual, extras...).To(SpecMatch(), optionalDescription...)
39 Expect(returnValue).To(Equal(expectedReturnValue))
40 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
41 if expectedFailureMessage != "" {
42 Expect(ig.FailureSkip).To(Equal([]int{5}))
43 }
44 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).To"))
45
46 ig = NewInstrumentedGomega()
47 returnValue = ig.G.Ω(actual, extras...).Should(SpecMatch(), optionalDescription...)
48 Expect(returnValue).To(Equal(expectedReturnValue))
49 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
50 if expectedFailureMessage != "" {
51 Expect(ig.FailureSkip).To(Equal([]int{2}))
52 }
53 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).Should"))
54 } else {
55 ig := NewInstrumentedGomega()
56 returnValue := ig.G.Expect(actual, extras...).ToNot(SpecMatch(), optionalDescription...)
57 Expect(returnValue).To(Equal(expectedReturnValue))
58 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
59 if expectedFailureMessage != "" {
60 Expect(ig.FailureSkip).To(Equal([]int{2}))
61 }
62 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).ToNot"))
63
64 ig = NewInstrumentedGomega()
65 returnValue = ig.G.Expect(actual, extras...).NotTo(SpecMatch(), optionalDescription...)
66 Expect(returnValue).To(Equal(expectedReturnValue))
67 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
68 if expectedFailureMessage != "" {
69 Expect(ig.FailureSkip).To(Equal([]int{2}))
70 }
71 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).NotTo"))
72
73 ig = NewInstrumentedGomega()
74 returnValue = ig.G.ExpectWithOffset(3, actual, extras...).NotTo(SpecMatch(), optionalDescription...)
75 Expect(returnValue).To(Equal(expectedReturnValue))
76 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
77 if expectedFailureMessage != "" {
78 Expect(ig.FailureSkip).To(Equal([]int{5}))
79 }
80 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).NotTo"))
81
82 ig = NewInstrumentedGomega()
83 returnValue = ig.G.Ω(actual, extras...).ShouldNot(SpecMatch(), optionalDescription...)
84 Expect(returnValue).To(Equal(expectedReturnValue))
85 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
86 if expectedFailureMessage != "" {
87 Expect(ig.FailureSkip).To(Equal([]int{2}))
88 }
89 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).ShouldNot"))
90 }
91 },
92 Entry(
93 "when the matcher matches and a positive assertion is being made",
94 MATCH, Extras(), OptionalDescription(),
95 SHOULD_MATCH, "", IT_PASSES,
96 ),
97 Entry(
98 "when the matcher matches and a negative assertion is being made",
99 MATCH, Extras(), OptionalDescription(),
100 SHOULD_NOT_MATCH, "negative: match", IT_FAILS,
101 ),
102 Entry(
103 "when the matcher does not match and a positive assertion is being made",
104 NO_MATCH, Extras(), OptionalDescription(),
105 SHOULD_MATCH, "positive: no match", IT_FAILS,
106 ),
107 Entry(
108 "when the matcher does not match and a negative assertion is being made",
109 NO_MATCH, Extras(), OptionalDescription(),
110 SHOULD_NOT_MATCH, "", IT_PASSES,
111 ),
112 Entry(
113 "when the matcher returns an error and a positive assertion is being made",
114 ERR_MATCH, Extras(), OptionalDescription(),
115 SHOULD_MATCH, "spec matcher error", IT_FAILS,
116 ),
117 Entry(
118 "when the matcher returns an error and a negative assertion is being made",
119 ERR_MATCH, Extras(), OptionalDescription(),
120 SHOULD_NOT_MATCH, "spec matcher error", IT_FAILS,
121 ),
122 Entry(
123 "when a failure occurs and there is a single optional description",
124 NO_MATCH, Extras(), OptionalDescription("a description"),
125 SHOULD_MATCH, "a description\npositive: no match", IT_FAILS,
126 ),
127 Entry(
128 "when a failure occurs and there are multiple optional descriptions",
129 NO_MATCH, Extras(), OptionalDescription("a description of [%d]", 3),
130 SHOULD_MATCH, "a description of [3]\npositive: no match", IT_FAILS,
131 ),
132 Entry(
133 "when a failure occurs and the optional description is a function",
134 NO_MATCH, Extras(), OptionalDescription(func() string { return "a description" }),
135 SHOULD_MATCH, "a description\npositive: no match", IT_FAILS,
136 ),
137 Entry(
138 "when the matcher matches and zero-valued extra parameters are included, it passes",
139 MATCH, Extras(0, "", struct{ Foo string }{}, nil), OptionalDescription(),
140 SHOULD_MATCH, "", IT_PASSES,
141 ),
142 Entry(
143 "when the matcher matches but a non-zero-valued extra parameter is included, it fails",
144 MATCH, Extras(1, "bam", struct{ Foo string }{Foo: "foo"}, nil), OptionalDescription(),
145 SHOULD_MATCH, "Unexpected non-nil/non-zero argument at index 1:\n\t<int>: 1", IT_FAILS,
146 ),
147 )
148
149 var SHOULD_OCCUR = true
150 var SHOULD_NOT_OCCUR = false
151
152 DescribeTable("error expectations",
153 func(a, b int, e error, isPositiveAssertion bool, expectedFailureMessage string, expectedReturnValue bool) {
154 abe := func(a, b int, e error) (int, int, error) {
155 return a, b, e
156 }
157 ig := NewInstrumentedGomega()
158 var returnValue bool
159 if isPositiveAssertion {
160 returnValue = ig.G.Expect(abe(a, b, e)).Error().To(HaveOccurred())
161 } else {
162 returnValue = ig.G.Expect(abe(a, b, e)).Error().NotTo(HaveOccurred())
163 }
164 Expect(returnValue).To(Equal(expectedReturnValue))
165 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
166 if expectedFailureMessage != "" {
167 Expect(ig.FailureSkip).To(Equal([]int{2}))
168 }
169 },
170 Entry(
171 "when non-zero results without error",
172 1, 2, nil,
173 SHOULD_NOT_OCCUR, "", IT_PASSES,
174 ),
175 Entry(
176 "when non-zero results with error",
177 1, 2, errors.New("D'oh!"),
178 SHOULD_NOT_OCCUR, "Unexpected non-nil/non-zero argument at index 0:\n\t<int>: 1", IT_FAILS,
179 ),
180 Entry(
181 "when non-zero results without error",
182 0, 0, errors.New("D'oh!"),
183 SHOULD_OCCUR, "", IT_PASSES,
184 ),
185 Entry(
186 "when non-zero results with error",
187 1, 2, errors.New("D'oh!"),
188 SHOULD_OCCUR, "Unexpected non-nil/non-zero argument at index 0:\n\t<int>: 1", IT_FAILS,
189 ),
190 )
191
192 })
0 package internal
1
2 import (
3 "errors"
4 "fmt"
5 "reflect"
6 "runtime"
7 "time"
8
9 "github.com/onsi/gomega/types"
10 )
11
12 type AsyncAssertionType uint
13
14 const (
15 AsyncAssertionTypeEventually AsyncAssertionType = iota
16 AsyncAssertionTypeConsistently
17 )
18
19 type AsyncAssertion struct {
20 asyncType AsyncAssertionType
21
22 actualIsFunc bool
23 actualValue interface{}
24 actualFunc func() ([]reflect.Value, error)
25
26 timeoutInterval time.Duration
27 pollingInterval time.Duration
28 offset int
29 g *Gomega
30 }
31
32 func NewAsyncAssertion(asyncType AsyncAssertionType, actualInput interface{}, g *Gomega, timeoutInterval time.Duration, pollingInterval time.Duration, offset int) *AsyncAssertion {
33 out := &AsyncAssertion{
34 asyncType: asyncType,
35 timeoutInterval: timeoutInterval,
36 pollingInterval: pollingInterval,
37 offset: offset,
38 g: g,
39 }
40
41 switch actualType := reflect.TypeOf(actualInput); {
42 case actualType.Kind() != reflect.Func:
43 out.actualValue = actualInput
44 case actualType.NumIn() == 0 && actualType.NumOut() > 0:
45 out.actualIsFunc = true
46 out.actualFunc = func() ([]reflect.Value, error) {
47 return reflect.ValueOf(actualInput).Call([]reflect.Value{}), nil
48 }
49 case actualType.NumIn() == 1 && actualType.In(0).Implements(reflect.TypeOf((*types.Gomega)(nil)).Elem()):
50 out.actualIsFunc = true
51 out.actualFunc = func() (values []reflect.Value, err error) {
52 var assertionFailure error
53 assertionCapturingGomega := NewGomega(g.DurationBundle).ConfigureWithFailHandler(func(message string, callerSkip ...int) {
54 skip := 0
55 if len(callerSkip) > 0 {
56 skip = callerSkip[0]
57 }
58 _, file, line, _ := runtime.Caller(skip + 1)
59 assertionFailure = fmt.Errorf("Assertion in callback at %s:%d failed:\n%s", file, line, message)
60 panic("stop execution")
61 })
62
63 defer func() {
64 if actualType.NumOut() == 0 {
65 if assertionFailure == nil {
66 values = []reflect.Value{reflect.Zero(reflect.TypeOf((*error)(nil)).Elem())}
67 } else {
68 values = []reflect.Value{reflect.ValueOf(assertionFailure)}
69 }
70 } else {
71 err = assertionFailure
72 }
73 if e := recover(); e != nil && assertionFailure == nil {
74 panic(e)
75 }
76 }()
77
78 values = reflect.ValueOf(actualInput).Call([]reflect.Value{reflect.ValueOf(assertionCapturingGomega)})
79 return
80 }
81 default:
82 msg := fmt.Sprintf("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes %d arguments and returns %d values.", actualType.NumIn(), actualType.NumOut())
83 g.Fail(msg, offset+4)
84 }
85
86 return out
87 }
88
89 func (assertion *AsyncAssertion) WithOffset(offset int) types.AsyncAssertion {
90 assertion.offset = offset
91 return assertion
92 }
93
94 func (assertion *AsyncAssertion) WithTimeout(interval time.Duration) types.AsyncAssertion {
95 assertion.timeoutInterval = interval
96 return assertion
97 }
98
99 func (assertion *AsyncAssertion) WithPolling(interval time.Duration) types.AsyncAssertion {
100 assertion.pollingInterval = interval
101 return assertion
102 }
103
104 func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
105 assertion.g.THelper()
106 return assertion.match(matcher, true, optionalDescription...)
107 }
108
109 func (assertion *AsyncAssertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
110 assertion.g.THelper()
111 return assertion.match(matcher, false, optionalDescription...)
112 }
113
114 func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interface{}) string {
115 switch len(optionalDescription) {
116 case 0:
117 return ""
118 case 1:
119 if describe, ok := optionalDescription[0].(func() string); ok {
120 return describe() + "\n"
121 }
122 }
123 return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
124 }
125
126 func (assertion *AsyncAssertion) pollActual() (interface{}, error) {
127 if !assertion.actualIsFunc {
128 return assertion.actualValue, nil
129 }
130
131 values, err := assertion.actualFunc()
132 if err != nil {
133 return nil, err
134 }
135 extras := []interface{}{nil}
136 for _, value := range values[1:] {
137 extras = append(extras, value.Interface())
138 }
139 success, message := vetActuals(extras, 0)
140 if !success {
141 return nil, errors.New(message)
142 }
143
144 return values[0].Interface(), nil
145 }
146
147 func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, value interface{}) bool {
148 if assertion.actualIsFunc {
149 return true
150 }
151 return types.MatchMayChangeInTheFuture(matcher, value)
152 }
153
154 func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
155 timer := time.Now()
156 timeout := time.After(assertion.timeoutInterval)
157
158 var matches bool
159 var err error
160 mayChange := true
161 value, err := assertion.pollActual()
162 if err == nil {
163 mayChange = assertion.matcherMayChange(matcher, value)
164 matches, err = matcher.Match(value)
165 }
166
167 assertion.g.THelper()
168
169 fail := func(preamble string) {
170 errMsg := ""
171 message := ""
172 if err != nil {
173 errMsg = "Error: " + err.Error()
174 } else {
175 if desiredMatch {
176 message = matcher.FailureMessage(value)
177 } else {
178 message = matcher.NegatedFailureMessage(value)
179 }
180 }
181 assertion.g.THelper()
182 description := assertion.buildDescription(optionalDescription...)
183 assertion.g.Fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset)
184 }
185
186 if assertion.asyncType == AsyncAssertionTypeEventually {
187 for {
188 if err == nil && matches == desiredMatch {
189 return true
190 }
191
192 if !mayChange {
193 fail("No future change is possible. Bailing out early")
194 return false
195 }
196
197 select {
198 case <-time.After(assertion.pollingInterval):
199 value, err = assertion.pollActual()
200 if err == nil {
201 mayChange = assertion.matcherMayChange(matcher, value)
202 matches, err = matcher.Match(value)
203 }
204 case <-timeout:
205 fail("Timed out")
206 return false
207 }
208 }
209 } else if assertion.asyncType == AsyncAssertionTypeConsistently {
210 for {
211 if !(err == nil && matches == desiredMatch) {
212 fail("Failed")
213 return false
214 }
215
216 if !mayChange {
217 return true
218 }
219
220 select {
221 case <-time.After(assertion.pollingInterval):
222 value, err = assertion.pollActual()
223 if err == nil {
224 mayChange = assertion.matcherMayChange(matcher, value)
225 matches, err = matcher.Match(value)
226 }
227 case <-timeout:
228 return true
229 }
230 }
231 }
232
233 return false
234 }
0 package internal_test
1
2 import (
3 "errors"
4 "runtime"
5 "time"
6
7 . "github.com/onsi/ginkgo"
8 . "github.com/onsi/gomega"
9 )
10
11 var _ = Describe("Asynchronous Assertions", func() {
12 var ig *InstrumentedGomega
13 BeforeEach(func() {
14 ig = NewInstrumentedGomega()
15 })
16
17 Describe("Basic Eventually support", func() {
18 Context("the positive case", func() {
19 It("polls the function and matcher until a match occurs", func() {
20 counter := 0
21 ig.G.Eventually(func() string {
22 counter++
23 if counter > 5 {
24 return MATCH
25 }
26 return NO_MATCH
27 }).Should(SpecMatch())
28 Ω(counter).Should(Equal(6))
29 Ω(ig.FailureMessage).Should(BeZero())
30 })
31
32 It("continues polling even if the matcher errors", func() {
33 counter := 0
34 ig.G.Eventually(func() string {
35 counter++
36 if counter > 5 {
37 return MATCH
38 }
39 return ERR_MATCH
40 }).Should(SpecMatch())
41 Ω(counter).Should(Equal(6))
42 Ω(ig.FailureMessage).Should(BeZero())
43 })
44
45 It("times out eventually if the assertion doesn't match in time", func() {
46 counter := 0
47 ig.G.Eventually(func() string {
48 counter++
49 if counter > 100 {
50 return MATCH
51 }
52 return NO_MATCH
53 }).WithTimeout(200 * time.Millisecond).WithPolling(20 * time.Millisecond).Should(SpecMatch())
54 Ω(counter).Should(BeNumerically(">", 2))
55 Ω(counter).Should(BeNumerically("<", 20))
56 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
57 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
58 Ω(ig.FailureSkip).Should(Equal([]int{3}))
59 })
60 })
61
62 Context("the negative case", func() {
63 It("polls the function and matcher until a match does not occur", func() {
64 counter := 0
65 ig.G.Eventually(func() string {
66 counter++
67 if counter > 5 {
68 return NO_MATCH
69 }
70 return MATCH
71 }).ShouldNot(SpecMatch())
72 Ω(counter).Should(Equal(6))
73 Ω(ig.FailureMessage).Should(BeZero())
74 })
75
76 It("continues polling when the matcher errors - an error does not count as a successful non-match", func() {
77 counter := 0
78 ig.G.Eventually(func() string {
79 counter++
80 if counter > 5 {
81 return NO_MATCH
82 }
83 return ERR_MATCH
84 }).ShouldNot(SpecMatch())
85 Ω(counter).Should(Equal(6))
86 Ω(ig.FailureMessage).Should(BeZero())
87 })
88
89 It("times out eventually if the assertion doesn't match in time", func() {
90 counter := 0
91 ig.G.Eventually(func() string {
92 counter++
93 if counter > 100 {
94 return NO_MATCH
95 }
96 return MATCH
97 }).WithTimeout(200 * time.Millisecond).WithPolling(20 * time.Millisecond).ShouldNot(SpecMatch())
98 Ω(counter).Should(BeNumerically(">", 2))
99 Ω(counter).Should(BeNumerically("<", 20))
100 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
101 Ω(ig.FailureMessage).Should(ContainSubstring("negative: match"))
102 Ω(ig.FailureSkip).Should(Equal([]int{3}))
103 })
104 })
105
106 Context("when a failure occurs", func() {
107 It("registers the appropriate helper functions", func() {
108 ig.G.Eventually(NO_MATCH).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(SpecMatch())
109 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
110 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
111 Ω(ig.FailureSkip).Should(Equal([]int{3}))
112 Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).Should"))
113 Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).match"))
114 })
115
116 It("renders the matcher's error if an error occured", func() {
117 ig.G.Eventually(ERR_MATCH).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(SpecMatch())
118 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
119 Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error"))
120 })
121
122 It("renders the optional description", func() {
123 ig.G.Eventually(NO_MATCH).WithTimeout(50*time.Millisecond).WithPolling(10*time.Millisecond).Should(SpecMatch(), "boop")
124 Ω(ig.FailureMessage).Should(ContainSubstring("boop"))
125 })
126
127 It("formats and renders the optional description when there are multiple arguments", func() {
128 ig.G.Eventually(NO_MATCH).WithTimeout(50*time.Millisecond).WithPolling(10*time.Millisecond).Should(SpecMatch(), "boop %d", 17)
129 Ω(ig.FailureMessage).Should(ContainSubstring("boop 17"))
130 })
131
132 It("calls the optional description if it is a function", func() {
133 ig.G.Eventually(NO_MATCH).WithTimeout(50*time.Millisecond).WithPolling(10*time.Millisecond).Should(SpecMatch(), func() string { return "boop" })
134 Ω(ig.FailureMessage).Should(ContainSubstring("boop"))
135 })
136 })
137 })
138
139 Describe("Basic Consistently support", func() {
140 Context("the positive case", func() {
141 It("polls the function and matcher ensuring a match occurs consistently", func() {
142 counter := 0
143 ig.G.Consistently(func() string {
144 counter++
145 return MATCH
146 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(SpecMatch())
147 Ω(counter).Should(BeNumerically(">", 1))
148 Ω(counter).Should(BeNumerically("<", 7))
149 Ω(ig.FailureMessage).Should(BeZero())
150 })
151
152 It("fails if the matcher ever errors", func() {
153 counter := 0
154 ig.G.Consistently(func() string {
155 counter++
156 if counter == 3 {
157 return ERR_MATCH
158 }
159 return MATCH
160 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(SpecMatch())
161 Ω(counter).Should(Equal(3))
162 Ω(ig.FailureMessage).Should(ContainSubstring("Failed after"))
163 Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error"))
164 })
165
166 It("fails if the matcher doesn't match at any point", func() {
167 counter := 0
168 ig.G.Consistently(func() string {
169 counter++
170 if counter == 3 {
171 return NO_MATCH
172 }
173 return MATCH
174 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(SpecMatch())
175 Ω(counter).Should(Equal(3))
176 Ω(ig.FailureMessage).Should(ContainSubstring("Failed after"))
177 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
178 })
179 })
180
181 Context("the negative case", func() {
182 It("polls the function and matcher ensuring a match never occurs", func() {
183 counter := 0
184 ig.G.Consistently(func() string {
185 counter++
186 return NO_MATCH
187 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(SpecMatch())
188 Ω(counter).Should(BeNumerically(">", 1))
189 Ω(counter).Should(BeNumerically("<", 7))
190 Ω(ig.FailureMessage).Should(BeZero())
191 })
192
193 It("fails if the matcher ever errors", func() {
194 counter := 0
195 ig.G.Consistently(func() string {
196 counter++
197 if counter == 3 {
198 return ERR_MATCH
199 }
200 return NO_MATCH
201 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(SpecMatch())
202 Ω(counter).Should(Equal(3))
203 Ω(ig.FailureMessage).Should(ContainSubstring("Failed after"))
204 Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error"))
205 })
206
207 It("fails if the matcher matches at any point", func() {
208 counter := 0
209 ig.G.Consistently(func() string {
210 counter++
211 if counter == 3 {
212 return MATCH
213 }
214 return NO_MATCH
215 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(SpecMatch())
216 Ω(counter).Should(Equal(3))
217 Ω(ig.FailureMessage).Should(ContainSubstring("Failed after"))
218 Ω(ig.FailureMessage).Should(ContainSubstring("negative: match"))
219 })
220 })
221
222 Context("when a failure occurs", func() {
223 It("registers the appropriate helper functions", func() {
224 ig.G.Consistently(NO_MATCH).Should(SpecMatch())
225 Ω(ig.FailureMessage).Should(ContainSubstring("Failed after"))
226 Ω(ig.FailureMessage).Should(ContainSubstring("positive: no match"))
227 Ω(ig.FailureSkip).Should(Equal([]int{3}))
228 Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).Should"))
229 Ω(ig.RegisteredHelpers).Should(ContainElement("(*AsyncAssertion).match"))
230 })
231
232 It("renders the matcher's error if an error occured", func() {
233 ig.G.Consistently(ERR_MATCH).Should(SpecMatch())
234 Ω(ig.FailureMessage).Should(ContainSubstring("Failed after"))
235 Ω(ig.FailureMessage).Should(ContainSubstring("Error: spec matcher error"))
236 })
237
238 It("renders the optional description", func() {
239 ig.G.Consistently(NO_MATCH).Should(SpecMatch(), "boop")
240 Ω(ig.FailureMessage).Should(ContainSubstring("boop"))
241 })
242
243 It("formats and renders the optional description when there are multiple arguments", func() {
244 ig.G.Consistently(NO_MATCH).Should(SpecMatch(), "boop %d", 17)
245 Ω(ig.FailureMessage).Should(ContainSubstring("boop 17"))
246 })
247
248 It("calls the optional description if it is a function", func() {
249 ig.G.Consistently(NO_MATCH).Should(SpecMatch(), func() string { return "boop" })
250 Ω(ig.FailureMessage).Should(ContainSubstring("boop"))
251 })
252 })
253 })
254
255 Describe("the passed-in actual", func() {
256 type Foo struct{ Bar string }
257
258 Context("when passed a value", func() {
259 It("(eventually) continuously checks on the value until a match occurs", func() {
260 c := make(chan bool)
261 go func() {
262 time.Sleep(100 * time.Millisecond)
263 close(c)
264 }()
265 ig.G.Eventually(c).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).Should(BeClosed())
266 Ω(ig.FailureMessage).Should(BeZero())
267 })
268
269 It("(consistently) continuously checks on the value ensuring a match always occurs", func() {
270 c := make(chan bool)
271 close(c)
272 ig.G.Consistently(c).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeClosed())
273 Ω(ig.FailureMessage).Should(BeZero())
274 })
275 })
276
277 Context("when passed a function that takes no arguments and returns one value", func() {
278 It("(eventually) polls the function until the returned value satisfies the matcher", func() {
279 counter := 0
280 ig.G.Eventually(func() int {
281 counter += 1
282 return counter
283 }).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).Should(BeNumerically(">", 5))
284 Ω(ig.FailureMessage).Should(BeZero())
285 })
286
287 It("(consistently) polls the function ensuring the returned value satisfies the matcher", func() {
288 counter := 0
289 ig.G.Consistently(func() int {
290 counter += 1
291 return counter
292 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 20))
293 Ω(counter).Should(BeNumerically(">", 2))
294 Ω(ig.FailureMessage).Should(BeZero())
295 })
296
297 It("works when the function returns nil", func() {
298 counter := 0
299 ig.G.Eventually(func() error {
300 counter += 1
301 if counter > 5 {
302 return nil
303 }
304 return errors.New("oops")
305 }).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).Should(BeNil())
306 Ω(ig.FailureMessage).Should(BeZero())
307 })
308 })
309
310 Context("when passed a function that takes no arguments and returns mutliple values", func() {
311 Context("with Eventually", func() {
312 It("polls the function until the first returned value satisfies the matcher _and_ all additional values are zero", func() {
313 counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi")
314 ig.G.Eventually(func() (int, string, Foo, error) {
315 switch counter += 1; counter {
316 case 2:
317 s = ""
318 case 3:
319 f = Foo{}
320 case 4:
321 err = nil
322 }
323 return counter, s, f, err
324 }).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100))
325 Ω(ig.FailureMessage).Should(BeZero())
326 Ω(counter).Should(Equal(4))
327 })
328
329 It("reports on the non-zero value if it times out", func() {
330 ig.G.Eventually(func() (int, string, Foo, error) {
331 return 1, "", Foo{Bar: "hi"}, nil
332 }).WithTimeout(30 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100))
333 Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero argument at index 2:"))
334 Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"hi"}`))
335 })
336
337 Context("when making a ShouldNot assertion", func() {
338 It("doesn't succeed until the matcher is (not) satisfied with the first returned value _and_ all additional values are zero", func() {
339 counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi")
340 ig.G.Eventually(func() (int, string, Foo, error) {
341 switch counter += 1; counter {
342 case 2:
343 s = ""
344 case 3:
345 f = Foo{}
346 case 4:
347 err = nil
348 }
349 return counter, s, f, err
350 }).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).ShouldNot(BeNumerically("<", 0))
351 Ω(ig.FailureMessage).Should(BeZero())
352 Ω(counter).Should(Equal(4))
353 })
354 })
355 })
356
357 Context("with Consistently", func() {
358 It("polls the function and succeeds if all the values are zero and the matcher is consistently satisfied", func() {
359 var err error
360 counter, s, f := 0, "", Foo{}
361 ig.G.Consistently(func() (int, string, Foo, error) {
362 counter += 1
363 return counter, s, f, err
364 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100))
365 Ω(ig.FailureMessage).Should(BeZero())
366 Ω(counter).Should(BeNumerically(">", 2))
367 })
368
369 It("polls the function and fails any of the values are non-zero", func() {
370 var err error
371 counter, s, f := 0, "", Foo{}
372 ig.G.Consistently(func() (int, string, Foo, error) {
373 counter += 1
374 if counter == 3 {
375 f = Foo{Bar: "welp"}
376 }
377 return counter, s, f, err
378 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100))
379 Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero argument at index 2:"))
380 Ω(ig.FailureMessage).Should(ContainSubstring(`Foo{Bar:"welp"}`))
381 Ω(counter).Should(Equal(3))
382 })
383
384 Context("when making a ShouldNot assertion", func() {
385 It("succeeds if all additional values are zero", func() {
386 var err error
387 counter, s, f := 0, "", Foo{}
388 ig.G.Consistently(func() (int, string, Foo, error) {
389 counter += 1
390 return counter, s, f, err
391 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(BeNumerically(">", 100))
392 Ω(ig.FailureMessage).Should(BeZero())
393 Ω(counter).Should(BeNumerically(">", 2))
394 })
395
396 It("fails if any additional values are ever non-zero", func() {
397 var err error
398 counter, s, f := 0, "", Foo{}
399 ig.G.Consistently(func() (int, string, Foo, error) {
400 counter += 1
401 if counter == 3 {
402 s = "welp"
403 }
404 return counter, s, f, err
405 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(BeNumerically(">", 100))
406 Ω(ig.FailureMessage).Should(ContainSubstring("Error: Unexpected non-nil/non-zero argument at index 1:"))
407 Ω(ig.FailureMessage).Should(ContainSubstring(`<string>: "welp"`))
408 Ω(counter).Should(Equal(3))
409 })
410 })
411 })
412 })
413
414 Context("when passed a function that takes a Gomega argument and returns values", func() {
415 Context("with Eventually", func() {
416 It("passes in a Gomega and passes if the matcher matches, all extra values are zero, and there are no failed assertions", func() {
417 counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi")
418 ig.G.Eventually(func(g Gomega) (int, string, Foo, error) {
419 switch counter += 1; counter {
420 case 2:
421 s = ""
422 case 3:
423 f = Foo{}
424 case 4:
425 err = nil
426 }
427 if counter == 5 {
428 g.Expect(true).To(BeTrue())
429 } else {
430 g.Expect(false).To(BeTrue())
431 panic("boom") //never see since the expectation stops execution
432 }
433 return counter, s, f, err
434 }).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100))
435 Ω(ig.FailureMessage).Should(BeZero())
436 Ω(counter).Should(Equal(5))
437 })
438
439 It("times out if assertions in the function never succeed and reports on the error", func() {
440 _, file, line, _ := runtime.Caller(0)
441 ig.G.Eventually(func(g Gomega) int {
442 g.Expect(false).To(BeTrue())
443 return 10
444 }).WithTimeout(30 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Equal(10))
445 Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+2))
446 Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n <bool>: false\nto be true"))
447 })
448
449 It("forwards panics", func() {
450 Ω(func() {
451 ig.G.Eventually(func(g Gomega) int {
452 g.Expect(true).To(BeTrue())
453 panic("boom")
454 }).WithTimeout(30 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Equal(10))
455 }).Should(PanicWith("boom"))
456 Ω(ig.FailureMessage).Should(BeEmpty())
457 })
458
459 Context("when making a ShouldNot assertion", func() {
460 It("doesn't succeed until all extra values are zero, there are no failed assertions, and the matcher is (not) satisfied", func() {
461 counter, s, f, err := 0, "hi", Foo{Bar: "hi"}, errors.New("hi")
462 ig.G.Eventually(func(g Gomega) (int, string, Foo, error) {
463 switch counter += 1; counter {
464 case 2:
465 s = ""
466 case 3:
467 f = Foo{}
468 case 4:
469 err = nil
470 }
471 if counter == 5 {
472 g.Expect(true).To(BeTrue())
473 } else {
474 g.Expect(false).To(BeTrue())
475 panic("boom") //never see since the expectation stops execution
476 }
477 return counter, s, f, err
478 }).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).ShouldNot(BeNumerically("<", 0))
479 Ω(ig.FailureMessage).Should(BeZero())
480 Ω(counter).Should(Equal(5))
481 })
482 })
483
484 It("fails if an assertion is never satisfied", func() {
485 _, file, line, _ := runtime.Caller(0)
486 ig.G.Eventually(func(g Gomega) int {
487 g.Expect(false).To(BeTrue())
488 return 9
489 }).WithTimeout(30 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Equal(10))
490 Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+2))
491 Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n <bool>: false\nto be true"))
492 })
493 })
494
495 Context("with Consistently", func() {
496 It("passes in a Gomega and passes if the matcher matches, all extra values are zero, and there are no failed assertions", func() {
497 var err error
498 counter, s, f := 0, "", Foo{}
499 ig.G.Consistently(func(g Gomega) (int, string, Foo, error) {
500 counter += 1
501 g.Expect(true).To(BeTrue())
502 return counter, s, f, err
503 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100))
504 Ω(ig.FailureMessage).Should(BeZero())
505 Ω(counter).Should(BeNumerically(">", 2))
506 })
507
508 It("fails if the passed-in gomega ever hits a failure", func() {
509 var err error
510 counter, s, f := 0, "", Foo{}
511 _, file, line, _ := runtime.Caller(0)
512 ig.G.Consistently(func(g Gomega) (int, string, Foo, error) {
513 counter += 1
514 g.Expect(true).To(BeTrue())
515 if counter == 3 {
516 g.Expect(false).To(BeTrue())
517 panic("boom") //never see this
518 }
519 return counter, s, f, err
520 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(BeNumerically("<", 100))
521 Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+5))
522 Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n <bool>: false\nto be true"))
523 Ω(counter).Should(Equal(3))
524 })
525
526 It("forwards panics", func() {
527 Ω(func() {
528 ig.G.Consistently(func(g Gomega) int {
529 g.Expect(true).To(BeTrue())
530 panic("boom")
531 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Equal(10))
532 }).Should(PanicWith("boom"))
533 Ω(ig.FailureMessage).Should(BeEmpty())
534 })
535
536 Context("when making a ShouldNot assertion", func() {
537 It("succeeds if any interior assertions always pass", func() {
538 ig.G.Consistently(func(g Gomega) int {
539 g.Expect(true).To(BeTrue())
540 return 9
541 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Equal(10))
542 Ω(ig.FailureMessage).Should(BeEmpty())
543 })
544
545 It("fails if any interior assertions ever fail", func() {
546 counter := 0
547 _, file, line, _ := runtime.Caller(0)
548 ig.G.Consistently(func(g Gomega) int {
549 g.Expect(true).To(BeTrue())
550 counter += 1
551 if counter == 3 {
552 g.Expect(false).To(BeTrue())
553 panic("boom") //never see this
554 }
555 return 9
556 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Equal(10))
557 Ω(ig.FailureMessage).Should(ContainSubstring("Error: Assertion in callback at %s:%d failed:", file, line+5))
558 Ω(ig.FailureMessage).Should(ContainSubstring("Expected\n <bool>: false\nto be true"))
559 })
560 })
561 })
562 })
563
564 Context("when passed a function that takes a Gomega argument and returns nothing", func() {
565 Context("with Eventually", func() {
566 It("returns the first failed assertion as an error and so should Succeed() if the callback ever runs without issue", func() {
567 counter := 0
568 ig.G.Eventually(func(g Gomega) {
569 counter += 1
570 if counter < 5 {
571 g.Expect(false).To(BeTrue())
572 g.Expect("bloop").To(Equal("blarp"))
573 }
574 }).WithTimeout(1 * time.Second).WithPolling(10 * time.Millisecond).Should(Succeed())
575 Ω(counter).Should(Equal(5))
576 Ω(ig.FailureMessage).Should(BeZero())
577 })
578
579 It("returns the first failed assertion as an error and so should timeout if the callback always fails", func() {
580 counter := 0
581 ig.G.Eventually(func(g Gomega) {
582 counter += 1
583 if counter < 5000 {
584 g.Expect(false).To(BeTrue())
585 g.Expect("bloop").To(Equal("blarp"))
586 }
587 }).WithTimeout(100 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Succeed())
588 Ω(counter).Should(BeNumerically(">", 1))
589 Ω(ig.FailureMessage).Should(ContainSubstring("Expected success, but got an error"))
590 Ω(ig.FailureMessage).Should(ContainSubstring("<bool>: false"))
591 Ω(ig.FailureMessage).Should(ContainSubstring("to be true"))
592 Ω(ig.FailureMessage).ShouldNot(ContainSubstring("bloop"))
593 })
594
595 It("returns the first failed assertion as an error and should satisy ShouldNot(Succeed) eventually", func() {
596 counter := 0
597 ig.G.Eventually(func(g Gomega) {
598 counter += 1
599 if counter > 5 {
600 g.Expect(false).To(BeTrue())
601 g.Expect("bloop").To(Equal("blarp"))
602 }
603 }).WithTimeout(100 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Succeed())
604 Ω(counter).Should(Equal(6))
605 Ω(ig.FailureMessage).Should(BeZero())
606 })
607
608 It("should fail to ShouldNot(Succeed) eventually if an error never occurs", func() {
609 ig.G.Eventually(func(g Gomega) {
610 g.Expect(true).To(BeTrue())
611 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Succeed())
612 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
613 Ω(ig.FailureMessage).Should(ContainSubstring("Expected failure, but got no error."))
614 })
615 })
616
617 Context("with Consistently", func() {
618 It("returns the first failed assertion as an error and so should Succeed() if the callback always runs without issue", func() {
619 counter := 0
620 ig.G.Consistently(func(g Gomega) {
621 counter += 1
622 g.Expect(true).To(BeTrue())
623 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Succeed())
624 Ω(counter).Should(BeNumerically(">", 2))
625 Ω(ig.FailureMessage).Should(BeZero())
626 })
627
628 It("returns the first failed assertion as an error and so should fail if the callback ever fails", func() {
629 counter := 0
630 ig.G.Consistently(func(g Gomega) {
631 counter += 1
632 g.Expect(true).To(BeTrue())
633 if counter == 3 {
634 g.Expect(false).To(BeTrue())
635 g.Expect("bloop").To(Equal("blarp"))
636 }
637 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).Should(Succeed())
638 Ω(ig.FailureMessage).Should(ContainSubstring("Expected success, but got an error"))
639 Ω(ig.FailureMessage).Should(ContainSubstring("<bool>: false"))
640 Ω(ig.FailureMessage).Should(ContainSubstring("to be true"))
641 Ω(ig.FailureMessage).ShouldNot(ContainSubstring("bloop"))
642 Ω(counter).Should(Equal(3))
643 })
644
645 It("returns the first failed assertion as an error and should satisy ShouldNot(Succeed) consistently if an error always occur", func() {
646 counter := 0
647 ig.G.Consistently(func(g Gomega) {
648 counter += 1
649 g.Expect(true).To(BeFalse())
650 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Succeed())
651 Ω(counter).Should(BeNumerically(">", 2))
652 Ω(ig.FailureMessage).Should(BeZero())
653 })
654
655 It("should fail to satisfy ShouldNot(Succeed) consistently if an error ever does not occur", func() {
656 counter := 0
657 ig.G.Consistently(func(g Gomega) {
658 counter += 1
659 if counter == 3 {
660 g.Expect(true).To(BeTrue())
661 } else {
662 g.Expect(false).To(BeTrue())
663 }
664 }).WithTimeout(50 * time.Millisecond).WithPolling(10 * time.Millisecond).ShouldNot(Succeed())
665 Ω(ig.FailureMessage).Should(ContainSubstring("Failed after"))
666 Ω(ig.FailureMessage).Should(ContainSubstring("Expected failure, but got no error."))
667 Ω(counter).Should(Equal(3))
668 })
669 })
670 })
671
672 Describe("when passed an invalid function", func() {
673 It("errors immediately", func() {
674 ig.G.Eventually(func() {})
675 Ω(ig.FailureMessage).Should(Equal("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes 0 arguments and returns 0 values."))
676 Ω(ig.FailureSkip).Should(Equal([]int{4}))
677
678 ig = NewInstrumentedGomega()
679 ig.G.Eventually(func(g Gomega, foo string) {})
680 Ω(ig.FailureMessage).Should(Equal("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes 2 arguments and returns 0 values."))
681 Ω(ig.FailureSkip).Should(Equal([]int{4}))
682
683 ig = NewInstrumentedGomega()
684 ig.G.Eventually(func(foo string) {})
685 Ω(ig.FailureMessage).Should(Equal("The function passed to Gomega's async assertions should either take no arguments and return values, or take a single Gomega interface that it can use to make assertions within the body of the function. When taking a Gomega interface the function can optionally return values or return nothing. The function you passed takes 1 arguments and returns 0 values."))
686 Ω(ig.FailureSkip).Should(Equal([]int{4}))
687 })
688 })
689 })
690
691 Describe("when using OracleMatchers", func() {
692 It("stops and gives up with an appropriate failure message if the OracleMatcher says things can't change", func() {
693 c := make(chan bool)
694 close(c)
695
696 t := time.Now()
697 ig.G.Eventually(c).WithTimeout(100*time.Millisecond).WithPolling(10*time.Millisecond).Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed")
698 Ω(time.Since(t)).Should(BeNumerically("<", 90*time.Millisecond))
699 Ω(ig.FailureMessage).Should(ContainSubstring("No future change is possible."))
700 Ω(ig.FailureMessage).Should(ContainSubstring("The channel is closed."))
701 })
702
703 It("never gives up if actual is a function", func() {
704 c := make(chan bool)
705 close(c)
706
707 t := time.Now()
708 ig.G.Eventually(func() chan bool { return c }).WithTimeout(100*time.Millisecond).WithPolling(10*time.Millisecond).Should(Receive(), "Receive is an OracleMatcher that gives up if the channel is closed")
709 Ω(time.Since(t)).Should(BeNumerically(">=", 90*time.Millisecond))
710 Ω(ig.FailureMessage).ShouldNot(ContainSubstring("No future change is possible."))
711 Ω(ig.FailureMessage).Should(ContainSubstring("Timed out after"))
712 })
713 })
714 })
+0
-198
internal/asyncassertion/async_assertion.go less more
0 // untested sections: 2
1
2 package asyncassertion
3
4 import (
5 "errors"
6 "fmt"
7 "reflect"
8 "time"
9
10 "github.com/onsi/gomega/internal/oraclematcher"
11 "github.com/onsi/gomega/types"
12 )
13
14 type AsyncAssertionType uint
15
16 const (
17 AsyncAssertionTypeEventually AsyncAssertionType = iota
18 AsyncAssertionTypeConsistently
19 )
20
21 type AsyncAssertion struct {
22 asyncType AsyncAssertionType
23 actualInput interface{}
24 timeoutInterval time.Duration
25 pollingInterval time.Duration
26 failWrapper *types.GomegaFailWrapper
27 offset int
28 }
29
30 func New(asyncType AsyncAssertionType, actualInput interface{}, failWrapper *types.GomegaFailWrapper, timeoutInterval time.Duration, pollingInterval time.Duration, offset int) *AsyncAssertion {
31 actualType := reflect.TypeOf(actualInput)
32 if actualType.Kind() == reflect.Func {
33 if actualType.NumIn() != 0 || actualType.NumOut() == 0 {
34 panic("Expected a function with no arguments and one or more return values.")
35 }
36 }
37
38 return &AsyncAssertion{
39 asyncType: asyncType,
40 actualInput: actualInput,
41 failWrapper: failWrapper,
42 timeoutInterval: timeoutInterval,
43 pollingInterval: pollingInterval,
44 offset: offset,
45 }
46 }
47
48 func (assertion *AsyncAssertion) Should(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
49 assertion.failWrapper.TWithHelper.Helper()
50 return assertion.match(matcher, true, optionalDescription...)
51 }
52
53 func (assertion *AsyncAssertion) ShouldNot(matcher types.GomegaMatcher, optionalDescription ...interface{}) bool {
54 assertion.failWrapper.TWithHelper.Helper()
55 return assertion.match(matcher, false, optionalDescription...)
56 }
57
58 func (assertion *AsyncAssertion) buildDescription(optionalDescription ...interface{}) string {
59 switch len(optionalDescription) {
60 case 0:
61 return ""
62 case 1:
63 if describe, ok := optionalDescription[0].(func() string); ok {
64 return describe() + "\n"
65 }
66 }
67 return fmt.Sprintf(optionalDescription[0].(string), optionalDescription[1:]...) + "\n"
68 }
69
70 func (assertion *AsyncAssertion) actualInputIsAFunction() bool {
71 actualType := reflect.TypeOf(assertion.actualInput)
72 return actualType.Kind() == reflect.Func && actualType.NumIn() == 0 && actualType.NumOut() > 0
73 }
74
75 func (assertion *AsyncAssertion) pollActual() (interface{}, error) {
76 if assertion.actualInputIsAFunction() {
77 values := reflect.ValueOf(assertion.actualInput).Call([]reflect.Value{})
78
79 extras := []interface{}{}
80 for _, value := range values[1:] {
81 extras = append(extras, value.Interface())
82 }
83
84 success, message := vetExtras(extras)
85
86 if !success {
87 return nil, errors.New(message)
88 }
89
90 return values[0].Interface(), nil
91 }
92
93 return assertion.actualInput, nil
94 }
95
96 func (assertion *AsyncAssertion) matcherMayChange(matcher types.GomegaMatcher, value interface{}) bool {
97 if assertion.actualInputIsAFunction() {
98 return true
99 }
100
101 return oraclematcher.MatchMayChangeInTheFuture(matcher, value)
102 }
103
104 func (assertion *AsyncAssertion) match(matcher types.GomegaMatcher, desiredMatch bool, optionalDescription ...interface{}) bool {
105 timer := time.Now()
106 timeout := time.After(assertion.timeoutInterval)
107
108 var matches bool
109 var err error
110 mayChange := true
111 value, err := assertion.pollActual()
112 if err == nil {
113 mayChange = assertion.matcherMayChange(matcher, value)
114 matches, err = matcher.Match(value)
115 }
116
117 assertion.failWrapper.TWithHelper.Helper()
118
119 fail := func(preamble string) {
120 errMsg := ""
121 message := ""
122 if err != nil {
123 errMsg = "Error: " + err.Error()
124 } else {
125 if desiredMatch {
126 message = matcher.FailureMessage(value)
127 } else {
128 message = matcher.NegatedFailureMessage(value)
129 }
130 }
131 assertion.failWrapper.TWithHelper.Helper()
132 description := assertion.buildDescription(optionalDescription...)
133 assertion.failWrapper.Fail(fmt.Sprintf("%s after %.3fs.\n%s%s%s", preamble, time.Since(timer).Seconds(), description, message, errMsg), 3+assertion.offset)
134 }
135
136 if assertion.asyncType == AsyncAssertionTypeEventually {
137 for {
138 if err == nil && matches == desiredMatch {
139 return true
140 }
141
142 if !mayChange {
143 fail("No future change is possible. Bailing out early")
144 return false
145 }
146
147 select {
148 case <-time.After(assertion.pollingInterval):
149 value, err = assertion.pollActual()
150 if err == nil {
151 mayChange = assertion.matcherMayChange(matcher, value)
152 matches, err = matcher.Match(value)
153 }
154 case <-timeout:
155 fail("Timed out")
156 return false
157 }
158 }
159 } else if assertion.asyncType == AsyncAssertionTypeConsistently {
160 for {
161 if !(err == nil && matches == desiredMatch) {
162 fail("Failed")
163 return false
164 }
165
166 if !mayChange {
167 return true
168 }
169
170 select {
171 case <-time.After(assertion.pollingInterval):
172 value, err = assertion.pollActual()
173 if err == nil {
174 mayChange = assertion.matcherMayChange(matcher, value)
175 matches, err = matcher.Match(value)
176 }
177 case <-timeout:
178 return true
179 }
180 }
181 }
182
183 return false
184 }
185
186 func vetExtras(extras []interface{}) (bool, string) {
187 for i, extra := range extras {
188 if extra != nil {
189 zeroValue := reflect.Zero(reflect.TypeOf(extra)).Interface()
190 if !reflect.DeepEqual(zeroValue, extra) {
191 message := fmt.Sprintf("Unexpected non-nil/non-zero extra argument at index %d:\n\t<%T>: %#v", i+1, extra, extra)
192 return false, message
193 }
194 }
195 }
196 return true, ""
197 }
+0
-13
internal/asyncassertion/async_assertion_suite_test.go less more
0 package asyncassertion_test
1
2 import (
3 . "github.com/onsi/ginkgo"
4 . "github.com/onsi/gomega"
5
6 "testing"
7 )
8
9 func TestAsyncAssertion(t *testing.T) {
10 RegisterFailHandler(Fail)
11 RunSpecs(t, "AsyncAssertion Suite")
12 }
+0
-389
internal/asyncassertion/async_assertion_test.go less more
0 package asyncassertion_test
1
2 import (
3 "errors"
4 "time"
5
6 "github.com/onsi/gomega/internal/testingtsupport"
7
8 . "github.com/onsi/ginkgo"
9 . "github.com/onsi/gomega"
10 "github.com/onsi/gomega/internal/asyncassertion"
11 "github.com/onsi/gomega/types"
12 )
13
14 var _ = Describe("Async Assertion", func() {
15 var (
16 failureMessage string
17 callerSkip int
18 )
19
20 var fakeFailWrapper = &types.GomegaFailWrapper{
21 Fail: func(message string, skip ...int) {
22 failureMessage = message
23 callerSkip = skip[0]
24 },
25 TWithHelper: testingtsupport.EmptyTWithHelper{},
26 }
27
28 BeforeEach(func() {
29 failureMessage = ""
30 callerSkip = 0
31 })
32
33 Describe("Eventually", func() {
34 Context("the positive case", func() {
35 It("should poll the function and matcher", func() {
36 counter := 0
37 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int {
38 counter++
39 return counter
40 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
41
42 a.Should(BeNumerically("==", 5))
43 Expect(failureMessage).Should(BeZero())
44 })
45
46 It("should continue when the matcher errors", func() {
47 counter := 0
48 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() interface{} {
49 counter++
50 if counter == 5 {
51 return "not-a-number" //this should cause the matcher to error
52 }
53 return counter
54 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
55
56 a.Should(BeNumerically("==", 5), "My description %d", 2)
57
58 Expect(failureMessage).Should(ContainSubstring("Timed out after"))
59 Expect(failureMessage).Should(ContainSubstring("My description 2"))
60 Expect(callerSkip).Should(Equal(4))
61 })
62
63 It("should be able to timeout", func() {
64 counter := 0
65 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int {
66 counter++
67 return counter
68 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
69
70 a.Should(BeNumerically(">", 100), "My description %d", 2)
71
72 Expect(counter).Should(BeNumerically(">", 8))
73 Expect(counter).Should(BeNumerically("<=", 10))
74 Expect(failureMessage).Should(ContainSubstring("Timed out after"))
75 Expect(failureMessage).Should(MatchRegexp(`\<int\>: \d`), "Should pass the correct value to the matcher message formatter.")
76 Expect(failureMessage).Should(ContainSubstring("My description 2"))
77 Expect(callerSkip).Should(Equal(4))
78 })
79
80 When("the optional description is a function", func() {
81 It("should append the description to the failure message", func() {
82 counter := 0
83 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() interface{} {
84 counter++
85 if counter == 5 {
86 return "not-a-number" //this should cause the matcher to error
87 }
88 return counter
89 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
90
91 a.Should(BeNumerically("==", 5), func() string { return "My description" })
92
93 Expect(failureMessage).Should(ContainSubstring("Timed out after"))
94 Expect(failureMessage).Should(ContainSubstring("My description"))
95 Expect(callerSkip).Should(Equal(4))
96 })
97
98 Context("and there is no failure", func() {
99 It("should not evaluate that function", func() {
100 counter := 0
101 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int {
102 counter++
103 return counter
104 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
105
106 evaluated := false
107 a.Should(BeNumerically("==", 5), func() string {
108 evaluated = true
109 return "A description"
110 })
111
112 Expect(failureMessage).Should(BeZero())
113 Expect(evaluated).Should(BeFalse())
114 })
115 })
116 })
117 })
118
119 Context("the negative case", func() {
120 It("should poll the function and matcher", func() {
121 counter := 0
122 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int {
123 counter += 1
124 return counter
125 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
126
127 a.ShouldNot(BeNumerically("<", 3))
128
129 Expect(counter).Should(Equal(3))
130 Expect(failureMessage).Should(BeZero())
131 })
132
133 It("should timeout when the matcher errors", func() {
134 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() interface{} {
135 return 0 //this should cause the matcher to error
136 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
137
138 a.ShouldNot(HaveLen(0), "My description %d", 2)
139
140 Expect(failureMessage).Should(ContainSubstring("Timed out after"))
141 Expect(failureMessage).Should(ContainSubstring("Error:"))
142 Expect(failureMessage).Should(ContainSubstring("My description 2"))
143 Expect(callerSkip).Should(Equal(4))
144 })
145
146 It("should be able to timeout", func() {
147 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int {
148 return 0
149 }, fakeFailWrapper, time.Duration(0.1*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
150
151 a.ShouldNot(Equal(0), "My description %d", 2)
152
153 Expect(failureMessage).Should(ContainSubstring("Timed out after"))
154 Expect(failureMessage).Should(ContainSubstring("<int>: 0"), "Should pass the correct value to the matcher message formatter.")
155 Expect(failureMessage).Should(ContainSubstring("My description 2"))
156 Expect(callerSkip).Should(Equal(4))
157 })
158 })
159
160 Context("with a function that returns multiple values", func() {
161 It("should eventually succeed if the additional arguments are nil", func() {
162 i := 0
163 Eventually(func() (int, error) {
164 i++
165 return i, nil
166 }).Should(Equal(10))
167 })
168
169 It("should eventually timeout if the additional arguments are not nil", func() {
170 i := 0
171 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() (int, error) {
172 i++
173 return i, errors.New("bam")
174 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
175 a.Should(Equal(2))
176
177 Expect(failureMessage).Should(ContainSubstring("Timed out after"))
178 Expect(failureMessage).Should(ContainSubstring("Error:"))
179 Expect(failureMessage).Should(ContainSubstring("bam"))
180 Expect(callerSkip).Should(Equal(4))
181 })
182 })
183
184 Context("Making an assertion without a registered fail handler", func() {
185 It("should panic", func() {
186 defer func() {
187 e := recover()
188 RegisterFailHandler(Fail)
189 if e == nil {
190 Fail("expected a panic to have occurred")
191 }
192 }()
193
194 RegisterFailHandler(nil)
195 c := make(chan bool, 1)
196 c <- true
197 Eventually(c).Should(Receive())
198 })
199 })
200 })
201
202 Describe("Consistently", func() {
203 Describe("The positive case", func() {
204 When("the matcher consistently passes for the duration", func() {
205 It("should pass", func() {
206 calls := 0
207 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() string {
208 calls++
209 return "foo"
210 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
211
212 a.Should(Equal("foo"))
213 Expect(calls).Should(BeNumerically(">", 8))
214 Expect(calls).Should(BeNumerically("<=", 10))
215 Expect(failureMessage).Should(BeZero())
216 })
217 })
218
219 When("the matcher fails at some point", func() {
220 It("should fail", func() {
221 calls := 0
222 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() interface{} {
223 calls++
224 if calls > 5 {
225 return "bar"
226 }
227 return "foo"
228 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
229
230 a.Should(Equal("foo"))
231 Expect(failureMessage).Should(ContainSubstring("to equal"))
232 Expect(callerSkip).Should(Equal(4))
233 })
234 })
235
236 When("the matcher errors at some point", func() {
237 It("should fail", func() {
238 calls := 0
239 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() interface{} {
240 calls++
241 if calls > 5 {
242 return 3
243 }
244 return []int{1, 2, 3}
245 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
246
247 a.Should(HaveLen(3))
248 Expect(failureMessage).Should(ContainSubstring("HaveLen matcher expects"))
249 Expect(callerSkip).Should(Equal(4))
250 })
251 })
252 })
253
254 Describe("The negative case", func() {
255 When("the matcher consistently passes for the duration", func() {
256 It("should pass", func() {
257 c := make(chan bool)
258 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, c, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
259
260 a.ShouldNot(Receive())
261 Expect(failureMessage).Should(BeZero())
262 })
263 })
264
265 When("the matcher fails at some point", func() {
266 It("should fail", func() {
267 c := make(chan bool)
268 go func() {
269 time.Sleep(time.Duration(100 * time.Millisecond))
270 c <- true
271 }()
272
273 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, c, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
274
275 a.ShouldNot(Receive())
276 Expect(failureMessage).Should(ContainSubstring("not to receive anything"))
277 })
278 })
279
280 When("the matcher errors at some point", func() {
281 It("should fail", func() {
282 calls := 0
283 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeConsistently, func() interface{} {
284 calls++
285 return calls
286 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
287
288 a.ShouldNot(BeNumerically(">", 5))
289 Expect(failureMessage).Should(ContainSubstring("not to be >"))
290 Expect(callerSkip).Should(Equal(4))
291 })
292 })
293 })
294
295 Context("with a function that returns multiple values", func() {
296 It("should consistently succeed if the additional arguments are nil", func() {
297 i := 2
298 Consistently(func() (int, error) {
299 i++
300 return i, nil
301 }).Should(BeNumerically(">=", 2))
302 })
303
304 It("should eventually timeout if the additional arguments are not nil", func() {
305 i := 2
306 a := asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() (int, error) {
307 i++
308 return i, errors.New("bam")
309 }, fakeFailWrapper, time.Duration(0.2*float64(time.Second)), time.Duration(0.02*float64(time.Second)), 1)
310 a.Should(BeNumerically(">=", 2))
311
312 Expect(failureMessage).Should(ContainSubstring("Error:"))
313 Expect(failureMessage).Should(ContainSubstring("bam"))
314 Expect(callerSkip).Should(Equal(4))
315 })
316 })
317
318 Context("Making an assertion without a registered fail handler", func() {
319 It("should panic", func() {
320 defer func() {
321 e := recover()
322 RegisterFailHandler(Fail)
323 if e == nil {
324 Fail("expected a panic to have occurred")
325 }
326 }()
327
328 RegisterFailHandler(nil)
329 c := make(chan bool)
330 Consistently(c).ShouldNot(Receive())
331 })
332 })
333 })
334
335 When("passed a function with the wrong # or arguments & returns", func() {
336 It("should panic", func() {
337 Expect(func() {
338 asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() {}, fakeFailWrapper, 0, 0, 1)
339 }).Should(Panic())
340
341 Expect(func() {
342 asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func(a string) int { return 0 }, fakeFailWrapper, 0, 0, 1)
343 }).Should(Panic())
344
345 Expect(func() {
346 asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() int { return 0 }, fakeFailWrapper, 0, 0, 1)
347 }).ShouldNot(Panic())
348
349 Expect(func() {
350 asyncassertion.New(asyncassertion.AsyncAssertionTypeEventually, func() (int, error) { return 0, nil }, fakeFailWrapper, 0, 0, 1)
351 }).ShouldNot(Panic())
352 })
353 })
354
355 Describe("bailing early", func() {
356 When("actual is a value", func() {
357 It("Eventually should bail out and fail early if the matcher says to", func() {
358 c := make(chan bool)
359 close(c)
360
361 t := time.Now()
362 failures := InterceptGomegaFailures(func() {
363 Eventually(c, 0.1).Should(Receive())
364 })
365 Expect(time.Since(t)).Should(BeNumerically("<", 90*time.Millisecond))
366
367 Expect(failures).Should(HaveLen(1))
368 })
369 })
370
371 When("actual is a function", func() {
372 It("should never bail early", func() {
373 c := make(chan bool)
374 close(c)
375
376 t := time.Now()
377 failures := InterceptGomegaFailures(func() {
378 Eventually(func() chan bool {
379 return c
380 }, 0.1).Should(Receive())
381 })
382 Expect(time.Since(t)).Should(BeNumerically(">=", 90*time.Millisecond))
383
384 Expect(failures).Should(HaveLen(1))
385 })
386 })
387 })
388 })
0 package internal_test
1
2 import (
3 "errors"
4 "runtime"
5 "time"
6
7 . "github.com/onsi/ginkgo"
8 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/internal"
10 )
11
12 func getGlobalDurationBundle() internal.DurationBundle {
13 return Default.(*internal.Gomega).DurationBundle
14 }
15
16 func setGlobalDurationBundle(bundle internal.DurationBundle) {
17 SetDefaultEventuallyTimeout(bundle.EventuallyTimeout)
18 SetDefaultEventuallyPollingInterval(bundle.EventuallyPollingInterval)
19 SetDefaultConsistentlyDuration(bundle.ConsistentlyDuration)
20 SetDefaultConsistentlyPollingInterval(bundle.ConsistentlyPollingInterval)
21 }
22
23 var _ = Describe("Gomega DSL", func() {
24 var globalDurationBundle internal.DurationBundle
25
26 BeforeEach(func() {
27 globalDurationBundle = getGlobalDurationBundle()
28 })
29
30 AfterEach(func() {
31 RegisterFailHandler(Fail)
32 setGlobalDurationBundle(globalDurationBundle)
33 })
34
35 Describe("The Default, global, Gomega", func() {
36 It("exists", func() {
37 Ω(Default).ShouldNot(BeNil())
38 })
39
40 It("is wired up via the global DSL", func() {
41 counter := 0
42 Eventually(func() int {
43 counter += 1
44 return counter
45 }).Should(Equal(5))
46 Ω(counter).Should(Equal(5))
47 })
48 })
49
50 Describe("NewGomega", func() {
51 It("creates and configures a new Gomega, using the global duration bundle", func() {
52 bundle := internal.DurationBundle{
53 EventuallyTimeout: time.Minute,
54 EventuallyPollingInterval: 2 * time.Minute,
55 ConsistentlyDuration: 3 * time.Minute,
56 ConsistentlyPollingInterval: 4 * time.Minute,
57 }
58 setGlobalDurationBundle(bundle)
59
60 var calledWith string
61 g := NewGomega(func(message string, skip ...int) {
62 calledWith = message
63 })
64
65 gAsStruct := g.(*internal.Gomega)
66 Ω(gAsStruct.DurationBundle).Should(Equal(bundle))
67
68 g.Ω(true).Should(BeFalse())
69 Ω(calledWith).Should(Equal("Expected\n <bool>: true\nto be false"))
70 })
71 })
72
73 Describe("NewWithT", func() {
74 It("creates and configure a new Gomega with the passed-in T, using the global duration bundle", func() {
75 bundle := internal.DurationBundle{
76 EventuallyTimeout: time.Minute,
77 EventuallyPollingInterval: 2 * time.Minute,
78 ConsistentlyDuration: 3 * time.Minute,
79 ConsistentlyPollingInterval: 4 * time.Minute,
80 }
81 setGlobalDurationBundle(bundle)
82
83 fakeT := &FakeGomegaTestingT{}
84 g := NewWithT(fakeT)
85
86 Ω(g.DurationBundle).Should(Equal(bundle))
87
88 g.Ω(true).Should(BeFalse())
89 Ω(fakeT.CalledFatalf).Should(Equal("\nExpected\n <bool>: true\nto be false"))
90 Ω(fakeT.CalledHelper).Should(BeTrue())
91 })
92 })
93
94 Describe("RegisterFailHandler", func() {
95 It("overrides the global fail handler", func() {
96 var calledWith string
97 RegisterFailHandler(func(message string, skip ...int) {
98 calledWith = message
99 })
100
101 Ω(true).Should(BeFalse())
102
103 RegisterFailHandler(Fail)
104 Ω(calledWith).Should(Equal("Expected\n <bool>: true\nto be false"))
105 })
106 })
107
108 Describe("RegisterTestingT", func() {
109 It("overrides the global fail handler", func() {
110 fakeT := &FakeGomegaTestingT{}
111 RegisterTestingT(fakeT)
112
113 Ω(true).Should(BeFalse())
114 RegisterFailHandler(Fail)
115 Ω(fakeT.CalledFatalf).Should(Equal("\nExpected\n <bool>: true\nto be false"))
116 Ω(fakeT.CalledHelper).Should(BeTrue())
117 })
118 })
119
120 Describe("InterceptGomegaFailures", func() {
121 Context("when no failures occur", func() {
122 It("returns an empty array", func() {
123 Expect(InterceptGomegaFailures(func() {
124 Expect("hi").To(Equal("hi"))
125 })).To(BeEmpty())
126 })
127 })
128
129 Context("when failures occur", func() {
130 It("does not stop execution and returns all the failures as strings", func() {
131 Expect(InterceptGomegaFailures(func() {
132 Expect("hi").To(Equal("bye"))
133 Expect(3).To(Equal(2))
134 })).To(Equal([]string{
135 "Expected\n <string>: hi\nto equal\n <string>: bye",
136 "Expected\n <int>: 3\nto equal\n <int>: 2",
137 }))
138
139 })
140 })
141 })
142
143 Describe("InterceptGomegaFailure", func() {
144 Context("when no failures occur", func() {
145 It("returns nil", func() {
146 Expect(InterceptGomegaFailure(func() {
147 Expect("hi").To(Equal("hi"))
148 })).To(BeNil())
149 })
150 })
151
152 Context("when failures occur", func() {
153 It("returns the first failure and stops execution", func() {
154 gotThere := false
155 Expect(InterceptGomegaFailure(func() {
156 Expect("hi").To(Equal("bye"))
157 gotThere = true
158 Expect(3).To(Equal(2))
159 })).To(Equal(errors.New("Expected\n <string>: hi\nto equal\n <string>: bye")))
160 Expect(gotThere).To(BeFalse())
161 })
162 })
163
164 Context("when the function panics", func() {
165 It("panics", func() {
166 Expect(func() {
167 InterceptGomegaFailure(func() {
168 panic("boom")
169 })
170 }).To(PanicWith("boom"))
171 })
172 })
173 })
174
175 Context("Making an assertion without a registered fail handler", func() {
176 It("should panic", func() {
177 defer func() {
178 e := recover()
179 RegisterFailHandler(Fail)
180 if e == nil {
181 Fail("expected a panic to have occurred")
182 }
183 }()
184
185 RegisterFailHandler(nil)
186 Expect(true).Should(BeTrue())
187 })
188 })
189
190 Describe("specifying default durations globally", func() {
191 It("should update the durations on the Default gomega", func() {
192 bundle := internal.DurationBundle{
193 EventuallyTimeout: time.Minute,
194 EventuallyPollingInterval: 2 * time.Minute,
195 ConsistentlyDuration: 3 * time.Minute,
196 ConsistentlyPollingInterval: 4 * time.Minute,
197 }
198
199 SetDefaultEventuallyTimeout(bundle.EventuallyTimeout)
200 SetDefaultEventuallyPollingInterval(bundle.EventuallyPollingInterval)
201 SetDefaultConsistentlyDuration(bundle.ConsistentlyDuration)
202 SetDefaultConsistentlyPollingInterval(bundle.ConsistentlyPollingInterval)
203
204 Ω(Default.(*internal.Gomega).DurationBundle).Should(Equal(bundle))
205 })
206 })
207
208 Describe("Offsets", func() {
209 AfterEach(func() {
210 RegisterFailHandler(Fail)
211 })
212
213 It("computes the correct offsets", func() {
214 doubleNested := func(eventually bool) {
215 func() {
216 if eventually {
217 Eventually(true, "10ms", "5ms").WithOffset(2).Should(BeFalse())
218 } else {
219 Expect(true).WithOffset(2).To(BeFalse())
220 }
221 }()
222 }
223
224 reportedFile, reportedLine := "", 0
225 captureLocation := func(message string, skip ...int) {
226 _, reportedFile, reportedLine, _ = runtime.Caller(skip[0] + 1)
227 }
228
229 _, thisFile, anchorLine, _ := runtime.Caller(0) // 0
230 RegisterFailHandler(captureLocation) // 1
231 Expect(true).To(BeFalse()) // *2*
232 RegisterFailHandler(Fail) // 3
233 Ω(reportedFile).Should(Equal(thisFile)) // 4
234 Ω(reportedLine - anchorLine).Should(Equal(2)) // 5
235 RegisterFailHandler(captureLocation) // 6
236 doubleNested(false) // *7*
237 RegisterFailHandler(Fail) // 8
238 Ω(reportedFile).Should(Equal(thisFile)) // 9
239 Ω(reportedLine - anchorLine).Should(Equal(7)) // 10
240 RegisterFailHandler(captureLocation) // 11
241 Eventually(true, "10ms", "5ms").Should(BeFalse()) // *12*
242 RegisterFailHandler(Fail) // 13
243 Ω(reportedFile).Should(Equal(thisFile)) // 14
244 Ω(reportedLine - anchorLine).Should(Equal(12)) // 15
245 RegisterFailHandler(captureLocation) // 16
246 doubleNested(true) // *17*
247 RegisterFailHandler(Fail) // 18
248 Ω(reportedFile).Should(Equal(thisFile)) // 19
249 Ω(reportedLine - anchorLine).Should(Equal(17)) // 20
250 })
251 })
252 })
0 package internal
1
2 import (
3 "fmt"
4 "os"
5 "reflect"
6 "time"
7 )
8
9 type DurationBundle struct {
10 EventuallyTimeout time.Duration
11 EventuallyPollingInterval time.Duration
12 ConsistentlyDuration time.Duration
13 ConsistentlyPollingInterval time.Duration
14 }
15
16 const (
17 EventuallyTimeoutEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT"
18 EventuallyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_POLLING_INTERVAL"
19
20 ConsistentlyDurationEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_DURATION"
21 ConsistentlyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL"
22 )
23
24 func FetchDefaultDurationBundle() DurationBundle {
25 return DurationBundle{
26 EventuallyTimeout: durationFromEnv(EventuallyTimeoutEnvVarName, time.Second),
27 EventuallyPollingInterval: durationFromEnv(EventuallyPollingIntervalEnvVarName, 10*time.Millisecond),
28
29 ConsistentlyDuration: durationFromEnv(ConsistentlyDurationEnvVarName, 100*time.Millisecond),
30 ConsistentlyPollingInterval: durationFromEnv(ConsistentlyPollingIntervalEnvVarName, 10*time.Millisecond),
31 }
32 }
33
34 func durationFromEnv(key string, defaultDuration time.Duration) time.Duration {
35 value := os.Getenv(key)
36 if value == "" {
37 return defaultDuration
38 }
39 duration, err := time.ParseDuration(value)
40 if err != nil {
41 panic(fmt.Sprintf("Expected a duration when using %s! Parse error %v", key, err))
42 }
43 return duration
44 }
45
46 func toDuration(input interface{}) time.Duration {
47 duration, ok := input.(time.Duration)
48 if ok {
49 return duration
50 }
51
52 value := reflect.ValueOf(input)
53 kind := reflect.TypeOf(input).Kind()
54
55 if reflect.Int <= kind && kind <= reflect.Int64 {
56 return time.Duration(value.Int()) * time.Second
57 } else if reflect.Uint <= kind && kind <= reflect.Uint64 {
58 return time.Duration(value.Uint()) * time.Second
59 } else if reflect.Float32 <= kind && kind <= reflect.Float64 {
60 return time.Duration(value.Float() * float64(time.Second))
61 } else if reflect.String == kind {
62 duration, err := time.ParseDuration(value.String())
63 if err != nil {
64 panic(fmt.Sprintf("%#v is not a valid parsable duration string.", input))
65 }
66 return duration
67 }
68
69 panic(fmt.Sprintf("%v is not a valid interval. Must be time.Duration, parsable duration string or a number.", input))
70 }
0 package internal_test
1
2 import (
3 "os"
4 "time"
5
6 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/gomega"
8
9 "github.com/onsi/gomega/internal"
10 )
11
12 var _ = Describe("DurationBundle and Duration Support", func() {
13 Describe("fetching default durations from the environment", func() {
14 var envVars []string
15 var originalValues map[string]string
16
17 BeforeEach(func() {
18 envVars = []string{internal.EventuallyTimeoutEnvVarName, internal.EventuallyPollingIntervalEnvVarName, internal.ConsistentlyDurationEnvVarName, internal.ConsistentlyPollingIntervalEnvVarName}
19 originalValues = map[string]string{}
20
21 for _, envVar := range envVars {
22 originalValues[envVar] = os.Getenv(envVar)
23 }
24 })
25
26 AfterEach(func() {
27 for _, envVar := range envVars {
28 Ω(os.Setenv(envVar, originalValues[envVar])).Should(Succeed())
29 }
30 })
31
32 Context("with no environment set", func() {
33 BeforeEach(func() {
34 for _, envVar := range envVars {
35 os.Unsetenv(envVar)
36 }
37 })
38
39 It("returns the default bundle", func() {
40 bundle := internal.FetchDefaultDurationBundle()
41 Ω(bundle.EventuallyTimeout).Should(Equal(time.Second))
42 Ω(bundle.EventuallyPollingInterval).Should(Equal(10 * time.Millisecond))
43 Ω(bundle.ConsistentlyDuration).Should(Equal(100 * time.Millisecond))
44 Ω(bundle.ConsistentlyPollingInterval).Should(Equal(10 * time.Millisecond))
45 })
46 })
47
48 Context("with a valid environment set", func() {
49 BeforeEach(func() {
50 os.Setenv(internal.EventuallyTimeoutEnvVarName, "1m")
51 os.Setenv(internal.EventuallyPollingIntervalEnvVarName, "2s")
52 os.Setenv(internal.ConsistentlyDurationEnvVarName, "1h")
53 os.Setenv(internal.ConsistentlyPollingIntervalEnvVarName, "3ms")
54 })
55
56 It("returns an appropriate bundle", func() {
57 bundle := internal.FetchDefaultDurationBundle()
58 Ω(bundle.EventuallyTimeout).Should(Equal(time.Minute))
59 Ω(bundle.EventuallyPollingInterval).Should(Equal(2 * time.Second))
60 Ω(bundle.ConsistentlyDuration).Should(Equal(time.Hour))
61 Ω(bundle.ConsistentlyPollingInterval).Should(Equal(3 * time.Millisecond))
62 })
63 })
64
65 Context("with an invalid environment set", func() {
66 BeforeEach(func() {
67 os.Setenv(internal.EventuallyTimeoutEnvVarName, "chicken nuggets")
68 })
69
70 It("panics", func() {
71 Ω(func() {
72 internal.FetchDefaultDurationBundle()
73 }).Should(PanicWith(`Expected a duration when using GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT! Parse error time: invalid duration "chicken nuggets"`))
74 })
75 })
76 })
77
78 Describe("specifying default durations on a Gomega instance", func() {
79 It("is supported", func() {
80 ig := NewInstrumentedGomega()
81 ig.G.SetDefaultConsistentlyDuration(50 * time.Millisecond)
82 ig.G.SetDefaultConsistentlyPollingInterval(5 * time.Millisecond)
83 ig.G.SetDefaultEventuallyTimeout(200 * time.Millisecond)
84 ig.G.SetDefaultEventuallyPollingInterval(20 * time.Millisecond)
85
86 counter := 0
87 t := time.Now()
88 ig.G.Consistently(func() bool {
89 counter += 1
90 return true
91 }).Should(BeTrue())
92 dt := time.Since(t)
93 Ω(dt).Should(BeNumerically("~", 50*time.Millisecond, 25*time.Millisecond))
94 Ω(counter).Should(BeNumerically("~", 10, 5))
95
96 t = time.Now()
97 counter = 0
98 ig.G.Eventually(func() bool {
99 counter += 1
100 if counter >= 6 {
101 return true
102 }
103 return false
104 }).Should(BeTrue())
105 dt = time.Since(t)
106 Ω(dt).Should(BeNumerically("~", 120*time.Millisecond, 20*time.Millisecond))
107 })
108 })
109
110 Describe("specifying durations", func() {
111 It("supports passing in a duration", func() {
112 t := time.Now()
113 Consistently(true, 50*time.Millisecond).Should(BeTrue())
114 Ω(time.Since(t)).Should(BeNumerically("~", 50*time.Millisecond, 30*time.Millisecond))
115 })
116
117 It("supports passing in a raw integer # of seconds", func() {
118 t := time.Now()
119 Consistently(true, 1).Should(BeTrue())
120 Ω(time.Since(t)).Should(BeNumerically("~", time.Second, 100*time.Millisecond))
121 })
122
123 It("supports passing in an unsigned integer # of seconds", func() {
124 t := time.Now()
125 Consistently(true, uint(1)).Should(BeTrue())
126 Ω(time.Since(t)).Should(BeNumerically("~", time.Second, 100*time.Millisecond))
127 })
128
129 It("supports passing in a float number of seconds", func() {
130 t := time.Now()
131 Consistently(true, 0.05).Should(BeTrue())
132 Ω(time.Since(t)).Should(BeNumerically("~", 50*time.Millisecond, 30*time.Millisecond))
133 })
134
135 It("supports passing in a duration string", func() {
136 t := time.Now()
137 Consistently(true, "50ms").Should(BeTrue())
138 Ω(time.Since(t)).Should(BeNumerically("~", 50*time.Millisecond, 30*time.Millisecond))
139 })
140
141 It("panics when the duration string can't be parsed", func() {
142 Ω(func() {
143 Consistently(true, "fries").Should(BeTrue())
144 }).Should(PanicWith(`"fries" is not a valid parsable duration string.`))
145 })
146
147 It("panics if anything else is passed in", func() {
148 Ω(func() {
149 Consistently(true, true).Should(BeTrue())
150 }).Should(PanicWith("true is not a valid interval. Must be time.Duration, parsable duration string or a number."))
151 })
152 })
153 })
+0
-23
internal/fakematcher/fake_matcher.go less more
0 package fakematcher
1
2 import "fmt"
3
4 type FakeMatcher struct {
5 ReceivedActual interface{}
6 MatchesToReturn bool
7 ErrToReturn error
8 }
9
10 func (matcher *FakeMatcher) Match(actual interface{}) (bool, error) {
11 matcher.ReceivedActual = actual
12
13 return matcher.MatchesToReturn, matcher.ErrToReturn
14 }
15
16 func (matcher *FakeMatcher) FailureMessage(actual interface{}) string {
17 return fmt.Sprintf("positive: %v", actual)
18 }
19
20 func (matcher *FakeMatcher) NegatedFailureMessage(actual interface{}) string {
21 return fmt.Sprintf("negative: %v", actual)
22 }
0 package internal
1
2 import (
3 "time"
4
5 "github.com/onsi/gomega/types"
6 )
7
8 type Gomega struct {
9 Fail types.GomegaFailHandler
10 THelper func()
11 DurationBundle DurationBundle
12 }
13
14 func NewGomega(bundle DurationBundle) *Gomega {
15 return &Gomega{
16 Fail: nil,
17 THelper: nil,
18 DurationBundle: bundle,
19 }
20 }
21
22 func (g *Gomega) IsConfigured() bool {
23 return g.Fail != nil && g.THelper != nil
24 }
25
26 func (g *Gomega) ConfigureWithFailHandler(fail types.GomegaFailHandler) *Gomega {
27 g.Fail = fail
28 g.THelper = func() {}
29 return g
30 }
31
32 func (g *Gomega) ConfigureWithT(t types.GomegaTestingT) *Gomega {
33 g.Fail = func(message string, _ ...int) {
34 t.Helper()
35 t.Fatalf("\n%s", message)
36 }
37 g.THelper = t.Helper
38 return g
39 }
40
41 func (g *Gomega) Ω(actual interface{}, extra ...interface{}) types.Assertion {
42 return g.ExpectWithOffset(0, actual, extra...)
43 }
44
45 func (g *Gomega) Expect(actual interface{}, extra ...interface{}) types.Assertion {
46 return g.ExpectWithOffset(0, actual, extra...)
47 }
48
49 func (g *Gomega) ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) types.Assertion {
50 return NewAssertion(actual, g, offset, extra...)
51 }
52
53 func (g *Gomega) Eventually(actual interface{}, intervals ...interface{}) types.AsyncAssertion {
54 return g.EventuallyWithOffset(0, actual, intervals...)
55 }
56
57 func (g *Gomega) EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) types.AsyncAssertion {
58 timeoutInterval := g.DurationBundle.EventuallyTimeout
59 pollingInterval := g.DurationBundle.EventuallyPollingInterval
60 if len(intervals) > 0 {
61 timeoutInterval = toDuration(intervals[0])
62 }
63 if len(intervals) > 1 {
64 pollingInterval = toDuration(intervals[1])
65 }
66
67 return NewAsyncAssertion(AsyncAssertionTypeEventually, actual, g, timeoutInterval, pollingInterval, offset)
68 }
69
70 func (g *Gomega) Consistently(actual interface{}, intervals ...interface{}) types.AsyncAssertion {
71 return g.ConsistentlyWithOffset(0, actual, intervals...)
72 }
73
74 func (g *Gomega) ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) types.AsyncAssertion {
75 timeoutInterval := g.DurationBundle.ConsistentlyDuration
76 pollingInterval := g.DurationBundle.ConsistentlyPollingInterval
77 if len(intervals) > 0 {
78 timeoutInterval = toDuration(intervals[0])
79 }
80 if len(intervals) > 1 {
81 pollingInterval = toDuration(intervals[1])
82 }
83
84 return NewAsyncAssertion(AsyncAssertionTypeConsistently, actual, g, timeoutInterval, pollingInterval, offset)
85 }
86
87 func (g *Gomega) SetDefaultEventuallyTimeout(t time.Duration) {
88 g.DurationBundle.EventuallyTimeout = t
89 }
90
91 func (g *Gomega) SetDefaultEventuallyPollingInterval(t time.Duration) {
92 g.DurationBundle.EventuallyPollingInterval = t
93 }
94
95 func (g *Gomega) SetDefaultConsistentlyDuration(t time.Duration) {
96 g.DurationBundle.ConsistentlyDuration = t
97 }
98
99 func (g *Gomega) SetDefaultConsistentlyPollingInterval(t time.Duration) {
100 g.DurationBundle.ConsistentlyPollingInterval = t
101 }
0 package internal_test
1
2 import (
3 "runtime"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/internal"
8 )
9
10 var _ = Describe("Gomega", func() {
11 It("is mostly tested in assertion_test and async_assertion_test", func() {
12
13 })
14 Describe("when initialized", func() {
15 var g *internal.Gomega
16
17 BeforeEach(func() {
18 g = internal.NewGomega(internal.DurationBundle{})
19 Ω(g.Fail).Should(BeNil())
20 Ω(g.THelper).Should(BeNil())
21 })
22
23 It("should be registered as unconfigured", func() {
24 Ω(g.IsConfigured()).Should(BeFalse())
25 })
26
27 Context("when configured with a fail handler", func() {
28 It("registers the fail handler and a no-op helper", func() {
29 var capturedMessage string
30 g.ConfigureWithFailHandler(func(message string, skip ...int) {
31 capturedMessage = message
32 })
33 Ω(g.IsConfigured()).Should(BeTrue())
34
35 g.Fail("hi bob")
36 Ω(capturedMessage).Should(Equal("hi bob"))
37 Ω(g.THelper).ShouldNot(Panic())
38 })
39 })
40
41 Context("when configured with a T", func() {
42 It("registers a fail handler an the T's helper", func() {
43 fake := &FakeGomegaTestingT{}
44 g.ConfigureWithT(fake)
45 Ω(g.IsConfigured()).Should(BeTrue())
46
47 g.Fail("hi bob")
48 Ω(fake.CalledHelper).Should(BeTrue())
49 Ω(fake.CalledFatalf).Should(Equal("\nhi bob"))
50
51 fake.CalledHelper = false
52 g.THelper()
53 Ω(fake.CalledHelper).Should(BeTrue())
54 })
55 })
56 })
57
58 Describe("Offset", func() {
59 It("computes the correct offsets", func() {
60 doubleNested := func(g Gomega, eventually bool) {
61 func() {
62 if eventually {
63 g.Eventually(true, "10ms", "5ms").WithOffset(2).Should(BeFalse())
64 } else {
65 g.Expect(true).WithOffset(2).To(BeFalse())
66 }
67 }()
68 }
69
70 reportedFile, reportedLine := "", 0
71 _, thisFile, anchorLine, _ := runtime.Caller(0) // 0
72 g := NewGomega(func(message string, skip ...int) { // 1
73 _, reportedFile, reportedLine, _ = runtime.Caller(skip[0] + 1) // 2
74 }) // 3
75 g.Expect(true).To(BeFalse()) // *4*
76 Ω(reportedFile).Should(Equal(thisFile)) // 5
77 Ω(reportedLine - anchorLine).Should(Equal(4)) // 6
78 doubleNested(g, false) // *7*
79 Ω(reportedFile).Should(Equal(thisFile)) // 8
80 Ω(reportedLine - anchorLine).Should(Equal(7)) // 9
81 g.Eventually(true, "10ms", "5ms").Should(BeFalse()) // *10*
82 Ω(reportedFile).Should(Equal(thisFile)) // 11
83 Ω(reportedLine - anchorLine).Should(Equal(10)) // 12
84 doubleNested(g, true) // *13*
85 Ω(reportedFile).Should(Equal(thisFile)) // 14
86 Ω(reportedLine - anchorLine).Should(Equal(13)) // 15
87 })
88 })
89 })
0 package internal_test
1
2 import (
3 "errors"
4 "fmt"
5 "runtime"
6 "strings"
7 "testing"
8
9 . "github.com/onsi/ginkgo"
10 . "github.com/onsi/gomega"
11 "github.com/onsi/gomega/internal"
12 )
13
14 func TestInternal(t *testing.T) {
15 RegisterFailHandler(Fail)
16 RunSpecs(t, "Internal Suite")
17 }
18
19 // InstrumentedGomega
20 type InstrumentedGomega struct {
21 G *internal.Gomega
22 FailureMessage string
23 FailureSkip []int
24 RegisteredHelpers []string
25 }
26
27 func NewInstrumentedGomega() *InstrumentedGomega {
28 out := &InstrumentedGomega{}
29
30 out.G = internal.NewGomega(internal.FetchDefaultDurationBundle())
31 out.G.Fail = func(message string, skip ...int) {
32 out.FailureMessage = message
33 out.FailureSkip = skip
34 }
35 out.G.THelper = func() {
36 pc, _, _, _ := runtime.Caller(1)
37 f := runtime.FuncForPC(pc)
38 funcName := strings.TrimPrefix(f.Name(), "github.com/onsi/gomega/internal.")
39 out.RegisteredHelpers = append(out.RegisteredHelpers, funcName)
40 }
41
42 return out
43 }
44
45 // TestMatcher
46 var MATCH = "match"
47 var NO_MATCH = "no match"
48 var ERR_MATCH = "err match"
49 var TEST_MATCHER_ERR = errors.New("spec matcher error")
50
51 type SpecMatcher struct{}
52
53 func (matcher SpecMatcher) Match(actual interface{}) (bool, error) {
54 switch actual {
55 case MATCH:
56 return true, nil
57 case NO_MATCH:
58 return false, nil
59 case ERR_MATCH:
60 return false, TEST_MATCHER_ERR
61 }
62 return false, fmt.Errorf("unkown actual %v", actual)
63 }
64
65 func (matcher SpecMatcher) FailureMessage(actual interface{}) string {
66 return fmt.Sprintf("positive: %s", actual)
67 }
68
69 func (matcher SpecMatcher) NegatedFailureMessage(actual interface{}) string {
70 return fmt.Sprintf("negative: %s", actual)
71 }
72
73 func SpecMatch() SpecMatcher {
74 return SpecMatcher{}
75 }
76
77 //FakeGomegaTestingT
78 type FakeGomegaTestingT struct {
79 CalledHelper bool
80 CalledFatalf string
81 }
82
83 func (f *FakeGomegaTestingT) Helper() {
84 f.CalledHelper = true
85 }
86
87 func (f *FakeGomegaTestingT) Fatalf(s string, args ...interface{}) {
88 f.CalledFatalf = fmt.Sprintf(s, args...)
89 }
+0
-25
internal/oraclematcher/oracle_matcher.go less more
0 package oraclematcher
1
2 import "github.com/onsi/gomega/types"
3
4 /*
5 GomegaMatchers that also match the OracleMatcher interface can convey information about
6 whether or not their result will change upon future attempts.
7
8 This allows `Eventually` and `Consistently` to short circuit if success becomes impossible.
9
10 For example, a process' exit code can never change. So, gexec's Exit matcher returns `true`
11 for `MatchMayChangeInTheFuture` until the process exits, at which point it returns `false` forevermore.
12 */
13 type OracleMatcher interface {
14 MatchMayChangeInTheFuture(actual interface{}) bool
15 }
16
17 func MatchMayChangeInTheFuture(matcher types.GomegaMatcher, value interface{}) bool {
18 oracleMatcher, ok := matcher.(OracleMatcher)
19 if !ok {
20 return true
21 }
22
23 return oracleMatcher.MatchMayChangeInTheFuture(value)
24 }
+0
-60
internal/testingtsupport/testing_t_support.go less more
0 package testingtsupport
1
2 import (
3 "regexp"
4 "runtime/debug"
5 "strings"
6
7 "github.com/onsi/gomega/types"
8 )
9
10 var StackTracePruneRE = regexp.MustCompile(`\/gomega\/|\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`)
11
12 type EmptyTWithHelper struct{}
13
14 func (e EmptyTWithHelper) Helper() {}
15
16 type gomegaTestingT interface {
17 Fatalf(format string, args ...interface{})
18 }
19
20 func BuildTestingTGomegaFailWrapper(t gomegaTestingT) *types.GomegaFailWrapper {
21 tWithHelper, hasHelper := t.(types.TWithHelper)
22 if !hasHelper {
23 tWithHelper = EmptyTWithHelper{}
24 }
25
26 fail := func(message string, callerSkip ...int) {
27 if hasHelper {
28 tWithHelper.Helper()
29 t.Fatalf("\n%s", message)
30 } else {
31 skip := 2
32 if len(callerSkip) > 0 {
33 skip += callerSkip[0]
34 }
35 stackTrace := pruneStack(string(debug.Stack()), skip)
36 t.Fatalf("\n%s\n%s\n", stackTrace, message)
37 }
38 }
39
40 return &types.GomegaFailWrapper{
41 Fail: fail,
42 TWithHelper: tWithHelper,
43 }
44 }
45
46 func pruneStack(fullStackTrace string, skip int) string {
47 stack := strings.Split(fullStackTrace, "\n")[1:]
48 if len(stack) > 2*skip {
49 stack = stack[2*skip:]
50 }
51 prunedStack := []string{}
52 for i := 0; i < len(stack)/2; i++ {
53 if !StackTracePruneRE.Match([]byte(stack[i*2])) {
54 prunedStack = append(prunedStack, stack[i*2])
55 prunedStack = append(prunedStack, stack[i*2+1])
56 }
57 }
58 return strings.Join(prunedStack, "\n")
59 }
00 package testingtsupport_test
11
22 import (
3 "regexp"
4 "time"
5
6 "github.com/onsi/gomega/internal/testingtsupport"
7
83 . "github.com/onsi/gomega"
94
10 "fmt"
115 "testing"
126 )
137
1610 Ω(true).Should(BeTrue())
1711 }
1812
19 type FakeTWithHelper struct {
20 LastFatal string
13 func TestNewGomegaWithT(t *testing.T) {
14 g := NewWithT(t)
15 g.Expect(true).To(BeTrue())
2116 }
22
23 func (f *FakeTWithHelper) Fatalf(format string, args ...interface{}) {
24 f.LastFatal = fmt.Sprintf(format, args...)
25 }
26
27 func TestGomegaWithTWithoutHelper(t *testing.T) {
28 g := NewGomegaWithT(t)
29
30 testingtsupport.StackTracePruneRE = regexp.MustCompile(`\/ginkgo\/`)
31
32 f := &FakeTWithHelper{}
33 testG := NewGomegaWithT(f)
34
35 testG.Expect("foo").To(Equal("foo"))
36 g.Expect(f.LastFatal).To(BeZero())
37
38 testG.Expect("foo").To(Equal("bar"))
39 g.Expect(f.LastFatal).To(ContainSubstring("<string>: foo"))
40 g.Expect(f.LastFatal).To(ContainSubstring("testingtsupport_test"), "It should include a stacktrace")
41
42 testG.Eventually("foo2", time.Millisecond).Should(Equal("bar"))
43 g.Expect(f.LastFatal).To(ContainSubstring("<string>: foo2"))
44
45 testG.Consistently("foo3", time.Millisecond).Should(Equal("bar"))
46 g.Expect(f.LastFatal).To(ContainSubstring("<string>: foo3"))
47 }
48
49 type FakeTWithoutHelper struct {
50 LastFatal string
51 HelperCount int
52 }
53
54 func (f *FakeTWithoutHelper) Fatalf(format string, args ...interface{}) {
55 f.LastFatal = fmt.Sprintf(format, args...)
56 }
57
58 func (f *FakeTWithoutHelper) Helper() {
59 f.HelperCount += 1
60 }
61
62 func (f *FakeTWithoutHelper) ResetHelper() {
63 f.HelperCount = 0
64 }
65
66 func TestGomegaWithTWithHelper(t *testing.T) {
67 g := NewGomegaWithT(t)
68
69 f := &FakeTWithoutHelper{}
70 testG := NewGomegaWithT(f)
71
72 testG.Expect("foo").To(Equal("foo"))
73 g.Expect(f.LastFatal).To(BeZero())
74 g.Expect(f.HelperCount).To(BeNumerically(">", 0))
75 f.ResetHelper()
76
77 testG.Expect("foo").To(Equal("bar"))
78 g.Expect(f.LastFatal).To(ContainSubstring("<string>: foo"))
79 g.Expect(f.LastFatal).NotTo(ContainSubstring("testingtsupport_test"), "It should _not_ include a stacktrace")
80 g.Expect(f.HelperCount).To(BeNumerically(">", 0))
81 f.ResetHelper()
82
83 testG.Eventually("foo2", time.Millisecond).Should(Equal("bar"))
84 g.Expect(f.LastFatal).To(ContainSubstring("<string>: foo2"))
85 g.Expect(f.HelperCount).To(BeNumerically(">", 0))
86 f.ResetHelper()
87
88 testG.Consistently("foo3", time.Millisecond).Should(Equal("bar"))
89 g.Expect(f.LastFatal).To(ContainSubstring("<string>: foo3"))
90 g.Expect(f.HelperCount).To(BeNumerically(">", 0))
91 }
33 "fmt"
44
55 "github.com/onsi/gomega/format"
6 "github.com/onsi/gomega/internal/oraclematcher"
76 "github.com/onsi/gomega/types"
87 )
98
5150 if m.firstFailedMatcher == nil {
5251 // so all matchers succeeded.. Any one of them changing would change the result.
5352 for _, matcher := range m.Matchers {
54 if oraclematcher.MatchMayChangeInTheFuture(matcher, actual) {
53 if types.MatchMayChangeInTheFuture(matcher, actual) {
5554 return true
5655 }
5756 }
5857 return false // none of were going to change
5958 }
6059 // one of the matchers failed.. it must be able to change in order to affect the result
61 return oraclematcher.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual)
60 return types.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual)
6261 }
00 package matchers_test
11
22 import (
3 "io/ioutil"
43 "os"
54
65 . "github.com/onsi/ginkgo"
1312 It("should do the right thing", func() {
1413 Expect("/dne/test").ShouldNot(BeADirectory())
1514
16 tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile")
15 tmpFile, err := os.CreateTemp("", "gomega-test-tempfile")
1716 Expect(err).ShouldNot(HaveOccurred())
1817 defer os.Remove(tmpFile.Name())
1918 Expect(tmpFile.Name()).ShouldNot(BeADirectory())
2019
21 tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir")
20 tmpDir, err := os.MkdirTemp("", "gomega-test-tempdir")
2221 Expect(err).ShouldNot(HaveOccurred())
2322 defer os.Remove(tmpDir)
2423 Expect(tmpDir).Should(BeADirectory())
00 package matchers_test
11
22 import (
3 "io/ioutil"
43 "os"
54
65 . "github.com/onsi/ginkgo"
1312 It("should do the right thing", func() {
1413 Expect("/dne/test").ShouldNot(BeARegularFile())
1514
16 tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile")
15 tmpFile, err := os.CreateTemp("", "gomega-test-tempfile")
1716 Expect(err).ShouldNot(HaveOccurred())
1817 defer os.Remove(tmpFile.Name())
1918 Expect(tmpFile.Name()).Should(BeARegularFile())
2019
21 tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir")
20 tmpDir, err := os.MkdirTemp("", "gomega-test-tempdir")
2221 Expect(err).ShouldNot(HaveOccurred())
2322 defer os.Remove(tmpDir)
2423 Expect(tmpDir).ShouldNot(BeARegularFile())
00 package matchers_test
11
22 import (
3 "io/ioutil"
43 "os"
54
65 . "github.com/onsi/ginkgo"
1312 It("should do the right thing", func() {
1413 Expect("/dne/test").ShouldNot(BeAnExistingFile())
1514
16 tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile")
15 tmpFile, err := os.CreateTemp("", "gomega-test-tempfile")
1716 Expect(err).ShouldNot(HaveOccurred())
1817 defer os.Remove(tmpFile.Name())
1918 Expect(tmpFile.Name()).Should(BeAnExistingFile())
2019
21 tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir")
20 tmpDir, err := os.MkdirTemp("", "gomega-test-tempdir")
2221 Expect(err).ShouldNot(HaveOccurred())
2322 defer os.Remove(tmpDir)
2423 Expect(tmpDir).Should(BeAnExistingFile())
1717 return false, fmt.Errorf("BeElement matcher expects actual to be typed")
1818 }
1919
20 length := len(matcher.Elements)
21 valueAt := func(i int) interface{} {
22 return matcher.Elements[i]
23 }
24 // Special handling of a single element of type Array or Slice
25 if length == 1 && isArrayOrSlice(valueAt(0)) {
26 element := valueAt(0)
27 value := reflect.ValueOf(element)
28 length = value.Len()
29 valueAt = func(i int) interface{} {
30 return value.Index(i).Interface()
31 }
32 }
33
3420 var lastError error
35 for i := 0; i < length; i++ {
36 matcher := &EqualMatcher{Expected: valueAt(i)}
21 for _, m := range flatten(matcher.Elements) {
22 matcher := &EqualMatcher{Expected: m}
3723 success, err := matcher.Match(actual)
3824 if err != nil {
3925 lastError = err
4834 }
4935
5036 func (matcher *BeElementOfMatcher) FailureMessage(actual interface{}) (message string) {
51 return format.Message(actual, "to be an element of", matcher.Elements)
37 return format.Message(actual, "to be an element of", presentable(matcher.Elements))
5238 }
5339
5440 func (matcher *BeElementOfMatcher) NegatedFailureMessage(actual interface{}) (message string) {
55 return format.Message(actual, "not to be an element of", matcher.Elements)
41 return format.Message(actual, "not to be an element of", presentable(matcher.Elements))
5642 }
3232 })
3333
3434 When("passed a correctly typed nil", func() {
35 It("should operate succesfully on the passed in value", func() {
35 It("should operate successfully on the passed in value", func() {
3636 var nilSlice []int
3737 Expect(1).ShouldNot(BeElementOf(nilSlice))
3838
5555
5656 It("builds failure message", func() {
5757 actual := BeElementOf(1, 2).FailureMessage(123)
58 Expect(actual).To(Equal("Expected\n <int>: 123\nto be an element of\n <[]interface {} | len:2, cap:2>: [1, 2]"))
58 Expect(actual).To(Equal("Expected\n <int>: 123\nto be an element of\n <[]int | len:2, cap:2>: [1, 2]"))
5959 })
6060
6161 It("builds negated failure message", func() {
6262 actual := BeElementOf(1, 2).NegatedFailureMessage(123)
63 Expect(actual).To(Equal("Expected\n <int>: 123\nnot to be an element of\n <[]interface {} | len:2, cap:2>: [1, 2]"))
63 Expect(actual).To(Equal("Expected\n <int>: 123\nnot to be an element of\n <[]int | len:2, cap:2>: [1, 2]"))
6464 })
6565 })
4444 return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[0], 1))
4545 }
4646 if len(matcher.CompareTo) == 2 && !isNumber(matcher.CompareTo[1]) {
47 return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[0], 1))
47 return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[1], 1))
4848 }
4949
5050 switch matcher.Comparator {
142142 success, err = (&BeNumericallyMatcher{Comparator: "~", CompareTo: []interface{}{3.0, "foo"}}).Match(5.0)
143143 Expect(success).Should(BeFalse())
144144 Expect(err).Should(HaveOccurred())
145 Expect(err.Error()).Should(ContainSubstring("foo"))
145146
146147 success, err = (&BeNumericallyMatcher{Comparator: "==", CompareTo: []interface{}{"bar"}}).Match(5)
147148 Expect(success).Should(BeFalse())
5656 return
5757 }
5858
59 func matchers(expectedElems []interface{}) (matchers []interface{}) {
60 elems := expectedElems
61 if len(expectedElems) == 1 && isArrayOrSlice(expectedElems[0]) {
62 elems = []interface{}{}
63 value := reflect.ValueOf(expectedElems[0])
64 for i := 0; i < value.Len(); i++ {
65 elems = append(elems, value.Index(i).Interface())
66 }
59 func flatten(elems []interface{}) []interface{} {
60 if len(elems) != 1 || !isArrayOrSlice(elems[0]) {
61 return elems
6762 }
6863
69 for _, e := range elems {
64 value := reflect.ValueOf(elems[0])
65 flattened := make([]interface{}, value.Len())
66 for i := 0; i < value.Len(); i++ {
67 flattened[i] = value.Index(i).Interface()
68 }
69 return flattened
70 }
71
72 func matchers(expectedElems []interface{}) (matchers []interface{}) {
73 for _, e := range flatten(expectedElems) {
7074 matcher, isMatcher := e.(omegaMatcher)
7175 if !isMatcher {
7276 matcher = &EqualMatcher{Expected: e}
7478 matchers = append(matchers, matcher)
7579 }
7680 return
81 }
82
83 func presentable(elems []interface{}) interface{} {
84 elems = flatten(elems)
85
86 if len(elems) == 0 {
87 return []interface{}{}
88 }
89
90 sv := reflect.ValueOf(elems)
91 tt := sv.Index(0).Elem().Type()
92 for i := 1; i < sv.Len(); i++ {
93 if sv.Index(i).Elem().Type() != tt {
94 return elems
95 }
96 }
97
98 ss := reflect.MakeSlice(reflect.SliceOf(tt), sv.Len(), sv.Len())
99 for i := 0; i < sv.Len(); i++ {
100 ss.Index(i).Set(sv.Index(i).Elem())
101 }
102
103 return ss.Interface()
77104 }
78105
79106 func valuesOf(actual interface{}) []interface{} {
94121 }
95122
96123 func (matcher *ConsistOfMatcher) FailureMessage(actual interface{}) (message string) {
97 message = format.Message(actual, "to consist of", matcher.Elements)
124 message = format.Message(actual, "to consist of", presentable(matcher.Elements))
98125 message = appendMissingElements(message, matcher.missingElements)
99126 if len(matcher.extraElements) > 0 {
100127 message = fmt.Sprintf("%s\nthe extra elements were\n%s", message,
101 format.Object(matcher.extraElements, 1))
128 format.Object(presentable(matcher.extraElements), 1))
102129 }
103130 return
104131 }
108135 return message
109136 }
110137 return fmt.Sprintf("%s\nthe missing elements were\n%s", message,
111 format.Object(missingElements, 1))
138 format.Object(presentable(missingElements), 1))
112139 }
113140
114141 func (matcher *ConsistOfMatcher) NegatedFailureMessage(actual interface{}) (message string) {
115 return format.Message(actual, "not to consist of", matcher.Elements)
142 return format.Message(actual, "not to consist of", presentable(matcher.Elements))
116143 }
106106 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
107107 })
108108 })
109
110 When("expected was specified as an array", func() {
111 It("flattens the array in the expectation message", func() {
112 failures := InterceptGomegaFailures(func() {
113 Expect([]string{"A", "B", "C"}).To(ConsistOf([]string{"A", "B"}))
114 })
115
116 expected := `Expected\n.*\["A", "B", "C"\]\nto consist of\n.*: \["A", "B"\]\nthe extra elements were\n.*\["C"\]`
117 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
118 })
119
120 It("flattens the array in the negated expectation message", func() {
121 failures := InterceptGomegaFailures(func() {
122 Expect([]string{"A", "B"}).NotTo(ConsistOf([]string{"A", "B"}))
123 })
124
125 expected := `Expected\n.*\["A", "B"\]\nnot to consist of\n.*: \["A", "B"\]`
126 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
127 })
128 })
129
130 When("the expected values are the same type", func() {
131 It("uses that type for the expectation slice", func() {
132 failures := InterceptGomegaFailures(func() {
133 Expect([]string{"A", "B"}).To(ConsistOf("A", "C"))
134 })
135
136 expected := `to consist of
137 \s*<\[\]string \| len:2, cap:2>: \["A", "C"\]
138 the missing elements were
139 \s*<\[\]string \| len:1, cap:1>: \["C"\]
140 the extra elements were
141 \s*<\[\]string \| len:1, cap:1>: \["B"\]`
142 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
143 })
144
145 It("uses that type for the negated expectation slice", func() {
146 failures := InterceptGomegaFailures(func() {
147 Expect([]uint64{1, 2}).NotTo(ConsistOf(uint64(1), uint64(2)))
148 })
149
150 expected := `not to consist of\n\s*<\[\]uint64 \| len:2, cap:2>: \[1, 2\]`
151 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
152 })
153 })
154
155 When("the expected values are different types", func() {
156 It("uses interface{} for the expectation slice", func() {
157 failures := InterceptGomegaFailures(func() {
158 Expect([]interface{}{1, true}).To(ConsistOf(1, "C"))
159 })
160
161 expected := `to consist of
162 \s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"C"\]
163 the missing elements were
164 \s*<\[\]string \| len:1, cap:1>: \["C"\]
165 the extra elements were
166 \s*<\[\]bool \| len:1, cap:1>: \[true\]`
167 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
168 })
169
170 It("uses interface{} for the negated expectation slice", func() {
171 failures := InterceptGomegaFailures(func() {
172 Expect([]interface{}{1, "B"}).NotTo(ConsistOf(1, "B"))
173 })
174
175 expected := `not to consist of\n\s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"B"\]`
176 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
177 })
178 })
109179 })
110180 })
3434 }
3535
3636 func (matcher *ContainElementsMatcher) FailureMessage(actual interface{}) (message string) {
37 message = format.Message(actual, "to contain elements", matcher.Elements)
37 message = format.Message(actual, "to contain elements", presentable(matcher.Elements))
3838 return appendMissingElements(message, matcher.missingElements)
3939 }
4040
4141 func (matcher *ContainElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
42 return format.Message(actual, "not to contain elements", matcher.Elements)
42 return format.Message(actual, "not to contain elements", presentable(matcher.Elements))
4343 }
8181 expected := "Expected\n.*\\[2\\]\nto contain elements\n.*\\[1, 2, 3\\]\nthe missing elements were\n.*\\[1, 3\\]"
8282 Expect(failures).To(ContainElements(MatchRegexp(expected)))
8383 })
84
85 When("expected was specified as an array", func() {
86 It("flattens the array in the expectation message", func() {
87 failures := InterceptGomegaFailures(func() {
88 Expect([]string{"A", "B", "C"}).To(ContainElements([]string{"A", "D"}))
89 })
90
91 expected := `Expected\n.*\["A", "B", "C"\]\nto contain elements\n.*: \["A", "D"\]\nthe missing elements were\n.*\["D"\]`
92 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
93 })
94
95 It("flattens the array in the negated expectation message", func() {
96 failures := InterceptGomegaFailures(func() {
97 Expect([]string{"A", "B"}).NotTo(ContainElements([]string{"A", "B"}))
98 })
99
100 expected := `Expected\n.*\["A", "B"\]\nnot to contain elements\n.*: \["A", "B"\]`
101 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
102 })
103 })
104
105 When("the expected values are the same type", func() {
106 It("uses that type for the expectation slice", func() {
107 failures := InterceptGomegaFailures(func() {
108 Expect([]string{"A", "B"}).To(ContainElements("A", "B", "C"))
109 })
110
111 expected := `to contain elements
112 \s*<\[\]string \| len:3, cap:3>: \["A", "B", "C"\]
113 the missing elements were
114 \s*<\[\]string \| len:1, cap:1>: \["C"\]`
115 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
116 })
117
118 It("uses that type for the negated expectation slice", func() {
119 failures := InterceptGomegaFailures(func() {
120 Expect([]uint64{1, 2}).NotTo(ContainElements(uint64(1), uint64(2)))
121 })
122
123 expected := `not to contain elements\n\s*<\[\]uint64 \| len:2, cap:2>: \[1, 2\]`
124 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
125 })
126 })
127
128 When("the expected values are different types", func() {
129 It("uses interface{} for the expectation slice", func() {
130 failures := InterceptGomegaFailures(func() {
131 Expect([]interface{}{1, true}).To(ContainElements(1, "C"))
132 })
133
134 expected := `to contain elements
135 \s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"C"\]
136 the missing elements were
137 \s*<\[\]string \| len:1, cap:1>: \["C"\]`
138 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
139 })
140
141 It("uses interface{} for the negated expectation slice", func() {
142 failures := InterceptGomegaFailures(func() {
143 Expect([]interface{}{1, "B"}).NotTo(ContainElements(1, "B"))
144 })
145
146 expected := `not to contain elements\n\s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"B"\]`
147 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
148 })
149 })
84150 })
85151 })
0 package matchers
1
2 import (
3 "fmt"
4 "reflect"
5 "strings"
6
7 "github.com/onsi/gomega/format"
8 )
9
10 func extractField(actual interface{}, field string) (interface{}, error) {
11 fields := strings.SplitN(field, ".", 2)
12 actualValue := reflect.ValueOf(actual)
13
14 if actualValue.Kind() != reflect.Struct {
15 return nil, fmt.Errorf("HaveField encountered:\n%s\nWhich is not a struct.", format.Object(actual, 1))
16 }
17
18 var extractedValue reflect.Value
19
20 if strings.HasSuffix(fields[0], "()") {
21 extractedValue = actualValue.MethodByName(strings.TrimSuffix(fields[0], "()"))
22 if extractedValue == (reflect.Value{}) {
23 return nil, fmt.Errorf("HaveField could not find method named '%s' in struct of type %T.", fields[0], actual)
24 }
25 t := extractedValue.Type()
26 if t.NumIn() != 0 || t.NumOut() != 1 {
27 return nil, fmt.Errorf("HaveField found an invalid method named '%s' in struct of type %T.\nMethods must take no arguments and return exactly one value.", fields[0], actual)
28 }
29 extractedValue = extractedValue.Call([]reflect.Value{})[0]
30 } else {
31 extractedValue = actualValue.FieldByName(fields[0])
32 if extractedValue == (reflect.Value{}) {
33 return nil, fmt.Errorf("HaveField could not find field named '%s' in struct:\n%s", fields[0], format.Object(actual, 1))
34 }
35 }
36
37 if len(fields) == 1 {
38 return extractedValue.Interface(), nil
39 } else {
40 return extractField(extractedValue.Interface(), fields[1])
41 }
42 }
43
44 type HaveFieldMatcher struct {
45 Field string
46 Expected interface{}
47
48 extractedField interface{}
49 expectedMatcher omegaMatcher
50 }
51
52 func (matcher *HaveFieldMatcher) Match(actual interface{}) (success bool, err error) {
53 matcher.extractedField, err = extractField(actual, matcher.Field)
54 if err != nil {
55 return false, err
56 }
57
58 var isMatcher bool
59 matcher.expectedMatcher, isMatcher = matcher.Expected.(omegaMatcher)
60 if !isMatcher {
61 matcher.expectedMatcher = &EqualMatcher{Expected: matcher.Expected}
62 }
63
64 return matcher.expectedMatcher.Match(matcher.extractedField)
65 }
66
67 func (matcher *HaveFieldMatcher) FailureMessage(actual interface{}) (message string) {
68 message = fmt.Sprintf("Value for field '%s' failed to satisfy matcher.\n", matcher.Field)
69 message += matcher.expectedMatcher.FailureMessage(matcher.extractedField)
70
71 return message
72 }
73
74 func (matcher *HaveFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) {
75 message = fmt.Sprintf("Value for field '%s' satisfied matcher, but should not have.\n", matcher.Field)
76 message += matcher.expectedMatcher.NegatedFailureMessage(matcher.extractedField)
77
78 return message
79 }
0 package matchers_test
1
2 import (
3 "fmt"
4 "time"
5
6 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/extensions/table"
8 . "github.com/onsi/gomega"
9 )
10
11 type Book struct {
12 Title string
13 Author person
14 Pages int
15 }
16
17 func (book Book) AuthorName() string {
18 return fmt.Sprintf("%s %s", book.Author.FirstName, book.Author.LastName)
19 }
20
21 func (book Book) AbbreviatedAuthor() person {
22 return person{
23 FirstName: book.Author.FirstName[0:3],
24 LastName: book.Author.LastName[0:3],
25 DOB: book.Author.DOB,
26 }
27 }
28
29 func (book Book) NoReturn() {
30 }
31
32 func (book Book) TooManyReturn() (string, error) {
33 return "", nil
34 }
35
36 func (book Book) HasArg(arg string) string {
37 return arg
38 }
39
40 type person struct {
41 FirstName string
42 LastName string
43 DOB time.Time
44 }
45
46 var _ = Describe("HaveField", func() {
47 var book Book
48 BeforeEach(func() {
49 book = Book{
50 Title: "Les Miserables",
51 Author: person{
52 FirstName: "Victor",
53 LastName: "Hugo",
54 DOB: time.Date(1802, 2, 26, 0, 0, 0, 0, time.UTC),
55 },
56 Pages: 2783,
57 }
58 })
59
60 DescribeTable("traversing the struct works",
61 func(field string, expected interface{}) {
62 Ω(book).Should(HaveField(field, expected))
63 },
64 Entry("Top-level field with default submatcher", "Title", "Les Miserables"),
65 Entry("Top-level field with custom submatcher", "Title", ContainSubstring("Les Mis")),
66 Entry("Nested field", "Author.FirstName", "Victor"),
67 Entry("Top-level method", "AuthorName()", "Victor Hugo"),
68 Entry("Nested method", "Author.DOB.Year()", BeNumerically("<", 1900)),
69 Entry("Traversing past a method", "AbbreviatedAuthor().FirstName", Equal("Vic")),
70 )
71
72 DescribeTable("negation works",
73 func(field string, expected interface{}) {
74 Ω(book).ShouldNot(HaveField(field, expected))
75 },
76 Entry("Top-level field with default submatcher", "Title", "Les Mis"),
77 Entry("Top-level field with custom submatcher", "Title", ContainSubstring("Notre Dame")),
78 Entry("Nested field", "Author.FirstName", "Hugo"),
79 Entry("Top-level method", "AuthorName()", "Victor M. Hugo"),
80 Entry("Nested method", "Author.DOB.Year()", BeNumerically(">", 1900)),
81 )
82
83 Describe("when field lookup fails", func() {
84 It("errors appropriately", func() {
85 success, err := HaveField("BookName", "Les Miserables").Match(book)
86 Ω(success).Should(BeFalse())
87 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "BookName"))
88
89 success, err = HaveField("BookName", "Les Miserables").Match(book)
90 Ω(success).Should(BeFalse())
91 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "BookName"))
92
93 success, err = HaveField("AuthorName", "Victor Hugo").Match(book)
94 Ω(success).Should(BeFalse())
95 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "AuthorName"))
96
97 success, err = HaveField("Title()", "Les Miserables").Match(book)
98 Ω(success).Should(BeFalse())
99 Ω(err.Error()).Should(ContainSubstring("HaveField could not find method named '%s' in struct of type matchers_test.Book.", "Title()"))
100
101 success, err = HaveField("NoReturn()", "Les Miserables").Match(book)
102 Ω(success).Should(BeFalse())
103 Ω(err.Error()).Should(ContainSubstring("HaveField found an invalid method named 'NoReturn()' in struct of type matchers_test.Book.\nMethods must take no arguments and return exactly one value."))
104
105 success, err = HaveField("TooManyReturn()", "Les Miserables").Match(book)
106 Ω(success).Should(BeFalse())
107 Ω(err.Error()).Should(ContainSubstring("HaveField found an invalid method named 'TooManyReturn()' in struct of type matchers_test.Book.\nMethods must take no arguments and return exactly one value."))
108
109 success, err = HaveField("HasArg()", "Les Miserables").Match(book)
110 Ω(success).Should(BeFalse())
111 Ω(err.Error()).Should(ContainSubstring("HaveField found an invalid method named 'HasArg()' in struct of type matchers_test.Book.\nMethods must take no arguments and return exactly one value."))
112
113 success, err = HaveField("Pages.Count", 2783).Match(book)
114 Ω(success).Should(BeFalse())
115 Ω(err.Error()).Should(Equal("HaveField encountered:\n <int>: 2783\nWhich is not a struct."))
116
117 success, err = HaveField("Author.Abbreviation", "Vic").Match(book)
118 Ω(success).Should(BeFalse())
119 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "Abbreviation"))
120 })
121 })
122
123 Describe("Failure Messages", func() {
124 It("renders the underlying matcher failure", func() {
125 matcher := HaveField("Title", "Les Mis")
126 success, err := matcher.Match(book)
127 Ω(success).Should(BeFalse())
128 Ω(err).ShouldNot(HaveOccurred())
129
130 msg := matcher.FailureMessage(book)
131 Ω(msg).Should(Equal("Value for field 'Title' failed to satisfy matcher.\nExpected\n <string>: Les Miserables\nto equal\n <string>: Les Mis"))
132
133 matcher = HaveField("Title", "Les Miserables")
134 success, err = matcher.Match(book)
135 Ω(success).Should(BeTrue())
136 Ω(err).ShouldNot(HaveOccurred())
137
138 msg = matcher.NegatedFailureMessage(book)
139 Ω(msg).Should(Equal("Value for field 'Title' satisfied matcher, but should not have.\nExpected\n <string>: Les Miserables\nnot to equal\n <string>: Les Miserables"))
140 })
141 })
142 })
0 package matchers
1
2 import (
3 "fmt"
4 "io"
5 "net/http"
6 "net/http/httptest"
7
8 "github.com/onsi/gomega/format"
9 "github.com/onsi/gomega/types"
10 )
11
12 type HaveHTTPBodyMatcher struct {
13 Expected interface{}
14 cachedBody []byte
15 }
16
17 func (matcher *HaveHTTPBodyMatcher) Match(actual interface{}) (bool, error) {
18 body, err := matcher.body(actual)
19 if err != nil {
20 return false, err
21 }
22
23 switch e := matcher.Expected.(type) {
24 case string:
25 return (&EqualMatcher{Expected: e}).Match(string(body))
26 case []byte:
27 return (&EqualMatcher{Expected: e}).Match(body)
28 case types.GomegaMatcher:
29 return e.Match(body)
30 default:
31 return false, fmt.Errorf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
32 }
33 }
34
35 func (matcher *HaveHTTPBodyMatcher) FailureMessage(actual interface{}) (message string) {
36 body, err := matcher.body(actual)
37 if err != nil {
38 return fmt.Sprintf("failed to read body: %s", err)
39 }
40
41 switch e := matcher.Expected.(type) {
42 case string:
43 return (&EqualMatcher{Expected: e}).FailureMessage(string(body))
44 case []byte:
45 return (&EqualMatcher{Expected: e}).FailureMessage(body)
46 case types.GomegaMatcher:
47 return e.FailureMessage(body)
48 default:
49 return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
50 }
51 }
52
53 func (matcher *HaveHTTPBodyMatcher) NegatedFailureMessage(actual interface{}) (message string) {
54 body, err := matcher.body(actual)
55 if err != nil {
56 return fmt.Sprintf("failed to read body: %s", err)
57 }
58
59 switch e := matcher.Expected.(type) {
60 case string:
61 return (&EqualMatcher{Expected: e}).NegatedFailureMessage(string(body))
62 case []byte:
63 return (&EqualMatcher{Expected: e}).NegatedFailureMessage(body)
64 case types.GomegaMatcher:
65 return e.NegatedFailureMessage(body)
66 default:
67 return fmt.Sprintf("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n%s", format.Object(matcher.Expected, 1))
68 }
69 }
70
71 // body returns the body. It is cached because once we read it in Match()
72 // the Reader is closed and it is not readable again in FailureMessage()
73 // or NegatedFailureMessage()
74 func (matcher *HaveHTTPBodyMatcher) body(actual interface{}) ([]byte, error) {
75 if matcher.cachedBody != nil {
76 return matcher.cachedBody, nil
77 }
78
79 body := func(a *http.Response) ([]byte, error) {
80 if a.Body != nil {
81 defer a.Body.Close()
82 var err error
83 matcher.cachedBody, err = io.ReadAll(a.Body)
84 if err != nil {
85 return nil, fmt.Errorf("error reading response body: %w", err)
86 }
87 }
88 return matcher.cachedBody, nil
89 }
90
91 switch a := actual.(type) {
92 case *http.Response:
93 return body(a)
94 case *httptest.ResponseRecorder:
95 return body(a.Result())
96 default:
97 return nil, fmt.Errorf("HaveHTTPBody matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
98 }
99
100 }
0 package matchers_test
1
2 import (
3 "bytes"
4 "io"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8
9 . "github.com/onsi/ginkgo"
10 . "github.com/onsi/gomega"
11 )
12
13 var _ = Describe("HaveHTTPBody", func() {
14 When("ACTUAL is *http.Response", func() {
15 It("matches the body", func() {
16 const body = "this is the body"
17 resp := &http.Response{Body: io.NopCloser(strings.NewReader(body))}
18 Expect(resp).To(HaveHTTPBody(body))
19 })
20
21 It("mismatches the body", func() {
22 const body = "this is the body"
23 resp := &http.Response{Body: io.NopCloser(strings.NewReader(body))}
24 Expect(resp).NotTo(HaveHTTPBody("something else"))
25 })
26 })
27
28 When("ACTUAL is *httptest.ResponseRecorder", func() {
29 It("matches the body", func() {
30 const body = "this is the body"
31 resp := &httptest.ResponseRecorder{Body: bytes.NewBufferString(body)}
32 Expect(resp).To(HaveHTTPBody(body))
33 })
34
35 It("mismatches the body", func() {
36 const body = "this is the body"
37 resp := &httptest.ResponseRecorder{Body: bytes.NewBufferString(body)}
38 Expect(resp).NotTo(HaveHTTPBody("something else"))
39 })
40 })
41
42 When("ACTUAL is neither *http.Response nor *httptest.ResponseRecorder", func() {
43 It("errors", func() {
44 failures := InterceptGomegaFailures(func() {
45 Expect("foo").To(HaveHTTPBody("bar"))
46 })
47 Expect(failures).To(ConsistOf("HaveHTTPBody matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n <string>: foo"))
48 })
49 })
50
51 When("EXPECTED is []byte", func() {
52 It("matches the body", func() {
53 const body = "this is the body"
54 resp := &http.Response{Body: io.NopCloser(strings.NewReader(body))}
55 Expect(resp).To(HaveHTTPBody([]byte(body)))
56 })
57
58 It("mismatches the body", func() {
59 const body = "this is the body"
60 resp := &http.Response{Body: io.NopCloser(strings.NewReader(body))}
61 Expect(resp).NotTo(HaveHTTPBody([]byte("something else")))
62 })
63 })
64
65 When("EXPECTED is a submatcher", func() {
66 It("matches the body", func() {
67 resp := &http.Response{Body: io.NopCloser(strings.NewReader(`{"some":"json"}`))}
68 Expect(resp).To(HaveHTTPBody(MatchJSON(`{ "some": "json" }`)))
69 })
70
71 It("mismatches the body", func() {
72 resp := &http.Response{Body: io.NopCloser(strings.NewReader(`{"some":"json"}`))}
73 Expect(resp).NotTo(HaveHTTPBody(MatchJSON(`{ "something": "different" }`)))
74 })
75 })
76
77 When("EXPECTED is something else", func() {
78 It("errors", func() {
79 failures := InterceptGomegaFailures(func() {
80 resp := &http.Response{Body: io.NopCloser(strings.NewReader("body"))}
81 Expect(resp).To(HaveHTTPBody(map[int]bool{}))
82 })
83 Expect(failures).To(HaveLen(1))
84 Expect(failures[0]).To(Equal("HaveHTTPBody matcher expects string, []byte, or GomegaMatcher. Got:\n <map[int]bool | len:0>: {}"))
85 })
86 })
87
88 Describe("FailureMessage", func() {
89 Context("EXPECTED is string", func() {
90 It("returns a match failure message", func() {
91 failures := InterceptGomegaFailures(func() {
92 resp := &http.Response{Body: io.NopCloser(strings.NewReader("this is the body"))}
93 Expect(resp).To(HaveHTTPBody("this is a different body"))
94 })
95 Expect(failures).To(HaveLen(1))
96 Expect(failures[0]).To(Equal(`Expected
97 <string>: this is the body
98 to equal
99 <string>: this is a different body`), failures[0])
100 })
101 })
102
103 Context("EXPECTED is []byte", func() {
104 It("returns a match failure message", func() {
105 failures := InterceptGomegaFailures(func() {
106 resp := &http.Response{Body: io.NopCloser(strings.NewReader("this is the body"))}
107 Expect(resp).To(HaveHTTPBody([]byte("this is a different body")))
108 })
109 Expect(failures).To(HaveLen(1))
110 Expect(failures[0]).To(MatchRegexp(`^Expected
111 <\[\]uint8 \| len:\d+, cap:\d+>: this is the body
112 to equal
113 <\[\]uint8 ]| len:\d+, cap:\d+>: this is a different body$`))
114 })
115 })
116
117 Context("EXPECTED is submatcher", func() {
118 It("returns a match failure message", func() {
119 failures := InterceptGomegaFailures(func() {
120 resp := &http.Response{Body: io.NopCloser(strings.NewReader(`{"some":"json"}`))}
121 Expect(resp).To(HaveHTTPBody(MatchJSON(`{"other":"stuff"}`)))
122 })
123 Expect(failures).To(HaveLen(1))
124 Expect(failures[0]).To(Equal(`Expected
125 <string>: {
126 "some": "json"
127 }
128 to match JSON of
129 <string>: {
130 "other": "stuff"
131 }`))
132 })
133 })
134 })
135
136 Describe("NegatedFailureMessage", func() {
137 Context("EXPECTED is string", func() {
138 It("returns a negated failure message", func() {
139 const body = "this is the body"
140 failures := InterceptGomegaFailures(func() {
141 resp := &http.Response{Body: io.NopCloser(strings.NewReader(body))}
142 Expect(resp).NotTo(HaveHTTPBody(body))
143 })
144 Expect(failures).To(HaveLen(1))
145 Expect(failures[0]).To(Equal(`Expected
146 <string>: this is the body
147 not to equal
148 <string>: this is the body`))
149 })
150 })
151
152 Context("EXPECTED is []byte", func() {
153 It("returns a match failure message", func() {
154 const body = "this is the body"
155 failures := InterceptGomegaFailures(func() {
156 resp := &http.Response{Body: io.NopCloser(strings.NewReader(body))}
157 Expect(resp).NotTo(HaveHTTPBody([]byte(body)))
158 })
159 Expect(failures).To(HaveLen(1))
160 Expect(failures[0]).To(MatchRegexp(`^Expected
161 <\[\]uint8 \| len:\d+, cap:\d+>: this is the body
162 not to equal
163 <\[\]uint8 \| len:\d+, cap:\d+>: this is the body$`))
164 })
165 })
166
167 Context("EXPECTED is submatcher", func() {
168 It("returns a match failure message", func() {
169 const body = `{"some":"json"}`
170 failures := InterceptGomegaFailures(func() {
171 resp := &http.Response{Body: io.NopCloser(strings.NewReader(body))}
172 Expect(resp).NotTo(HaveHTTPBody(MatchJSON(body)))
173 })
174 Expect(failures).To(HaveLen(1))
175 Expect(failures[0]).To(Equal(`Expected
176 <string>: {
177 "some": "json"
178 }
179 not to match JSON of
180 <string>: {
181 "some": "json"
182 }`))
183 })
184 })
185 })
186 })
0 package matchers
1
2 import (
3 "fmt"
4 "net/http"
5 "net/http/httptest"
6
7 "github.com/onsi/gomega/format"
8 "github.com/onsi/gomega/types"
9 )
10
11 type HaveHTTPHeaderWithValueMatcher struct {
12 Header string
13 Value interface{}
14 }
15
16 func (matcher *HaveHTTPHeaderWithValueMatcher) Match(actual interface{}) (success bool, err error) {
17 headerValue, err := matcher.extractHeader(actual)
18 if err != nil {
19 return false, err
20 }
21
22 headerMatcher, err := matcher.getSubMatcher()
23 if err != nil {
24 return false, err
25 }
26
27 return headerMatcher.Match(headerValue)
28 }
29
30 func (matcher *HaveHTTPHeaderWithValueMatcher) FailureMessage(actual interface{}) string {
31 headerValue, err := matcher.extractHeader(actual)
32 if err != nil {
33 panic(err) // protected by Match()
34 }
35
36 headerMatcher, err := matcher.getSubMatcher()
37 if err != nil {
38 panic(err) // protected by Match()
39 }
40
41 diff := format.IndentString(headerMatcher.FailureMessage(headerValue), 1)
42 return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff)
43 }
44
45 func (matcher *HaveHTTPHeaderWithValueMatcher) NegatedFailureMessage(actual interface{}) (message string) {
46 headerValue, err := matcher.extractHeader(actual)
47 if err != nil {
48 panic(err) // protected by Match()
49 }
50
51 headerMatcher, err := matcher.getSubMatcher()
52 if err != nil {
53 panic(err) // protected by Match()
54 }
55
56 diff := format.IndentString(headerMatcher.NegatedFailureMessage(headerValue), 1)
57 return fmt.Sprintf("HTTP header %q:\n%s", matcher.Header, diff)
58 }
59
60 func (matcher *HaveHTTPHeaderWithValueMatcher) getSubMatcher() (types.GomegaMatcher, error) {
61 switch m := matcher.Value.(type) {
62 case string:
63 return &EqualMatcher{Expected: matcher.Value}, nil
64 case types.GomegaMatcher:
65 return m, nil
66 default:
67 return nil, fmt.Errorf("HaveHTTPHeaderWithValue matcher must be passed a string or a GomegaMatcher. Got:\n%s", format.Object(matcher.Value, 1))
68 }
69 }
70
71 func (matcher *HaveHTTPHeaderWithValueMatcher) extractHeader(actual interface{}) (string, error) {
72 switch r := actual.(type) {
73 case *http.Response:
74 return r.Header.Get(matcher.Header), nil
75 case *httptest.ResponseRecorder:
76 return r.Result().Header.Get(matcher.Header), nil
77 default:
78 return "", fmt.Errorf("HaveHTTPHeaderWithValue matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
79 }
80 }
0 package matchers_test
1
2 import (
3 "net/http"
4 "net/http/httptest"
5
6 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/gomega"
8 )
9
10 var _ = Describe("HaveHTTPHeader", func() {
11 It("can match an HTTP header", func() {
12 resp := &http.Response{}
13 resp.Header = make(http.Header)
14 resp.Header.Add("fake-header", "fake value")
15 Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "fake value"))
16 })
17
18 It("can mismatch an HTTP header", func() {
19 resp := &http.Response{}
20 resp.Header = make(http.Header)
21 resp.Header.Add("fake-header", "fake value")
22 Expect(resp).NotTo(HaveHTTPHeaderWithValue("other-header", "fake value"))
23 Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "other value"))
24 })
25
26 When("the header is set more than once", func() {
27 It("matches the first value and not the second", func() {
28 resp := &http.Response{}
29 resp.Header = make(http.Header)
30 resp.Header.Add("fake-header", "fake value1")
31 resp.Header.Add("fake-header", "fake value2")
32 Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "fake value1"))
33 Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "fake value2"))
34 })
35 })
36
37 When("ACTUAL is *httptest.ResponseRecorder", func() {
38 It("can match an HTTP header", func() {
39 resp := &httptest.ResponseRecorder{}
40 resp.Header().Add("fake-header", "fake value")
41 Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "fake value"))
42 })
43
44 It("can mismatch an HTTP header", func() {
45 resp := &httptest.ResponseRecorder{}
46 resp.Header().Add("fake-header", "fake value")
47 Expect(resp).NotTo(HaveHTTPHeaderWithValue("other-header", "fake value"))
48 Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "other value"))
49 })
50 })
51
52 When("ACTUAL is neither *http.Response nor *httptest.ResponseRecorder", func() {
53 It("errors", func() {
54 failures := InterceptGomegaFailures(func() {
55 Expect("foo").To(HaveHTTPHeaderWithValue("bar", "baz"))
56 })
57 Expect(failures).To(HaveLen(1))
58 Expect(failures[0]).To(Equal("HaveHTTPHeaderWithValue matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n <string>: foo"))
59 })
60 })
61
62 When("EXPECTED VALUE is a matcher", func() {
63 It("can match an HTTP header", func() {
64 resp := &http.Response{}
65 resp.Header = make(http.Header)
66 resp.Header.Add("fake-header", "fake value")
67 Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("value")))
68 })
69
70 It("can mismatch an HTTP header", func() {
71 resp := &http.Response{}
72 resp.Header = make(http.Header)
73 resp.Header.Add("fake-header", "fake value")
74 Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("foo")))
75 })
76 })
77
78 When("EXPECTED VALUE is something else", func() {
79 It("errors", func() {
80 failures := InterceptGomegaFailures(func() {
81 resp := &http.Response{}
82 Expect(resp).To(HaveHTTPHeaderWithValue("bar", 42))
83 })
84 Expect(failures).To(HaveLen(1))
85 Expect(failures[0]).To(Equal("HaveHTTPHeaderWithValue matcher must be passed a string or a GomegaMatcher. Got:\n <int>: 42"))
86 })
87 })
88
89 Describe("FailureMessage", func() {
90 When("matching a string", func() {
91 It("returns message", func() {
92 failures := InterceptGomegaFailures(func() {
93 resp := &http.Response{}
94 resp.Header = make(http.Header)
95 resp.Header.Add("fake-header", "fake value")
96 Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", "other value"))
97 })
98 Expect(failures).To(HaveLen(1))
99 Expect(failures[0]).To(Equal(`HTTP header "fake-header":
100 Expected
101 <string>: fake value
102 to equal
103 <string>: other value`), failures[0])
104 })
105 })
106
107 When("matching a matcher", func() {
108 It("returns message", func() {
109 failures := InterceptGomegaFailures(func() {
110 resp := &http.Response{}
111 resp.Header = make(http.Header)
112 resp.Header.Add("fake-header", "fake value")
113 Expect(resp).To(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("other")))
114 })
115 Expect(failures).To(HaveLen(1))
116 Expect(failures[0]).To(Equal(`HTTP header "fake-header":
117 Expected
118 <string>: fake value
119 to contain substring
120 <string>: other`), failures[0])
121 })
122 })
123 })
124
125 Describe("NegatedFailureMessage", func() {
126 When("matching a string", func() {
127 It("returns message", func() {
128 failures := InterceptGomegaFailures(func() {
129 resp := &http.Response{}
130 resp.Header = make(http.Header)
131 resp.Header.Add("fake-header", "fake value")
132 Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", "fake value"))
133 })
134 Expect(failures).To(HaveLen(1))
135 Expect(failures[0]).To(Equal(`HTTP header "fake-header":
136 Expected
137 <string>: fake value
138 not to equal
139 <string>: fake value`), failures[0])
140 })
141 })
142
143 When("matching a matcher", func() {
144 It("returns message", func() {
145 failures := InterceptGomegaFailures(func() {
146 resp := &http.Response{}
147 resp.Header = make(http.Header)
148 resp.Header.Add("fake-header", "fake value")
149 Expect(resp).NotTo(HaveHTTPHeaderWithValue("fake-header", ContainSubstring("value")))
150 })
151 Expect(failures).To(HaveLen(1))
152 Expect(failures[0]).To(Equal(`HTTP header "fake-header":
153 Expected
154 <string>: fake value
155 not to contain substring
156 <string>: value`), failures[0])
157 })
158 })
159 })
160 })
11
22 import (
33 "fmt"
4 "io"
45 "net/http"
56 "net/http/httptest"
7 "reflect"
8 "strings"
69
710 "github.com/onsi/gomega/format"
811 )
912
1013 type HaveHTTPStatusMatcher struct {
11 Expected interface{}
14 Expected []interface{}
1215 }
1316
1417 func (matcher *HaveHTTPStatusMatcher) Match(actual interface{}) (success bool, err error) {
2225 return false, fmt.Errorf("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n%s", format.Object(actual, 1))
2326 }
2427
25 switch e := matcher.Expected.(type) {
26 case int:
27 return resp.StatusCode == e, nil
28 case string:
29 return resp.Status == e, nil
28 if len(matcher.Expected) == 0 {
29 return false, fmt.Errorf("HaveHTTPStatus matcher must be passed an int or a string. Got nothing")
3030 }
3131
32 return false, fmt.Errorf("HaveHTTPStatus matcher must be passed an int or a string. Got:\n%s", format.Object(matcher.Expected, 1))
32 for _, expected := range matcher.Expected {
33 switch e := expected.(type) {
34 case int:
35 if resp.StatusCode == e {
36 return true, nil
37 }
38 case string:
39 if resp.Status == e {
40 return true, nil
41 }
42 default:
43 return false, fmt.Errorf("HaveHTTPStatus matcher must be passed int or string types. Got:\n%s", format.Object(expected, 1))
44 }
45 }
46
47 return false, nil
3348 }
3449
3550 func (matcher *HaveHTTPStatusMatcher) FailureMessage(actual interface{}) (message string) {
36 return format.Message(actual, "to have HTTP status", matcher.Expected)
51 return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "to have HTTP status", matcher.expectedString())
3752 }
3853
3954 func (matcher *HaveHTTPStatusMatcher) NegatedFailureMessage(actual interface{}) (message string) {
40 return format.Message(actual, "not to have HTTP status", matcher.Expected)
55 return fmt.Sprintf("Expected\n%s\n%s\n%s", formatHttpResponse(actual), "not to have HTTP status", matcher.expectedString())
4156 }
57
58 func (matcher *HaveHTTPStatusMatcher) expectedString() string {
59 var lines []string
60 for _, expected := range matcher.Expected {
61 lines = append(lines, format.Object(expected, 1))
62 }
63 return strings.Join(lines, "\n")
64 }
65
66 func formatHttpResponse(input interface{}) string {
67 var resp *http.Response
68 switch r := input.(type) {
69 case *http.Response:
70 resp = r
71 case *httptest.ResponseRecorder:
72 resp = r.Result()
73 default:
74 return "cannot format invalid HTTP response"
75 }
76
77 body := "<nil>"
78 if resp.Body != nil {
79 defer resp.Body.Close()
80 data, err := io.ReadAll(resp.Body)
81 if err != nil {
82 data = []byte("<error reading body>")
83 }
84 body = format.Object(string(data), 0)
85 }
86
87 var s strings.Builder
88 s.WriteString(fmt.Sprintf("%s<%s>: {\n", format.Indent, reflect.TypeOf(input)))
89 s.WriteString(fmt.Sprintf("%s%sStatus: %s\n", format.Indent, format.Indent, format.Object(resp.Status, 0)))
90 s.WriteString(fmt.Sprintf("%s%sStatusCode: %s\n", format.Indent, format.Indent, format.Object(resp.StatusCode, 0)))
91 s.WriteString(fmt.Sprintf("%s%sBody: %s\n", format.Indent, format.Indent, body))
92 s.WriteString(fmt.Sprintf("%s}", format.Indent))
93
94 return s.String()
95 }
00 package matchers_test
11
22 import (
3 "io"
34 "net/http"
45 "net/http/httptest"
6 "strings"
57
68 . "github.com/onsi/ginkgo"
79 . "github.com/onsi/gomega"
810 )
911
1012 var _ = Describe("HaveHTTPStatus", func() {
11 When("ACTUAL is *http.Response", func() {
12 When("EXPECTED is integer", func() {
13 It("matches the StatusCode", func() {
13 When("EXPECTED is single integer", func() {
14 It("matches the StatusCode", func() {
15 resp := &http.Response{StatusCode: http.StatusOK}
16 Expect(resp).To(HaveHTTPStatus(http.StatusOK))
17 Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound))
18 })
19 })
20
21 When("EXPECTED is single string", func() {
22 It("matches the Status", func() {
23 resp := &http.Response{Status: "200 OK"}
24 Expect(resp).To(HaveHTTPStatus("200 OK"))
25 Expect(resp).NotTo(HaveHTTPStatus("404 Not Found"))
26 })
27 })
28
29 When("EXPECTED is empty", func() {
30 It("errors", func() {
31 failures := InterceptGomegaFailures(func() {
1432 resp := &http.Response{StatusCode: http.StatusOK}
15 Expect(resp).To(HaveHTTPStatus(http.StatusOK))
16 Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound))
17 })
18 })
19 When("EXPECTED is string", func() {
20 It("matches the Status", func() {
21 resp := &http.Response{Status: "200 OK"}
22 Expect(resp).To(HaveHTTPStatus("200 OK"))
23 Expect(resp).NotTo(HaveHTTPStatus("404 Not Found"))
24 })
25 })
26 When("EXPECTED is anything else", func() {
27 It("does not match", func() {
28 failures := InterceptGomegaFailures(func() {
29 resp := &http.Response{StatusCode: http.StatusOK}
30 Expect(resp).NotTo(HaveHTTPStatus(true))
31 })
32 Expect(failures).To(ConsistOf("HaveHTTPStatus matcher must be passed an int or a string. Got:\n <bool>: true"))
33 })
33 Expect(resp).To(HaveHTTPStatus())
34 })
35 Expect(failures).To(HaveLen(1))
36 Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed an int or a string. Got nothing"))
37 })
38 })
39
40 When("EXPECTED is not a string or integer", func() {
41 It("errors", func() {
42 failures := InterceptGomegaFailures(func() {
43 resp := &http.Response{StatusCode: http.StatusOK}
44 Expect(resp).To(HaveHTTPStatus(true))
45 })
46 Expect(failures).To(HaveLen(1))
47 Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed int or string types. Got:\n <bool>: true"))
48 })
49 })
50
51 When("EXPECTED is a list of strings and integers", func() {
52 It("matches the StatusCode and Status", func() {
53 resp := &http.Response{
54 Status: "200 OK",
55 StatusCode: http.StatusOK,
56 }
57 Expect(resp).To(HaveHTTPStatus(http.StatusOK, http.StatusNoContent, http.StatusNotFound))
58 Expect(resp).To(HaveHTTPStatus("204 Feeling Fine", "200 OK", "404 Not Found"))
59 Expect(resp).To(HaveHTTPStatus("204 Feeling Fine", http.StatusOK, "404 Not Found"))
60 Expect(resp).To(HaveHTTPStatus(http.StatusNoContent, "200 OK", http.StatusNotFound))
61 Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound, http.StatusNoContent, http.StatusGone))
62 Expect(resp).NotTo(HaveHTTPStatus("204 Feeling Fine", "201 Sleeping", "404 Not Found"))
63 Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound, "404 Not Found", http.StatusGone))
64 })
65 })
66
67 When("EXPECTED is a list containing non-string or integer types", func() {
68 It("errors", func() {
69 failures := InterceptGomegaFailures(func() {
70 resp := &http.Response{StatusCode: http.StatusOK}
71 Expect(resp).To(HaveHTTPStatus(http.StatusGone, "204 No Content", true, http.StatusNotFound))
72 })
73 Expect(failures).To(HaveLen(1))
74 Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed int or string types. Got:\n <bool>: true"))
3475 })
3576 })
3677
4283 Expect(resp).NotTo(HaveHTTPStatus(http.StatusNotFound))
4384 })
4485 })
86
4587 When("EXPECTED is string", func() {
4688 It("matches the Status", func() {
4789 resp := &httptest.ResponseRecorder{Code: http.StatusOK}
4991 Expect(resp).NotTo(HaveHTTPStatus("404 Not Found"))
5092 })
5193 })
94
5295 When("EXPECTED is anything else", func() {
5396 It("does not match", func() {
5497 failures := InterceptGomegaFailures(func() {
5598 resp := &httptest.ResponseRecorder{Code: http.StatusOK}
5699 Expect(resp).NotTo(HaveHTTPStatus(nil))
57100 })
58 Expect(failures).To(ConsistOf("HaveHTTPStatus matcher must be passed an int or a string. Got:\n <nil>: nil"))
101 Expect(failures).To(HaveLen(1))
102 Expect(failures[0]).To(Equal("HaveHTTPStatus matcher must be passed int or string types. Got:\n <nil>: nil"))
59103 })
60104 })
61105 })
65109 failures := InterceptGomegaFailures(func() {
66110 Expect("foo").To(HaveHTTPStatus(http.StatusOK))
67111 })
68 Expect(failures).To(ConsistOf("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n <string>: foo"))
112 Expect(failures).To(HaveLen(1))
113 Expect(failures[0]).To(Equal("HaveHTTPStatus matcher expects *http.Response or *httptest.ResponseRecorder. Got:\n <string>: foo"))
69114 })
70115 })
71116
72117 Describe("FailureMessage", func() {
73 It("returns message", func() {
74 failures := InterceptGomegaFailures(func() {
75 resp := &http.Response{StatusCode: http.StatusBadGateway}
118 It("returns a message for a single expected value", func() {
119 failures := InterceptGomegaFailures(func() {
120 resp := &http.Response{
121 StatusCode: http.StatusBadGateway,
122 Status: "502 Bad Gateway",
123 Body: io.NopCloser(strings.NewReader("did not like it")),
124 }
76125 Expect(resp).To(HaveHTTPStatus(http.StatusOK))
77126 })
78 Expect(failures).To(ConsistOf(MatchRegexp("Expected(.|\n)*StatusCode: 502(.|\n)*to have HTTP status\n <int>: 200")))
79 })
80 })
127 Expect(failures).To(HaveLen(1))
128 Expect(failures[0]).To(Equal(`Expected
129 <*http.Response>: {
130 Status: <string>: "502 Bad Gateway"
131 StatusCode: <int>: 502
132 Body: <string>: "did not like it"
133 }
134 to have HTTP status
135 <int>: 200`), failures[0])
136 })
137
138 It("returns a message for a multiple expected values", func() {
139 failures := InterceptGomegaFailures(func() {
140 resp := &http.Response{
141 StatusCode: http.StatusBadGateway,
142 Status: "502 Bad Gateway",
143 Body: io.NopCloser(strings.NewReader("did not like it")),
144 }
145 Expect(resp).To(HaveHTTPStatus(http.StatusOK, http.StatusNotFound, "204 No content"))
146 })
147 Expect(failures).To(HaveLen(1))
148 Expect(failures[0]).To(Equal(`Expected
149 <*http.Response>: {
150 Status: <string>: "502 Bad Gateway"
151 StatusCode: <int>: 502
152 Body: <string>: "did not like it"
153 }
154 to have HTTP status
155 <int>: 200
156 <int>: 404
157 <string>: 204 No content`), failures[0])
158 })
159 })
160
81161 Describe("NegatedFailureMessage", func() {
82 It("returns message", func() {
83 failures := InterceptGomegaFailures(func() {
84 resp := &http.Response{StatusCode: http.StatusOK}
162 It("returns a message for a single expected value", func() {
163 failures := InterceptGomegaFailures(func() {
164 resp := &http.Response{
165 StatusCode: http.StatusOK,
166 Status: "200 OK",
167 Body: io.NopCloser(strings.NewReader("got it!")),
168 }
85169 Expect(resp).NotTo(HaveHTTPStatus(http.StatusOK))
86170 })
87 Expect(failures).To(ConsistOf(MatchRegexp("Expected(.|\n)*StatusCode: 200(.|\n)*not to have HTTP status\n <int>: 200")))
171 Expect(failures).To(HaveLen(1))
172 Expect(failures[0]).To(Equal(`Expected
173 <*http.Response>: {
174 Status: <string>: "200 OK"
175 StatusCode: <int>: 200
176 Body: <string>: "got it!"
177 }
178 not to have HTTP status
179 <int>: 200`), failures[0])
180 })
181
182 It("returns a message for a multiple expected values", func() {
183 failures := InterceptGomegaFailures(func() {
184 resp := &http.Response{
185 StatusCode: http.StatusOK,
186 Status: "200 OK",
187 Body: io.NopCloser(strings.NewReader("got it!")),
188 }
189 Expect(resp).NotTo(HaveHTTPStatus(http.StatusOK, "204 No content", http.StatusGone))
190 })
191 Expect(failures).To(HaveLen(1))
192 Expect(failures[0]).To(Equal(`Expected
193 <*http.Response>: {
194 Status: <string>: "200 OK"
195 StatusCode: <int>: 200
196 Body: <string>: "got it!"
197 }
198 not to have HTTP status
199 <int>: 200
200 <string>: 204 No content
201 <int>: 410`), failures[0])
88202 })
89203 })
90204 })
00 package matchers
11
22 import (
3 "errors"
34 "fmt"
45 "reflect"
56
67 "github.com/onsi/gomega/format"
7 "golang.org/x/xerrors"
88 )
99
1010 type MatchErrorMatcher struct {
2424 expected := matcher.Expected
2525
2626 if isError(expected) {
27 return reflect.DeepEqual(actualErr, expected) || xerrors.Is(actualErr, expected.(error)), nil
27 return reflect.DeepEqual(actualErr, expected) || errors.Is(actualErr, expected.(error)), nil
2828 }
2929
3030 if isString(expected) {
66 . "github.com/onsi/ginkgo"
77 . "github.com/onsi/gomega"
88 . "github.com/onsi/gomega/matchers"
9 "golang.org/x/xerrors"
109 )
1110
1211 type CustomError struct {
3332
3433 It("should succeed when any error in the chain matches the passed error", func() {
3534 innerErr := errors.New("inner error")
36 outerErr := xerrors.Errorf("outer error wrapping: %w", innerErr)
35 outerErr := fmt.Errorf("outer error wrapping: %w", innerErr)
3736
3837 Expect(outerErr).Should(MatchError(innerErr))
3938 })
11
22 import (
33 "fmt"
4 "io/ioutil"
4 "io"
55 "os"
66 "testing"
77
3333
3434 func readFileContents(filePath string) []byte {
3535 f := openFile(filePath)
36 b, err := ioutil.ReadAll(f)
36 b, err := io.ReadAll(f)
3737 if err != nil {
3838 panic(fmt.Errorf("failed to read file contents: %v", err))
3939 }
00 package matchers
11
22 import (
3 "github.com/onsi/gomega/internal/oraclematcher"
43 "github.com/onsi/gomega/types"
54 )
65
2524 }
2625
2726 func (m *NotMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
28 return oraclematcher.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value
27 return types.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value
2928 }
33 "fmt"
44
55 "github.com/onsi/gomega/format"
6 "github.com/onsi/gomega/internal/oraclematcher"
76 "github.com/onsi/gomega/types"
87 )
98
5352
5453 if m.firstSuccessfulMatcher != nil {
5554 // one of the matchers succeeded.. it must be able to change in order to affect the result
56 return oraclematcher.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual)
55 return types.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual)
5756 } else {
5857 // so all matchers failed.. Any one of them changing would change the result.
5958 for _, matcher := range m.Matchers {
60 if oraclematcher.MatchMayChangeInTheFuture(matcher, actual) {
59 if types.MatchMayChangeInTheFuture(matcher, actual) {
6160 return true
6261 }
6362 }
0 package matchers
1
2 import (
3 "fmt"
4 "reflect"
5
6 "github.com/onsi/gomega/format"
7 )
8
9 type SatisfyMatcher struct {
10 Predicate interface{}
11
12 // cached type
13 predicateArgType reflect.Type
14 }
15
16 func NewSatisfyMatcher(predicate interface{}) *SatisfyMatcher {
17 if predicate == nil {
18 panic("predicate cannot be nil")
19 }
20 predicateType := reflect.TypeOf(predicate)
21 if predicateType.Kind() != reflect.Func {
22 panic("predicate must be a function")
23 }
24 if predicateType.NumIn() != 1 {
25 panic("predicate must have 1 argument")
26 }
27 if predicateType.NumOut() != 1 || predicateType.Out(0).Kind() != reflect.Bool {
28 panic("predicate must return bool")
29 }
30
31 return &SatisfyMatcher{
32 Predicate: predicate,
33 predicateArgType: predicateType.In(0),
34 }
35 }
36
37 func (m *SatisfyMatcher) Match(actual interface{}) (success bool, err error) {
38 // prepare a parameter to pass to the predicate
39 var param reflect.Value
40 if actual != nil && reflect.TypeOf(actual).AssignableTo(m.predicateArgType) {
41 // The dynamic type of actual is compatible with the predicate argument.
42 param = reflect.ValueOf(actual)
43
44 } else if actual == nil && m.predicateArgType.Kind() == reflect.Interface {
45 // The dynamic type of actual is unknown, so there's no way to make its
46 // reflect.Value. Create a nil of the predicate argument, which is known.
47 param = reflect.Zero(m.predicateArgType)
48
49 } else {
50 return false, fmt.Errorf("predicate expects '%s' but we have '%T'", m.predicateArgType, actual)
51 }
52
53 // call the predicate with `actual`
54 fn := reflect.ValueOf(m.Predicate)
55 result := fn.Call([]reflect.Value{param})
56 return result[0].Bool(), nil
57 }
58
59 func (m *SatisfyMatcher) FailureMessage(actual interface{}) (message string) {
60 return format.Message(actual, "to satisfy predicate", m.Predicate)
61 }
62
63 func (m *SatisfyMatcher) NegatedFailureMessage(actual interface{}) (message string) {
64 return format.Message(actual, "to not satisfy predicate", m.Predicate)
65 }
0 package matchers_test
1
2 import (
3 "errors"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 )
8
9 var _ = Describe("SatisfyMatcher", func() {
10
11 var isEven = func(x int) bool { return x%2 == 0 }
12
13 Context("Panic if predicate is invalid", func() {
14 panicsWithPredicate := func(predicate interface{}) {
15 Expect(func() { Satisfy(predicate) }).WithOffset(1).To(Panic())
16 }
17 It("nil", func() {
18 panicsWithPredicate(nil)
19 })
20 Context("Invalid number of args, but correct return value count", func() {
21 It("zero", func() {
22 panicsWithPredicate(func() int { return 5 })
23 })
24 It("two", func() {
25 panicsWithPredicate(func(i, j int) int { return 5 })
26 })
27 })
28 Context("Invalid return types, but correct number of arguments", func() {
29 It("zero", func() {
30 panicsWithPredicate(func(i int) {})
31 })
32 It("two", func() {
33 panicsWithPredicate(func(i int) (int, int) { return 5, 6 })
34 })
35 It("invalid type", func() {
36 panicsWithPredicate(func(i int) string { return "" })
37 })
38 })
39 })
40
41 When("the actual value is incompatible", func() {
42 It("fails to pass int to func(string)", func() {
43 actual, predicate := int(0), func(string) bool { return false }
44 success, err := Satisfy(predicate).Match(actual)
45 Expect(success).To(BeFalse())
46 Expect(err).To(HaveOccurred())
47 Expect(err.Error()).To(ContainSubstring("expects 'string'"))
48 Expect(err.Error()).To(ContainSubstring("have 'int'"))
49 })
50
51 It("fails to pass string to func(interface)", func() {
52 actual, predicate := "bang", func(error) bool { return false }
53 success, err := Satisfy(predicate).Match(actual)
54 Expect(success).To(BeFalse())
55 Expect(err).To(HaveOccurred())
56 Expect(err.Error()).To(ContainSubstring("expects 'error'"))
57 Expect(err.Error()).To(ContainSubstring("have 'string'"))
58 })
59
60 It("fails to pass nil interface to func(int)", func() {
61 actual, predicate := error(nil), func(int) bool { return false }
62 success, err := Satisfy(predicate).Match(actual)
63 Expect(success).To(BeFalse())
64 Expect(err).To(HaveOccurred())
65 Expect(err.Error()).To(ContainSubstring("expects 'int'"))
66 Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
67 })
68
69 It("fails to pass nil interface to func(pointer)", func() {
70 actual, predicate := error(nil), func(*string) bool { return false }
71 success, err := Satisfy(predicate).Match(actual)
72 Expect(success).To(BeFalse())
73 Expect(err).To(HaveOccurred())
74 Expect(err.Error()).To(ContainSubstring("expects '*string'"))
75 Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
76 })
77 })
78
79 It("works with positive cases", func() {
80 Expect(2).To(Satisfy(isEven))
81
82 // transform expects interface
83 takesError := func(error) bool { return true }
84 Expect(nil).To(Satisfy(takesError), "handles nil actual values")
85 Expect(errors.New("abc")).To(Satisfy(takesError))
86 })
87
88 It("works with negative cases", func() {
89 Expect(1).ToNot(Satisfy(isEven))
90 })
91
92 Context("failure messages", func() {
93 When("match fails", func() {
94 It("gives a descriptive message", func() {
95 m := Satisfy(isEven)
96 Expect(m.Match(1)).To(BeFalse())
97 Expect(m.FailureMessage(1)).To(ContainSubstring("Expected\n <int>: 1\nto satisfy predicate\n <func(int) bool>: "))
98 })
99 })
100
101 When("match succeeds, but expected it to fail", func() {
102 It("gives a descriptive message", func() {
103 m := Not(Satisfy(isEven))
104 Expect(m.Match(2)).To(BeFalse())
105 Expect(m.FailureMessage(2)).To(ContainSubstring("Expected\n <int>: 2\nto not satisfy predicate\n <func(int) bool>: "))
106 })
107 })
108
109 Context("actual value is incompatible with predicate's argument type", func() {
110 It("gracefully fails", func() {
111 m := Satisfy(isEven)
112 result, err := m.Match("hi") // give it a string but predicate expects int; doesn't panic
113 Expect(result).To(BeFalse())
114 Expect(err).To(MatchError("predicate expects 'int' but we have 'string'"))
115 })
116 })
117 })
118 })
33 "fmt"
44 "reflect"
55
6 "github.com/onsi/gomega/internal/oraclematcher"
76 "github.com/onsi/gomega/types"
87 )
98
109 type WithTransformMatcher struct {
1110 // input
12 Transform interface{} // must be a function of one parameter that returns one value
11 Transform interface{} // must be a function of one parameter that returns one value and an optional error
1312 Matcher types.GomegaMatcher
1413
1514 // cached value
1918 transformedValue interface{}
2019 }
2120
21 // reflect.Type for error
22 var errorT = reflect.TypeOf((*error)(nil)).Elem()
23
2224 func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher) *WithTransformMatcher {
2325 if transform == nil {
2426 panic("transform function cannot be nil")
2729 if txType.NumIn() != 1 {
2830 panic("transform function must have 1 argument")
2931 }
30 if txType.NumOut() != 1 {
31 panic("transform function must have 1 return value")
32 if numout := txType.NumOut(); numout != 1 {
33 if numout != 2 || !txType.Out(1).AssignableTo(errorT) {
34 panic("transform function must either have 1 return value, or 1 return value plus 1 error value")
35 }
3236 }
3337
3438 return &WithTransformMatcher{
3943 }
4044
4145 func (m *WithTransformMatcher) Match(actual interface{}) (bool, error) {
42 // return error if actual's type is incompatible with Transform function's argument type
43 actualType := reflect.TypeOf(actual)
44 if !actualType.AssignableTo(m.transformArgType) {
45 return false, fmt.Errorf("Transform function expects '%s' but we have '%s'", m.transformArgType, actualType)
46 // prepare a parameter to pass to the Transform function
47 var param reflect.Value
48 if actual != nil && reflect.TypeOf(actual).AssignableTo(m.transformArgType) {
49 // The dynamic type of actual is compatible with the transform argument.
50 param = reflect.ValueOf(actual)
51
52 } else if actual == nil && m.transformArgType.Kind() == reflect.Interface {
53 // The dynamic type of actual is unknown, so there's no way to make its
54 // reflect.Value. Create a nil of the transform argument, which is known.
55 param = reflect.Zero(m.transformArgType)
56
57 } else {
58 return false, fmt.Errorf("Transform function expects '%s' but we have '%T'", m.transformArgType, actual)
4659 }
4760
4861 // call the Transform function with `actual`
4962 fn := reflect.ValueOf(m.Transform)
50 result := fn.Call([]reflect.Value{reflect.ValueOf(actual)})
63 result := fn.Call([]reflect.Value{param})
64 if len(result) == 2 {
65 if !result[1].IsNil() {
66 return false, fmt.Errorf("Transform function failed: %s", result[1].Interface().(error).Error())
67 }
68 }
5169 m.transformedValue = result[0].Interface() // expect exactly one value
5270
5371 return m.Matcher.Match(m.transformedValue)
6785 // Querying the next matcher is fine if the transformer always will return the same value.
6886 // But if the transformer is non-deterministic and returns a different value each time, then there
6987 // is no point in querying the next matcher, since it can only comment on the last transformed value.
70 return oraclematcher.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue)
88 return types.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue)
7189 }
1313
1414 Context("Panic if transform function invalid", func() {
1515 panicsWithTransformer := func(transform interface{}) {
16 ExpectWithOffset(1, func() { WithTransform(transform, nil) }).To(Panic())
16 Expect(func() { WithTransform(transform, nil) }).WithOffset(1).To(Panic())
1717 }
1818 It("nil", func() {
1919 panicsWithTransformer(nil)
3434 panicsWithTransformer(func(i int) (int, int) { return 5, 6 })
3535 })
3636 })
37 Context("Invalid number of return values, but correct number of arguments", func() {
38 It("Two return values, but second return value not an error", func() {
39 panicsWithTransformer(func(interface{}) (int, int) { return 5, 6 })
40 })
41 })
42 })
43
44 When("the actual value is incompatible", func() {
45 It("fails to pass int to func(string)", func() {
46 actual, transform := int(0), func(string) int { return 0 }
47 success, err := WithTransform(transform, Equal(0)).Match(actual)
48 Expect(success).To(BeFalse())
49 Expect(err).To(HaveOccurred())
50 Expect(err.Error()).To(ContainSubstring("function expects 'string'"))
51 Expect(err.Error()).To(ContainSubstring("have 'int'"))
52 })
53
54 It("fails to pass string to func(interface)", func() {
55 actual, transform := "bang", func(error) int { return 0 }
56 success, err := WithTransform(transform, Equal(0)).Match(actual)
57 Expect(success).To(BeFalse())
58 Expect(err).To(HaveOccurred())
59 Expect(err.Error()).To(ContainSubstring("function expects 'error'"))
60 Expect(err.Error()).To(ContainSubstring("have 'string'"))
61 })
62
63 It("fails to pass nil interface to func(int)", func() {
64 actual, transform := error(nil), func(int) int { return 0 }
65 success, err := WithTransform(transform, Equal(0)).Match(actual)
66 Expect(success).To(BeFalse())
67 Expect(err).To(HaveOccurred())
68 Expect(err.Error()).To(ContainSubstring("function expects 'int'"))
69 Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
70 })
71
72 It("fails to pass nil interface to func(pointer)", func() {
73 actual, transform := error(nil), func(*string) int { return 0 }
74 success, err := WithTransform(transform, Equal(0)).Match(actual)
75 Expect(success).To(BeFalse())
76 Expect(err).To(HaveOccurred())
77 Expect(err.Error()).To(ContainSubstring("function expects '*string'"))
78 Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
79 })
3780 })
3881
3982 It("works with positive cases", func() {
5093 Expect(S{1, "hi"}).To(WithTransform(transformer, Equal("hi")))
5194
5295 // transform expects interface
53 errString := func(e error) string { return e.Error() }
96 errString := func(e error) string {
97 if e == nil {
98 return "safe"
99 }
100 return e.Error()
101 }
102 Expect(nil).To(WithTransform(errString, Equal("safe")), "handles nil actual values")
54103 Expect(errors.New("abc")).To(WithTransform(errString, Equal("abc")))
55104 })
56105
73122 m := Not(WithTransform(plus1, Equal(3)))
74123 Expect(m.Match(2)).To(BeFalse())
75124 Expect(m.FailureMessage(2)).To(Equal("Expected\n <int>: 3\nnot to equal\n <int>: 3"))
125 })
126 })
127
128 When("transform fails", func() {
129 It("reports the transformation error", func() {
130 actual, trafo := "foo", func(string) (string, error) { return "", errors.New("that does not transform") }
131 success, err := WithTransform(trafo, Equal(actual)).Match(actual)
132 Expect(success).To(BeFalse())
133 Expect(err).To(HaveOccurred())
134 Expect(err.Error()).To(MatchRegexp(": that does not transform$"))
76135 })
77136 })
78137
338338 return &matchers.HaveKeyWithValueMatcher{
339339 Key: key,
340340 Value: value,
341 }
342 }
343
344 //HaveField succeeds if actual is a struct and the value at the passed in field
345 //matches the passed in matcher. By default HaveField used Equal() to perform the match,
346 //however a matcher can be passed in in stead.
347 //
348 //The field must be a string that resolves to the name of a field in the struct. Structs can be traversed
349 //using the '.' delimiter. If the field ends with '()' a method named field is assumed to exist on the struct and is invoked.
350 //Such methods must take no arguments and return a single value:
351 //
352 // type Book struct {
353 // Title string
354 // Author Person
355 // }
356 // type Person struct {
357 // FirstName string
358 // LastName string
359 // DOB time.Time
360 // }
361 // Expect(book).To(HaveField("Title", "Les Miserables"))
362 // Expect(book).To(HaveField("Title", ContainSubstring("Les"))
363 // Expect(book).To(HaveField("Person.FirstName", Equal("Victor"))
364 // Expect(book).To(HaveField("Person.DOB.Year()", BeNumerically("<", 1900))
365 func HaveField(field string, expected interface{}) types.GomegaMatcher {
366 return &matchers.HaveFieldMatcher{
367 Field: field,
368 Expected: expected,
341369 }
342370 }
343371
422450 //Expected must be either an int or a string.
423451 // Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) // asserts that resp.StatusCode == 200
424452 // Expect(resp).Should(HaveHTTPStatus("404 Not Found")) // asserts that resp.Status == "404 Not Found"
425 func HaveHTTPStatus(expected interface{}) types.GomegaMatcher {
453 // Expect(resp).Should(HaveHTTPStatus(http.StatusOK, http.StatusNoContent)) // asserts that resp.StatusCode == 200 || resp.StatusCode == 204
454 func HaveHTTPStatus(expected ...interface{}) types.GomegaMatcher {
426455 return &matchers.HaveHTTPStatusMatcher{Expected: expected}
456 }
457
458 // HaveHTTPHeaderWithValue succeeds if the header is found and the value matches.
459 // Actual must be either a *http.Response or *httptest.ResponseRecorder.
460 // Expected must be a string header name, followed by a header value which
461 // can be a string, or another matcher.
462 func HaveHTTPHeaderWithValue(header string, value interface{}) types.GomegaMatcher {
463 return &matchers.HaveHTTPHeaderWithValueMatcher{
464 Header: header,
465 Value: value,
466 }
467 }
468
469 // HaveHTTPBody matches if the body matches.
470 // Actual must be either a *http.Response or *httptest.ResponseRecorder.
471 // Expected must be either a string, []byte, or other matcher
472 func HaveHTTPBody(expected interface{}) types.GomegaMatcher {
473 return &matchers.HaveHTTPBodyMatcher{Expected: expected}
427474 }
428475
429476 //And succeeds only if all of the given matchers succeed.
465512 }
466513
467514 //WithTransform applies the `transform` to the actual value and matches it against `matcher`.
468 //The given transform must be a function of one parameter that returns one value.
515 //The given transform must be either a function of one parameter that returns one value or a
516 // function of one parameter that returns two values, where the second value must be of the
517 // error type.
469518 // var plus1 = func(i int) int { return i + 1 }
470519 // Expect(1).To(WithTransform(plus1, Equal(2))
520 //
521 // var failingplus1 = func(i int) (int, error) { return 42, "this does not compute" }
522 // Expect(1).To(WithTransform(failingplus1, Equal(2)))
471523 //
472524 //And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
473525 func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
474526 return matchers.NewWithTransformMatcher(transform, matcher)
475527 }
528
529 //Satisfy matches the actual value against the `predicate` function.
530 //The given predicate must be a function of one paramter that returns bool.
531 // var isEven = func(i int) bool { return i%2 == 0 }
532 // Expect(2).To(Satisfy(isEven))
533 func Satisfy(predicate interface{}) types.GomegaMatcher {
534 return matchers.NewSatisfyMatcher(predicate)
535 }
00 package types
11
2 type TWithHelper interface {
3 Helper()
4 }
2 import (
3 "time"
4 )
55
66 type GomegaFailHandler func(message string, callerSkip ...int)
77
8 type GomegaFailWrapper struct {
9 Fail GomegaFailHandler
10 TWithHelper TWithHelper
8 //A simple *testing.T interface wrapper
9 type GomegaTestingT interface {
10 Helper()
11 Fatalf(format string, args ...interface{})
1112 }
1213
13 //A simple *testing.T interface wrapper
14 type GomegaTestingT interface {
15 Fatalf(format string, args ...interface{})
14 // Gomega represents an object that can perform synchronous and assynchronous assertions with Gomega matchers
15 type Gomega interface {
16 Ω(actual interface{}, extra ...interface{}) Assertion
17 Expect(actual interface{}, extra ...interface{}) Assertion
18 ExpectWithOffset(offset int, actual interface{}, extra ...interface{}) Assertion
19
20 Eventually(actual interface{}, intervals ...interface{}) AsyncAssertion
21 EventuallyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion
22
23 Consistently(actual interface{}, intervals ...interface{}) AsyncAssertion
24 ConsistentlyWithOffset(offset int, actual interface{}, intervals ...interface{}) AsyncAssertion
25
26 SetDefaultEventuallyTimeout(time.Duration)
27 SetDefaultEventuallyPollingInterval(time.Duration)
28 SetDefaultConsistentlyDuration(time.Duration)
29 SetDefaultConsistentlyPollingInterval(time.Duration)
1630 }
1731
1832 //All Gomega matchers must implement the GomegaMatcher interface
2337 FailureMessage(actual interface{}) (message string)
2438 NegatedFailureMessage(actual interface{}) (message string)
2539 }
40
41 /*
42 GomegaMatchers that also match the OracleMatcher interface can convey information about
43 whether or not their result will change upon future attempts.
44
45 This allows `Eventually` and `Consistently` to short circuit if success becomes impossible.
46
47 For example, a process' exit code can never change. So, gexec's Exit matcher returns `true`
48 for `MatchMayChangeInTheFuture` until the process exits, at which point it returns `false` forevermore.
49 */
50 type OracleMatcher interface {
51 MatchMayChangeInTheFuture(actual interface{}) bool
52 }
53
54 func MatchMayChangeInTheFuture(matcher GomegaMatcher, value interface{}) bool {
55 oracleMatcher, ok := matcher.(OracleMatcher)
56 if !ok {
57 return true
58 }
59
60 return oracleMatcher.MatchMayChangeInTheFuture(value)
61 }
62
63 // AsyncAssertions are returned by Eventually and Consistently and enable matchers to be polled repeatedly to ensure
64 // they are eventually satisfied
65 type AsyncAssertion interface {
66 Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool
67 ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool
68
69 WithOffset(offset int) AsyncAssertion
70 WithTimeout(interval time.Duration) AsyncAssertion
71 WithPolling(interval time.Duration) AsyncAssertion
72 }
73
74 // Assertions are returned by Ω and Expect and enable assertions against Gomega matchers
75 type Assertion interface {
76 Should(matcher GomegaMatcher, optionalDescription ...interface{}) bool
77 ShouldNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool
78
79 To(matcher GomegaMatcher, optionalDescription ...interface{}) bool
80 ToNot(matcher GomegaMatcher, optionalDescription ...interface{}) bool
81 NotTo(matcher GomegaMatcher, optionalDescription ...interface{}) bool
82
83 WithOffset(offset int) Assertion
84
85 Error() Assertion
86 }