diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e28041ad..cd492c28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - name: Install Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: 1.26.x + go-version: 1.27.x - run: make check_license - run: make style - run: make -C assets style @@ -33,20 +33,18 @@ jobs: - name: Install Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: 1.26.x + go-version: 1.27.x - run: make -C assets test test: name: Test runs-on: ubuntu-latest - env: - # Override Go 1.18 security deprecations. - GODEBUG: "x509sha1=1,tls10default=1" strategy: matrix: go: - 1.25.x - 1.26.x + - 1.27.x steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 45704a95..a2d392e1 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -30,9 +30,9 @@ jobs: with: persist-credentials: false - name: Install Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: 1.26.x + go-version: 1.27.x - name: Install snmp_exporter/generator dependencies run: sudo apt-get update && sudo apt-get -y install libsnmp-dev if: github.repository == 'prometheus/snmp_exporter' diff --git a/Makefile.common b/Makefile.common index cd54cb41..85a36f6d 100644 --- a/Makefile.common +++ b/Makefile.common @@ -61,7 +61,7 @@ PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_ SKIP_GOLANGCI_LINT := GOLANGCI_LINT := GOLANGCI_LINT_OPTS ?= -GOLANGCI_LINT_VERSION ?= v2.12.2 +GOLANGCI_LINT_VERSION ?= v2.13.1 GOLANGCI_FMT_OPTS ?= # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64. # windows isn't included here because of the path separator being different. diff --git a/model/api_bench_test.go b/model/api_bench_test.go new file mode 100644 index 00000000..79c2186f --- /dev/null +++ b/model/api_bench_test.go @@ -0,0 +1,140 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package model + +import ( + "encoding/json" + "strconv" + "testing" + "time" +) + +func generateData(timeseries, datapoints int) (floatMatrix, histogramMatrix Matrix) { + for i := range timeseries { + lset := map[LabelName]LabelValue{ + MetricNameLabel: LabelValue("timeseries_" + strconv.Itoa(i)), + "foo": "bar", + } + now := Time(1677587274055) + floats := make([]SamplePair, datapoints) + histograms := make([]SampleHistogramPair, datapoints) + + for x := datapoints; x > 0; x-- { + f := float64(x) + floats[x-1] = SamplePair{ + // Set the time back assuming a 15s interval. Since this is used for + // Marshal/Unmarshal testing the actual interval doesn't matter. + Timestamp: now.Add(time.Second * -15 * time.Duration(x)), + Value: SampleValue(f), + } + histograms[x-1] = SampleHistogramPair{ + Timestamp: now.Add(time.Second * -15 * time.Duration(x)), + Histogram: &SampleHistogram{ + Count: FloatString(13.5 * f), + Sum: FloatString(.1 * f), + Buckets: HistogramBuckets{ + { + Boundaries: 1, + Lower: -4870.992343051145, + Upper: -4466.7196729968955, + Count: FloatString(1 * f), + }, + { + Boundaries: 1, + Lower: -861.0779292198035, + Upper: -789.6119426088657, + Count: FloatString(2 * f), + }, + { + Boundaries: 1, + Lower: -558.3399591246119, + Upper: -512, + Count: FloatString(3 * f), + }, + { + Boundaries: 0, + Lower: 2048, + Upper: 2233.3598364984477, + Count: FloatString(1.5 * f), + }, + { + Boundaries: 0, + Lower: 2896.3093757400984, + Upper: 3158.4477704354626, + Count: FloatString(2.5 * f), + }, + { + Boundaries: 0, + Lower: 4466.7196729968955, + Upper: 4870.992343051145, + Count: FloatString(3.5 * f), + }, + }, + }, + } + } + + fss := &SampleStream{ + Metric: Metric(lset), + Values: floats, + } + hss := &SampleStream{ + Metric: Metric(lset), + Histograms: histograms, + } + + floatMatrix = append(floatMatrix, fss) + histogramMatrix = append(histogramMatrix, hss) + } + return floatMatrix, histogramMatrix +} + +func BenchmarkSamplesJSONUnmarshal(b *testing.B) { + for _, timeseriesCount := range []int{10, 100, 1000} { + b.Run("series="+strconv.Itoa(timeseriesCount), func(b *testing.B) { + for _, datapointCount := range []int{10, 100, 1000} { + b.Run("dp="+strconv.Itoa(datapointCount), func(b *testing.B) { + floats, histograms := generateData(timeseriesCount, datapointCount) + + floatBytes, err := json.Marshal(floats) + if err != nil { + b.Fatalf("Error marshaling: %v", err) + } + histogramBytes, err := json.Marshal(histograms) + if err != nil { + b.Fatalf("Error marshaling: %v", err) + } + + b.Run("type=floats", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + var m Matrix + if err := json.Unmarshal(floatBytes, &m); err != nil { + b.Fatal(err) + } + } + }) + b.Run("type=histograms", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + var m Matrix + if err := json.Unmarshal(histogramBytes, &m); err != nil { + b.Fatal(err) + } + } + }) + }) + } + }) + } +} diff --git a/model/time127_test.go b/model/time127_test.go new file mode 100644 index 00000000..6259a644 --- /dev/null +++ b/model/time127_test.go @@ -0,0 +1,95 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "math" + "strconv" + "testing" +) + +func TestTimeJSONV2(t *testing.T) { + cases := []int64{ + math.MinInt64, + -9999999999995500, + -9999999999994500, + -9007199254740992, // smallest integer with float precision + -8123456789012345, + math.MinInt32, + -10000, + -9000, + -8000, + -7000, + -6000, + -5000, + -4000, + -3000, + -2000, + -1000, + -900, + -800, + -700, + -600, + -500, + -400, + -300, + -200, + -100, + -10, + -1, + 0, + 1, + 10, + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, + 2000, + 3000, + 4000, + 5000, + 6000, + 7000, + 8000, + 9000, + 10000, + math.MaxInt32, + 8123456789012345, + 9007199254740992, // largest integer with float precision + math.MaxInt64, + } + + for _, i := range cases { + t.Run(strconv.FormatInt(i, 10), func(t *testing.T) { + if i != math.MinInt64 { + testV1V2Marshal(t, "-1", Time(i-1)) + } + testV1V2Marshal(t, "=", Time(i)) + if i != math.MaxInt64 { + testV1V2Marshal(t, "+1", Time(i+1)) + } + }) + } + testV1V2Marshal(t, "-1", Time(-1)) + testV1V2Marshal(t, "0", Time(0)) + testV1V2Marshal(t, "1", Time(1)) +} diff --git a/model/util127_test.go b/model/util127_test.go new file mode 100644 index 00000000..35cfe7e4 --- /dev/null +++ b/model/util127_test.go @@ -0,0 +1,101 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + jsonv1 "encoding/json" + jsonv2 "encoding/json/v2" + "reflect" + "testing" +) + +func testV1V2Marshal(t *testing.T, name string, v jsonv1.Marshaler) { + t.Run("v1v2_"+name, func(t *testing.T) { + b1, err := v.MarshalJSON() + if err != nil { + t.Fatal(err) + } + b2, err := jsonv2.Marshal(v) + if err != nil { + t.Fatal(err) + } + if string(b1) != string(b2) { + t.Logf("v1=%s", string(b1)) + t.Logf("v2=%s", string(b2)) + t.Fatalf("v1/v2 mismatch") + } + }) +} + +func testRoundTrip(t *testing.T, name string, v any) { + t.Run("roundtrip_"+name, func(t *testing.T) { + b, err := jsonv1.Marshal(v) + if err != nil { + t.Fatal(err) + } + t.Log(string(b)) + + outPtr := reflect.New(reflect.TypeOf(v)).Interface() + if err := jsonv1.Unmarshal(b, outPtr); err != nil { + t.Fatal(err) + } + + out := reflect.ValueOf(outPtr).Elem().Interface() + if !reflect.DeepEqual(v, out) { + t.Fatalf("did not round-trip\nwant: %#v\ngot: %#v", v, out) + } + }) +} + +type unmarshaler interface { + UnmarshalJSON([]byte) error +} + +func testDecodeSuccessParity[T unmarshaler](t *testing.T, input string, newObj func() T) { + t.Run("decode_success_"+input, func(t *testing.T) { + s1 := newObj() + v1err := s1.UnmarshalJSON([]byte(input)) + + s2 := newObj() + v2err := jsonv2.Unmarshal([]byte(input), s2, jsonv1.DefaultOptionsV1()) // matches how decoding works when called from encoding/json + + if v1err != nil { + t.Fatalf("expected success, got error from v1: %v", v1err) + } + if v2err != nil { + t.Fatalf("expected success, got error from v2: %v", v2err) + } + if !reflect.DeepEqual(s1, s2) { + t.Errorf("inconsistent result: v1: %#v, v2=%#v", s1, s2) + } + }) +} + +func testDecodeErrorParity[T unmarshaler](t *testing.T, input string, newObj func() T) { + t.Run("decode_error_"+input, func(t *testing.T) { + s1 := newObj() + v1err := s1.UnmarshalJSON([]byte(input)) + if v1err == nil { + t.Fatalf("expected error, got none from v1") + } + + s2 := newObj() + v2err := jsonv2.Unmarshal([]byte(input), s2) + if v2err == nil { + t.Fatalf("expected error, got none from v2") + } + }) +} diff --git a/model/value.go b/model/value.go index 8dffd9c4..eed5c9ef 100644 --- a/model/value.go +++ b/model/value.go @@ -224,28 +224,6 @@ func (ss SampleStream) MarshalJSON() ([]byte, error) { } } -func (ss *SampleStream) UnmarshalJSON(b []byte) error { - v := struct { - Metric Metric `json:"metric"` - Values []SamplePair `json:"values"` - Histograms []SampleHistogramPair `json:"histograms"` - }{ - Metric: ss.Metric, - Values: ss.Values, - Histograms: ss.Histograms, - } - - if err := json.Unmarshal(b, &v); err != nil { - return err - } - - ss.Metric = v.Metric - ss.Values = v.Values - ss.Histograms = v.Histograms - - return nil -} - // Scalar is a scalar value evaluated at the set timestamp. type Scalar struct { Value SampleValue `json:"value"` diff --git a/model/value127_test.go b/model/value127_test.go new file mode 100644 index 00000000..ff6865be --- /dev/null +++ b/model/value127_test.go @@ -0,0 +1,43 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "fmt" + "testing" +) + +func TestSampleStreamJSONV2(t *testing.T) { + cases := []struct { + name string + object SampleStream + }{ + {name: "empty", object: SampleStream{}}, + {name: "metric only", object: SampleStream{Metric: Metric{}}}, + {name: "metric ordering", object: SampleStream{Metric: Metric{"a": "", "b": "", "c": "", "d": "", "e": "", "f": "", "g": ""}}}, + {name: "zero length values", object: SampleStream{Values: []SamplePair{}}}, + {name: "empty value", object: SampleStream{Values: []SamplePair{{}}}}, + {name: "populated value", object: SampleStream{Values: []SamplePair{{Timestamp: Time(1), Value: SampleValue(1)}}}}, + {name: "histogram only", object: SampleStream{Histograms: []SampleHistogramPair{{Timestamp: Time(1), Histogram: &SampleHistogram{}}}}}, + {name: "metric and values", object: SampleStream{Metric: Metric{}, Values: []SamplePair{}}}, + {name: "all fields", object: SampleStream{Metric: Metric{}, Values: []SamplePair{{}}, Histograms: []SampleHistogramPair{{Timestamp: Time(1), Histogram: &SampleHistogram{}}}}}, + } + + for i, v := range cases { + testV1V2Marshal(t, fmt.Sprintf("%d_%s", i, v.name), v.object) + testRoundTrip(t, fmt.Sprintf("%d_%s", i, v.name), v.object) + } +} diff --git a/model/value_float127.go b/model/value_float127.go new file mode 100644 index 00000000..821cc1e7 --- /dev/null +++ b/model/value_float127.go @@ -0,0 +1,70 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "bytes" + "encoding/json/jsontext" + "errors" +) + +var nullBytes = []byte("null") + +func (s *SamplePair) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() == jsontext.KindNull { + return nil + } else if t.Kind() != jsontext.KindBeginArray { + return errors.New("expected [") + } + + // Loop until we see the end of the array (tolerate arrays of any length). + for i := 0; dec.PeekKind() != jsontext.KindEndArray; i++ { + switch i { + case 0: + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Timestamp.UnmarshalJSON(v); err != nil { + return err + } + case 1: + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Value.UnmarshalJSON(v); err != nil { + return err + } + default: + // Skip any remaining array items to match UnmarshalJSON behavior. + if err := dec.SkipValue(); err != nil { + return err + } + } + } + + // Read the final end array token. + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindEndArray { + return errors.New("expected ]") + } + + return nil +} diff --git a/model/value_float127_test.go b/model/value_float127_test.go new file mode 100644 index 00000000..5a88f7f9 --- /dev/null +++ b/model/value_float127_test.go @@ -0,0 +1,83 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "fmt" + "math" + "testing" +) + +func TestSampleValueJSONV2(t *testing.T) { + cases := []float64{ + math.Inf(1), + math.MaxFloat64, + math.MaxFloat32, + math.SmallestNonzeroFloat64, + math.SmallestNonzeroFloat32, + 1e21, + 1.0, + 1e-6, + 0, + -1e-6, + -1.0, + -1e21, + -math.MaxFloat64, + -math.MaxFloat32, + -math.SmallestNonzeroFloat64, + -math.SmallestNonzeroFloat32, + math.Inf(-1), + math.NaN(), + } + + for _, i := range cases { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + if i != -math.MaxFloat64 { + testV1V2Marshal(t, "-", SampleValue(math.Nextafter(i, math.Inf(-1)))) + } + testV1V2Marshal(t, "=", SampleValue(i)) + if i != math.MaxFloat64 { + testV1V2Marshal(t, "+", SampleValue(math.Nextafter(i, math.Inf(1)))) + } + }) + } + testRoundTrip(t, "-1", SampleValue(-1)) + testRoundTrip(t, "-1.1", SampleValue(-1.1)) + testRoundTrip(t, "0", SampleValue(0)) + testRoundTrip(t, "1", SampleValue(1)) + testRoundTrip(t, "1.1", SampleValue(1.1)) +} + +func TestSamplePairJSONV2(t *testing.T) { + cases := []SamplePair{ + {}, + {Timestamp: Time(1)}, + {Value: SampleValue(1)}, + {Timestamp: Time(1), Value: SampleValue(1)}, + } + + for _, v := range cases { + testV1V2Marshal(t, fmt.Sprintf("%#v", v), v) + testRoundTrip(t, fmt.Sprintf("%#v", v), v) + } + + for _, input := range []string{`null`, `[]`, `[1.123]`, `[1.123,"2"]`, `[null,"2"]`, `[1.123,null]`, `[1.123,"2","bogus","trailing","data"]`} { + testDecodeSuccessParity(t, input, func() *SamplePair { return &SamplePair{Timestamp: Time(9), Value: SampleValue(9)} }) + } + for _, input := range []string{`[`, `[1.123,"2" true`, `{}`} { + testDecodeErrorParity(t, input, func() *SamplePair { return &SamplePair{Timestamp: Time(9), Value: SampleValue(9)} }) + } +} diff --git a/model/value_histogram127.go b/model/value_histogram127.go new file mode 100644 index 00000000..fbcfb3f9 --- /dev/null +++ b/model/value_histogram127.go @@ -0,0 +1,95 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "bytes" + "encoding/json/jsontext" + "encoding/json/v2" + "errors" +) + +func (s *SampleHistogramPair) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindBeginArray { + return errors.New("expected [") + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Timestamp.UnmarshalJSON(v); err != nil { + return err + } + + if err := json.UnmarshalDecode(dec, &s.Histogram); err != nil { + return err + } else if s.Histogram == nil { + return errors.New("histogram is nil") + } + + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindEndArray { + return errors.New("expected ]") + } + return nil +} + +func (s *HistogramBucket) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindBeginArray { + return errors.New("expected [") + } + + if err := json.UnmarshalDecode(dec, &s.Boundaries); err != nil { + return err + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Lower.UnmarshalJSON(v); err != nil { + return err + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Upper.UnmarshalJSON(v); err != nil { + return err + } + + if v, err := dec.ReadValue(); err != nil { + return err + } else if bytes.Equal(v, nullBytes) { + // Skip null values to match SamplePair#UnmarshalJSON behavior. + } else if err := s.Count.UnmarshalJSON(v); err != nil { + return err + } + + if t, err := dec.ReadToken(); err != nil { + return err + } else if t.Kind() != jsontext.KindEndArray { + return errors.New("expected ]") + } + return nil +} diff --git a/model/value_histogram127_test.go b/model/value_histogram127_test.go new file mode 100644 index 00000000..e964d03b --- /dev/null +++ b/model/value_histogram127_test.go @@ -0,0 +1,42 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build go1.27 + +package model + +import ( + "testing" +) + +func TestSampleHistogramPairJSONV2(t *testing.T) { + for _, input := range []string{`[1.123,{}]`, `[null,{}]`, `[1.123,{"count":"1"}]`, `[1.123,{"count":"1","COUNT":"2","unknown":"value"}]`} { + testDecodeSuccessParity(t, input, func() *SampleHistogramPair { return &SampleHistogramPair{Timestamp: Time(9)} }) + } + for _, input := range []string{`null`, `[]`, `[`, `[1.123]`, `[1.123,null]`, `[1.123,{} true`, `[1.123,{},"bogus","trailing","data"]`, `{}`} { + testDecodeErrorParity(t, input, func() *SampleHistogramPair { return &SampleHistogramPair{Timestamp: Time(9)} }) + } +} + +func TestHistogramBucketJSONV2(t *testing.T) { + for _, input := range []string{`[2,"2.123","3.123","4.123"]`, `[null,"2.123","3.123","4.123"]`, `[2,null,"3.123","4.123"]`, `[2,"2.123",null,"4.123"]`, `[2,"2.123","3.123",null]`} { + testDecodeSuccessParity(t, input, func() *HistogramBucket { + return &HistogramBucket{Boundaries: 1, Lower: FloatString(1.123), Upper: FloatString(1.123), Count: FloatString(1.123)} + }) + } + for _, input := range []string{`null`, `[]`, `[2]`, `[2,"2.123"]`, `[2,"2.123","3.123"]`, `[2,"2.123","3.123","4.123","random","trailing","data",true]`, `[`, `[1.123,null]`, `[2 true`, `{}`} { + testDecodeErrorParity(t, input, func() *HistogramBucket { + return &HistogramBucket{Boundaries: 1, Lower: FloatString(1.123), Upper: FloatString(1.123), Count: FloatString(1.123)} + }) + } +}