Codebase list golang-gomega / f513905
Import upstream version 1.13.0+git20210602.1.febd7a2 Debian Janitor 2 years ago
61 changed file(s) with 4725 addition(s) and 490 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
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.15', '1.16' ]
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: make test
00 language: go
1 arch:
2 - amd64
3 - ppc64le
14
25 go:
3 - 1.14.x
6 - gotip
7 - 1.16.x
48 - 1.15.x
5 - gotip
69
710 env:
811 - GO111MODULE=on
912
10 install:
11 - go get -v ./...
12 - go build ./...
13 - go get github.com/onsi/ginkgo
14 - go install github.com/onsi/ginkgo/ginkgo
13 install: skip
1514
16 script: make test
15 script:
16 - go mod tidy && git diff --exit-code go.mod go.sum
17 - make test
0 ## 1.13.0
1
2 ### Features
3 - gmeasure provides BETA support for benchmarking (#447) [8f2dfbf]
4 - Set consistently and eventually defaults on init (#443) [12eb778]
5
6 ## 1.12.0
7
8 ### Features
9 - Add Satisfy() matcher (#437) [c548f31]
10 - tweak truncation message [3360b8c]
11 - Add format.GomegaStringer (#427) [cc80b6f]
12 - Add Clear() method to gbytes.Buffer [c3c0920]
13
14 ### Fixes
15 - Fix error message in BeNumericallyMatcher (#432) [09c074a]
16 - Bump github.com/onsi/ginkgo from 1.12.1 to 1.16.2 (#442) [e5f6ea0]
17 - Bump github.com/golang/protobuf from 1.4.3 to 1.5.2 (#431) [adae3bf]
18 - Bump golang.org/x/net (#441) [3275b35]
19
20 ## 1.11.0
21
22 ### Features
23 - feature: add index to gstruct element func (#419) [334e00d]
24 - feat(gexec) Add CompileTest functions. Close #410 (#411) [47c613f]
25
26 ### Fixes
27 - Check more carefully for nils in WithTransform (#423) [3c60a15]
28 - fix: typo in Makefile [b82522a]
29 - Allow WithTransform function to accept a nil value (#422) [b75d2f2]
30 - fix: print value type for interface{} containers (#409) [f08e2dc]
31 - fix(BeElementOf): consistently flatten expected values [1fa9468]
32
33 ## 1.10.5
34
35 ### Fixes
36 - fix: collections matchers should display type of expectation (#408) [6b4eb5a]
37 - fix(ContainElements): consistently flatten expected values [073b880]
38 - fix(ConsistOf): consistently flatten expected values [7266efe]
39
40 ## 1.10.4
41
42 ### Fixes
43 - update golang net library to more recent version without vulnerability (#406) [817a8b9]
44 - Correct spelling: alloted -> allotted (#403) [0bae715]
45 - fix a panic in MessageWithDiff with long message (#402) [ea06b9b]
46
047 ## 1.10.3
148
249 ### Fixes
0 FROM golang:1.15
0 test:
1 [ -z "`gofmt -s -w -l -e .`" ]
2 go vet
3 ginkgo -p -r --randomizeAllSpecs --failOnPending --randomizeSuites --race
0 ###### Help ###################################################################
41
5 .PHONY: test
2 .DEFAULT_GOAL = help
3
4 .PHONY: help
5
6 help: ## list Makefile targets
7 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
8
9 ###### Targets ################################################################
10
11 test: version download fmt vet ginkgo ## Runs all build, static analysis, and test steps
12
13 download: ## Download dependencies
14 go mod download
15
16 vet: ## Run static code analysis
17 go vet ./...
18
19 ginkgo: ## Run tests using Ginkgo
20 go run github.com/onsi/ginkgo/ginkgo -p -r --randomizeAllSpecs --failOnPending --randomizeSuites --race
21
22 fmt: ## Checks that the code is formatted correcty
23 @@if [ -n "$$(gofmt -s -e -l -d .)" ]; then \
24 echo "gofmt check failed: run 'gofmt -s -e -l -w .'"; \
25 exit 1; \
26 fi
27
28 docker_test: ## Run tests in a container via docker-compose
29 docker-compose build test && docker-compose run --rm test make test
30
31 version: ## Display the version of Go
32 @@go version
00 ![Gomega: Ginkgo's Preferred Matcher Library](http://onsi.github.io/gomega/images/gomega.png)
11
2 [![Build Status](https://travis-ci.org/onsi/gomega.svg?branch=master)](https://travis-ci.org/onsi/gomega)
2 [![test](https://github.com/onsi/gomega/actions/workflows/test.yml/badge.svg)](https://github.com/onsi/gomega/actions/workflows/test.yml)
33
44 Jump straight to the [docs](http://onsi.github.io/gomega/) to learn about Gomega, including a list of [all available matchers](http://onsi.github.io/gomega/#provided-matchers).
55
0 version: '3.0'
1
2 services:
3 test:
4 build:
5 dockerfile: Dockerfile
6 context: .
7 working_dir: /app
8 volumes:
9 - ${PWD}:/app
0 package gomega
1
2 import (
3 "os"
4
5 "github.com/onsi/gomega/internal/defaults"
6 )
7
8 const (
9 ConsistentlyDurationEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_DURATION"
10 ConsistentlyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_CONSISTENTLY_POLLING_INTERVAL"
11 EventuallyTimeoutEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_TIMEOUT"
12 EventuallyPollingIntervalEnvVarName = "GOMEGA_DEFAULT_EVENTUALLY_POLLING_INTERVAL"
13 )
14
15 func init() {
16 defaults.SetDurationFromEnv(
17 os.Getenv,
18 SetDefaultConsistentlyDuration,
19 ConsistentlyDurationEnvVarName,
20 )
21
22 defaults.SetDurationFromEnv(
23 os.Getenv,
24 SetDefaultConsistentlyPollingInterval,
25 ConsistentlyPollingIntervalEnvVarName,
26 )
27
28 defaults.SetDurationFromEnv(
29 os.Getenv,
30 SetDefaultEventuallyTimeout,
31 EventuallyTimeoutEnvVarName,
32 )
33
34 defaults.SetDurationFromEnv(
35 os.Getenv,
36 SetDefaultEventuallyPollingInterval,
37 EventuallyPollingIntervalEnvVarName,
38 )
39 }
66 package format
77
88 import (
9 "context"
910 "fmt"
1011 "reflect"
1112 "strconv"
1617 // Use MaxDepth to set the maximum recursion depth when printing deeply nested objects
1718 var MaxDepth = uint(10)
1819
20 // MaxLength of the string representation of an object.
21 // If MaxLength is set to 0, the Object will not be truncated.
22 var MaxLength = 4000
23
1924 /*
2025 By default, all objects (even those that implement fmt.Stringer and fmt.GoStringer) are recursively inspected to generate output.
2126
4348 // after the first diff location in a truncated string assertion error message.
4449 var CharactersAroundMismatchToInclude uint = 5
4550
46 // Ctx interface defined here to keep backwards compatibility with go < 1.7
47 // It matches the context.Context interface
48 type Ctx interface {
49 Deadline() (deadline time.Time, ok bool)
50 Done() <-chan struct{}
51 Err() error
52 Value(key interface{}) interface{}
53 }
54
55 var contextType = reflect.TypeOf((*Ctx)(nil)).Elem()
51 var contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
5652 var timeType = reflect.TypeOf(time.Time{})
5753
5854 //The default indentation string emitted by the format package
5955 var Indent = " "
6056
6157 var longFormThreshold = 20
58
59 // GomegaStringer allows for custom formating of objects for gomega.
60 type GomegaStringer interface {
61 // GomegaString will be used to custom format an object.
62 // It does not follow UseStringerRepresentation value and will always be called regardless.
63 // It also ignores the MaxLength value.
64 GomegaString() string
65 }
6266
6367 /*
6468 Generates a formatted matcher success/failure message of the form:
104108
105109 tabLength := 4
106110 spaceFromMessageToActual := tabLength + len("<string>: ") - len(message)
107 padding := strings.Repeat(" ", spaceFromMessageToActual+spacesBeforeFormattedMismatch) + "|"
111
112 paddingCount := spaceFromMessageToActual + spacesBeforeFormattedMismatch
113 if paddingCount < 0 {
114 return Message(formattedActual, message, formattedExpected)
115 }
116
117 padding := strings.Repeat(" ", paddingCount) + "|"
108118 return Message(formattedActual, message+padding, formattedExpected)
109119 }
110120
160170 return 0
161171 }
162172
173 const truncateHelpText = `
174 Gomega truncated this representation as it exceeds 'format.MaxLength'.
175 Consider having the object provide a custom 'GomegaStringer' representation
176 or adjust the parameters in Gomega's 'format' package.
177
178 Learn more here: https://onsi.github.io/gomega/#adjusting-output
179 `
180
181 func truncateLongStrings(s string) string {
182 if MaxLength > 0 && len(s) > MaxLength {
183 var sb strings.Builder
184 for i, r := range s {
185 if i < MaxLength {
186 sb.WriteRune(r)
187 continue
188 }
189 break
190 }
191
192 sb.WriteString("...\n")
193 sb.WriteString(truncateHelpText)
194
195 return sb.String()
196 }
197 return s
198 }
199
163200 /*
164201 Pretty prints the passed in object at the passed in indentation level.
165202
174211 func Object(object interface{}, indentation uint) string {
175212 indent := strings.Repeat(Indent, int(indentation))
176213 value := reflect.ValueOf(object)
177 return fmt.Sprintf("%s<%s>: %s", indent, formatType(object), formatValue(value, indentation))
214 return fmt.Sprintf("%s<%s>: %s", indent, formatType(value), formatValue(value, indentation))
178215 }
179216
180217 /*
194231 return result
195232 }
196233
197 func formatType(object interface{}) string {
198 t := reflect.TypeOf(object)
199 if t == nil {
234 func formatType(v reflect.Value) string {
235 switch v.Kind() {
236 case reflect.Invalid:
200237 return "nil"
201 }
202 switch t.Kind() {
203238 case reflect.Chan:
204 v := reflect.ValueOf(object)
205 return fmt.Sprintf("%T | len:%d, cap:%d", object, v.Len(), v.Cap())
239 return fmt.Sprintf("%s | len:%d, cap:%d", v.Type(), v.Len(), v.Cap())
206240 case reflect.Ptr:
207 return fmt.Sprintf("%T | %p", object, object)
241 return fmt.Sprintf("%s | 0x%x", v.Type(), v.Pointer())
208242 case reflect.Slice:
209 v := reflect.ValueOf(object)
210 return fmt.Sprintf("%T | len:%d, cap:%d", object, v.Len(), v.Cap())
243 return fmt.Sprintf("%s | len:%d, cap:%d", v.Type(), v.Len(), v.Cap())
211244 case reflect.Map:
212 v := reflect.ValueOf(object)
213 return fmt.Sprintf("%T | len:%d", object, v.Len())
245 return fmt.Sprintf("%s | len:%d", v.Type(), v.Len())
214246 default:
215 return fmt.Sprintf("%T", object)
247 return fmt.Sprintf("%s", v.Type())
216248 }
217249 }
218250
225257 return "nil"
226258 }
227259
228 if UseStringerRepresentation {
229 if value.CanInterface() {
230 obj := value.Interface()
260 if value.CanInterface() {
261 obj := value.Interface()
262
263 // GomegaStringer will take precedence to other representations and disregards UseStringerRepresentation
264 if x, ok := obj.(GomegaStringer); ok {
265 // do not truncate a user-defined GoMegaString() value
266 return x.GomegaString()
267 }
268
269 if UseStringerRepresentation {
231270 switch x := obj.(type) {
232271 case fmt.GoStringer:
233 return x.GoString()
272 return truncateLongStrings(x.GoString())
234273 case fmt.Stringer:
235 return x.String()
274 return truncateLongStrings(x.String())
236275 }
237276 }
238277 }
263302 case reflect.Ptr:
264303 return formatValue(value.Elem(), indentation)
265304 case reflect.Slice:
266 return formatSlice(value, indentation)
305 return truncateLongStrings(formatSlice(value, indentation))
267306 case reflect.String:
268 return formatString(value.String(), indentation)
307 return truncateLongStrings(formatString(value.String(), indentation))
269308 case reflect.Array:
270 return formatSlice(value, indentation)
309 return truncateLongStrings(formatSlice(value, indentation))
271310 case reflect.Map:
272 return formatMap(value, indentation)
311 return truncateLongStrings(formatMap(value, indentation))
273312 case reflect.Struct:
274313 if value.Type() == timeType && value.CanInterface() {
275314 t, _ := value.Interface().(time.Time)
276315 return t.Format(time.RFC3339Nano)
277316 }
278 return formatStruct(value, indentation)
317 return truncateLongStrings(formatStruct(value, indentation))
279318 case reflect.Interface:
280 return formatValue(value.Elem(), indentation)
319 return formatInterface(value, indentation)
281320 default:
282321 if value.CanInterface() {
283 return fmt.Sprintf("%#v", value.Interface())
284 }
285 return fmt.Sprintf("%#v", value)
322 return truncateLongStrings(fmt.Sprintf("%#v", value.Interface()))
323 }
324 return truncateLongStrings(fmt.Sprintf("%#v", value))
286325 }
287326 }
288327
372411 return fmt.Sprintf("{%s}", strings.Join(result, ", "))
373412 }
374413
414 func formatInterface(v reflect.Value, indentation uint) string {
415 return fmt.Sprintf("<%s>%s", formatType(v.Elem()), formatValue(v.Elem(), indentation))
416 }
417
375418 func isNilValue(a reflect.Value) bool {
376419 switch a.Kind() {
377420 case reflect.Invalid:
00 package format_test
11
22 import (
3 "context"
34 "fmt"
45 "strings"
56 "time"
7273 return "string"
7374 }
7475
75 type ctx struct {
76 }
77
78 func (c *ctx) Deadline() (deadline time.Time, ok bool) {
79 return time.Time{}, false
80 }
81
82 func (c *ctx) Done() <-chan struct{} {
83 return nil
84 }
85
86 func (c *ctx) Err() error {
87 return nil
88 }
89
90 func (c *ctx) Value(key interface{}) interface{} {
91 return nil
76 type gomegaStringer struct {
77 }
78
79 func (g gomegaStringer) GomegaString() string {
80 return "gomegastring"
81 }
82
83 type gomegaStringerLong struct {
84 }
85
86 func (g gomegaStringerLong) GomegaString() string {
87 return strings.Repeat("s", MaxLength*2)
9288 }
9389
9490 var _ = Describe("Format", func() {
112108 for i := range arr {
113109 arr[i] = entriesSwitch
114110 }
115 return "{" + strings.Join(arr, ", ") + "}"
111 return "{\\s*" + strings.Join(arr, ",\\s* ") + ",?\\s*}"
116112 }
117113
118114 Describe("Message", func() {
119115 Context("with only an actual value", func() {
116 BeforeEach(func() {
117 MaxLength = 4000
118 })
119
120120 It("should print out an indented formatted representation of the value and the message", func() {
121121 Expect(Message(3, "to be three.")).Should(Equal("Expected\n <int>: 3\nto be three."))
122 })
123
124 It("should print out an indented formatted representation of the value and the message, and trucate it when too long", func() {
125 tooLong := strings.Repeat("s", MaxLength+1)
126 tooLongResult := strings.Repeat("s", MaxLength) + "...\n" + TruncatedHelpText()
127 Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLongResult + "\nto be truncated"))
128 })
129
130 It("should print out an indented formatted representation of the value and the message, and not trucate it when MaxLength = 0", func() {
131 MaxLength = 0
132 tooLong := strings.Repeat("s", MaxLength+1)
133 Expect(Message(tooLong, "to be truncated")).Should(Equal("Expected\n <string>: " + tooLong + "\nto be truncated"))
122134 })
123135 })
124136
171183 stringB := "something_else"
172184
173185 Expect(MessageWithDiff(stringA, "to equal", stringB)).Should(Equal(expectedSpecialCharacterFailureMessage))
186 })
187
188 It("handles negative padding length", func() {
189 stringWithB := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
190 stringWithZ := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
191 longMessage := "to equal very long message"
192
193 Expect(MessageWithDiff(stringWithB, longMessage, stringWithZ)).Should(Equal(expectedDiffLongMessage))
174194 })
175195
176196 Context("With truncated diff disabled", func() {
535555 })
536556 })
537557
558 Describe("formatting nested interface{} types", func() {
559 It("should print out the types of the container and value", func() {
560 Expect(Object([]interface{}{"foo"}, 1)).
561 To(match("[]interface {} | len:1, cap:1", `[<string>"foo"]`))
562
563 Expect(Object(map[string]interface{}{"foo": true}, 1)).
564 To(match("map[string]interface {} | len:1", `{"foo": <bool>true}`))
565
566 Expect(Object(struct{ A interface{} }{A: 1}, 1)).
567 To(match("struct { A interface {} }", "{A: <int>1}"))
568
569 v := struct{ A interface{} }{A: struct{ B string }{B: "foo"}}
570 Expect(Object(v, 1)).To(match(`struct { A interface {} }`, `{
571 A: <struct { B string }>{B: "foo"},
572 }`))
573 })
574 })
575
538576 Describe("formatting times", func() {
539577 It("should format time as RFC3339", func() {
540578 t := time.Date(2016, 10, 31, 9, 57, 23, 12345, time.UTC)
583621 byteArrValue: \[17, 20, 32\],
584622 mapValue: %s,
585623 structValue: {Exported: "exported"},
586 interfaceValue: {"a key": 17},
624 interfaceValue: <map\[string\]int \| len:1>{"a key": 17},
587625 }`, s.chanValue, s.funcValue, hashMatchingRegexp(`"a key": 20`, `"b key": 30`))
588626
589627 Expect(Object(s, 1)).Should(matchRegexp(`format_test\.SecretiveStruct`, expected))
599637 outerHash["integer"] = 2
600638 outerHash["map"] = innerHash
601639
602 expected := hashMatchingRegexp(`"integer": 2`, `"map": {"inner": 3}`)
640 expected := hashMatchingRegexp(`"integer": <int>2`, `"map": <map\[string\]int \| len:1>{"inner": 3}`)
603641 Expect(Object(outerHash, 1)).Should(matchRegexp(`map\[string\]interface {} \| len:2`, expected))
604642 })
605643 })
645683 Expect(Object(Stringer{}, 1)).Should(ContainSubstring("<format_test.Stringer>: string"))
646684 })
647685 })
686
687 When("passed a GomegaStringer", func() {
688 It("should use what GomegaString() returns", func() {
689 Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
690 UseStringerRepresentation = false
691 Expect(Object(gomegaStringer{}, 1)).Should(ContainSubstring("<format_test.gomegaStringer>: gomegastring"))
692 })
693
694 It("should use what GomegaString() returns, disregarding MaxLength", func() {
695 Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
696 UseStringerRepresentation = false
697 Expect(Object(gomegaStringerLong{}, 1)).Should(Equal(" <format_test.gomegaStringerLong>: " + strings.Repeat("s", MaxLength*2)))
698 })
699 })
648700 })
649701
650702 Describe("Printing a context.Context field", func() {
651703
652704 type structWithContext struct {
653 Context Ctx
705 Context context.Context
654706 Value string
655707 }
656708
657 context := ctx{}
658 objWithContext := structWithContext{Value: "some-value", Context: &context}
709 objWithContext := structWithContext{Value: "some-value", Context: context.TODO()}
659710
660711 It("Suppresses the content by default", func() {
661712 Expect(Object(objWithContext, 1)).Should(ContainSubstring("<suppressed context>"))
662713 })
663714
664 It("Doesn't supress the context if it's the object being printed", func() {
665 Expect(Object(context, 1)).ShouldNot(MatchRegexp("^.*<suppressed context>$"))
715 It("Doesn't suppress the context if it's the object being printed", func() {
716 Expect(Object(context.TODO(), 1)).ShouldNot(MatchRegexp("^.*<suppressed context>$"))
666717 })
667718
668719 Context("PrintContextObjects is set", func() {
760811 to equal |
761812 <string>: "...z..."
762813 `)
814
815 var expectedDiffLongMessage = strings.TrimSpace(`
816 Expected
817 <string>: "...aaaaabaaaaa..."
818 to equal very long message
819 <string>: "...aaaaazaaaaa..."
820 `)
0 /*
1 Gomega's format test helper package.
2 */
3
4 package format
5
6 // TruncateHelpText returns truncateHelpText.
7 // This function is only accessible during tests.
8 func TruncatedHelpText() string {
9 return truncateHelpText
10 }
105105 b.readCursor += uint64(n)
106106
107107 return n, nil
108 }
109
110 /*
111 Clear clears out the buffer's contents
112 */
113 func (b *Buffer) Clear() error {
114 b.lock.Lock()
115 defer b.lock.Unlock()
116
117 if b.closed {
118 return errors.New("attempt to clear closed buffer")
119 }
120
121 b.contents = []byte{}
122 b.readCursor = 0
123 return nil
108124 }
109125
110126 /*
180180 })
181181 })
182182
183 Describe("clearing the buffer", func() {
184 It("should clear out the contents of the buffer", func() {
185 buffer.Write([]byte("abc"))
186 Expect(buffer).To(Say("ab"))
187 Expect(buffer.Clear()).To(Succeed())
188 Expect(buffer).NotTo(Say("c"))
189 Expect(buffer.Contents()).To(BeEmpty())
190 buffer.Write([]byte("123"))
191 Expect(buffer).To(Say("123"))
192 Expect(buffer.Contents()).To(Equal([]byte("123")))
193 })
194
195 It("should error when the buffer is closed", func() {
196 buffer.Write([]byte("abc"))
197 buffer.Close()
198 err := buffer.Clear()
199 Expect(err).To(HaveOccurred())
200 })
201 })
202
183203 Describe("closing the buffer", func() {
184204 It("should error when further write attempts are made", func() {
185205 _, err := buffer.Write([]byte("abc"))
88 // ErrTimeout is returned by TimeoutCloser, TimeoutReader, and TimeoutWriter when the underlying Closer/Reader/Writer does not return within the specified timeout
99 var ErrTimeout = errors.New("timeout occurred")
1010
11 // TimeoutCloser returns an io.Closer that wraps the passed-in io.Closer. If the underlying Closer fails to close within the alloted timeout ErrTimeout is returned.
11 // TimeoutCloser returns an io.Closer that wraps the passed-in io.Closer. If the underlying Closer fails to close within the allotted timeout ErrTimeout is returned.
1212 func TimeoutCloser(c io.Closer, timeout time.Duration) io.Closer {
1313 return timeoutReaderWriterCloser{c: c, d: timeout}
1414 }
1515
16 // TimeoutReader returns an io.Reader that wraps the passed-in io.Reader. If the underlying Reader fails to read within the alloted timeout ErrTimeout is returned.
16 // TimeoutReader returns an io.Reader that wraps the passed-in io.Reader. If the underlying Reader fails to read within the allotted timeout ErrTimeout is returned.
1717 func TimeoutReader(r io.Reader, timeout time.Duration) io.Reader {
1818 return timeoutReaderWriterCloser{r: r, d: timeout}
1919 }
2020
21 // TimeoutWriter returns an io.Writer that wraps the passed-in io.Writer. If the underlying Writer fails to write within the alloted timeout ErrTimeout is returned.
21 // TimeoutWriter returns an io.Writer that wraps the passed-in io.Writer. If the underlying Writer fails to write within the allotted timeout ErrTimeout is returned.
2222 func TimeoutWriter(w io.Writer, timeout time.Duration) io.Writer {
2323 return timeoutReaderWriterCloser{w: w, d: timeout}
2424 }
0 package main_test
1
2 import "testing"
3
4 func Test(t *testing.T) {
5 t.Log("Hum, it seems okay.")
6 }
4545 return doBuild(gopath, packagePath, nil, args...)
4646 }
4747
48 func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
49 executable, err := newExecutablePath(gopath, packagePath)
50 if err != nil {
51 return "", err
52 }
53
54 cmdArgs := append([]string{"build"}, args...)
55 cmdArgs = append(cmdArgs, "-o", executable, packagePath)
56
57 build := exec.Command("go", cmdArgs...)
58 build.Env = replaceGoPath(os.Environ(), gopath)
59 build.Env = append(build.Env, env...)
60
61 output, err := build.CombinedOutput()
62 if err != nil {
63 return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
64 }
65
66 return executable, nil
67 }
68
69 /*
70 CompileTest uses go test to compile the test package at packagePath. The resulting binary is saved off in a temporary directory.
71 A path pointing to this binary is returned.
72
73 CompileTest uses the $GOPATH set in your environment. If $GOPATH is not set and you are using Go 1.8+,
74 it will use the default GOPATH instead. It passes the variadic args on to `go test`.
75 */
76 func CompileTest(packagePath string, args ...string) (compiledPath string, err error) {
77 return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
78 }
79
80 /*
81 GetAndCompileTest is identical to CompileTest but `go get` the package before compiling tests.
82 */
83 func GetAndCompileTest(packagePath string, args ...string) (compiledPath string, err error) {
84 if err := getForTest(build.Default.GOPATH, packagePath, nil); err != nil {
85 return "", err
86 }
87
88 return doCompileTest(build.Default.GOPATH, packagePath, nil, args...)
89 }
90
91 /*
92 CompileTestWithEnvironment is identical to CompileTest but allows you to specify env vars to be set at build time.
93 */
94 func CompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
95 return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
96 }
97
98 /*
99 GetAndCompileTestWithEnvironment is identical to GetAndCompileTest but allows you to specify env vars to be set at build time.
100 */
101 func GetAndCompileTestWithEnvironment(packagePath string, env []string, args ...string) (compiledPath string, err error) {
102 if err := getForTest(build.Default.GOPATH, packagePath, env); err != nil {
103 return "", err
104 }
105
106 return doCompileTest(build.Default.GOPATH, packagePath, env, args...)
107 }
108
109 /*
110 CompileTestIn is identical to CompileTest but allows you to specify a custom $GOPATH (the first argument).
111 */
112 func CompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
113 return doCompileTest(gopath, packagePath, nil, args...)
114 }
115
116 /*
117 GetAndCompileTestIn is identical to GetAndCompileTest but allows you to specify a custom $GOPATH (the first argument).
118 */
119 func GetAndCompileTestIn(gopath string, packagePath string, args ...string) (compiledPath string, err error) {
120 if err := getForTest(gopath, packagePath, nil); err != nil {
121 return "", err
122 }
123
124 return doCompileTest(gopath, packagePath, nil, args...)
125 }
126
127 func isLocalPackage(packagePath string) bool {
128 return strings.HasPrefix(packagePath, ".")
129 }
130
131 func getForTest(gopath, packagePath string, env []string) error {
132 if isLocalPackage(packagePath) {
133 return nil
134 }
135
136 return doGet(gopath, packagePath, env, "-t")
137 }
138
139 func doGet(gopath, packagePath string, env []string, args ...string) error {
140 args = append(args, packagePath)
141 args = append([]string{"get"}, args...)
142
143 goGet := exec.Command("go", args...)
144 goGet.Dir = gopath
145 goGet.Env = replaceGoPath(os.Environ(), gopath)
146 goGet.Env = append(goGet.Env, env...)
147
148 output, err := goGet.CombinedOutput()
149 if err != nil {
150 return fmt.Errorf("Failed to get %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
151 }
152
153 return nil
154 }
155
156 func doCompileTest(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
157 executable, err := newExecutablePath(gopath, packagePath, ".test")
158 if err != nil {
159 return "", err
160 }
161
162 cmdArgs := append([]string{"test", "-c"}, args...)
163 cmdArgs = append(cmdArgs, "-o", executable, packagePath)
164
165 build := exec.Command("go", cmdArgs...)
166 build.Env = replaceGoPath(os.Environ(), gopath)
167 build.Env = append(build.Env, env...)
168
169 output, err := build.CombinedOutput()
170 if err != nil {
171 return "", fmt.Errorf("Failed to build %s:\n\nError:\n%s\n\nOutput:\n%s", packagePath, err, string(output))
172 }
173
174 return executable, nil
175 }
176
48177 func replaceGoPath(environ []string, newGoPath string) []string {
49178 newEnviron := []string{}
50179 for _, v := range environ {
55184 return append(newEnviron, "GOPATH="+newGoPath)
56185 }
57186
58 func doBuild(gopath, packagePath string, env []string, args ...string) (compiledPath string, err error) {
187 func newExecutablePath(gopath, packagePath string, suffixes ...string) (string, error) {
59188 tmpDir, err := temporaryDirectory()
60189 if err != nil {
61190 return "", err
66195 }
67196
68197 executable := filepath.Join(tmpDir, path.Base(packagePath))
198
69199 if runtime.GOOS == "windows" {
70200 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))
83201 }
84202
85203 return executable, nil
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 })
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() {
100126 _, err := gexec.BuildIn(gopath, target)
127 Expect(err).NotTo(HaveOccurred())
128 Expect(os.Getenv("GOPATH")).To(Equal(filepath.Join(os.TempDir(), "emptyFakeGopath")))
129 })
130 })
131
132 var _ = Describe(".CompileTest", func() {
133 Context("a remote package", func() {
134 const remotePackage = "github.com/onsi/ginkgo/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/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 = ioutil.TempDir("", "")
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/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)
101273 Expect(err).NotTo(HaveOccurred())
102274 Expect(os.Getenv("GOPATH")).To(Equal(filepath.Join(os.TempDir(), "emptyFakeGopath")))
103275 })
2020 var session *Session
2121
2222 BeforeEach(func() {
23 var err error
23 fireflyPath, err := Build("./_fixture/firefly")
24 Expect(err).ShouldNot(HaveOccurred())
25
2426 command = exec.Command(fireflyPath, "0")
2527 session, err = Start(command, nil, nil)
2628 Expect(err).ShouldNot(HaveOccurred())
77 "testing"
88 )
99
10 var fireflyPath string
11
1210 func TestGexec(t *testing.T) {
13 BeforeSuite(func() {
14 var err error
15 fireflyPath, err = gexec.Build("./_fixture/firefly")
16 Expect(err).ShouldNot(HaveOccurred())
17 })
18
1911 AfterSuite(func() {
2012 gexec.CleanupBuildArtifacts()
2113 })
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(done Done) {
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 }
61
62 close(done)
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 = ioutil.Discard
329 errWriter = ioutil.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 = ioutil.Discard
390 errWriter = ioutil.Discard
391 })
392
393 It("executes succesfuly", func() {
394 Eventually(session).Should(Exit())
395 })
327396 })
328397 })
329398 })
0 package gmeasure
1
2 import (
3 "crypto/md5"
4 "encoding/json"
5 "fmt"
6 "io/ioutil"
7 "os"
8 "path/filepath"
9 )
10
11 const CACHE_EXT = ".gmeasure-cache"
12
13 /*
14 ExperimentCache provides a director-and-file based cache of experiments
15 */
16 type ExperimentCache struct {
17 Path string
18 }
19
20 /*
21 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).
22
23 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.
24 */
25 func NewExperimentCache(path string) (ExperimentCache, error) {
26 stat, err := os.Stat(path)
27 if os.IsNotExist(err) {
28 err := os.MkdirAll(path, 0777)
29 if err != nil {
30 return ExperimentCache{}, err
31 }
32 } else if !stat.IsDir() {
33 return ExperimentCache{}, fmt.Errorf("%s is not a directory", path)
34 }
35
36 return ExperimentCache{
37 Path: path,
38 }, nil
39 }
40
41 /*
42 CachedExperimentHeader captures the name of the Cached Experiment and its Version
43 */
44 type CachedExperimentHeader struct {
45 Name string
46 Version int
47 }
48
49 func (cache ExperimentCache) hashOf(name string) string {
50 return fmt.Sprintf("%x", md5.Sum([]byte(name)))
51 }
52
53 func (cache ExperimentCache) readHeader(filename string) (CachedExperimentHeader, error) {
54 out := CachedExperimentHeader{}
55 f, err := os.Open(filepath.Join(cache.Path, filename))
56 if err != nil {
57 return out, err
58 }
59 defer f.Close()
60 err = json.NewDecoder(f).Decode(&out)
61 return out, err
62 }
63
64 /*
65 List returns a list of all Cached Experiments found in the cache.
66 */
67 func (cache ExperimentCache) List() ([]CachedExperimentHeader, error) {
68 out := []CachedExperimentHeader{}
69 infos, err := ioutil.ReadDir(cache.Path)
70 if err != nil {
71 return out, err
72 }
73 for _, info := range infos {
74 if filepath.Ext(info.Name()) != CACHE_EXT {
75 continue
76 }
77 header, err := cache.readHeader(info.Name())
78 if err != nil {
79 return out, err
80 }
81 out = append(out, header)
82 }
83 return out, nil
84 }
85
86 /*
87 Clear empties out the cache - this will delete any and all detected cache files in the cache directory. Use with caution!
88 */
89 func (cache ExperimentCache) Clear() error {
90 infos, err := ioutil.ReadDir(cache.Path)
91 if err != nil {
92 return err
93 }
94 for _, info := range infos {
95 if filepath.Ext(info.Name()) != CACHE_EXT {
96 continue
97 }
98 err := os.Remove(filepath.Join(cache.Path, info.Name()))
99 if err != nil {
100 return err
101 }
102 }
103 return nil
104 }
105
106 /*
107 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.
108
109 If an experiment with corresponding name and version >= the passed-in version is found, it is unmarshaled and returned.
110
111 If no experiment is found, or the cached version is smaller than the passed-in version, Load will return nil.
112
113 When paired with Ginkgo you can cache experiments and prevent potentially expensive recomputation with this pattern:
114
115 const EXPERIMENT_VERSION = 1 //bump this to bust the cache and recompute _all_ experiments
116
117 Describe("some experiments", func() {
118 var cache gmeasure.ExperimentCache
119 var experiment *gmeasure.Experiment
120
121 BeforeEach(func() {
122 cache = gmeasure.NewExperimentCache("./gmeasure-cache")
123 name := CurrentSpecReport().LeafNodeText
124 experiment = cache.Load(name, EXPERIMENT_VERSION)
125 if experiment != nil {
126 AddReportEntry(experiment)
127 Skip("cached")
128 }
129 experiment = gmeasure.NewExperiment(name)
130 AddReportEntry(experiment)
131 })
132
133 It("foo runtime", func() {
134 experiment.SampleDuration("runtime", func() {
135 //do stuff
136 }, gmeasure.SamplingConfig{N:100})
137 })
138
139 It("bar runtime", func() {
140 experiment.SampleDuration("runtime", func() {
141 //do stuff
142 }, gmeasure.SamplingConfig{N:100})
143 })
144
145 AfterEach(func() {
146 if !CurrentSpecReport().State.Is(types.SpecStateSkipped) {
147 cache.Save(experiment.Name, EXPERIMENT_VERSION, experiment)
148 }
149 })
150 })
151 */
152 func (cache ExperimentCache) Load(name string, version int) *Experiment {
153 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
154 f, err := os.Open(path)
155 if err != nil {
156 return nil
157 }
158 defer f.Close()
159 dec := json.NewDecoder(f)
160 header := CachedExperimentHeader{}
161 dec.Decode(&header)
162 if header.Version < version {
163 return nil
164 }
165 out := NewExperiment("")
166 err = dec.Decode(out)
167 if err != nil {
168 return nil
169 }
170 return out
171 }
172
173 /*
174 Save stores the passed-in experiment to the cache with the passed-in name and version.
175 */
176 func (cache ExperimentCache) Save(name string, version int, experiment *Experiment) error {
177 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
178 f, err := os.Create(path)
179 if err != nil {
180 return err
181 }
182 defer f.Close()
183 enc := json.NewEncoder(f)
184 err = enc.Encode(CachedExperimentHeader{
185 Name: name,
186 Version: version,
187 })
188 if err != nil {
189 return err
190 }
191 return enc.Encode(experiment)
192 }
193
194 /*
195 Delete removes the experiment with the passed-in name from the cache
196 */
197 func (cache ExperimentCache) Delete(name string) error {
198 path := filepath.Join(cache.Path, cache.hashOf(name)+CACHE_EXT)
199 return os.Remove(path)
200 }
0 package gmeasure_test
1
2 import (
3 "fmt"
4 "os"
5
6 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/gomega"
8 "github.com/onsi/gomega/gmeasure"
9 )
10
11 var _ = Describe("Cache", func() {
12 var path string
13 var cache gmeasure.ExperimentCache
14 var e1, e2 *gmeasure.Experiment
15
16 BeforeEach(func() {
17 var err error
18 path = fmt.Sprintf("./cache-%d", GinkgoParallelNode())
19 cache, err = gmeasure.NewExperimentCache(path)
20 Ί(err).ShouldNot(HaveOccurred())
21 e1 = gmeasure.NewExperiment("Experiment-1")
22 e1.RecordValue("foo", 32)
23 e2 = gmeasure.NewExperiment("Experiment-2")
24 e2.RecordValue("bar", 64)
25 })
26
27 AfterEach(func() {
28 Ί(os.RemoveAll(path)).Should(Succeed())
29 })
30
31 Describe("when creating a cache that points to a file", func() {
32 It("errors", func() {
33 f, err := os.Create("cache-temp-file")
34 Ί(err).ShouldNot(HaveOccurred())
35 f.Close()
36 cache, err := gmeasure.NewExperimentCache("cache-temp-file")
37 Ί(err).Should(MatchError("cache-temp-file is not a directory"))
38 Ί(cache).Should(BeZero())
39 Ί(os.RemoveAll("cache-temp-file")).Should(Succeed())
40 })
41 })
42
43 Describe("the happy path", func() {
44 It("can save, load, list, delete, and clear the cache", func() {
45 Ί(cache.Save("e1", 1, e1)).Should(Succeed())
46 Ί(cache.Save("e2", 7, e2)).Should(Succeed())
47
48 Ί(cache.Load("e1", 1)).Should(Equal(e1))
49 Ί(cache.Load("e2", 7)).Should(Equal(e2))
50
51 Ί(cache.List()).Should(ConsistOf(
52 gmeasure.CachedExperimentHeader{"e1", 1},
53 gmeasure.CachedExperimentHeader{"e2", 7},
54 ))
55
56 Ί(cache.Delete("e2")).Should(Succeed())
57 Ί(cache.Load("e1", 1)).Should(Equal(e1))
58 Ί(cache.Load("e2", 7)).Should(BeNil())
59 Ί(cache.List()).Should(ConsistOf(
60 gmeasure.CachedExperimentHeader{"e1", 1},
61 ))
62
63 Ί(cache.Clear()).Should(Succeed())
64 Ί(cache.List()).Should(BeEmpty())
65 Ί(cache.Load("e1", 1)).Should(BeNil())
66 Ί(cache.Load("e2", 7)).Should(BeNil())
67 })
68 })
69
70 Context("with an empty cache", func() {
71 It("should list nothing", func() {
72 Ί(cache.List()).Should(BeEmpty())
73 })
74
75 It("should not error when clearing", func() {
76 Ί(cache.Clear()).Should(Succeed())
77 })
78
79 It("returs nil when loading a non-existing experiment", func() {
80 Ί(cache.Load("floop", 17)).Should(BeNil())
81 })
82 })
83
84 Describe("version management", func() {
85 BeforeEach(func() {
86 Ί(cache.Save("e1", 7, e1)).Should(Succeed())
87 })
88
89 Context("when the cached version is older than the requested version", func() {
90 It("returns nil", func() {
91 Ί(cache.Load("e1", 8)).Should(BeNil())
92 })
93 })
94
95 Context("when the cached version equals the requested version", func() {
96 It("returns the cached version", func() {
97 Ί(cache.Load("e1", 7)).Should(Equal(e1))
98 })
99 })
100
101 Context("when the cached version is newer than the requested version", func() {
102 It("returns the cached version", func() {
103 Ί(cache.Load("e1", 6)).Should(Equal(e1))
104 })
105 })
106 })
107
108 })
0 package gmeasure
1
2 import "encoding/json"
3
4 type enumSupport struct {
5 toString map[uint]string
6 toEnum map[string]uint
7 maxEnum uint
8 }
9
10 func newEnumSupport(toString map[uint]string) enumSupport {
11 toEnum, maxEnum := map[string]uint{}, uint(0)
12 for k, v := range toString {
13 toEnum[v] = k
14 if maxEnum < k {
15 maxEnum = k
16 }
17 }
18 return enumSupport{toString: toString, toEnum: toEnum, maxEnum: maxEnum}
19 }
20
21 func (es enumSupport) String(e uint) string {
22 if e > es.maxEnum {
23 return es.toString[0]
24 }
25 return es.toString[e]
26 }
27
28 func (es enumSupport) UnmarshJSON(b []byte) (uint, error) {
29 var dec string
30 if err := json.Unmarshal(b, &dec); err != nil {
31 return 0, err
32 }
33 out := es.toEnum[dec] // if we miss we get 0 which is what we want anyway
34 return out, nil
35 }
36
37 func (es enumSupport) MarshJSON(e uint) ([]byte, error) {
38 if e == 0 || e > es.maxEnum {
39 return json.Marshal(nil)
40 }
41 return json.Marshal(es.toString[e])
42 }
0 /*
1 Package gomega/gmeasure provides support for benchmarking and measuring code. It is intended as a more robust replacement for Ginkgo V1's Measure nodes.
2
3 **gmeasure IS CURRENTLY IN BETA - THE API MAY CHANGE IN THE NEAR-FUTURE. gmeasure WILL BE CONSIDERED GA WHEN Ginkgo V2 IS GA.
4
5 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
6 measuring Values (of type float64) and Durations (of type time.Duration).
7
8 Experiments allows the user to record Measurements directly by passing in Values (i.e. float64) or Durations (i.e. time.Duration)
9 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)
10 and/or recording returned values (for Value measurements). Experiments also support sampling functions - when told to sample Experiments will run functions repeatedly
11 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)
12 and the number of concurrent samples to take.
13
14 Measurements can be decorated with additional information. This is supported by passing in special typed decorators when recording measurements. These include:
15
16 - Units("any string") - to attach units to a Value Measurement (Duration Measurements always have units of "duration")
17 - 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.
18 - 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.
19
20 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.
21
22 Once measurements are complete, an Experiment can generate a comprehensive report by calling its String() or ColorableString() method.
23
24 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
25 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
26 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.
27
28 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
29 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
30 variations in performance/behavior.
31
32 When used with Ginkgo, you can emit experiment reports and encode them in test reports easily using Ginkgo V2's support for Report Entries.
33 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
34 and Rankings to emit measurement summaries in rank order.
35
36 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
37 different sections of code and can be useful when debugging or evaluating bottlenecks in a given codepath.
38 */
39 package gmeasure
40
41 import (
42 "fmt"
43 "math"
44 "reflect"
45 "sync"
46 "time"
47
48 "github.com/onsi/gomega/gmeasure/table"
49 )
50
51 /*
52 SamplingConfig configures the Sample family of experiment methods.
53 These methods invoke passed-in functions repeatedly to sample and record a given measurement.
54 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.
55 SamplingConfig can also enable concurrent sampling.
56 */
57 type SamplingConfig struct {
58 // N - the maximum number of samples to record
59 N int
60 // Duration - the maximum amount of time to spend recording samples
61 Duration time.Duration
62 // NumParallel - the number of parallel workers to spin up to record samples.
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 maxTime := time.Now().Add(100000 * time.Hour)
448 if samplingConfig.Duration > 0 {
449 maxTime = time.Now().Add(samplingConfig.Duration)
450 }
451 maxN := math.MaxInt64
452 if samplingConfig.N > 0 {
453 maxN = samplingConfig.N
454 }
455 numParallel := 1
456 if samplingConfig.NumParallel > numParallel {
457 numParallel = samplingConfig.NumParallel
458 }
459
460 work := make(chan int)
461 if numParallel > 1 {
462 for worker := 0; worker < numParallel; worker++ {
463 go func() {
464 for idx := range work {
465 callback(idx)
466 }
467 }()
468 }
469 }
470
471 idx := 0
472 var avgDt time.Duration
473 for {
474 t := time.Now()
475 if numParallel > 1 {
476 work <- idx
477 } else {
478 callback(idx)
479 }
480 dt := time.Since(t)
481 if idx >= numParallel {
482 avgDt = (avgDt*time.Duration(idx-numParallel) + dt) / time.Duration(idx-numParallel+1)
483 }
484 idx += 1
485 if idx >= maxN {
486 return
487 }
488 if time.Now().Add(avgDt).After(maxTime) {
489 return
490 }
491 }
492 }
493
494 /*
495 Get returns the Measurement with the associated name. If no Measurement is found a zero Measurement{} is returned.
496 */
497 func (e *Experiment) Get(name string) Measurement {
498 e.lock.Lock()
499 defer e.lock.Unlock()
500 idx := e.Measurements.IdxWithName(name)
501 if idx == -1 {
502 return Measurement{}
503 }
504 return e.Measurements[idx]
505 }
506
507 /*
508 GetStats returns the Stats for the Measurement with the associated name. If no Measurement is found a zero Stats{} is returned.
509
510 experiment.GetStats(name) is equivalent to experiment.Get(name).Stats()
511 */
512 func (e *Experiment) GetStats(name string) Stats {
513 measurement := e.Get(name)
514 e.lock.Lock()
515 defer e.lock.Unlock()
516 return measurement.Stats()
517 }
0 package gmeasure_test
1
2 import (
3 "fmt"
4 "strings"
5 "sync"
6 "time"
7
8 . "github.com/onsi/ginkgo"
9 . "github.com/onsi/gomega"
10
11 "github.com/onsi/gomega/gmeasure"
12 )
13
14 var _ = Describe("Experiment", func() {
15 var e *gmeasure.Experiment
16 BeforeEach(func() {
17 e = gmeasure.NewExperiment("Test Experiment")
18 })
19
20 Describe("Recording Notes", func() {
21 It("creates a note Measurement", func() {
22 e.RecordNote("I'm a note", gmeasure.Style("{{blue}}"))
23 measurement := e.Measurements[0]
24 Ί(measurement.Type).Should(Equal(gmeasure.MeasurementTypeNote))
25 Ί(measurement.ExperimentName).Should(Equal("Test Experiment"))
26 Ί(measurement.Note).Should(Equal("I'm a note"))
27 Ί(measurement.Style).Should(Equal("{{blue}}"))
28 })
29 })
30
31 Describe("Recording Durations", func() {
32 commonMeasurementAssertions := func() gmeasure.Measurement {
33 measurement := e.Get("runtime")
34 Ί(measurement.Type).Should(Equal(gmeasure.MeasurementTypeDuration))
35 Ί(measurement.ExperimentName).Should(Equal("Test Experiment"))
36 Ί(measurement.Name).Should(Equal("runtime"))
37 Ί(measurement.Units).Should(Equal("duration"))
38 Ί(measurement.Style).Should(Equal("{{red}}"))
39 Ί(measurement.PrecisionBundle.Duration).Should(Equal(time.Millisecond))
40 return measurement
41 }
42
43 BeforeEach(func() {
44 e.RecordDuration("runtime", time.Second, gmeasure.Annotation("first"), gmeasure.Style("{{red}}"), gmeasure.Precision(time.Millisecond), gmeasure.Units("ignored"))
45 })
46
47 Describe("RecordDuration", func() {
48 It("generates a measurement and records the passed-in duration along with any relevant decorations", func() {
49 e.RecordDuration("runtime", time.Minute, gmeasure.Annotation("second"))
50 measurement := commonMeasurementAssertions()
51 Ί(measurement.Durations).Should(Equal([]time.Duration{time.Second, time.Minute}))
52 Ί(measurement.Annotations).Should(Equal([]string{"first", "second"}))
53 })
54 })
55
56 Describe("MeasureDuration", func() {
57 It("measure the duration of the passed-in function", func() {
58 e.MeasureDuration("runtime", func() {
59 time.Sleep(200 * time.Millisecond)
60 }, gmeasure.Annotation("second"))
61 measurement := commonMeasurementAssertions()
62 Ί(measurement.Durations[0]).Should(Equal(time.Second))
63 Ί(measurement.Durations[1]).Should(BeNumerically("~", 200*time.Millisecond, 20*time.Millisecond))
64 Ί(measurement.Annotations).Should(Equal([]string{"first", "second"}))
65 })
66 })
67
68 Describe("SampleDuration", func() {
69 It("samples the passed-in function according to SampleConfig and records the measured durations", func() {
70 e.SampleDuration("runtime", func(_ int) {
71 time.Sleep(100 * time.Millisecond)
72 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled"))
73 measurement := commonMeasurementAssertions()
74 Ί(measurement.Durations[0]).Should(Equal(time.Second))
75 Ί(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
76 Ί(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
77 Ί(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
78 Ί(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"}))
79 })
80 })
81
82 Describe("SampleAnnotatedDuration", func() {
83 It("samples the passed-in function according to SampleConfig and records the measured durations and returned annotations", func() {
84 e.SampleAnnotatedDuration("runtime", func(idx int) gmeasure.Annotation {
85 time.Sleep(100 * time.Millisecond)
86 return gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1))
87 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored"))
88 measurement := commonMeasurementAssertions()
89 Ί(measurement.Durations[0]).Should(Equal(time.Second))
90 Ί(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
91 Ί(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
92 Ί(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
93 Ί(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"}))
94 })
95 })
96 })
97
98 Describe("Stopwatch Support", func() {
99 It("can generate a new stopwatch tied to the experiment", func() {
100 s := e.NewStopwatch()
101 time.Sleep(50 * time.Millisecond)
102 s.Record("runtime", gmeasure.Annotation("first")).Reset()
103 time.Sleep(100 * time.Millisecond)
104 s.Record("runtime", gmeasure.Annotation("second")).Reset()
105 time.Sleep(150 * time.Millisecond)
106 s.Record("runtime", gmeasure.Annotation("third"))
107 measurement := e.Get("runtime")
108 Ί(measurement.Durations[0]).Should(BeNumerically("~", 50*time.Millisecond, 20*time.Millisecond))
109 Ί(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond))
110 Ί(measurement.Durations[2]).Should(BeNumerically("~", 150*time.Millisecond, 20*time.Millisecond))
111 Ί(measurement.Annotations).Should(Equal([]string{"first", "second", "third"}))
112 })
113 })
114
115 Describe("Recording Values", func() {
116 commonMeasurementAssertions := func() gmeasure.Measurement {
117 measurement := e.Get("sprockets")
118 Ί(measurement.Type).Should(Equal(gmeasure.MeasurementTypeValue))
119 Ί(measurement.ExperimentName).Should(Equal("Test Experiment"))
120 Ί(measurement.Name).Should(Equal("sprockets"))
121 Ί(measurement.Units).Should(Equal("widgets"))
122 Ί(measurement.Style).Should(Equal("{{yellow}}"))
123 Ί(measurement.PrecisionBundle.ValueFormat).Should(Equal("%.0f"))
124 return measurement
125 }
126
127 BeforeEach(func() {
128 e.RecordValue("sprockets", 3.2, gmeasure.Annotation("first"), gmeasure.Style("{{yellow}}"), gmeasure.Precision(0), gmeasure.Units("widgets"))
129 })
130
131 Describe("RecordValue", func() {
132 It("generates a measurement and records the passed-in value along with any relevant decorations", func() {
133 e.RecordValue("sprockets", 17.4, gmeasure.Annotation("second"))
134 measurement := commonMeasurementAssertions()
135 Ί(measurement.Values).Should(Equal([]float64{3.2, 17.4}))
136 Ί(measurement.Annotations).Should(Equal([]string{"first", "second"}))
137 })
138 })
139
140 Describe("MeasureValue", func() {
141 It("records the value returned by the passed-in function", func() {
142 e.MeasureValue("sprockets", func() float64 {
143 return 17.4
144 }, gmeasure.Annotation("second"))
145 measurement := commonMeasurementAssertions()
146 Ί(measurement.Values).Should(Equal([]float64{3.2, 17.4}))
147 Ί(measurement.Annotations).Should(Equal([]string{"first", "second"}))
148 })
149 })
150
151 Describe("SampleValue", func() {
152 It("samples the passed-in function according to SampleConfig and records the resulting values", func() {
153 e.SampleValue("sprockets", func(idx int) float64 {
154 return 17.4 + float64(idx)
155 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled"))
156 measurement := commonMeasurementAssertions()
157 Ί(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4}))
158 Ί(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"}))
159 })
160 })
161
162 Describe("SampleAnnotatedValue", func() {
163 It("samples the passed-in function according to SampleConfig and records the returned values and annotations", func() {
164 e.SampleAnnotatedValue("sprockets", func(idx int) (float64, gmeasure.Annotation) {
165 return 17.4 + float64(idx), gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1))
166 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored"))
167 measurement := commonMeasurementAssertions()
168 Ί(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4}))
169 Ί(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"}))
170 })
171 })
172 })
173
174 Describe("Sampling", func() {
175 var indices []int
176 BeforeEach(func() {
177 indices = []int{}
178 })
179
180 ints := func(n int) []int {
181 out := []int{}
182 for i := 0; i < n; i++ {
183 out = append(out, i)
184 }
185 return out
186 }
187
188 It("calls the function repeatedly passing in an index", func() {
189 e.Sample(func(idx int) {
190 indices = append(indices, idx)
191 }, gmeasure.SamplingConfig{N: 3})
192
193 Ί(indices).Should(Equal(ints(3)))
194 })
195
196 It("can cap the maximum number of samples", func() {
197 e.Sample(func(idx int) {
198 indices = append(indices, idx)
199 }, gmeasure.SamplingConfig{N: 10, Duration: time.Minute})
200
201 Ί(indices).Should(Equal(ints(10)))
202 })
203
204 It("can cap the maximum sample time", func() {
205 e.Sample(func(idx int) {
206 indices = append(indices, idx)
207 time.Sleep(10 * time.Millisecond)
208 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond})
209
210 Ί(len(indices)).Should(BeNumerically("~", 10, 3))
211 Ί(indices).Should(Equal(ints(len(indices))))
212 })
213
214 It("can run samples in parallel", func() {
215 lock := &sync.Mutex{}
216
217 e.Sample(func(idx int) {
218 lock.Lock()
219 indices = append(indices, idx)
220 lock.Unlock()
221 time.Sleep(10 * time.Millisecond)
222 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond, NumParallel: 3})
223
224 lock.Lock()
225 defer lock.Unlock()
226 Ί(len(indices)).Should(BeNumerically("~", 30, 10))
227 Ί(indices).Should(ConsistOf(ints(len(indices))))
228 })
229
230 It("panics if the SamplingConfig is misconfigured", func() {
231 Expect(func() {
232 e.Sample(func(_ int) {}, gmeasure.SamplingConfig{})
233 }).To(PanicWith("you must specify at least one of SamplingConfig.N and SamplingConfig.Duration"))
234 })
235 })
236
237 Describe("recording multiple entries", func() {
238 It("always appends to the correct measurement (by name)", func() {
239 e.RecordDuration("alpha", time.Second)
240 e.RecordDuration("beta", time.Minute)
241 e.RecordValue("gamma", 1)
242 e.RecordValue("delta", 2.71)
243 e.RecordDuration("alpha", 2*time.Second)
244 e.RecordDuration("beta", 2*time.Minute)
245 e.RecordValue("gamma", 2)
246 e.RecordValue("delta", 3.141)
247
248 Ί(e.Measurements).Should(HaveLen(4))
249 Ί(e.Get("alpha").Durations).Should(Equal([]time.Duration{time.Second, 2 * time.Second}))
250 Ί(e.Get("beta").Durations).Should(Equal([]time.Duration{time.Minute, 2 * time.Minute}))
251 Ί(e.Get("gamma").Values).Should(Equal([]float64{1, 2}))
252 Ί(e.Get("delta").Values).Should(Equal([]float64{2.71, 3.141}))
253 })
254
255 It("panics if you incorrectly mix types", func() {
256 e.RecordDuration("runtime", time.Second)
257 Ί(func() {
258 e.RecordValue("runtime", 3.141)
259 }).Should(PanicWith("attempting to record value with name 'runtime'. That name is already in-use for recording durations."))
260
261 e.RecordValue("sprockets", 2)
262 Ί(func() {
263 e.RecordDuration("sprockets", time.Minute)
264 }).Should(PanicWith("attempting to record duration with name 'sprockets'. That name is already in-use for recording values."))
265 })
266 })
267
268 Describe("Decorators", func() {
269 It("uses the default precisions when none is specified", func() {
270 e.RecordValue("sprockets", 2)
271 e.RecordDuration("runtime", time.Minute)
272
273 Ί(e.Get("sprockets").PrecisionBundle.ValueFormat).Should(Equal("%.3f"))
274 Ί(e.Get("runtime").PrecisionBundle.Duration).Should(Equal(100 * time.Microsecond))
275 })
276
277 It("panics if an unsupported type is passed into Precision", func() {
278 Ί(func() {
279 gmeasure.Precision("aardvark")
280 }).Should(PanicWith("invalid precision type, must be time.Duration or int"))
281 })
282
283 It("panics if an unrecognized argumnet is passed in", func() {
284 Ί(func() {
285 e.RecordValue("sprockets", 2, "boom")
286 }).Should(PanicWith(`unrecognized argument "boom"`))
287 })
288 })
289
290 Describe("Getting Measurements", func() {
291 Context("when the Measurement does not exist", func() {
292 It("returns the zero Measurement", func() {
293 Ί(e.Get("not here")).Should(BeZero())
294 })
295 })
296 })
297
298 Describe("Getting Stats", func() {
299 It("returns the Measurement's Stats", func() {
300 e.RecordValue("alpha", 1)
301 e.RecordValue("alpha", 2)
302 e.RecordValue("alpha", 3)
303 Ί(e.GetStats("alpha")).Should(Equal(e.Get("alpha").Stats()))
304 })
305 })
306
307 Describe("Generating Reports", func() {
308 BeforeEach(func() {
309 e.RecordNote("A note")
310 e.RecordValue("sprockets", 7, gmeasure.Units("widgets"), gmeasure.Precision(0), gmeasure.Style("{{yellow}}"), gmeasure.Annotation("sprockets-1"))
311 e.RecordDuration("runtime", time.Second, gmeasure.Precision(100*time.Millisecond), gmeasure.Style("{{red}}"), gmeasure.Annotation("runtime-1"))
312 e.RecordNote("A blue note", gmeasure.Style("{{blue}}"))
313 e.RecordValue("gear ratio", 10.3, gmeasure.Precision(2), gmeasure.Style("{{green}}"), gmeasure.Annotation("ratio-1"))
314
315 e.RecordValue("sprockets", 8, gmeasure.Annotation("sprockets-2"))
316 e.RecordValue("sprockets", 9, gmeasure.Annotation("sprockets-3"))
317
318 e.RecordDuration("runtime", 2*time.Second, gmeasure.Annotation("runtime-2"))
319 e.RecordValue("gear ratio", 13.758, gmeasure.Precision(2), gmeasure.Annotation("ratio-2"))
320 })
321
322 It("emits a nicely formatted table", func() {
323 expected := strings.Join([]string{
324 "Test Experiment",
325 "Name | N | Min | Median | Mean | StdDev | Max ",
326 "=============================================================================",
327 "A note ",
328 "-----------------------------------------------------------------------------",
329 "sprockets [widgets] | 3 | 7 | 8 | 8 | 1 | 9 ",
330 " | | sprockets-1 | | | | sprockets-3",
331 "-----------------------------------------------------------------------------",
332 "runtime [duration] | 2 | 1s | 1.5s | 1.5s | 500ms | 2s ",
333 " | | runtime-1 | | | | runtime-2 ",
334 "-----------------------------------------------------------------------------",
335 "A blue note ",
336 "-----------------------------------------------------------------------------",
337 "gear ratio | 2 | 10.30 | 12.03 | 12.03 | 1.73 | 13.76 ",
338 " | | ratio-1 | | | | ratio-2 ",
339 "",
340 }, "\n")
341 Ί(e.String()).Should(Equal(expected))
342 })
343
344 It("can also emit a styled table", func() {
345 expected := strings.Join([]string{
346 "{{bold}}Test Experiment",
347 "{{/}}{{bold}}Name {{/}} | {{bold}}N{{/}} | {{bold}}Min {{/}} | {{bold}}Median{{/}} | {{bold}}Mean {{/}} | {{bold}}StdDev{{/}} | {{bold}}Max {{/}}",
348 "=============================================================================",
349 "A note ",
350 "-----------------------------------------------------------------------------",
351 "{{yellow}}sprockets [widgets]{{/}} | {{yellow}}3{{/}} | {{yellow}}7 {{/}} | {{yellow}}8 {{/}} | {{yellow}}8 {{/}} | {{yellow}}1 {{/}} | {{yellow}}9 {{/}}",
352 " | | {{yellow}}sprockets-1{{/}} | | | | {{yellow}}sprockets-3{{/}}",
353 "-----------------------------------------------------------------------------",
354 "{{red}}runtime [duration] {{/}} | {{red}}2{{/}} | {{red}}1s {{/}} | {{red}}1.5s {{/}} | {{red}}1.5s {{/}} | {{red}}500ms {{/}} | {{red}}2s {{/}}",
355 " | | {{red}}runtime-1 {{/}} | | | | {{red}}runtime-2 {{/}}",
356 "-----------------------------------------------------------------------------",
357 "{{blue}}A blue note {{/}}",
358 "-----------------------------------------------------------------------------",
359 "{{green}}gear ratio {{/}} | {{green}}2{{/}} | {{green}}10.30 {{/}} | {{green}}12.03 {{/}} | {{green}}12.03{{/}} | {{green}}1.73 {{/}} | {{green}}13.76 {{/}}",
360 " | | {{green}}ratio-1 {{/}} | | | | {{green}}ratio-2 {{/}}",
361 "",
362 }, "\n")
363 Ί(e.ColorableString()).Should(Equal(expected))
364 })
365 })
366 })
0 package gmeasure_test
1
2 import (
3 "testing"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 )
8
9 func TestGmeasure(t *testing.T) {
10 RegisterFailHandler(Fail)
11 RunSpecs(t, "Gmeasure Suite")
12 }
0 package gmeasure
1
2 import (
3 "fmt"
4 "math"
5 "sort"
6 "time"
7
8 "github.com/onsi/gomega/gmeasure/table"
9 )
10
11 type MeasurementType uint
12
13 const (
14 MeasurementTypeInvalid MeasurementType = iota
15 MeasurementTypeNote
16 MeasurementTypeDuration
17 MeasurementTypeValue
18 )
19
20 var letEnumSupport = newEnumSupport(map[uint]string{uint(MeasurementTypeInvalid): "INVALID LOG ENTRY TYPE", uint(MeasurementTypeNote): "Note", uint(MeasurementTypeDuration): "Duration", uint(MeasurementTypeValue): "Value"})
21
22 func (s MeasurementType) String() string { return letEnumSupport.String(uint(s)) }
23 func (s *MeasurementType) UnmarshalJSON(b []byte) error {
24 out, err := letEnumSupport.UnmarshJSON(b)
25 *s = MeasurementType(out)
26 return err
27 }
28 func (s MeasurementType) MarshalJSON() ([]byte, error) { return letEnumSupport.MarshJSON(uint(s)) }
29
30 /*
31 Measurement records all captured data for a given measurement. You generally don't make Measurements directly - but you can fetch them from Experiments using Get().
32
33 When using Ginkgo, you can register Measurements as Report Entries via AddReportEntry. This will emit all the captured data points when Ginkgo generates the report.
34 */
35 type Measurement struct {
36 // Type is the MeasurementType - one of MeasurementTypeNote, MeasurementTypeDuration, or MeasurementTypeValue
37 Type MeasurementType
38
39 // ExperimentName is the name of the experiment that this Measurement is associated with
40 ExperimentName string
41
42 // If Type is MeasurementTypeNote, Note is populated with the note text.
43 Note string
44
45 // If Type is MeasurementTypeDuration or MeasurementTypeValue, Name is the name of the recorded measurement
46 Name string
47
48 // Style captures the styling information (if any) for this Measurement
49 Style string
50
51 // Units capture the units (if any) for this Measurement. Units is set to "duration" if the Type is MeasurementTypeDuration
52 Units string
53
54 // PrecisionBundle captures the precision to use when rendering data for this Measurement.
55 // If Type is MeasurementTypeDuration then PrecisionBundle.Duration is used to round any durations before presentation.
56 // If Type is MeasurementTypeValue then PrecisionBundle.ValueFormat is used to format any values before presentation
57 PrecisionBundle PrecisionBundle
58
59 // If Type is MeasurementTypeDuration, Durations will contain all durations recorded for this measurement
60 Durations []time.Duration
61
62 // If Type is MeasurementTypeValue, Values will contain all float64s recorded for this measurement
63 Values []float64
64
65 // If Type is MeasurementTypeDuration or MeasurementTypeValue then Annotations will include string annotations for all recorded Durations or Values.
66 // If the user does not pass-in an Annotation() decoration for a particular value or duration, the corresponding entry in the Annotations slice will be the empty string ""
67 Annotations []string
68 }
69
70 type Measurements []Measurement
71
72 func (m Measurements) IdxWithName(name string) int {
73 for idx, measurement := range m {
74 if measurement.Name == name {
75 return idx
76 }
77 }
78
79 return -1
80 }
81
82 func (m Measurement) report(enableStyling bool) string {
83 out := ""
84 style := m.Style
85 if !enableStyling {
86 style = ""
87 }
88 switch m.Type {
89 case MeasurementTypeNote:
90 out += fmt.Sprintf("%s - Note\n%s\n", m.ExperimentName, m.Note)
91 if style != "" {
92 out = style + out + "{{/}}"
93 }
94 return out
95 case MeasurementTypeValue, MeasurementTypeDuration:
96 out += fmt.Sprintf("%s - %s", m.ExperimentName, m.Name)
97 if m.Units != "" {
98 out += " [" + m.Units + "]"
99 }
100 if style != "" {
101 out = style + out + "{{/}}"
102 }
103 out += "\n"
104 out += m.Stats().String() + "\n"
105 }
106 t := table.NewTable()
107 t.TableStyle.EnableTextStyling = enableStyling
108 switch m.Type {
109 case MeasurementTypeValue:
110 t.AppendRow(table.R(table.C("Value", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style))
111 for idx := range m.Values {
112 t.AppendRow(table.R(
113 table.C(fmt.Sprintf(m.PrecisionBundle.ValueFormat, m.Values[idx]), table.AlignTypeRight),
114 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft),
115 ))
116 }
117 case MeasurementTypeDuration:
118 t.AppendRow(table.R(table.C("Duration", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style))
119 for idx := range m.Durations {
120 t.AppendRow(table.R(
121 table.C(m.Durations[idx].Round(m.PrecisionBundle.Duration).String(), style, table.AlignTypeRight),
122 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft),
123 ))
124 }
125 }
126 out += t.Render()
127 return out
128 }
129
130 /*
131 ColorableString generates a styled report that includes all the data points for this Measurement.
132 It is called automatically by Ginkgo's reporting infrastructure when the Measurement is registered as a ReportEntry via AddReportEntry.
133 */
134 func (m Measurement) ColorableString() string {
135 return m.report(true)
136 }
137
138 /*
139 String generates an unstyled report that includes all the data points for this Measurement.
140 */
141 func (m Measurement) String() string {
142 return m.report(false)
143 }
144
145 /*
146 Stats returns a Stats struct summarizing the statistic of this measurement
147 */
148 func (m Measurement) Stats() Stats {
149 if m.Type == MeasurementTypeInvalid || m.Type == MeasurementTypeNote {
150 return Stats{}
151 }
152
153 out := Stats{
154 ExperimentName: m.ExperimentName,
155 MeasurementName: m.Name,
156 Style: m.Style,
157 Units: m.Units,
158 PrecisionBundle: m.PrecisionBundle,
159 }
160
161 switch m.Type {
162 case MeasurementTypeValue:
163 out.Type = StatsTypeValue
164 out.N = len(m.Values)
165 if out.N == 0 {
166 return out
167 }
168 indices, sum := make([]int, len(m.Values)), 0.0
169 for idx, v := range m.Values {
170 indices[idx] = idx
171 sum += v
172 }
173 sort.Slice(indices, func(i, j int) bool {
174 return m.Values[indices[i]] < m.Values[indices[j]]
175 })
176 out.ValueBundle = map[Stat]float64{
177 StatMin: m.Values[indices[0]],
178 StatMax: m.Values[indices[out.N-1]],
179 StatMean: sum / float64(out.N),
180 StatStdDev: 0.0,
181 }
182 out.AnnotationBundle = map[Stat]string{
183 StatMin: m.Annotations[indices[0]],
184 StatMax: m.Annotations[indices[out.N-1]],
185 }
186
187 if out.N%2 == 0 {
188 out.ValueBundle[StatMedian] = (m.Values[indices[out.N/2]] + m.Values[indices[out.N/2-1]]) / 2.0
189 } else {
190 out.ValueBundle[StatMedian] = m.Values[indices[(out.N-1)/2]]
191 }
192
193 for _, v := range m.Values {
194 out.ValueBundle[StatStdDev] += (v - out.ValueBundle[StatMean]) * (v - out.ValueBundle[StatMean])
195 }
196 out.ValueBundle[StatStdDev] = math.Sqrt(out.ValueBundle[StatStdDev] / float64(out.N))
197 case MeasurementTypeDuration:
198 out.Type = StatsTypeDuration
199 out.N = len(m.Durations)
200 if out.N == 0 {
201 return out
202 }
203 indices, sum := make([]int, len(m.Durations)), time.Duration(0)
204 for idx, v := range m.Durations {
205 indices[idx] = idx
206 sum += v
207 }
208 sort.Slice(indices, func(i, j int) bool {
209 return m.Durations[indices[i]] < m.Durations[indices[j]]
210 })
211 out.DurationBundle = map[Stat]time.Duration{
212 StatMin: m.Durations[indices[0]],
213 StatMax: m.Durations[indices[out.N-1]],
214 StatMean: sum / time.Duration(out.N),
215 }
216 out.AnnotationBundle = map[Stat]string{
217 StatMin: m.Annotations[indices[0]],
218 StatMax: m.Annotations[indices[out.N-1]],
219 }
220
221 if out.N%2 == 0 {
222 out.DurationBundle[StatMedian] = (m.Durations[indices[out.N/2]] + m.Durations[indices[out.N/2-1]]) / 2
223 } else {
224 out.DurationBundle[StatMedian] = m.Durations[indices[(out.N-1)/2]]
225 }
226 stdDev := 0.0
227 for _, v := range m.Durations {
228 stdDev += float64(v-out.DurationBundle[StatMean]) * float64(v-out.DurationBundle[StatMean])
229 }
230 out.DurationBundle[StatStdDev] = time.Duration(math.Sqrt(stdDev / float64(out.N)))
231 }
232
233 return out
234 }
0 package gmeasure_test
1
2 import (
3 "math"
4 "strings"
5 "time"
6
7 . "github.com/onsi/ginkgo"
8 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/gmeasure"
10 )
11
12 var _ = Describe("Measurement", func() {
13 var e *gmeasure.Experiment
14 var measurement gmeasure.Measurement
15
16 BeforeEach(func() {
17 e = gmeasure.NewExperiment("Test Experiment")
18 })
19
20 Describe("Note Measurement", func() {
21 BeforeEach(func() {
22 e.RecordNote("I'm a red note", gmeasure.Style("{{red}}"))
23 measurement = e.Measurements[0]
24 })
25
26 Describe("Generating Stats", func() {
27 It("returns an empty stats", func() {
28 Ί(measurement.Stats()).Should(BeZero())
29 })
30 })
31
32 Describe("Emitting an unstyled report", func() {
33 It("does not include styling", func() {
34 Ί(measurement.String()).Should(Equal("Test Experiment - Note\nI'm a red note\n"))
35 })
36 })
37
38 Describe("Emitting a styled report", func() {
39 It("does include styling", func() {
40 Ί(measurement.ColorableString()).Should(Equal("{{red}}Test Experiment - Note\nI'm a red note\n{{/}}"))
41 })
42 })
43 })
44
45 Describe("Value Measurement", func() {
46 var min, median, mean, stdDev, max float64
47 BeforeEach(func() {
48 e.RecordValue("flange widths", 7.128, gmeasure.Annotation("A"), gmeasure.Precision(2), gmeasure.Units("inches"), gmeasure.Style("{{blue}}"))
49 e.RecordValue("flange widths", 3.141, gmeasure.Annotation("B"))
50 e.RecordValue("flange widths", 9.28223, gmeasure.Annotation("C"))
51 e.RecordValue("flange widths", 14.249, gmeasure.Annotation("D"))
52 e.RecordValue("flange widths", 8.975, gmeasure.Annotation("E"))
53 measurement = e.Measurements[0]
54 min = 3.141
55 max = 14.249
56 median = 8.975
57 mean = (7.128 + 3.141 + 9.28223 + 14.249 + 8.975) / 5.0
58 stdDev = (7.128-mean)*(7.128-mean) + (3.141-mean)*(3.141-mean) + (9.28223-mean)*(9.28223-mean) + (14.249-mean)*(14.249-mean) + (8.975-mean)*(8.975-mean)
59 stdDev = math.Sqrt(stdDev / 5.0)
60 })
61
62 Describe("Generating Stats", func() {
63 It("generates a correctly configured Stats with correct values", func() {
64 stats := measurement.Stats()
65 Ί(stats.ExperimentName).Should(Equal("Test Experiment"))
66 Ί(stats.MeasurementName).Should(Equal("flange widths"))
67 Ί(stats.Style).Should(Equal("{{blue}}"))
68 Ί(stats.Units).Should(Equal("inches"))
69 Ί(stats.PrecisionBundle.ValueFormat).Should(Equal("%.2f"))
70
71 Ί(stats.ValueBundle[gmeasure.StatMin]).Should(Equal(min))
72 Ί(stats.AnnotationBundle[gmeasure.StatMin]).Should(Equal("B"))
73 Ί(stats.ValueBundle[gmeasure.StatMax]).Should(Equal(max))
74 Ί(stats.AnnotationBundle[gmeasure.StatMax]).Should(Equal("D"))
75 Ί(stats.ValueBundle[gmeasure.StatMedian]).Should(Equal(median))
76 Ί(stats.ValueBundle[gmeasure.StatMean]).Should(Equal(mean))
77 Ί(stats.ValueBundle[gmeasure.StatStdDev]).Should(Equal(stdDev))
78 })
79 })
80
81 Describe("Emitting an unstyled report", func() {
82 It("does not include styling", func() {
83 expected := strings.Join([]string{
84 "Test Experiment - flange widths [inches]",
85 "3.14 < [8.97] | <8.56> Âą3.59 < 14.25",
86 "Value | Annotation",
87 "==================",
88 " 7.13 | A ",
89 "------------------",
90 " 3.14 | B ",
91 "------------------",
92 " 9.28 | C ",
93 "------------------",
94 "14.25 | D ",
95 "------------------",
96 " 8.97 | E ",
97 "",
98 }, "\n")
99 Ί(measurement.String()).Should(Equal(expected))
100 })
101 })
102
103 Describe("Emitting a styled report", func() {
104 It("does include styling", func() {
105 expected := strings.Join([]string{
106 "{{blue}}Test Experiment - flange widths [inches]{{/}}",
107 "3.14 < [8.97] | <8.56> Âą3.59 < 14.25",
108 "{{blue}}Value{{/}} | {{blue}}Annotation{{/}}",
109 "==================",
110 " 7.13 | {{gray}}A {{/}}",
111 "------------------",
112 " 3.14 | {{gray}}B {{/}}",
113 "------------------",
114 " 9.28 | {{gray}}C {{/}}",
115 "------------------",
116 "14.25 | {{gray}}D {{/}}",
117 "------------------",
118 " 8.97 | {{gray}}E {{/}}",
119 "",
120 }, "\n")
121 Ί(measurement.ColorableString()).Should(Equal(expected))
122 })
123 })
124
125 Describe("Computing medians", func() {
126 Context("with an odd number of values", func() {
127 It("returns the middle element", func() {
128 e.RecordValue("odd", 5)
129 e.RecordValue("odd", 1)
130 e.RecordValue("odd", 2)
131 e.RecordValue("odd", 4)
132 e.RecordValue("odd", 3)
133
134 Ί(e.GetStats("odd").ValueBundle[gmeasure.StatMedian]).Should(Equal(3.0))
135 })
136 })
137
138 Context("when an even number of values", func() {
139 It("returns the mean of the two middle elements", func() {
140 e.RecordValue("even", 1)
141 e.RecordValue("even", 2)
142 e.RecordValue("even", 4)
143 e.RecordValue("even", 3)
144
145 Ί(e.GetStats("even").ValueBundle[gmeasure.StatMedian]).Should(Equal(2.5))
146 })
147 })
148 })
149 })
150
151 Describe("Duration Measurement", func() {
152 var min, median, mean, stdDev, max time.Duration
153 BeforeEach(func() {
154 e.RecordDuration("runtime", 7128*time.Millisecond, gmeasure.Annotation("A"), gmeasure.Precision(time.Millisecond*100), gmeasure.Style("{{blue}}"))
155 e.RecordDuration("runtime", 3141*time.Millisecond, gmeasure.Annotation("B"))
156 e.RecordDuration("runtime", 9282*time.Millisecond, gmeasure.Annotation("C"))
157 e.RecordDuration("runtime", 14249*time.Millisecond, gmeasure.Annotation("D"))
158 e.RecordDuration("runtime", 8975*time.Millisecond, gmeasure.Annotation("E"))
159 measurement = e.Measurements[0]
160 min = 3141 * time.Millisecond
161 max = 14249 * time.Millisecond
162 median = 8975 * time.Millisecond
163 mean = ((7128 + 3141 + 9282 + 14249 + 8975) * time.Millisecond) / 5
164 stdDev = time.Duration(math.Sqrt((float64(7128*time.Millisecond-mean)*float64(7128*time.Millisecond-mean) + float64(3141*time.Millisecond-mean)*float64(3141*time.Millisecond-mean) + float64(9282*time.Millisecond-mean)*float64(9282*time.Millisecond-mean) + float64(14249*time.Millisecond-mean)*float64(14249*time.Millisecond-mean) + float64(8975*time.Millisecond-mean)*float64(8975*time.Millisecond-mean)) / 5.0))
165 })
166
167 Describe("Generating Stats", func() {
168 It("generates a correctly configured Stats with correct values", func() {
169 stats := measurement.Stats()
170 Ί(stats.ExperimentName).Should(Equal("Test Experiment"))
171 Ί(stats.MeasurementName).Should(Equal("runtime"))
172 Ί(stats.Style).Should(Equal("{{blue}}"))
173 Ί(stats.Units).Should(Equal("duration"))
174 Ί(stats.PrecisionBundle.Duration).Should(Equal(time.Millisecond * 100))
175
176 Ί(stats.DurationBundle[gmeasure.StatMin]).Should(Equal(min))
177 Ί(stats.AnnotationBundle[gmeasure.StatMin]).Should(Equal("B"))
178 Ί(stats.DurationBundle[gmeasure.StatMax]).Should(Equal(max))
179 Ί(stats.AnnotationBundle[gmeasure.StatMax]).Should(Equal("D"))
180 Ί(stats.DurationBundle[gmeasure.StatMedian]).Should(Equal(median))
181 Ί(stats.DurationBundle[gmeasure.StatMean]).Should(Equal(mean))
182 Ί(stats.DurationBundle[gmeasure.StatStdDev]).Should(Equal(stdDev))
183 })
184 })
185
186 Describe("Emitting an unstyled report", func() {
187 It("does not include styling", func() {
188 expected := strings.Join([]string{
189 "Test Experiment - runtime [duration]",
190 "3.1s < [9s] | <8.6s> Âą3.6s < 14.2s",
191 "Duration | Annotation",
192 "=====================",
193 " 7.1s | A ",
194 "---------------------",
195 " 3.1s | B ",
196 "---------------------",
197 " 9.3s | C ",
198 "---------------------",
199 " 14.2s | D ",
200 "---------------------",
201 " 9s | E ",
202 "",
203 }, "\n")
204 Ί(measurement.String()).Should(Equal(expected))
205 })
206 })
207
208 Describe("Emitting a styled report", func() {
209 It("does include styling", func() {
210 expected := strings.Join([]string{
211 "{{blue}}Test Experiment - runtime [duration]{{/}}",
212 "3.1s < [9s] | <8.6s> Âą3.6s < 14.2s",
213 "{{blue}}Duration{{/}} | {{blue}}Annotation{{/}}",
214 "=====================",
215 "{{blue}} 7.1s{{/}} | {{gray}}A {{/}}",
216 "---------------------",
217 "{{blue}} 3.1s{{/}} | {{gray}}B {{/}}",
218 "---------------------",
219 "{{blue}} 9.3s{{/}} | {{gray}}C {{/}}",
220 "---------------------",
221 "{{blue}} 14.2s{{/}} | {{gray}}D {{/}}",
222 "---------------------",
223 "{{blue}} 9s{{/}} | {{gray}}E {{/}}",
224 "",
225 }, "\n")
226 Ί(measurement.ColorableString()).Should(Equal(expected))
227 })
228 })
229
230 Describe("Computing medians", func() {
231 Context("with an odd number of values", func() {
232 It("returns the middle element", func() {
233 e.RecordDuration("odd", 5*time.Second)
234 e.RecordDuration("odd", 1*time.Second)
235 e.RecordDuration("odd", 2*time.Second)
236 e.RecordDuration("odd", 4*time.Second)
237 e.RecordDuration("odd", 3*time.Second)
238
239 Ί(e.GetStats("odd").DurationBundle[gmeasure.StatMedian]).Should(Equal(3 * time.Second))
240 })
241 })
242
243 Context("when an even number of values", func() {
244 It("returns the mean of the two middle elements", func() {
245 e.RecordDuration("even", 1*time.Second)
246 e.RecordDuration("even", 2*time.Second)
247 e.RecordDuration("even", 4*time.Second)
248 e.RecordDuration("even", 3*time.Second)
249
250 Ί(e.GetStats("even").DurationBundle[gmeasure.StatMedian]).Should(Equal(2500 * time.Millisecond))
251 })
252 })
253 })
254 })
255 })
0 package gmeasure
1
2 import (
3 "fmt"
4 "sort"
5
6 "github.com/onsi/gomega/gmeasure/table"
7 )
8
9 /*
10 RankingCriteria is an enum representing the criteria by which Stats should be ranked. The enum names should be self explanatory. e.g. LowerMeanIsBetter means that Stats with lower mean values are considered more beneficial, with the lowest mean being declared the "winner" .
11 */
12 type RankingCriteria uint
13
14 const (
15 LowerMeanIsBetter RankingCriteria = iota
16 HigherMeanIsBetter
17 LowerMedianIsBetter
18 HigherMedianIsBetter
19 LowerMinIsBetter
20 HigherMinIsBetter
21 LowerMaxIsBetter
22 HigherMaxIsBetter
23 )
24
25 var rcEnumSupport = newEnumSupport(map[uint]string{uint(LowerMeanIsBetter): "Lower Mean is Better", uint(HigherMeanIsBetter): "Higher Mean is Better", uint(LowerMedianIsBetter): "Lower Median is Better", uint(HigherMedianIsBetter): "Higher Median is Better", uint(LowerMinIsBetter): "Lower Mins is Better", uint(HigherMinIsBetter): "Higher Min is Better", uint(LowerMaxIsBetter): "Lower Max is Better", uint(HigherMaxIsBetter): "Higher Max is Better"})
26
27 func (s RankingCriteria) String() string { return rcEnumSupport.String(uint(s)) }
28 func (s *RankingCriteria) UnmarshalJSON(b []byte) error {
29 out, err := rcEnumSupport.UnmarshJSON(b)
30 *s = RankingCriteria(out)
31 return err
32 }
33 func (s RankingCriteria) MarshalJSON() ([]byte, error) { return rcEnumSupport.MarshJSON(uint(s)) }
34
35 /*
36 Ranking ranks a set of Stats by a specified RankingCritera. Use RankStats to create a Ranking.
37
38 When using Ginkgo, you can register Rankings as Report Entries via AddReportEntry. This will emit a formatted table representing the Stats in rank-order when Ginkgo generates the report.
39 */
40 type Ranking struct {
41 Criteria RankingCriteria
42 Stats []Stats
43 }
44
45 /*
46 RankStats creates a new ranking of the passed-in stats according to the passed-in criteria.
47 */
48 func RankStats(criteria RankingCriteria, stats ...Stats) Ranking {
49 sort.Slice(stats, func(i int, j int) bool {
50 switch criteria {
51 case LowerMeanIsBetter:
52 return stats[i].FloatFor(StatMean) < stats[j].FloatFor(StatMean)
53 case HigherMeanIsBetter:
54 return stats[i].FloatFor(StatMean) > stats[j].FloatFor(StatMean)
55 case LowerMedianIsBetter:
56 return stats[i].FloatFor(StatMedian) < stats[j].FloatFor(StatMedian)
57 case HigherMedianIsBetter:
58 return stats[i].FloatFor(StatMedian) > stats[j].FloatFor(StatMedian)
59 case LowerMinIsBetter:
60 return stats[i].FloatFor(StatMin) < stats[j].FloatFor(StatMin)
61 case HigherMinIsBetter:
62 return stats[i].FloatFor(StatMin) > stats[j].FloatFor(StatMin)
63 case LowerMaxIsBetter:
64 return stats[i].FloatFor(StatMax) < stats[j].FloatFor(StatMax)
65 case HigherMaxIsBetter:
66 return stats[i].FloatFor(StatMax) > stats[j].FloatFor(StatMax)
67 }
68 return false
69 })
70
71 out := Ranking{
72 Criteria: criteria,
73 Stats: stats,
74 }
75
76 return out
77 }
78
79 /*
80 Winner returns the Stats with the most optimal rank based on the specified ranking criteria. For example, if the RankingCriteria is LowerMaxIsBetter then the Stats with the lowest value or duration for StatMax will be returned as the "winner"
81 */
82 func (c Ranking) Winner() Stats {
83 if len(c.Stats) == 0 {
84 return Stats{}
85 }
86 return c.Stats[0]
87 }
88
89 func (c Ranking) report(enableStyling bool) string {
90 if len(c.Stats) == 0 {
91 return "Empty Ranking"
92 }
93 t := table.NewTable()
94 t.TableStyle.EnableTextStyling = enableStyling
95 t.AppendRow(table.R(
96 table.C("Experiment"), table.C("Name"), table.C("N"), table.C("Min"), table.C("Median"), table.C("Mean"), table.C("StdDev"), table.C("Max"),
97 table.Divider("="),
98 "{{bold}}",
99 ))
100
101 for idx, stats := range c.Stats {
102 name := stats.MeasurementName
103 if stats.Units != "" {
104 name = name + " [" + stats.Units + "]"
105 }
106 experimentName := stats.ExperimentName
107 style := stats.Style
108 if idx == 0 {
109 style = "{{bold}}" + style
110 name += "\n*Winner*"
111 experimentName += "\n*Winner*"
112 }
113 r := table.R(style)
114 t.AppendRow(r)
115 r.AppendCell(table.C(experimentName), table.C(name))
116 r.AppendCell(stats.cells()...)
117
118 }
119 out := fmt.Sprintf("Ranking Criteria: %s\n", c.Criteria)
120 if enableStyling {
121 out = "{{bold}}" + out + "{{/}}"
122 }
123 out += t.Render()
124 return out
125 }
126
127 /*
128 ColorableString generates a styled report that includes a table of the rank-ordered Stats
129 It is called automatically by Ginkgo's reporting infrastructure when the Ranking is registered as a ReportEntry via AddReportEntry.
130 */
131 func (c Ranking) ColorableString() string {
132 return c.report(true)
133 }
134
135 /*
136 String generates an unstyled report that includes a table of the rank-ordered Stats
137 */
138 func (c Ranking) String() string {
139 return c.report(false)
140 }
0 package gmeasure_test
1
2 import (
3 "strings"
4 "time"
5
6 . "github.com/onsi/ginkgo"
7 . "github.com/onsi/ginkgo/extensions/table"
8 . "github.com/onsi/gomega"
9 "github.com/onsi/gomega/gmeasure"
10 )
11
12 var _ = Describe("Rank", func() {
13 var A, B, C, D gmeasure.Stats
14
15 Describe("Ranking Values", func() {
16 makeStats := func(name string, min float64, max float64, mean float64, median float64) gmeasure.Stats {
17 return gmeasure.Stats{
18 Type: gmeasure.StatsTypeValue,
19 ExperimentName: "Exp-" + name,
20 MeasurementName: name,
21 N: 100,
22 PrecisionBundle: gmeasure.Precision(2),
23 ValueBundle: map[gmeasure.Stat]float64{
24 gmeasure.StatMin: min,
25 gmeasure.StatMax: max,
26 gmeasure.StatMean: mean,
27 gmeasure.StatMedian: median,
28 gmeasure.StatStdDev: 2.0,
29 },
30 }
31 }
32
33 BeforeEach(func() {
34 A = makeStats("A", 1, 2, 3, 4)
35 B = makeStats("B", 2, 3, 4, 1)
36 C = makeStats("C", 3, 4, 1, 2)
37 D = makeStats("D", 4, 1, 2, 3)
38 })
39
40 DescribeTable("ranking by criteria",
41 func(criteria gmeasure.RankingCriteria, expectedOrder func() []gmeasure.Stats) {
42 ranking := gmeasure.RankStats(criteria, A, B, C, D)
43 expected := expectedOrder()
44 Ί(ranking.Winner()).Should(Equal(expected[0]))
45 Ί(ranking.Stats).Should(Equal(expected))
46 },
47 Entry("entry", gmeasure.LowerMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, D, A, B} }),
48 Entry("entry", gmeasure.HigherMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, A, D, C} }),
49 Entry("entry", gmeasure.LowerMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, C, D, A} }),
50 Entry("entry", gmeasure.HigherMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, D, C, B} }),
51 Entry("entry", gmeasure.LowerMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, B, C, D} }),
52 Entry("entry", gmeasure.HigherMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, C, B, A} }),
53 Entry("entry", gmeasure.LowerMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, A, B, C} }),
54 Entry("entry", gmeasure.HigherMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, B, A, D} }),
55 )
56
57 Describe("Generating Reports", func() {
58 It("can generate an unstyled report", func() {
59 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
60 Ί(ranking.String()).Should(Equal(strings.Join([]string{
61 "Ranking Criteria: Lower Mean is Better",
62 "Experiment | Name | N | Min | Median | Mean | StdDev | Max ",
63 "==================================================================",
64 "Exp-C | C | 100 | 3.00 | 2.00 | 1.00 | 2.00 | 4.00",
65 "*Winner* | *Winner* | | | | | | ",
66 "------------------------------------------------------------------",
67 "Exp-D | D | 100 | 4.00 | 3.00 | 2.00 | 2.00 | 1.00",
68 "------------------------------------------------------------------",
69 "Exp-A | A | 100 | 1.00 | 4.00 | 3.00 | 2.00 | 2.00",
70 "------------------------------------------------------------------",
71 "Exp-B | B | 100 | 2.00 | 1.00 | 4.00 | 2.00 | 3.00",
72 "",
73 }, "\n")))
74 })
75
76 It("can generate a styled report", func() {
77 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
78 Ί(ranking.ColorableString()).Should(Equal(strings.Join([]string{
79 "{{bold}}Ranking Criteria: Lower Mean is Better",
80 "{{/}}{{bold}}Experiment{{/}} | {{bold}}Name {{/}} | {{bold}}N {{/}} | {{bold}}Min {{/}} | {{bold}}Median{{/}} | {{bold}}Mean{{/}} | {{bold}}StdDev{{/}} | {{bold}}Max {{/}}",
81 "==================================================================",
82 "{{bold}}Exp-C {{/}} | {{bold}}C {{/}} | {{bold}}100{{/}} | {{bold}}3.00{{/}} | {{bold}}2.00 {{/}} | {{bold}}1.00{{/}} | {{bold}}2.00 {{/}} | {{bold}}4.00{{/}}",
83 "{{bold}}*Winner* {{/}} | {{bold}}*Winner*{{/}} | | | | | | ",
84 "------------------------------------------------------------------",
85 "Exp-D | D | 100 | 4.00 | 3.00 | 2.00 | 2.00 | 1.00",
86 "------------------------------------------------------------------",
87 "Exp-A | A | 100 | 1.00 | 4.00 | 3.00 | 2.00 | 2.00",
88 "------------------------------------------------------------------",
89 "Exp-B | B | 100 | 2.00 | 1.00 | 4.00 | 2.00 | 3.00",
90 "",
91 }, "\n")))
92 })
93 })
94 })
95
96 Describe("Ranking Durations", func() {
97 makeStats := func(name string, min time.Duration, max time.Duration, mean time.Duration, median time.Duration) gmeasure.Stats {
98 return gmeasure.Stats{
99 Type: gmeasure.StatsTypeDuration,
100 ExperimentName: "Exp-" + name,
101 MeasurementName: name,
102 N: 100,
103 PrecisionBundle: gmeasure.Precision(time.Millisecond * 100),
104 DurationBundle: map[gmeasure.Stat]time.Duration{
105 gmeasure.StatMin: min,
106 gmeasure.StatMax: max,
107 gmeasure.StatMean: mean,
108 gmeasure.StatMedian: median,
109 gmeasure.StatStdDev: 2.0,
110 },
111 }
112 }
113
114 BeforeEach(func() {
115 A = makeStats("A", 1*time.Second, 2*time.Second, 3*time.Second, 4*time.Second)
116 B = makeStats("B", 2*time.Second, 3*time.Second, 4*time.Second, 1*time.Second)
117 C = makeStats("C", 3*time.Second, 4*time.Second, 1*time.Second, 2*time.Second)
118 D = makeStats("D", 4*time.Second, 1*time.Second, 2*time.Second, 3*time.Second)
119 })
120
121 DescribeTable("ranking by criteria",
122 func(criteria gmeasure.RankingCriteria, expectedOrder func() []gmeasure.Stats) {
123 ranking := gmeasure.RankStats(criteria, A, B, C, D)
124 expected := expectedOrder()
125 Ί(ranking.Winner()).Should(Equal(expected[0]))
126 Ί(ranking.Stats).Should(Equal(expected))
127 },
128 Entry("entry", gmeasure.LowerMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, D, A, B} }),
129 Entry("entry", gmeasure.HigherMeanIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, A, D, C} }),
130 Entry("entry", gmeasure.LowerMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{B, C, D, A} }),
131 Entry("entry", gmeasure.HigherMedianIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, D, C, B} }),
132 Entry("entry", gmeasure.LowerMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{A, B, C, D} }),
133 Entry("entry", gmeasure.HigherMinIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, C, B, A} }),
134 Entry("entry", gmeasure.LowerMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{D, A, B, C} }),
135 Entry("entry", gmeasure.HigherMaxIsBetter, func() []gmeasure.Stats { return []gmeasure.Stats{C, B, A, D} }),
136 )
137
138 Describe("Generating Reports", func() {
139 It("can generate an unstyled report", func() {
140 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
141 Ί(ranking.String()).Should(Equal(strings.Join([]string{
142 "Ranking Criteria: Lower Mean is Better",
143 "Experiment | Name | N | Min | Median | Mean | StdDev | Max",
144 "================================================================",
145 "Exp-C | C | 100 | 3s | 2s | 1s | 0s | 4s ",
146 "*Winner* | *Winner* | | | | | | ",
147 "----------------------------------------------------------------",
148 "Exp-D | D | 100 | 4s | 3s | 2s | 0s | 1s ",
149 "----------------------------------------------------------------",
150 "Exp-A | A | 100 | 1s | 4s | 3s | 0s | 2s ",
151 "----------------------------------------------------------------",
152 "Exp-B | B | 100 | 2s | 1s | 4s | 0s | 3s ",
153 "",
154 }, "\n")))
155 })
156
157 It("can generate a styled report", func() {
158 ranking := gmeasure.RankStats(gmeasure.LowerMeanIsBetter, A, B, C, D)
159 Ί(ranking.ColorableString()).Should(Equal(strings.Join([]string{
160 "{{bold}}Ranking Criteria: Lower Mean is Better",
161 "{{/}}{{bold}}Experiment{{/}} | {{bold}}Name {{/}} | {{bold}}N {{/}} | {{bold}}Min{{/}} | {{bold}}Median{{/}} | {{bold}}Mean{{/}} | {{bold}}StdDev{{/}} | {{bold}}Max{{/}}",
162 "================================================================",
163 "{{bold}}Exp-C {{/}} | {{bold}}C {{/}} | {{bold}}100{{/}} | {{bold}}3s {{/}} | {{bold}}2s {{/}} | {{bold}}1s {{/}} | {{bold}}0s {{/}} | {{bold}}4s {{/}}",
164 "{{bold}}*Winner* {{/}} | {{bold}}*Winner*{{/}} | | | | | | ",
165 "----------------------------------------------------------------",
166 "Exp-D | D | 100 | 4s | 3s | 2s | 0s | 1s ",
167 "----------------------------------------------------------------",
168 "Exp-A | A | 100 | 1s | 4s | 3s | 0s | 2s ",
169 "----------------------------------------------------------------",
170 "Exp-B | B | 100 | 2s | 1s | 4s | 0s | 3s ",
171 "",
172 }, "\n")))
173 })
174 })
175 })
176
177 })
0 package gmeasure
1
2 import (
3 "fmt"
4 "time"
5
6 "github.com/onsi/gomega/gmeasure/table"
7 )
8
9 /*
10 Stat is an enum representing the statistics you can request of a Stats struct
11 */
12 type Stat uint
13
14 const (
15 StatInvalid Stat = iota
16 StatMin
17 StatMax
18 StatMean
19 StatMedian
20 StatStdDev
21 )
22
23 var statEnumSupport = newEnumSupport(map[uint]string{uint(StatInvalid): "INVALID STAT", uint(StatMin): "Min", uint(StatMax): "Max", uint(StatMean): "Mean", uint(StatMedian): "Median", uint(StatStdDev): "StdDev"})
24
25 func (s Stat) String() string { return statEnumSupport.String(uint(s)) }
26 func (s *Stat) UnmarshalJSON(b []byte) error {
27 out, err := statEnumSupport.UnmarshJSON(b)
28 *s = Stat(out)
29 return err
30 }
31 func (s Stat) MarshalJSON() ([]byte, error) { return statEnumSupport.MarshJSON(uint(s)) }
32
33 type StatsType uint
34
35 const (
36 StatsTypeInvalid StatsType = iota
37 StatsTypeValue
38 StatsTypeDuration
39 )
40
41 var statsTypeEnumSupport = newEnumSupport(map[uint]string{uint(StatsTypeInvalid): "INVALID STATS TYPE", uint(StatsTypeValue): "StatsTypeValue", uint(StatsTypeDuration): "StatsTypeDuration"})
42
43 func (s StatsType) String() string { return statsTypeEnumSupport.String(uint(s)) }
44 func (s *StatsType) UnmarshalJSON(b []byte) error {
45 out, err := statsTypeEnumSupport.UnmarshJSON(b)
46 *s = StatsType(out)
47 return err
48 }
49 func (s StatsType) MarshalJSON() ([]byte, error) { return statsTypeEnumSupport.MarshJSON(uint(s)) }
50
51 /*
52 Stats records the key statistics for a given measurement. You generally don't make Stats directly - but you can fetch them from Experiments using GetStats() and from Measurements using Stats().
53
54 When using Ginkgo, you can register Measurements as Report Entries via AddReportEntry. This will emit all the captured data points when Ginkgo generates the report.
55 */
56 type Stats struct {
57 // Type is the StatType - one of StatTypeDuration or StatTypeValue
58 Type StatsType
59
60 // ExperimentName is the name of the Experiment that recorded the Measurement from which this Stat is derived
61 ExperimentName string
62
63 // MeasurementName is the name of the Measurement from which this Stat is derived
64 MeasurementName string
65
66 // Units captures the Units of the Measurement from which this Stat is derived
67 Units string
68
69 // Style captures the Style of the Measurement from which this Stat is derived
70 Style string
71
72 // PrecisionBundle captures the precision to use when rendering data for this Measurement.
73 // If Type is StatTypeDuration then PrecisionBundle.Duration is used to round any durations before presentation.
74 // If Type is StatTypeValue then PrecisionBundle.ValueFormat is used to format any values before presentation
75 PrecisionBundle PrecisionBundle
76
77 // N represents the total number of data points in the Meassurement from which this Stat is derived
78 N int
79
80 // If Type is StatTypeValue, ValueBundle will be populated with float64s representing this Stat's statistics
81 ValueBundle map[Stat]float64
82
83 // If Type is StatTypeDuration, DurationBundle will be populated with float64s representing this Stat's statistics
84 DurationBundle map[Stat]time.Duration
85
86 // AnnotationBundle is populated with Annotations corresponding to the data points that can be associated with a Stat.
87 // For example AnnotationBundle[StatMin] will return the Annotation for the data point that has the minimum value/duration.
88 AnnotationBundle map[Stat]string
89 }
90
91 // String returns a minimal summary of the stats of the form "MIN < [MEDIAN] | <MEAN> ÂąSTDDEV < MAX"
92 func (s Stats) String() string {
93 return fmt.Sprintf("%s < [%s] | <%s> Âą%s < %s", s.StringFor(StatMin), s.StringFor(StatMedian), s.StringFor(StatMean), s.StringFor(StatStdDev), s.StringFor(StatMax))
94 }
95
96 // ValueFor returns the float64 value for a particular Stat. You should only use this if the Stats has Type StatsTypeValue
97 // For example:
98 //
99 // median := experiment.GetStats("length").ValueFor(gmeasure.StatMedian)
100 //
101 // will return the median data point for the "length" Measurement.
102 func (s Stats) ValueFor(stat Stat) float64 {
103 return s.ValueBundle[stat]
104 }
105
106 // DurationFor returns the time.Duration for a particular Stat. You should only use this if the Stats has Type StatsTypeDuration
107 // For example:
108 //
109 // mean := experiment.GetStats("runtime").ValueFor(gmeasure.StatMean)
110 //
111 // will return the mean duration for the "runtime" Measurement.
112 func (s Stats) DurationFor(stat Stat) time.Duration {
113 return s.DurationBundle[stat]
114 }
115
116 // FloatFor returns a float64 representation of the passed-in Stat.
117 // When Type is StatsTypeValue this is equivalent to s.ValueFor(stat).
118 // When Type is StatsTypeDuration this is equivalent to float64(s.DurationFor(stat))
119 func (s Stats) FloatFor(stat Stat) float64 {
120 switch s.Type {
121 case StatsTypeValue:
122 return s.ValueFor(stat)
123 case StatsTypeDuration:
124 return float64(s.DurationFor(stat))
125 }
126 return 0
127 }
128
129 // StringFor returns a formatted string representation of the passed-in Stat.
130 // The formatting honors the precision directives provided in stats.PrecisionBundle
131 func (s Stats) StringFor(stat Stat) string {
132 switch s.Type {
133 case StatsTypeValue:
134 return fmt.Sprintf(s.PrecisionBundle.ValueFormat, s.ValueFor(stat))
135 case StatsTypeDuration:
136 return s.DurationFor(stat).Round(s.PrecisionBundle.Duration).String()
137 }
138 return ""
139 }
140
141 func (s Stats) cells() []table.Cell {
142 out := []table.Cell{}
143 out = append(out, table.C(fmt.Sprintf("%d", s.N)))
144 for _, stat := range []Stat{StatMin, StatMedian, StatMean, StatStdDev, StatMax} {
145 content := s.StringFor(stat)
146 if s.AnnotationBundle[stat] != "" {
147 content += "\n" + s.AnnotationBundle[stat]
148 }
149 out = append(out, table.C(content))
150 }
151 return out
152 }
0 package gmeasure_test
1
2 import (
3 "time"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/gmeasure"
8 )
9
10 var _ = Describe("Stats", func() {
11 var stats gmeasure.Stats
12
13 Describe("Stats representing values", func() {
14 BeforeEach(func() {
15 stats = gmeasure.Stats{
16 Type: gmeasure.StatsTypeValue,
17 ExperimentName: "My Test Experiment",
18 MeasurementName: "Sprockets",
19 Units: "widgets",
20 N: 100,
21 PrecisionBundle: gmeasure.Precision(2),
22 ValueBundle: map[gmeasure.Stat]float64{
23 gmeasure.StatMin: 17.48992,
24 gmeasure.StatMax: 293.4820,
25 gmeasure.StatMean: 187.3023,
26 gmeasure.StatMedian: 87.2235,
27 gmeasure.StatStdDev: 73.6394,
28 },
29 }
30 })
31
32 Describe("String()", func() {
33 It("returns a one-line summary", func() {
34 Ί(stats.String()).Should(Equal("17.49 < [87.22] | <187.30> ¹73.64 < 293.48"))
35 })
36 })
37
38 Describe("ValueFor()", func() {
39 It("returns the value for the requested stat", func() {
40 Ί(stats.ValueFor(gmeasure.StatMin)).Should(Equal(17.48992))
41 Ί(stats.ValueFor(gmeasure.StatMean)).Should(Equal(187.3023))
42 })
43 })
44
45 Describe("FloatFor", func() {
46 It("returns the requested stat as a float", func() {
47 Ί(stats.FloatFor(gmeasure.StatMin)).Should(Equal(17.48992))
48 Ί(stats.FloatFor(gmeasure.StatMean)).Should(Equal(187.3023))
49 })
50 })
51
52 Describe("StringFor", func() {
53 It("returns the requested stat rendered with the configured precision", func() {
54 Ί(stats.StringFor(gmeasure.StatMin)).Should(Equal("17.49"))
55 Ί(stats.StringFor(gmeasure.StatMean)).Should(Equal("187.30"))
56 })
57 })
58 })
59
60 Describe("Stats representing durations", func() {
61 BeforeEach(func() {
62 stats = gmeasure.Stats{
63 Type: gmeasure.StatsTypeDuration,
64 ExperimentName: "My Test Experiment",
65 MeasurementName: "Runtime",
66 N: 100,
67 PrecisionBundle: gmeasure.Precision(time.Millisecond * 100),
68 DurationBundle: map[gmeasure.Stat]time.Duration{
69 gmeasure.StatMin: 17375 * time.Millisecond,
70 gmeasure.StatMax: 890321 * time.Millisecond,
71 gmeasure.StatMean: 328712 * time.Millisecond,
72 gmeasure.StatMedian: 552390 * time.Millisecond,
73 gmeasure.StatStdDev: 186259 * time.Millisecond,
74 },
75 }
76 })
77 Describe("String()", func() {
78 It("returns a one-line summary", func() {
79 Ί(stats.String()).Should(Equal("17.4s < [9m12.4s] | <5m28.7s> ¹3m6.3s < 14m50.3s"))
80 })
81 })
82 Describe("DurationFor()", func() {
83 It("returns the duration for the requested stat", func() {
84 Ί(stats.DurationFor(gmeasure.StatMin)).Should(Equal(17375 * time.Millisecond))
85 Ί(stats.DurationFor(gmeasure.StatMean)).Should(Equal(328712 * time.Millisecond))
86 })
87 })
88
89 Describe("FloatFor", func() {
90 It("returns the float64 representation for the requested duration stat", func() {
91 Ί(stats.FloatFor(gmeasure.StatMin)).Should(Equal(float64(17375 * time.Millisecond)))
92 Ί(stats.FloatFor(gmeasure.StatMean)).Should(Equal(float64(328712 * time.Millisecond)))
93 })
94 })
95
96 Describe("StringFor", func() {
97 It("returns the requested stat rendered with the configured precision", func() {
98 Ί(stats.StringFor(gmeasure.StatMin)).Should(Equal("17.4s"))
99 Ί(stats.StringFor(gmeasure.StatMean)).Should(Equal("5m28.7s"))
100 })
101 })
102 })
103 })
0 package gmeasure
1
2 import "time"
3
4 /*
5 Stopwatch provides a convenient abstraction for recording durations. There are two ways to make a Stopwatch:
6
7 You can make a Stopwatch from an Experiment via experiment.NewStopwatch(). This is how you first get a hold of a Stopwatch.
8
9 You can subsequently call stopwatch.NewStopwatch() to get a fresh Stopwatch.
10 This is only necessary if you need to record durations on a different goroutine as a single Stopwatch is not considered thread-safe.
11
12 The Stopwatch starts as soon as it is created. You can Pause() the stopwatch and Reset() it as needed.
13
14 Stopwatches refer back to their parent Experiment. They use this reference to record any measured durations back with the Experiment.
15 */
16 type Stopwatch struct {
17 Experiment *Experiment
18 t time.Time
19 pauseT time.Time
20 pauseDuration time.Duration
21 running bool
22 }
23
24 func newStopwatch(experiment *Experiment) *Stopwatch {
25 return &Stopwatch{
26 Experiment: experiment,
27 t: time.Now(),
28 running: true,
29 }
30 }
31
32 /*
33 NewStopwatch returns a new Stopwatch pointing to the same Experiment as this Stopwatch
34 */
35 func (s *Stopwatch) NewStopwatch() *Stopwatch {
36 return newStopwatch(s.Experiment)
37 }
38
39 /*
40 Record captures the amount of time that has passed since the Stopwatch was created or most recently Reset(). It records the duration on it's associated Experiment in a Measurement with the passed-in name.
41
42 Record takes all the decorators that experiment.RecordDuration takes (e.g. Annotation("...") can be used to annotate this duration)
43
44 Note that Record does not Reset the Stopwatch. It does, however, return the Stopwatch so the following pattern is common:
45
46 stopwatch := experiment.NewStopwatch()
47 // first expensive operation
48 stopwatch.Record("first operation").Reset() //records the duration of the first operation and resets the stopwatch.
49 // second expensive operation
50 stopwatch.Record("second operation").Reset() //records the duration of the second operation and resets the stopwatch.
51
52 omitting the Reset() after the first operation would cause the duration recorded for the second operation to include the time elapsed by both the first _and_ second operations.
53
54 The Stopwatch must be running (i.e. not paused) when Record is called.
55 */
56 func (s *Stopwatch) Record(name string, args ...interface{}) *Stopwatch {
57 if !s.running {
58 panic("stopwatch is not running - call Resume or Reset before calling Record")
59 }
60 duration := time.Since(s.t) - s.pauseDuration
61 s.Experiment.RecordDuration(name, duration, args...)
62 return s
63 }
64
65 /*
66 Reset resets the Stopwatch. Subsequent recorded durations will measure the time elapsed from the moment Reset was called.
67 If the Stopwatch was Paused it is unpaused after calling Reset.
68 */
69 func (s *Stopwatch) Reset() *Stopwatch {
70 s.running = true
71 s.t = time.Now()
72 s.pauseDuration = 0
73 return s
74 }
75
76 /*
77 Pause pauses the Stopwatch. While pasued the Stopwatch does not accumulate elapsed time. This is useful for ignoring expensive operations that are incidental to the behavior you are attempting to characterize.
78 Note: You must call Resume() before you can Record() subsequent measurements.
79
80 For example:
81
82 stopwatch := experiment.NewStopwatch()
83 // first expensive operation
84 stopwatch.Record("first operation").Reset()
85 // second expensive operation - part 1
86 stopwatch.Pause()
87 // something expensive that we don't care about
88 stopwatch.Resume()
89 // second expensive operation - part 2
90 stopwatch.Record("second operation").Reset() // the recorded duration captures the time elapsed during parts 1 and 2 of the second expensive operation, but not the bit in between
91
92
93 The Stopwatch must be running when Pause is called.
94 */
95 func (s *Stopwatch) Pause() *Stopwatch {
96 if !s.running {
97 panic("stopwatch is not running - call Resume or Reset before calling Pause")
98 }
99 s.running = false
100 s.pauseT = time.Now()
101 return s
102 }
103
104 /*
105 Resume resumes a paused Stopwatch. Any time that elapses after Resume is called will be accumulated as elapsed time when a subsequent duration is Recorded.
106
107 The Stopwatch must be Paused when Resume is called
108 */
109 func (s *Stopwatch) Resume() *Stopwatch {
110 if s.running {
111 panic("stopwatch is running - call Pause before calling Resume")
112 }
113 s.running = true
114 s.pauseDuration = s.pauseDuration + time.Since(s.pauseT)
115 return s
116 }
0 package gmeasure_test
1
2 import (
3 "time"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 "github.com/onsi/gomega/gmeasure"
8 )
9
10 var _ = Describe("Stopwatch", func() {
11 var e *gmeasure.Experiment
12 var stopwatch *gmeasure.Stopwatch
13
14 BeforeEach(func() {
15 e = gmeasure.NewExperiment("My Test Experiment")
16 stopwatch = e.NewStopwatch()
17 })
18
19 It("records durations", func() {
20 time.Sleep(100 * time.Millisecond)
21 stopwatch.Record("recordings", gmeasure.Annotation("A"))
22 time.Sleep(100 * time.Millisecond)
23 stopwatch.Record("recordings", gmeasure.Annotation("B")).Reset()
24 time.Sleep(100 * time.Millisecond)
25 stopwatch.Record("recordings", gmeasure.Annotation("C")).Reset()
26 time.Sleep(100 * time.Millisecond)
27 stopwatch.Pause()
28 time.Sleep(100 * time.Millisecond)
29 stopwatch.Resume()
30 time.Sleep(100 * time.Millisecond)
31 stopwatch.Pause()
32 time.Sleep(100 * time.Millisecond)
33 stopwatch.Resume()
34 time.Sleep(100 * time.Millisecond)
35 stopwatch.Record("recordings", gmeasure.Annotation("D"))
36 durations := e.Get("recordings").Durations
37 annotations := e.Get("recordings").Annotations
38 Ί(annotations).Should(Equal([]string{"A", "B", "C", "D"}))
39 Ί(durations[0]).Should(BeNumerically("~", 100*time.Millisecond, 50*time.Millisecond))
40 Ί(durations[1]).Should(BeNumerically("~", 200*time.Millisecond, 50*time.Millisecond))
41 Ί(durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 50*time.Millisecond))
42 Ί(durations[3]).Should(BeNumerically("~", 300*time.Millisecond, 50*time.Millisecond))
43
44 })
45
46 It("panics when asked to record but not running", func() {
47 stopwatch.Pause()
48 Ί(func() {
49 stopwatch.Record("A")
50 }).Should(PanicWith("stopwatch is not running - call Resume or Reset before calling Record"))
51 })
52
53 It("panics when paused but not running", func() {
54 stopwatch.Pause()
55 Ί(func() {
56 stopwatch.Pause()
57 }).Should(PanicWith("stopwatch is not running - call Resume or Reset before calling Pause"))
58 })
59
60 It("panics when asked to resume but not paused", func() {
61 Ί(func() {
62 stopwatch.Resume()
63 }).Should(PanicWith("stopwatch is running - call Pause before calling Resume"))
64 })
65 })
0 package table
1
2 // This is a temporary package - Table will move to github.com/onsi/consolable once some more dust settles
3
4 import (
5 "reflect"
6 "strings"
7 "unicode/utf8"
8 )
9
10 type AlignType uint
11
12 const (
13 AlignTypeLeft AlignType = iota
14 AlignTypeCenter
15 AlignTypeRight
16 )
17
18 type Divider string
19
20 type Row struct {
21 Cells []Cell
22 Divider string
23 Style string
24 }
25
26 func R(args ...interface{}) *Row {
27 r := &Row{
28 Divider: "-",
29 }
30 for _, arg := range args {
31 switch reflect.TypeOf(arg) {
32 case reflect.TypeOf(Divider("")):
33 r.Divider = string(arg.(Divider))
34 case reflect.TypeOf(r.Style):
35 r.Style = arg.(string)
36 case reflect.TypeOf(Cell{}):
37 r.Cells = append(r.Cells, arg.(Cell))
38 }
39 }
40 return r
41 }
42
43 func (r *Row) AppendCell(cells ...Cell) *Row {
44 r.Cells = append(r.Cells, cells...)
45 return r
46 }
47
48 func (r *Row) Render(widths []int, totalWidth int, tableStyle TableStyle, isLastRow bool) string {
49 out := ""
50 if len(r.Cells) == 1 {
51 out += strings.Join(r.Cells[0].render(totalWidth, r.Style, tableStyle), "\n") + "\n"
52 } else {
53 if len(r.Cells) != len(widths) {
54 panic("row vs width mismatch")
55 }
56 renderedCells := make([][]string, len(r.Cells))
57 maxHeight := 0
58 for colIdx, cell := range r.Cells {
59 renderedCells[colIdx] = cell.render(widths[colIdx], r.Style, tableStyle)
60 if len(renderedCells[colIdx]) > maxHeight {
61 maxHeight = len(renderedCells[colIdx])
62 }
63 }
64 for colIdx := range r.Cells {
65 for len(renderedCells[colIdx]) < maxHeight {
66 renderedCells[colIdx] = append(renderedCells[colIdx], strings.Repeat(" ", widths[colIdx]))
67 }
68 }
69 border := strings.Repeat(" ", tableStyle.Padding)
70 if tableStyle.VerticalBorders {
71 border += "|" + border
72 }
73 for lineIdx := 0; lineIdx < maxHeight; lineIdx++ {
74 for colIdx := range r.Cells {
75 out += renderedCells[colIdx][lineIdx]
76 if colIdx < len(r.Cells)-1 {
77 out += border
78 }
79 }
80 out += "\n"
81 }
82 }
83 if tableStyle.HorizontalBorders && !isLastRow && r.Divider != "" {
84 out += strings.Repeat(string(r.Divider), totalWidth) + "\n"
85 }
86
87 return out
88 }
89
90 type Cell struct {
91 Contents []string
92 Style string
93 Align AlignType
94 }
95
96 func C(contents string, args ...interface{}) Cell {
97 c := Cell{
98 Contents: strings.Split(contents, "\n"),
99 }
100 for _, arg := range args {
101 switch reflect.TypeOf(arg) {
102 case reflect.TypeOf(c.Style):
103 c.Style = arg.(string)
104 case reflect.TypeOf(c.Align):
105 c.Align = arg.(AlignType)
106 }
107 }
108 return c
109 }
110
111 func (c Cell) Width() (int, int) {
112 w, minW := 0, 0
113 for _, line := range c.Contents {
114 lineWidth := utf8.RuneCountInString(line)
115 if lineWidth > w {
116 w = lineWidth
117 }
118 for _, word := range strings.Split(line, " ") {
119 wordWidth := utf8.RuneCountInString(word)
120 if wordWidth > minW {
121 minW = wordWidth
122 }
123 }
124 }
125 return w, minW
126 }
127
128 func (c Cell) alignLine(line string, width int) string {
129 lineWidth := utf8.RuneCountInString(line)
130 if lineWidth == width {
131 return line
132 }
133 if lineWidth < width {
134 gap := width - lineWidth
135 switch c.Align {
136 case AlignTypeLeft:
137 return line + strings.Repeat(" ", gap)
138 case AlignTypeRight:
139 return strings.Repeat(" ", gap) + line
140 case AlignTypeCenter:
141 leftGap := gap / 2
142 rightGap := gap - leftGap
143 return strings.Repeat(" ", leftGap) + line + strings.Repeat(" ", rightGap)
144 }
145 }
146 return line
147 }
148
149 func (c Cell) splitWordToWidth(word string, width int) []string {
150 out := []string{}
151 n, subWord := 0, ""
152 for _, c := range word {
153 subWord += string(c)
154 n += 1
155 if n == width-1 {
156 out = append(out, subWord+"-")
157 n, subWord = 0, ""
158 }
159 }
160 return out
161 }
162
163 func (c Cell) splitToWidth(line string, width int) []string {
164 lineWidth := utf8.RuneCountInString(line)
165 if lineWidth <= width {
166 return []string{line}
167 }
168
169 outLines := []string{}
170 words := strings.Split(line, " ")
171 outWords := []string{words[0]}
172 length := utf8.RuneCountInString(words[0])
173 if length > width {
174 splitWord := c.splitWordToWidth(words[0], width)
175 lastIdx := len(splitWord) - 1
176 outLines = append(outLines, splitWord[:lastIdx]...)
177 outWords = []string{splitWord[lastIdx]}
178 length = utf8.RuneCountInString(splitWord[lastIdx])
179 }
180
181 for _, word := range words[1:] {
182 wordLength := utf8.RuneCountInString(word)
183 if length+wordLength+1 <= width {
184 length += wordLength + 1
185 outWords = append(outWords, word)
186 continue
187 }
188 outLines = append(outLines, strings.Join(outWords, " "))
189
190 outWords = []string{word}
191 length = wordLength
192 if length > width {
193 splitWord := c.splitWordToWidth(word, width)
194 lastIdx := len(splitWord) - 1
195 outLines = append(outLines, splitWord[:lastIdx]...)
196 outWords = []string{splitWord[lastIdx]}
197 length = utf8.RuneCountInString(splitWord[lastIdx])
198 }
199 }
200 if len(outWords) > 0 {
201 outLines = append(outLines, strings.Join(outWords, " "))
202 }
203
204 return outLines
205 }
206
207 func (c Cell) render(width int, style string, tableStyle TableStyle) []string {
208 out := []string{}
209 for _, line := range c.Contents {
210 out = append(out, c.splitToWidth(line, width)...)
211 }
212 for idx := range out {
213 out[idx] = c.alignLine(out[idx], width)
214 }
215
216 if tableStyle.EnableTextStyling {
217 style = style + c.Style
218 if style != "" {
219 for idx := range out {
220 out[idx] = style + out[idx] + "{{/}}"
221 }
222 }
223 }
224
225 return out
226 }
227
228 type TableStyle struct {
229 Padding int
230 VerticalBorders bool
231 HorizontalBorders bool
232 MaxTableWidth int
233 MaxColWidth int
234 EnableTextStyling bool
235 }
236
237 var DefaultTableStyle = TableStyle{
238 Padding: 1,
239 VerticalBorders: true,
240 HorizontalBorders: true,
241 MaxTableWidth: 120,
242 MaxColWidth: 40,
243 EnableTextStyling: true,
244 }
245
246 type Table struct {
247 Rows []*Row
248
249 TableStyle TableStyle
250 }
251
252 func NewTable() *Table {
253 return &Table{
254 TableStyle: DefaultTableStyle,
255 }
256 }
257
258 func (t *Table) AppendRow(row *Row) *Table {
259 t.Rows = append(t.Rows, row)
260 return t
261 }
262
263 func (t *Table) Render() string {
264 out := ""
265 totalWidth, widths := t.computeWidths()
266 for rowIdx, row := range t.Rows {
267 out += row.Render(widths, totalWidth, t.TableStyle, rowIdx == len(t.Rows)-1)
268 }
269 return out
270 }
271
272 func (t *Table) computeWidths() (int, []int) {
273 nCol := 0
274 for _, row := range t.Rows {
275 if len(row.Cells) > nCol {
276 nCol = len(row.Cells)
277 }
278 }
279
280 // lets compute the contribution to width from the borders + padding
281 borderWidth := t.TableStyle.Padding
282 if t.TableStyle.VerticalBorders {
283 borderWidth += 1 + t.TableStyle.Padding
284 }
285 totalBorderWidth := borderWidth * (nCol - 1)
286
287 // lets compute the width of each column
288 widths := make([]int, nCol)
289 minWidths := make([]int, nCol)
290 for colIdx := range widths {
291 for _, row := range t.Rows {
292 if colIdx >= len(row.Cells) {
293 // ignore rows with fewer columns
294 continue
295 }
296 w, minWid := row.Cells[colIdx].Width()
297 if w > widths[colIdx] {
298 widths[colIdx] = w
299 }
300 if minWid > minWidths[colIdx] {
301 minWidths[colIdx] = minWid
302 }
303 }
304 }
305
306 // do we already fit?
307 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth {
308 // yes! we're done
309 return sum(widths) + totalBorderWidth, widths
310 }
311
312 // clamp the widths and minWidths to MaxColWidth
313 for colIdx := range widths {
314 widths[colIdx] = min(widths[colIdx], t.TableStyle.MaxColWidth)
315 minWidths[colIdx] = min(minWidths[colIdx], t.TableStyle.MaxColWidth)
316 }
317
318 // do we fit now?
319 if sum(widths)+totalBorderWidth <= t.TableStyle.MaxTableWidth {
320 // yes! we're done
321 return sum(widths) + totalBorderWidth, widths
322 }
323
324 // hmm... still no... can we possibly squeeze the table in without violating minWidths?
325 if sum(minWidths)+totalBorderWidth >= t.TableStyle.MaxTableWidth {
326 // nope - we're just going to have to exceed MaxTableWidth
327 return sum(minWidths) + totalBorderWidth, minWidths
328 }
329
330 // looks like we don't fit yet, but we should be able to fit without violating minWidths
331 // lets start scaling down
332 n := 0
333 for sum(widths)+totalBorderWidth > t.TableStyle.MaxTableWidth {
334 budget := t.TableStyle.MaxTableWidth - totalBorderWidth
335 baseline := sum(widths)
336
337 for colIdx := range widths {
338 widths[colIdx] = max((widths[colIdx]*budget)/baseline, minWidths[colIdx])
339 }
340 n += 1
341 if n > 100 {
342 break // in case we somehow fail to converge
343 }
344 }
345
346 return sum(widths) + totalBorderWidth, widths
347 }
348
349 func sum(s []int) int {
350 out := 0
351 for _, v := range s {
352 out += v
353 }
354 return out
355 }
356
357 func min(a int, b int) int {
358 if a < b {
359 return a
360 }
361 return b
362 }
363
364 func max(a int, b int) int {
365 if a > b {
366 return a
367 }
368 return b
369 }
22 go 1.14
33
44 require (
5 github.com/golang/protobuf v1.4.2
6 github.com/onsi/ginkgo v1.12.1
7 golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0
8 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
9 gopkg.in/yaml.v2 v2.3.0
5 github.com/golang/protobuf v1.5.2
6 github.com/onsi/ginkgo v1.16.2
7 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
8 gopkg.in/yaml.v2 v2.4.0
109 )
0 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
0 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
2 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
3 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
4 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
5 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
36 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
47 github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
58 github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
69 github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
710 github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
811 github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
9 github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
1012 github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
13 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
14 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
15 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
1116 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
1217 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=
1418 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=
19 github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
20 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
1621 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
17 github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=
1822 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
19 github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
23 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
24 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
2025 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=
2226 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
27 github.com/onsi/ginkgo v1.16.2 h1:HFB2fbVIlhIfCfOW81bZFbiC/RvnpXSdhbF2/DJr134=
28 github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
2329 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
30 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
31 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
32 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
33 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
34 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
2435 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
36 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
2537 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=
38 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
2739 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
2840 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=
41 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
3042 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=
43 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
44 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
45 golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
3446 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=
47 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
48 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
3649 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3750 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
3851 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
3952 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=
53 golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4154 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=
4355 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4456 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
45 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
57 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
58 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
59 golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
60 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
61 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
4662 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
47 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
4863 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
64 golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
65 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
4966 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=
67 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
68 golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
69 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
70 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5171 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
72 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
73 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5274 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
5375 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
5476 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
5577 google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
5678 google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
57 google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
5879 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
80 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
81 google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
82 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
5983 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
6084 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=
6285 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
6386 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
6487 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
65 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
88 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
6689 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
67 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
6890 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
91 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
92 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
2323 "github.com/onsi/gomega/types"
2424 )
2525
26 const GOMEGA_VERSION = "1.10.3"
26 const GOMEGA_VERSION = "1.13.0"
2727
2828 const nilFailHandlerPanic = `You are trying to make an assertion, but Gomega's fail handler is nil.
2929 If you're using Ginkgo then you probably forgot to put your assertion in an It().
66 "fmt"
77 "reflect"
88 "runtime/debug"
9 "strconv"
910
1011 "github.com/onsi/gomega/format"
1112 errorsutil "github.com/onsi/gomega/gstruct/errors"
2324 // "b": Equal("b"),
2425 // }))
2526 func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher {
27 return &ElementsMatcher{
28 Identifier: identifier,
29 Elements: elements,
30 }
31 }
32
33 //MatchAllElementsWithIndex succeeds if every element of a slice matches the element matcher it maps to
34 //through the id with index function, and every element matcher is matched.
35 // idFn := func(index int, element interface{}) string {
36 // return strconv.Itoa(index)
37 // }
38 //
39 // Expect([]string{"a", "b"}).To(MatchAllElements(idFn, Elements{
40 // "0": Equal("a"),
41 // "1": Equal("b"),
42 // }))
43 func MatchAllElementsWithIndex(identifier IdentifierWithIndex, elements Elements) types.GomegaMatcher {
2644 return &ElementsMatcher{
2745 Identifier: identifier,
2846 Elements: elements,
4664 // "d": Equal("d"),
4765 // }))
4866 func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher {
67 return &ElementsMatcher{
68 Identifier: identifier,
69 Elements: elements,
70 IgnoreExtras: options&IgnoreExtras != 0,
71 IgnoreMissing: options&IgnoreMissing != 0,
72 AllowDuplicates: options&AllowDuplicates != 0,
73 }
74 }
75
76 //MatchElementsWithIndex succeeds if each element of a slice matches the element matcher it maps to
77 //through the id with index function. It can ignore extra elements and/or missing elements.
78 // idFn := func(index int, element interface{}) string {
79 // return strconv.Itoa(index)
80 // }
81 //
82 // Expect([]string{"a", "b", "c"}).To(MatchElements(idFn, IgnoreExtras, Elements{
83 // "0": Equal("a"),
84 // "1": Equal("b"),
85 // }))
86 // Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing, Elements{
87 // "0": Equal("a"),
88 // "1": Equal("b"),
89 // "2": Equal("c"),
90 // "3": Equal("d"),
91 // }))
92 func MatchElementsWithIndex(identifier IdentifierWithIndex, options Options, elements Elements) types.GomegaMatcher {
4993 return &ElementsMatcher{
5094 Identifier: identifier,
5195 Elements: elements,
62106 // Matchers for each element.
63107 Elements Elements
64108 // Function mapping an element to the string key identifying its matcher.
65 Identifier Identifier
109 Identifier Identify
66110
67111 // Whether to ignore extra elements or consider it an error.
68112 IgnoreExtras bool
81125 // Function for identifying (mapping) elements.
82126 type Identifier func(element interface{}) string
83127
128 // Calls the underlying fucntion with the provided params.
129 // Identifier drops the index.
130 func (i Identifier) WithIndexAndElement(index int, element interface{}) string {
131 return i(element)
132 }
133
134 // Uses the index and element to generate an element name
135 type IdentifierWithIndex func(index int, element interface{}) string
136
137 // Calls the underlying fucntion with the provided params.
138 // IdentifierWithIndex uses the index.
139 func (i IdentifierWithIndex) WithIndexAndElement(index int, element interface{}) string {
140 return i(index, element)
141 }
142
143 // Interface for identifing the element
144 type Identify interface {
145 WithIndexAndElement(i int, element interface{}) string
146 }
147
148 // IndexIdentity is a helper function for using an index as
149 // the key in the element map
150 func IndexIdentity(index int, _ interface{}) string {
151 return strconv.Itoa(index)
152 }
153
84154 func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) {
85155 if reflect.TypeOf(actual).Kind() != reflect.Slice {
86156 return false, fmt.Errorf("%v is type %T, expected slice", actual, actual)
105175 elements := map[string]bool{}
106176 for i := 0; i < val.Len(); i++ {
107177 element := val.Index(i).Interface()
108 id := m.Identifier(element)
178 id := m.Identifier.WithIndexAndElement(i, element)
109179 if elements[id] {
110180 if !m.AllowDuplicates {
111181 errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
136136 Expect(nils).Should(m, "should allow an uninitialized slice")
137137 })
138138 })
139
140 Context("with index identifier", func() {
141 allElements := []string{"a", "b"}
142 missingElements := []string{"a"}
143 extraElements := []string{"a", "b", "c"}
144 duplicateElements := []string{"a", "a", "b"}
145 empty := []string{}
146 var nils []string
147
148 It("should use index", func() {
149 m := MatchAllElementsWithIndex(IndexIdentity, Elements{
150 "0": Equal("a"),
151 "1": Equal("b"),
152 })
153 Expect(allElements).Should(m, "should match all elements")
154 Expect(missingElements).ShouldNot(m, "should fail with missing elements")
155 Expect(extraElements).ShouldNot(m, "should fail with extra elements")
156 Expect(duplicateElements).ShouldNot(m, "should fail with duplicate elements")
157 Expect(nils).ShouldNot(m, "should fail with an uninitialized slice")
158
159 m = MatchAllElementsWithIndex(IndexIdentity, Elements{
160 "0": Equal("a"),
161 "1": Equal("fail"),
162 })
163 Expect(allElements).ShouldNot(m, "should run nested matchers")
164
165 m = MatchAllElementsWithIndex(IndexIdentity, Elements{})
166 Expect(empty).Should(m, "should handle empty slices")
167 Expect(allElements).ShouldNot(m, "should handle only empty slices")
168 Expect(nils).Should(m, "should handle nil slices")
169 })
170 })
139171 })
140172
141173 func id(element interface{}) string {
0 package defaults_test
1
2 import (
3 "testing"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 )
8
9 func TestDefaults(t *testing.T) {
10 RegisterFailHandler(Fail)
11 RunSpecs(t, "Gomega Defaults Suite")
12 }
0 package defaults
1
2 import (
3 "fmt"
4 "time"
5 )
6
7 func SetDurationFromEnv(getDurationFromEnv func(string) string, varSetter func(time.Duration), name string) {
8 durationFromEnv := getDurationFromEnv(name)
9
10 if len(durationFromEnv) == 0 {
11 return
12 }
13
14 duration, err := time.ParseDuration(durationFromEnv)
15
16 if err != nil {
17 panic(fmt.Sprintf("Expected a duration when using %s! Parse error %v", name, err))
18 }
19
20 varSetter(duration)
21 }
0 package defaults_test
1
2 import (
3 "time"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7
8 d "github.com/onsi/gomega/internal/defaults"
9 )
10
11 var _ = Describe("Durations", func() {
12 var (
13 duration *time.Duration
14 envVarGot string
15 envVarToReturn string
16
17 getDurationFromEnv = func(name string) string {
18 envVarGot = name
19 return envVarToReturn
20 }
21
22 setDuration = func(t time.Duration) {
23 duration = &t
24 }
25
26 setDurationCalled = func() bool {
27 return duration != nil
28 }
29
30 resetDuration = func() {
31 duration = nil
32 }
33 )
34
35 BeforeEach(func() {
36 resetDuration()
37 })
38
39 Context("When the environment has a duration", func() {
40 Context("When the duration is valid", func() {
41 BeforeEach(func() {
42 envVarToReturn = "10m"
43
44 d.SetDurationFromEnv(getDurationFromEnv, setDuration, "MY_ENV_VAR")
45 })
46
47 It("sets the duration", func() {
48 Expect(envVarGot).To(Equal("MY_ENV_VAR"))
49 Expect(setDurationCalled()).To(Equal(true))
50 Expect(*duration).To(Equal(10 * time.Minute))
51 })
52 })
53
54 Context("When the duration is not valid", func() {
55 BeforeEach(func() {
56 envVarToReturn = "10"
57 })
58
59 It("panics with a helpful error message", func() {
60 Expect(func() {
61 d.SetDurationFromEnv(getDurationFromEnv, setDuration, "MY_ENV_VAR")
62 }).To(PanicWith(MatchRegexp("Expected a duration when using MY_ENV_VAR")))
63 })
64 })
65 })
66
67 Context("When the environment does not have a duration", func() {
68 BeforeEach(func() {
69 envVarToReturn = ""
70
71 d.SetDurationFromEnv(getDurationFromEnv, setDuration, "MY_ENV_VAR")
72 })
73
74 It("does not set the duration", func() {
75 Expect(envVarGot).To(Equal("MY_ENV_VAR"))
76 Expect(setDurationCalled()).To(Equal(false))
77 Expect(duration).To(BeNil())
78 })
79 })
80 })
1717 return false, fmt.Errorf("BeElement matcher expects actual to be typed")
1818 }
1919
20 length := len(matcher.Elements)
21 valueAt := func(i int) interface{} {
22 return matcher.Elements[i]
23 }
24 // Special handling of a single element of type Array or Slice
25 if length == 1 && isArrayOrSlice(valueAt(0)) {
26 element := valueAt(0)
27 value := reflect.ValueOf(element)
28 length = value.Len()
29 valueAt = func(i int) interface{} {
30 return value.Index(i).Interface()
31 }
32 }
33
3420 var lastError error
35 for i := 0; i < length; i++ {
36 matcher := &EqualMatcher{Expected: valueAt(i)}
21 for _, m := range flatten(matcher.Elements) {
22 matcher := &EqualMatcher{Expected: m}
3723 success, err := matcher.Match(actual)
3824 if err != nil {
3925 lastError = err
4834 }
4935
5036 func (matcher *BeElementOfMatcher) FailureMessage(actual interface{}) (message string) {
51 return format.Message(actual, "to be an element of", matcher.Elements)
37 return format.Message(actual, "to be an element of", presentable(matcher.Elements))
5238 }
5339
5440 func (matcher *BeElementOfMatcher) NegatedFailureMessage(actual interface{}) (message string) {
55 return format.Message(actual, "not to be an element of", matcher.Elements)
41 return format.Message(actual, "not to be an element of", presentable(matcher.Elements))
5642 }
3232 })
3333
3434 When("passed a correctly typed nil", func() {
35 It("should operate succesfully on the passed in value", func() {
35 It("should operate successfully on the passed in value", func() {
3636 var nilSlice []int
3737 Expect(1).ShouldNot(BeElementOf(nilSlice))
3838
5555
5656 It("builds failure message", func() {
5757 actual := BeElementOf(1, 2).FailureMessage(123)
58 Expect(actual).To(Equal("Expected\n <int>: 123\nto be an element of\n <[]interface {} | len:2, cap:2>: [1, 2]"))
58 Expect(actual).To(Equal("Expected\n <int>: 123\nto be an element of\n <[]int | len:2, cap:2>: [1, 2]"))
5959 })
6060
6161 It("builds negated failure message", func() {
6262 actual := BeElementOf(1, 2).NegatedFailureMessage(123)
63 Expect(actual).To(Equal("Expected\n <int>: 123\nnot to be an element of\n <[]interface {} | len:2, cap:2>: [1, 2]"))
63 Expect(actual).To(Equal("Expected\n <int>: 123\nnot to be an element of\n <[]int | len:2, cap:2>: [1, 2]"))
6464 })
6565 })
4444 return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[0], 1))
4545 }
4646 if len(matcher.CompareTo) == 2 && !isNumber(matcher.CompareTo[1]) {
47 return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[0], 1))
47 return false, fmt.Errorf("Expected a number. Got:\n%s", format.Object(matcher.CompareTo[1], 1))
4848 }
4949
5050 switch matcher.Comparator {
142142 success, err = (&BeNumericallyMatcher{Comparator: "~", CompareTo: []interface{}{3.0, "foo"}}).Match(5.0)
143143 Expect(success).Should(BeFalse())
144144 Expect(err).Should(HaveOccurred())
145 Expect(err.Error()).Should(ContainSubstring("foo"))
145146
146147 success, err = (&BeNumericallyMatcher{Comparator: "==", CompareTo: []interface{}{"bar"}}).Match(5)
147148 Expect(success).Should(BeFalse())
5656 return
5757 }
5858
59 func matchers(expectedElems []interface{}) (matchers []interface{}) {
60 elems := expectedElems
61 if len(expectedElems) == 1 && isArrayOrSlice(expectedElems[0]) {
62 elems = []interface{}{}
63 value := reflect.ValueOf(expectedElems[0])
64 for i := 0; i < value.Len(); i++ {
65 elems = append(elems, value.Index(i).Interface())
66 }
59 func flatten(elems []interface{}) []interface{} {
60 if len(elems) != 1 || !isArrayOrSlice(elems[0]) {
61 return elems
6762 }
6863
69 for _, e := range elems {
64 value := reflect.ValueOf(elems[0])
65 flattened := make([]interface{}, value.Len())
66 for i := 0; i < value.Len(); i++ {
67 flattened[i] = value.Index(i).Interface()
68 }
69 return flattened
70 }
71
72 func matchers(expectedElems []interface{}) (matchers []interface{}) {
73 for _, e := range flatten(expectedElems) {
7074 matcher, isMatcher := e.(omegaMatcher)
7175 if !isMatcher {
7276 matcher = &EqualMatcher{Expected: e}
7478 matchers = append(matchers, matcher)
7579 }
7680 return
81 }
82
83 func presentable(elems []interface{}) interface{} {
84 elems = flatten(elems)
85
86 if len(elems) == 0 {
87 return []interface{}{}
88 }
89
90 sv := reflect.ValueOf(elems)
91 tt := sv.Index(0).Elem().Type()
92 for i := 1; i < sv.Len(); i++ {
93 if sv.Index(i).Elem().Type() != tt {
94 return elems
95 }
96 }
97
98 ss := reflect.MakeSlice(reflect.SliceOf(tt), sv.Len(), sv.Len())
99 for i := 0; i < sv.Len(); i++ {
100 ss.Index(i).Set(sv.Index(i).Elem())
101 }
102
103 return ss.Interface()
77104 }
78105
79106 func valuesOf(actual interface{}) []interface{} {
94121 }
95122
96123 func (matcher *ConsistOfMatcher) FailureMessage(actual interface{}) (message string) {
97 message = format.Message(actual, "to consist of", matcher.Elements)
124 message = format.Message(actual, "to consist of", presentable(matcher.Elements))
98125 message = appendMissingElements(message, matcher.missingElements)
99126 if len(matcher.extraElements) > 0 {
100127 message = fmt.Sprintf("%s\nthe extra elements were\n%s", message,
101 format.Object(matcher.extraElements, 1))
128 format.Object(presentable(matcher.extraElements), 1))
102129 }
103130 return
104131 }
108135 return message
109136 }
110137 return fmt.Sprintf("%s\nthe missing elements were\n%s", message,
111 format.Object(missingElements, 1))
138 format.Object(presentable(missingElements), 1))
112139 }
113140
114141 func (matcher *ConsistOfMatcher) NegatedFailureMessage(actual interface{}) (message string) {
115 return format.Message(actual, "not to consist of", matcher.Elements)
142 return format.Message(actual, "not to consist of", presentable(matcher.Elements))
116143 }
106106 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
107107 })
108108 })
109
110 When("expected was specified as an array", func() {
111 It("flattens the array in the expectation message", func() {
112 failures := InterceptGomegaFailures(func() {
113 Expect([]string{"A", "B", "C"}).To(ConsistOf([]string{"A", "B"}))
114 })
115
116 expected := `Expected\n.*\["A", "B", "C"\]\nto consist of\n.*: \["A", "B"\]\nthe extra elements were\n.*\["C"\]`
117 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
118 })
119
120 It("flattens the array in the negated expectation message", func() {
121 failures := InterceptGomegaFailures(func() {
122 Expect([]string{"A", "B"}).NotTo(ConsistOf([]string{"A", "B"}))
123 })
124
125 expected := `Expected\n.*\["A", "B"\]\nnot to consist of\n.*: \["A", "B"\]`
126 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
127 })
128 })
129
130 When("the expected values are the same type", func() {
131 It("uses that type for the expectation slice", func() {
132 failures := InterceptGomegaFailures(func() {
133 Expect([]string{"A", "B"}).To(ConsistOf("A", "C"))
134 })
135
136 expected := `to consist of
137 \s*<\[\]string \| len:2, cap:2>: \["A", "C"\]
138 the missing elements were
139 \s*<\[\]string \| len:1, cap:1>: \["C"\]
140 the extra elements were
141 \s*<\[\]string \| len:1, cap:1>: \["B"\]`
142 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
143 })
144
145 It("uses that type for the negated expectation slice", func() {
146 failures := InterceptGomegaFailures(func() {
147 Expect([]uint64{1, 2}).NotTo(ConsistOf(uint64(1), uint64(2)))
148 })
149
150 expected := `not to consist of\n\s*<\[\]uint64 \| len:2, cap:2>: \[1, 2\]`
151 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
152 })
153 })
154
155 When("the expected values are different types", func() {
156 It("uses interface{} for the expectation slice", func() {
157 failures := InterceptGomegaFailures(func() {
158 Expect([]interface{}{1, true}).To(ConsistOf(1, "C"))
159 })
160
161 expected := `to consist of
162 \s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"C"\]
163 the missing elements were
164 \s*<\[\]string \| len:1, cap:1>: \["C"\]
165 the extra elements were
166 \s*<\[\]bool \| len:1, cap:1>: \[true\]`
167 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
168 })
169
170 It("uses interface{} for the negated expectation slice", func() {
171 failures := InterceptGomegaFailures(func() {
172 Expect([]interface{}{1, "B"}).NotTo(ConsistOf(1, "B"))
173 })
174
175 expected := `not to consist of\n\s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"B"\]`
176 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
177 })
178 })
109179 })
110180 })
3434 }
3535
3636 func (matcher *ContainElementsMatcher) FailureMessage(actual interface{}) (message string) {
37 message = format.Message(actual, "to contain elements", matcher.Elements)
37 message = format.Message(actual, "to contain elements", presentable(matcher.Elements))
3838 return appendMissingElements(message, matcher.missingElements)
3939 }
4040
4141 func (matcher *ContainElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
42 return format.Message(actual, "not to contain elements", matcher.Elements)
42 return format.Message(actual, "not to contain elements", presentable(matcher.Elements))
4343 }
8181 expected := "Expected\n.*\\[2\\]\nto contain elements\n.*\\[1, 2, 3\\]\nthe missing elements were\n.*\\[1, 3\\]"
8282 Expect(failures).To(ContainElements(MatchRegexp(expected)))
8383 })
84
85 When("expected was specified as an array", func() {
86 It("flattens the array in the expectation message", func() {
87 failures := InterceptGomegaFailures(func() {
88 Expect([]string{"A", "B", "C"}).To(ContainElements([]string{"A", "D"}))
89 })
90
91 expected := `Expected\n.*\["A", "B", "C"\]\nto contain elements\n.*: \["A", "D"\]\nthe missing elements were\n.*\["D"\]`
92 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
93 })
94
95 It("flattens the array in the negated expectation message", func() {
96 failures := InterceptGomegaFailures(func() {
97 Expect([]string{"A", "B"}).NotTo(ContainElements([]string{"A", "B"}))
98 })
99
100 expected := `Expected\n.*\["A", "B"\]\nnot to contain elements\n.*: \["A", "B"\]`
101 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
102 })
103 })
104
105 When("the expected values are the same type", func() {
106 It("uses that type for the expectation slice", func() {
107 failures := InterceptGomegaFailures(func() {
108 Expect([]string{"A", "B"}).To(ContainElements("A", "B", "C"))
109 })
110
111 expected := `to contain elements
112 \s*<\[\]string \| len:3, cap:3>: \["A", "B", "C"\]
113 the missing elements were
114 \s*<\[\]string \| len:1, cap:1>: \["C"\]`
115 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
116 })
117
118 It("uses that type for the negated expectation slice", func() {
119 failures := InterceptGomegaFailures(func() {
120 Expect([]uint64{1, 2}).NotTo(ContainElements(uint64(1), uint64(2)))
121 })
122
123 expected := `not to contain elements\n\s*<\[\]uint64 \| len:2, cap:2>: \[1, 2\]`
124 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
125 })
126 })
127
128 When("the expected values are different types", func() {
129 It("uses interface{} for the expectation slice", func() {
130 failures := InterceptGomegaFailures(func() {
131 Expect([]interface{}{1, true}).To(ContainElements(1, "C"))
132 })
133
134 expected := `to contain elements
135 \s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"C"\]
136 the missing elements were
137 \s*<\[\]string \| len:1, cap:1>: \["C"\]`
138 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
139 })
140
141 It("uses interface{} for the negated expectation slice", func() {
142 failures := InterceptGomegaFailures(func() {
143 Expect([]interface{}{1, "B"}).NotTo(ContainElements(1, "B"))
144 })
145
146 expected := `not to contain elements\n\s*<\[\]interface {} \| len:2, cap:2>: \[<int>1, <string>"B"\]`
147 Expect(failures).To(ConsistOf(MatchRegexp(expected)))
148 })
149 })
84150 })
85151 })
00 package matchers
11
22 import (
3 "errors"
34 "fmt"
45 "reflect"
56
67 "github.com/onsi/gomega/format"
7 "golang.org/x/xerrors"
88 )
99
1010 type MatchErrorMatcher struct {
2424 expected := matcher.Expected
2525
2626 if isError(expected) {
27 return reflect.DeepEqual(actualErr, expected) || xerrors.Is(actualErr, expected.(error)), nil
27 return reflect.DeepEqual(actualErr, expected) || errors.Is(actualErr, expected.(error)), nil
2828 }
2929
3030 if isString(expected) {
66 . "github.com/onsi/ginkgo"
77 . "github.com/onsi/gomega"
88 . "github.com/onsi/gomega/matchers"
9 "golang.org/x/xerrors"
109 )
1110
1211 type CustomError struct {
3332
3433 It("should succeed when any error in the chain matches the passed error", func() {
3534 innerErr := errors.New("inner error")
36 outerErr := xerrors.Errorf("outer error wrapping: %w", innerErr)
35 outerErr := fmt.Errorf("outer error wrapping: %w", innerErr)
3736
3837 Expect(outerErr).Should(MatchError(innerErr))
3938 })
0 package matchers
1
2 import (
3 "fmt"
4 "reflect"
5
6 "github.com/onsi/gomega/format"
7 )
8
9 type SatisfyMatcher struct {
10 Predicate interface{}
11
12 // cached type
13 predicateArgType reflect.Type
14 }
15
16 func NewSatisfyMatcher(predicate interface{}) *SatisfyMatcher {
17 if predicate == nil {
18 panic("predicate cannot be nil")
19 }
20 predicateType := reflect.TypeOf(predicate)
21 if predicateType.Kind() != reflect.Func {
22 panic("predicate must be a function")
23 }
24 if predicateType.NumIn() != 1 {
25 panic("predicate must have 1 argument")
26 }
27 if predicateType.NumOut() != 1 || predicateType.Out(0).Kind() != reflect.Bool {
28 panic("predicate must return bool")
29 }
30
31 return &SatisfyMatcher{
32 Predicate: predicate,
33 predicateArgType: predicateType.In(0),
34 }
35 }
36
37 func (m *SatisfyMatcher) Match(actual interface{}) (success bool, err error) {
38 // prepare a parameter to pass to the predicate
39 var param reflect.Value
40 if actual != nil && reflect.TypeOf(actual).AssignableTo(m.predicateArgType) {
41 // The dynamic type of actual is compatible with the predicate argument.
42 param = reflect.ValueOf(actual)
43
44 } else if actual == nil && m.predicateArgType.Kind() == reflect.Interface {
45 // The dynamic type of actual is unknown, so there's no way to make its
46 // reflect.Value. Create a nil of the predicate argument, which is known.
47 param = reflect.Zero(m.predicateArgType)
48
49 } else {
50 return false, fmt.Errorf("predicate expects '%s' but we have '%T'", m.predicateArgType, actual)
51 }
52
53 // call the predicate with `actual`
54 fn := reflect.ValueOf(m.Predicate)
55 result := fn.Call([]reflect.Value{param})
56 return result[0].Bool(), nil
57 }
58
59 func (m *SatisfyMatcher) FailureMessage(actual interface{}) (message string) {
60 return format.Message(actual, "to satisfy predicate", m.Predicate)
61 }
62
63 func (m *SatisfyMatcher) NegatedFailureMessage(actual interface{}) (message string) {
64 return format.Message(actual, "to not satisfy predicate", m.Predicate)
65 }
0 package matchers_test
1
2 import (
3 "errors"
4
5 . "github.com/onsi/ginkgo"
6 . "github.com/onsi/gomega"
7 )
8
9 var _ = Describe("SatisfyMatcher", func() {
10
11 var isEven = func(x int) bool { return x%2 == 0 }
12
13 Context("Panic if predicate is invalid", func() {
14 panicsWithPredicate := func(predicate interface{}) {
15 ExpectWithOffset(1, func() { Satisfy(predicate) }).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 })
3939 }
4040
4141 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)
42 // prepare a parameter to pass to the Transform function
43 var param reflect.Value
44 if actual != nil && reflect.TypeOf(actual).AssignableTo(m.transformArgType) {
45 // The dynamic type of actual is compatible with the transform argument.
46 param = reflect.ValueOf(actual)
47
48 } else if actual == nil && m.transformArgType.Kind() == reflect.Interface {
49 // The dynamic type of actual is unknown, so there's no way to make its
50 // reflect.Value. Create a nil of the transform argument, which is known.
51 param = reflect.Zero(m.transformArgType)
52
53 } else {
54 return false, fmt.Errorf("Transform function expects '%s' but we have '%T'", m.transformArgType, actual)
4655 }
4756
4857 // call the Transform function with `actual`
4958 fn := reflect.ValueOf(m.Transform)
50 result := fn.Call([]reflect.Value{reflect.ValueOf(actual)})
59 result := fn.Call([]reflect.Value{param})
5160 m.transformedValue = result[0].Interface() // expect exactly one value
5261
5362 return m.Matcher.Match(m.transformedValue)
3636 })
3737 })
3838
39 When("the actual value is incompatible", func() {
40 It("fails to pass int to func(string)", func() {
41 actual, transform := int(0), func(string) int { return 0 }
42 success, err := WithTransform(transform, Equal(0)).Match(actual)
43 Expect(success).To(BeFalse())
44 Expect(err).To(HaveOccurred())
45 Expect(err.Error()).To(ContainSubstring("function expects 'string'"))
46 Expect(err.Error()).To(ContainSubstring("have 'int'"))
47 })
48
49 It("fails to pass string to func(interface)", func() {
50 actual, transform := "bang", func(error) int { return 0 }
51 success, err := WithTransform(transform, Equal(0)).Match(actual)
52 Expect(success).To(BeFalse())
53 Expect(err).To(HaveOccurred())
54 Expect(err.Error()).To(ContainSubstring("function expects 'error'"))
55 Expect(err.Error()).To(ContainSubstring("have 'string'"))
56 })
57
58 It("fails to pass nil interface to func(int)", func() {
59 actual, transform := error(nil), func(int) int { return 0 }
60 success, err := WithTransform(transform, Equal(0)).Match(actual)
61 Expect(success).To(BeFalse())
62 Expect(err).To(HaveOccurred())
63 Expect(err.Error()).To(ContainSubstring("function expects 'int'"))
64 Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
65 })
66
67 It("fails to pass nil interface to func(pointer)", func() {
68 actual, transform := error(nil), func(*string) int { return 0 }
69 success, err := WithTransform(transform, Equal(0)).Match(actual)
70 Expect(success).To(BeFalse())
71 Expect(err).To(HaveOccurred())
72 Expect(err.Error()).To(ContainSubstring("function expects '*string'"))
73 Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
74 })
75 })
76
3977 It("works with positive cases", func() {
4078 Expect(1).To(WithTransform(plus1, Equal(2)))
4179 Expect(1).To(WithTransform(plus1, WithTransform(plus1, Equal(3))))
5088 Expect(S{1, "hi"}).To(WithTransform(transformer, Equal("hi")))
5189
5290 // transform expects interface
53 errString := func(e error) string { return e.Error() }
91 errString := func(e error) string {
92 if e == nil {
93 return "safe"
94 }
95 return e.Error()
96 }
97 Expect(nil).To(WithTransform(errString, Equal("safe")), "handles nil actual values")
5498 Expect(errors.New("abc")).To(WithTransform(errString, Equal("abc")))
5599 })
56100
473473 func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
474474 return matchers.NewWithTransformMatcher(transform, matcher)
475475 }
476
477 //Satisfy matches the actual value against the `predicate` function.
478 //The given predicate must be a function of one paramter that returns bool.
479 // var isEven = func(i int) bool { return i%2 == 0 }
480 // Expect(2).To(Satisfy(isEven))
481 func Satisfy(predicate interface{}) types.GomegaMatcher {
482 return matchers.NewSatisfyMatcher(predicate)
483 }