Codebase list golang-gomega / 93775fb
Import upstream version 1.18.1 Debian Janitor 2 years ago
155 changed file(s) with 12829 addition(s) and 2194 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 build:
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 - run: go mod tidy && git diff --exit-code go.mod go.sum
17 - run: go vet ./...
18 - run: mv ./tools ./tools.go
19 - run: go get github.com/onsi/ginkgo/v2/ginkgo
20 - run: go run github.com/onsi/ginkgo/v2/ginkgo -r --randomize-all --randomize-suites --race --trace --fail-on-pending --keep-going
+0
-17
.travis.yml less more
0 language: go
1
2 go:
3 - 1.14.x
4 - 1.15.x
5 - gotip
6
7 env:
8 - GO111MODULE=on
9
10 install:
11 - go get -v ./...
12 - go build ./...
13 - go get github.com/onsi/ginkgo
14 - go install github.com/onsi/ginkgo/ginkgo
15
16 script: make test
0 ## 1.18.1
1
2 ## Fixes
3 - Add pointer support to HaveField matcher (#495) [79e41a3]
4
5 ## 1.18.0
6
7 ## Features
8 - Docs now live on the master branch in the docs folder which will make for easier PRs. The docs also use Ginkgo 2.0's new docs html/css/js. [2570272]
9 - New HaveValue matcher can handle actuals that are either values (in which case they are passed on unscathed) or pointers (in which case they are indirected). [Docs here.](https://onsi.github.io/gomega/#working-with-values) (#485) [bdc087c]
10 - Gmeasure has been declared GA [360db9d]
11
12 ## Fixes
13 - Gomega now uses ioutil for Go 1.15 and lower (#492) - official support is only for the most recent two major versions of Go but this will unblock users who need to stay on older unsupported versions of Go. [c29c1c0]
14
15 ## Maintenace
16 - Remove Travis workflow (#491) [72e6040]
17 - Upgrade to Ginkgo 2.0.0 GA [f383637]
18 - chore: fix description of HaveField matcher (#487) [2b4b2c0]
19 - use tools.go to ensure Ginkgo cli dependencies are included [f58a52b]
20 - remove dockerfile and simplify github actions to match ginkgo's actions [3f8160d]
21
22 ## 1.17.0
23
24 ### Features
25 - Add HaveField matcher [3a26311]
26 - add Error() assertions on the final error value of multi-return values (#480) [2f96943]
27 - separate out offsets and timeouts (#478) [18a4723]
28 - fix transformation error reporting (#479) [e001fab]
29 - allow transform functions to report errors (#472) [bf93408]
30
31 ### Fixes
32 Stop using deprecated ioutil package (#467) [07f405d]
33
34 ## 1.16.0
35
36 ### Features
37 - feat: HaveHTTPStatus multiple expected values (#465) [aa69f1b]
38 - feat: HaveHTTPHeaderWithValue() matcher (#463) [dd83a96]
39 - feat: HaveHTTPBody matcher (#462) [504e1f2]
40 - feat: formatter for HTTP responses (#461) [e5b3157]
41
42 ## 1.15.0
43
44 ### Fixes
45 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.
46
47 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.
48
49 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.
50
51 ## 1.14.0
52
53 ### Features
54 - gmeasure.SamplingConfig now suppers a MinSamplingInterval [e94dbca]
55 - Eventually and Consistently support functions that make assertions [2f04e6e]
56 - Eventually and Consistently now allow their passed-in functions to make assertions.
57 These assertions must pass or the function is considered to have failed and is retried.
58 - Eventually and Consistently can now take functions with no return values. These implicitly return nil
59 if they contain no failed assertion. Otherwise they return an error wrapping the first assertion failure. This allows
60 these functions to be used with the Succeed() matcher.
61 - Introduce InterceptGomegaFailure - an analogue to InterceptGomegaFailures - that captures the first assertion failure
62 and halts execution in its passed-in callback.
63
64 ### Fixes
65 - Call Verify GHTTPWithGomega receiver funcs (#454) [496e6fd]
66 - Build a binary with an expected name (#446) [7356360]
67
68 ## 1.13.0
69
70 ### Features
71 - gmeasure provides BETA support for benchmarking (#447) [8f2dfbf]
72 - Set consistently and eventually defaults on init (#443) [12eb778]
73
74 ## 1.12.0
75
76 ### Features
77 - Add Satisfy() matcher (#437) [c548f31]
78 - tweak truncation message [3360b8c]
79 - Add format.GomegaStringer (#427) [cc80b6f]
80 - Add Clear() method to gbytes.Buffer [c3c0920]
81
82 ### Fixes
83 - Fix error message in BeNumericallyMatcher (#432) [09c074a]
84 - Bump github.com/onsi/ginkgo from 1.12.1 to 1.16.2 (#442) [e5f6ea0]
85 - Bump github.com/golang/protobuf from 1.4.3 to 1.5.2 (#431) [adae3bf]
86 - Bump golang.org/x/net (#441) [3275b35]
87
88 ## 1.11.0
89
90 ### Features
91 - feature: add index to gstruct element func (#419) [334e00d]
92 - feat(gexec) Add CompileTest functions. Close #410 (#411) [47c613f]
93
94 ### Fixes
95 - Check more carefully for nils in WithTransform (#423) [3c60a15]
96 - fix: typo in Makefile [b82522a]
97 - Allow WithTransform function to accept a nil value (#422) [b75d2f2]
98 - fix: print value type for interface{} containers (#409) [f08e2dc]
99 - fix(BeElementOf): consistently flatten expected values [1fa9468]
100
101 ## 1.10.5
102
103 ### Fixes
104 - fix: collections matchers should display type of expectation (#408) [6b4eb5a]
105 - fix(ContainElements): consistently flatten expected values [073b880]
106 - fix(ConsistOf): consistently flatten expected values [7266efe]
107
108 ## 1.10.4
109
110 ### Fixes
111 - update golang net library to more recent version without vulnerability (#406) [817a8b9]
112 - Correct spelling: alloted -> allotted (#403) [0bae715]
113 - fix a panic in MessageWithDiff with long message (#402) [ea06b9b]
114
0115 ## 1.10.3
1116
2117 ### Fixes
+0
-6
Makefile less more
0 test:
1 [ -z "`gofmt -s -w -l -e .`" ]
2 go vet
3 ginkgo -p -r --randomizeAllSpecs --failOnPending --randomizeSuites --race
4
5 .PHONY: test
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 _site
1 .sass-cache
2 .jekyll-cache
3 .jekyll-metadata
4 vendor
0 source "https://rubygems.org"
1 # Hello! This is where you manage which Jekyll version is used to run.
2 # When you want to use a different version, change it below, save the
3 # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
4 #
5 # bundle exec jekyll serve
6 #
7 # This will help ensure the proper Jekyll version is running.
8 # Happy Jekylling!
9 # gem "jekyll", "~> 4.2.1"
10 # This is the default theme for new Jekyll sites. You may change this to anything you like.
11 gem "minima", "~> 2.5"
12 # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
13 # uncomment the line below. To upgrade, run `bundle update github-pages`.
14 gem "github-pages", "~> 219", group: :jekyll_plugins
15 # If you have any plugins, put them here!
16 group :jekyll_plugins do
17 gem "jekyll-feed", "~> 0.12"
18 end
19
20 gem "webrick"
21
22 # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
23 # and associated library.
24 platforms :mingw, :x64_mingw, :mswin, :jruby do
25 gem "tzinfo", "~> 1.2"
26 gem "tzinfo-data"
27 end
28
29 # Performance-booster for watching directories on Windows
30 gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
31
0 GEM
1 remote: https://rubygems.org/
2 specs:
3 activesupport (6.0.4.1)
4 concurrent-ruby (~> 1.0, >= 1.0.2)
5 i18n (>= 0.7, < 2)
6 minitest (~> 5.1)
7 tzinfo (~> 1.1)
8 zeitwerk (~> 2.2, >= 2.2.2)
9 addressable (2.8.0)
10 public_suffix (>= 2.0.2, < 5.0)
11 coffee-script (2.4.1)
12 coffee-script-source
13 execjs
14 coffee-script-source (1.11.1)
15 colorator (1.1.0)
16 commonmarker (0.17.13)
17 ruby-enum (~> 0.5)
18 concurrent-ruby (1.1.9)
19 dnsruby (1.61.7)
20 simpleidn (~> 0.1)
21 em-websocket (0.5.2)
22 eventmachine (>= 0.12.9)
23 http_parser.rb (~> 0.6.0)
24 ethon (0.14.0)
25 ffi (>= 1.15.0)
26 eventmachine (1.2.7)
27 execjs (2.8.1)
28 faraday (1.8.0)
29 faraday-em_http (~> 1.0)
30 faraday-em_synchrony (~> 1.0)
31 faraday-excon (~> 1.1)
32 faraday-httpclient (~> 1.0.1)
33 faraday-net_http (~> 1.0)
34 faraday-net_http_persistent (~> 1.1)
35 faraday-patron (~> 1.0)
36 faraday-rack (~> 1.0)
37 multipart-post (>= 1.2, < 3)
38 ruby2_keywords (>= 0.0.4)
39 faraday-em_http (1.0.0)
40 faraday-em_synchrony (1.0.0)
41 faraday-excon (1.1.0)
42 faraday-httpclient (1.0.1)
43 faraday-net_http (1.0.1)
44 faraday-net_http_persistent (1.2.0)
45 faraday-patron (1.0.0)
46 faraday-rack (1.0.0)
47 ffi (1.15.4)
48 forwardable-extended (2.6.0)
49 gemoji (3.0.1)
50 github-pages (219)
51 github-pages-health-check (= 1.17.7)
52 jekyll (= 3.9.0)
53 jekyll-avatar (= 0.7.0)
54 jekyll-coffeescript (= 1.1.1)
55 jekyll-commonmark-ghpages (= 0.1.6)
56 jekyll-default-layout (= 0.1.4)
57 jekyll-feed (= 0.15.1)
58 jekyll-gist (= 1.5.0)
59 jekyll-github-metadata (= 2.13.0)
60 jekyll-mentions (= 1.6.0)
61 jekyll-optional-front-matter (= 0.3.2)
62 jekyll-paginate (= 1.1.0)
63 jekyll-readme-index (= 0.3.0)
64 jekyll-redirect-from (= 0.16.0)
65 jekyll-relative-links (= 0.6.1)
66 jekyll-remote-theme (= 0.4.3)
67 jekyll-sass-converter (= 1.5.2)
68 jekyll-seo-tag (= 2.7.1)
69 jekyll-sitemap (= 1.4.0)
70 jekyll-swiss (= 1.0.0)
71 jekyll-theme-architect (= 0.2.0)
72 jekyll-theme-cayman (= 0.2.0)
73 jekyll-theme-dinky (= 0.2.0)
74 jekyll-theme-hacker (= 0.2.0)
75 jekyll-theme-leap-day (= 0.2.0)
76 jekyll-theme-merlot (= 0.2.0)
77 jekyll-theme-midnight (= 0.2.0)
78 jekyll-theme-minimal (= 0.2.0)
79 jekyll-theme-modernist (= 0.2.0)
80 jekyll-theme-primer (= 0.6.0)
81 jekyll-theme-slate (= 0.2.0)
82 jekyll-theme-tactile (= 0.2.0)
83 jekyll-theme-time-machine (= 0.2.0)
84 jekyll-titles-from-headings (= 0.5.3)
85 jemoji (= 0.12.0)
86 kramdown (= 2.3.1)
87 kramdown-parser-gfm (= 1.1.0)
88 liquid (= 4.0.3)
89 mercenary (~> 0.3)
90 minima (= 2.5.1)
91 nokogiri (>= 1.10.4, < 2.0)
92 rouge (= 3.26.0)
93 terminal-table (~> 1.4)
94 github-pages-health-check (1.17.7)
95 addressable (~> 2.3)
96 dnsruby (~> 1.60)
97 octokit (~> 4.0)
98 public_suffix (>= 3.0, < 5.0)
99 typhoeus (~> 1.3)
100 html-pipeline (2.14.0)
101 activesupport (>= 2)
102 nokogiri (>= 1.4)
103 http_parser.rb (0.6.0)
104 i18n (0.9.5)
105 concurrent-ruby (~> 1.0)
106 jekyll (3.9.0)
107 addressable (~> 2.4)
108 colorator (~> 1.0)
109 em-websocket (~> 0.5)
110 i18n (~> 0.7)
111 jekyll-sass-converter (~> 1.0)
112 jekyll-watch (~> 2.0)
113 kramdown (>= 1.17, < 3)
114 liquid (~> 4.0)
115 mercenary (~> 0.3.3)
116 pathutil (~> 0.9)
117 rouge (>= 1.7, < 4)
118 safe_yaml (~> 1.0)
119 jekyll-avatar (0.7.0)
120 jekyll (>= 3.0, < 5.0)
121 jekyll-coffeescript (1.1.1)
122 coffee-script (~> 2.2)
123 coffee-script-source (~> 1.11.1)
124 jekyll-commonmark (1.3.1)
125 commonmarker (~> 0.14)
126 jekyll (>= 3.7, < 5.0)
127 jekyll-commonmark-ghpages (0.1.6)
128 commonmarker (~> 0.17.6)
129 jekyll-commonmark (~> 1.2)
130 rouge (>= 2.0, < 4.0)
131 jekyll-default-layout (0.1.4)
132 jekyll (~> 3.0)
133 jekyll-feed (0.15.1)
134 jekyll (>= 3.7, < 5.0)
135 jekyll-gist (1.5.0)
136 octokit (~> 4.2)
137 jekyll-github-metadata (2.13.0)
138 jekyll (>= 3.4, < 5.0)
139 octokit (~> 4.0, != 4.4.0)
140 jekyll-mentions (1.6.0)
141 html-pipeline (~> 2.3)
142 jekyll (>= 3.7, < 5.0)
143 jekyll-optional-front-matter (0.3.2)
144 jekyll (>= 3.0, < 5.0)
145 jekyll-paginate (1.1.0)
146 jekyll-readme-index (0.3.0)
147 jekyll (>= 3.0, < 5.0)
148 jekyll-redirect-from (0.16.0)
149 jekyll (>= 3.3, < 5.0)
150 jekyll-relative-links (0.6.1)
151 jekyll (>= 3.3, < 5.0)
152 jekyll-remote-theme (0.4.3)
153 addressable (~> 2.0)
154 jekyll (>= 3.5, < 5.0)
155 jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
156 rubyzip (>= 1.3.0, < 3.0)
157 jekyll-sass-converter (1.5.2)
158 sass (~> 3.4)
159 jekyll-seo-tag (2.7.1)
160 jekyll (>= 3.8, < 5.0)
161 jekyll-sitemap (1.4.0)
162 jekyll (>= 3.7, < 5.0)
163 jekyll-swiss (1.0.0)
164 jekyll-theme-architect (0.2.0)
165 jekyll (> 3.5, < 5.0)
166 jekyll-seo-tag (~> 2.0)
167 jekyll-theme-cayman (0.2.0)
168 jekyll (> 3.5, < 5.0)
169 jekyll-seo-tag (~> 2.0)
170 jekyll-theme-dinky (0.2.0)
171 jekyll (> 3.5, < 5.0)
172 jekyll-seo-tag (~> 2.0)
173 jekyll-theme-hacker (0.2.0)
174 jekyll (> 3.5, < 5.0)
175 jekyll-seo-tag (~> 2.0)
176 jekyll-theme-leap-day (0.2.0)
177 jekyll (> 3.5, < 5.0)
178 jekyll-seo-tag (~> 2.0)
179 jekyll-theme-merlot (0.2.0)
180 jekyll (> 3.5, < 5.0)
181 jekyll-seo-tag (~> 2.0)
182 jekyll-theme-midnight (0.2.0)
183 jekyll (> 3.5, < 5.0)
184 jekyll-seo-tag (~> 2.0)
185 jekyll-theme-minimal (0.2.0)
186 jekyll (> 3.5, < 5.0)
187 jekyll-seo-tag (~> 2.0)
188 jekyll-theme-modernist (0.2.0)
189 jekyll (> 3.5, < 5.0)
190 jekyll-seo-tag (~> 2.0)
191 jekyll-theme-primer (0.6.0)
192 jekyll (> 3.5, < 5.0)
193 jekyll-github-metadata (~> 2.9)
194 jekyll-seo-tag (~> 2.0)
195 jekyll-theme-slate (0.2.0)
196 jekyll (> 3.5, < 5.0)
197 jekyll-seo-tag (~> 2.0)
198 jekyll-theme-tactile (0.2.0)
199 jekyll (> 3.5, < 5.0)
200 jekyll-seo-tag (~> 2.0)
201 jekyll-theme-time-machine (0.2.0)
202 jekyll (> 3.5, < 5.0)
203 jekyll-seo-tag (~> 2.0)
204 jekyll-titles-from-headings (0.5.3)
205 jekyll (>= 3.3, < 5.0)
206 jekyll-watch (2.2.1)
207 listen (~> 3.0)
208 jemoji (0.12.0)
209 gemoji (~> 3.0)
210 html-pipeline (~> 2.2)
211 jekyll (>= 3.0, < 5.0)
212 kramdown (2.3.1)
213 rexml
214 kramdown-parser-gfm (1.1.0)
215 kramdown (~> 2.0)
216 liquid (4.0.3)
217 listen (3.7.0)
218 rb-fsevent (~> 0.10, >= 0.10.3)
219 rb-inotify (~> 0.9, >= 0.9.10)
220 mercenary (0.3.6)
221 minima (2.5.1)
222 jekyll (>= 3.5, < 5.0)
223 jekyll-feed (~> 0.9)
224 jekyll-seo-tag (~> 2.1)
225 minitest (5.14.4)
226 multipart-post (2.1.1)
227 nokogiri (1.12.5-arm64-darwin)
228 racc (~> 1.4)
229 nokogiri (1.12.5-x86_64-darwin)
230 racc (~> 1.4)
231 octokit (4.21.0)
232 faraday (>= 0.9)
233 sawyer (~> 0.8.0, >= 0.5.3)
234 pathutil (0.16.2)
235 forwardable-extended (~> 2.6)
236 public_suffix (4.0.6)
237 racc (1.5.2)
238 rb-fsevent (0.11.0)
239 rb-inotify (0.10.1)
240 ffi (~> 1.0)
241 rexml (3.2.5)
242 rouge (3.26.0)
243 ruby-enum (0.9.0)
244 i18n
245 ruby2_keywords (0.0.5)
246 rubyzip (2.3.2)
247 safe_yaml (1.0.5)
248 sass (3.7.4)
249 sass-listen (~> 4.0.0)
250 sass-listen (4.0.0)
251 rb-fsevent (~> 0.9, >= 0.9.4)
252 rb-inotify (~> 0.9, >= 0.9.7)
253 sawyer (0.8.2)
254 addressable (>= 2.3.5)
255 faraday (> 0.8, < 2.0)
256 simpleidn (0.2.1)
257 unf (~> 0.1.4)
258 terminal-table (1.8.0)
259 unicode-display_width (~> 1.1, >= 1.1.1)
260 thread_safe (0.3.6)
261 typhoeus (1.4.0)
262 ethon (>= 0.9.0)
263 tzinfo (1.2.9)
264 thread_safe (~> 0.1)
265 unf (0.1.4)
266 unf_ext
267 unf_ext (0.0.8)
268 unicode-display_width (1.8.0)
269 webrick (1.7.0)
270 zeitwerk (2.4.2)
271
272 PLATFORMS
273 arm64-darwin-21
274 x86_64-darwin-19
275
276 DEPENDENCIES
277 github-pages (~> 219)
278 jekyll-feed (~> 0.12)
279 minima (~> 2.5)
280 tzinfo (~> 1.2)
281 tzinfo-data
282 wdm (~> 0.1.1)
283 webrick
284
285 BUNDLED WITH
286 2.2.31
0 baseurl: "/gomega" # the subpath of your site, e.g. /blog
1
2 # Build settings
3 name: Gomega
4 markdown: GFM
5 highlighter: rouge
6 lsi: false
7 exclude:
8 - "*.go"
0 <!doctype html>
1 <html lang="en">
2 <head>
3 <meta charset="utf-8">
4 <meta name="viewport" content="width=device-width, initial-scale=1">
5
6 <link rel="stylesheet" href="./css/primer-minimal.css">
7 <link rel="stylesheet" href="./css/layout.css">
8 </head>
9 <body>
10 <div id="container">
11 <div id="left-background"></div>
12 <div id="right-background"></div>
13 <div id="header">
14 <a class="brand" href="#top">Gomega</a>
15 <div class="spacer"></div>
16 <a href="https://github.com/onsi/gomega"><img class="logo" src="./images/github.png"></a>
17 <a href="https://github.com/sponsors/onsi"><img class="logo" src="./images/sponsor.png"></a>
18 <a href="https://pkg.go.dev/github.com/onsi/gomega"><img class="logo" src="./images/go.png"></a>
19 <div id="disclosure">
20 <div class="hamburger-slice"></div>
21 <div class="hamburger-slice"></div>
22 <div class="hamburger-slice"></div>
23 </div>
24 </div>
25 <div id="sidebar"></div>
26 <div id="mask"></div>
27 <div id="content" class="markdown-body">
28 {{ content }}
29 </div>
30 </div>
31
32 <script src="https://cdnjs.cloudflare.com/ajax/libs/anchor-js/4.1.0/anchor.min.js" integrity="sha256-lZaRhKri35AyJSypXXs4o6OPFTbTmUoltBbDCbdzegg=" crossorigin="anonymous"></script>
33 <script>anchors.add();</script>
34
35 <script src="./js/docs.js"></script>
36 </body>
37 </html>
0 :root {
1 --max-width: 1024px;
2 --header-height: 50px;
3 --breakpoint: 640px;
4 --gomega-blue: #2d6cad;
5 --gomega-blue-darker: #0d5c8d;
6 --gomega-blue-faint: #fafaff;
7 }
8
9 body {
10 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
11 font-size: 14px;
12 margin: 0;
13 }
14
15 #header {
16 background-color: var(--gomega-blue);
17 color: #fff;
18 border-bottom: solid 1px var(--gomega-blue-darker);
19 }
20
21 #left-background, #right-background {
22 height: 100vh;
23 background-color: var(--gomega-blue-faint);
24 }
25
26 #left-background {
27 box-shadow: inset -2px 0 7px -5px var(--gomega-blue-darker);
28 }
29
30 #right-background {
31 box-shadow: inset 2px 0 7px -5px var(--gomega-blue-darker);
32 }
33
34 #content {
35 background-color: #fff;
36 overflow-y: scroll;
37 padding: 5px;
38 padding-right:10px;
39 }
40
41 #sidebar {
42 background-color: #fff;
43 overflow-y: scroll;
44 padding:5px;
45 position:relative;
46 font-size: 16px;
47 }
48
49 #mask {
50 display:none;
51 }
52
53 img[alt="Ginkgo"] {
54 max-width: 80%;
55 max-height: 200px;
56 display: block;
57 margin-left: auto;
58 margin-right: auto;
59 }
60
61 /* code styling */
62
63 .markdown-body div.highlight {
64 margin-left: 20px;
65 margin-right: 20px;
66 border-radius: 10px;
67 margin-bottom: 16px !important;
68 max-width: 800px;
69 border: solid 2px #f0f0f0;
70 }
71
72 .markdown-body .highlight pre, .markdown-body pre {
73 background-color: transparent;
74 }
75
76 div.highlight.invalid {
77 border: solid 2px #f0aaaa;
78 background-color: #f0dddd;
79 }
80
81 /* sidebar items */
82
83 .sidebar-heading, .sidebar-item {
84 display:block;
85 text-decoration: none;
86 color: #000;
87 }
88
89 .sidebar-heading {
90 margin:5px 5px;
91 padding:3px 0;
92 }
93
94 .sidebar-heading.active {
95 font-weight:bold;
96 color: var(--gomega-blue);
97 }
98
99 .sidebar-item {
100 font-size: 0.8rem;
101 margin: 5px 0 5px 10px;
102 }
103
104 .sidebar-item:nth-child(2n+1) {
105 color:#555;
106 }
107
108 .sidebar-item.active {
109 font-weight:bold;
110 color: var(--gomega-blue);
111 }
112
113 .sidebar-section {
114 overflow: hidden;
115 max-height: 0;
116 // transition: max-height 0.3s;
117 }
118
119 .sidebar-heading.active + .sidebar-section {
120 max-height: 1000px;
121 }
122
123 /* header */
124
125 #header {
126 display: flex;
127 align-items: center;
128 }
129
130 .brand {
131 font-size: 1.8rem;
132 margin-left:10px;
133 font-weight: bold;
134 text-decoration: none;
135 color: #fff;
136 }
137
138 .spacer {
139 flex: 1;
140 }
141
142 .logo {
143 max-height: 22px;
144 margin: 0 5px;
145 }
146
147 a:last-of-type .logo {
148 margin-right: 10px;
149 }
150
151 #disclosure {
152 box-sizing: border-box;
153 width: var(--header-height);
154 height: var(--header-height);
155
156 display: flex;
157 flex-direction: column;
158 align-items: center;
159 justify-content: space-around;
160 padding: 12px 0;
161 cursor: pointer;
162 }
163
164 .hamburger-slice {
165 width: calc(var(--header-height) - 20px);
166 height: 2px;
167 border-radius: 4px;
168 background-color: #fff;
169 }
170
171 /* content styling */
172
173 #content h2 {
174 border-bottom: none;
175 }
176
177 /* Desktop */
178 @media screen and (min-width: 640px) {
179 #container {
180 --sidebar-width: 200px;
181
182 display: grid;
183 grid-template-areas: "left-background header header right-background"
184 "left-background sidebar content right-background";
185 grid-template-columns: 1fr var(--sidebar-width) minmax(calc(var(--breakpoint) - var(--sidebar-width)), var(--max-width)) 1fr;
186 grid-template-rows: var(--header-height) auto;
187 gap: 0;
188
189 height:100vh;
190 }
191
192 #left-background {
193 grid-area; left-background;
194 }
195
196 #right-background {
197 grid-area; right-background;
198 }
199
200 #header {
201 grid-area: header;
202 }
203
204 #sidebar {
205 grid-area: sidebar;
206 border-right: 5px solid #fff;
207 }
208
209 #content {
210 grid-area: content;
211 }
212
213 #disclosure {
214 display: none;
215 }
216 }
217
218 /* Mobile */
219 @media screen and (max-width: 640px) {
220 #container {
221 --sidebar-width: 300px;
222
223 display: grid;
224 grid-template-areas: "header"
225 "content";
226 grid-template-columns: 1fr;
227 grid-template-rows: var(--header-height) auto;
228 gap: 0;
229
230 height:100vh;
231 }
232
233 #header {
234 grid-area: header;
235 }
236
237 #content {
238 grid-area: content;
239 }
240
241 #left-background {
242 display: none;
243 }
244
245 #right-background {
246 display: none;
247 }
248
249 #sidebar {
250 position: fixed;
251 width: var(--sidebar-width);
252 top: var(--header-height);
253 bottom: 0px;
254 right: calc(-5px - var(--sidebar-width));
255 z-index:2;
256 transition: right 0.3s;
257 }
258
259 #mask {
260 background-color: rgba(0,0,0,0);
261 position: fixed;
262 width: 100vw;
263 top: var(--header-height);
264 bottom: 0px;
265 left:0px;
266 z-index:1;
267 cursor: pointer;
268 transition: background-color 0.3s;
269 }
270
271 #container.reveal-sidebar #sidebar {
272 right: 0px;
273 }
274
275 #container.reveal-sidebar #mask {
276 display:block;
277 background-color: rgba(0,0,0,0.2);
278 }
279
280 .hamburger-slice {
281 transition: transform 0.3s, opacity 0.1s;
282 }
283
284 #container .hamburger-slice:nth-child(1) {
285 transform: rotate(0deg);
286 transform-origin: top left;
287 }
288
289 #container .hamburger-slice:nth-child(2) {
290 opacity: 100;
291 }
292
293 #container .hamburger-slice:nth-child(3) {
294 transform: rotate(0deg);
295 transform-origin: bottom left;
296 }
297
298
299 #container.reveal-sidebar .hamburger-slice:nth-child(1) {
300 transform: rotate(35deg);
301 }
302
303 #container.reveal-sidebar .hamburger-slice:nth-child(2) {
304 opacity: 0;
305 }
306
307 #container.reveal-sidebar .hamburger-slice:nth-child(3) {
308 transform: rotate(-35deg);
309 }
310 }
311
312 /* Print */
313 @media print {
314 body {
315 font-size: 10pt;
316 margin: 0.25in;
317 background-image: none;
318 }
319 #header {
320 display: none;
321 }
322 #sidebar {
323 display: none;
324 }
325 #left-background {
326 display: none;
327 }
328 #right-background {
329 display: none;
330 }
331
332 h2 {
333 break-before: page;
334 }
335
336 .highlight {
337 break-inside: avoid-page;
338 }
339 }
0 .markdown-body {
1 line-height: 1.5;
2 word-wrap: break-word;
3 }
4
5 .markdown-body > * :first-child {
6 margin-top: 0 !important;
7 }
8
9 .markdown-body > * :last-child {
10 margin-bottom: 0 !important;
11 }
12
13 .markdown-body a {
14 color: #000;
15 text-decoration: underline;
16 }
17
18 .markdown-body a:visited {
19 color: #333;
20 text-decoration: underline;
21 }
22
23 .markdown-body .anchorjs-link {
24 text-decoration: none;
25 }
26
27 .markdown-body .absent {
28 color: #cb2431;
29 }
30
31 .markdown-body .anchor {
32 float: left;
33 padding-right: 4px;
34 margin-left: -20px;
35 line-height: 1;
36 }
37
38 .markdown-body .anchor:focus {
39 outline: none;
40 }
41
42 .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre {
43 margin-top: 0;
44 margin-bottom: 16px;
45 }
46
47 .markdown-body hr {
48 height: 0.25em;
49 padding: 0;
50 margin: 24px 0;
51 background-color: #e1e4e8;
52 border: 0;
53 }
54
55 .markdown-body blockquote {
56 padding: 0 1em;
57 color: #6a737d;
58 border-left: 0.25em solid #dfe2e5;
59 }
60
61 .markdown-body blockquote > :first-child {
62 margin-top: 0;
63 }
64
65 .markdown-body blockquote > :last-child {
66 margin-bottom: 0;
67 }
68
69 .markdown-body kbd {
70 display: inline-block;
71 padding: 3px 5px;
72 font-size: 11px;
73 line-height: 10px;
74 color: #444d56;
75 vertical-align: middle;
76 background-color: #fafbfc;
77 border: solid 1px #c6cbd1;
78 border-bottom-color: #959da5;
79 border-radius: 3px;
80 box-shadow: inset 0 -1px 0 #959da5;
81 }
82
83 .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 {
84 margin-top: 24px;
85 margin-bottom: 16px;
86 font-weight: 600;
87 line-height: 1.25;
88 }
89
90 .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link {
91 color: #1b1f23;
92 vertical-align: middle;
93 visibility: hidden;
94 }
95
96 .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor {
97 text-decoration: none;
98 }
99
100 .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link {
101 visibility: visible;
102 }
103
104 .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code {
105 font-size: inherit;
106 }
107
108 .markdown-body h1 {
109 padding-bottom: 0.3em;
110 font-size: 2em;
111 border-bottom: 1px solid #eaecef;
112 }
113
114 .markdown-body h2 {
115 padding-bottom: 0.3em;
116 font-size: 1.5em;
117 border-bottom: 1px solid #eaecef;
118 }
119
120 .markdown-body h3 {
121 font-size: 1.25em;
122 }
123
124 .markdown-body h4 {
125 font-size: 1em;
126 }
127
128 .markdown-body h5 {
129 font-size: 0.875em;
130 }
131
132 .markdown-body h6 {
133 font-size: 0.85em;
134 color: #6a737d;
135 }
136
137 .markdown-body ul, .markdown-body ol {
138 padding-left: 2em;
139 }
140
141 .markdown-body ul.no-list, .markdown-body ol.no-list {
142 padding: 0;
143 list-style-type: none;
144 }
145
146 .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul {
147 margin-top: 0;
148 margin-bottom: 0;
149 }
150
151 .markdown-body li {
152 word-wrap: break-all;
153 }
154
155 .markdown-body li > p {
156 margin-top: 16px;
157 }
158
159 .markdown-body li + li {
160 margin-top: 0.25em;
161 }
162
163 .markdown-body dl {
164 padding: 0;
165 }
166
167 .markdown-body dl dt {
168 padding: 0;
169 margin-top: 16px;
170 font-size: 1em;
171 font-style: italic;
172 font-weight: 600;
173 }
174
175 .markdown-body dl dd {
176 padding: 0 16px;
177 margin-bottom: 16px;
178 }
179
180 .markdown-body table {
181 display: block;
182 width: 100%;
183 overflow: auto;
184 }
185
186 .markdown-body table th {
187 font-weight: 600;
188 }
189
190 .markdown-body table th, .markdown-body table td {
191 padding: 6px 13px;
192 border: 1px solid #dfe2e5;
193 }
194
195 .markdown-body table tr {
196 background-color: #fff;
197 border-top: 1px solid #c6cbd1;
198 }
199
200 .markdown-body table tr:nth-child(2n) {
201 background-color: #f6f8fa;
202 }
203
204 .markdown-body table img {
205 background-color: transparent;
206 }
207
208 .markdown-body img {
209 max-width: 100%;
210 box-sizing: content-box;
211 background-color: #fff;
212 }
213
214 .markdown-body img[align=right] {
215 padding-left: 20px;
216 }
217
218 .markdown-body img[align=left] {
219 padding-right: 20px;
220 }
221
222 .markdown-body .emoji {
223 max-width: none;
224 vertical-align: text-top;
225 background-color: transparent;
226 }
227
228 .markdown-body span.frame {
229 display: block;
230 overflow: hidden;
231 }
232
233 .markdown-body span.frame > span {
234 display: block;
235 float: left;
236 width: auto;
237 padding: 7px;
238 margin: 13px 0 0;
239 overflow: hidden;
240 border: 1px solid #dfe2e5;
241 }
242
243 .markdown-body span.frame span img {
244 display: block;
245 float: left;
246 }
247
248 .markdown-body span.frame span span {
249 display: block;
250 padding: 5px 0 0;
251 clear: both;
252 color: #24292e;
253 }
254
255 .markdown-body span.align-center {
256 display: block;
257 overflow: hidden;
258 clear: both;
259 }
260
261 .markdown-body span.align-center > span {
262 display: block;
263 margin: 13px auto 0;
264 overflow: hidden;
265 text-align: center;
266 }
267
268 .markdown-body span.align-center span img {
269 margin: 0 auto;
270 text-align: center;
271 }
272
273 .markdown-body span.align-right {
274 display: block;
275 overflow: hidden;
276 clear: both;
277 }
278
279 .markdown-body span.align-right > span {
280 display: block;
281 margin: 13px 0 0;
282 overflow: hidden;
283 text-align: right;
284 }
285
286 .markdown-body span.align-right span img {
287 margin: 0;
288 text-align: right;
289 }
290
291 .markdown-body span.float-left {
292 display: block;
293 float: left;
294 margin-right: 13px;
295 overflow: hidden;
296 }
297
298 .markdown-body span.float-left span {
299 margin: 13px 0 0;
300 }
301
302 .markdown-body span.float-right {
303 display: block;
304 float: right;
305 margin-left: 13px;
306 overflow: hidden;
307 }
308
309 .markdown-body span.float-right > span {
310 display: block;
311 margin: 13px auto 0;
312 overflow: hidden;
313 text-align: right;
314 }
315
316 .markdown-body code, .markdown-body tt {
317 padding: 0.2em 0.4em;
318 margin: 0;
319 font-size: 85%;
320 background-color: rgba(27, 31, 35, 0.05);
321 border-radius: 3px;
322 }
323
324 .markdown-body code br, .markdown-body tt br {
325 display: none;
326 }
327
328 .markdown-body del code {
329 text-decoration: inherit;
330 }
331
332 .markdown-body pre {
333 word-wrap: normal;
334 }
335
336 .markdown-body pre > code {
337 padding: 0;
338 margin: 0;
339 font-size: 100%;
340 word-break: normal;
341 white-space: pre;
342 background: transparent;
343 border: 0;
344 }
345
346 .markdown-body .highlight {
347 margin-bottom: 16px;
348 }
349
350 .markdown-body .highlight pre {
351 margin-bottom: 0;
352 word-break: normal;
353 }
354
355 .markdown-body .highlight pre, .markdown-body pre {
356 padding: 16px;
357 overflow: auto;
358 font-size: 85%;
359 line-height: 1.45;
360 background-color: #f6f8fa;
361 border-radius: 3px;
362 }
363
364 .markdown-body pre code, .markdown-body pre tt {
365 display: inline;
366 max-width: auto;
367 padding: 0;
368 margin: 0;
369 overflow: visible;
370 line-height: inherit;
371 word-wrap: normal;
372 background-color: transparent;
373 border: 0;
374 }
375
376 .markdown-body .csv-data td, .markdown-body .csv-data th {
377 padding: 5px;
378 overflow: hidden;
379 font-size: 12px;
380 line-height: 1;
381 text-align: left;
382 white-space: nowrap;
383 }
384
385 .markdown-body .csv-data .blob-num {
386 padding: 10px 8px 9px;
387 text-align: right;
388 background: #fff;
389 border: 0;
390 }
391
392 .markdown-body .csv-data tr {
393 border-top: 0;
394 }
395
396 .markdown-body .csv-data th {
397 font-weight: 600;
398 background: #f6f8fa;
399 border-top: 0;
400 }
401
402 .highlight table td {
403 padding: 5px;
404 }
405
406 .highlight table pre {
407 margin: 0;
408 }
409
410 .highlight .cm {
411 color: #999988;
412 font-style: italic;
413 }
414
415 .highlight .cp {
416 color: #999999;
417 font-weight: bold;
418 }
419
420 .highlight .c1 {
421 color: #999988;
422 font-style: italic;
423 }
424
425 .highlight .cs {
426 color: #999999;
427 font-weight: bold;
428 font-style: italic;
429 }
430
431 .highlight .c, .highlight .cd {
432 color: #999988;
433 font-style: italic;
434 }
435
436 .highlight .err {
437 color: #a61717;
438 background-color: #e3d2d2;
439 }
440
441 .highlight .gd {
442 color: #000000;
443 background-color: #ffdddd;
444 }
445
446 .highlight .ge {
447 color: #000000;
448 font-style: italic;
449 }
450
451 .highlight .gr {
452 color: #aa0000;
453 }
454
455 .highlight .gh {
456 color: #999999;
457 }
458
459 .highlight .gi {
460 color: #000000;
461 background-color: #ddffdd;
462 }
463
464 .highlight .go {
465 color: #888888;
466 }
467
468 .highlight .gp {
469 color: #555555;
470 }
471
472 .highlight .gs {
473 font-weight: bold;
474 }
475
476 .highlight .gu {
477 color: #aaaaaa;
478 }
479
480 .highlight .gt {
481 color: #aa0000;
482 }
483
484 .highlight .kc {
485 color: #000000;
486 font-weight: bold;
487 }
488
489 .highlight .kd {
490 color: #000000;
491 font-weight: bold;
492 }
493
494 .highlight .kn {
495 color: #000000;
496 font-weight: bold;
497 }
498
499 .highlight .kp {
500 color: #000000;
501 font-weight: bold;
502 }
503
504 .highlight .kr {
505 color: #000000;
506 font-weight: bold;
507 }
508
509 .highlight .kt {
510 color: #445588;
511 font-weight: bold;
512 }
513
514 .highlight .k, .highlight .kv {
515 color: #000000;
516 font-weight: bold;
517 }
518
519 .highlight .mf {
520 color: #009999;
521 }
522
523 .highlight .mh {
524 color: #009999;
525 }
526
527 .highlight .il {
528 color: #009999;
529 }
530
531 .highlight .mi {
532 color: #009999;
533 }
534
535 .highlight .mo {
536 color: #009999;
537 }
538
539 .highlight .m, .highlight .mb, .highlight .mx {
540 color: #009999;
541 }
542
543 .highlight .sb {
544 color: #d14;
545 }
546
547 .highlight .sc {
548 color: #d14;
549 }
550
551 .highlight .sd {
552 color: #d14;
553 }
554
555 .highlight .s2 {
556 color: #d14;
557 }
558
559 .highlight .se {
560 color: #d14;
561 }
562
563 .highlight .sh {
564 color: #d14;
565 }
566
567 .highlight .si {
568 color: #d14;
569 }
570
571 .highlight .sx {
572 color: #d14;
573 }
574
575 .highlight .sr {
576 color: #009926;
577 }
578
579 .highlight .s1 {
580 color: #d14;
581 }
582
583 .highlight .ss {
584 color: #990073;
585 }
586
587 .highlight .s {
588 color: #d14;
589 }
590
591 .highlight .na {
592 color: #008080;
593 }
594
595 .highlight .bp {
596 color: #999999;
597 }
598
599 .highlight .nb {
600 color: #0086B3;
601 }
602
603 .highlight .nc {
604 color: #445588;
605 font-weight: bold;
606 }
607
608 .highlight .no {
609 color: #008080;
610 }
611
612 .highlight .nd {
613 color: #3c5d5d;
614 font-weight: bold;
615 }
616
617 .highlight .ni {
618 color: #800080;
619 }
620
621 .highlight .ne {
622 color: #990000;
623 font-weight: bold;
624 }
625
626 .highlight .nf {
627 color: #990000;
628 font-weight: bold;
629 }
630
631 .highlight .nl {
632 color: #990000;
633 font-weight: bold;
634 }
635
636 .highlight .nn {
637 color: #555555;
638 }
639
640 .highlight .nt {
641 color: #000080;
642 }
643
644 .highlight .vc {
645 color: #008080;
646 }
647
648 .highlight .vg {
649 color: #008080;
650 }
651
652 .highlight .vi {
653 color: #008080;
654 }
655
656 .highlight .nv {
657 color: #008080;
658 }
659
660 .highlight .ow {
661 color: #000000;
662 font-weight: bold;
663 }
664
665 .highlight .o {
666 color: #000000;
667 font-weight: bold;
668 }
669
670 .highlight .w {
671 color: #bbbbbb;
672 }
673
674 .highlight {
675 background-color: #f8f8f8;
676 }
Binary diff not shown
Binary diff not shown
Binary diff not shown
Binary diff not shown
0 ---
1 layout: default
2 title: Gomega
3 ---
4 {% raw %}
5 ![Gomega](./images/gomega.png)
6
7 [Gomega](http://github.com/onsi/gomega) is a matcher/assertion library. It is best paired with the [Ginkgo](http://github.com/onsi/ginkgo) BDD test framework, but can be adapted for use in other contexts too.
8
9 ## Support Policy
10
11 Gomega provides support for versions of Go that are noted by the [Go release policy](https://golang.org/doc/devel/release.html#policy) i.e. N and N-1 major versions.
12
13 ## Getting Gomega
14
15 Just `go get` it:
16
17 ```bash
18 $ go get github.com/onsi/gomega/...
19 ```
20
21 ## Getting Gomega as needed
22
23 Instead of getting all of Gomega and it's dependency tree, you can use the go command to get the dependencies as needed.
24
25 For example, import gomega in your test code:
26
27 ```go
28 import "github.com/onsi/gomega"
29 ```
30
31 Use `go get -t` to retrieve the packages referenced in your test code:
32
33 ```bash
34 $ cd /path/to/my/app
35 $ go get -t ./...
36 ```
37
38 ## Using Gomega with Ginkgo
39
40 When a Gomega assertion fails, Gomega calls a `GomegaFailHandler`. This is a function that you must provide using `gomega.RegisterFailHandler()`.
41
42 If you're using Ginkgo, all you need to do is:
43
44 ```go
45 gomega.RegisterFailHandler(ginkgo.Fail)
46 ```
47
48 before you start your test suite.
49
50 If you use the `ginkgo` CLI to `ginkgo bootstrap` a test suite, this hookup will be automatically generated for you.
51
52 > `GomegaFailHandler` is defined in the `types` subpackage.
53
54 ## Using Gomega with Golang's XUnit-style Tests
55
56 Though Gomega is tailored to work best with Ginkgo it is easy to use Gomega with Golang's XUnit style tests. Here's how:
57
58 To use Gomega with Golang's XUnit style tests:
59
60 ```go
61 func TestFarmHasCow(t *testing.T) {
62 g := NewGomegaWithT(t)
63
64 f := farm.New([]string{"Cow", "Horse"})
65 g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow")
66 }
67 ```
68
69 `NewGomegaWithT(t)` wraps a `*testing.T` and returns a struct that supports `Expect`, `Eventually`, and `Consistently`.
70
71 ## Making Assertions
72
73 Gomega provides two notations for making assertions. These notations are functionally equivalent and their differences are purely aesthetic.
74
75 - When you use the `Ω` notation, your assertions look like this:
76
77 ```go
78 Ω(ACTUAL).Should(Equal(EXPECTED))
79 Ω(ACTUAL).ShouldNot(Equal(EXPECTED))
80 ```
81
82 - When you use the `Expect` notation, your assertions look like this:
83
84 ```go
85 Expect(ACTUAL).To(Equal(EXPECTED))
86 Expect(ACTUAL).NotTo(Equal(EXPECTED))
87 Expect(ACTUAL).ToNot(Equal(EXPECTED))
88 ```
89
90 On OS X the `Ω` character should be easy to type, it is usually just option-z: `⌥z`.
91
92 On the left hand side, you can pass anything you want in to `Ω` and `Expect` for `ACTUAL`. On the right hand side you must pass an object that satisfies the `GomegaMatcher` interface. Gomega's matchers (e.g. `Equal(EXPECTED)`) are simply functions that create and initialize an appropriate `GomegaMatcher` object.
93
94 > Note that `Should` and `To` are just syntactic sugar and are functionally identical. Same is the case for `ToNot` and `NotTo`.
95
96 > The `GomegaMatcher` interface is pretty simple and is discussed in the [custom matchers](#adding-your-own-matchers) section. It is defined in the `types` subpackage.
97
98 ### Handling Errors
99
100 It is a common pattern, in Golang, for functions and methods to return two things - a value and an error. For example:
101
102 ```go
103 func DoSomethingHard() (string, error) {
104 ...
105 }
106 ```
107
108 To assert on the return value of such a method you might write a test that looks like this:
109
110 ```go
111 result, err := DoSomethingHard()
112 Ω(err).ShouldNot(HaveOccurred())
113 Ω(result).Should(Equal("foo"))
114 ```
115
116 Gomega streamlines this very common use case. Both `Ω` and `Expect` accept *multiple* arguments. The first argument is passed to the matcher, and the match only succeeds if *all* subsequent arguments are `nil` or zero-valued. With this, we can rewrite the above example as:
117
118 ```go
119 Ω(DoSomethingHard()).Should(Equal("foo"))
120 ```
121
122 This will only pass if the return value of `DoSomethingHard()` is `("foo", nil)`.
123
124 Additionally, if you call a function with a single `error` return value you can use the `Succeed` matcher to assert the function has returned without error. So for a function of the form:
125
126 ```go
127 func DoSomethingSimple() error {
128 ...
129 }
130 ```
131
132 You can either write:
133
134 ```go
135 err := DoSomethingSimple()
136 Ω(err).ShouldNot(HaveOccurred())
137 ```
138
139 Or you can write:
140
141 ```go
142 Ω(DoSomethingSimple()).Should(Succeed())
143 ```
144
145 > You should not use a function with multiple return values (like `DoSomethingHard`) with `Succeed`. Matchers are only passed the *first* value provided to `Ω`/`Expect`, the subsequent arguments are handled by `Ω` and `Expect` as outlined above. As a result of this behavior `Ω(DoSomethingHard()).ShouldNot(Succeed())` would never pass.
146
147 Assertions about errors on functions with multiple return values can be made as follows (and in a lazy way when not asserting that all other return values are zero values):
148
149 ```go
150 _, _, _, err := MUltipleReturnValuesFunc()
151 Ω(err).Should(HaveOccurred())
152 ```
153
154 Alternatively, such error assertions on multi return value functions can be simplified by chaining `Error` to `Ω` and `Expect`. Doing so will additionally automatically assert that all return values, except for the trailing error return value, are in fact zero values:
155
156 ```go
157 Ω(MultipleReturnValuesFunc()).Error().Should(HaveOccurred())
158 ```
159
160 Similar, asserting that no error occured is supported, too (where the other return values are allowed to take on any value):
161
162 ```go
163 Ω(MultipleReturnValuesFunc()).Error().ShouldNot(HaveOccured())
164 ```
165
166 ### Annotating Assertions
167
168 You can annotate any assertion by passing either a format string (and optional inputs to format) or a function of type `func() string` after the `GomegaMatcher`:
169
170 ```go
171 Ω(ACTUAL).Should(Equal(EXPECTED), "My annotation %d", foo)
172 Ω(ACTUAL).ShouldNot(Equal(EXPECTED), "My annotation %d", foo)
173 Expect(ACTUAL).To(Equal(EXPECTED), "My annotation %d", foo)
174 Expect(ACTUAL).NotTo(Equal(EXPECTED), "My annotation %d", foo)
175 Expect(ACTUAL).ToNot(Equal(EXPECTED), "My annotation %d", foo)
176 Expect(ACTUAL).To(Equal(EXPECTED), func() string { return "My annotation" })
177 ```
178
179 If you pass a format string, the format string and inputs will be passed to `fmt.Sprintf(...)`.
180 If you instead pass a function, the function will be lazily evaluated if the assertion fails.
181 In both cases, if the assertion fails, Gomega will print your annotation alongside its standard failure message.
182
183 This is useful in cases where the standard failure message lacks context. For example, if the following assertion fails:
184
185 ```go
186 Ω(SprocketsAreLeaky()).Should(BeFalse())
187 ```
188
189 Gomega will output:
190
191 ```
192 Expected
193 <bool>: true
194 to be false
195 ```
196
197 But this assertion:
198
199 ```go
200 Ω(SprocketsAreLeaky()).Should(BeFalse(), "Sprockets shouldn't leak")
201 ```
202
203 Will offer the more helpful output:
204
205 ```
206 Sprockets shouldn't leak
207 Expected
208 <bool>: true
209 to be false
210 ```
211
212
213 ### Adjusting Output
214
215 When a failure occurs, Gomega prints out a recursive description of the objects involved in the failed assertion. This output can be very verbose, but Gomega's philosophy is to give as much output as possible to aid in identifying the root cause of a test failure.
216
217 These recursive object renditions are performed by the `format` subpackage. `format` provides some globally adjustable settings to tune Gomega's output:
218
219 - `format.MaxLength = 4000`: Gomega will recursively traverse nested data structures as it produces output. If the length of this string representation is more than MaxLength, it will be truncated to MaxLength. To disable this behavior, set the MaxLength to `0`.
220 - `format.MaxDepth = 10`: Gomega will recursively traverse nested data structures as it produces output. By default the maximum depth of this recursion is set to `10` you can adjust this to see deeper or shallower representations of objects.
221 - Implementing `format.GomegaStringer`: If `GomegaStringer` interface is implemented on an object, Gomega will call `GomegaString` for an object's string representation. This is regardless of the `format.UseStringerRepresentation` value. Best practice to implement this interface is to implement it in a helper test file (e.g. `helper_test.go`) to avoid leaking it to your package's exported API.
222 - `format.UseStringerRepresentation = false`: Gomega does *not* call `String` or `GoString` on objects that satisfy the `Stringer` and `GoStringer` interfaces. Oftentimes such representations, while more human readable, do not contain all the relevant information associated with an object thereby making it harder to understand why a test might be failing. If you'd rather see the output of `String` or `GoString` set this property to `true`.
223
224
225 > For a tricky example of why `format.UseStringerRepresentation = false` is your friend, check out issue [#37](https://github.com/onsi/gomega/issues/37).
226
227 - `format.PrintContextObjects = false`: Gomega by default will not print the content of objects satisfying the context.Context interface, due to too much output. If you want to enable displaying that content, set this property to `true`.
228
229 If you want to use Gomega's recursive object description in your own code you can call into the `format` package directly:
230
231 ```go
232 fmt.Println(format.Object(theThingYouWantToPrint, 1))
233 ```
234
235 - `format.TruncatedDiff = true`: Gomega will truncate long strings and only show where they differ. You can set this to `false` if
236 you want to see the full strings.
237
238 ## Making Asynchronous Assertions
239
240 Gomega has support for making *asynchronous* assertions. There are two functions that provide this support: `Eventually` and `Consistently`.
241
242 ### Eventually
243
244 `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:
245
246 ```go
247 Eventually(ACTUAL, (TIMEOUT), (POLLING_INTERVAL)).Should(MATCHER)
248 ```
249
250 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 (e.g. "100ms") or `float64` (in which case they are interpreted as seconds).
251
252 > As with synchronous assertions, you can annotate asynchronous assertions by passing either a format string and optional inputs or a function of type `func() string` after the `GomegaMatcher`.
253
254 Alternatively, the timeout and polling interval can also be specified by chaining `WithTimeout` and `WithPolling` to `Eventually`:
255
256 ```go
257 Eventually(ACTUAL).WithTimeout(TIMEOUT).WithPolling(POLLING_INTERVAL).Should(MATCHER)
258 ```
259
260 Eventually works with any Gomega compatible matcher and supports making assertions against three categories of `ACTUAL` value:
261
262 #### Category 1: Making `Eventually` assertions on values
263
264 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:
265
266 ```go
267 c := make(chan bool)
268 go DoStuff(c)
269 Eventually(c, "50ms").Should(BeClosed())
270 ```
271
272 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.
273
274 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:
275
276 ```go
277 Eventually(session).Should(gexec.Exit(0))
278 ```
279
280 And the `gomega/gbytes` package allows you to monitor a streaming `*gbytes.Buffer` until a given string is seen:
281
282 ```go
283 Eventually(buffer).Should(gbytes.Say("hello there"))
284 ```
285
286 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:
287
288 ```go
289 /* === INVALID === */
290 var s *string
291 go mutateStringEventually(s)
292 Eventually(s).Should(Equal("I've changed"))
293 ```
294
295 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.
296
297 Similarly, something like `Eventually(slice).Should(HaveLen(N))` probably won't do what you think it should -- `Eventually` will be passed a pointer to the slice, yes, but if the slice is being `append`ed to (as in: `slice = append(slice, ...)`) Go will generate a new pointer and the pointer passed to `Eventually` will not contain the new elements.
298
299 In both cases you should always pass `Eventually` a function that, when polled, returns the latest value of the object in question in a thread-safe way.
300
301 #### Category 2: Making `Eventually` assertions on functions
302
303 `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.
304
305 For example:
306
307 ```go
308 Eventually(func() int {
309 return client.FetchCount()
310 }).Should(BeNumerically(">=", 17))
311 ```
312
313 will repeatedly poll `client.FetchCount` until the `BeNumerically` matcher is satisfied.
314
315 > Note that this example could have been written as `Eventually(client.FetchCount).Should(BeNumerically(">=", 17))`
316
317 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 pattern in Go.
318
319 For example, consider a method that returns a value and an error:
320
321 ```go
322 func FetchFromDB() (string, error)
323 ```
324
325 Then
326
327 ```go
328 Eventually(FetchFromDB).Should(Equal("got it"))
329 ```
330
331 will pass only if and when the returned error is `nil` *and* the returned string satisfies the matcher.
332
333 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.
334
335 #### Category 3: Making assertions _in_ the function passed into `Eventually`
336
337 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**.
338
339 Here's an example that makes some asssertions and returns a value and error:
340
341 ```go
342 Eventually(func(g Gomega) (Widget, error) {
343 ids, err := client.FetchIDs()
344 g.Expect(err).NotTo(HaveOccurred())
345 g.Expect(ids).To(ContainElement(1138))
346 return client.FetchWidget(1138)
347 }).Should(Equal(expectedWidget))
348 ```
349
350 will pass only if all the assertions in the polled function pass and the return value satisfied the matcher. Note that the assertions in the body of the polled function must be performed using the passed-in `g Gomega` object. If you use the global DSL expectations, `Eventually` will not intercept any failures and the test will fail.
351
352 `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.
353
354 For example:
355
356 ```go
357 Eventually(func(g Gomega) {
358 model, err := client.Find(1138)
359 g.Expect(err).NotTo(HaveOccurred())
360 g.Expect(model.Reticulate()).To(Succeed())
361 g.Expect(model.IsReticulated()).To(BeTrue())
362 g.Expect(model.Save()).To(Succeed())
363 }).Should(Succeed())
364 ```
365
366 will rerun the function until all assertions pass.
367
368
369 ### Consistently
370
371 `Consistently` checks that an assertion passes for a period of time. It does this by polling its argument repeatedly during the period. It fails if the matcher ever fails during that period.
372
373 For example:
374
375 ```go
376 Consistently(func() []int {
377 return thing.MemoryUsage()
378 }).Should(BeNumerically("<", 10))
379 ```
380
381 `Consistently` will poll the passed in function repeatedly and check the return value against the `GomegaMatcher`. `Consistently` blocks and only returns when the desired duration has elapsed or if the matcher fails. The default value for the wait-duration is 100 milliseconds. The default polling interval is 10 milliseconds. Like `Eventually`, you can change these values by passing them in just after your function:
382
383 ```go
384 Consistently(ACTUAL, DURATION, POLLING_INTERVAL).Should(MATCHER)
385 ```
386
387 As with `Eventually`, these can be `time.Duration`s, string representations of a `time.Duration` (e.g. `"200ms"`) or `float64`s that are interpreted as seconds.
388
389 Also as with `Eventually`, `Consistently` supports chaining `WithTimeout` and `WithPolling` in the form of:
390
391 ```go
392 Consistently(ACTUAL).WithTimeout(DURATION).WithPolling(POLLING_INTERVAL).Should(MATCHER)
393 ```
394
395 `Consistently` tries to capture the notion that something "does not eventually" happen. A common use-case is to assert that no goroutine writes to a channel for a period of time. If you pass `Consistently` an argument that is not a function, it simply passes that argument to the matcher. So we can assert that:
396
397 ```go
398 Consistently(channel).ShouldNot(Receive())
399 ```
400
401 To assert that nothing gets sent to a channel.
402
403 As with `Eventually`, you can also pass `Consistently` a function. In fact, `Consistently` works with the three categories of `ACTUAL` value outlined for `Eventually` in the section above.
404
405 > Developers often try to use `runtime.Gosched()` to nudge background goroutines to run. This can lead to flaky tests as it is not deterministic that a given goroutine will run during the `Gosched`. `Consistently` is particularly handy in these cases: it polls for 100ms which is typically more than enough time for all your Goroutines to run. Yes, this is basically like putting a time.Sleep() in your tests... Sometimes, when making negative assertions in a concurrent world, that's the best you can do!
406
407 ### Modifying Default Intervals
408
409 By default, `Eventually` will poll every 10 milliseconds for up to 1 second and `Consistently` will monitor every 10 milliseconds for up to 100 milliseconds. You can modify these defaults across your test suite with:
410
411 ```go
412 SetDefaultEventuallyTimeout(t time.Duration)
413 SetDefaultEventuallyPollingInterval(t time.Duration)
414 SetDefaultConsistentlyDuration(t time.Duration)
415 SetDefaultConsistentlyPollingInterval(t time.Duration)
416 ```
417
418 You can also adjust these global timeouts by setting the `GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT`, `GOMEGA_DEFAULT_EVENTUALLY_POLLING_INTERVAL`, `GOMEGA_DEFAULT_CONSISTENTLY_DURATION`, and `GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL` environment variables to a parseable duration string.
419
420 ## Making Assertions in Helper Functions
421
422 While writing [custom matchers](#adding-your-own-matchers) is an expressive way to make assertions against your code, it is often more convenient to write one-off helper functions like so:
423
424 ```go
425 var _ = Describe("Turbo-encabulator", func() {
426 ...
427 func assertTurboEncabulatorContains(components ...string) {
428 teComponents, err := turboEncabulator.GetComponents()
429 Expect(err).NotTo(HaveOccurred())
430
431 Expect(teComponents).To(HaveLen(components))
432 for _, component := range components {
433 Expect(teComponents).To(ContainElement(component))
434 }
435 }
436
437 It("should have components", func() {
438 assertTurboEncabulatorContains("semi-boloid slots", "grammeters")
439 })
440 })
441 ```
442
443 This makes your tests more expressive and reduces boilerplate. However, when an assertion in the helper fails the line numbers provided by Gomega are unhelpful. Instead of pointing you to the line in your test that failed, they point you the line in the helper.
444
445 To get around this, Gomega provides versions of `Expect`, `Eventually` and `Consistently` named `ExpectWithOffset`, `EventuallyWithOffset` and `ConsistentlyWithOffset` that allow you to specify an *offset* in the call stack. The offset is the first argument to these functions.
446
447 With this, we can rewrite our helper as:
448
449 ```go
450 func assertTurboEncabulatorContains(components ...string) {
451 teComponents, err := turboEncabulator.GetComponents()
452 ExpectWithOffset(1, err).NotTo(HaveOccurred())
453
454 ExpectWithOffset(1, teComponents).To(HaveLen(components))
455 for _, component := range components {
456 ExpectWithOffset(1, teComponents).To(ContainElement(component))
457 }
458 }
459 ```
460
461 Now, failed assertions will point to the correct call to the helper in the test.
462
463 Alternatively, you can just use the baseline versions of `Expect`, `Eventually` and `Consistently` and combine them with `WithOffset`:
464
465 ```go
466 func assertTurboEncabulatorContains(components ...string) {
467 teComponents, err := turboEncabulator.GetComponents()
468 Expect(err).WithOffset(1).NotTo(HaveOccurred())
469
470 Expect(teComponents).WithOffset(1).To(HaveLen(components))
471 for _, component := range components {
472 Expect(teComponents).WithOffset(1).To(ContainElement(component))
473 }
474 }
475 ```
476
477 ## Provided Matchers
478
479 Gomega comes with a bunch of `GomegaMatcher`s. They're all documented here. If there's one you'd like to see written either [send a pull request or open an issue](http://github.com/onsi/gomega).
480
481 A number of community-supported matchers have appeared as well. A list is maintained on the Gomega [wiki](https://github.com/onsi/gomega/wiki).
482
483 These docs only go over the positive assertion case (`Should`), the negative case (`ShouldNot`) is simply the negation of the positive case. They also use the `Ω` notation, but - as mentioned above - the `Expect` notation is equivalent.
484
485 ### Asserting Equivalence
486
487 #### Equal(expected interface{})
488
489 ```go
490 Ω(ACTUAL).Should(Equal(EXPECTED))
491 ```
492
493 uses [`reflect.DeepEqual`](http://golang.org/pkg/reflect#deepequal) to compare `ACTUAL` with `EXPECTED`.
494
495 `reflect.DeepEqual` is awesome. It will use `==` when appropriate (e.g. when comparing primitives) but will recursively dig into maps, slices, arrays, and even your own structs to ensure deep equality. `reflect.DeepEqual`, however, is strict about comparing types. Both `ACTUAL` and `EXPECTED` *must* have the same type. If you want to compare across different types (e.g. if you've defined a type alias) you should use `BeEquivalentTo`.
496
497 It is an error for both `ACTUAL` and `EXPECTED` to be nil, you should use `BeNil()` instead.
498
499 When both `ACTUAL` and `EXPECTED` are a very long strings, it will attempt to pretty-print the diff and display exactly where they differ.
500
501 > For asserting equality between numbers of different types, you'll want to use the [`BeNumerically()`](#benumericallycomparator-string-compareto-interface) matcher.
502
503 #### BeEquivalentTo(expected interface{})
504
505 ```go
506 Ω(ACTUAL).Should(BeEquivalentTo(EXPECTED))
507 ```
508
509 Like `Equal`, `BeEquivalentTo` uses `reflect.DeepEqual` to compare `ACTUAL` with `EXPECTED`. Unlike `Equal`, however, `BeEquivalentTo` will first convert `ACTUAL`'s type to that of `EXPECTED` before making the comparison with `reflect.DeepEqual`.
510
511 This means that `BeEquivalentTo` will successfully match equivalent values of different types. This is particularly useful, for example, with type aliases:
512
513 ```go
514 type FoodSrce string
515
516 Ω(FoodSrce("Cheeseboard Pizza")
517 ).Should(Equal("Cheeseboard Pizza")) //will fail
518 Ω(FoodSrce("Cheeseboard Pizza")
519 ).Should(BeEquivalentTo("Cheeseboard Pizza")) //will pass
520 ```
521
522 As with `Equal` it is an error for both `ACTUAL` and `EXPECTED` to be nil, you should use `BeNil()` instead.
523
524 As a rule, you **should not** use `BeEquivalentTo` with numbers. Both of the following assertions are true:
525
526 ```go
527 Ω(5.1).Should(BeEquivalentTo(5))
528 Ω(5).ShouldNot(BeEquivalentTo(5.1))
529 ```
530
531 the first assertion passes because 5.1 will be cast to an integer and will get rounded down! Such false positives are terrible and should be avoided. Use [`BeNumerically()`](#benumericallycomparator-string-compareto-interface) to compare numbers instead.
532
533 #### BeIdenticalTo(expected interface{})
534
535 ```go
536 Ω(ACTUAL).Should(BeIdenticalTo(EXPECTED))
537 ```
538
539 Like `Equal`, `BeIdenticalTo` compares `ACTUAL` to `EXPECTED` for equality. Unlike `Equal`, however, it uses `==` to compare values. In practice, this means that primitive values like strings, integers and floats are identical to, as well as pointers to values.
540
541 `BeIdenticalTo` is most useful when you want to assert that two pointers point to the exact same location in memory.
542
543 As with `Equal` it is an error for both `ACTUAL` and `EXPECTED` to be nil, you should use `BeNil()` instead.
544
545 #### BeAssignableToTypeOf(expected interface)
546
547 ```go
548 Ω(ACTUAL).Should(BeAssignableToTypeOf(EXPECTED interface))
549 ```
550
551 succeeds if `ACTUAL` is a type that can be assigned to a variable with the same type as `EXPECTED`. It is an error for either `ACTUAL` or `EXPECTED` to be `nil`.
552
553 ### Asserting Presence
554
555 #### BeNil()
556
557 ```go
558 Ω(ACTUAL).Should(BeNil())
559 ```
560
561 succeeds if `ACTUAL` is, in fact, `nil`.
562
563 #### BeZero()
564
565 ```go
566 Ω(ACTUAL).Should(BeZero())
567 ```
568
569 succeeds if `ACTUAL` is the zero value for its type *or* if `ACTUAL` is `nil`.
570
571 ### Asserting Truthiness
572
573 #### BeTrue()
574
575 ```go
576 Ω(ACTUAL).Should(BeTrue())
577 ```
578
579 succeeds if `ACTUAL` is `bool` typed and has the value `true`. It is an error for `ACTUAL` to not be a `bool`.
580
581 > Some matcher libraries have a notion of "truthiness" to assert that an object is present. Gomega is strict, and `BeTrue()` only works with `bool`s. You can use `Ω(ACTUAL).ShouldNot(BeZero())` or `Ω(ACTUAL).ShouldNot(BeNil())` to verify object presence.
582
583 #### BeFalse()
584
585 ```go
586 Ω(ACTUAL).Should(BeFalse())
587 ```
588
589 succeeds if `ACTUAL` is `bool` typed and has the value `false`. It is an error for `ACTUAL` to not be a `bool`.
590
591 ### Asserting on Errors
592
593 #### HaveOccurred()
594
595 ```go
596 Ω(ACTUAL).Should(HaveOccurred())
597 ```
598
599 succeeds if `ACTUAL` is a non-nil `error`. Thus, the typical Go error checking pattern looks like:
600
601 ```go
602 err := SomethingThatMightFail()
603 Ω(err).ShouldNot(HaveOccurred())
604 ```
605
606 #### Succeed()
607
608 ```go
609 Ω(ACTUAL).Should(Succeed())
610 ```
611
612 succeeds if `ACTUAL` is `nil`. The intended usage is
613
614 ```go
615 Ω(FUNCTION()).Should(Succeed())
616 ```
617
618 where `FUNCTION()` is a function call that returns an error-type as its *first or only* return value. See [Handling Errors](#handling-errors) for a more detailed discussion.
619
620 #### MatchError(expected interface{})
621
622 ```go
623 Ω(ACTUAL).Should(MatchError(EXPECTED))
624 ```
625
626 succeeds if `ACTUAL` is a non-nil `error` that matches `EXPECTED`. `EXPECTED` must be one of the following:
627
628 - A string, in which case `ACTUAL.Error()` will be compared against `EXPECTED`.
629 - A matcher, in which case `ACTUAL.Error()` is tested against the matcher.
630 - An error, in which case `ACTUAL` and `EXPECTED` are compared via `reflect.DeepEqual()`. If they are not deeply equal, they are tested by `errors.Is(ACTUAL, EXPECTED)`. (The latter allows to test whether `ACTUAL` wraps an `EXPECTED` error.)
631
632 Any other type for `EXPECTED` is an error.
633
634 ### Working with Channels
635
636 #### BeClosed()
637
638 ```go
639 Ω(ACTUAL).Should(BeClosed())
640 ```
641
642 succeeds if `ACTUAL` is a closed channel. It is an error to pass a non-channel to `BeClosed`, it is also an error to pass `nil`.
643
644 In order to check whether or not the channel is closed, Gomega must try to read from the channel (even in the `ShouldNot(BeClosed())` case). You should keep this in mind if you wish to make subsequent assertions about values coming down the channel.
645
646 Also, if you are testing that a *buffered* channel is closed you must first read all values out of the channel before asserting that it is closed (it is not possible to detect that a buffered-channel has been closed until all its buffered values are read).
647
648 Finally, as a corollary: it is an error to check whether or not a send-only channel is closed.
649
650 #### Receive()
651
652 ```go
653 Ω(ACTUAL).Should(Receive(<optionalPointer>))
654 ```
655
656 succeeds if there is a message to be received on actual. Actual must be a channel (and cannot be a send-only channel) -- anything else is an error.
657
658 `Receive` returns *immediately*. It *never* blocks:
659
660 - If there is nothing on the channel `c` then `Ω(c).Should(Receive())` will fail and `Ω(c).ShouldNot(Receive())` will pass.
661 - If there is something on the channel `c` ready to be read, then `Ω(c).Should(Receive())` will pass and `Ω(c).ShouldNot(Receive())` will fail.
662 - If the channel `c` is closed then `Ω(c).Should(Receive())` will fail and `Ω(c).ShouldNot(Receive())` will pass.
663
664 If you have a go-routine running in the background that will write to channel `c`, for example:
665
666 ```go
667 go func() {
668 time.Sleep(100 * time.Millisecond)
669 c <- true
670 }()
671 ```
672
673 you can assert that `c` receives something (anything!) eventually:
674
675 ```go
676 Eventually(c).Should(Receive())
677 ```
678
679 This will timeout if nothing gets sent to `c` (you can modify the timeout interval as you normally do with `Eventually`).
680
681 A similar use-case is to assert that no go-routine writes to a channel (for a period of time). You can do this with `Consistently`:
682
683 ```go
684 Consistently(c).ShouldNot(Receive())
685 ```
686
687 `Receive` also allows you to make assertions on the received object. You do this by passing `Receive` a matcher:
688
689 ```go
690 Eventually(c).Should(Receive(Equal("foo")))
691 ```
692
693 This assertion will only succeed if `c` receives an object *and* that object satisfies `Equal("foo")`. Note that `Eventually` will continually poll `c` until this condition is met. If there are objects coming down the channel that do not satisfy the passed in matcher, they will be pulled off and discarded until an object that *does* satisfy the matcher is received.
694
695 In addition, there are occasions when you need to grab the object sent down the channel (e.g. to make several assertions against the object). To do this, you can ask the `Receive` matcher for the value passed to the channel by passing it a pointer to a variable of the appropriate type:
696
697 ```go
698 var receivedBagel Bagel
699 Eventually(bagelChan).Should(Receive(&receivedBagel))
700 Ω(receivedBagel.Contents()).Should(ContainElement("cream cheese"))
701 Ω(receivedBagel.Kind()).Should(Equal("sesame"))
702 ```
703
704 Of course, this could have been written as `receivedBagel := <-bagelChan` - however using `Receive` makes it easy to avoid hanging the test suite should nothing ever come down the channel. The pointer can point to any variable whose type is assignable from the channel element type, or if the channel type is an interface and the underlying type is assignable to the pointer.
705
706 Finally, `Receive` *never* blocks. `Eventually(c).Should(Receive())` repeatedly polls `c` in a non-blocking fashion. That means that you cannot use this pattern to verify that a *non-blocking send* has occurred on the channel - [more details at this GitHub issue](https://github.com/onsi/gomega/issues/82).
707
708 #### BeSent(value interface{})
709
710 ```go
711 Ω(ACTUAL).Should(BeSent(VALUE))
712 ```
713
714 attempts to send `VALUE` to the channel `ACTUAL` without blocking. It succeeds if this is possible.
715
716 `ACTUAL` must be a channel (and cannot be a receive-only channel) that can be sent the type of the `VALUE` passed into `BeSent` -- anything else is an error. In addition, `ACTUAL` must not be closed.
717
718 `BeSent` never blocks:
719
720 - If the channel `c` is not ready to receive then `Ω(c).Should(BeSent("foo"))` will fail immediately.
721 - If the channel `c` is eventually ready to receive then `Eventually(c).Should(BeSent("foo"))` will succeed... presuming the channel becomes ready to receive before `Eventually`'s timeout.
722 - If the channel `c` is closed then `Ω(c).Should(BeSent("foo"))` and `Ω(c).ShouldNot(BeSent("foo"))` will both fail immediately.
723
724 Of course, `VALUE` is actually sent to the channel. The point of `BeSent` is less to make an assertion about the availability of the channel (which is typically an implementation detail that your test should not be concerned with). Rather, the point of `BeSent` is to make it possible to easily and expressively write tests that can timeout on blocked channel sends.
725
726 ### Working with files
727
728 #### BeAnExistingFile
729
730 ```go
731 Ω(ACTUAL).Should(BeAnExistingFile())
732 ```
733
734 succeeds if a file located at `ACTUAL` exists.
735
736 `ACTUAL` must be a string representing the filepath.
737
738 #### BeARegularFile
739
740 ```go
741 Ω(ACTUAL).Should(BeARegularFile())
742 ```
743
744 succeeds IFF a file located at `ACTUAL` exists and is a regular file.
745
746 `ACTUAL` must be a string representing the filepath.
747
748 #### BeADirectory
749
750 ```go
751 Ω(ACTUAL).Should(BeADirectory())
752 ```
753
754 succeeds IFF a file is located at `ACTUAL` exists and is a directory.
755
756 `ACTUAL` must be a string representing the filepath.
757
758 ### Working with Strings, JSON and YAML
759
760 #### ContainSubstring(substr string, args ...interface{})
761
762 ```go
763 Ω(ACTUAL).Should(ContainSubstring(STRING, ARGS...))
764 ```
765
766 succeeds if `ACTUAL` contains the substring generated by:
767
768 ```go
769 fmt.Sprintf(STRING, ARGS...)
770 ```
771
772 `ACTUAL` must either be a `string`, `[]byte` or a `Stringer` (a type implementing the `String()` method). Any other input is an error.
773
774 > Note, of course, that the `ARGS...` are not required. They are simply a convenience to allow you to build up strings programmatically inline in the matcher.
775
776 #### HavePrefix(prefix string, args ...interface{})
777
778 ```go
779 Ω(ACTUAL).Should(HavePrefix(STRING, ARGS...))
780 ```
781
782 succeeds if `ACTUAL` has the string prefix generated by:
783
784 ```go
785 fmt.Sprintf(STRING, ARGS...)
786 ```
787
788 `ACTUAL` must either be a `string`, `[]byte` or a `Stringer` (a type implementing the `String()` method). Any other input is an error.
789
790 > Note, of course, that the `ARGS...` are not required. They are simply a convenience to allow you to build up strings programmatically inline in the matcher.
791
792 #### HaveSuffix(suffix string, args ...interface{})
793
794 ```go
795 Ω(ACTUAL).Should(HaveSuffix(STRING, ARGS...))
796 ```
797
798 succeeds if `ACTUAL` has the string suffix generated by:
799
800 ```go
801 fmt.Sprintf(STRING, ARGS...)
802 ```
803
804 `ACTUAL` must either be a `string`, `[]byte` or a `Stringer` (a type implementing the `String()` method). Any other input is an error.
805
806 > Note, of course, that the `ARGS...` are not required. They are simply a convenience to allow you to build up strings programmatically inline in the matcher.
807
808 #### MatchRegexp(regexp string, args ...interface{})
809
810 ```go
811 Ω(ACTUAL).Should(MatchRegexp(STRING, ARGS...))
812 ```
813
814 succeeds if `ACTUAL` is matched by the regular expression string generated by:
815
816 ```go
817 fmt.Sprintf(STRING, ARGS...)
818 ```
819
820 `ACTUAL` must either be a `string`, `[]byte` or a `Stringer` (a type implementing the `String()` method). Any other input is an error. It is also an error for the regular expression to fail to compile.
821
822 > Note, of course, that the `ARGS...` are not required. They are simply a convenience to allow you to build up strings programmatically inline in the matcher.
823
824 #### MatchJSON(json interface{})
825
826 ```go
827 Ω(ACTUAL).Should(MatchJSON(EXPECTED))
828 ```
829
830 Both `ACTUAL` and `EXPECTED` must be a `string`, `[]byte` or a `Stringer`. `MatchJSON` succeeds if both `ACTUAL` and `EXPECTED` are JSON representations of the same object. This is verified by parsing both `ACTUAL` and `EXPECTED` and then asserting equality on the resulting objects with `reflect.DeepEqual`. By doing this `MatchJSON` avoids any issues related to white space, formatting, and key-ordering.
831
832 It is an error for either `ACTUAL` or `EXPECTED` to be invalid JSON.
833
834 In some cases it is useful to match two JSON strings while ignoring list order. For this you can use the community maintained [MatchUnorderedJSON](https://github.com/Benjamintf1/Expanded-Unmarshalled-Matchers) matcher.
835
836 #### MatchXML(xml interface{})
837
838 ```go
839 Ω(ACTUAL).Should(MatchXML(EXPECTED))
840 ```
841
842 Both `ACTUAL` and `EXPECTED` must be a `string`, `[]byte` or a `Stringer`. `MatchXML` succeeds if both `ACTUAL` and `EXPECTED` are XML representations of the same object. This is verified by parsing both `ACTUAL` and `EXPECTED` and then asserting equality on the resulting objects with `reflect.DeepEqual`. By doing this `MatchXML` avoids any issues related to white space or formatting.
843
844 It is an error for either `ACTUAL` or `EXPECTED` to be invalid XML.
845
846 #### MatchYAML(yaml interface{})
847
848 ```go
849 Ω(ACTUAL).Should(MatchYAML(EXPECTED))
850 ```
851
852 Both `ACTUAL` and `EXPECTED` must be a `string`, `[]byte` or a `Stringer`. `MatchYAML` succeeds if both `ACTUAL` and `EXPECTED` are YAML representations of the same object. This is verified by parsing both `ACTUAL` and `EXPECTED` and then asserting equality on the resulting objects with `reflect.DeepEqual`. By doing this `MatchYAML` avoids any issues related to white space, formatting, and key-ordering.
853
854 It is an error for either `ACTUAL` or `EXPECTED` to be invalid YAML.
855
856 ### Working with Collections
857
858 #### BeEmpty()
859
860 ```go
861 Ω(ACTUAL).Should(BeEmpty())
862 ```
863
864 succeeds if `ACTUAL` is, in fact, empty. `ACTUAL` must be of type `string`, `array`, `map`, `chan`, or `slice`. It is an error for it to have any other type.
865
866 #### HaveLen(count int)
867
868 ```go
869 Ω(ACTUAL).Should(HaveLen(INT))
870 ```
871
872 succeeds if the length of `ACTUAL` is `INT`. `ACTUAL` must be of type `string`, `array`, `map`, `chan`, or `slice`. It is an error for it to have any other type.
873
874 #### HaveCap(count int)
875
876 ```go
877 Ω(ACTUAL).Should(HaveCap(INT))
878 ```
879
880 succeeds if the capacity of `ACTUAL` is `INT`. `ACTUAL` must be of type `array`, `chan`, or `slice`. It is an error for it to have any other type.
881
882 #### ContainElement(element interface{})
883
884 ```go
885 Ω(ACTUAL).Should(ContainElement(ELEMENT))
886 ```
887
888 succeeds if `ACTUAL` contains an element that equals `ELEMENT`. `ACTUAL` must be an `array`, `slice`, or `map` -- anything else is an error. For `map`s `ContainElement` searches through the map's values (not keys!).
889
890 By default `ContainElement()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s elements and `ELEMENT`. You can change this, however, by passing `ContainElement` a `GomegaMatcher`. For example, to check that a slice of strings has an element that matches a substring:
891
892 ```go
893 Ω([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubstring("Bar")))
894 ```
895
896 #### ContainElements(element ...interface{})
897
898 ```go
899 Ω(ACTUAL).Should(ContainElements(ELEMENT1, ELEMENT2, ELEMENT3, ...))
900 ```
901
902 or
903
904 ```go
905 Ω(ACTUAL).Should(ContainElements([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...}))
906 ```
907
908 succeeds if `ACTUAL` contains the elements passed into the matcher. The ordering of the elements does not matter.
909
910 By default `ContainElements()` uses `Equal()` to match the elements, however custom matchers can be passed in instead. Here are some examples:
911
912 ```go
913 Ω([]string{"Foo", "FooBar"}).Should(ContainElements("FooBar"))
914 Ω([]string{"Foo", "FooBar"}).Should(ContainElements(ContainSubstring("Bar"), "Foo"))
915 ```
916
917 Actual must be an `array`, `slice` or `map`. For maps, `ContainElements` matches against the `map`'s values.
918
919 You typically pass variadic arguments to `ContainElements` (as in the examples above). However, if you need to pass in a slice you can provided that it
920 is the only element passed in to `ContainElements`:
921
922 ```go
923 Ω([]string{"Foo", "FooBar"}).Should(ContainElements([]string{"FooBar", "Foo"}))
924 ```
925
926 Note that Go's type system does not allow you to write this as `ContainElements([]string{"FooBar", "Foo"}...)` as `[]string` and `[]interface{}` are different types - hence the need for this special rule.
927
928 The difference between the `ContainElements` and `ConsistOf` matchers is that the latter is more restrictive because the `ConsistOf` matcher checks additionally that the `ACTUAL` elements and the elements passed into the matcher have the same length.
929
930 #### BeElementOf(elements ...interface{})
931
932 ```go
933 Ω(ACTUAL).Should(BeElementOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))
934 ```
935
936 succeeds if `ACTUAL` equals one of the elements passed into the matcher. When a single element `ELEMENT` of type `array` or `slice` is passed into the matcher, `BeElementOf` succeeds if `ELEMENT` contains an element that equals `ACTUAL` (reverse of `ContainElement`). `BeElementOf` always uses the `Equal()` matcher under the hood to assert equality.
937
938 #### ConsistOf(element ...interface{})
939
940 ```go
941 Ω(ACTUAL).Should(ConsistOf(ELEMENT1, ELEMENT2, ELEMENT3, ...))
942 ```
943
944 or
945
946 ```go
947 Ω(ACTUAL).Should(ConsistOf([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...}))
948 ```
949
950 succeeds if `ACTUAL` contains precisely the elements passed into the matcher. The ordering of the elements does not matter.
951
952 By default `ConsistOf()` uses `Equal()` to match the elements, however custom matchers can be passed in instead. Here are some examples:
953
954 ```go
955 Ω([]string{"Foo", "FooBar"}).Should(ConsistOf("FooBar", "Foo"))
956 Ω([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Bar"), "Foo"))
957 Ω([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Foo"), ContainSubstring("Foo")))
958 ```
959
960 Actual must be an `array`, `slice` or `map`. For maps, `ConsistOf` matches against the `map`'s values.
961
962 You typically pass variadic arguments to `ConsistOf` (as in the examples above). However, if you need to pass in a slice you can provided that it
963 is the only element passed in to `ConsistOf`:
964
965 ```go
966 Ω([]string{"Foo", "FooBar"}).Should(ConsistOf([]string{"FooBar", "Foo"}))
967 ```
968
969 Note that Go's type system does not allow you to write this as `ConsistOf([]string{"FooBar", "Foo"}...)` as `[]string` and `[]interface{}` are different types - hence the need for this special rule.
970
971
972 #### HaveKey(key interface{})
973
974 ```go
975 Ω(ACTUAL).Should(HaveKey(KEY))
976 ```
977
978 succeeds if `ACTUAL` is a map with a key that equals `KEY`. It is an error for `ACTUAL` to not be a `map`.
979
980 By default `HaveKey()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s keys and `KEY`. You can change this, however, by passing `HaveKey` a `GomegaMatcher`. For example, to check that a map has a key that matches a regular expression:
981
982 ```go
983 Ω(map[string]string{"Foo": "Bar", "BazFoo": "Duck"}).Should(HaveKey(MatchRegexp(`.+Foo$`)))
984 ```
985
986 #### HaveKeyWithValue(key interface{}, value interface{})
987
988 ```go
989 Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE))
990 ```
991
992 succeeds if `ACTUAL` is a map with a key that equals `KEY` mapping to a value that equals `VALUE`. It is an error for `ACTUAL` to not be a `map`.
993
994 By default `HaveKeyWithValue()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s keys and `KEY` and between the associated value and `VALUE`. You can change this, however, by passing `HaveKeyWithValue` a `GomegaMatcher` for either parameter. For example, to check that a map has a key that matches a regular expression and which is also associated with a value that passes some numerical threshold:
995
996 ```go
997 Ω(map[string]int{"Foo": 3, "BazFoo": 4}).Should(HaveKeyWithValue(MatchRegexp(`.+Foo$`), BeNumerically(">", 3)))
998 ```
999
1000 #### HaveField(field interface{}, value interface{})
1001
1002 ```go
1003 Ω(ACTUAL).Should(HaveField(FIELD, VALUE))
1004 ```
1005
1006 succeeds if `ACTUAL` is a struct with a value that can be traversed via `FIELD` that equals `VALUE`. It is an error for `ACTUAL` to not be a `struct`.
1007
1008 By default `HaveField()` uses the `Equal()` matcher under the hood to assert equality between the extracted value and `VALUE`. You can change this, however, by passing `HaveField` a `GomegaMatcher` for `VALUE`.
1009
1010 `FIELD` allows you to access fields within the `ACTUAL` struct. Nested structs can be accessed using the `.` delimiter. `HaveField()` also allows you to invoke methods on the struct by adding a `()` suffix to the `FIELD` - these methods must take no arguments and return exactly one value. For example consider the following types:
1011
1012 ```go
1013 type Book struct {
1014 Title string
1015 Author Person
1016 }
1017
1018 type Person struct {
1019 Name string
1020 DOB time.Time
1021 }
1022 ```
1023
1024 and an instance book `var book = Book{...}` - you can use `HaveField` to make assertions like:
1025
1026 ```go
1027 Ω(book).Should(HaveField("Title", "Les Miserables"))
1028 Ω(book).Should(HaveField("Title", ContainSubstring("Les Mis")))
1029 Ω(book).Should(HaveField("Author.Name", "Victor Hugo"))
1030 Ω(book).Should(HaveField("Author.DOB.Year()", BeNumerically("<", 1900)))
1031 ```
1032
1033 `HaveField` can pair powerfully with a collection matcher like `ContainElement`. To assert that a list of books as at least one element with an author born in February you could write:
1034
1035 ```go
1036 Ω(books).Should(ContainElement(HaveField("Author.DOB.Month()", Equal(2))))
1037 ```
1038
1039 If you want to make lots of complex assertions against the fields of a struct take a look at the [`gstruct`package](#gstruct-testing-complex-data-types) package documented below.
1040
1041 ### Working with Numbers and Times
1042
1043 #### BeNumerically(comparator string, compareTo ...interface{})
1044
1045 ```go
1046 Ω(ACTUAL).Should(BeNumerically(COMPARATOR_STRING, EXPECTED, <THRESHOLD>))
1047 ```
1048
1049 performs numerical assertions in a type-agnostic way. `ACTUAL` and `EXPECTED` should be numbers, though the specific type of number is irrelevant (`float32`, `float64`, `uint8`, etc...). It is an error for `ACTUAL` or `EXPECTED` to not be a number.
1050
1051 There are six supported comparators:
1052
1053 - `Ω(ACTUAL).Should(BeNumerically("==", EXPECTED))`:
1054 asserts that `ACTUAL` and `EXPECTED` are numerically equal.
1055
1056 - `Ω(ACTUAL).Should(BeNumerically("~", EXPECTED, <THRESHOLD>))`:
1057 asserts that `ACTUAL` and `EXPECTED` are within `<THRESHOLD>` of one another. By default `<THRESHOLD>` is `1e-8` but you can specify a custom value.
1058
1059 - `Ω(ACTUAL).Should(BeNumerically(">", EXPECTED))`:
1060 asserts that `ACTUAL` is greater than `EXPECTED`.
1061
1062 - `Ω(ACTUAL).Should(BeNumerically(">=", EXPECTED))`:
1063 asserts that `ACTUAL` is greater than or equal to `EXPECTED`.
1064
1065 - `Ω(ACTUAL).Should(BeNumerically("<", EXPECTED))`:
1066 asserts that `ACTUAL` is less than `EXPECTED`.
1067
1068 - `Ω(ACTUAL).Should(BeNumerically("<=", EXPECTED))`:
1069 asserts that `ACTUAL` is less than or equal to `EXPECTED`.
1070
1071 Any other comparator is an error.
1072
1073 #### BeTemporally(comparator string, compareTo time.Time, threshold ...time.Duration)
1074
1075 ```go
1076 Ω(ACTUAL).Should(BeTemporally(COMPARATOR_STRING, EXPECTED_TIME, <THRESHOLD_DURATION>))
1077 ```
1078
1079 performs time-related assertions. `ACTUAL` must be a `time.Time`.
1080
1081 There are six supported comparators:
1082
1083 - `Ω(ACTUAL).Should(BeTemporally("==", EXPECTED_TIME))`:
1084 asserts that `ACTUAL` and `EXPECTED_TIME` are identical `time.Time`s.
1085
1086 - `Ω(ACTUAL).Should(BeTemporally("~", EXPECTED_TIME, <THRESHOLD_DURATION>))`:
1087 asserts that `ACTUAL` and `EXPECTED_TIME` are within `<THRESHOLD_DURATION>` of one another. By default `<THRESHOLD_DURATION>` is `time.Millisecond` but you can specify a custom value.
1088
1089 - `Ω(ACTUAL).Should(BeTemporally(">", EXPECTED_TIME))`:
1090 asserts that `ACTUAL` is after `EXPECTED_TIME`.
1091
1092 - `Ω(ACTUAL).Should(BeTemporally(">=", EXPECTED_TIME))`:
1093 asserts that `ACTUAL` is after or at `EXPECTED_TIME`.
1094
1095 - `Ω(ACTUAL).Should(BeTemporally("<", EXPECTED_TIME))`:
1096 asserts that `ACTUAL` is before `EXPECTED_TIME`.
1097
1098 - `Ω(ACTUAL).Should(BeTemporally("<=", EXPECTED_TIME))`:
1099 asserts that `ACTUAL` is before or at `EXPECTED_TIME`.
1100
1101 Any other comparator is an error.
1102
1103 ### Working with Values
1104
1105 #### HaveValue(matcher types.GomegaMatcher)
1106
1107 `HaveValue` applies `MATCHER` to the value that results from dereferencing `ACTUAL` in case of a pointer or an interface, or otherwise `ACTUAL` itself. Pointers and interfaces are dereferenced multiple times as necessary, with a limit of at most 31 dereferences.
1108
1109 ```go
1110 Expect(ACTUAL).To(HaveValue(MATCHER))
1111 ```
1112
1113 For instance:
1114
1115 ```go
1116 i := 42
1117 Expect(&i).To(HaveValue(Equal(42)))
1118 Expect(i).To(HaveValue(Equal(42)))
1119 ```
1120
1121 `HaveValue` can be used, for instance, in tests and custom matchers where the it doesn't matter (as opposed to `PointTo`) if a value first needs to be dereferenced or not. This is especially useful to custom matchers that are to be used in mixed contexts of pointers as well as non-pointers.
1122
1123 ### Working with HTTP responses
1124
1125 #### HaveHTTPStatus(expected interface{})
1126
1127 ```go
1128 Expect(ACTUAL).To(HaveHTTPStatus(EXPECTED, ...))
1129 ```
1130
1131 succeeds if the `Status` or `StatusCode` field of an HTTP response matches.
1132
1133 `ACTUAL` must be either a `*http.Response` or `*httptest.ResponseRecorder`.
1134
1135 `EXPECTED` must be one or more `int` or `string` types. An `int` is compared
1136 to `StatusCode` and a `string` is compared to `Status`.
1137 The matcher succeeds if any of the `EXPECTED` values match.
1138
1139 Here are some examples:
1140
1141 - `Expect(resp).To(HaveHTTPStatus(http.StatusOK, http.StatusNoContext))`:
1142 asserts that `resp.StatusCode == 200` or `resp.StatusCode == 204`
1143
1144 - `Expect(resp).To(HaveHTTPStatus("404 Not Found"))`:
1145 asserts that `resp.Status == "404 Not Found"`.
1146
1147 #### HaveHTTPBody(expected interface{})
1148
1149 ```go
1150 Expect(ACTUAL).To(HaveHTTPBody(EXPECTED))
1151 ```
1152
1153 Succeeds if the body of an HTTP Response matches.
1154
1155 `ACTUAL` must be either a `*http.Response` or `*httptest.ResponseRecorder`.
1156
1157 `EXPECTED` must be one of the following:
1158 - A `string`
1159 - A `[]byte`
1160 - A matcher, in which case the matcher will be called with the body as a `[]byte`.
1161
1162 Here are some examples:
1163
1164 - `Expect(resp).To(HaveHTTPBody("bar"))`:
1165 asserts that when `resp.Body` is read, it will equal `bar`.
1166
1167 - `Expect(resp).To(HaveHTTPBody(MatchJSON("{\"some\":\"json\""))`:
1168 asserts that when `resp.Body` is read, the `MatchJSON` matches it to `{"some":"json"}`.
1169
1170 Note that the body is an `io.ReadCloser` and the `HaveHTTPBody()` will read it and the close it.
1171 This means that subsequent attempts to read the body may have unexpected results.
1172
1173 #### HaveHTTPHeaderWithValue(key string, value interface{})
1174
1175 ```go
1176 Expect(ACTUAL).To(HaveHTTPHeaderWithValue(KEY, VALUE))
1177 ```
1178
1179 Succeeds if the HTTP Response has a matching header and value.
1180
1181 `ACTUAL` must be either a `*http.Response` or `*httptest.ResponseRecorder`.
1182
1183 `KEY` must be a `string`. It is passed to
1184 [`http.Header.Get(key string)`](https://pkg.go.dev/net/http#Header.Get),
1185 and will have the same behaviors regarding order of headers and capitalization.
1186
1187 `VALUE` must be one of the following:
1188 - A `string`
1189 - A matcher, in which case the matcher will be called to match the value.
1190
1191 Here are some examples:
1192
1193 - `Expect(resp).To(HaveHTTPHeaderWithValue("Content-Type", "application/json"))`:
1194 asserts that the `Content-Type` header has exactly the value `application/json`.
1195
1196 - `Expect(resp).To(HaveHTTPHeaderWithValue(ContainsSubstring("json")))`:
1197 asserts that the `Content-Type` header contains the substring `json`.
1198
1199 ### Asserting on Panics
1200
1201 #### Panic()
1202
1203 ```go
1204 Ω(ACTUAL).Should(Panic())
1205 ```
1206
1207 succeeds if `ACTUAL` is a function that, when invoked, panics. `ACTUAL` must be a function that takes no arguments and returns no result -- any other type for `ACTUAL` is an error.
1208
1209 #### PanicWith()
1210
1211 ```go
1212 Ω(ACTUAL).Should(PanicWith(VALUE))
1213 ```
1214
1215 succeeds if `ACTUAL` is a function that, when invoked, panics with a value of `VALUE`. `ACTUAL` must be a function that takes no arguments and returns no result -- any other type for `ACTUAL` is an error.
1216
1217 By default `PanicWith()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s panic value and `VALUE`. You can change this, however, by passing `PanicWith` a `GomegaMatcher`. For example, to check that the panic value matches a regular expression:
1218
1219 ```go
1220 Ω(func() { panic("FooBarBaz") }).Should(PanicWith(MatchRegexp(`.+Baz$`)))
1221 ```
1222
1223 ### Composing Matchers
1224
1225 You may form larger matcher expressions using the following operators: `And()`, `Or()`, `Not()` and `WithTransform()`.
1226
1227 Note: `And()` and `Or()` can also be referred to as `SatisfyAll()` and `SatisfyAny()`, respectively.
1228
1229 With these operators you can express multiple requirements in a single `Expect()` or `Eventually()` statement. For example:
1230
1231 ```go
1232 Expect(number).To(SatisfyAll(
1233 BeNumerically(">", 0),
1234 BeNumerically("<", 10)))
1235
1236 Expect(msg).To(SatisfyAny(
1237 Equal("Success"),
1238 MatchRegexp(`^Error .+$`)))
1239 ```
1240
1241 It can also provide a lightweight syntax to create new matcher types from existing ones. For example:
1242
1243 ```go
1244 func BeBetween(min, max int) GomegaMatcher {
1245 return SatisfyAll(
1246 BeNumerically(">", min),
1247 BeNumerically("<", max))
1248 }
1249
1250 Ω(number).Should(BeBetween(0, 10))
1251 ```
1252
1253 #### And(matchers ...GomegaMatcher)
1254
1255 #### SatisfyAll(matchers ...GomegaMatcher)
1256
1257 ```go
1258 Ω(ACTUAL).Should(And(MATCHER1, MATCHER2, ...))
1259 ```
1260
1261 or
1262
1263 ```go
1264 Ω(ACTUAL).Should(SatisfyAll(MATCHER1, MATCHER2, ...))
1265 ```
1266
1267 succeeds if `ACTUAL` satisfies all of the specified matchers (similar to a logical AND).
1268
1269 Tests the given matchers in order, returning immediately if one fails, without needing to test the remaining matchers.
1270
1271 #### Or(matchers ...GomegaMatcher)
1272
1273 #### SatisfyAny(matchers ...GomegaMatcher)
1274
1275 ```go
1276 Ω(ACTUAL).Should(Or(MATCHER1, MATCHER2, ...))
1277 ```
1278
1279 or
1280
1281 ```go
1282 Ω(ACTUAL).Should(SatisfyAny(MATCHER1, MATCHER2, ...))
1283 ```
1284
1285 succeeds if `ACTUAL` satisfies any of the specified matchers (similar to a logical OR).
1286
1287 Tests the given matchers in order, returning immediately if one succeeds, without needing to test the remaining matchers.
1288
1289 #### Not(matcher GomegaMatcher)
1290
1291 ```go
1292 Ω(ACTUAL).Should(Not(MATCHER))
1293 ```
1294
1295 succeeds if `ACTUAL` does **not** satisfy the specified matcher (similar to a logical NOT).
1296
1297 #### WithTransform(transform interface{}, matcher GomegaMatcher)
1298
1299 ```go
1300 Ω(ACTUAL).Should(WithTransform(TRANSFORM, MATCHER))
1301 ```
1302
1303 succeeds if applying the `TRANSFORM` function to `ACTUAL` (i.e. the value of `TRANSFORM(ACTUAL)`) will satisfy the given `MATCHER`. For example:
1304
1305 ```go
1306 GetColor := func(e Element) Color { return e.Color }
1307
1308 Ω(element).Should(WithTransform(GetColor, Equal(BLUE)))
1309 ```
1310
1311 Or the same thing expressed by introducing a new, lightweight matcher:
1312
1313 ```go
1314 // HaveColor returns a matcher that expects the element to have the given color.
1315 func HaveColor(c Color) GomegaMatcher {
1316 return WithTransform(func(e Element) Color {
1317 return e.Color
1318 }, Equal(c))
1319 }
1320
1321 Ω(element).Should(HaveColor(BLUE)))
1322 ```
1323
1324 `TRANSFORM` functions optionally can return an additional error value in case a transformation is not possible, avoiding the need to `panic`. Returning errors can be useful when using `WithTransform` to build lightweight matchers that accept different value types and that can gracefully fail when presented the wrong value type.
1325
1326 As before, such a `TRANSFORM` expects a single actual value. But now it returns the transformed value together with an error value. This follows the common Go idiom to communicate errors via an explicit, separate return value.
1327
1328 The following lightweight matcher expects to be used either on a `Sprocket` value or `*Sprocket` pointer. It gracefully fails when the actual value is something else.
1329
1330 ```go
1331 // HaveSprocketName returns a matcher that expects the actual value to be
1332 // either a Sprocket or a *Sprocket, having the specified name.
1333 func HaveSprocketName(name string) GomegaMatcher {
1334 return WithTransform(
1335 func(actual interface{}) (string, error) {
1336 switch sprocket := actual.(type) {
1337 case *Sprocket:
1338 return Sprocket.Name, nil
1339 case Sprocket:
1340 return Sprocket.Name, nil
1341 default:
1342 return "", fmt.Errorf("HaveSprocketName expects a Sprocket or *Sprocket, but got %T", actual)
1343 }
1344 }, Equal(name))
1345 }
1346
1347 Ω(element).Should(HaveSprocketName("gomega")))
1348 ```
1349
1350 #### Satisfy(predicate interface{})
1351
1352 ```go
1353 Ω(ACTUAL).Should(Satisfy(PREDICATE))
1354 ```
1355
1356 succeeds if applying the `PREDICATE` function to `ACTUAL` (i.e. the value of `PREDICATE(ACTUAL)`) will return `true`. For example:
1357
1358 ```go
1359 IsEven := func(i int) bool { return i%2==0 }
1360
1361 Ω(number).Should(Satisfy(IsEven))
1362 ```
1363
1364 ## Adding Your Own Matchers
1365
1366 A matcher, in Gomega, is any type that satisfies the `GomegaMatcher` interface:
1367
1368 ```go
1369 type GomegaMatcher interface {
1370 Match(actual interface{}) (success bool, err error)
1371 FailureMessage(actual interface{}) (message string)
1372 NegatedFailureMessage(actual interface{}) (message string)
1373 }
1374 ```
1375
1376 For the simplest cases, new matchers can be [created by composition](#composing-matchers). In addition to this chapter, please take a look at the [Building Custom Matchers](https://onsi.github.io/ginkgo/#building-custom-matchers) section of the Ginkgo and Gomega patterns chapter in the Ginkgo docs. Gomega's building blocks have evolved since the Gomega docs were written and while this section remains valid - the [Building Custom Matchers](https://onsi.github.io/ginkgo/#building-custom-matchers) docs present a modern way to more quickly construct custom matchers.
1377
1378 But writing domain-specific custom matchers is also trivial and highly encouraged. Let's work through an example.
1379
1380 > The `GomegaMatcher` interface is defined in the `types` subpackage.
1381
1382 ### A Custom Matcher: RepresentJSONifiedObject(EXPECTED interface{})
1383
1384 Say you're working on a JSON API and you want to assert that your server returns the correct JSON representation. Rather than marshal/unmarshal JSON in your tests, you want to write an expressive matcher that checks that the received response is a JSON representation for the object in question. This is what the `RepresentJSONifiedObject` matcher could look like:
1385
1386 ```go
1387 package json_response_matcher
1388
1389 import (
1390 "github.com/onsi/gomega/types"
1391
1392 "encoding/json"
1393 "fmt"
1394 "net/http"
1395 "reflect"
1396 )
1397
1398 func RepresentJSONifiedObject(expected interface{}) types.GomegaMatcher {
1399 return &representJSONMatcher{
1400 expected: expected,
1401 }
1402 }
1403
1404 type representJSONMatcher struct {
1405 expected interface{}
1406 }
1407
1408 func (matcher *representJSONMatcher) Match(actual interface{}) (success bool, err error) {
1409 response, ok := actual.(*http.Response)
1410 if !ok {
1411 return false, fmt.Errorf("RepresentJSONifiedObject matcher expects an http.Response")
1412 }
1413
1414 pointerToObjectOfExpectedType := reflect.New(reflect.TypeOf(matcher.expected)).Interface()
1415 err = json.NewDecoder(response.Body).Decode(pointerToObjectOfExpectedType)
1416
1417 if err != nil {
1418 return false, fmt.Errorf("Failed to decode JSON: %s", err.Error())
1419 }
1420
1421 decodedObject := reflect.ValueOf(pointerToObjectOfExpectedType).Elem().Interface()
1422
1423 return reflect.DeepEqual(decodedObject, matcher.expected), nil
1424 }
1425
1426 func (matcher *representJSONMatcher) FailureMessage(actual interface{}) (message string) {
1427 return fmt.Sprintf("Expected\n\t%#v\nto contain the JSON representation of\n\t%#v", actual, matcher.expected)
1428 }
1429
1430 func (matcher *representJSONMatcher) NegatedFailureMessage(actual interface{}) (message string) {
1431 return fmt.Sprintf("Expected\n\t%#v\nnot to contain the JSON representation of\n\t%#v", actual, matcher.expected)
1432 }
1433 ```
1434
1435 Let's break this down:
1436
1437 - Most matchers have a constructor function that returns an instance of the matcher. In this case we've created `RepresentJSONifiedObject`. Where possible, your constructor function should take explicit types or interfaces. For our use case, however, we need to accept any possible expected type so `RepresentJSONifiedObject` takes an argument with the generic `interface{}` type.
1438 - The constructor function then initializes and returns an instance of our matcher: the `representJSONMatcher`. These rarely need to be exported outside of your matcher package.
1439 - The `representJSONMatcher` must satisfy the `GomegaMatcher` interface. It does this by implementing the `Match`, `FailureMessage`, and `NegatedFailureMessage` method:
1440 - If the `GomegaMatcher` receives invalid inputs `Match` returns a non-nil error explaining the problems with the input. This allows Gomega to fail the assertion whether the assertion is for the positive or negative case.
1441 - If the `actual` and `expected` values match, `Match` should return `true`.
1442 - Similarly, if the `actual` and `expected` values do not match, `Match` should return `false`.
1443 - If the `GomegaMatcher` was testing the `Should` case, and `Match` returned `false`, `FailureMessage` will be called to print a message explaining the failure.
1444 - Likewise, if the `GomegaMatcher` was testing the `ShouldNot` case, and `Match` returned `true`, `NegatedFailureMessage` will be called.
1445 - It is guaranteed that `FailureMessage` and `NegatedFailureMessage` will only be called *after* `Match`, so you can save off any state you need to compute the messages in `Match`.
1446 - Finally, it is common for matchers to make extensive use of the `reflect` library to interpret the generic inputs they receive. In this case, the `representJSONMatcher` goes through some `reflect` gymnastics to create a pointer to a new object with the same type as the `expected` object, read and decode JSON from `actual` into that pointer, and then deference the pointer and compare the result to the `expected` object.
1447
1448 You might test drive this matcher while writing it using Ginkgo. Your test might look like:
1449
1450 ```go
1451 package json_response_matcher_test
1452
1453 import (
1454 . "github.com/onsi/ginkgo"
1455 . "github.com/onsi/gomega"
1456 . "jsonresponsematcher"
1457
1458 "bytes"
1459 "encoding/json"
1460 "io/ioutil"
1461 "net/http"
1462 "strings"
1463
1464 "testing"
1465 )
1466
1467 func TestCustomMatcher(t *testing.T) {
1468 RegisterFailHandler(Fail)
1469 RunSpecs(t, "Custom Matcher Suite")
1470 }
1471
1472 type Book struct {
1473 Title string `json:"title"`
1474 Author string `json:"author"`
1475 }
1476
1477 var _ = Describe("RepresentJSONified Object", func() {
1478 var (
1479 book Book
1480 bookJSON []byte
1481 response *http.Response
1482 )
1483
1484 BeforeEach(func() {
1485 book = Book{
1486 Title: "Les Miserables",
1487 Author: "Victor Hugo",
1488 }
1489
1490 var err error
1491 bookJSON, err = json.Marshal(book)
1492 Ω(err).ShouldNot(HaveOccurred())
1493 })
1494
1495 Context("when actual is not an http response", func() {
1496 It("should error", func() {
1497 _, err := RepresentJSONifiedObject(book).Match("not a response")
1498 Ω(err).Should(HaveOccurred())
1499 })
1500 })
1501
1502 Context("when actual is an http response", func() {
1503 BeforeEach(func() {
1504 response = &http.Response{}
1505 })
1506
1507 Context("with a body containing the JSON representation of actual", func() {
1508 BeforeEach(func() {
1509 response.ContentLength = int64(len(bookJSON))
1510 response.Body = ioutil.NopCloser(bytes.NewBuffer(bookJSON))
1511 })
1512
1513 It("should succeed", func() {
1514 Ω(response).Should(RepresentJSONifiedObject(book))
1515 })
1516 })
1517
1518 Context("with a body containing the JSON representation of something else", func() {
1519 BeforeEach(func() {
1520 reader := strings.NewReader(`{}`)
1521 response.ContentLength = int64(reader.Len())
1522 response.Body = ioutil.NopCloser(reader)
1523 })
1524
1525 It("should fail", func() {
1526 Ω(response).ShouldNot(RepresentJSONifiedObject(book))
1527 })
1528 })
1529
1530 Context("with a body containing invalid JSON", func() {
1531 BeforeEach(func() {
1532 reader := strings.NewReader(`floop`)
1533 response.ContentLength = int64(reader.Len())
1534 response.Body = ioutil.NopCloser(reader)
1535 })
1536
1537 It("should error", func() {
1538 _, err := RepresentJSONifiedObject(book).Match(response)
1539 Ω(err).Should(HaveOccurred())
1540 })
1541 })
1542 })
1543 })
1544 ```
1545
1546 This also offers an example of what using the matcher would look like in your tests. Note that testing the cases when the matcher returns an error involves creating the matcher and invoking `Match` manually (instead of using an `Ω` or `Expect` assertion).
1547
1548 ### Aborting Eventually/Consistently
1549
1550 There are sometimes instances where `Eventually` or `Consistently` should stop polling a matcher because the result of the match simply cannot change.
1551
1552 For example, consider a test that looks like:
1553
1554 ```go
1555 Eventually(myChannel).Should(Receive(Equal("bar")))
1556 ```
1557
1558 `Eventually` will repeatedly invoke the `Receive` matcher against `myChannel` until the match succeeds. However, if the channel becomes *closed* there is *no way* for the match to ever succeed. Allowing `Eventually` to continue polling is inefficient and slows the test suite down.
1559
1560 To get around this, a matcher can optionally implement:
1561
1562 ```go
1563 MatchMayChangeInTheFuture(actual interface{}) bool
1564 ```
1565
1566 This is not part of the `GomegaMatcher` interface and, in general, most matchers do not need to implement `MatchMayChangeInTheFuture`.
1567
1568 If implemented, however, `MatchMayChangeInTheFuture` will be called with the appropriate `actual` value by `Eventually` and `Consistently` *after* the call to `Match` during every polling interval. If `MatchMayChangeInTheFuture` returns `true`, `Eventually` and `Consistently` will continue polling. If, however, `MatchMayChangeInTheFuture` returns `false`, `Eventually` and `Consistently` will stop polling and either fail or pass as appropriate.
1569
1570 If you'd like to look at a simple example of `MatchMayChangeInTheFuture` check out [`gexec`'s `Exit` matcher](https://github.com/onsi/gomega/tree/master/gexec/exit_matcher.go). Here, `MatchMayChangeInTheFuture` returns true if the `gexec.Session` under test has not exited yet, but returns false if it has. Because of this, if a process exits with status code 3, but an assertion is made of the form:
1571
1572 ```go
1573 Eventually(session, 30).Should(gexec.Exit(0))
1574 ```
1575
1576 `Eventually` will not block for 30 seconds but will return (and fail, correctly) as soon as the mismatched exit code arrives!
1577
1578 > Note: `Eventually` and `Consistently` only exercise the `MatchMayChangeInTheFuture` method *if* they are passed a bare value. If they are passed functions to be polled it is not possible to guarantee that the return value of the function will not change between polling intervals. In this case, `MatchMayChangeInTheFuture` is not called and the polling continues until either a match is found or the timeout elapses.
1579
1580 ### Contributing to Gomega
1581
1582 Contributions are more than welcome. Either [open an issue](http://github.com/onsi/gomega/issues) for a matcher you'd like to see or, better yet, test drive the matcher and [send a pull request](https://github.com/onsi/gomega/pulls).
1583
1584 When adding a new matcher please mimic the style use in Gomega's current matchers: you should use the `format` package to format your output, put the matcher and its tests in the `matchers` package, and the constructor in the `matchers.go` file in the top-level package.
1585
1586 ## `ghttp`: Testing HTTP Clients
1587 The `ghttp` package provides support for testing http *clients*. The typical pattern in Go for testing http clients entails spinning up an `httptest.Server` using the `net/http/httptest` package and attaching test-specific handlers that perform assertions.
1588
1589 `ghttp` provides `ghttp.Server` - a wrapper around `httptest.Server` that allows you to easily build up a stack of test handlers. These handlers make assertions against the incoming request and return a pre-fabricated response. `ghttp` provides a number of prebuilt handlers that cover the most common assertions. You can combine these handlers to build out full-fledged assertions that test multiple aspects of the incoming requests.
1590
1591 The goal of this documentation is to provide you with an adequate mental model to use `ghttp` correctly. For a full reference of all the available handlers and the various methods on `ghttp.Server` look at the [godoc](https://godoc.org/github.com/onsi/gomega/ghttp) documentation.
1592
1593 ### Making assertions against an incoming request
1594
1595 Let's start with a simple example. Say you are building an API client that provides a `FetchSprockets(category string)` method that makes an http request to a remote server to fetch sprockets of a given category.
1596
1597 For now, let's not worry about the values returned by `FetchSprockets` but simply assert that the correct request was made. Here's the setup for our `ghttp`-based Ginkgo test:
1598
1599 ```go
1600 Describe("The sprockets client", func() {
1601 var server *ghttp.Server
1602 var client *sprockets.Client
1603
1604 BeforeEach(func() {
1605 server = ghttp.NewServer()
1606 client = sprockets.NewClient(server.URL())
1607 })
1608
1609 AfterEach(func() {
1610 //shut down the server between tests
1611 server.Close()
1612 })
1613 })
1614 ```
1615
1616 Note that the server's URL is auto-generated and varies between test runs. Because of this, you must always inject the server URL into your client. Let's add a simple test that asserts that `FetchSprockets` hits the correct endpoint with the correct HTTP verb:
1617
1618 ```go
1619 Describe("The sprockets client", func() {
1620 //...see above
1621
1622 Describe("fetching sprockets", func() {
1623 BeforeEach(func() {
1624 server.AppendHandlers(
1625 ghttp.VerifyRequest("GET", "/sprockets"),
1626 )
1627 })
1628
1629 It("should make a request to fetch sprockets", func() {
1630 client.FetchSprockets("")
1631 Ω(server.ReceivedRequests()).Should(HaveLen(1))
1632 })
1633 })
1634 })
1635 ```
1636
1637 Here we append a `VerifyRequest` handler to the `server` and call `client.FetchSprockets`. This call (assuming it's a blocking call) will make a round-trip to the test `server` before returning. The test `server` receives the request and passes it through the `VerifyRequest` handler which will validate that the request is a `GET` request hitting the `/sprockets` endpoint. If it's not, the test will fail.
1638
1639 Note that the test can pass trivially if `client.FetchSprockets()` doesn't actually make a request. To guard against this you can assert that the `server` has actually received a request. All the requests received by the server are saved off and made available via `server.ReceivedRequests()`. We use this to assert that there should have been exactly one received requests.
1640
1641 > Guarding against the trivial "false positive" case outlined above isn't really necessary. Just good practice when test *driving*.
1642
1643 Let's add some more to our example. Let's make an assertion that `FetchSprockets` can request sprockets filtered by a particular category:
1644
1645 ```go
1646 Describe("The sprockets client", func() {
1647 //...see above
1648
1649 Describe("fetching sprockets", func() {
1650 BeforeEach(func() {
1651 server.AppendHandlers(
1652 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators"),
1653 )
1654 })
1655
1656 It("should make a request to fetch sprockets", func() {
1657 client.FetchSprockets("encabulators")
1658 Ω(server.ReceivedRequests()).Should(HaveLen(1))
1659 })
1660 })
1661 })
1662 ```
1663
1664 `ghttp.VerifyRequest` takes an optional third parameter that is matched against the request `URL`'s `RawQuery`.
1665
1666 Let's extend the example some more. In addition to asserting that the request is a `GET` request to the correct endpoint with the correct query params, let's also assert that it includes the correct `BasicAuth` information and a correct custom header. Here's the complete example:
1667
1668 ```go
1669 Describe("The sprockets client", func() {
1670 var (
1671 server *ghttp.Server
1672 client *sprockets.Client
1673 username, password string
1674 )
1675
1676 BeforeEach(func() {
1677 username, password = "gopher", "tacoshell"
1678 server = ghttp.NewServer()
1679 client = sprockets.NewClient(server.URL(), username, password)
1680 })
1681
1682 AfterEach(func() {
1683 server.Close()
1684 })
1685
1686 Describe("fetching sprockets", func() {
1687 BeforeEach(func() {
1688 server.AppendHandlers(
1689 ghttp.CombineHandlers(
1690 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators"),
1691 ghttp.VerifyBasicAuth(username, password),
1692 ghttp.VerifyHeader(http.Header{
1693 "X-Sprocket-API-Version": []string{"1.0"},
1694 }),
1695 )
1696 )
1697 })
1698
1699 It("should make a request to fetch sprockets", func() {
1700 client.FetchSprockets("encabulators")
1701 Ω(server.ReceivedRequests()).Should(HaveLen(1))
1702 })
1703 })
1704 })
1705 ```
1706
1707 This example *combines* multiple `ghttp` verify handlers using `ghttp.CombineHandlers`. Under the hood, this returns a new handler that wraps and invokes the three passed in verify handlers. The request sent by the client will pass through each of these verify handlers and must pass them all for the test to pass.
1708
1709 Note that you can easily add your own verify handler into the mix. Just pass in a regular `http.HandlerFunc` and make assertions against the received request.
1710
1711 > It's important to understand that you must pass `AppendHandlers` **one** handler *per* incoming request (see [below](#handling-multiple-requests)). In order to apply multiple handlers to a single request we must first combine them with `ghttp.CombineHandlers` and then pass that *one* wrapper handler in to `AppendHandlers`.
1712
1713 ### Providing responses
1714
1715 So far, we've only made assertions about the outgoing request. Clients are also responsible for parsing responses and returning valid data. Let's say that `FetchSprockets()` returns two things: a slice `[]Sprocket` and an `error`. Here's what a happy path test that asserts the correct data is returned might look like:
1716
1717 ```go
1718 Describe("The sprockets client", func() {
1719 //...
1720 Describe("fetching sprockets", func() {
1721 BeforeEach(func() {
1722 server.AppendHandlers(
1723 ghttp.CombineHandlers(
1724 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators"),
1725 ghttp.VerifyBasicAuth(username, password),
1726 ghttp.VerifyHeader(http.Header{
1727 "X-Sprocket-API-Version": []string{"1.0"},
1728 }),
1729 ghttp.RespondWith(http.StatusOK, `[
1730 {"name": "entropic decoupler", "color": "red"},
1731 {"name": "defragmenting ramjet", "color": "yellow"}
1732 ]`),
1733 )
1734 )
1735 })
1736
1737 It("should make a request to fetch sprockets", func() {
1738 sprockets, err := client.FetchSprockets("encabulators")
1739 Ω(err).ShouldNot(HaveOccurred())
1740 Ω(sprockets).Should(Equal([]Sprocket{
1741 sprockets.Sprocket{Name: "entropic decoupler", Color: "red"},
1742 sprockets.Sprocket{Name: "defragmenting ramjet", Color: "yellow"},
1743 }))
1744 })
1745 })
1746 })
1747 ```
1748
1749 We use `ghttp.RespondWith` to specify the response return by the server. In this case we're passing back a status code of `200` (`http.StatusOK`) and a pile of JSON. We then assert, in the test, that the client succeeds and returns the correct set of sprockets.
1750
1751 The fact that details of the JSON encoding are bleeding into this test is somewhat unfortunate, and there's a lot of repetition going on. `ghttp` provides a `RespondWithJSONEncoded` handler that accepts an arbitrary object and JSON encodes it for you. Here's a cleaner test:
1752
1753 ```go
1754 Describe("The sprockets client", func() {
1755 //...
1756 Describe("fetching sprockets", func() {
1757 var returnedSprockets []Sprocket
1758 BeforeEach(func() {
1759 returnedSprockets = []Sprocket{
1760 sprockets.Sprocket{Name: "entropic decoupler", Color: "red"},
1761 sprockets.Sprocket{Name: "defragmenting ramjet", Color: "yellow"},
1762 }
1763
1764 server.AppendHandlers(
1765 ghttp.CombineHandlers(
1766 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators"),
1767 ghttp.VerifyBasicAuth(username, password),
1768 ghttp.VerifyHeader(http.Header{
1769 "X-Sprocket-API-Version": []string{"1.0"},
1770 }),
1771 ghttp.RespondWithJSONEncoded(http.StatusOK, returnedSprockets),
1772 )
1773 )
1774 })
1775
1776 It("should make a request to fetch sprockets", func() {
1777 sprockets, err := client.FetchSprockets("encabulators")
1778 Ω(err).ShouldNot(HaveOccurred())
1779 Ω(sprockets).Should(Equal(returnedSprockets))
1780 })
1781 })
1782 })
1783 ```
1784
1785 ### Testing different response scenarios
1786
1787 Our test currently only handles the happy path where the server returns a `200`. We should also test a handful of sad paths. In particular, we'd like to return a `SprocketsErrorNotFound` error when the server `404`s and a `SprocketsErrorUnauthorized` error when the server returns a `401`. But how to do this without redefining our server handler three times?
1788
1789 `ghttp` provides `RespondWithPtr` and `RespondWithJSONEncodedPtr` for just this use case. Both take *pointers* to status codes and respond bodies (objects for the `JSON` case). Here's the more complete test:
1790
1791 ```go
1792 Describe("The sprockets client", func() {
1793 //...
1794 Describe("fetching sprockets", func() {
1795 var returnedSprockets []Sprocket
1796 var statusCode int
1797
1798 BeforeEach(func() {
1799 returnedSprockets = []Sprocket{
1800 sprockets.Sprocket{Name: "entropic decoupler", Color: "red"},
1801 sprockets.Sprocket{Name: "defragmenting ramjet", Color: "yellow"},
1802 }
1803
1804 server.AppendHandlers(
1805 ghttp.CombineHandlers(
1806 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators"),
1807 ghttp.VerifyBasicAuth(username, password),
1808 ghttp.VerifyHeader(http.Header{
1809 "X-Sprocket-API-Version": []string{"1.0"},
1810 }),
1811 ghttp.RespondWithJSONEncodedPtr(&statusCode, &returnedSprockets),
1812 )
1813 )
1814 })
1815
1816 Context("when the request succeeds", func() {
1817 BeforeEach(func() {
1818 statusCode = http.StatusOK
1819 })
1820
1821 It("should return the fetched sprockets without erroring", func() {
1822 sprockets, err := client.FetchSprockets("encabulators")
1823 Ω(err).ShouldNot(HaveOccurred())
1824 Ω(sprockets).Should(Equal(returnedSprockets))
1825 })
1826 })
1827
1828 Context("when the response is unauthorized", func() {
1829 BeforeEach(func() {
1830 statusCode = http.StatusUnauthorized
1831 })
1832
1833 It("should return the SprocketsErrorUnauthorized error", func() {
1834 sprockets, err := client.FetchSprockets("encabulators")
1835 Ω(sprockets).Should(BeEmpty())
1836 Ω(err).Should(MatchError(SprocketsErrorUnauthorized))
1837 })
1838 })
1839
1840 Context("when the response is not found", func() {
1841 BeforeEach(func() {
1842 statusCode = http.StatusNotFound
1843 })
1844
1845 It("should return the SprocketsErrorNotFound error", func() {
1846 sprockets, err := client.FetchSprockets("encabulators")
1847 Ω(sprockets).Should(BeEmpty())
1848 Ω(err).Should(MatchError(SprocketsErrorNotFound))
1849 })
1850 })
1851 })
1852 })
1853 ```
1854
1855 In this way, the status code and returned value (not shown here) can be changed in sub-contexts without having to modify the original test setup.
1856
1857 ### Handling multiple requests
1858
1859 So far, we've only seen examples where one request is made per test. `ghttp` supports handling *multiple* requests too. `server.AppendHandlers` can be passed multiple handlers and these handlers are evaluated in order as requests come in.
1860
1861 This can be helpful in cases where it is not possible (or desirable) to have calls to the client under test only generate *one* request. A common example is pagination. If the sprockets API is paginated it may be desirable for `FetchSprockets` to provide a simpler interface that simply fetches all available sprockets.
1862
1863 Here's what a test might look like:
1864
1865 ```go
1866 Describe("fetching sprockets from a paginated endpoint", func() {
1867 var returnedSprockets []Sprocket
1868 var firstResponse, secondResponse PaginatedResponse
1869
1870 BeforeEach(func() {
1871 returnedSprockets = []Sprocket{
1872 sprockets.Sprocket{Name: "entropic decoupler", Color: "red"},
1873 sprockets.Sprocket{Name: "defragmenting ramjet", Color: "yellow"},
1874 sprockets.Sprocket{Name: "parametric demuxer", Color: "blue"},
1875 }
1876
1877 firstResponse = sprockets.PaginatedResponse{
1878 Sprockets: returnedSprockets[0:2], //first batch
1879 PaginationToken: "get-second-batch", //some opaque non-empty token
1880 }
1881
1882 secondResponse = sprockets.PaginatedResponse{
1883 Sprockets: returnedSprockets[2:], //second batch
1884 PaginationToken: "", //signifies the last batch
1885 }
1886
1887 server.AppendHandlers(
1888 ghttp.CombineHandlers(
1889 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators"),
1890 ghttp.RespondWithJSONEncoded(http.StatusOK, firstResponse),
1891 ),
1892 ghttp.CombineHandlers(
1893 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators&pagination-token=get-second-batch"),
1894 ghttp.RespondWithJSONEncoded(http.StatusOK, secondResponse),
1895 ),
1896 )
1897 })
1898
1899 It("should fetch all the sprockets", func() {
1900 sprockets, err := client.FetchSprockets("encabulators")
1901 Ω(err).ShouldNot(HaveOccurred())
1902 Ω(sprockets).Should(Equal(returnedSprockets))
1903 })
1904 })
1905 ```
1906
1907 By default the `ghttp` server fails the test if the number of requests received exceeds the number of handlers registered, so this test ensures that the `client` stops sending requests after receiving the second (and final) set of paginated data.
1908
1909 ### MUXing Routes to Handlers
1910
1911 `AppendHandlers` allows you to make ordered assertions about incoming requests. This places a strong constraint on all incoming requests: namely that exactly the right requests have to arrive in exactly the right order and that no additional requests are allowed.
1912
1913 One can take a different testing strategy, however. Instead of asserting that requests come in in a predefined order, you may which to build a test server that can handle arbitrarily many requests to a set of predefined routes. In fact, there may be some circumstances where you want to make ordered assertions on *some* requests (via `AppendHandlers`) but still support sending particular responses to *other* requests that may interleave the ordered assertions.
1914
1915 `ghttp` supports these sorts of usecases via `server.RouteToHandler(method, path, handler)`.
1916
1917 Let's cook up an example. Perhaps, instead of authenticating via basic auth our sprockets client logs in and fetches a token from the server when performing requests that require authentication. We could pepper our `AppendHandlers` calls with a handler that handles these requests (this is not a terrible idea, of course!) *or* we could set up a single route at the top of our tests.
1918
1919 Here's what such a test might look like:
1920
1921 ```go
1922 Describe("CRUDing sprockes", func() {
1923 BeforeEach(func() {
1924 server.RouteToHandler("POST", "/login", ghttp.CombineHandlers(
1925 ghttp.VerifyRequest("POST", "/login", "user=bob&password=password"),
1926 ghttp.RespondWith(http.StatusOK, "your-auth-token"),
1927 ))
1928 })
1929 Context("GETting sprockets", func() {
1930 var returnedSprockets []Sprocket
1931
1932 BeforeEach(func() {
1933 returnedSprockets = []Sprocket{
1934 sprockets.Sprocket{Name: "entropic decoupler", Color: "red"},
1935 sprockets.Sprocket{Name: "defragmenting ramjet", Color: "yellow"},
1936 sprockets.Sprocket{Name: "parametric demuxer", Color: "blue"},
1937 }
1938
1939 server.AppendHandlers(
1940 ghttp.CombineHandlers(
1941 ghttp.VerifyRequest("GET", "/sprockets", "category=encabulators"),
1942 ghttp.RespondWithJSONEncoded(http.StatusOK, returnedSprockets),
1943 ),
1944 )
1945 })
1946
1947 It("should fetch all the sprockets", func() {
1948 sprockets, err := client.FetchSprockes("encabulators")
1949 Ω(err).ShouldNot(HaveOccurred())
1950 Ω(sprockets).Should(Equal(returnedSprockets))
1951 })
1952 })
1953
1954 Context("POSTing sprockets", func() {
1955 var sprocketToSave Sprocket
1956 BeforeEach(func() {
1957 sprocketToSave = sprockets.Sprocket{Name: "endothermic penambulator", Color: "purple"}
1958
1959 server.AppendHandlers(
1960 ghttp.CombineHandlers(
1961 ghttp.VerifyRequest("POST", "/sprocket", "token=your-auth-token"),
1962 ghttp.VerifyJSONRepresenting(sprocketToSave)
1963 ghttp.RespondWithJSONEncoded(http.StatusOK, nil),
1964 ),
1965 )
1966 })
1967
1968 It("should save the sprocket", func() {
1969 err := client.SaveSprocket(sprocketToSave)
1970 Ω(err).ShouldNot(HaveOccurred())
1971 })
1972 })
1973 })
1974 ```
1975
1976 Here, saving a sprocket triggers authentication, which is handled by the registered `RouteToHandler` handler whereas fetching the list of sprockets does not.
1977
1978 > `RouteToHandler` can take either a string as a route (as seen in this example) or a `regexp.Regexp`.
1979
1980 ### Allowing unhandled requests
1981
1982 By default, `ghttp`'s server marks the test as failed if a request is made for which there is no registered handler.
1983
1984 It is sometimes useful to have a fake server that simply returns a fixed status code for all unhandled incoming requests. `ghttp` supports this: just call `server.SetAllowUnhandledRequests(true)` and `server.SetUnhandledRequestStatusCode(statusCode)`, passing whatever status code you'd like to return.
1985
1986 In addition to returning the registered status code, `ghttp`'s server will also save all received requests. These can be accessed by calling `server.ReceivedRequests()`. This is useful for cases where you may want to make assertions against requests *after* they've been made.
1987
1988 To bring it all together: there are three ways to instruct a `ghttp` server to handle requests: you can map routes to handlers using `RouteToHandler`, you can append handlers via `AppendHandlers`, and you can `SetAllowUnhandledRequests` and specify the status code by calling `SetUnhandledRequestStatusCode`.
1989
1990 When a `ghttp` server receives a request it first checks against the set of handlers registered via `RouteToHandler` if there is no such handler it proceeds to pop an `AppendHandlers` handler off the stack, if the stack of ordered handlers is empty, it will check whether `GetAllowUnhandledRequests` returns `true` or `false`. If `false` the test fails. If `true`, a response is sent with whatever `GetUnhandledRequestStatusCode` returns.
1991
1992 ## `gbytes`: Testing Streaming Buffers
1993
1994 `gbytes` implements `gbytes.Buffer` - an `io.WriteCloser` that captures all input to an in-memory buffer.
1995
1996 When used in concert with the `gbytes.Say` matcher, the `gbytes.Buffer` allows you make *ordered* assertions against streaming data.
1997
1998 What follows is a contrived example. `gbytes` is best paired with [`gexec`](#gexec-testing-external-processes).
1999
2000 Say you have an integration test that is streaming output from an external API. You can feed this stream into a `gbytes.Buffer` and make ordered assertions like so:
2001
2002 ```go
2003 Describe("attach to the data stream", func() {
2004 var (
2005 client *apiclient.Client
2006 buffer *gbytes.Buffer
2007 )
2008 BeforeEach(func() {
2009 buffer = gbytes.NewBuffer()
2010 client := apiclient.New()
2011 go client.AttachToDataStream(buffer)
2012 })
2013
2014 It("should stream data", func() {
2015 Eventually(buffer).Should(gbytes.Say(`Attached to stream as client \d+`))
2016
2017 client.ReticulateSplines()
2018 Eventually(buffer).Should(gbytes.Say(`reticulating splines`))
2019 client.EncabulateRetros(7)
2020 Eventually(buffer).Should(gbytes.Say(`encabulating 7 retros`))
2021 })
2022 })
2023 ```
2024
2025 These assertions will only pass if the strings passed to `Say` (which are interpreted as regular expressions - make sure to escape characters appropriately!) appear in the buffer. An opaque read cursor (that you cannot access or modify) is fast-forwarded as successful assertions are made. So, for example:
2026
2027 ```go
2028 Eventually(buffer).Should(gbytes.Say(`reticulating splines`))
2029 Consistently(buffer).ShouldNot(gbytes.Say(`reticulating splines`))
2030 ```
2031
2032 will (counterintuitively) pass. This allows you to write tests like:
2033
2034 ```go
2035 client.ReticulateSplines()
2036 Eventually(buffer).Should(gbytes.Say(`reticulating splines`))
2037 client.ReticulateSplines()
2038 Eventually(buffer).Should(gbytes.Say(`reticulating splines`))
2039 ```
2040
2041 and ensure that the test is correctly asserting that `reticulating splines` appears *twice*.
2042
2043 At any time, you can access the entire contents written to the buffer via `buffer.Contents()`. This includes *everything* ever written to the buffer regardless of the current position of the read cursor.
2044
2045 ### Handling branches
2046
2047 Sometimes (rarely!) you must write a test that must perform different actions depending on the output streamed to the buffer. This can be accomplished using `buffer.Detect`. Here's a contrived example:
2048
2049 ```go
2050 func LoginIfNecessary() {
2051 client.Authorize()
2052 select {
2053 case <-buffer.Detect("You are not logged in"):
2054 client.Login()
2055 case <-buffer.Detect("Success"):
2056 return
2057 case <-time.After(time.Second):
2058 ginkgo.Fail("timed out waiting for output")
2059 }
2060 buffer.CancelDetects()
2061 }
2062 ```
2063
2064 `buffer.Detect` takes a string (interpreted as a regular expression) and returns a channel that will fire *once* if the requested string is detected. Upon detection, the buffer's opaque read cursor is fast-forwarded so subsequent uses of `gbytes.Say` will pick up from where the succeeding `Detect` left off. You *must* call `buffer.CancelDetects()` to clean up afterwards (`buffer` spawns one goroutine per call to `Detect`).
2065
2066 ### Testing `io.Reader`s, `io.Writer`s, and `io.Closer`s
2067
2068 Implementations of `io.Reader`, `io.Writer`, and `io.Closer` are expected to be blocking. This makes the following class of tests unsafe:
2069
2070 ```go
2071 It("should read something", func() {
2072 p := make([]byte, 5)
2073 _, err := reader.Read(p) //unsafe! this could block forever
2074 Ω(err).ShouldNot(HaveOccurred())
2075 Ω(p).Should(Equal([]byte("abcde")))
2076 })
2077 ```
2078
2079 It is safer to wrap `io.Reader`s, `io.Writer`s, and `io.Closer`s with explicit timeouts. You can do this with `gbytes.TimeoutReader`, `gbytes.TimeoutWriter`, and `gbytes.TimeoutCloser` like so:
2080
2081 ```go
2082 It("should read something", func() {
2083 p := make([]byte, 5)
2084 _, err := gbytes.TimeoutReader(reader, time.Second).Read(p)
2085 Ω(err).ShouldNot(HaveOccurred())
2086 Ω(p).Should(Equal([]byte("abcde")))
2087 })
2088 ```
2089
2090 The `gbytes` wrappers will return `gbytes.ErrTimeout` if a timeout occurs.
2091
2092 In the case of `io.Reader`s you can leverage the `Say` matcher and the functionality of `gbytes.Buffer` by building a `gbytes.Buffer` that reads from the `io.Reader` asynchronously. You can do this with the `gbytes` package like so:
2093
2094 ```go
2095 It("should read something", func() {
2096 Eventually(gbytes.BufferReader(reader)).Should(gbytes.Say("abcde"))
2097 })
2098 ```
2099
2100 `gbytes.BufferReader` takes an `io.Reader` and returns a `gbytes.Buffer`. Under the hood an `io.Copy` goroutine is launched to copy data from the `io.Reader` into the `gbytes.Buffer`. The `gbytes.Buffer` is closed when the `io.Copy` completes. Because the `io.Copy` is launched asynchronously you *must* make assertions against the reader using `Eventually`.
2101
2102
2103 ## `gexec`: Testing External Processes
2104
2105 `gexec` simplifies testing external processes. It can help you [compile go binaries](#compiling-external-binaries), [start external processes](#starting-external-processes), [send signals and wait for them to exit](#sending-signals-and-waiting-for-the-process-to-exit), make [assertions against the exit code](#asserting-against-exit-code), and stream output into `gbytes.Buffer`s to allow you [make assertions against output](#making-assertions-against-the-process-output).
2106
2107 ### Compiling external binaries
2108
2109 You use `gexec.Build()` to compile Go binaries. These are built using `go build` and are stored off in a temporary directory. You'll want to `gexec.CleanupBuildArtifacts()` when you're done with the test.
2110
2111 A common pattern is to compile binaries once at the beginning of the test using `BeforeSuite` and to clean up once at the end of the test using `AfterSuite`:
2112
2113 ```go
2114 var pathToSprocketCLI string
2115
2116 BeforeSuite(func() {
2117 var err error
2118 pathToSprocketCLI, err = gexec.Build("github.com/spacely/sprockets")
2119 Ω(err).ShouldNot(HaveOccurred())
2120 })
2121
2122 AfterSuite(func() {
2123 gexec.CleanupBuildArtifacts()
2124 })
2125 ```
2126
2127 > By default, `gexec.Build` uses the GOPATH specified in your environment. You can also use `gexec.BuildIn(gopath string, packagePath string)` to specify a custom GOPATH for the build command. This is useful to, for example, build a binary against its vendored Godeps.
2128
2129 > You can specify arbitrary environment variables for the build command – such as GOOS and GOARCH for building on other platforms – using `gexec.BuildWithEnvironment(packagePath string, envs []string)`.
2130
2131 ### Starting external processes
2132
2133 `gexec` provides a `Session` that wraps `exec.Cmd`. `Session` includes a number of features that will be explored in the next few sections. You create a `Session` by instructing `gexec` to start a command:
2134
2135 ```go
2136 command := exec.Command(pathToSprocketCLI, "-api=127.0.0.1:8899")
2137 session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
2138 Ω(err).ShouldNot(HaveOccurred())
2139 ```
2140
2141 `gexec.Start` calls `command.Start` for you and forwards the command's `stdout` and `stderr` to `io.Writer`s that you provide. In the code above, we pass in Ginkgo's `GinkgoWriter`. This makes working with external processes quite convenient: when a test passes no output is printed to screen, however if a test fails then any output generated by the command will be provided.
2142
2143 > If you want to see all your output regardless of test status, just run `ginkgo` in verbose mode (`-v`) - now everything written to `GinkgoWriter` makes it onto the screen.
2144
2145 ### Sending signals and waiting for the process to exit
2146
2147 `gexec.Session` makes it easy to send signals to your started command:
2148
2149 ```go
2150 session.Kill() //sends SIGKILL
2151 session.Interrupt() //sends SIGINT
2152 session.Terminate() //sends SIGTERM
2153 session.Signal(signal) //sends the passed in os.Signal signal
2154 ```
2155
2156 If the process has already exited these signal calls are no-ops.
2157
2158 In addition to starting the wrapped command, `gexec.Session` also *monitors* the command until it exits. You can ask `gexec.Session` to `Wait` until the process exits:
2159
2160 ```go
2161 session.Wait()
2162 ```
2163
2164 this will block until the session exits and will *fail* if it does not exit within the default `Eventually` timeout. You can override this timeout by specifying a custom one:
2165
2166 ```go
2167 session.Wait(5 * time.Second)
2168 ```
2169
2170 > Though you can access the wrapped command using `session.Command` you should not attempt to `Wait` on it yourself. `gexec` has already called `Wait` in order to monitor your process for you.
2171
2172 > Under the hood `session.Wait` simply uses `Eventually`.
2173
2174
2175 Since the signalling methods return the session you can chain calls together:
2176
2177 ```go
2178 session.Terminate().Wait()
2179 ```
2180
2181 will send `SIGTERM` and then wait for the process to exit.
2182
2183 ### Asserting against exit code
2184
2185 Once a session has exited you can fetch its exit code with `session.ExitCode()`. You can subsequently make assertions against the exit code.
2186
2187 A more idiomatic way to assert that a command has exited is to use the `gexec.Exit()` matcher:
2188
2189 ```go
2190 Eventually(session).Should(Exit())
2191 ```
2192
2193 Will verify that the `session` exits within `Eventually`'s default timeout. You can assert that the process exits with a specified exit code too:
2194
2195 ```go
2196 Eventually(session).Should(Exit(0))
2197 ```
2198
2199 > If the process has not exited yet, `session.ExitCode()` returns `-1`
2200
2201 ### Making assertions against the process output
2202
2203 In addition to streaming output to the passed in `io.Writer`s (the `GinkgoWriter` in our example above), `gexec.Start` attaches `gbytes.Buffer`s to the command's output streams. These are available on the `session` object via:
2204
2205 ```go
2206 session.Out //a gbytes.Buffer connected to the command's stdout
2207 session.Err //a gbytes.Buffer connected to the command's stderr
2208 ```
2209
2210 This allows you to make assertions against the stream of output:
2211
2212 ```go
2213 Eventually(session.Out).Should(gbytes.Say("hello [A-Za-z], nice to meet you"))
2214 Eventually(session.Err).Should(gbytes.Say("oops!"))
2215 ```
2216
2217 Since `gexec.Session` is a `gbytes.BufferProvider` that provides the `Out` buffer you can write assertions against `stdout` output like so:
2218
2219 ```go
2220 Eventually(session).Should(gbytes.Say("hello [A-Za-z], nice to meet you"))
2221 ```
2222
2223 Using the `Say` matcher is convenient when making *ordered* assertions against a stream of data generated by a live process. Sometimes, however, all you need is to
2224 wait for the process to exit and then make assertions against the entire contents of its output. Since `Wait()` returns `session` you can wait for the process to exit, then grab all its stdout as a `[]byte` buffer with a simple one-liner:
2225
2226 ```go
2227 Ω(session.Wait().Out.Contents()).Should(ContainSubstring("finished successfully"))
2228 ```
2229
2230 ### Signaling all processes
2231 `gexec` provides methods to track and send signals to all processes that it starts.
2232
2233 ```go
2234 gexec.Kill() //sends SIGKILL to all processes
2235 gexec.Terminate() //sends SIGTERM to all processes
2236 gexec.Signal(int) //sends the passed in os.Signal signal to all the processes
2237 gexec.Interrupt() //sends SIGINT to all processes
2238 ```
2239
2240 If the any of the processes have already exited these signal calls are no-ops.
2241
2242 `gexec` also provides methods to cleanup and wait for all the processes it started.
2243
2244 ```go
2245 gexec.KillAndWait()
2246 gexec.TerminateAndWait()
2247 ```
2248
2249 You can specify a custom timeout by:
2250
2251 ```go
2252 gexec.KillAndWait(5 * time.Second)
2253 gexec.TerminateAndWait(2 * time.Second)
2254 ```
2255
2256 The timeout is applied for each of the processes.
2257
2258 It is considered good practice to ensure all of your processes have been killed before the end of the test suite. If you are using `ginkgo` you can use:
2259
2260 ```go
2261 AfterSuite(func(){
2262 gexec.KillAndWait()
2263 })
2264 ```
2265
2266 Due to the global nature of these methods, keep in mind that signaling processes will affect all processes started by `gexec`, in any context. For example if these methods where used in an `AfterEach`, then processes started in `BeforeSuite` would also be signaled.
2267
2268 ## `gstruct`: Testing Complex Data Types
2269
2270 `gstruct` simplifies testing large and nested structs and slices. It is used for building up complex matchers that apply different tests to each field or element.
2271
2272 ### Testing type `struct`
2273
2274 `gstruct` provides the `FieldsMatcher` through the `MatchAllFields` and `MatchFields` functions for applying a separate matcher to each field of a struct:
2275
2276 ```go
2277 actual := struct{
2278 A int
2279 B bool
2280 C string
2281 }{5, true, "foo"}
2282 Expect(actual).To(MatchAllFields(Fields{
2283 "A": BeNumerically("<", 10),
2284 "B": BeTrue(),
2285 "C": Equal("foo"),
2286 }))
2287 ```
2288
2289 `MatchAllFields` requires that every field is matched, and each matcher is mapped to a field. To match a subset or superset of a struct, you should use the `MatchFields` function with the `IgnoreExtras` and `IgnoreMissing` options. `IgnoreExtras` will ignore fields that don't map to a matcher, e.g.
2290
2291 ```go
2292 Expect(actual).To(MatchFields(IgnoreExtras, Fields{
2293 "A": BeNumerically("<", 10),
2294 "B": BeTrue(),
2295 // Ignore lack of "C" in the matcher.
2296 }))
2297 ```
2298
2299 `IgnoreMissing` will ignore matchers that don't map to a field, e.g.
2300
2301 ```go
2302 Expect(actual).To(MatchFields(IgnoreMissing, Fields{
2303 "A": BeNumerically("<", 10),
2304 "B": BeTrue(),
2305 "C": Equal("foo"),
2306 "D": Equal("bar"), // Ignored, since actual.D does not exist.
2307 }))
2308 ```
2309
2310 The options can be combined with the binary or: `IgnoreMissing|IgnoreExtras`.
2311
2312 ### Testing type slice
2313
2314 `gstruct` provides the `ElementsMatcher` through the `MatchAllElements` and `MatchElements` function for applying a separate matcher to each element, identified by an `Identifier` function:
2315
2316 ```go
2317 actual := []string{
2318 "A: foo bar baz",
2319 "B: once upon a time",
2320 "C: the end",
2321 }
2322 id := func(element interface{}) string {
2323 return string(element.(string)[0])
2324 }
2325 Expect(actual).To(MatchAllElements(id, Elements{
2326 "A": Not(BeZero()),
2327 "B": MatchRegexp("[A-Z]: [a-z ]+"),
2328 "C": ContainSubstring("end"),
2329 }))
2330 ```
2331
2332 `MatchAllElements` requires that there is a 1:1 mapping from every element to every matcher. To match a subset or superset of elements, you should use the `MatchElements` function with the `IgnoreExtras` and `IgnoreMissing` options. `IgnoreExtras` will ignore elements that don't map to a matcher, e.g.
2333
2334 ```go
2335 Expect(actual).To(MatchElements(id, IgnoreExtras, Elements{
2336 "A": Not(BeZero()),
2337 "B": MatchRegexp("[A-Z]: [a-z ]+"),
2338 // Ignore lack of "C" in the matcher.
2339 }))
2340 ```
2341
2342 `IgnoreMissing` will ignore matchers that don't map to an element, e.g.
2343
2344 ```go
2345 Expect(actual).To(MatchElements(id, IgnoreMissing, Elements{
2346 "A": Not(BeZero()),
2347 "B": MatchRegexp("[A-Z]: [a-z ]+"),
2348 "C": ContainSubstring("end"),
2349 "D": Equal("bar"), // Ignored, since actual.D does not exist.
2350 }))
2351 ```
2352
2353 You can also use the flag `AllowDuplicates` to permit multiple elements in your slice to map to a single key and matcher in your fields (this flag is not meaningful when applied to structs).
2354
2355 ```go
2356 everyElementID := func(element interface{}) string {
2357 return "a constant" // Every element will map to the same key in this case; you can group them into multiple keys, however.
2358 }
2359 Expect(actual).To(MatchElements(everyElementID, AllowDuplicates, Elements{
2360 "a constant": ContainSubstring(": "), // Because every element passes this test
2361 }))
2362 Expect(actual).NotTo(MatchElements(everyElementID, AllowDuplicates, Elements{
2363 "a constant": ContainSubstring("foo bar baz"), // Only the first element passes this test
2364 }))
2365 ```
2366
2367 The options can be combined with the binary or: `IgnoreMissing|IgnoreExtras|AllowDuplicates`.
2368
2369 Additionally, `gstruct` provides `MatchAllElementsWithIndex` and `MatchElementsWithIndex` function for applying a matcher with index to each element, identified by an `IdentifierWithIndex` function. A helper function is also included with `gstruct` called `IndexIdentity` that provides the functionality of the just using the index as your identifier as seen below.
2370
2371 ```go
2372 actual := []string{
2373 "A: foo bar baz",
2374 "B: once upon a time",
2375 "C: the end",
2376 }
2377 id := func(index int, _ interface{}) string {
2378 return strconv.Itoa(index)
2379 }
2380 Expect(actual).To(MatchAllElementsWithIndex(id, Elements{
2381 "0": Not(BeZero()),
2382 "1": MatchRegexp("[A-Z]: [a-z ]+"),
2383 "2": ContainSubstring("end"),
2384 }))
2385 // IndexIdentity is a helper function equivalent to id in this example
2386 Expect(actual).To(MatchAllElementsWithIndex(IndexIdentity, Elements{
2387 "0": Not(BeZero()),
2388 "1": MatchRegexp("[A-Z]: [a-z ]+"),
2389 "2": ContainSubstring("end"),
2390 }))
2391 ```
2392
2393 The `WithIndex` variants take the same options as the other functions.
2394
2395 ### Testing type `map`
2396
2397 All of the `*Fields` functions and types have a corresponding definitions `*Keys` which can perform analogous tests against map types:
2398
2399 ```go
2400 actual := map[string]string{
2401 "A": "correct",
2402 "B": "incorrect",
2403 }
2404
2405 // fails, because `actual` includes the key B
2406 Expect(actual).To(MatchAllKeys(Keys{
2407 "A": Equal("correct"),
2408 }))
2409
2410 // passes
2411 Expect(actual).To(MatchAllKeys(Keys{
2412 "A": Equal("correct"),
2413 "B": Equal("incorrect"),
2414 }))
2415
2416 // passes
2417 Expect(actual).To(MatchKeys(IgnoreMissing, Keys{
2418 "A": Equal("correct"),
2419 "B": Equal("incorrect"),
2420 "C": Equal("whatever"), // ignored, because `actual` doesn't have this key
2421 }))
2422 ```
2423
2424 ### Testing pointer values
2425
2426 `gstruct` provides the `PointTo` function to apply a matcher to the value pointed-to. It will fail if the pointer value is `nil`:
2427
2428 foo := 5
2429 Expect(&foo).To(PointTo(Equal(5)))
2430 var bar *int
2431 Expect(bar).NotTo(PointTo(BeNil()))
2432
2433 ### Putting it all together: testing complex structures
2434
2435 The `gstruct` matchers are intended to be composable, and can be combined to apply fuzzy-matching to large and deeply nested structures. The additional `Ignore()` and `Reject()` matchers are provided for ignoring (always succeed) fields and elements, or rejecting (always fail) fields and elements.
2436
2437 Example:
2438
2439 ```go
2440 coreID := func(element interface{}) string {
2441 return strconv.Itoa(element.(CoreStats).Index)
2442 }
2443 Expect(actual).To(MatchAllFields(Fields{
2444 "Name": Ignore(),
2445 "StartTime": BeTemporally(">=", time.Now().Add(-100 * time.Hour)),
2446 "CPU": PointTo(MatchAllFields(Fields{
2447 "Time": BeTemporally(">=", time.Now().Add(-time.Hour)),
2448 "UsageNanoCores": BeNumerically("~", 1E9, 1E8),
2449 "UsageCoreNanoSeconds": BeNumerically(">", 1E6),
2450 "Cores": MatchElements(coreID, IgnoreExtras, Elements{
2451 "0": MatchAllFields(Fields{
2452 Index: Ignore(),
2453 "UsageNanoCores": BeNumerically("<", 1E9),
2454 "UsageCoreNanoSeconds": BeNumerically(">", 1E5),
2455 }),
2456 "1": MatchAllFields(Fields{
2457 Index: Ignore(),
2458 "UsageNanoCores": BeNumerically("<", 1E9),
2459 "UsageCoreNanoSeconds": BeNumerically(">", 1E5),
2460 }),
2461 }),
2462 })),
2463 "Memory": PointTo(MatchAllFields(Fields{
2464 "Time": BeTemporally(">=", time.Now().Add(-time.Hour)),
2465 "AvailableBytes": BeZero(),
2466 "UsageBytes": BeNumerically(">", 5E6),
2467 "WorkingSetBytes": BeNumerically(">", 5E6),
2468 "RSSBytes": BeNumerically("<", 1E9),
2469 "PageFaults": BeNumerically("~", 1000, 100),
2470 "MajorPageFaults": BeNumerically("~", 100, 50),
2471 })),
2472 "Rootfs": m.Ignore(),
2473 "Logs": m.Ignore(),
2474 }))
2475 ```
2476
2477 ## `gmeasure`: Benchmarking Code
2478
2479 `gmeasure` provides support for measuring and recording benchmarks of your code and tests. It can be used as a simple standalone benchmarking framework, or as part of your code's test suite. `gmeasure` integrates cleanly with Ginkgo V2 to enable rich benchmarking of code alognside your tests.
2480
2481 ### A Mental Model for `gmeasure`
2482
2483 `gmeasure` is organized around the metaphor of `Experiment`s that can each record multiple `Measurement`s. To use `gmeasure` you create a `NewExperiment` and use the resulting `experiment` object to record values and durations. You can then print out the `experiment` to get a report of all measurements or access specific measurements and their statistical aggregates to perform comparisons and/or make assertions.
2484
2485 An `experiment` can record _multiple_ `Measurement`s. Each `Measurement` has a `Name`, a `Type` (either `MeasurementTypeDuration` or `MeasurementTypeValue`), and a collection of recorded data points (of type `float64` for Value measurements and `time.Duration` for Duration measurements). In this way an experiment might describe a system or context being measured and can contain multiple measurements - one for each aspect of the system in question.
2486
2487 `Experiment`s can either record values and durations that the user passes in directly. Or they can invoke callbacks and accept their return values as Value data points, or measure their runtimes to compute Duration data points. `Experiment`s can also _sample_ callbacks, calling them repeatedly to get an ensemble of data points.
2488
2489 A `Measurement` is created when its first data point is recorded by an `Experiment`. Subsequent data points with the same measurement name are appended to the measurement:
2490
2491 ```go
2492 experiment := gmeasure.NewExperiment("My Experiment")
2493 experiment.RecordDuration("runtime", 3*time.Second) //creates a new Measurement called "runtime"
2494 experiment.RecordDuration("runtime", 5*time.Second) //appends a data point to "runtime"
2495 ```
2496
2497 As described below, Measurements can be decorated with additional information. This includes information about the `Units` for the measurement, the `Precision` with which to render the measurement, and any `Style` to apply when rendering the measurement. Individual data points can also be decorated with an `Annotation` - an arbitrary string that is associated with that data point and gives it context. Decorations are applied as typed variadic arguments:
2498
2499 ```go
2500 experiment := gmeasure.NewExperiment("My Experiment")
2501
2502 // The first call to `RecordValue` for a measurement must set up any units, style, or precision decorations
2503 experiment.RecordValue("length", 3.141, gmeasure.Units("inches"), gmeasure.Style("{{blue}}"), gmeasure.Precision(2), gmeasure.Annotation("box A)"))
2504
2505 // Subsequent calls can attach an annotation. In this call a new data-point of `2.71` is added to the `length` measurement with the annotation `box B`.
2506 experiment.RecordValue("length", 2.71, gmeasure.Annotation("box B"))
2507 ```
2508
2509 Once recorded, `Measurements` can be fetched from the `experiment` by name via `experiment.Get("name")`. The returned `Measurement` object includes all the data points. To get a statistical summary of the data points (that includes the min, max, median, mean, and standard deviation) call `measurement.Stats()` or `experiment.GetStats("name")`. These statistical summaries can also be rank-ordered with `RankStats()`.
2510
2511 `gmeasure` is designed to integrate with Ginkgo. This is done by registering `Experiment`s, `Measurement`s and `Ranking`s as `ReportEntry`s via Ginkgo's `AddReportEntry`. This will cause Ginkgo to emit nicely formatted and styled summaries of each of these objects when generating the test report.
2512
2513 Finally, `gmeasure` provides a mechanism to cache `Experiment`s to disk with a specified version number. This enables multiple use-cases. You can cache expensive experiments to avoid rerunning them while you iterate on other experiments. You can also compare experiments to cached experiments to explore whether changes in performance have been introduced to the codebase under test.
2514
2515 `gmeasure` includes detailed [godoc documentation](https://pkg.go.dev/github.com/onsi/gomega/gmeasure) - this narrative documentation is intended to help you get started with `gmeasure`.
2516
2517 ### Measuring Values
2518
2519 `Experiment`s can record arbitrary `float64` values. You can do this by directly providing a `float64` via `experiment.RecordValue(measurementName string, value float64, decorators ...interface{})` or by providing a callback that returns a float64 via `experiment.MeasureValue(measurementName string, callback func() float64, decorators ...interface{})`.
2520
2521 You can apply `Units`, `Style`, and `Precision` decorators to control the appearance of the `Measurement` when reports are generated. These decorators must be applied when the first data point is recorded but can be elided thereafter. You can also associate an `Annotation` decoration with any recorded data point.
2522
2523 `Experiment`s are thread-safe so you can call `RecordValue` and `MeasureValue` from any goroutine.
2524
2525 ### Measuring Durations
2526
2527 `Experiment`s can record arbitrary `time.Duration` durations. You can do this by directly providing a `time.Duration` via `experiment.RecordDuration(measurementName string, duration time.Duration, decorators ...interface{})` or by providing a callback via `experiment.MeasureDuration(measurementName string, callback func(), decorators ...interface{})`. `gmeasure` will run the callback and measure how long it takes to complete.
2528
2529 You can apply `Style` and `Precision` decorators to control the appearance of the `Measurement` when reports are generated. These decorators must be applied when the first data point is recorded but can be elided thereafter. You can also associate an `Annotation` decoration with any recorded data point.
2530
2531 `Experiment`s are thread-safe so you can call `RecordDuration` and `MeasureDuration` from any goroutine.
2532
2533 ### Sampling
2534
2535 `Experiment`s support sampling callback functions repeatedly to build an ensemble of data points. All the sampling methods are configured by passing in a `SamplingConfig`:
2536
2537 ```go
2538 type SamplingConfig struct {
2539 N int
2540 Duration time.Duration
2541 NumParallel int
2542 MinSamplingInterval time.Duration
2543 }
2544 ```
2545
2546 Setting `SamplingConfig.N` limits the total number of samples to perform to `N`. Setting `SamplingConfig.Duration` limits the total time spent sampling to `Duration`. At least one of these fields must be set. If both are set then `gmeasure` will `sample` until the first limiting condition is met. Setting `SamplingConfig.MinSamplingInterval` causes `gmeasure` to wait until at least `MinSamplingInterval` has elapsed between subsequent samples.
2547
2548 By default, the `Experiment`'s sampling methods will run their callbacks serially within the calling goroutine. If `NumParallel` greater than `1`, however, the sampling methods will spin up `NumParallel` goroutines and farm the work among them. You cannot use `NumParallel` with `MinSamplingInterval`.
2549
2550 The basic sampling method is `experiment.Sample(callback func(idx int), samplingConfig SamplingConfig)`. This will call the callback function repeatedly, passing in an `idx` counter that increments between each call. The sampling will end based on the conditions provided in `SamplingConfig`. Note that `experiment.Sample` is not explicitly associated with a measurement. You can use `experiment.Sample` whenever you want to repeatedly invoke a callback up to a limit of `N` and/or `Duration`. You can then record arbitrarily many value or duration measurements in the body of the callback.
2551
2552 A common use-case, however, is to invoke a callback repeatedly to measure its duration or record its returned value and thereby generate an ensemble of data-points. This is supported via the `SampleX` family of methods built on top of `Sample`:
2553
2554 ```go
2555 experiment.SampleValue(measurementName string, callback func(idx int) float64, samplingConfig SamplingConfig, decorations ...interface{})
2556 experiment.SampleDuration(measurementName string, callback func(idx int), samplingConfig SamplingConfig, decorations ...interface{})
2557 experiment.SampleAnnotatedValue(measurementName string, callback func(idx int) (float64, Annotation), samplingConfig SamplingConfig, decorations ...interface{})
2558 experiment.SampleAnnotatedDuration(measurementName string, callback func(idx int) Annotation, samplingConfig SamplingConfig, decorations ...interface{})
2559 ```
2560
2561 each of these will contribute data points to the `Measurement` with name `measurementName`. `SampleValue` records the `float64` values returned by its callback. `SampleDuration` times each invocation of its callback and records the measured duration. `SampleAnnotatedValue` and `SampleAnnotatedDuration` expect their callbacks to return `Annotation`s. These are attached to each generated data point.
2562
2563 All these methods take the same decorators as their corresponding `RecordX` methods.
2564
2565 ### Measuring Durations with `Stopwatch`
2566
2567 In addition to `RecordDuration` and `MeasureDuration`, `gmeasure` also provides a `Stopwatch`-based abstraction for recording durations. To motivate `Stopwatch` consider the following example. Let's say we want to measure the end-to-end performance of a web-server. Here's the code we'd like to measure:
2568
2569 ```go
2570 It("measures the end-to-end performance of the web-server", func() {
2571 model, err := client.Fetch("model-id-17")
2572 Expect(err).NotTo(HaveOccurred())
2573
2574 err = model.ReticulateSpines()
2575 Expect(err).NotTo(HaveOccurred())
2576
2577 Expect(client.Save(model)).To(Succeed())
2578
2579 reticulatedModels, err := client.List("reticulated-models")
2580 Expect(err).NotTo(HaveOccurred())
2581 Expect(reticulatedModels).To(ContainElement(model))
2582 })
2583 ```
2584
2585 One approach would be to use `MeasureDuration`:
2586
2587 ```go
2588 It("measures the end-to-end performance of the web-server", func() {
2589 experiment := gmeasure.NewExperiment("end-to-end web-server performance")
2590 AddReportEntry(experiment.Name, experiment)
2591
2592 var model Model
2593 var err error
2594 experiment.MeasureDuration("fetch", func() {
2595 model, err = client.Fetch("model-id-17")
2596 })
2597 Expect(err).NotTo(HaveOccurred())
2598
2599 err = model.ReticulateSpines()
2600 Expect(err).NotTo(HaveOccurred())
2601
2602 experiment.MeasureDuration("save", func() {
2603 Expect(client.Save(model)).To(Succeed())
2604 })
2605
2606 var reticulatedModels []Models
2607 experiment.MeasureDuration("list", func() {
2608 reticulatedModels, err = client.List("reticulated-models")
2609 })
2610 Expect(err).NotTo(HaveOccurred())
2611 Expect(reticulatedModels).To(ContainElement(model))
2612 })
2613 ```
2614
2615 this... _works_. But all those closures and local variables make the test a bit harder to read. We can clean it up with a `Stopwatch`:
2616
2617 ```go
2618 It("measures the end-to-end performance of the web-server", func() {
2619 experiment := gmeasure.NewExperiment("end-to-end web-server performance")
2620 AddReportEntry(experiment.Name, experiment)
2621
2622 stopwatch := experiment.NewStopwatch() // start the stopwatch
2623
2624 model, err := client.Fetch("model-id-17")
2625 stopwatch.Record("fetch") // record the amount of time elapsed and store it in a Measurement named fetch
2626 Expect(err).NotTo(HaveOccurred())
2627
2628 err = model.ReticulateSpines()
2629 Expect(err).NotTo(HaveOccurred())
2630
2631 stopwatch.Reset() // reset the stopwatch
2632 Expect(client.Save(model)).To(Succeed())
2633 stopwatch.Record("save").Reset() // record the amount of time elapsed since the last Reset and store it in a Measurement named save, then reset the stopwatch
2634
2635 reticulatedModels, err := client.List("reticulated-models")
2636 stopwatch.Record("list")
2637 Expect(err).NotTo(HaveOccurred())
2638 Expect(reticulatedModels).To(ContainElement(model))
2639 })
2640 ```
2641
2642 that's now much cleaner and easier to reason about. If we wanted to sample the server's performance concurrently we could now simply wrap the relevant code in an `experiment.Sample`:
2643
2644 ```go
2645 It("measures the end-to-end performance of the web-server", func() {
2646 experiment := gmeasure.NewExperiment("end-to-end web-server performance")
2647 AddReportEntry(experiment.Name, experiment)
2648
2649 experiment.Sample(func(idx int) {
2650 defer GinkgoRecover() // necessary since these will launch as goroutines and contain assertions
2651 stopwatch := experiment.NewStopwatch() // we make a new stopwatch for each sample. Experiments are threadsafe, but Stopwatches are not.
2652
2653 model, err := client.Fetch("model-id-17")
2654 stopwatch.Record("fetch")
2655 Expect(err).NotTo(HaveOccurred())
2656
2657 err = model.ReticulateSpines()
2658 Expect(err).NotTo(HaveOccurred())
2659
2660 stopwatch.Reset()
2661 Expect(client.Save(model)).To(Succeed())
2662 stopwatch.Record("save").Reset()
2663
2664 reticulatedModels, err := client.List("reticulated-models")
2665 stopwatch.Record("list")
2666 Expect(err).NotTo(HaveOccurred())
2667 Expect(reticulatedModels).To(ContainElement(model))
2668 }, gmeasure.SamplingConfig{N:100, Duration:time.Minute, NumParallel:8})
2669 })
2670 ```
2671
2672 Check out the [godoc documentation](https://pkg.go.dev/github.com/onsi/gomega/gmeasure#Stopwatch) for more details about `Stopwatch` including support for `Pause`ing and `Resume`ing the stopwatch.
2673
2674 ### Stats and Rankings: Comparing Measurements
2675
2676 Once you've recorded a few measurements you'll want to try to understand and interpret them. `gmeasure` allows you to quickly compute statistics for a given measurement. Consider the following example. Let's say we have two different ideas for how to implement a sorting algorithm and want to hone in on the algorithm with the shortest median runtime. We could run an experiment:
2677
2678 ```go
2679 It("identifies the fastest algorithm", func() {
2680 experiment := gmeasure.NewExperiment("dueling algorithms")
2681 AddReportEntry(experiment.Name, experiment)
2682
2683 experiment.SampleDuration("runtime: algorithm 1", func(_ int) {
2684 RunAlgorithm1()
2685 }, gmeasure.SamplingConfig{N:1000})
2686
2687 experiment.SampleDuration("runtime: algorithm 2", func(_ int) {
2688 RunAlgorithm2()
2689 }, gmeasure.SamplingConfig{N:1000})
2690 })
2691 ```
2692
2693 This will sample the two competing tables and print out a tabular representation of the resulting statistics. (Note - you don't need to use Ginkgo here, you could just use `gmeasure` in your code directly and then `fmt.Println` the `experiment` to get the tabular report).
2694
2695 We could compare the tables by eye manually - or ask `gmeasure` to pick the winning algorithm for us:
2696
2697 ```go
2698 It("identifies the fastest algorithm", func() {
2699 experiment := gmeasure.NewExperiment("dueling algorithms")
2700 AddReportEntry(experiment.Name, experiment)
2701
2702 experiment.SampleDuration("runtime: algorithm 1", func(_ int) {
2703 RunAlgorithm1()
2704 }, gmeasure.SamplingConfig{N:1000})
2705
2706 experiment.SampleDuration("runtime: algorithm 2", func(_ int) {
2707 RunAlgorithm2()
2708 }, gmeasure.SamplingConfig{N:1000})
2709
2710 ranking := gmeasure.RankStats(gmeasure.LowerMedianIsBetter, experiment.GetStats("runtime: algorithm 1"), experiment.GetStats("runtime: algorithm 2"))
2711 AddReportEntry("Ranking", ranking)
2712 })
2713 ```
2714
2715 This will now emit a ranking result that will highlight the winning algorithm (in this case, the algorithm with the lower Median). `RankStats` supports the following `RankingCriteria`:
2716
2717 - `LowerMeanIsBetter`
2718 - `HigherMeanIsBetter`
2719 - `LowerMedianIsBetter`
2720 - `HigherMedianIsBetter`
2721 - `LowerMinIsBetter`
2722 - `HigherMinIsBetter`
2723 - `LowerMaxIsBetter`
2724 - `HigherMaxIsBetter`
2725
2726 We can also inspect the statistics of the two algorithms programatically. `experiment.GetStats` returns a `Stats` object that provides access to the following `Stat`s:
2727
2728 - `StatMin` - the data point with the smallest value
2729 - `StatMax` - the data point with the highest values
2730 - `StatMedian` - the median data point
2731 - `StatMean` - the mean of all the data points
2732 - `StatStdDev` - the standard deviation of all the data points
2733
2734 `Stats` can represent either Value Measurements or Duration Measurements. When inspecting a Value Measurement you can pull out the requested `Stat` (say, `StatMedian`) via `stats.ValueFor(StatMedian)` - this returns a `float64`. When inspecting Duration Measurements you can fetch `time.Duration` statistics via `stats.DurationFor(StatX)`. For either type you can fetch an appropriately formatted string representation of the stat via `stats.StringFor(StatX)`. You can also get a `float64` for either type by calling `stats.FloatFor(StatX)` (this simply returns a `float64(time.Duration)` for Duration Measurements and can be useful when you need to do some math with the stats).
2735
2736 Going back to our dueling algorithms example. Lets say we find that Algorithm 2 is the winner with a median runtime of around 3 seconds - and we want to be alerted by a failing test should the winner ever change, or the median runtime vary substantially. We can do that by writing a few assertions:
2737
2738 ```go
2739 It("identifies the fastest algorithm", func() {
2740 experiment := gmeasure.NewExperiment("dueling algorithms")
2741 AddReportEntry(experiment.Name, experiment)
2742
2743 experiment.SampleDuration("runtime: algorithm 1", func(_ int) {
2744 RunAlgorithm1()
2745 }, gmeasure.SamplingConfig{N:1000})
2746
2747 experiment.SampleDuration("runtime: algorithm 2", func(_ int) {
2748 RunAlgorithm2()
2749 }, gmeasure.SamplingConfig{N:1000})
2750
2751 ranking := gmeasure.RankStats(gmeasure.LowerMedianIsBetter, experiment.GetStats("runtime: algorithm 1"), experiment.GetStats("runtime: algorithm 2"))
2752 AddReportEntry("Ranking", ranking)
2753
2754 //assert that algorithm 2 is the winner
2755 Expect(ranking.Winner().MeasurementName).To(Equal("runtime: algorithm 2"))
2756
2757 //assert that algorithm 2's median is within 0.5 seconds of 3 seconds
2758 Expect(experiment.GetStats("runtime: algorithm 2").DurationFor(gmeasure.StatMedian)).To(BeNumerically("~", 3*time.Second, 500*time.Millisecond))
2759 })
2760 ```
2761
2762 ### Formatting Experiment and Measurement Output
2763
2764 `gmeasure` can produce formatted tabular output for `Experiment`s, `Measurement`s, and `Ranking`s. Each of these objects provides a `String()` method and a `ColorableString()` method. The `String()` method returns a string that does not include any styling tags whereas the `ColorableString()` method returns a string that includes Ginkgo's console styling tags (e.g. Ginkgo will render a string like `{{blue}}{{bold}}hello{{/}} there` as a bold blue "hello" followed by a default-styled " there"). `ColorableString()` is called for you automatically when you register any of these `gmeasure` objects as Ginkgo `ReportEntry`s.
2765
2766 When printing out `Experiment`s, `gmeasure` will produce a table whose columns correspond to the key statistics provided by `gmeasure.Stats` and whose rows are the various `Measurement`s recorded by the `Experiment`. Users can also record and emit notes - contextual information about the experiment - by calling `experiment.RecordNote(note string)`. Each note will get its own row in the table.
2767
2768 When printing out `Measurement`s, `gmeasure` will produce a table that includes _all_ the data points and annotations for the `Measurement`.
2769
2770 When printing out `Ranking`s, `gmeasure` will produce a table similar to the `Experiment` table with the `Measurement`s sorted by `RankingCriteria`.
2771
2772 Users can adjust a few aspects of `gmeasure`s output. This is done by providing decorators to the `Experiment` methods that record data points:
2773
2774 - `Units(string)` - the `Units` decorator allows you to associate a set of units with a measurement. Subsequent renderings of the measurement's name will include the units in `[]` square brackets.
2775 - `Precision(int or time.Duration)` - the `Precision` decorator controls the rendering of numerical information. For Value Measurements an `int` is used to express the number of decimal points to print. For example `Precision(3)` will render values with `fmt.Sprintf("%.3f", value)`. For Duration Measurements a `time.Duration` is used to round durations before rendering them. For example `Precision(time.Second)` will render durations via `duration.Round(time.Second).String()`.
2776 - `Style(string)` - the `Style` decorator allows you to associate a Ginkgo console style to a measurement. The measurement's row will be rendered with this style. For example `Style("{{green}}")` will emit a green row.
2777
2778 These formatting decorators **must** be applied to the _first_ data point recorded for a given Measurement (this is when the Measurement object is initialized and its style, precision, and units fields are populated).
2779
2780 Just to get concrete here's a fleshed out example that uses all the things:
2781
2782 ```go
2783 It("explores a complex object", func() {
2784 experiment := gmeasure.NewExperiment("exploring the encabulator")
2785 AddReportEntry(experiment.Name, experiment)
2786
2787 experiment.RecordNote("Encabulation Properties")
2788 experiment.Sample(func(idx int) {
2789 stopwatch := experiment.NewStopwatch()
2790 encabulator.Encabulate()
2791 stopwatch.Record("Encabulate Runtime", gmeasure.Style("{{green}}"), gmeasure.Precision(time.Millisecond))
2792
2793 var m runtime.MemStats
2794 runtime.ReadMemStats(&m)
2795 experiment.RecordValue("Encabulate Memory Usage", float64(m.Alloc / 1024 / 1024), gmeasure.Style("{{red}}"), gmeasure.Precision(3), gmeasure.Units("MB"), gmeasure.Annotation(fmt.Sprintf("%d", idx)))
2796 }, gmeasure.SamplingConfig{N:1000, NumParallel:4})
2797
2798 experiment.RecordNote("Encabulation Teardown")
2799 experiment.MeasureDuration("Teardown Runtime", func() {
2800 encabulator.Teardown()
2801 }, gmeasure.Style("{{yellow}}"))
2802
2803 memoryStats := experiment.GetStats("Encabulate Memory Usage")
2804 minMemory := memoryStats.ValueFor(gmeasure.StatMin)
2805 maxMemory := memoryStats.ValueFor(gmeasure.StatMax)
2806 Expect(maxMemory - minMemory).To(BeNumerically("<=", 10), "Should not see memory fluctuations exceeding 10 megabytes")
2807 })
2808 ```
2809
2810 ### Ginkgo Integration
2811
2812 The examples throughout this documentation have illustrated how `gmeasure` interoperates with Ginkgo. In short - you can emit output for `Experiment`, `Measurement`s, and `Ranking`s by registering them as Ginkgo `ReportEntry`s via `AddReportEntry()`.
2813
2814 This simple connection point ensures that the output is appropriately formatted and associated with the spec in question. It also ensures that Ginkgo's machine readable reports will include appropriately encoded versions of these `gmeasure` objects. So, for example, `ginkgo --json-report=report.json` will include JSON encoded `Experiment`s in `report.json` if you remember to `AddReportEntry` the `Experiment`s.
2815
2816 ### Caching Experiments
2817
2818 `gmeasure` supports caching experiments to local disk. Experiments can be stored and retreived from the cache by name and version number. Caching allows you to skip rerunning expensive experiments and versioned caching allows you to bust the cache by incrementing the version number. Under the hood, the cache is simply a set of files in a directory. Each file contains a JSON encoded header with the experiment's name and version number followed by the JSON-encoded experiment. The various cache methods are documented over at [pkg.go.dev](https://pkg.go.dev/github.com/onsi/gomega/gmeasure#ExperimentCache).
2819
2820 Using an `ExperimentCache` with Ginkgo takes a little bit of wiring. Here's an example:
2821
2822 ```go
2823 const EXPERIMENT_VERSION = 1 //bump this to bust the cache and recompute _all_ experiments
2824
2825 Describe("some experiments", func() {
2826 var cache gmeasure.ExperimentCache
2827 var experiment *gmeasure.Experiment
2828
2829 BeforeEach(func() {
2830 cache = gmeasure.NewExperimentCache("./gmeasure-cache")
2831 name := CurrentSpecReport().LeafNodeText // we use the text in each It block as the name of the experiment
2832 experiment = cache.Load(name, EXPERIMENT_VERSION) // we try to load the experiment from the cache
2833 if experiment != nil {
2834 // we have a cache hit - report on the experiment and skip this test.
2835 AddReportEntry(experiment)
2836 Skip("cached")
2837 }
2838 //we have a cache miss, make a new experiment and proceed with the test.
2839 experiment = gmeasure.NewExperiment(name)
2840 AddReportEntry(experiment)
2841 })
2842
2843 It("measures foo runtime", func() {
2844 experiment.SampleDuration("runtime", func() {
2845 //do stuff
2846 }, gmeasure.SamplingConfig{N:100})
2847 })
2848
2849 It("measures bar runtime", func() {
2850 experiment.SampleDuration("runtime", func() {
2851 //do stuff
2852 }, gmeasure.SamplingConfig{N:100})
2853 })
2854
2855 AfterEach(func() {
2856 // AfterEaches always run, even for tests that call `Skip`. So we make sure we aren't a skipped test then save the experiment to the cache
2857 if !CurrentSpecReport().State.Is(types.SpecStateSkipped) {
2858 cache.Save(experiment.Name, EXPERIMENT_VERSION, experiment)
2859 }
2860 })
2861 })
2862 ```
2863
2864 this test will load the experiment from the cache if it's available or run the experiment and store it in the cache if it is not. Incrementing `EXPERIMENT_VERSION` will force all experiments to rerun.
2865
2866 Another usecase for `ExperimentCache` is to cache and commit experiments to source control for use as future baselines. Your code can assert that measurements are within a certain range of the stored baseline. For example:
2867
2868 ```go
2869 Describe("server performance", func() {
2870 It("ensures a performance regression has not been introduced", func() {
2871 // make an experiment
2872 experiment := gmeasure.NewExperiment("performance regression test")
2873 AddReportEntry(experiment.Name, experiment)
2874
2875 // measure the performance of one endpoint
2876 experiment.SampleDuration("fetching one", func() {
2877 model, err := client.Get("id-1")
2878 Expect(err).NotTo(HaveOccurred())
2879 Expect(model.Id).To(Equal("id-1"))
2880 }, gmeasure.SamplingConfig{N:100})
2881
2882 // measure the performance of another endpoint
2883 experiment.SampleDuration("listing", func() {
2884 models, err := client.List()
2885 Expect(err).NotTo(HaveOccurred())
2886 Expect(models).To(HaveLen(30))
2887 }, gmeasure.SamplingConfig{N:100})
2888
2889 cache := gmeasure.NewExperimentCache("./gemasure-cache")
2890 baseline := cache.Load("performance regression test", 1)
2891 if baseline == nil {
2892 // this is the first run, let's store a baseline
2893 cache.Save("performacne regression test", 1, experiment)
2894 } else {
2895 for _, m := range []string{"fetching one", "listing"} {
2896 baselineStats := baseline.GetStats(m)
2897 currentStats := experiment.GetStats(m)
2898
2899 //make sure the mean of the current performance measure is within one standard deviation of the baseline
2900 Expect(currentStats.DurationFor(gmeasure.StatMean)).To(BeNumerically("~", baselineStats.DurationFor(gmeasure.StatsMean), baselineStats.DurationFor(gmeasure.StatsStdDev)), m)
2901 }
2902 }
2903 })
2904 })
2905 ```
2906
2907 {% endraw %}
0 (() => {
1 let sidebar = document.getElementById("sidebar")
2 let headings = document.querySelectorAll("#content h2,h3")
3 let headingsLookup = {}
4 let currentHeadingGroup = null
5 let collapsibleGroup = null
6 for (let heading of headings) {
7 let el = document.createElement("a")
8 el.href = `#${heading.id}`
9 el.id = `${heading.id}-item`
10 el.innerText = heading.innerText
11
12 if (heading.tagName == "H2") {
13 currentHeadingGroup = heading.id
14
15 el.classList = "sidebar-heading"
16 sidebar.appendChild(el)
17
18 collapsibleGroup = document.createElement("div")
19 collapsibleGroup.classList = "sidebar-section"
20 sidebar.appendChild(collapsibleGroup)
21 } else {
22 el.classList = "sidebar-item"
23 collapsibleGroup.appendChild(el)
24 }
25
26 headingsLookup[heading.id] = currentHeadingGroup
27 }
28
29 let ticking = false;
30 document.getElementById("content").addEventListener("scroll", (e) => {
31 if (!ticking) {
32 window.requestAnimationFrame(function () {
33 let viewportHeight = window.visualViewport.height;
34 let winner = null;
35 for (let heading of headings) {
36 let rect = heading.getBoundingClientRect();
37 if (rect.top > viewportHeight) { break }
38 winner = heading.id
39 if (rect.top > 0) { break }
40 }
41 if (winner != null) {
42 document.querySelectorAll("#sidebar .active").forEach(e => e.classList.remove("active"))
43 document.getElementById(`${winner}-item`).classList.add("active")
44 document.getElementById(`${headingsLookup[winner]}-item`).classList.add("active");
45 }
46 ticking = false;
47 });
48
49 ticking = true;
50 }
51 })
52
53 document.querySelector("img[alt=Gomega]").id = "top"
54
55 document.querySelectorAll("div.highlight").forEach(el => {
56 if (el.innerText.includes("/* === INVALID === */")) {
57 el.classList.add("invalid")
58 }
59 })
60
61 document.getElementById("disclosure").addEventListener("click", (e) => {
62 document.getElementById("container").classList.toggle("reveal-sidebar")
63 })
64
65 document.getElementById("mask").addEventListener("click", (e) => {
66 document.getElementById("container").classList.toggle("reveal-sidebar")
67 })
68 })()
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 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55
66 "testing"
00 package format_test
11
22 import (
3 "context"
34 "fmt"
45 "strings"
56 "time"
67
7 . "github.com/onsi/ginkgo"
8 . "github.com/onsi/ginkgo/v2"
89 . "github.com/onsi/gomega"
910 . "github.com/onsi/gomega/format"
1011 "github.com/onsi/gomega/types"
1112 )
1213
1314 //recursive struct
15
16 const truncateHelpText = `
17 Gomega truncated this representation as it exceeds 'format.MaxLength'.
18 Consider having the object provide a custom 'GomegaStringer' representation
19 or adjust the parameters in Gomega's 'format' package.
20
21 Learn more here: https://onsi.github.io/gomega/#adjusting-output
22 `
1423
1524 type StringAlias string
1625 type ByteAlias []byte
7281 return "string"
7382 }
7483
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
84 type gomegaStringer struct {
85 }
86
87 func (g gomegaStringer) GomegaString() string {
88 return "gomegastring"
89 }
90
91 type gomegaStringerLong struct {
92 }
93
94 func (g gomegaStringerLong) GomegaString() string {
95 return strings.Repeat("s", MaxLength*2)
9296 }
9397
9498 var _ = Describe("Format", func() {
112116 for i := range arr {
113117 arr[i] = entriesSwitch
114118 }
115 return "{" + strings.Join(arr, ", ") + "}"
119 return "{\\s*" + strings.Join(arr, ",\\s* ") + ",?\\s*}"
116120 }
117121
118122 Describe("Message", func() {
119123 Context("with only an actual value", func() {
124 BeforeEach(func() {
125 MaxLength = 4000
126 })
127
120128 It("should print out an indented formatted representation of the value and the message", func() {
121129 Expect(Message(3, "to be three.")).Should(Equal("Expected\n <int>: 3\nto be three."))
130 })
131
132 It("should print out an indented formatted representation of the value and the message, and trucate it when too long", func() {
133 tooLong := strings.Repeat("s", MaxLength+1)
134 tooLongResult := strings.Repeat("s", MaxLength) + "...\n" + truncateHelpText
135 Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLongResult + "\nto be truncated"))
136 })
137
138 It("should print out an indented formatted representation of the value and the message, and not trucate it when MaxLength = 0", func() {
139 MaxLength = 0
140 tooLong := strings.Repeat("s", MaxLength+1)
141 Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLong + "\nto be truncated"))
122142 })
123143 })
124144
171191 stringB := "something_else"
172192
173193 Expect(MessageWithDiff(stringA, "to equal", stringB)).Should(Equal(expectedSpecialCharacterFailureMessage))
194 })
195
196 It("handles negative padding length", func() {
197 stringWithB := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
198 stringWithZ := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
199 longMessage := "to equal very long message"
200
201 Expect(MessageWithDiff(stringWithB, longMessage, stringWithZ)).Should(Equal(expectedDiffLongMessage))
174202 })
175203
176204 Context("With truncated diff disabled", func() {
535563 })
536564 })
537565
566 Describe("formatting nested interface{} types", func() {
567 It("should print out the types of the container and value", func() {
568 Expect(Object([]interface{}{"foo"}, 1)).
569 To(match("[]interface {} | len:1, cap:1", `[<string>"foo"]`))
570
571 Expect(Object(map[string]interface{}{"foo": true}, 1)).
572 To(match("map[string]interface {} | len:1", `{"foo": <bool>true}`))
573
574 Expect(Object(struct{ A interface{} }{A: 1}, 1)).
575 To(match("struct { A interface {} }", "{A: <int>1}"))
576
577 v := struct{ A interface{} }{A: struct{ B string }{B: "foo"}}
578 Expect(Object(v, 1)).To(match(`struct { A interface {} }`, `{
579 A: <struct { B string }>{B: "foo"},
580 }`))
581 })
582 })
583
538584 Describe("formatting times", func() {
539585 It("should format time as RFC3339", func() {
540586 t := time.Date(2016, 10, 31, 9, 57, 23, 12345, time.UTC)
583629 byteArrValue: \[17, 20, 32\],
584630 mapValue: %s,
585631 structValue: {Exported: "exported"},
586 interfaceValue: {"a key": 17},
632 interfaceValue: <map\[string\]int \| len:1>{"a key": 17},
587633 }`, s.chanValue, s.funcValue, hashMatchingRegexp(`"a key": 20`, `"b key": 30`))
588634
589635 Expect(Object(s, 1)).Should(matchRegexp(`format_test\.SecretiveStruct`, expected))
599645 outerHash["integer"] = 2
600646 outerHash["map"] = innerHash
601647
602 expected := hashMatchingRegexp(`"integer": 2`, `"map": {"inner": 3}`)
648 expected := hashMatchingRegexp(`"integer": <int>2`, `"map": <map\[string\]int \| len:1>{"inner": 3}`)
603649 Expect(Object(outerHash, 1)).Should(matchRegexp(`map\[string\]interface {} \| len:2`, expected))
604650 })
605651 })
645691 Expect(Object(Stringer{}, 1)).Should(ContainSubstring("<format_test.Stringer>: string"))
646692 })
647693 })
694
695 When("passed a GomegaStringer", func() {
696 It("should use what GomegaString() returns", func() {
697 Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
698 UseStringerRepresentation = false
699 Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
700 })
701
702 It("should use what GomegaString() returns, disregarding MaxLength", func() {
703 Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
704 UseStringerRepresentation = false
705 Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
706 })
707 })
648708 })
649709
650710 Describe("Printing a context.Context field", func() {
651711
652712 type structWithContext struct {
653 Context Ctx
713 Context context.Context
654714 Value string
655715 }
656716
657 context := ctx{}
658 objWithContext := structWithContext{Value: "some-value", Context: &context}
717 objWithContext := structWithContext{Value: "some-value", Context: context.TODO()}
659718
660719 It("Suppresses the content by default", func() {
661720 Expect(Object(objWithContext, 1)).Should(ContainSubstring("<suppressed context>"))
662721 })
663722
664 It("Doesn't supress the context if it's the object being printed", func() {
665 Expect(Object(context, 1)).ShouldNot(MatchRegexp("^.*<suppressed context>$"))
723 It("Doesn't suppress the context if it's the object being printed", func() {
724 Expect(Object(context.TODO(), 1)).ShouldNot(MatchRegexp("^.*<suppressed context>$"))
666725 })
667726
668727 Context("PrintContextObjects is set", func() {
760819 to equal |
761820 <string>: "...z..."
762821 `)
822
823 var expectedDiffLongMessage = strings.TrimSpace(`
824 Expected
825 <string>: "...aaaaabaaaaa..."
826 to equal very long message
827 <string>: "...aaaaazaaaaa..."
828 `)
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 /*
77
88 "bytes"
99
10 . "github.com/onsi/ginkgo"
10 . "github.com/onsi/ginkgo/v2"
1111 . "github.com/onsi/gomega"
1212 )
1313
120120 })
121121
122122 Describe("detecting regular expressions", func() {
123 It("should fire the appropriate channel when the passed in pattern matches, then close it", func(done Done) {
123 It("should fire the appropriate channel when the passed in pattern matches, then close it", func() {
124124 go func() {
125125 time.Sleep(10 * time.Millisecond)
126126 buffer.Write([]byte("abcde"))
134134 case gotIt = <-A:
135135 case <-B:
136136 Fail("should not have gotten here")
137 case <-time.After(time.Second):
138 Fail("timed out waiting for detection")
137139 }
138140
139141 Expect(gotIt).Should(BeTrue())
142144 buffer.Write([]byte("f"))
143145 Eventually(B).Should(Receive())
144146 Eventually(B).Should(BeClosed())
145
146 close(done)
147 })
148
149 It("should fast-forward the buffer upon detection", func(done Done) {
147 })
148
149 It("should fast-forward the buffer upon detection", func() {
150150 buffer.Write([]byte("abcde"))
151 <-buffer.Detect("abc")
151 select {
152 case <-buffer.Detect("abc"):
153 case <-time.After(time.Second):
154 Fail("timed out waiting for detection")
155 }
152156 Expect(buffer).ShouldNot(Say("abc"))
153157 Expect(buffer).Should(Say("de"))
154 close(done)
155 })
156
157 It("should only fast-forward the buffer when the channel is read, and only if doing so would not rewind it", func(done Done) {
158 })
159
160 It("should only fast-forward the buffer when the channel is read, and only if doing so would not rewind it", func() {
158161 buffer.Write([]byte("abcde"))
159162 A := buffer.Detect("abc")
160163 time.Sleep(20 * time.Millisecond) //give the goroutine a chance to detect and write to the channel
161164 Expect(buffer).Should(Say("abcd"))
162 <-A
165 select {
166 case <-A:
167 case <-time.After(time.Second):
168 Fail("timed out waiting for detection")
169 }
163170 Expect(buffer).ShouldNot(Say("d"))
164171 Expect(buffer).Should(Say("e"))
165172 Eventually(A).Should(BeClosed())
166 close(done)
167 })
168
169 It("should be possible to cancel a detection", func(done Done) {
173 })
174
175 It("should be possible to cancel a detection", func() {
170176 A := buffer.Detect("abc")
171177 B := buffer.Detect("def")
172178 buffer.CancelDetects()
175181 Eventually(B).Should(BeClosed())
176182
177183 Expect(buffer).Should(Say("bcde"))
178 <-buffer.Detect("f")
179 close(done)
184 select {
185 case <-buffer.Detect("f"):
186 case <-time.After(time.Second):
187 Fail("timed out waiting for detection")
188 }
189 })
190 })
191
192 Describe("clearing the buffer", func() {
193 It("should clear out the contents of the buffer", func() {
194 buffer.Write([]byte("abc"))
195 Expect(buffer).To(Say("ab"))
196 Expect(buffer.Clear()).To(Succeed())
197 Expect(buffer).NotTo(Say("c"))
198 Expect(buffer.Contents()).To(BeEmpty())
199 buffer.Write([]byte("123"))
200 Expect(buffer).To(Say("123"))
201 Expect(buffer.Contents()).To(Equal([]byte("123")))
202 })
203
204 It("should error when the buffer is closed", func() {
205 buffer.Write([]byte("abc"))
206 buffer.Close()
207 err := buffer.Clear()
208 Expect(err).To(HaveOccurred())
180209 })
181210 })
182211
00 package gbytes_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55
66 "testing"
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 }
66
77 . "github.com/onsi/gomega/gbytes"
88
9 . "github.com/onsi/ginkgo"
9 . "github.com/onsi/ginkgo/v2"
1010 . "github.com/onsi/gomega"
1111 )
1212
44
55 . "github.com/onsi/gomega/gbytes"
66
7 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/v2"
88 . "github.com/onsi/gomega"
99 )
1010
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"
1312 "runtime"
1413 "strings"
1514 "sync"
15
16 "github.com/onsi/gomega/internal/gutil"
1617 )
1718
1819 var (
4344 */
4445 func BuildIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
4546 return doBuild(gopath, packagePath, nil, args...)
47 }
48
49 func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
50 executable, err := newExecutablePath(gopath, packagePath)
51 if err != nil {
52 return "", err
53 }
54
55 cmdArgs := append([]string{"build"}, args...)
56 cmdArgs = append(cmdArgs, "-o", executable, packagePath)
57
58 build := exec.Command("go", cmdArgs...)
59 build.Env = replaceGoPath(os.Environ(), gopath)
60 build.Env = append(build.Env, env...)
61
62 output, err := build.CombinedOutput()
63 if err != nil {
64 return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
65 }
66
67 return executable, nil
68 }
69
70 /*
71 CompileTest uses go test to compile the test package at packagePath. The resulting binary is saved off in a temporary directory.
72 A path pointing to this binary is returned.
73
74 CompileTest uses the $GOPATH set in your environment. If $GOPATH is not set and you are using Go 1.8+,
75 it will use the default GOPATH instead. It passes the variadic args on to `go test`.
76 */
77 func CompileTest(packagePath string, args ...string) (compiledPath string, err error) {
78 return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
79 }
80
81 /*
82 GetAndCompileTest is identical to CompileTest but `go get` the package before compiling tests.
83 */
84 func GetAndCompileTest(packagePath string, args ...string) (compiledPath string, err error) {
85 if err := getForTest(build.Default.GOPATH, packagePath, nil); err != nil {
86 return "", err
87 }
88
89 return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
90 }
91
92 /*
93 CompileTestWithEnvironment is identical to CompileTest but allows you to specify env vars to be set at build time.
94 */
95 func CompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
96 return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
97 }
98
99 /*
100 GetAndCompileTestWithEnvironment is identical to GetAndCompileTest but allows you to specify env vars to be set at build time.
101 */
102 func GetAndCompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
103 if err := getForTest(build.Default.GOPATH, packagePath, env); err != nil {
104 return "", err
105 }
106
107 return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
108 }
109
110 /*
111 CompileTestIn is identical to CompileTest but allows you to specify a custom $GOPATH (the first argument).
112 */
113 func CompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
114 return doCompileTest(gopath, packagePath, nil, args...)
115 }
116
117 /*
118 GetAndCompileTestIn is identical to GetAndCompileTest but allows you to specify a custom $GOPATH (the first argument).
119 */
120 func GetAndCompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
121 if err := getForTest(gopath, packagePath, nil); err != nil {
122 return "", err
123 }
124
125 return doCompileTest(gopath, packagePath, nil, args...)
126 }
127
128 func isLocalPackage(packagePath string) bool {
129 return strings.HasPrefix(packagePath, ".")
130 }
131
132 func getForTest(gopath, packagePath string, env []string) error {
133 if isLocalPackage(packagePath) {
134 return nil
135 }
136
137 return doGet(gopath, packagePath, env, "-t")
138 }
139
140 func doGet(gopath, packagePath string, env []string, args ...string) error {
141 args = append(args, packagePath)
142 args = append([]string{"get"}, args...)
143
144 goGet := exec.Command("go", args...)
145 goGet.Dir = gopath
146 goGet.Env = replaceGoPath(os.Environ(), gopath)
147 goGet.Env = append(goGet.Env, env...)
148
149 output, err := goGet.CombinedOutput()
150 if err != nil {
151 return fmt.Errorf("Failed to get %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
152 }
153
154 return nil
155 }
156
157 func doCompileTest(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
158 executable, err := newExecutablePath(gopath, packagePath, ".test")
159 if err != nil {
160 return "", err
161 }
162
163 cmdArgs := append([]string{"test", "-c"}, args...)
164 cmdArgs = append(cmdArgs, "-o", executable, packagePath)
165
166 build := exec.Command("go", cmdArgs...)
167 build.Env = replaceGoPath(os.Environ(), gopath)
168 build.Env = append(build.Env, env...)
169
170 output, err := build.CombinedOutput()
171 if err != nil {
172 return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
173 }
174
175 return executable, nil
46176 }
47177
48178 func replaceGoPath(environ []string, newGoPath string) []string {
55185 return append(newEnviron, "GOPATH="+newGoPath)
56186 }
57187
58 func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
188 func newExecutablePath(gopath, packagePath string, suffixes ...string) (string, error) {
59189 tmpDir, err := temporaryDirectory()
60190 if err != nil {
61191 return "", err
66196 }
67197
68198 executable := filepath.Join(tmpDir, path.Base(packagePath))
199
69200 if runtime.GOOS == "windows" {
70201 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))
83202 }
84203
85204 return executable, nil
103222 mu.Lock()
104223 defer mu.Unlock()
105224 if tmpDir == "" {
106 tmpDir, err = ioutil.TempDir("", "gexec_artifacts")
225 tmpDir, err = gutil.MkdirTemp("", "gexec_artifacts")
107226 if err != nil {
108227 return "", err
109228 }
110229 }
111230
112 return ioutil.TempDir(tmpDir, "g")
113 }
231 return gutil.MkdirTemp(tmpDir, "g")
232 }
11
22 import (
33 "fmt"
4 "io/ioutil"
54 "os"
65 "path/filepath"
76
8 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/v2"
98 . "github.com/onsi/gomega"
109 "github.com/onsi/gomega/gexec"
10 "github.com/onsi/gomega/internal/gutil"
1111 )
1212
1313 var packagePath = "./_fixture/firefly"
1414
1515 var _ = Describe(".Build", func() {
16 When("there have been previous calls to Build", func() {
17 BeforeEach(func() {
18 _, err := gexec.Build(packagePath)
16 When("there have been previous calls to CompileTest", func() {
17 BeforeEach(func() {
18 _, err := gexec.CompileTest(packagePath)
1919 Expect(err).ShouldNot(HaveOccurred())
2020 })
2121
2323 compiledPath, err := gexec.Build(packagePath)
2424 Expect(err).ShouldNot(HaveOccurred())
2525 Expect(compiledPath).Should(BeAnExistingFile())
26 Expect(filepath.Base(compiledPath)).Should(MatchRegexp(`firefly(|.exe)$`))
2627 })
2728
2829 Context("and CleanupBuildArtifacts has been called", func() {
3132 })
3233
3334 It("compiles the specified package", func() {
34 var err error
35 fireflyPath, err = gexec.Build(packagePath)
35 fireflyPath, err := gexec.Build(packagePath)
36 Expect(err).ShouldNot(HaveOccurred())
37 Expect(fireflyPath).Should(BeAnExistingFile())
38 })
39 })
40 })
41
42 When("there have been previous calls to Build", func() {
43 BeforeEach(func() {
44 _, err := gexec.Build(packagePath)
45 Expect(err).ShouldNot(HaveOccurred())
46 })
47
48 It("compiles the specified package", func() {
49 compiledPath, err := gexec.Build(packagePath)
50 Expect(err).ShouldNot(HaveOccurred())
51 Expect(compiledPath).Should(BeAnExistingFile())
52 })
53
54 Context("and CleanupBuildArtifacts has been called", func() {
55 BeforeEach(func() {
56 gexec.CleanupBuildArtifacts()
57 })
58
59 It("compiles the specified package", func() {
60 fireflyPath, err := gexec.Build(packagePath)
3661 Expect(err).ShouldNot(HaveOccurred())
3762 Expect(fireflyPath).Should(BeAnExistingFile())
3863 })
7398 BeforeEach(func() {
7499 var err error
75100 original = os.Getenv("GOPATH")
76 gopath, err = ioutil.TempDir("", "")
101 gopath, err = gutil.MkdirTemp("", "")
77102 Expect(err).NotTo(HaveOccurred())
78103 copyFile(filepath.Join("_fixture", "firefly", "main.go"), filepath.Join(gopath, "src", target), "main.go")
79104 Expect(os.Setenv("GOPATH", filepath.Join(os.TempDir(), "emptyFakeGopath"))).To(Succeed())
92117 })
93118
94119 It("appends the gopath env var", func() {
95 _, err := gexec.BuildIn(gopath, target)
96 Expect(err).NotTo(HaveOccurred())
120 compiledPath, err := gexec.BuildIn(gopath, target)
121 Expect(err).NotTo(HaveOccurred())
122 Expect(compiledPath).Should(BeAnExistingFile())
97123 })
98124
99125 It("resets GOPATH to its original value", func() {
103129 })
104130 })
105131
132 var _ = Describe(".CompileTest", func() {
133 Context("a remote package", func() {
134 const remotePackage = "github.com/onsi/ginkgo/v2/types"
135
136 It("compiles the specified test package", func() {
137 compiledPath, err := gexec.GetAndCompileTest(remotePackage)
138 Expect(err).ShouldNot(HaveOccurred())
139 Expect(compiledPath).Should(BeAnExistingFile())
140 })
141 })
142
143 When("there have been previous calls to CompileTest", func() {
144 BeforeEach(func() {
145 _, err := gexec.CompileTest(packagePath)
146 Expect(err).ShouldNot(HaveOccurred())
147 })
148
149 It("compiles the specified test package", func() {
150 compiledPath, err := gexec.CompileTest(packagePath)
151 Expect(err).ShouldNot(HaveOccurred())
152 Expect(compiledPath).Should(BeAnExistingFile())
153 })
154
155 Context("and CleanupBuildArtifacts has been called", func() {
156 BeforeEach(func() {
157 gexec.CleanupBuildArtifacts()
158 })
159
160 It("compiles the specified test package", func() {
161 fireflyTestPath, err := gexec.CompileTest(packagePath)
162 Expect(err).ShouldNot(HaveOccurred())
163 Expect(fireflyTestPath).Should(BeAnExistingFile())
164 })
165 })
166 })
167
168 When("there have been previous calls to Build", func() {
169 BeforeEach(func() {
170 _, err := gexec.Build(packagePath)
171 Expect(err).ShouldNot(HaveOccurred())
172 })
173
174 It("compiles the specified test package", func() {
175 compiledPath, err := gexec.CompileTest(packagePath)
176 Expect(err).ShouldNot(HaveOccurred())
177 Expect(compiledPath).Should(BeAnExistingFile())
178 })
179
180 Context("and CleanupBuildArtifacts has been called", func() {
181 BeforeEach(func() {
182 gexec.CleanupBuildArtifacts()
183 })
184
185 It("compiles the specified test package", func() {
186 fireflyTestPath, err := gexec.CompileTest(packagePath)
187 Expect(err).ShouldNot(HaveOccurred())
188 Expect(fireflyTestPath).Should(BeAnExistingFile())
189 })
190 })
191 })
192 })
193
194 var _ = Describe(".CompileTestWithEnvironment", func() {
195 var err error
196 env := []string{
197 "GOOS=linux",
198 "GOARCH=amd64",
199 }
200
201 Context("a remote package", func() {
202 const remotePackage = "github.com/onsi/ginkgo/v2/types"
203
204 It("compiles the specified test package with the specified env vars", func() {
205 compiledPath, err := gexec.GetAndCompileTestWithEnvironment(remotePackage, env)
206 Expect(err).ShouldNot(HaveOccurred())
207 Expect(compiledPath).Should(BeAnExistingFile())
208 })
209 })
210
211 It("compiles the specified test package with the specified env vars", func() {
212 compiledPath, err := gexec.CompileTestWithEnvironment(packagePath, env)
213 Expect(err).ShouldNot(HaveOccurred())
214 Expect(compiledPath).Should(BeAnExistingFile())
215 })
216
217 It("returns the environment to a good state", func() {
218 _, err = gexec.CompileTestWithEnvironment(packagePath, env)
219 Expect(err).ShouldNot(HaveOccurred())
220 Expect(os.Environ()).ShouldNot(ContainElement("GOOS=linux"))
221 })
222 })
223
224 var _ = Describe(".CompiledTestIn", func() {
225 const (
226 target = "github.com/onsi/gomega/gexec/_fixture/firefly"
227 )
228
229 var (
230 original string
231 gopath string
232 )
233
234 BeforeEach(func() {
235 var err error
236 original = os.Getenv("GOPATH")
237 gopath, err = gutil.MkdirTemp("", "")
238 Expect(err).NotTo(HaveOccurred())
239 copyFile(filepath.Join("_fixture", "firefly", "main.go"), filepath.Join(gopath, "src", target), "main.go")
240 Expect(os.Setenv("GOPATH", filepath.Join(os.TempDir(), "emptyFakeGopath"))).To(Succeed())
241 Expect(os.Environ()).To(ContainElement(fmt.Sprintf("GOPATH=%s", filepath.Join(os.TempDir(), "emptyFakeGopath"))))
242 })
243
244 AfterEach(func() {
245 if original == "" {
246 Expect(os.Unsetenv("GOPATH")).To(Succeed())
247 } else {
248 Expect(os.Setenv("GOPATH", original)).To(Succeed())
249 }
250 if gopath != "" {
251 os.RemoveAll(gopath)
252 }
253 })
254
255 Context("a remote package", func() {
256 const remotePackage = "github.com/onsi/ginkgo/v2/types"
257
258 It("compiles the specified test package", func() {
259 compiledPath, err := gexec.GetAndCompileTestIn(gopath, remotePackage)
260 Expect(err).ShouldNot(HaveOccurred())
261 Expect(compiledPath).Should(BeAnExistingFile())
262 })
263 })
264
265 It("appends the gopath env var", func() {
266 compiledPath, err := gexec.CompileTestIn(gopath, target)
267 Expect(err).NotTo(HaveOccurred())
268 Expect(compiledPath).Should(BeAnExistingFile())
269 })
270
271 It("resets GOPATH to its original value", func() {
272 _, err := gexec.CompileTestIn(gopath, target)
273 Expect(err).NotTo(HaveOccurred())
274 Expect(os.Getenv("GOPATH")).To(Equal(filepath.Join(os.TempDir(), "emptyFakeGopath")))
275 })
276 })
277
106278 func copyFile(source, directory, basename string) {
107279 Expect(os.MkdirAll(directory, 0755)).To(Succeed())
108 content, err := ioutil.ReadFile(source)
280 content, err := gutil.ReadFile(source)
109281 Expect(err).NotTo(HaveOccurred())
110 Expect(ioutil.WriteFile(filepath.Join(directory, basename), content, 0644)).To(Succeed())
282 Expect(gutil.WriteFile(filepath.Join(directory, basename), content)).To(Succeed())
111283 }
55
66 . "github.com/onsi/gomega/gexec"
77
8 . "github.com/onsi/ginkgo"
8 . "github.com/onsi/ginkgo/v2"
99 . "github.com/onsi/gomega"
1010 )
1111
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())
00 package gexec_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 "github.com/onsi/gomega/gexec"
66
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 })
44
55 . "github.com/onsi/gomega/gexec"
66
7 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/v2"
88 . "github.com/onsi/gomega"
99 )
1010
0 //go:build !windows
01 // +build !windows
12
23 package gexec_test
34
45 import (
56 "io"
6 "io/ioutil"
77 "os/exec"
88 "syscall"
99 "time"
1111 . "github.com/onsi/gomega/gbytes"
1212 . "github.com/onsi/gomega/gexec"
1313
14 . "github.com/onsi/ginkgo"
14 . "github.com/onsi/ginkgo/v2"
1515 . "github.com/onsi/gomega"
1616 )
1717
1818 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
19 Context("firefly binary", func() {
20 var fireflyPath string
21 var command *exec.Cmd
22 var session *Session
23
24 var outWriter, errWriter io.Writer
25
26 BeforeEach(func() {
27 outWriter = nil
28 errWriter = nil
29
30 var err error
31 fireflyPath, err = Build("./_fixture/firefly")
32 Expect(err).ShouldNot(HaveOccurred())
33
34 })
35
36 JustBeforeEach(func() {
37 command = exec.Command(fireflyPath)
38 var err error
39 session, err = Start(command, outWriter, errWriter)
40 Expect(err).ShouldNot(HaveOccurred())
41 })
42
43 Context("running a command", func() {
44 It("should start the process", func() {
45 Expect(command.Process).ShouldNot(BeNil())
46 })
47
48 It("should wrap the process's stdout and stderr with gbytes buffers", func() {
49 Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
50 Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))
51 defer session.Out.CancelDetects()
52
53 select {
54 case <-session.Out.Detect("Can we maybe vote on the whole murdering people issue"):
55 Eventually(session).Should(Exit(0))
56 case <-session.Out.Detect("I swear by my pretty floral bonnet, I will end you."):
57 Eventually(session).Should(Exit(1))
58 case <-session.Out.Detect("My work's illegal, but at least it's honest."):
59 Eventually(session).Should(Exit(2))
60 case <-time.After(5 * time.Second):
61 Fail("timed out waiting for detection")
62 }
63 })
64
65 It("should satisfy the gbytes.BufferProvider interface, passing Stdout", func() {
66 Eventually(session).Should(Say("We've done the impossible, and that makes us mighty"))
67 Eventually(session).Should(Exit())
68 })
69 })
70
71 Describe("providing the exit code", func() {
72 It("should provide the app's exit code", func() {
73 Expect(session.ExitCode()).Should(Equal(-1))
74
75 Eventually(session).Should(Exit())
76 Expect(session.ExitCode()).Should(BeNumerically(">=", 0))
77 Expect(session.ExitCode()).Should(BeNumerically("<", 3))
78 })
79 })
80
81 Describe("wait", func() {
82 It("should wait till the command exits", func() {
83 Expect(session.ExitCode()).Should(Equal(-1))
84 Expect(session.Wait().ExitCode()).Should(BeNumerically(">=", 0))
85 Expect(session.Wait().ExitCode()).Should(BeNumerically("<", 3))
86 })
87 })
88
89 Describe("exited", func() {
90 It("should close when the command exits", func() {
91 Eventually(session.Exited).Should(BeClosed())
92 Expect(session.ExitCode()).ShouldNot(Equal(-1))
93 })
94 })
95
96 Describe("kill", func() {
97 It("should kill the command", func() {
98 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
99 Expect(err).ShouldNot(HaveOccurred())
100
101 session.Kill()
102 Eventually(session).Should(Exit(128 + 9))
103 })
104 })
105
106 Describe("interrupt", func() {
107 It("should interrupt the command", func() {
108 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
109 Expect(err).ShouldNot(HaveOccurred())
110
111 session.Interrupt()
112 Eventually(session).Should(Exit(128 + 2))
113 })
114 })
115
116 Describe("terminate", func() {
117 It("should terminate the command", func() {
118 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
119 Expect(err).ShouldNot(HaveOccurred())
120
121 session.Terminate()
122 Eventually(session).Should(Exit(128 + 15))
123 })
124 })
125
126 Describe("signal", func() {
127 It("should send the signal to the command", func() {
128 session, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
129 Expect(err).ShouldNot(HaveOccurred())
130
131 session.Signal(syscall.SIGABRT)
132 Eventually(session).Should(Exit(128 + 6))
133 })
134
135 It("should ignore sending a signal if the command did not start", func() {
136 session, err := Start(exec.Command("notexisting"), GinkgoWriter, GinkgoWriter)
137 Expect(err).To(HaveOccurred())
138
139 Expect(func() { session.Signal(syscall.SIGUSR1) }).NotTo(Panic())
140 })
141 })
142
143 Context("tracking sessions", func() {
144 BeforeEach(func() {
145 KillAndWait()
146 })
147
148 Describe("kill", func() {
149 It("should kill all the started sessions", func() {
150 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
151 Expect(err).ShouldNot(HaveOccurred())
152
153 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
154 Expect(err).ShouldNot(HaveOccurred())
155
156 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
157 Expect(err).ShouldNot(HaveOccurred())
158
159 Kill()
160
161 Eventually(session1).Should(Exit(128 + 9))
162 Eventually(session2).Should(Exit(128 + 9))
163 Eventually(session3).Should(Exit(128 + 9))
164 })
165
166 It("should not track unstarted sessions", func() {
167 _, err := Start(exec.Command("does not exist", "10000000"), GinkgoWriter, GinkgoWriter)
168 Expect(err).Should(HaveOccurred())
169
170 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
171 Expect(err).ShouldNot(HaveOccurred())
172
173 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
174 Expect(err).ShouldNot(HaveOccurred())
175
176 Kill()
177
178 Eventually(session2).Should(Exit(128 + 9))
179 Eventually(session3).Should(Exit(128 + 9))
180 })
181
182 })
183
184 Describe("killAndWait", func() {
185 It("should kill all the started sessions and wait for them to finish", func() {
186 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
187 Expect(err).ShouldNot(HaveOccurred())
188
189 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
190 Expect(err).ShouldNot(HaveOccurred())
191
192 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
193 Expect(err).ShouldNot(HaveOccurred())
194
195 KillAndWait()
196 Expect(session1).Should(Exit(128+9), "Should have exited")
197 Expect(session2).Should(Exit(128+9), "Should have exited")
198 Expect(session3).Should(Exit(128+9), "Should have exited")
199 })
200 })
201
202 Describe("terminate", func() {
203 It("should terminate all the started sessions", func() {
204 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
205 Expect(err).ShouldNot(HaveOccurred())
206
207 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
208 Expect(err).ShouldNot(HaveOccurred())
209
210 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
211 Expect(err).ShouldNot(HaveOccurred())
212
213 Terminate()
214
215 Eventually(session1).Should(Exit(128 + 15))
216 Eventually(session2).Should(Exit(128 + 15))
217 Eventually(session3).Should(Exit(128 + 15))
218 })
219 })
220
221 Describe("terminateAndWait", func() {
222 It("should terminate all the started sessions, and wait for them to exit", func() {
223 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
224 Expect(err).ShouldNot(HaveOccurred())
225
226 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
227 Expect(err).ShouldNot(HaveOccurred())
228
229 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
230 Expect(err).ShouldNot(HaveOccurred())
231
232 TerminateAndWait()
233
234 Expect(session1).Should(Exit(128+15), "Should have exited")
235 Expect(session2).Should(Exit(128+15), "Should have exited")
236 Expect(session3).Should(Exit(128+15), "Should have exited")
237 })
238 })
239
240 Describe("signal", func() {
241 It("should signal all the started sessions", func() {
242 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
243 Expect(err).ShouldNot(HaveOccurred())
244
245 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
246 Expect(err).ShouldNot(HaveOccurred())
247
248 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
249 Expect(err).ShouldNot(HaveOccurred())
250
251 Signal(syscall.SIGABRT)
252
253 Eventually(session1).Should(Exit(128 + 6))
254 Eventually(session2).Should(Exit(128 + 6))
255 Eventually(session3).Should(Exit(128 + 6))
256 })
257 })
258
259 Describe("interrupt", func() {
260 It("should interrupt all the started sessions, and not wait", func() {
261 session1, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
262 Expect(err).ShouldNot(HaveOccurred())
263
264 session2, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
265 Expect(err).ShouldNot(HaveOccurred())
266
267 session3, err := Start(exec.Command("sleep", "10000000"), GinkgoWriter, GinkgoWriter)
268 Expect(err).ShouldNot(HaveOccurred())
269
270 Interrupt()
271
272 Eventually(session1).Should(Exit(128 + 2))
273 Eventually(session2).Should(Exit(128 + 2))
274 Eventually(session3).Should(Exit(128 + 2))
275 })
276 })
277 })
278
279 When("the command exits", func() {
280 It("should close the buffers", func() {
281 Eventually(session).Should(Exit())
282
283 Expect(session.Out.Closed()).Should(BeTrue())
284 Expect(session.Err.Closed()).Should(BeTrue())
285
286 Expect(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
287 })
288
289 var So = It
290
291 So("this means that eventually should short circuit", func() {
292 t := time.Now()
293 failures := InterceptGomegaFailures(func() {
294 Eventually(session).Should(Say("blah blah blah blah blah"))
295 })
296 Expect(time.Since(t)).Should(BeNumerically("<=", 500*time.Millisecond))
297 Expect(failures).Should(HaveLen(1))
298 })
299 })
300
301 When("wrapping out and err", func() {
302 var (
303 outWriterBuffer, errWriterBuffer *Buffer
304 )
305
306 BeforeEach(func() {
307 outWriterBuffer = NewBuffer()
308 outWriter = outWriterBuffer
309 errWriterBuffer = NewBuffer()
310 errWriter = errWriterBuffer
311 })
312
313 It("should route to both the provided writers and the gbytes buffers", func() {
314 Eventually(session.Out).Should(Say("We've done the impossible, and that makes us mighty"))
315 Eventually(session.Err).Should(Say("Ah, curse your sudden but inevitable betrayal!"))
316
317 Expect(outWriterBuffer.Contents()).Should(ContainSubstring("We've done the impossible, and that makes us mighty"))
318 Expect(errWriterBuffer.Contents()).Should(ContainSubstring("Ah, curse your sudden but inevitable betrayal!"))
319
320 Eventually(session).Should(Exit())
321
322 Expect(outWriterBuffer.Contents()).Should(Equal(session.Out.Contents()))
323 Expect(errWriterBuffer.Contents()).Should(Equal(session.Err.Contents()))
324 })
325
326 When("discarding the output of the command", func() {
327 BeforeEach(func() {
328 outWriter = io.Discard
329 errWriter = io.Discard
330 })
331
332 It("executes succesfuly", func() {
333 Eventually(session).Should(Exit())
334 })
335 })
336 })
27337 })
28338
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)
339 Context("firefly tests", func() {
340 var fireflyTestPath string
341 var command *exec.Cmd
342 var session *Session
343
344 var outWriter, errWriter io.Writer
345
346 BeforeEach(func() {
347 outWriter = nil
348 errWriter = nil
349
350 var err error
351 fireflyTestPath, err = CompileTest("./_fixture/firefly")
92352 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)
353 })
354
355 JustBeforeEach(func() {
356 command = exec.Command(fireflyTestPath)
357 var err error
358 session, err = Start(command, outWriter, errWriter)
102359 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() {
360 })
361
362 When("wrapping out and err", func() {
363 var (
364 outWriterBuffer, errWriterBuffer *Buffer
365 )
366
320367 BeforeEach(func() {
321 outWriter = ioutil.Discard
322 errWriter = ioutil.Discard
323 })
324
325 It("executes succesfuly", func() {
368 outWriterBuffer = NewBuffer()
369 outWriter = outWriterBuffer
370 errWriterBuffer = NewBuffer()
371 errWriter = errWriterBuffer
372 })
373
374 It("should route to both the provided writers and the gbytes buffers", func() {
375 Eventually(session.Out).Should(Say("PASS"))
376 Eventually(session.Err).Should(Say(""))
377
378 Expect(outWriterBuffer.Contents()).Should(ContainSubstring("PASS"))
379 Expect(errWriterBuffer.Contents()).Should(BeEmpty())
380
326381 Eventually(session).Should(Exit())
382
383 Expect(outWriterBuffer.Contents()).Should(Equal(session.Out.Contents()))
384 Expect(errWriterBuffer.Contents()).Should(Equal(session.Err.Contents()))
385 })
386
387 When("discarding the output of the command", func() {
388 BeforeEach(func() {
389 outWriter = io.Discard
390 errWriter = io.Discard
391 })
392
393 It("executes succesfuly", func() {
394 Eventually(session).Should(Exit())
395 })
327396 })
328397 })
329398 })
55 "encoding/base64"
66 "encoding/json"
77 "fmt"
8 "io/ioutil"
98 "net/http"
109 "net/url"
1110 "reflect"
1413 "github.com/golang/protobuf/proto"
1514 "github.com/onsi/gomega"
1615 . "github.com/onsi/gomega"
16 "github.com/onsi/gomega/internal/gutil"
1717 "github.com/onsi/gomega/types"
1818 )
1919
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 := gutil.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 := gutil.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 := gutil.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"
120119 "sync"
121120
122121 . "github.com/onsi/gomega"
122 "github.com/onsi/gomega/internal/gutil"
123123 )
124124
125125 func new() *Server {
268268 } else {
269269 s.rwMutex.Unlock()
270270 if s.GetAllowUnhandledRequests() {
271 ioutil.ReadAll(req.Body)
271 gutil.ReadAll(req.Body)
272272 req.Body.Close()
273273 w.WriteHeader(s.GetUnhandledRequestStatusCode())
274274 } else {
00 package ghttp_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55
66 "testing"
22 import (
33 "bytes"
44 "io"
5 "io/ioutil"
65 "net/http"
76 "net/url"
87 "regexp"
109 "github.com/golang/protobuf/proto"
1110 "github.com/onsi/gomega/gbytes"
1211 "github.com/onsi/gomega/ghttp/protobuf"
13
14 . "github.com/onsi/ginkgo"
12 "github.com/onsi/gomega/internal/gutil"
13
14 . "github.com/onsi/ginkgo/v2"
1515 . "github.com/onsi/gomega"
1616 . "github.com/onsi/gomega/ghttp"
1717 )
5959 Expect(err).ShouldNot(HaveOccurred())
6060 Expect(resp.StatusCode).Should(Equal(200))
6161
62 body, err := ioutil.ReadAll(resp.Body)
62 body, err := gutil.ReadAll(resp.Body)
6363 resp.Body.Close()
6464 Expect(err).ShouldNot(HaveOccurred())
6565
6969 Expect(err).ShouldNot(HaveOccurred())
7070 Expect(resp.StatusCode).Should(Equal(200))
7171
72 body2, err := ioutil.ReadAll(resp.Body)
72 body2, err := gutil.ReadAll(resp.Body)
7373 resp.Body.Close()
7474 Expect(err).ShouldNot(HaveOccurred())
7575
101101 Expect(err).ShouldNot(HaveOccurred())
102102 Expect(resp.StatusCode).Should(Equal(http.StatusForbidden))
103103
104 data, err := ioutil.ReadAll(resp.Body)
104 data, err := gutil.ReadAll(resp.Body)
105105 Expect(err).ShouldNot(HaveOccurred())
106106 Expect(data).Should(BeEmpty())
107107 })
258258 s.AppendHandlers(func(w http.ResponseWriter, req *http.Request) {
259259 // Expect(true).Should(BeFalse()) <-- would be nice to do it this way, but the test just can't be written this way
260260
261 By("We're cheating a bit here -- we're throwing a GINKGO_PANIC which simulates a failed assertion")
262 panic(GINKGO_PANIC)
261 By("We're cheating a bit here -- we're pretending to throw a Ginkgo panic which simulates a failed assertion")
262 panic("defer GinkgoRecover()")
263263 })
264264 })
265265
791791
792792 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
793793
794 body, err := ioutil.ReadAll(resp.Body)
794 body, err := gutil.ReadAll(resp.Body)
795795 Expect(err).ShouldNot(HaveOccurred())
796796 Expect(body).Should(Equal([]byte("sweet")))
797797
800800
801801 Expect(resp.StatusCode).Should(Equal(http.StatusOK))
802802
803 body, err = ioutil.ReadAll(resp.Body)
803 body, err = gutil.ReadAll(resp.Body)
804804 Expect(err).ShouldNot(HaveOccurred())
805805 Expect(body).Should(Equal([]byte("sour")))
806806 })
819819 Expect(err).ShouldNot(HaveOccurred())
820820
821821 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
822 Expect(ioutil.ReadAll(resp.Body)).Should(Equal([]byte("sweet")))
822 Expect(gutil.ReadAll(resp.Body)).Should(Equal([]byte("sweet")))
823823 Expect(resp.Header.Get("X-Custom-Header")).Should(Equal("my header"))
824824 })
825825 })
853853
854854 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
855855
856 body, err := ioutil.ReadAll(resp.Body)
856 body, err := gutil.ReadAll(resp.Body)
857857 Expect(err).ShouldNot(HaveOccurred())
858858 Expect(body).Should(Equal([]byte("tasty")))
859859
862862
863863 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
864864
865 body, err = ioutil.ReadAll(resp.Body)
865 body, err = gutil.ReadAll(resp.Body)
866866 Expect(err).ShouldNot(HaveOccurred())
867867 Expect(body).Should(Equal([]byte("treat")))
868868 })
880880
881881 Expect(err).ShouldNot(HaveOccurred())
882882 Expect(resp.StatusCode).Should(Equal(http.StatusOK))
883 body, err := ioutil.ReadAll(resp.Body)
883 body, err := gutil.ReadAll(resp.Body)
884884 Expect(err).ShouldNot(HaveOccurred())
885885 Expect(body).Should(BeEmpty())
886886
904904
905905 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
906906
907 body, err := ioutil.ReadAll(resp.Body)
907 body, err := gutil.ReadAll(resp.Body)
908908 Expect(err).ShouldNot(HaveOccurred())
909909 Expect(body).Should(MatchJSON("[1,2,3]"))
910910 })
989989
990990 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
991991
992 body, err := ioutil.ReadAll(resp.Body)
992 body, err := gutil.ReadAll(resp.Body)
993993 Expect(err).ShouldNot(HaveOccurred())
994994 Expect(body).Should(MatchJSON(`{"Key": "Jim", "Value": "Codes"}`))
995995 })
10701070 Expect(resp.StatusCode).Should(Equal(http.StatusCreated))
10711071
10721072 var received protobuf.SimpleMessage
1073 body, err := ioutil.ReadAll(resp.Body)
1073 body, err := gutil.ReadAll(resp.Body)
10741074 Expect(err).ShouldNot(HaveOccurred())
10751075 err = proto.Unmarshal(body, &received)
10761076 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 "github.com/onsi/gomega/internal/gutil"
10 )
11
12 const CACHE_EXT = ".gmeasure-cache"
13
14 /*
15 ExperimentCache provides a director-and-file based cache of experiments
16 */
17 type ExperimentCache struct {
18 Path string
19 }
20
21 /*
22 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).
23
24 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.
25 */
26 func NewExperimentCache(path string) (ExperimentCache, error) {
27 stat, err := os.Stat(path)
28 if os.IsNotExist(err) {
29 err := os.MkdirAll(path, 0777)
30 if err != nil {
31 return ExperimentCache{}, err
32 }
33 } else if !stat.IsDir() {
34 return ExperimentCache{}, fmt.Errorf("%s is not a directory", path)
35 }
36
37 return ExperimentCache{
38 Path: path,
39 }, nil
40 }
41
42 /*
43 CachedExperimentHeader captures the name of the Cached Experiment and its Version
44 */
45 type CachedExperimentHeader struct {
46 Name string
47 Version int
48 }
49
50 func (cache ExperimentCache) hashOf(name string) string {
51 return fmt.Sprintf("%x", md5.Sum([]byte(name)))
52 }
53
54 func (cache ExperimentCache) readHeader(filename string) (CachedExperimentHeader, error) {
55 out := CachedExperimentHeader{}
56 f, err := os.Open(filepath.Join(cache.Path, filename))
57 if err != nil {
58 return out, err
59 }
60 defer f.Close()
61 err = json.NewDecoder(f).Decode(&out)
62 return out, err
63 }
64
65 /*
66 List returns a list of all Cached Experiments found in the cache.
67 */
68 func (cache ExperimentCache) List() ([]CachedExperimentHeader, error) {
69 var out []CachedExperimentHeader
70 names, err := gutil.ReadDir(cache.Path)
71 if err != nil {
72 return out, err
73 }
74 for _, name := range names {
75 if filepath.Ext(name) != CACHE_EXT {
76 continue
77 }
78 header, err := cache.readHeader(name)
79 if err != nil {
80 return out, err
81 }
82 out = append(out, header)
83 }
84 return out, nil
85 }
86
87 /*
88 Clear empties out the cache - this will delete any and all detected cache files in the cache directory. Use with caution!
89 */
90 func (cache ExperimentCache) Clear() error {
91 names, err := gutil.ReadDir(cache.Path)
92 if err != nil {
93 return err
94 }
95 for _, name := range names {
96 if filepath.Ext(name) != CACHE_EXT {
97 continue
98 }
99 err := os.Remove(filepath.Join(cache.Path, name))
100 if err != nil {
101 return err
102 }
103 }
104 return nil
105 }
106
107 /*
108 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.
109
110 If an experiment with corresponding name and version >= the passed-in version is found, it is unmarshaled and returned.
111
112 If no experiment is found, or the cached version is smaller than the passed-in version, Load will return nil.
113
114 When paired with Ginkgo you can cache experiments and prevent potentially expensive recomputation with this pattern:
115
116 const EXPERIMENT_VERSION = 1 //bump this to bust the cache and recompute _all_ experiments
117
118 Describe("some experiments", func() {
119 var cache gmeasure.ExperimentCache
120 var experiment *gmeasure.Experiment
121
122 BeforeEach(func() {
123 cache = gmeasure.NewExperimentCache("./gmeasure-cache")
124 name := CurrentSpecReport().LeafNodeText
125 experiment = cache.Load(name, EXPERIMENT_VERSION)
126 if experiment != nil {
127 AddReportEntry(experiment)
128 Skip("cached")
129 }
130 experiment = gmeasure.NewExperiment(name)
131 AddReportEntry(experiment)
132 })
133
134 It("foo runtime", func() {
135 experiment.SampleDuration("runtime", func() {
136 //do stuff
137 }, gmeasure.SamplingConfig{N:100})
138 })
139
140 It("bar runtime", func() {
141 experiment.SampleDuration("runtime", func() {
142 //do stuff
143 }, gmeasure.SamplingConfig{N:100})
144 })
145
146 AfterEach(func() {
147 if !CurrentSpecReport().State.Is(types.SpecStateSkipped) {
148 cache.Save(experiment.Name, EXPERIMENT_VERSION, experiment)
149 }
150 })
151 })
152 */
153 func (cache ExperimentCache) Load(name string, version int) *Experiment {
154 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
155 f, err := os.Open(path)
156 if err != nil {
157 return nil
158 }
159 defer f.Close()
160 dec := json.NewDecoder(f)
161 header := CachedExperimentHeader{}
162 dec.Decode(&header)
163 if header.Version < version {
164 return nil
165 }
166 out := NewExperiment("")
167 err = dec.Decode(out)
168 if err != nil {
169 return nil
170 }
171 return out
172 }
173
174 /*
175 Save stores the passed-in experiment to the cache with the passed-in name and version.
176 */
177 func (cache ExperimentCache) Save(name string, version int, experiment *Experiment) error {
178 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
179 f, err := os.Create(path)
180 if err != nil {
181 return err
182 }
183 defer f.Close()
184 enc := json.NewEncoder(f)
185 err = enc.Encode(CachedExperimentHeader{
186 Name: name,
187 Version: version,
188 })
189 if err != nil {
190 return err
191 }
192 return enc.Encode(experiment)
193 }
194
195 /*
196 Delete removes the experiment with the passed-in name from the cache
197 */
198 func (cache ExperimentCache) Delete(name string) error {
199 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
200 return os.Remove(path)
201 }
0 package gmeasure_test
1
2 import (
3 "fmt"
4 "os"
5
6 . "github.com/onsi/ginkgo/v2"
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", GinkgoParallelProcess())
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/v2"
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/v2"
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/v2"
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(BeNumerically("~", 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/v2"
7 . "github.com/onsi/gomega"
8 "github.com/onsi/gomega/gmeasure"
9 )
10
11 var _ = Describe("Rank", func() {
12 var A, B, C, D gmeasure.Stats
13
14 Describe("Ranking Values", func() {
15 makeStats := func(name string, min float64, max float64, mean float64, median float64) gmeasure.Stats {
16 return gmeasure.Stats{
17 Type: gmeasure.StatsTypeValue,
18 ExperimentName: "Exp-" + name,
19 MeasurementName: name,
20 N: 100,
21 PrecisionBundle: gmeasure.Precision(2),
22 ValueBundle: map[gmeasure.Stat]float64{
23 gmeasure.StatMin: min,
24 gmeasure.StatMax: max,
25 gmeasure.StatMean: mean,
26 gmeasure.StatMedian: median,
27 gmeasure.StatStdDev: 2.0,
28 },
29 }
30 }
31
32 BeforeEach(func() {
33 A = makeStats("A", 1, 2, 3, 4)
34 B = makeStats("B", 2, 3, 4, 1)
35 C = makeStats("C", 3, 4, 1, 2)
36 D = makeStats("D", 4, 1, 2, 3)
37 })
38
39 DescribeTable("ranking by criteria",
40 func(criteria gmeasure.RankingCriteria, expectedOrder func() []gmeasure.Stats) {
41 ranking := gmeasure.RankStats(criteria, A, B, C, D)
42 expected := expectedOrder()
43 Ω(ranking.Winner()).Should(Equal(expected[0]))
44 Ω(ranking.Stats).Should(Equal(expected))
45 },
46 Entry("entry", gmeasure.LowerMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, D, A, B} }),
47 Entry("entry", gmeasure.HigherMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, A, D, C} }),
48 Entry("entry", gmeasure.LowerMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, C, D, A} }),
49 Entry("entry", gmeasure.HigherMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, D, C, B} }),
50 Entry("entry", gmeasure.LowerMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, B, C, D} }),
51 Entry("entry", gmeasure.HigherMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, C, B, A} }),
52 Entry("entry", gmeasure.LowerMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, A, B, C} }),
53 Entry("entry", gmeasure.HigherMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, B, A, D} }),
54 )
55
56 Describe("Generating Reports", func() {
57 It("can generate an unstyled report", func() {
58 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
59 Ω(ranking.String()).Should(Equal(strings.Join([]string{
60 "Ranking Criteria: Lower Mean is Better",
61 "Experiment | Name | N | Min | Median | Mean | StdDev | Max ",
62 "==================================================================",
63 "Exp-C | C | 100 | 3.00 | 2.00 | 1.00 | 2.00 | 4.00",
64 "*Winner* | *Winner* | | | | | | ",
65 "------------------------------------------------------------------",
66 "Exp-D | D | 100 | 4.00 | 3.00 | 2.00 | 2.00 | 1.00",
67 "------------------------------------------------------------------",
68 "Exp-A | A | 100 | 1.00 | 4.00 | 3.00 | 2.00 | 2.00",
69 "------------------------------------------------------------------",
70 "Exp-B | B | 100 | 2.00 | 1.00 | 4.00 | 2.00 | 3.00",
71 "",
72 }, "\n")))
73 })
74
75 It("can generate a styled report", func() {
76 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
77 Ω(ranking.ColorableString()).Should(Equal(strings.Join([]string{
78 "{{bold}}Ranking Criteria: Lower Mean is Better",
79 "{{/}}{{bold}}Experiment{{/}} | {{bold}}Name {{/}} | {{bold}}N {{/}} | {{bold}}Min {{/}} | {{bold}}Median{{/}} | {{bold}}Mean{{/}} | {{bold}}StdDev{{/}} | {{bold}}Max {{/}}",
80 "==================================================================",
81 "{{bold}}Exp-C {{/}} | {{bold}}C {{/}} | {{bold}}100{{/}} | {{bold}}3.00{{/}} | {{bold}}2.00 {{/}} | {{bold}}1.00{{/}} | {{bold}}2.00 {{/}} | {{bold}}4.00{{/}}",
82 "{{bold}}*Winner* {{/}} | {{bold}}*Winner*{{/}} | | | | | | ",
83 "------------------------------------------------------------------",
84 "Exp-D | D | 100 | 4.00 | 3.00 | 2.00 | 2.00 | 1.00",
85 "------------------------------------------------------------------",
86 "Exp-A | A | 100 | 1.00 | 4.00 | 3.00 | 2.00 | 2.00",
87 "------------------------------------------------------------------",
88 "Exp-B | B | 100 | 2.00 | 1.00 | 4.00 | 2.00 | 3.00",
89 "",
90 }, "\n")))
91 })
92 })
93 })
94
95 Describe("Ranking Durations", func() {
96 makeStats := func(name string, min time.Duration, max time.Duration, mean time.Duration, median time.Duration) gmeasure.Stats {
97 return gmeasure.Stats{
98 Type: gmeasure.StatsTypeDuration,
99 ExperimentName: "Exp-" + name,
100 MeasurementName: name,
101 N: 100,
102 PrecisionBundle: gmeasure.Precision(time.Millisecond * 100),
103 DurationBundle: map[gmeasure.Stat]time.Duration{
104 gmeasure.StatMin: min,
105 gmeasure.StatMax: max,
106 gmeasure.StatMean: mean,
107 gmeasure.StatMedian: median,
108 gmeasure.StatStdDev: 2.0,
109 },
110 }
111 }
112
113 BeforeEach(func() {
114 A = makeStats("A", 1*time.Second, 2*time.Second, 3*time.Second, 4*time.Second)
115 B = makeStats("B", 2*time.Second, 3*time.Second, 4*time.Second, 1*time.Second)
116 C = makeStats("C", 3*time.Second, 4*time.Second, 1*time.Second, 2*time.Second)
117 D = makeStats("D", 4*time.Second, 1*time.Second, 2*time.Second, 3*time.Second)
118 })
119
120 DescribeTable("ranking by criteria",
121 func(criteria gmeasure.RankingCriteria, expectedOrder func() []gmeasure.Stats) {
122 ranking := gmeasure.RankStats(criteria, A, B, C, D)
123 expected := expectedOrder()
124 Ω(ranking.Winner()).Should(Equal(expected[0]))
125 Ω(ranking.Stats).Should(Equal(expected))
126 },
127 Entry("entry", gmeasure.LowerMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, D, A, B} }),
128 Entry("entry", gmeasure.HigherMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, A, D, C} }),
129 Entry("entry", gmeasure.LowerMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, C, D, A} }),
130 Entry("entry", gmeasure.HigherMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, D, C, B} }),
131 Entry("entry", gmeasure.LowerMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, B, C, D} }),
132 Entry("entry", gmeasure.HigherMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, C, B, A} }),
133 Entry("entry", gmeasure.LowerMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, A, B, C} }),
134 Entry("entry", gmeasure.HigherMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, B, A, D} }),
135 )
136
137 Describe("Generating Reports", func() {
138 It("can generate an unstyled report", func() {
139 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
140 Ω(ranking.String()).Should(Equal(strings.Join([]string{
141 "Ranking Criteria: Lower Mean is Better",
142 "Experiment | Name | N | Min | Median | Mean | StdDev | Max",
143 "================================================================",
144 "Exp-C | C | 100 | 3s | 2s | 1s | 0s | 4s ",
145 "*Winner* | *Winner* | | | | | | ",
146 "----------------------------------------------------------------",
147 "Exp-D | D | 100 | 4s | 3s | 2s | 0s | 1s ",
148 "----------------------------------------------------------------",
149 "Exp-A | A | 100 | 1s | 4s | 3s | 0s | 2s ",
150 "----------------------------------------------------------------",
151 "Exp-B | B | 100 | 2s | 1s | 4s | 0s | 3s ",
152 "",
153 }, "\n")))
154 })
155
156 It("can generate a styled report", func() {
157 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
158 Ω(ranking.ColorableString()).Should(Equal(strings.Join([]string{
159 "{{bold}}Ranking Criteria: Lower Mean is Better",
160 "{{/}}{{bold}}Experiment{{/}} | {{bold}}Name {{/}} | {{bold}}N {{/}} | {{bold}}Min{{/}} | {{bold}}Median{{/}} | {{bold}}Mean{{/}} | {{bold}}StdDev{{/}} | {{bold}}Max{{/}}",
161 "================================================================",
162 "{{bold}}Exp-C {{/}} | {{bold}}C {{/}} | {{bold}}100{{/}} | {{bold}}3s {{/}} | {{bold}}2s {{/}} | {{bold}}1s {{/}} | {{bold}}0s {{/}} | {{bold}}4s {{/}}",
163 "{{bold}}*Winner* {{/}} | {{bold}}*Winner*{{/}} | | | | | | ",
164 "----------------------------------------------------------------",
165 "Exp-D | D | 100 | 4s | 3s | 2s | 0s | 1s ",
166 "----------------------------------------------------------------",
167 "Exp-A | A | 100 | 1s | 4s | 3s | 0s | 2s ",
168 "----------------------------------------------------------------",
169 "Exp-B | B | 100 | 2s | 1s | 4s | 0s | 3s ",
170 "",
171 }, "\n")))
172 })
173 })
174 })
175
176 })
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/v2"
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/v2"
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/v2 v2.0.0
7 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
8 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
9 gopkg.in/yaml.v2 v2.4.0
1010 )
0 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
0 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
1 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
2 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
15 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
2 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
6 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
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=
15 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
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=
23 github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
1624 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
17 github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
25 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
1826 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
19 github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
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=
32 github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ=
33 github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
2334 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
35 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
36 github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
37 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
38 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
39 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
40 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
2441 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
42 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
2543 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=
44 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
2745 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
2846 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=
47 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
3048 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=
49 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
50 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
51 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
3452 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=
53 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
54 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
3655 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3756 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3857 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
3958 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=
59 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4160 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=
61 golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4362 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4463 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
64 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
65 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
66 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
67 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
68 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
69 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
4670 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
47 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
4871 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
72 golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
73 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
4974 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=
75 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
76 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
77 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
78 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5179 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
80 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
81 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5282 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
5383 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
5484 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
5585 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
5686 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
57 google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
5887 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
88 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
89 google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
90 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
5991 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
6092 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
61 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
6293 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
63 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
6494 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
65 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
95 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
6696 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
67 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
6897 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
98 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
99 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.18.1"
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))
00 package gstruct_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/gstruct"
66 )
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 {
00 package gstruct_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/gstruct"
66 )
22 import (
33 "testing"
44
5 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
66 . "github.com/onsi/gomega"
77 )
88
00 package gstruct_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/gstruct"
66 )
00 package gstruct_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/gstruct"
66 )
00 package gstruct_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/gstruct"
66 )
+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/v2"
6 . "github.com/onsi/gomega"
7 )
8
9 var _ = Describe("Making Synchronous Assertions", func() {
10 var SHOULD_MATCH = true
11 var SHOULD_NOT_MATCH = false
12 var IT_PASSES = true
13 var IT_FAILS = false
14
15 Extras := func(extras ...interface{}) []interface{} {
16 return extras
17 }
18
19 OptionalDescription := func(optionalDescription ...interface{}) []interface{} {
20 return optionalDescription
21 }
22
23 DescribeTable(
24 "the various cases",
25 func(actual interface{}, extras []interface{}, optionalDescription []interface{}, isPositiveAssertion bool, expectedFailureMessage string, expectedReturnValue bool) {
26 if isPositiveAssertion {
27 ig := NewInstrumentedGomega()
28 returnValue := ig.G.Expect(actual, extras...).To(SpecMatch(), optionalDescription...)
29 Expect(returnValue).To(Equal(expectedReturnValue))
30 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
31 if expectedFailureMessage != "" {
32 Expect(ig.FailureSkip).To(Equal([]int{2}))
33 }
34 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).To"))
35
36 ig = NewInstrumentedGomega()
37 returnValue = ig.G.ExpectWithOffset(3, actual, extras...).To(SpecMatch(), optionalDescription...)
38 Expect(returnValue).To(Equal(expectedReturnValue))
39 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
40 if expectedFailureMessage != "" {
41 Expect(ig.FailureSkip).To(Equal([]int{5}))
42 }
43 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).To"))
44
45 ig = NewInstrumentedGomega()
46 returnValue = ig.G.Ω(actual, extras...).Should(SpecMatch(), optionalDescription...)
47 Expect(returnValue).To(Equal(expectedReturnValue))
48 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
49 if expectedFailureMessage != "" {
50 Expect(ig.FailureSkip).To(Equal([]int{2}))
51 }
52 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).Should"))
53 } else {
54 ig := NewInstrumentedGomega()
55 returnValue := ig.G.Expect(actual, extras...).ToNot(SpecMatch(), optionalDescription...)
56 Expect(returnValue).To(Equal(expectedReturnValue))
57 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
58 if expectedFailureMessage != "" {
59 Expect(ig.FailureSkip).To(Equal([]int{2}))
60 }
61 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).ToNot"))
62
63 ig = NewInstrumentedGomega()
64 returnValue = ig.G.Expect(actual, extras...).NotTo(SpecMatch(), optionalDescription...)
65 Expect(returnValue).To(Equal(expectedReturnValue))
66 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
67 if expectedFailureMessage != "" {
68 Expect(ig.FailureSkip).To(Equal([]int{2}))
69 }
70 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).NotTo"))
71
72 ig = NewInstrumentedGomega()
73 returnValue = ig.G.ExpectWithOffset(3, actual, extras...).NotTo(SpecMatch(), optionalDescription...)
74 Expect(returnValue).To(Equal(expectedReturnValue))
75 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
76 if expectedFailureMessage != "" {
77 Expect(ig.FailureSkip).To(Equal([]int{5}))
78 }
79 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).NotTo"))
80
81 ig = NewInstrumentedGomega()
82 returnValue = ig.G.Ω(actual, extras...).ShouldNot(SpecMatch(), optionalDescription...)
83 Expect(returnValue).To(Equal(expectedReturnValue))
84 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
85 if expectedFailureMessage != "" {
86 Expect(ig.FailureSkip).To(Equal([]int{2}))
87 }
88 Expect(ig.RegisteredHelpers).To(ContainElement("(*Assertion).ShouldNot"))
89 }
90 },
91 Entry(
92 "when the matcher matches and a positive assertion is being made",
93 MATCH, Extras(), OptionalDescription(),
94 SHOULD_MATCH, "", IT_PASSES,
95 ),
96 Entry(
97 "when the matcher matches and a negative assertion is being made",
98 MATCH, Extras(), OptionalDescription(),
99 SHOULD_NOT_MATCH, "negative: match", IT_FAILS,
100 ),
101 Entry(
102 "when the matcher does not match and a positive assertion is being made",
103 NO_MATCH, Extras(), OptionalDescription(),
104 SHOULD_MATCH, "positive: no match", IT_FAILS,
105 ),
106 Entry(
107 "when the matcher does not match and a negative assertion is being made",
108 NO_MATCH, Extras(), OptionalDescription(),
109 SHOULD_NOT_MATCH, "", IT_PASSES,
110 ),
111 Entry(
112 "when the matcher returns an error and a positive assertion is being made",
113 ERR_MATCH, Extras(), OptionalDescription(),
114 SHOULD_MATCH, "spec matcher error", IT_FAILS,
115 ),
116 Entry(
117 "when the matcher returns an error and a negative assertion is being made",
118 ERR_MATCH, Extras(), OptionalDescription(),
119 SHOULD_NOT_MATCH, "spec matcher error", IT_FAILS,
120 ),
121 Entry(
122 "when a failure occurs and there is a single optional description",
123 NO_MATCH, Extras(), OptionalDescription("a description"),
124 SHOULD_MATCH, "a description\npositive: no match", IT_FAILS,
125 ),
126 Entry(
127 "when a failure occurs and there are multiple optional descriptions",
128 NO_MATCH, Extras(), OptionalDescription("a description of [%d]", 3),
129 SHOULD_MATCH, "a description of [3]\npositive: no match", IT_FAILS,
130 ),
131 Entry(
132 "when a failure occurs and the optional description is a function",
133 NO_MATCH, Extras(), OptionalDescription(func() string { return "a description" }),
134 SHOULD_MATCH, "a description\npositive: no match", IT_FAILS,
135 ),
136 Entry(
137 "when the matcher matches and zero-valued extra parameters are included, it passes",
138 MATCH, Extras(0, "", struct{ Foo string }{}, nil), OptionalDescription(),
139 SHOULD_MATCH, "", IT_PASSES,
140 ),
141 Entry(
142 "when the matcher matches but a non-zero-valued extra parameter is included, it fails",
143 MATCH, Extras(1, "bam", struct{ Foo string }{Foo: "foo"}, nil), OptionalDescription(),
144 SHOULD_MATCH, "Unexpected non-nil/non-zero argument at index 1:\n\t<int>: 1", IT_FAILS,
145 ),
146 )
147
148 var SHOULD_OCCUR = true
149 var SHOULD_NOT_OCCUR = false
150
151 DescribeTable("error expectations",
152 func(a, b int, e error, isPositiveAssertion bool, expectedFailureMessage string, expectedReturnValue bool) {
153 abe := func(a, b int, e error) (int, int, error) {
154 return a, b, e
155 }
156 ig := NewInstrumentedGomega()
157 var returnValue bool
158 if isPositiveAssertion {
159 returnValue = ig.G.Expect(abe(a, b, e)).Error().To(HaveOccurred())
160 } else {
161 returnValue = ig.G.Expect(abe(a, b, e)).Error().NotTo(HaveOccurred())
162 }
163 Expect(returnValue).To(Equal(expectedReturnValue))
164 Expect(ig.FailureMessage).To(Equal(expectedFailureMessage))
165 if expectedFailureMessage != "" {
166 Expect(ig.FailureSkip).To(Equal([]int{2}))
167 }
168 },
169 Entry(
170 "when non-zero results without error",
171 1, 2, nil,
172 SHOULD_NOT_OCCUR, "", IT_PASSES,
173 ),
174 Entry(
175 "when non-zero results with error",
176 1, 2, errors.New("D'oh!"),
177 SHOULD_NOT_OCCUR, "Unexpected non-nil/non-zero argument at index 0:\n\t<int>: 1", IT_FAILS,
178 ),
179 Entry(
180 "when non-zero results without error",
181 0, 0, errors.New("D'oh!"),
182 SHOULD_OCCUR, "", IT_PASSES,
183 ),
184 Entry(
185 "when non-zero results with error",
186 1, 2, errors.New("D'oh!"),
187 SHOULD_OCCUR, "Unexpected non-nil/non-zero argument at index 0:\n\t<int>: 1", IT_FAILS,
188 ),
189 )
190
191 })
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/v2"
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/v2"
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/v2"
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/v2"
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 //go:build go1.16
1 // +build go1.16
2
3 // Package gutil is a replacement for ioutil, which should not be used in new
4 // code as of Go 1.16. With Go 1.16 and higher, this implementation
5 // uses the ioutil replacement functions in "io" and "os" with some
6 // Gomega specifics. This means that we should not get deprecation warnings
7 // for ioutil when they are added.
8 package gutil
9
10 import (
11 "io"
12 "os"
13 )
14
15 func NopCloser(r io.Reader) io.ReadCloser {
16 return io.NopCloser(r)
17 }
18
19 func ReadAll(r io.Reader) ([]byte, error) {
20 return io.ReadAll(r)
21 }
22
23 func ReadDir(dirname string) ([]string, error) {
24 entries, err := os.ReadDir(dirname)
25 if err != nil {
26 return nil, err
27 }
28
29 var names []string
30 for _, entry := range entries {
31 names = append(names, entry.Name())
32 }
33
34 return names, nil
35 }
36
37 func ReadFile(filename string) ([]byte, error) {
38 return os.ReadFile(filename)
39 }
40
41 func MkdirTemp(dir, pattern string) (string, error) {
42 return os.MkdirTemp(dir, pattern)
43 }
44
45 func WriteFile(filename string, data []byte) error {
46 return os.WriteFile(filename, data, 0644)
47 }
0 //go:build !go1.16
1 // +build !go1.16
2
3 // Package gutil is a replacement for ioutil, which should not be used in new
4 // code as of Go 1.16. With Go 1.15 and lower, this implementation
5 // uses the ioutil functions, meaning that although Gomega is not officially
6 // supported on these versions, it is still likely to work.
7 package gutil
8
9 import (
10 "io"
11 "io/ioutil"
12 )
13
14 func NopCloser(r io.Reader) io.ReadCloser {
15 return ioutil.NopCloser(r)
16 }
17
18 func ReadAll(r io.Reader) ([]byte, error) {
19 return ioutil.ReadAll(r)
20 }
21
22 func ReadDir(dirname string) ([]string, error) {
23 files, err := ioutil.ReadDir(dirname)
24 if err != nil {
25 return nil, err
26 }
27
28 var names []string
29 for _, file := range files {
30 names = append(names, file.Name())
31 }
32
33 return names, nil
34 }
35
36 func ReadFile(filename string) ([]byte, error) {
37 return ioutil.ReadFile(filename)
38 }
39
40 func MkdirTemp(dir, pattern string) (string, error) {
41 return ioutil.TempDir(dir, pattern)
42 }
43
44 func WriteFile(filename string, data []byte) error {
45 return ioutil.WriteFile(filename, data, 0644)
46 }
0 package internal_test
1
2 import (
3 "errors"
4 "fmt"
5 "runtime"
6 "strings"
7 "testing"
8
9 . "github.com/onsi/ginkgo/v2"
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 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 "github.com/onsi/gomega/types"
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 "io/ioutil"
43 "os"
54
6 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
76 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/internal/gutil"
88 . "github.com/onsi/gomega/matchers"
99 )
1010
1313 It("should do the right thing", func() {
1414 Expect("/dne/test").ShouldNot(BeADirectory())
1515
16 tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile")
16 tmpFile, err := os.CreateTemp("", "gomega-test-tempfile")
1717 Expect(err).ShouldNot(HaveOccurred())
1818 defer os.Remove(tmpFile.Name())
1919 Expect(tmpFile.Name()).ShouldNot(BeADirectory())
2020
21 tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir")
21 tmpDir, err := gutil.MkdirTemp("", "gomega-test-tempdir")
2222 Expect(err).ShouldNot(HaveOccurred())
2323 defer os.Remove(tmpDir)
2424 Expect(tmpDir).Should(BeADirectory())
00 package matchers_test
11
22 import (
3 "io/ioutil"
43 "os"
54
6 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
76 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/internal/gutil"
88 . "github.com/onsi/gomega/matchers"
99 )
1010
1313 It("should do the right thing", func() {
1414 Expect("/dne/test").ShouldNot(BeARegularFile())
1515
16 tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile")
16 tmpFile, err := os.CreateTemp("", "gomega-test-tempfile")
1717 Expect(err).ShouldNot(HaveOccurred())
1818 defer os.Remove(tmpFile.Name())
1919 Expect(tmpFile.Name()).Should(BeARegularFile())
2020
21 tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir")
21 tmpDir, err := gutil.MkdirTemp("", "gomega-test-tempdir")
2222 Expect(err).ShouldNot(HaveOccurred())
2323 defer os.Remove(tmpDir)
2424 Expect(tmpDir).ShouldNot(BeARegularFile())
00 package matchers_test
11
22 import (
3 "io/ioutil"
43 "os"
54
6 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
76 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/internal/gutil"
88 . "github.com/onsi/gomega/matchers"
99 )
1010
1313 It("should do the right thing", func() {
1414 Expect("/dne/test").ShouldNot(BeAnExistingFile())
1515
16 tmpFile, err := ioutil.TempFile("", "gomega-test-tempfile")
16 tmpFile, err := os.CreateTemp("", "gomega-test-tempfile")
1717 Expect(err).ShouldNot(HaveOccurred())
1818 defer os.Remove(tmpFile.Name())
1919 Expect(tmpFile.Name()).Should(BeAnExistingFile())
2020
21 tmpDir, err := ioutil.TempDir("", "gomega-test-tempdir")
21 tmpDir, err := gutil.MkdirTemp("", "gomega-test-tempdir")
2222 Expect(err).ShouldNot(HaveOccurred())
2323 defer os.Remove(tmpDir)
2424 Expect(tmpDir).Should(BeAnExistingFile())
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
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 }
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
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 })
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
22 import (
33 "errors"
44
5 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
66 . "github.com/onsi/gomega"
77 . "github.com/onsi/gomega/matchers"
88 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 )
66
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 {
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
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())
44
55 . "github.com/onsi/gomega/matchers"
66
7 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/v2"
88 . "github.com/onsi/gomega"
99 )
1010
22 import (
33 "time"
44
5 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
66 . "github.com/onsi/gomega"
77 . "github.com/onsi/gomega/matchers"
88 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 )
66
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 }
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 )
66
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 })
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
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 }
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 )
66
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 })
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
33 "errors"
44 "strings"
55
6 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/ginkgo/v2"
77 . "github.com/onsi/gomega"
88 . "github.com/onsi/gomega/matchers"
99 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
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.Ptr {
15 actualValue = actualValue.Elem()
16 }
17 if actualValue == (reflect.Value{}) {
18 return nil, fmt.Errorf("HaveField encountered nil while dereferencing a pointer of type %T.", actual)
19 }
20
21 if actualValue.Kind() != reflect.Struct {
22 return nil, fmt.Errorf("HaveField encountered:\n%s\nWhich is not a struct.", format.Object(actual, 1))
23 }
24
25 var extractedValue reflect.Value
26
27 if strings.HasSuffix(fields[0], "()") {
28 extractedValue = actualValue.MethodByName(strings.TrimSuffix(fields[0], "()"))
29 if extractedValue == (reflect.Value{}) {
30 return nil, fmt.Errorf("HaveField could not find method named '%s' in struct of type %T.", fields[0], actual)
31 }
32 t := extractedValue.Type()
33 if t.NumIn() != 0 || t.NumOut() != 1 {
34 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)
35 }
36 extractedValue = extractedValue.Call([]reflect.Value{})[0]
37 } else {
38 extractedValue = actualValue.FieldByName(fields[0])
39 if extractedValue == (reflect.Value{}) {
40 return nil, fmt.Errorf("HaveField could not find field named '%s' in struct:\n%s", fields[0], format.Object(actual, 1))
41 }
42 }
43
44 if len(fields) == 1 {
45 return extractedValue.Interface(), nil
46 } else {
47 return extractField(extractedValue.Interface(), fields[1])
48 }
49 }
50
51 type HaveFieldMatcher struct {
52 Field string
53 Expected interface{}
54
55 extractedField interface{}
56 expectedMatcher omegaMatcher
57 }
58
59 func (matcher *HaveFieldMatcher) Match(actual interface{}) (success bool, err error) {
60 matcher.extractedField, err = extractField(actual, matcher.Field)
61 if err != nil {
62 return false, err
63 }
64
65 var isMatcher bool
66 matcher.expectedMatcher, isMatcher = matcher.Expected.(omegaMatcher)
67 if !isMatcher {
68 matcher.expectedMatcher = &EqualMatcher{Expected: matcher.Expected}
69 }
70
71 return matcher.expectedMatcher.Match(matcher.extractedField)
72 }
73
74 func (matcher *HaveFieldMatcher) FailureMessage(actual interface{}) (message string) {
75 message = fmt.Sprintf("Value for field '%s' failed to satisfy matcher.\n", matcher.Field)
76 message += matcher.expectedMatcher.FailureMessage(matcher.extractedField)
77
78 return message
79 }
80
81 func (matcher *HaveFieldMatcher) NegatedFailureMessage(actual interface{}) (message string) {
82 message = fmt.Sprintf("Value for field '%s' satisfied matcher, but should not have.\n", matcher.Field)
83 message += matcher.expectedMatcher.NegatedFailureMessage(matcher.extractedField)
84
85 return message
86 }
0 package matchers_test
1
2 import (
3 "fmt"
4 "time"
5
6 . "github.com/onsi/ginkgo/v2"
7 . "github.com/onsi/gomega"
8 )
9
10 type Book struct {
11 Title string
12 Author person
13 Pages int
14 Sequel *Book
15 Prequel *Book
16 }
17
18 func (book Book) AuthorName() string {
19 return fmt.Sprintf("%s %s", book.Author.FirstName, book.Author.LastName)
20 }
21
22 func (book Book) AbbreviatedAuthor() person {
23 return person{
24 FirstName: book.Author.FirstName[0:3],
25 LastName: book.Author.LastName[0:3],
26 DOB: book.Author.DOB,
27 }
28 }
29
30 func (book Book) NoReturn() {
31 }
32
33 func (book Book) TooManyReturn() (string, error) {
34 return "", nil
35 }
36
37 func (book Book) HasArg(arg string) string {
38 return arg
39 }
40
41 type person struct {
42 FirstName string
43 LastName string
44 DOB time.Time
45 }
46
47 var _ = Describe("HaveField", func() {
48 var book Book
49 BeforeEach(func() {
50 book = Book{
51 Title: "Les Miserables",
52 Author: person{
53 FirstName: "Victor",
54 LastName: "Hugo",
55 DOB: time.Date(1802, 2, 26, 0, 0, 0, 0, time.UTC),
56 },
57 Pages: 2783,
58 Sequel: &Book{
59 Title: "Les Miserables 2",
60 },
61 }
62 })
63
64 DescribeTable("traversing the struct works",
65 func(field string, expected interface{}) {
66 Ω(book).Should(HaveField(field, expected))
67 },
68 Entry("Top-level field with default submatcher", "Title", "Les Miserables"),
69 Entry("Top-level field with custom submatcher", "Title", ContainSubstring("Les Mis")),
70 Entry("Nested field", "Author.FirstName", "Victor"),
71 Entry("Top-level method", "AuthorName()", "Victor Hugo"),
72 Entry("Nested method", "Author.DOB.Year()", BeNumerically("<", 1900)),
73 Entry("Traversing past a method", "AbbreviatedAuthor().FirstName", Equal("Vic")),
74 Entry("Traversing a pointer", "Sequel.Title", "Les Miserables 2"),
75 )
76
77 DescribeTable("negation works",
78 func(field string, expected interface{}) {
79 Ω(book).ShouldNot(HaveField(field, expected))
80 },
81 Entry("Top-level field with default submatcher", "Title", "Les Mis"),
82 Entry("Top-level field with custom submatcher", "Title", ContainSubstring("Notre Dame")),
83 Entry("Nested field", "Author.FirstName", "Hugo"),
84 Entry("Top-level method", "AuthorName()", "Victor M. Hugo"),
85 Entry("Nested method", "Author.DOB.Year()", BeNumerically(">", 1900)),
86 Entry("Traversing a pointer", "Sequel.Title", "Les Mis 2"),
87 )
88
89 Describe("when field lookup fails", func() {
90 It("errors appropriately", func() {
91 success, err := HaveField("BookName", "Les Miserables").Match(book)
92 Ω(success).Should(BeFalse())
93 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "BookName"))
94
95 success, err = HaveField("BookName", "Les Miserables").Match(book)
96 Ω(success).Should(BeFalse())
97 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "BookName"))
98
99 success, err = HaveField("AuthorName", "Victor Hugo").Match(book)
100 Ω(success).Should(BeFalse())
101 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "AuthorName"))
102
103 success, err = HaveField("Title()", "Les Miserables").Match(book)
104 Ω(success).Should(BeFalse())
105 Ω(err.Error()).Should(ContainSubstring("HaveField could not find method named '%s' in struct of type matchers_test.Book.", "Title()"))
106
107 success, err = HaveField("NoReturn()", "Les Miserables").Match(book)
108 Ω(success).Should(BeFalse())
109 Ω(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."))
110
111 success, err = HaveField("TooManyReturn()", "Les Miserables").Match(book)
112 Ω(success).Should(BeFalse())
113 Ω(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."))
114
115 success, err = HaveField("HasArg()", "Les Miserables").Match(book)
116 Ω(success).Should(BeFalse())
117 Ω(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."))
118
119 success, err = HaveField("Pages.Count", 2783).Match(book)
120 Ω(success).Should(BeFalse())
121 Ω(err.Error()).Should(Equal("HaveField encountered:\n <int>: 2783\nWhich is not a struct."))
122
123 success, err = HaveField("Author.Abbreviation", "Vic").Match(book)
124 Ω(success).Should(BeFalse())
125 Ω(err.Error()).Should(ContainSubstring("HaveField could not find field named '%s' in struct:", "Abbreviation"))
126
127 success, err = HaveField("Prequel.Title", "Les Miserables 0").Match(book)
128 Ω(success).Should(BeFalse())
129 Ω(err.Error()).Should(ContainSubstring("HaveField encountered nil while dereferencing a pointer of type *matchers_test.Book."))
130 })
131 })
132
133 Describe("Failure Messages", func() {
134 It("renders the underlying matcher failure", func() {
135 matcher := HaveField("Title", "Les Mis")
136 success, err := matcher.Match(book)
137 Ω(success).Should(BeFalse())
138 Ω(err).ShouldNot(HaveOccurred())
139
140 msg := matcher.FailureMessage(book)
141 Ω(msg).Should(Equal("Value for field 'Title' failed to satisfy matcher.\nExpected\n <string>: Les Miserables\nto equal\n <string>: Les Mis"))
142
143 matcher = HaveField("Title", "Les Miserables")
144 success, err = matcher.Match(book)
145 Ω(success).Should(BeTrue())
146 Ω(err).ShouldNot(HaveOccurred())
147
148 msg = matcher.NegatedFailureMessage(book)
149 Ω(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"))
150 })
151 })
152 })
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/internal/gutil"
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 = gutil.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 "net/http"
5 "net/http/httptest"
6 "strings"
7
8 . "github.com/onsi/ginkgo/v2"
9 . "github.com/onsi/gomega"
10 "github.com/onsi/gomega/internal/gutil"
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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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: gutil.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/v2"
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 })
33 "fmt"
44 "net/http"
55 "net/http/httptest"
6 "reflect"
7 "strings"
68
79 "github.com/onsi/gomega/format"
10 "github.com/onsi/gomega/internal/gutil"
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 := gutil.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 }
22 import (
33 "net/http"
44 "net/http/httptest"
5
6 . "github.com/onsi/ginkgo"
5 "strings"
6
7 . "github.com/onsi/ginkgo/v2"
78 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/internal/gutil"
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: gutil.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: gutil.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: gutil.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: gutil.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_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
22 import (
33 "errors"
44
5 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
66 . "github.com/onsi/gomega"
77 . "github.com/onsi/gomega/matchers"
88 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
0 package matchers
1
2 import (
3 "errors"
4 "reflect"
5
6 "github.com/onsi/gomega/format"
7 "github.com/onsi/gomega/types"
8 )
9
10 const maxIndirections = 31
11
12 type HaveValueMatcher struct {
13 Matcher types.GomegaMatcher // the matcher to apply to the "resolved" actual value.
14 resolvedActual interface{} // the ("resolved") value.
15 }
16
17 func (m *HaveValueMatcher) Match(actual interface{}) (bool, error) {
18 val := reflect.ValueOf(actual)
19 for allowedIndirs := maxIndirections; allowedIndirs > 0; allowedIndirs-- {
20 // return an error if value isn't valid. Please note that we cannot
21 // check for nil here, as we might not deal with a pointer or interface
22 // at this point.
23 if !val.IsValid() {
24 return false, errors.New(format.Message(
25 actual, "not to be <nil>"))
26 }
27 switch val.Kind() {
28 case reflect.Ptr, reflect.Interface:
29 // resolve pointers and interfaces to their values, then rinse and
30 // repeat.
31 if val.IsNil() {
32 return false, errors.New(format.Message(
33 actual, "not to be <nil>"))
34 }
35 val = val.Elem()
36 continue
37 default:
38 // forward the final value to the specified matcher.
39 m.resolvedActual = val.Interface()
40 return m.Matcher.Match(m.resolvedActual)
41 }
42 }
43 // too many indirections: extreme star gazing, indeed...?
44 return false, errors.New(format.Message(actual, "too many indirections"))
45 }
46
47 func (m *HaveValueMatcher) FailureMessage(_ interface{}) (message string) {
48 return m.Matcher.FailureMessage(m.resolvedActual)
49 }
50
51 func (m *HaveValueMatcher) NegatedFailureMessage(_ interface{}) (message string) {
52 return m.Matcher.NegatedFailureMessage(m.resolvedActual)
53 }
0 package matchers_test
1
2 import (
3 "reflect"
4
5 . "github.com/onsi/ginkgo/v2"
6 . "github.com/onsi/gomega"
7 )
8
9 type I interface {
10 M()
11 }
12
13 type S struct {
14 V int
15 }
16
17 func (s S) M() {}
18
19 var _ = Describe("HaveValue", func() {
20
21 It("should fail when passed nil", func() {
22 var p *struct{}
23 m := HaveValue(BeNil())
24 Expect(m.Match(p)).Error().To(MatchError(MatchRegexp("not to be <nil>$")))
25 })
26
27 It("should fail when passed nil indirectly", func() {
28 var p *struct{}
29 m := HaveValue(BeNil())
30 Expect(m.Match(&p)).Error().To(MatchError(MatchRegexp("not to be <nil>$")))
31 })
32
33 It("should use the matcher's failure message", func() {
34 m := HaveValue(Equal(42))
35 Expect(m.Match(666)).To(BeFalse())
36 Expect(m.FailureMessage(nil)).To(Equal("Expected\n <int>: 666\nto equal\n <int>: 42"))
37 Expect(m.NegatedFailureMessage(nil)).To(Equal("Expected\n <int>: 666\nnot to equal\n <int>: 42"))
38 })
39
40 It("should unwrap the value pointed to, even repeatedly", func() {
41 i := 1
42 Expect(&i).To(HaveValue(Equal(1)))
43 Expect(&i).NotTo(HaveValue(Equal(2)))
44
45 pi := &i
46 Expect(pi).To(HaveValue(Equal(1)))
47 Expect(pi).NotTo(HaveValue(Equal(2)))
48
49 Expect(&pi).To(HaveValue(Equal(1)))
50 Expect(&pi).NotTo(HaveValue(Equal(2)))
51 })
52
53 It("shouldn't endlessly star-gaze", func() {
54 dave := "It's full of stars!"
55 stargazer := reflect.ValueOf(dave)
56 for stars := 1; stars <= 31; stars++ {
57 p := reflect.New(stargazer.Type())
58 p.Elem().Set(stargazer)
59 stargazer = p
60 }
61 m := HaveValue(Equal(dave))
62 Expect(m.Match(stargazer.Interface())).Error().To(
63 MatchError(MatchRegexp(`too many indirections`)))
64 Expect(m.Match(stargazer.Elem().Interface())).To(BeTrue())
65 })
66
67 It("should unwrap the value of an interface", func() {
68 var i I = &S{V: 42}
69 Expect(i).To(HaveValue(Equal(S{V: 42})))
70 Expect(i).NotTo(HaveValue(Equal(S{})))
71 })
72
73 })
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) {
33 "errors"
44 "fmt"
55
6 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/ginkgo/v2"
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 })
22 import (
33 "encoding/json"
44
5 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
66 . "github.com/onsi/gomega"
77 . "github.com/onsi/gomega/matchers"
88 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55
66 . "github.com/onsi/gomega/matchers"
11
22 import (
33 "fmt"
4 "io/ioutil"
54 "os"
65 "testing"
76
8 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/v2"
98 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/internal/gutil"
1010 )
1111
1212 type myStringer struct {
3333
3434 func readFileContents(filePath string) []byte {
3535 f := openFile(filePath)
36 b, err := ioutil.ReadAll(f)
36 b, err := gutil.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 }
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
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 }
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
00 package matchers_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55 . "github.com/onsi/gomega/matchers"
66 )
22 import (
33 "time"
44
5 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
66 . "github.com/onsi/gomega"
77 . "github.com/onsi/gomega/matchers"
88 )
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/v2"
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 "errors"
44 "regexp"
55
6 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/ginkgo/v2"
77 . "github.com/onsi/gomega"
88 . "github.com/onsi/gomega/matchers"
99 )
00 package bipartitegraph_test
11
22 import (
3 . "github.com/onsi/ginkgo"
3 . "github.com/onsi/ginkgo/v2"
44 . "github.com/onsi/gomega"
55
66 "testing"
44
55 . "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph"
66
7 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/v2"
88 . "github.com/onsi/gomega"
99 )
1010
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 }
22 import (
33 "errors"
44
5 . "github.com/onsi/ginkgo"
5 . "github.com/onsi/ginkgo/v2"
66 . "github.com/onsi/gomega"
77 . "github.com/onsi/gomega/matchers"
88 )
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("Author.FirstName", Equal("Victor"))
364 // Expect(book).To(HaveField("Author.DOB.Year()", BeNumerically("<", 1900))
365 func HaveField(field string, expected interface{}) types.GomegaMatcher {
366 return &matchers.HaveFieldMatcher{
367 Field: field,
368 Expected: expected,
369 }
370 }
371
372 // HaveValue applies the given matcher to the value of actual, optionally and
373 // repeatedly dereferencing pointers or taking the concrete value of interfaces.
374 // Thus, the matcher will always be applied to non-pointer and non-interface
375 // values only. HaveValue will fail with an error if a pointer or interface is
376 // nil. It will also fail for more than 31 pointer or interface dereferences to
377 // guard against mistakenly applying it to arbitrarily deep linked pointers.
378 //
379 // HaveValue differs from gstruct.PointTo in that it does not expect actual to
380 // be a pointer (as gstruct.PointTo does) but instead also accepts non-pointer
381 // and even interface values.
382 //
383 // actual := 42
384 // Expect(actual).To(HaveValue(42))
385 // Expect(&actual).To(HaveValue(42))
386 func HaveValue(matcher types.GomegaMatcher) types.GomegaMatcher {
387 return &matchers.HaveValueMatcher{
388 Matcher: matcher,
341389 }
342390 }
343391
422470 //Expected must be either an int or a string.
423471 // Expect(resp).Should(HaveHTTPStatus(http.StatusOK)) // asserts that resp.StatusCode == 200
424472 // Expect(resp).Should(HaveHTTPStatus("404 Not Found")) // asserts that resp.Status == "404 Not Found"
425 func HaveHTTPStatus(expected interface{}) types.GomegaMatcher {
473 // Expect(resp).Should(HaveHTTPStatus(http.StatusOK, http.StatusNoContent)) // asserts that resp.StatusCode == 200 || resp.StatusCode == 204
474 func HaveHTTPStatus(expected ...interface{}) types.GomegaMatcher {
426475 return &matchers.HaveHTTPStatusMatcher{Expected: expected}
476 }
477
478 // HaveHTTPHeaderWithValue succeeds if the header is found and the value matches.
479 // Actual must be either a *http.Response or *httptest.ResponseRecorder.
480 // Expected must be a string header name, followed by a header value which
481 // can be a string, or another matcher.
482 func HaveHTTPHeaderWithValue(header string, value interface{}) types.GomegaMatcher {
483 return &matchers.HaveHTTPHeaderWithValueMatcher{
484 Header: header,
485 Value: value,
486 }
487 }
488
489 // HaveHTTPBody matches if the body matches.
490 // Actual must be either a *http.Response or *httptest.ResponseRecorder.
491 // Expected must be either a string, []byte, or other matcher
492 func HaveHTTPBody(expected interface{}) types.GomegaMatcher {
493 return &matchers.HaveHTTPBodyMatcher{Expected: expected}
427494 }
428495
429496 //And succeeds only if all of the given matchers succeed.
465532 }
466533
467534 //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.
535 //The given transform must be either a function of one parameter that returns one value or a
536 // function of one parameter that returns two values, where the second value must be of the
537 // error type.
469538 // var plus1 = func(i int) int { return i + 1 }
470539 // Expect(1).To(WithTransform(plus1, Equal(2))
540 //
541 // var failingplus1 = func(i int) (int, error) { return 42, "this does not compute" }
542 // Expect(1).To(WithTransform(failingplus1, Equal(2)))
471543 //
472544 //And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
473545 func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
474546 return matchers.NewWithTransformMatcher(transform, matcher)
475547 }
548
549 //Satisfy matches the actual value against the `predicate` function.
550 //The given predicate must be a function of one paramter that returns bool.
551 // var isEven = func(i int) bool { return i%2 == 0 }
552 // Expect(2).To(Satisfy(isEven))
553 func Satisfy(predicate interface{}) types.GomegaMatcher {
554 return matchers.NewSatisfyMatcher(predicate)
555 }
0 //go:build tools
1 // +build tools
2
3 package main
4
5 import (
6 _ "github.com/onsi/ginkgo/v2/ginkgo"
7 )
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 }