github.com/onsi/gomega@v1.32.0/gmeasure/experiment_test.go (about) 1 package gmeasure_test 2 3 import ( 4 "fmt" 5 "strings" 6 "sync" 7 "time" 8 9 . "github.com/onsi/ginkgo/v2" 10 . "github.com/onsi/gomega" 11 12 "github.com/onsi/gomega/gmeasure" 13 ) 14 15 var _ = Describe("Experiment", func() { 16 var e *gmeasure.Experiment 17 BeforeEach(func() { 18 e = gmeasure.NewExperiment("Test Experiment") 19 }) 20 21 Describe("Recording Notes", func() { 22 It("creates a note Measurement", func() { 23 e.RecordNote("I'm a note", gmeasure.Style("{{blue}}")) 24 measurement := e.Measurements[0] 25 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeNote)) 26 Ω(measurement.ExperimentName).Should(Equal("Test Experiment")) 27 Ω(measurement.Note).Should(Equal("I'm a note")) 28 Ω(measurement.Style).Should(Equal("{{blue}}")) 29 }) 30 }) 31 32 Describe("Recording Durations", func() { 33 commonMeasurementAssertions := func() gmeasure.Measurement { 34 measurement := e.Get("runtime") 35 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeDuration)) 36 Ω(measurement.ExperimentName).Should(Equal("Test Experiment")) 37 Ω(measurement.Name).Should(Equal("runtime")) 38 Ω(measurement.Units).Should(Equal("duration")) 39 Ω(measurement.Style).Should(Equal("{{red}}")) 40 Ω(measurement.PrecisionBundle.Duration).Should(Equal(time.Millisecond)) 41 return measurement 42 } 43 44 BeforeEach(func() { 45 e.RecordDuration("runtime", time.Second, gmeasure.Annotation("first"), gmeasure.Style("{{red}}"), gmeasure.Precision(time.Millisecond), gmeasure.Units("ignored")) 46 }) 47 48 Describe("RecordDuration", func() { 49 It("generates a measurement and records the passed-in duration along with any relevant decorations", func() { 50 e.RecordDuration("runtime", time.Minute, gmeasure.Annotation("second")) 51 measurement := commonMeasurementAssertions() 52 Ω(measurement.Durations).Should(Equal([]time.Duration{time.Second, time.Minute})) 53 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"})) 54 }) 55 }) 56 57 Describe("MeasureDuration", func() { 58 It("measure the duration of the passed-in function", func() { 59 e.MeasureDuration("runtime", func() { 60 time.Sleep(200 * time.Millisecond) 61 }, gmeasure.Annotation("second")) 62 measurement := commonMeasurementAssertions() 63 Ω(measurement.Durations[0]).Should(Equal(time.Second)) 64 Ω(measurement.Durations[1]).Should(BeNumerically("~", 200*time.Millisecond, 20*time.Millisecond)) 65 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"})) 66 }) 67 }) 68 69 Describe("SampleDuration", func() { 70 It("samples the passed-in function according to SampleConfig and records the measured durations", func() { 71 e.SampleDuration("runtime", func(_ int) { 72 time.Sleep(100 * time.Millisecond) 73 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled")) 74 measurement := commonMeasurementAssertions() 75 Ω(measurement.Durations[0]).Should(Equal(time.Second)) 76 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond)) 77 Ω(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond)) 78 Ω(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond)) 79 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"})) 80 }) 81 }) 82 83 Describe("SampleAnnotatedDuration", func() { 84 It("samples the passed-in function according to SampleConfig and records the measured durations and returned annotations", func() { 85 e.SampleAnnotatedDuration("runtime", func(idx int) gmeasure.Annotation { 86 time.Sleep(100 * time.Millisecond) 87 return gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1)) 88 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored")) 89 measurement := commonMeasurementAssertions() 90 Ω(measurement.Durations[0]).Should(Equal(time.Second)) 91 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond)) 92 Ω(measurement.Durations[2]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond)) 93 Ω(measurement.Durations[3]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond)) 94 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"})) 95 }) 96 }) 97 }) 98 99 Describe("Stopwatch Support", func() { 100 It("can generate a new stopwatch tied to the experiment", func() { 101 s := e.NewStopwatch() 102 time.Sleep(50 * time.Millisecond) 103 s.Record("runtime", gmeasure.Annotation("first")).Reset() 104 time.Sleep(100 * time.Millisecond) 105 s.Record("runtime", gmeasure.Annotation("second")).Reset() 106 time.Sleep(150 * time.Millisecond) 107 s.Record("runtime", gmeasure.Annotation("third")) 108 measurement := e.Get("runtime") 109 Ω(measurement.Durations[0]).Should(BeNumerically("~", 50*time.Millisecond, 20*time.Millisecond)) 110 Ω(measurement.Durations[1]).Should(BeNumerically("~", 100*time.Millisecond, 20*time.Millisecond)) 111 Ω(measurement.Durations[2]).Should(BeNumerically("~", 150*time.Millisecond, 20*time.Millisecond)) 112 Ω(measurement.Annotations).Should(Equal([]string{"first", "second", "third"})) 113 }) 114 }) 115 116 Describe("Recording Values", func() { 117 commonMeasurementAssertions := func() gmeasure.Measurement { 118 measurement := e.Get("sprockets") 119 Ω(measurement.Type).Should(Equal(gmeasure.MeasurementTypeValue)) 120 Ω(measurement.ExperimentName).Should(Equal("Test Experiment")) 121 Ω(measurement.Name).Should(Equal("sprockets")) 122 Ω(measurement.Units).Should(Equal("widgets")) 123 Ω(measurement.Style).Should(Equal("{{yellow}}")) 124 Ω(measurement.PrecisionBundle.ValueFormat).Should(Equal("%.0f")) 125 return measurement 126 } 127 128 BeforeEach(func() { 129 e.RecordValue("sprockets", 3.2, gmeasure.Annotation("first"), gmeasure.Style("{{yellow}}"), gmeasure.Precision(0), gmeasure.Units("widgets")) 130 }) 131 132 Describe("RecordValue", func() { 133 It("generates a measurement and records the passed-in value along with any relevant decorations", func() { 134 e.RecordValue("sprockets", 17.4, gmeasure.Annotation("second")) 135 measurement := commonMeasurementAssertions() 136 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4})) 137 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"})) 138 }) 139 }) 140 141 Describe("MeasureValue", func() { 142 It("records the value returned by the passed-in function", func() { 143 e.MeasureValue("sprockets", func() float64 { 144 return 17.4 145 }, gmeasure.Annotation("second")) 146 measurement := commonMeasurementAssertions() 147 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4})) 148 Ω(measurement.Annotations).Should(Equal([]string{"first", "second"})) 149 }) 150 }) 151 152 Describe("SampleValue", func() { 153 It("samples the passed-in function according to SampleConfig and records the resulting values", func() { 154 e.SampleValue("sprockets", func(idx int) float64 { 155 return 17.4 + float64(idx) 156 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("sampled")) 157 measurement := commonMeasurementAssertions() 158 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4})) 159 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled", "sampled", "sampled"})) 160 }) 161 }) 162 163 Describe("SampleAnnotatedValue", func() { 164 It("samples the passed-in function according to SampleConfig and records the returned values and annotations", func() { 165 e.SampleAnnotatedValue("sprockets", func(idx int) (float64, gmeasure.Annotation) { 166 return 17.4 + float64(idx), gmeasure.Annotation(fmt.Sprintf("sampled-%d", idx+1)) 167 }, gmeasure.SamplingConfig{N: 3}, gmeasure.Annotation("ignored")) 168 measurement := commonMeasurementAssertions() 169 Ω(measurement.Values).Should(Equal([]float64{3.2, 17.4, 18.4, 19.4})) 170 Ω(measurement.Annotations).Should(Equal([]string{"first", "sampled-1", "sampled-2", "sampled-3"})) 171 }) 172 }) 173 }) 174 175 Describe("Sampling", func() { 176 var indices []int 177 BeforeEach(func() { 178 indices = []int{} 179 }) 180 181 ints := func(n int) []int { 182 out := []int{} 183 for i := 0; i < n; i++ { 184 out = append(out, i) 185 } 186 return out 187 } 188 189 It("calls the function repeatedly passing in an index", func() { 190 e.Sample(func(idx int) { 191 indices = append(indices, idx) 192 }, gmeasure.SamplingConfig{N: 3}) 193 194 Ω(indices).Should(Equal(ints(3))) 195 }) 196 197 It("can cap the maximum number of samples", func() { 198 e.Sample(func(idx int) { 199 indices = append(indices, idx) 200 }, gmeasure.SamplingConfig{N: 10, Duration: time.Minute}) 201 202 Ω(indices).Should(Equal(ints(10))) 203 }) 204 205 It("can cap the maximum sample time", func() { 206 e.Sample(func(idx int) { 207 indices = append(indices, idx) 208 time.Sleep(10 * time.Millisecond) 209 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond, MinSamplingInterval: 5 * time.Millisecond}) 210 211 Ω(len(indices)).Should(BeNumerically("~", 10, 3)) 212 Ω(indices).Should(Equal(ints(len(indices)))) 213 }) 214 215 It("can ensure a minimum interval between samples", func() { 216 times := map[int]time.Time{} 217 e.Sample(func(idx int) { 218 times[idx] = time.Now() 219 }, gmeasure.SamplingConfig{N: 10, Duration: 200 * time.Millisecond, MinSamplingInterval: 50 * time.Millisecond, NumParallel: 1}) 220 221 Ω(len(times)).Should(BeNumerically("~", 4, 2)) 222 Ω(times[1]).Should(BeTemporally(">", times[0], 50*time.Millisecond)) 223 Ω(times[2]).Should(BeTemporally(">", times[1], 50*time.Millisecond)) 224 }) 225 226 It("can run samples in parallel", func() { 227 lock := &sync.Mutex{} 228 229 e.Sample(func(idx int) { 230 lock.Lock() 231 indices = append(indices, idx) 232 lock.Unlock() 233 time.Sleep(10 * time.Millisecond) 234 }, gmeasure.SamplingConfig{N: 100, Duration: 100 * time.Millisecond, NumParallel: 3}) 235 236 lock.Lock() 237 defer lock.Unlock() 238 Ω(len(indices)).Should(BeNumerically("~", 30, 10)) 239 Ω(indices).Should(ConsistOf(ints(len(indices)))) 240 }) 241 242 It("panics if the SamplingConfig does not specify a ceiling", func() { 243 Expect(func() { 244 e.Sample(func(_ int) {}, gmeasure.SamplingConfig{MinSamplingInterval: time.Second}) 245 }).To(PanicWith("you must specify at least one of SamplingConfig.N and SamplingConfig.Duration")) 246 }) 247 248 It("panics if the SamplingConfig includes both a minimum interval and a directive to run in parallel", func() { 249 Expect(func() { 250 e.Sample(func(_ int) {}, gmeasure.SamplingConfig{N: 10, MinSamplingInterval: time.Second, NumParallel: 2}) 251 }).To(PanicWith("you cannot specify both SamplingConfig.MinSamplingInterval and SamplingConfig.NumParallel")) 252 }) 253 }) 254 255 Describe("recording multiple entries", func() { 256 It("always appends to the correct measurement (by name)", func() { 257 e.RecordDuration("alpha", time.Second) 258 e.RecordDuration("beta", time.Minute) 259 e.RecordValue("gamma", 1) 260 e.RecordValue("delta", 2.71) 261 e.RecordDuration("alpha", 2*time.Second) 262 e.RecordDuration("beta", 2*time.Minute) 263 e.RecordValue("gamma", 2) 264 e.RecordValue("delta", 3.141) 265 266 Ω(e.Measurements).Should(HaveLen(4)) 267 Ω(e.Get("alpha").Durations).Should(Equal([]time.Duration{time.Second, 2 * time.Second})) 268 Ω(e.Get("beta").Durations).Should(Equal([]time.Duration{time.Minute, 2 * time.Minute})) 269 Ω(e.Get("gamma").Values).Should(Equal([]float64{1, 2})) 270 Ω(e.Get("delta").Values).Should(Equal([]float64{2.71, 3.141})) 271 }) 272 273 It("panics if you incorrectly mix types", func() { 274 e.RecordDuration("runtime", time.Second) 275 Ω(func() { 276 e.RecordValue("runtime", 3.141) 277 }).Should(PanicWith("attempting to record value with name 'runtime'. That name is already in-use for recording durations.")) 278 279 e.RecordValue("sprockets", 2) 280 Ω(func() { 281 e.RecordDuration("sprockets", time.Minute) 282 }).Should(PanicWith("attempting to record duration with name 'sprockets'. That name is already in-use for recording values.")) 283 }) 284 }) 285 286 Describe("Decorators", func() { 287 It("uses the default precisions when none is specified", func() { 288 e.RecordValue("sprockets", 2) 289 e.RecordDuration("runtime", time.Minute) 290 291 Ω(e.Get("sprockets").PrecisionBundle.ValueFormat).Should(Equal("%.3f")) 292 Ω(e.Get("runtime").PrecisionBundle.Duration).Should(Equal(100 * time.Microsecond)) 293 }) 294 295 It("panics if an unsupported type is passed into Precision", func() { 296 Ω(func() { 297 gmeasure.Precision("aardvark") 298 }).Should(PanicWith("invalid precision type, must be time.Duration or int")) 299 }) 300 301 It("panics if an unrecognized argumnet is passed in", func() { 302 Ω(func() { 303 e.RecordValue("sprockets", 2, "boom") 304 }).Should(PanicWith(`unrecognized argument "boom"`)) 305 }) 306 }) 307 308 Describe("Getting Measurements", func() { 309 Context("when the Measurement does not exist", func() { 310 It("returns the zero Measurement", func() { 311 Ω(e.Get("not here")).Should(BeZero()) 312 }) 313 }) 314 }) 315 316 Describe("Getting Stats", func() { 317 It("returns the Measurement's Stats", func() { 318 e.RecordValue("alpha", 1) 319 e.RecordValue("alpha", 2) 320 e.RecordValue("alpha", 3) 321 Ω(e.GetStats("alpha")).Should(Equal(e.Get("alpha").Stats())) 322 }) 323 }) 324 325 Describe("Generating Reports", func() { 326 BeforeEach(func() { 327 e.RecordNote("A note") 328 e.RecordValue("sprockets", 7, gmeasure.Units("widgets"), gmeasure.Precision(0), gmeasure.Style("{{yellow}}"), gmeasure.Annotation("sprockets-1")) 329 e.RecordDuration("runtime", time.Second, gmeasure.Precision(100*time.Millisecond), gmeasure.Style("{{red}}"), gmeasure.Annotation("runtime-1")) 330 e.RecordNote("A blue note", gmeasure.Style("{{blue}}")) 331 e.RecordValue("gear ratio", 10.3, gmeasure.Precision(2), gmeasure.Style("{{green}}"), gmeasure.Annotation("ratio-1")) 332 333 e.RecordValue("sprockets", 8, gmeasure.Annotation("sprockets-2")) 334 e.RecordValue("sprockets", 9, gmeasure.Annotation("sprockets-3")) 335 336 e.RecordDuration("runtime", 2*time.Second, gmeasure.Annotation("runtime-2")) 337 e.RecordValue("gear ratio", 13.758, gmeasure.Precision(2), gmeasure.Annotation("ratio-2")) 338 }) 339 340 It("emits a nicely formatted table", func() { 341 expected := strings.Join([]string{ 342 "Test Experiment", 343 "Name | N | Min | Median | Mean | StdDev | Max ", 344 "=============================================================================", 345 "A note ", 346 "-----------------------------------------------------------------------------", 347 "sprockets [widgets] | 3 | 7 | 8 | 8 | 1 | 9 ", 348 " | | sprockets-1 | | | | sprockets-3", 349 "-----------------------------------------------------------------------------", 350 "runtime [duration] | 2 | 1s | 1.5s | 1.5s | 500ms | 2s ", 351 " | | runtime-1 | | | | runtime-2 ", 352 "-----------------------------------------------------------------------------", 353 "A blue note ", 354 "-----------------------------------------------------------------------------", 355 "gear ratio | 2 | 10.30 | 12.03 | 12.03 | 1.73 | 13.76 ", 356 " | | ratio-1 | | | | ratio-2 ", 357 "", 358 }, "\n") 359 Ω(e.String()).Should(Equal(expected)) 360 }) 361 362 It("can also emit a styled table", func() { 363 expected := strings.Join([]string{ 364 "{{bold}}Test Experiment", 365 "{{/}}{{bold}}Name {{/}} | {{bold}}N{{/}} | {{bold}}Min {{/}} | {{bold}}Median{{/}} | {{bold}}Mean {{/}} | {{bold}}StdDev{{/}} | {{bold}}Max {{/}}", 366 "=============================================================================", 367 "A note ", 368 "-----------------------------------------------------------------------------", 369 "{{yellow}}sprockets [widgets]{{/}} | {{yellow}}3{{/}} | {{yellow}}7 {{/}} | {{yellow}}8 {{/}} | {{yellow}}8 {{/}} | {{yellow}}1 {{/}} | {{yellow}}9 {{/}}", 370 " | | {{yellow}}sprockets-1{{/}} | | | | {{yellow}}sprockets-3{{/}}", 371 "-----------------------------------------------------------------------------", 372 "{{red}}runtime [duration] {{/}} | {{red}}2{{/}} | {{red}}1s {{/}} | {{red}}1.5s {{/}} | {{red}}1.5s {{/}} | {{red}}500ms {{/}} | {{red}}2s {{/}}", 373 " | | {{red}}runtime-1 {{/}} | | | | {{red}}runtime-2 {{/}}", 374 "-----------------------------------------------------------------------------", 375 "{{blue}}A blue note {{/}}", 376 "-----------------------------------------------------------------------------", 377 "{{green}}gear ratio {{/}} | {{green}}2{{/}} | {{green}}10.30 {{/}} | {{green}}12.03 {{/}} | {{green}}12.03{{/}} | {{green}}1.73 {{/}} | {{green}}13.76 {{/}}", 378 " | | {{green}}ratio-1 {{/}} | | | | {{green}}ratio-2 {{/}}", 379 "", 380 }, "\n") 381 Ω(e.ColorableString()).Should(Equal(expected)) 382 }) 383 }) 384 })