Skip to content

Commit 02c9313

Browse files
committed
Test: Enable fuzz tests for native histograms
Signed-off-by: 🌲 Harry 🌊 John 🏔 <[email protected]>
1 parent eb186b2 commit 02c9313

File tree

11 files changed

+431
-67
lines changed

11 files changed

+431
-67
lines changed

engine/engine_test.go

Lines changed: 185 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/thanos-io/promql-engine/engine"
2525
"github.com/thanos-io/promql-engine/execution/model"
2626
"github.com/thanos-io/promql-engine/execution/warnings"
27+
"github.com/thanos-io/promql-engine/extlabels"
2728
"github.com/thanos-io/promql-engine/logicalplan"
2829
"github.com/thanos-io/promql-engine/query"
2930
"github.com/thanos-io/promql-engine/storage/prometheus"
@@ -2314,6 +2315,21 @@ or
23142315
end: time.UnixMilli(124000),
23152316
step: 15 * time.Second,
23162317
},
2318+
{
2319+
name: "fuzz native histogram approx float comparison",
2320+
load: `load 2m
2321+
http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:30 sum:14.00 buckets:[27 2 1]}}+{{schema:0 count:30 buckets:[27 2 1]}}x20
2322+
http_request_duration_seconds{pod="nginx-2"} {{schema:-2 count:58 sum:4368.00 buckets:[54 2 2]}}+{{schema:-2 count:58 buckets:[54 2 2]}}x30`,
2323+
query: `
2324+
-(
2325+
-{__name__="http_request_duration_seconds"}
2326+
/
2327+
histogram_stdvar({__name__="http_request_duration_seconds"})
2328+
)`,
2329+
start: time.UnixMilli(83000),
2330+
end: time.UnixMilli(160000),
2331+
step: time.Minute + 16*time.Second,
2332+
},
23172333
}
23182334

23192335
disableOptimizerOpts := []bool{true, false}
@@ -5995,12 +6011,91 @@ func (b samplesByLabels) Len() int { return len(b) }
59956011
func (b samplesByLabels) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
59966012
func (b samplesByLabels) Less(i, j int) bool { return labels.Compare(b[i].Metric, b[j].Metric) < 0 }
59976013

