github.com/onsi/gomega@v1.32.0/gmeasure/measurement.go (about) 1 package gmeasure 2 3 import ( 4 "fmt" 5 "math" 6 "sort" 7 "time" 8 9 "github.com/onsi/gomega/gmeasure/table" 10 ) 11 12 type MeasurementType uint 13 14 const ( 15 MeasurementTypeInvalid MeasurementType = iota 16 MeasurementTypeNote 17 MeasurementTypeDuration 18 MeasurementTypeValue 19 ) 20 21 var letEnumSupport = newEnumSupport(map[uint]string{uint(MeasurementTypeInvalid): "INVALID LOG ENTRY TYPE", uint(MeasurementTypeNote): "Note", uint(MeasurementTypeDuration): "Duration", uint(MeasurementTypeValue): "Value"}) 22 23 func (s MeasurementType) String() string { return letEnumSupport.String(uint(s)) } 24 func (s *MeasurementType) UnmarshalJSON(b []byte) error { 25 out, err := letEnumSupport.UnmarshJSON(b) 26 *s = MeasurementType(out) 27 return err 28 } 29 func (s MeasurementType) MarshalJSON() ([]byte, error) { return letEnumSupport.MarshJSON(uint(s)) } 30 31 /* 32 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(). 33 34 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. 35 */ 36 type Measurement struct { 37 // Type is the MeasurementType - one of MeasurementTypeNote, MeasurementTypeDuration, or MeasurementTypeValue 38 Type MeasurementType 39 40 // ExperimentName is the name of the experiment that this Measurement is associated with 41 ExperimentName string 42 43 // If Type is MeasurementTypeNote, Note is populated with the note text. 44 Note string 45 46 // If Type is MeasurementTypeDuration or MeasurementTypeValue, Name is the name of the recorded measurement 47 Name string 48 49 // Style captures the styling information (if any) for this Measurement 50 Style string 51 52 // Units capture the units (if any) for this Measurement. Units is set to "duration" if the Type is MeasurementTypeDuration 53 Units string 54 55 // PrecisionBundle captures the precision to use when rendering data for this Measurement. 56 // If Type is MeasurementTypeDuration then PrecisionBundle.Duration is used to round any durations before presentation. 57 // If Type is MeasurementTypeValue then PrecisionBundle.ValueFormat is used to format any values before presentation 58 PrecisionBundle PrecisionBundle 59 60 // If Type is MeasurementTypeDuration, Durations will contain all durations recorded for this measurement 61 Durations []time.Duration 62 63 // If Type is MeasurementTypeValue, Values will contain all float64s recorded for this measurement 64 Values []float64 65 66 // If Type is MeasurementTypeDuration or MeasurementTypeValue then Annotations will include string annotations for all recorded Durations or Values. 67 // 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 "" 68 Annotations []string 69 } 70 71 type Measurements []Measurement 72 73 func (m Measurements) IdxWithName(name string) int { 74 for idx, measurement := range m { 75 if measurement.Name == name { 76 return idx 77 } 78 } 79 80 return -1 81 } 82 83 func (m Measurement) report(enableStyling bool) string { 84 out := "" 85 style := m.Style 86 if !enableStyling { 87 style = "" 88 } 89 switch m.Type { 90 case MeasurementTypeNote: 91 out += fmt.Sprintf("%s - Note\n%s\n", m.ExperimentName, m.Note) 92 if style != "" { 93 out = style + out + "{{/}}" 94 } 95 return out 96 case MeasurementTypeValue, MeasurementTypeDuration: 97 out += fmt.Sprintf("%s - %s", m.ExperimentName, m.Name) 98 if m.Units != "" { 99 out += " [" + m.Units + "]" 100 } 101 if style != "" { 102 out = style + out + "{{/}}" 103 } 104 out += "\n" 105 out += m.Stats().String() + "\n" 106 } 107 t := table.NewTable() 108 t.TableStyle.EnableTextStyling = enableStyling 109 switch m.Type { 110 case MeasurementTypeValue: 111 t.AppendRow(table.R(table.C("Value", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style)) 112 for idx := range m.Values { 113 t.AppendRow(table.R( 114 table.C(fmt.Sprintf(m.PrecisionBundle.ValueFormat, m.Values[idx]), table.AlignTypeRight), 115 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft), 116 )) 117 } 118 case MeasurementTypeDuration: 119 t.AppendRow(table.R(table.C("Duration", table.AlignTypeCenter), table.C("Annotation", table.AlignTypeCenter), table.Divider("="), style)) 120 for idx := range m.Durations { 121 t.AppendRow(table.R( 122 table.C(m.Durations[idx].Round(m.PrecisionBundle.Duration).String(), style, table.AlignTypeRight), 123 table.C(m.Annotations[idx], "{{gray}}", table.AlignTypeLeft), 124 )) 125 } 126 } 127 out += t.Render() 128 return out 129 } 130 131 /* 132 ColorableString generates a styled report that includes all the data points for this Measurement. 133 It is called automatically by Ginkgo's reporting infrastructure when the Measurement is registered as a ReportEntry via AddReportEntry. 134 */ 135 func (m Measurement) ColorableString() string { 136 return m.report(true) 137 } 138 139 /* 140 String generates an unstyled report that includes all the data points for this Measurement. 141 */ 142 func (m Measurement) String() string { 143 return m.report(false) 144 } 145 146 /* 147 Stats returns a Stats struct summarizing the statistic of this measurement 148 */ 149 func (m Measurement) Stats() Stats { 150 if m.Type == MeasurementTypeInvalid || m.Type == MeasurementTypeNote { 151 return Stats{} 152 } 153 154 out := Stats{ 155 ExperimentName: m.ExperimentName, 156 MeasurementName: m.Name, 157 Style: m.Style, 158 Units: m.Units, 159 PrecisionBundle: m.PrecisionBundle, 160 } 161 162 switch m.Type { 163 case MeasurementTypeValue: 164 out.Type = StatsTypeValue 165 out.N = len(m.Values) 166 if out.N == 0 { 167 return out 168 } 169 indices, sum := make([]int, len(m.Values)), 0.0 170 for idx, v := range m.Values { 171 indices[idx] = idx 172 sum += v 173 } 174 sort.Slice(indices, func(i, j int) bool { 175 return m.Values[indices[i]] < m.Values[indices[j]] 176 }) 177 out.ValueBundle = map[Stat]float64{ 178 StatMin: m.Values[indices[0]], 179 StatMax: m.Values[indices[out.N-1]], 180 StatMean: sum / float64(out.N), 181 StatStdDev: 0.0, 182 } 183 out.AnnotationBundle = map[Stat]string{ 184 StatMin: m.Annotations[indices[0]], 185 StatMax: m.Annotations[indices[out.N-1]], 186 } 187 188 if out.N%2 == 0 { 189 out.ValueBundle[StatMedian] = (m.Values[indices[out.N/2]] + m.Values[indices[out.N/2-1]]) / 2.0 190 } else { 191 out.ValueBundle[StatMedian] = m.Values[indices[(out.N-1)/2]] 192 } 193 194 for _, v := range m.Values { 195 out.ValueBundle[StatStdDev] += (v - out.ValueBundle[StatMean]) * (v - out.ValueBundle[StatMean]) 196 } 197 out.ValueBundle[StatStdDev] = math.Sqrt(out.ValueBundle[StatStdDev] / float64(out.N)) 198 case MeasurementTypeDuration: 199 out.Type = StatsTypeDuration 200 out.N = len(m.Durations) 201 if out.N == 0 { 202 return out 203 } 204 indices, sum := make([]int, len(m.Durations)), time.Duration(0) 205 for idx, v := range m.Durations { 206 indices[idx] = idx 207 sum += v 208 } 209 sort.Slice(indices, func(i, j int) bool { 210 return m.Durations[indices[i]] < m.Durations[indices[j]] 211 }) 212 out.DurationBundle = map[Stat]time.Duration{ 213 StatMin: m.Durations[indices[0]], 214 StatMax: m.Durations[indices[out.N-1]], 215 StatMean: sum / time.Duration(out.N), 216 } 217 out.AnnotationBundle = map[Stat]string{ 218 StatMin: m.Annotations[indices[0]], 219 StatMax: m.Annotations[indices[out.N-1]], 220 } 221 222 if out.N%2 == 0 { 223 out.DurationBundle[StatMedian] = (m.Durations[indices[out.N/2]] + m.Durations[indices[out.N/2-1]]) / 2 224 } else { 225 out.DurationBundle[StatMedian] = m.Durations[indices[(out.N-1)/2]] 226 } 227 stdDev := 0.0 228 for _, v := range m.Durations { 229 stdDev += float64(v-out.DurationBundle[StatMean]) * float64(v-out.DurationBundle[StatMean]) 230 } 231 out.DurationBundle[StatStdDev] = time.Duration(math.Sqrt(stdDev / float64(out.N))) 232 } 233 234 return out 235 }