...

Source file src/github.com/onsi/gomega/gmeasure/experiment_test.go

Documentation: github.com/onsi/gomega/gmeasure

     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  })
   385  

View as plain text