6014+
const epsilon = 1e-6
6015+
const fraction = 1e-10
6016+
6017+
func floatsMatch(f1, f2 []float64) bool {
6018+
if len(f1) != len(f2) {
6019+
return false
6020+
}
6021+
for i, f := range f1 {
6022+
if !cmp.Equal(f, f2[i], cmpopts.EquateNaNs(), cmpopts.EquateApprox(fraction, epsilon)) {
6023+
return false
6024+
}
6025+
}
6026+
return true
6027+
}
6028+
6029+
// spansMatch returns true if both spans represent the same bucket layout
6030+
// after combining zero length spans with the next non-zero length span.
6031+
// Copied from: https://github.com/prometheus/prometheus/blob/3d245e31d31774f62ff18c36039315fa55fe252c/model/histogram/histogram.go#L287
6032+
func spansMatch(s1, s2 []histogram.Span) bool {
6033+
if len(s1) == 0 && len(s2) == 0 {
6034+
return true
6035+
}
6036+
6037+
s1idx, s2idx := 0, 0
6038+
for {
6039+
if s1idx >= len(s1) {
6040+
return allEmptySpans(s2[s2idx:])
6041+
}
6042+
if s2idx >= len(s2) {
6043+
return allEmptySpans(s1[s1idx:])
6044+
}
6045+
6046+
currS1, currS2 := s1[s1idx], s2[s2idx]
6047+
s1idx++
6048+
s2idx++
6049+
if currS1.Length == 0 {
6050+
// This span is zero length, so we add consecutive such spans
6051+
// until we find a non-zero span.
6052+
for ; s1idx < len(s1) && s1[s1idx].Length == 0; s1idx++ {
6053+
currS1.Offset += s1[s1idx].Offset
6054+
}
6055+
if s1idx < len(s1) {
6056+
currS1.Offset += s1[s1idx].Offset
6057+
currS1.Length = s1[s1idx].Length
6058+
s1idx++
6059+
}
6060+
}
6061+
if currS2.Length == 0 {
6062+
// This span is zero length, so we add consecutive such spans
6063+
// until we find a non-zero span.
6064+
for ; s2idx < len(s2) && s2[s2idx].Length == 0; s2idx++ {
6065+
currS2.Offset += s2[s2idx].Offset
6066+
}
6067+
if s2idx < len(s2) {
6068+
currS2.Offset += s2[s2idx].Offset
6069+
currS2.Length = s2[s2idx].Length
6070+
s2idx++
6071+
}
6072+
}
6073+
6074+
if currS1.Length == 0 && currS2.Length == 0 {
6075+
// The last spans of both set are zero length. Previous spans match.
6076+
return true
6077+
}
6078+
6079+
if currS1.Offset != currS2.Offset || currS1.Length != currS2.Length {
6080+
return false
6081+
}
6082+
}
6083+
}
6084+
6085+
func allEmptySpans(s []histogram.Span) bool {
6086+
for _, ss := range s {
6087+
if ss.Length > 0 {
6088+
return false
6089+
}
6090+
}
6091+
return true
6092+
}
6093+
59986094
var (
59996095
// comparer should be used to compare promql results between engines.
60006096
comparer = cmp.Comparer(func(x, y *promql.Result) bool {
60016097
compareFloats := func(l, r float64) bool {
6002-
const epsilon = 1e-6
6003-
return cmp.Equal(l, r, cmpopts.EquateNaNs(), cmpopts.EquateApprox(0, epsilon))
6098+
return cmp.Equal(l, r, cmpopts.EquateNaNs(), cmpopts.EquateApprox(fraction, epsilon))
60046099
}
60056100
compareHistograms := func(l, r *histogram.FloatHistogram) bool {
60066101
if l == nil && r == nil {
@@ -6011,7 +6106,39 @@ var (
60116106
return false
60126107
}
60136108

6014-
return l.Equals(r)
6109+
// Copied from https://github.com/prometheus/prometheus/blob/3d245e31d31774f62ff18c36039315fa55fe252c/model/histogram/float_histogram.go#L471
6110+
// and extended to use approx comparison instead of exact match.
6111+
if l.Schema != r.Schema || !compareFloats(l.Count, r.Count) || !compareFloats(l.Sum, r.Sum) {
6112+
return false
6113+
}
6114+
6115+
if l.UsesCustomBuckets() {
6116+
if !floatsMatch(l.CustomValues, r.CustomValues) {
6117+
return false
6118+
}
6119+
}
6120+
6121+
if l.ZeroThreshold != r.ZeroThreshold || !compareFloats(l.ZeroCount, r.ZeroCount) {
6122+
return false
6123+
}
6124+
6125+
if !spansMatch(l.NegativeSpans, r.NegativeSpans) {
6126+
return false
6127+
}
6128+
6129+
if !floatsMatch(l.NegativeBuckets, r.NegativeBuckets) {
6130+
return false
6131+
}
6132+
6133+
if !spansMatch(l.PositiveSpans, r.PositiveSpans) {
6134+
return false
6135+
}
6136+
6137+
if !floatsMatch(l.PositiveBuckets, r.PositiveBuckets) {
6138+
return false
6139+
}
6140+
6141+
return true
60156142
}
60166143
compareAnnotations := func(l, r annotations.Annotations) bool {
60176144
// TODO: discard promql annotations for now, once we support them we should add them back
@@ -6038,14 +6165,65 @@ var (
60386165
}
60396166
return true
60406167
}
6168+
compareValueMetrics := func(l, r labels.Labels) (valueMetric bool, equals bool) {
6169+
// For count_value() float values embedded in the labels should be extracted out and compared separately from other labels.
6170+
lLabels := l.Copy()
6171+
rLabels := r.Copy()
6172+
var (
6173+
lVal, rVal string
6174+
lFloat, rFloat float64
6175+
err error
6176+
)
6177+
6178+
if lVal = lLabels.Get("value"); lVal == "" {
6179+
return false, false
6180+
}
6181+
6182+
if rVal = rLabels.Get("value"); rVal == "" {
6183+
return false, false
6184+
}
6185+
6186+
if lFloat, err = strconv.ParseFloat(lVal, 64); err != nil {
6187+
return false, false
6188+
}
6189+
if rFloat, err = strconv.ParseFloat(rVal, 64); err != nil {
6190+
return false, false
6191+
}
6192+
6193+
// Exclude the value label in comparison.
6194+
lLabels = lLabels.MatchLabels(false, "value")
6195+
rLabels = rLabels.MatchLabels(false, "value")
6196+
6197+
if !labels.Equal(lLabels, rLabels) {
6198+
return false, false
6199+
}
6200+
6201+
return true, compareFloats(lFloat, rFloat)
6202+
}
60416203
compareMetrics := func(l, r labels.Labels) bool {
6204+
if valueMetric, equals := compareValueMetrics(l, r); valueMetric {
6205+
return equals
6206+
}
60426207
return l.Hash() == r.Hash()
60436208
}
60446209

6045-
if x.Err != nil && y.Err != nil {
6046-
return cmp.Equal(x.Err.Error(), y.Err.Error())
6047-
} else if x.Err != nil || y.Err != nil {
6048-
return false
6210+
compareErrors := func(l, r error) (stop bool, result bool) {
6211+
if l == nil && r == nil {
6212+
return false, true
6213+
}
6214+
if l != nil && r != nil {
6215+
return true, l.Error() == r.Error()
6216+
}
6217+
err := l
6218+
if err == nil {
6219+
err = r
6220+
}
6221+
// Thanos engine handles duplicate label check differently than Prometheus engine.
6222+
return true, err.Error() == extlabels.ErrDuplicateLabelSet.Error()
6223+
}
6224+
6225+
if stop, result := compareErrors(x.Err, y.Err); stop {
6226+
return result
60496227
}
60506228

60516229
if !compareAnnotations(x.Warnings, y.Warnings) {

0 commit comments

Comments
 (0)