From 7d876094a00be101ede7b817d608fe0a5dba23d7 Mon Sep 17 00:00:00 2001 From: bwplotka Date: Tue, 31 Mar 2026 08:15:46 +0100 Subject: [PATCH 1/3] added float encoding tests Signed-off-by: bwplotka --- remotewrite/sender/helpers.go | 44 +++-- remotewrite/sender/samples.go | 270 +++++++++++++++++++++++++- remotewrite/sender/samples_test.go | 204 ------------------- remotewrite/sender/timestamps_test.go | 75 ------- 4 files changed, 291 insertions(+), 302 deletions(-) delete mode 100644 remotewrite/sender/samples_test.go delete mode 100644 remotewrite/sender/timestamps_test.go diff --git a/remotewrite/sender/helpers.go b/remotewrite/sender/helpers.go index 6e91525a..75885b1f 100644 --- a/remotewrite/sender/helpers.go +++ b/remotewrite/sender/helpers.go @@ -393,24 +393,30 @@ func runTestCases(t *testing.T, tests []TestCase) { } } +type timeseriesResult struct { + TimeSeries *writev2.TimeSeries + Labels map[string]string +} + // findTimeseriesByMetricName finds a timeseries by metric name from a captured request. -func findTimeseriesByMetricName(req *writev2.Request, metricName string) (*writev2.TimeSeries, map[string]string) { +func findTimeseriesByMetricName(req *writev2.Request, metricName string) []timeseriesResult { + var results []timeseriesResult for i := range req.Timeseries { ts := &req.Timeseries[i] labels := extractLabels(ts, req.Symbols) if labels["__name__"] == metricName { - return ts, labels + results = append(results, timeseriesResult{TimeSeries: ts, Labels: labels}) } } - return nil, nil + return results } // requireTimeseriesByMetricName finds a timeseries by metric name and fails the test if not found. -func requireTimeseriesByMetricName(t *testing.T, req *writev2.Request, metricName string) (*writev2.TimeSeries, map[string]string) { +func requireTimeseriesByMetricName(t *testing.T, req *writev2.Request, metricName string) []timeseriesResult { t.Helper() - ts, labels := findTimeseriesByMetricName(req, metricName) - require.NotNil(t, ts, "Timeseries with metric name %q must be present", metricName) - return ts, labels + results := findTimeseriesByMetricName(req, metricName) + require.NotEmpty(t, results, "Timeseries with metric name %q must be present", metricName) + return results } // requireTimeseriesRW1ByMetricName finds a timeseries by metric name and fails the test if not found. @@ -455,15 +461,15 @@ func findHistogramData(req *writev2.Request, baseName string) (classicFound bool // Returns (count, found) where found indicates if count was successfully extracted. func extractHistogramCount(req *writev2.Request, baseName string) (float64, bool) { // Try classic format first. - ts, _ := findTimeseriesByMetricName(req, baseName+"_count") - if ts != nil && len(ts.Samples) > 0 { - return ts.Samples[0].Value, true + results := findTimeseriesByMetricName(req, baseName+"_count") + if len(results) > 0 && len(results[0].TimeSeries.Samples) > 0 { + return results[0].TimeSeries.Samples[0].Value, true } // Try native format. - ts, _ = findTimeseriesByMetricName(req, baseName) - if ts != nil && len(ts.Histograms) > 0 { - hist := ts.Histograms[0] + results = findTimeseriesByMetricName(req, baseName) + if len(results) > 0 && len(results[0].TimeSeries.Histograms) > 0 { + hist := results[0].TimeSeries.Histograms[0] if hist.Count != nil { if countInt, ok := hist.Count.(*writev2.Histogram_CountInt); ok { return float64(countInt.CountInt), true @@ -479,15 +485,15 @@ func extractHistogramCount(req *writev2.Request, baseName string) (float64, bool // extractHistogramSum extracts sum from either classic or native histogram format. func extractHistogramSum(req *writev2.Request, baseName string) (float64, bool) { // Try classic format first. - ts, _ := findTimeseriesByMetricName(req, baseName+"_sum") - if ts != nil && len(ts.Samples) > 0 { - return ts.Samples[0].Value, true + results := findTimeseriesByMetricName(req, baseName+"_sum") + if len(results) > 0 && len(results[0].TimeSeries.Samples) > 0 { + return results[0].TimeSeries.Samples[0].Value, true } // Try native format. - ts, _ = findTimeseriesByMetricName(req, baseName) - if ts != nil && len(ts.Histograms) > 0 { - return ts.Histograms[0].Sum, true + results = findTimeseriesByMetricName(req, baseName) + if len(results) > 0 && len(results[0].TimeSeries.Histograms) > 0 { + return results[0].TimeSeries.Histograms[0].Sum, true } return 0, false diff --git a/remotewrite/sender/samples.go b/remotewrite/sender/samples.go index f1204b99..0a4e8114 100644 --- a/remotewrite/sender/samples.go +++ b/remotewrite/sender/samples.go @@ -15,6 +15,8 @@ package sender import ( "fmt" + "math" + "slices" "testing" "time" @@ -72,7 +74,9 @@ test_gauge_with_ts 2 %v Description: "Sample MUST have value", RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { - ts, _ := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_counter_total") + ts := results[0].TimeSeries require.NotEmpty(t, ts.Samples, "Timeseries test_counter_total must contain samples") require.Len(t, ts.Samples, 1, "Timeseries test_counter_total must contain a single sample") require.Equal(t, 101.13, ts.Samples[0].Value, @@ -96,7 +100,9 @@ test_gauge_with_ts 2 %v // Description: "Sample with the explicit timestamp work", // RFCLevel: sendertest.RecommendedLevel, // Prometheus spec, not Remote Write. // Validate: func(t *testing.T, res sendertest.ReceiverResult) { - // ts, _ := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge_with_ts") + // results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge_with_ts") + // require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge_with_ts") + // tts := results[0].TimeSeries // require.NotEmpty(t, ts.Samples, "Timeseries test_gauge_with_ts must contain samples") // require.Len(t, ts.Samples, 1, "Timeseries test_gauge_with_ts must contain a single sample") // require.Equal(t, timestamp.FromTime(explicitTS), ts.Samples[0].Timestamp) @@ -107,7 +113,9 @@ test_gauge_with_ts 2 %v Description: "Sample SHOULD have start timestamp for a counter", RFCLevel: ShouldLevel, Validate: func(t *testing.T, res ReceiverResult) { - ts, _ := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_counter_total") + ts := results[0].TimeSeries require.NotEmpty(t, ts.Samples, "Timeseries test_counter_total must contain samples") require.Len(t, ts.Samples, 1, "Timeseries test_counter_total must contain a single sample") require.Equal(t, timestamp.FromTime(st), ts.Samples[0].StartTimestamp, @@ -119,13 +127,267 @@ test_gauge_with_ts 2 %v Description: "Sample SHOULD have start timestamp for a histogram", RFCLevel: ShouldLevel, Validate: func(t *testing.T, res ReceiverResult) { - ts, _ := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_histogram_count") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_histogram_count") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_histogram_count") + ts := results[0].TimeSeries require.NotEmpty(t, ts.Samples, "Timeseries test_histogram_count must contain samples") require.Len(t, ts.Samples, 1, "Timeseries test_histogram_count must contain a single sample") require.Equal(t, timestamp.FromTime(st), ts.Samples[0].StartTimestamp, "Sample for test_histogram_count does not have ST") }, }, + { + Name: "start_timestamp before timestamp", + Description: "Start timestamp is SHOULD be 0 or before or equal to timestamp", + RFCLevel: ShouldLevel, + Validate: func(t *testing.T, res ReceiverResult) { + for _, ts := range res.Requests[0].RW2.Timeseries { + for _, sample := range ts.Samples { + if sample.StartTimestamp != 0 { + require.LessOrEqual(t, sample.StartTimestamp, sample.Timestamp, "Start timestamp should be before or equal to timestamp") + } + } + } + }, + }, + }, + }, + { + Name: "samples sorted", + Description: "Sender MUST send samples in a sorted order", + RFCLevel: MustLevel, + ScrapeData: fmt.Sprintf(`# TYPE test_counter counter +test_counter_total 101.13 %v +test_counter_total 102.13 %v +test_counter_total 103.13 %v +`, + ToOpenMetricsTimestampString(explicitTS), + ToOpenMetricsTimestampString(explicitTS.Add(15*time.Second)), + ToOpenMetricsTimestampString(explicitTS.Add(30*time.Second)), + ), + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") + // Prometheus will store 3 series, but support 3 batched samples too. + if len(results) == 3 { + require.Len(t, results[0].TimeSeries.Samples, 1) + require.Len(t, results[1].TimeSeries.Samples, 1) + require.Len(t, results[2].TimeSeries.Samples, 1) + require.True(t, slices.IsSorted([]int64{ + results[0].TimeSeries.Samples[0].Timestamp, + results[1].TimeSeries.Samples[0].Timestamp, + results[2].TimeSeries.Samples[0].Timestamp, + })) + return + } + if len(results) == 1 { + require.Len(t, results[0].TimeSeries.Samples, 3) + require.True(t, slices.IsSorted([]int64{ + results[0].TimeSeries.Samples[0].Timestamp, + results[0].TimeSeries.Samples[1].Timestamp, + results[0].TimeSeries.Samples[2].Timestamp, + })) + return + } + t.Fatal("Should receive exactly one timeseries for test_counter_total with 3 samples, or 3 timeseries with one sample each; results:", results) + }, + }, + { + Name: "float_value_encoding", + Description: "Sender MUST correctly encode regular float values", + RFCLevel: MustLevel, + ScrapeData: "test_metric 123.45\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_metric") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_metric") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.Equal(t, 123.45, ts.Samples[0].Value, "Sample value must be correctly encoded") + }, + }, + { + Name: "integer_value_encoding", + Description: "Sender MUST correctly encode integer values as floats", + RFCLevel: MustLevel, + ScrapeData: "test_counter_total 42\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_counter_total") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.Equal(t, 42.0, ts.Samples[0].Value, "Integer value must be encoded as float") + }, + }, + { + Name: "zero_value_encoding", + Description: "Sender MUST correctly encode zero values", + RFCLevel: MustLevel, + ScrapeData: "test_gauge 0\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.Equal(t, 0.0, ts.Samples[0].Value, "Zero value must be correctly encoded") + }, + }, + { + Name: "negative_value_encoding", + Description: "Sender MUST correctly encode negative values", + RFCLevel: MustLevel, + ScrapeData: "temperature_celsius -15.5\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "temperature_celsius") + require.Len(t, results, 1, "Should receive exactly one timeseries for temperature_celsius") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.Equal(t, -15.5, ts.Samples[0].Value, "Negative value must be correctly encoded") + }, + }, + { + Name: "positive_infinity_encoding", + Description: "Sender MUST correctly encode +Inf values", + RFCLevel: MustLevel, + ScrapeData: "test_gauge +Inf\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.True(t, math.IsInf(ts.Samples[0].Value, 1), "Positive infinity must be correctly encoded") + }, + }, + { + Name: "negative_infinity_encoding", + Description: "Sender MUST correctly encode -Inf values", + RFCLevel: MustLevel, + ScrapeData: "test_gauge -Inf\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.True(t, math.IsInf(ts.Samples[0].Value, -1), "Negative infinity must be correctly encoded") + }, + }, + { + Name: "nan_encoding", + Description: "Sender MUST correctly encode NaN values", + RFCLevel: MustLevel, + ScrapeData: "test_gauge NaN\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.True(t, math.IsNaN(ts.Samples[0].Value), "NaN must be correctly encoded") + }, + }, + { + Name: "large_float_values", + Description: "Sender MUST handle very large float values", + RFCLevel: MustLevel, + ScrapeData: "test_large 1.7976931348623157e+308\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_large") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_large") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.Greater(t, ts.Samples[0].Value, 1e307, "Large float value must be correctly encoded") + }, + }, + { + Name: "small_float_values", + Description: "Sender MUST handle very small float values", + RFCLevel: MustLevel, + ScrapeData: "test_small 2.2250738585072014e-308\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_small") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_small") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.Less(t, ts.Samples[0].Value, 1e-307, "Small float value must be correctly encoded") + require.Greater(t, ts.Samples[0].Value, 0.0, "Small float value must be positive") + }, + }, + { + Name: "scientific_notation", + Description: "Sender MUST handle values in scientific notation", + RFCLevel: MustLevel, + ScrapeData: "test_scientific 1.23e-4\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_scientific") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_scientific") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + require.InDelta(t, 0.000123, ts.Samples[0].Value, 0.0000001, "Scientific notation value must be correctly parsed and encoded") + }, + }, + { + Name: "precision_preservation", + Description: "Sender SHOULD preserve float precision", + RFCLevel: ShouldLevel, + ScrapeData: "test_precision 0.123456789012345\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_precision") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_precision") + ts := results[0].TimeSeries + require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") + }, + }, + { + Name: "job_instance_labels_present", + Description: "Sender SHOULD include job and instance labels in samples", + RFCLevel: ShouldLevel, + ScrapeData: "test_metric 42\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_metric") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_metric") + labels := results[0].Labels + require.NotEmpty(t, labels["job"], "Sample should include 'job' label") + require.NotEmpty(t, labels["instance"], "Sample should include 'instance' label") + }, + }, + { + Name: "sample_ordering", + Description: "Sender MUST send samples with older timestamps before newer ones within a series", + RFCLevel: MustLevel, + ScrapeData: "metric_a 1\nmetric_b 2\nmetric_c 3\n", + Version: remote.WriteV2MessageType, + Validate: func(t *testing.T, res ReceiverResult) { + require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") + for _, ts := range res.Requests[0].RW2.Timeseries { + if len(ts.Samples) > 1 { + for i := 1; i < len(ts.Samples); i++ { + require.LessOrEqual(t, ts.Samples[i-1].Timestamp, ts.Samples[i].Timestamp, "Samples within a timeseries must be ordered by timestamp") + } + } + } }, }, } diff --git a/remotewrite/sender/samples_test.go b/remotewrite/sender/samples_test.go deleted file mode 100644 index 4a923910..00000000 --- a/remotewrite/sender/samples_test.go +++ /dev/null @@ -1,204 +0,0 @@ -// 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 sender - -import ( - "math" - "testing" -) - -// TestSampleEncoding validates that senders correctly encode float samples. -func TestSampleEncoding_Old(t *testing.T) { - t.Skip("TODO: Revise and move to a new framework") - - tests := []TestCase{ - { - Name: "float_value_encoding", - Description: "Sender MUST correctly encode regular float values", - RFCLevel: "MUST", - ScrapeData: "test_metric 123.45\n", - Validator: func(t *testing.T, req *CapturedRequest) { - must(t).NotEmpty(req.Request.Timeseries, "Request must contain timeseries") - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_metric") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).Equal(123.45, ts.Samples[0].Value, - "Sample value must be correctly encoded") - }, - }, - { - Name: "integer_value_encoding", - Description: "Sender MUST correctly encode integer values as floats", - RFCLevel: "MUST", - ScrapeData: "test_counter_total 42\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_counter_total") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).Equal(42.0, ts.Samples[0].Value, - "Integer value must be encoded as float") - }, - }, - { - Name: "zero_value_encoding", - Description: "Sender MUST correctly encode zero values", - RFCLevel: "MUST", - ScrapeData: "test_gauge 0\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_gauge") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).Equal(0.0, ts.Samples[0].Value, - "Zero value must be correctly encoded") - }, - }, - { - Name: "negative_value_encoding", - Description: "Sender MUST correctly encode negative values", - RFCLevel: "MUST", - ScrapeData: "temperature_celsius -15.5\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "temperature_celsius") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).Equal(-15.5, ts.Samples[0].Value, - "Negative value must be correctly encoded") - }, - }, - { - Name: "positive_infinity_encoding", - Description: "Sender MUST correctly encode +Inf values", - RFCLevel: "MUST", - ScrapeData: "test_gauge +Inf\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_gauge") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).True(math.IsInf(ts.Samples[0].Value, 1), - "Positive infinity must be correctly encoded") - }, - }, - { - Name: "negative_infinity_encoding", - Description: "Sender MUST correctly encode -Inf values", - RFCLevel: "MUST", - ScrapeData: "test_gauge -Inf\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_gauge") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).True(math.IsInf(ts.Samples[0].Value, -1), - "Negative infinity must be correctly encoded") - }, - }, - { - Name: "nan_encoding", - Description: "Sender MUST correctly encode NaN values", - RFCLevel: "MUST", - ScrapeData: "test_gauge NaN\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_gauge") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).True(math.IsNaN(ts.Samples[0].Value), - "NaN must be correctly encoded") - }, - }, - { - Name: "large_float_values", - Description: "Sender MUST handle very large float values", - RFCLevel: "MUST", - ScrapeData: "test_large 1.7976931348623157e+308\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_large") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).Greater(ts.Samples[0].Value, 1e307, - "Large float value must be correctly encoded") - }, - }, - { - Name: "small_float_values", - Description: "Sender MUST handle very small float values", - RFCLevel: "MUST", - ScrapeData: "test_small 2.2250738585072014e-308\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_small") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).Less(ts.Samples[0].Value, 1e-307, - "Small float value must be correctly encoded") - must(t).Greater(ts.Samples[0].Value, 0.0, - "Small float value must be positive") - }, - }, - { - Name: "scientific_notation", - Description: "Sender MUST handle values in scientific notation", - RFCLevel: "MUST", - ScrapeData: "test_scientific 1.23e-4\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_scientific") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - must(t).InDelta(0.000123, ts.Samples[0].Value, 0.0000001, - "Scientific notation value must be correctly parsed and encoded") - }, - }, - { - Name: "precision_preservation", - Description: "Sender SHOULD preserve float precision", - RFCLevel: "SHOULD", - ScrapeData: "test_precision 0.123456789012345\n", - Validator: func(t *testing.T, req *CapturedRequest) { - ts, _ := requireTimeseriesByMetricName(t, req.Request, "test_precision") - must(t).NotEmpty(ts.Samples, "Timeseries must contain samples") - }, - }, - { - Name: "job_instance_labels_present", - Description: "Sender SHOULD include job and instance labels in samples", - RFCLevel: "SHOULD", - ScrapeData: "test_metric 42\n", - Validator: func(t *testing.T, req *CapturedRequest) { - _, labels := requireTimeseriesByMetricName(t, req.Request, "test_metric") - should(t, len(labels["job"]) > 0, "Sample should include 'job' label") - should(t, len(labels["instance"]) > 0, "Sample should include 'instance' label") - }, - }, - } - - runTestCases(t, tests) -} - -// TestSampleOrdering validates timestamp ordering in samples. -func TestSampleOrdering_Old(t *testing.T) { - t.Skip("TODO: Revise and move to a new framework") - - t.Attr("rfcLevel", "MUST") - t.Attr("description", "Sender MUST send samples with older timestamps before newer ones within a series") - - scrapeData := `# Multiple metrics -metric_a 1 -metric_b 2 -metric_c 3 -` - - forEachSender(t, func(t *testing.T, targetName string, target Sender) { - runSenderTest(t, targetName, target, SenderTestScenario{ - ScrapeData: scrapeData, - Validator: func(t *testing.T, req *CapturedRequest) { - // Verify that all samples in the request have valid timestamps. - for _, ts := range req.Request.Timeseries { - if len(ts.Samples) > 1 { - for i := 1; i < len(ts.Samples); i++ { - must(t).LessOrEqual(ts.Samples[i-1].Timestamp, ts.Samples[i].Timestamp, - "Samples within a timeseries must be ordered by timestamp") - } - } - } - }, - }) - }) -} diff --git a/remotewrite/sender/timestamps_test.go b/remotewrite/sender/timestamps_test.go deleted file mode 100644 index 76d0061d..00000000 --- a/remotewrite/sender/timestamps_test.go +++ /dev/null @@ -1,75 +0,0 @@ -// 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 sender - -import ( - "testing" -) - -// TestTimestampEncoding validates timestamp encoding and handling. -func TestTimestampEncoding_Old(t *testing.T) { - t.Skip("TODO: Revise and move to a new framework") - - tests := []TestCase{ - { - Name: "timestamp_ordering_within_series", - Description: "Within a timeseries, samples MUST be ordered by timestamp (oldest first)", - RFCLevel: "MUST", - ScrapeData: `# Multiple metrics over time -metric_a 1 -metric_b 2 -metric_c 3 -`, - Validator: func(t *testing.T, req *CapturedRequest) { - // Check that if a timeseries has multiple samples, they're ordered. - for _, ts := range req.Request.Timeseries { - if len(ts.Samples) > 1 { - for i := 1; i < len(ts.Samples); i++ { - must(t).LessOrEqual(ts.Samples[i-1].Timestamp, ts.Samples[i].Timestamp, - "Samples within timeseries must be ordered by timestamp (oldest first)") - } - } - - // Same for histograms. - if len(ts.Histograms) > 1 { - for i := 1; i < len(ts.Histograms); i++ { - must(t).LessOrEqual(ts.Histograms[i-1].Timestamp, ts.Histograms[i].Timestamp, - "Histograms within timeseries must be ordered by timestamp (oldest first)") - } - } - } - }, - }, - { - Name: "start_timestamp_before_sample_timestamp", - Description: "Start timestamp SHOULD be before or equal to sample timestamp", - RFCLevel: "SHOULD", - ScrapeData: `# TYPE test_counter counter -test_counter_total 100 -`, - Validator: func(t *testing.T, req *CapturedRequest) { - for _, ts := range req.Request.Timeseries { - for _, sample := range ts.Samples { - if sample.StartTimestamp != 0 { - should(t, sample.StartTimestamp <= sample.Timestamp, "Start timestamp should be before or equal to sample timestamp") - t.Logf("Start: %d, Sample: %d", sample.StartTimestamp, sample.Timestamp) - } - } - } - }, - }, - } - - runTestCases(t, tests) -} From 752a5a322e2adac76bb1edbd421fdb0f3540deb0 Mon Sep 17 00:00:00 2001 From: bwplotka Date: Tue, 31 Mar 2026 08:52:36 +0100 Subject: [PATCH 2/3] organize differently Signed-off-by: bwplotka --- remotewrite/sender/labels_test.go | 16 ++ remotewrite/sender/samples.go | 317 ++++++++++++------------------ 2 files changed, 143 insertions(+), 190 deletions(-) diff --git a/remotewrite/sender/labels_test.go b/remotewrite/sender/labels_test.go index 8ed5f784..7c1de6f0 100644 --- a/remotewrite/sender/labels_test.go +++ b/remotewrite/sender/labels_test.go @@ -19,6 +19,22 @@ import ( "testing" ) +/* +TODO later +{ + Name: "job_instance_labels_present", + Description: "Sender SHOULD include job and instance labels in samples", + RFCLevel: ShouldLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_float") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_float") + labels := results[0].Labels + require.NotEmpty(t, labels["job"], "Sample should include 'job' label") + require.NotEmpty(t, labels["instance"], "Sample should include 'instance' label") + }, + }, +*/ + // TestLabelValidation validates label encoding and formatting. func TestLabelValidation_Old(t *testing.T) { t.Skip("TODO: Revise and move to a new framework") diff --git a/remotewrite/sender/samples.go b/remotewrite/sender/samples.go index 0a4e8114..95c405ff 100644 --- a/remotewrite/sender/samples.go +++ b/remotewrite/sender/samples.go @@ -194,200 +194,137 @@ test_counter_total 103.13 %v }, }, { - Name: "float_value_encoding", - Description: "Sender MUST correctly encode regular float values", + Name: "encoding_cases", + Description: "Sender MUST correctly encode various value types and special cases", RFCLevel: MustLevel, - ScrapeData: "test_metric 123.45\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_metric") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_metric") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.Equal(t, 123.45, ts.Samples[0].Value, "Sample value must be correctly encoded") - }, - }, - { - Name: "integer_value_encoding", - Description: "Sender MUST correctly encode integer values as floats", - RFCLevel: MustLevel, - ScrapeData: "test_counter_total 42\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_counter_total") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.Equal(t, 42.0, ts.Samples[0].Value, "Integer value must be encoded as float") - }, - }, - { - Name: "zero_value_encoding", - Description: "Sender MUST correctly encode zero values", - RFCLevel: MustLevel, - ScrapeData: "test_gauge 0\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.Equal(t, 0.0, ts.Samples[0].Value, "Zero value must be correctly encoded") - }, - }, - { - Name: "negative_value_encoding", - Description: "Sender MUST correctly encode negative values", - RFCLevel: MustLevel, - ScrapeData: "temperature_celsius -15.5\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "temperature_celsius") - require.Len(t, results, 1, "Should receive exactly one timeseries for temperature_celsius") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.Equal(t, -15.5, ts.Samples[0].Value, "Negative value must be correctly encoded") - }, - }, - { - Name: "positive_infinity_encoding", - Description: "Sender MUST correctly encode +Inf values", - RFCLevel: MustLevel, - ScrapeData: "test_gauge +Inf\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.True(t, math.IsInf(ts.Samples[0].Value, 1), "Positive infinity must be correctly encoded") - }, - }, - { - Name: "negative_infinity_encoding", - Description: "Sender MUST correctly encode -Inf values", - RFCLevel: MustLevel, - ScrapeData: "test_gauge -Inf\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.True(t, math.IsInf(ts.Samples[0].Value, -1), "Negative infinity must be correctly encoded") - }, - }, - { - Name: "nan_encoding", - Description: "Sender MUST correctly encode NaN values", - RFCLevel: MustLevel, - ScrapeData: "test_gauge NaN\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.True(t, math.IsNaN(ts.Samples[0].Value), "NaN must be correctly encoded") - }, - }, - { - Name: "large_float_values", - Description: "Sender MUST handle very large float values", - RFCLevel: MustLevel, - ScrapeData: "test_large 1.7976931348623157e+308\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_large") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_large") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.Greater(t, ts.Samples[0].Value, 1e307, "Large float value must be correctly encoded") - }, - }, - { - Name: "small_float_values", - Description: "Sender MUST handle very small float values", - RFCLevel: MustLevel, - ScrapeData: "test_small 2.2250738585072014e-308\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_small") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_small") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.Less(t, ts.Samples[0].Value, 1e-307, "Small float value must be correctly encoded") - require.Greater(t, ts.Samples[0].Value, 0.0, "Small float value must be positive") - }, - }, - { - Name: "scientific_notation", - Description: "Sender MUST handle values in scientific notation", - RFCLevel: MustLevel, - ScrapeData: "test_scientific 1.23e-4\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_scientific") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_scientific") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - require.InDelta(t, 0.000123, ts.Samples[0].Value, 0.0000001, "Scientific notation value must be correctly parsed and encoded") - }, - }, - { - Name: "precision_preservation", - Description: "Sender SHOULD preserve float precision", - RFCLevel: ShouldLevel, - ScrapeData: "test_precision 0.123456789012345\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_precision") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_precision") - ts := results[0].TimeSeries - require.NotEmpty(t, ts.Samples, "Timeseries must contain samples") - }, - }, - { - Name: "job_instance_labels_present", - Description: "Sender SHOULD include job and instance labels in samples", - RFCLevel: ShouldLevel, - ScrapeData: "test_metric 42\n", - Version: remote.WriteV2MessageType, + ScrapeData: `test_float 123.45 +test_integer 42 +test_zero 0 +test_negative -15.5 +test_pos_inf +Inf +test_neg_inf -Inf +test_nan NaN +test_large 1.7976931348623157e+308 +test_small 2.2250738585072014e-308 +test_scientific 1.23e-4 +test_precision 0.123456789012345 +`, + Version: remote.WriteV2MessageType, Validate: func(t *testing.T, res ReceiverResult) { require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_metric") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_metric") - labels := results[0].Labels - require.NotEmpty(t, labels["job"], "Sample should include 'job' label") - require.NotEmpty(t, labels["instance"], "Sample should include 'instance' label") }, - }, - { - Name: "sample_ordering", - Description: "Sender MUST send samples with older timestamps before newer ones within a series", - RFCLevel: MustLevel, - ScrapeData: "metric_a 1\nmetric_b 2\nmetric_c 3\n", - Version: remote.WriteV2MessageType, - Validate: func(t *testing.T, res ReceiverResult) { - require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") - for _, ts := range res.Requests[0].RW2.Timeseries { - if len(ts.Samples) > 1 { - for i := 1; i < len(ts.Samples); i++ { - require.LessOrEqual(t, ts.Samples[i-1].Timestamp, ts.Samples[i].Timestamp, "Samples within a timeseries must be ordered by timestamp") - } - } - } + ValidateCases: []ValidateCase{ + { + Name: "float_value_encoding", + Description: "Sender MUST correctly encode regular float values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_float") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_float") + require.Equal(t, 123.45, results[0].TimeSeries.Samples[0].Value, "Sample value must be correctly encoded") + }, + }, + { + Name: "integer_value_encoding", + Description: "Sender MUST correctly encode integer values as floats", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_integer") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_integer") + require.Equal(t, 42.0, results[0].TimeSeries.Samples[0].Value, "Integer value must be encoded as float") + }, + }, + { + Name: "zero_value_encoding", + Description: "Sender MUST correctly encode zero values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_zero") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_zero") + require.Equal(t, 0.0, results[0].TimeSeries.Samples[0].Value, "Zero value must be correctly encoded") + }, + }, + { + Name: "negative_value_encoding", + Description: "Sender MUST correctly encode negative values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_negative") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_negative") + require.Equal(t, -15.5, results[0].TimeSeries.Samples[0].Value, "Negative value must be correctly encoded") + }, + }, + { + Name: "positive_infinity_encoding", + Description: "Sender MUST correctly encode +Inf values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_pos_inf") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_pos_inf") + require.True(t, math.IsInf(results[0].TimeSeries.Samples[0].Value, 1), "Positive infinity must be correctly encoded") + }, + }, + { + Name: "negative_infinity_encoding", + Description: "Sender MUST correctly encode -Inf values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_neg_inf") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_neg_inf") + require.True(t, math.IsInf(results[0].TimeSeries.Samples[0].Value, -1), "Negative infinity must be correctly encoded") + }, + }, + { + Name: "nan_encoding", + Description: "Sender MUST correctly encode NaN values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_nan") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_nan") + require.True(t, math.IsNaN(results[0].TimeSeries.Samples[0].Value), "NaN must be correctly encoded") + }, + }, + { + Name: "large_float_values", + Description: "Sender MUST handle very large float values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_large") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_large") + require.Greater(t, results[0].TimeSeries.Samples[0].Value, 1e307, "Large float value must be correctly encoded") + }, + }, + { + Name: "small_float_values", + Description: "Sender MUST handle very small float values", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_small") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_small") + require.Less(t, results[0].TimeSeries.Samples[0].Value, 1e-307, "Small float value must be correctly encoded") + require.Greater(t, results[0].TimeSeries.Samples[0].Value, 0.0, "Small float value must be positive") + }, + }, + { + Name: "scientific_notation", + Description: "Sender MUST handle values in scientific notation", + RFCLevel: MustLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_scientific") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_scientific") + require.InDelta(t, 0.000123, results[0].TimeSeries.Samples[0].Value, 0.0000001, "Scientific notation value must be correctly parsed and encoded") + }, + }, + { + Name: "precision_preservation", + Description: "Sender SHOULD preserve float precision", + RFCLevel: ShouldLevel, + Validate: func(t *testing.T, res ReceiverResult) { + results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_precision") + require.Len(t, results, 1, "Should receive exactly one timeseries for test_precision") + require.NotEmpty(t, results[0].TimeSeries.Samples, "Timeseries must contain samples") + }, + }, }, }, } From b6d14fbc8228e219b9f1b80a3c3320c6206d1fe1 Mon Sep 17 00:00:00 2001 From: bwplotka Date: Tue, 31 Mar 2026 09:16:41 +0100 Subject: [PATCH 3/3] reorder Signed-off-by: bwplotka --- .github/workflows/compliance-tests.yml | 5 +- remotewrite/sender/{test.go => compliance.go} | 0 remotewrite/sender/compliance_test.go | 25 ++++++++ remotewrite/sender/helpers.go | 21 +++++++ .../{test_test.go => prometheus_test.go} | 11 ++-- remotewrite/sender/samples.go | 58 +++++++------------ 6 files changed, 72 insertions(+), 48 deletions(-) rename remotewrite/sender/{test.go => compliance.go} (100%) create mode 100644 remotewrite/sender/compliance_test.go rename remotewrite/sender/{test_test.go => prometheus_test.go} (95%) diff --git a/.github/workflows/compliance-tests.yml b/.github/workflows/compliance-tests.yml index 4c265637..a24a8307 100644 --- a/.github/workflows/compliance-tests.yml +++ b/.github/workflows/compliance-tests.yml @@ -18,7 +18,4 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - - run: cd remotewrite && make sender PROMETHEUS_RW_COMPLIANCE_SKIP_TEST_RE=${SKIP_TESTS} - env: - # Skip ST tests on CI as Prometheus does not implement those yet (see PROM-60). - SKIP_TESTS: "TestCompliance/prometheus/samples/rw2/start_timestamp*" \ No newline at end of file + - run: cd remotewrite && make sender \ No newline at end of file diff --git a/remotewrite/sender/test.go b/remotewrite/sender/compliance.go similarity index 100% rename from remotewrite/sender/test.go rename to remotewrite/sender/compliance.go diff --git a/remotewrite/sender/compliance_test.go b/remotewrite/sender/compliance_test.go new file mode 100644 index 00000000..fa6d2075 --- /dev/null +++ b/remotewrite/sender/compliance_test.go @@ -0,0 +1,25 @@ +// 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 sender_test + +import ( + "testing" + + "github.com/prometheus/compliance/remotewrite/sender" +) + +// TestCompliance runs all tests in this compliance suite against prometheus{} +func TestCompliance(t *testing.T) { + sender.RunTests(t, prometheus{}, sender.ComplianceTests()) +} diff --git a/remotewrite/sender/helpers.go b/remotewrite/sender/helpers.go index 75885b1f..b5a05a1e 100644 --- a/remotewrite/sender/helpers.go +++ b/remotewrite/sender/helpers.go @@ -398,6 +398,27 @@ type timeseriesResult struct { Labels map[string]string } +func flattenTimeseriesResult(t testing.TB, results []timeseriesResult) (ret timeseriesResult) { + require.NotEmpty(t, results) + + var lbls map[string]string + for _, r := range results { + if lbls == nil { + lbls = r.Labels + } else { + require.Equal(t, lbls, r.Labels, "found two different series with the same name; expected same series") + } + if ret.TimeSeries == nil { + ret.TimeSeries = r.TimeSeries + } else { + ret.TimeSeries.Samples = append(ret.TimeSeries.Samples, r.TimeSeries.Samples...) + ret.TimeSeries.Histograms = append(ret.TimeSeries.Histograms, r.TimeSeries.Histograms...) + ret.TimeSeries.Exemplars = append(ret.TimeSeries.Exemplars, r.TimeSeries.Exemplars...) + } + } + return ret +} + // findTimeseriesByMetricName finds a timeseries by metric name from a captured request. func findTimeseriesByMetricName(req *writev2.Request, metricName string) []timeseriesResult { var results []timeseriesResult diff --git a/remotewrite/sender/test_test.go b/remotewrite/sender/prometheus_test.go similarity index 95% rename from remotewrite/sender/test_test.go rename to remotewrite/sender/prometheus_test.go index 88d6e768..795329ee 100644 --- a/remotewrite/sender/test_test.go +++ b/remotewrite/sender/prometheus_test.go @@ -29,14 +29,13 @@ import ( "runtime" "strings" "sync" - "testing" "text/template" "github.com/prometheus/compliance/remotewrite/sender" ) const ( - prometheusDownloadURL = "https://github.com/prometheus/prometheus/releases/download/v3.9.1/prometheus-3.9.1.{{.OS}}-{{.Arch}}.tar.gz" + prometheusDownloadURL = "https://github.com/prometheus/prometheus/releases/download/v3.11.0-rc.0/prometheus-3.11.0-rc.0.{{.OS}}-{{.Arch}}.tar.gz" scrapeConfigTemplate = ` global: scrape_interval: 1s @@ -71,7 +70,8 @@ func (p prometheus) Name() string { return "prometheus" } // Run runs a Prometheus process for a test target options, until ctx is done. // // It auto-downloads Prometheus binary from the official release URL (see prometheusDownloadURL). -// TODO(bwplotka): Process based runners are prone to leaking processes; add docker runner and/or figure out cleanup. Manually this could be done with 'killall -m "prometheus-3." -kill'. +// TODO(bwplotka): Process based runners are prone to leaking processes; add docker runner and/or figure out cleanup. +// Manually this could be done with 'killall -m "prometheus-3." -kill'. func (p prometheus) Run(ctx context.Context, opts sender.Options) error { binary, err := downloadBinary(prometheusDownloadURL, "prometheus") if err != nil { @@ -97,6 +97,7 @@ func (p prometheus) Run(ctx context.Context, opts sender.Options) error { `--web.listen-address=0.0.0.0:0`, fmt.Sprintf("--storage.tsdb.path=%v", dir), fmt.Sprintf("--config.file=%s", configFile), + "--enable-feature=st-storage", ) } @@ -298,7 +299,3 @@ func extractTarGz(srcFile, filename, destFile string) error { return fmt.Errorf("did not find binary in .tar.gz: %s", filename) } - -func TestCompliance(t *testing.T) { - sender.RunTests(t, prometheus{}, sender.ComplianceTests()) -} diff --git a/remotewrite/sender/samples.go b/remotewrite/sender/samples.go index 95c405ff..6730b2cd 100644 --- a/remotewrite/sender/samples.go +++ b/remotewrite/sender/samples.go @@ -23,6 +23,7 @@ import ( "github.com/prometheus/client_golang/exp/api/remote" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/model/timestamp" + writev2 "github.com/prometheus/prometheus/prompb/io/prometheus/write/v2" "github.com/stretchr/testify/require" ) @@ -75,7 +76,7 @@ test_gauge_with_ts 2 %v RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_counter_total") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_counter_total") ts := results[0].TimeSeries require.NotEmpty(t, ts.Samples, "Timeseries test_counter_total must contain samples") require.Len(t, ts.Samples, 1, "Timeseries test_counter_total must contain a single sample") @@ -101,7 +102,7 @@ test_gauge_with_ts 2 %v // RFCLevel: sendertest.RecommendedLevel, // Prometheus spec, not Remote Write. // Validate: func(t *testing.T, res sendertest.ReceiverResult) { // results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_gauge_with_ts") - // require.Len(t, results, 1, "Should receive exactly one timeseries for test_gauge_with_ts") + // require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_gauge_with_ts") // tts := results[0].TimeSeries // require.NotEmpty(t, ts.Samples, "Timeseries test_gauge_with_ts must contain samples") // require.Len(t, ts.Samples, 1, "Timeseries test_gauge_with_ts must contain a single sample") @@ -114,7 +115,7 @@ test_gauge_with_ts 2 %v RFCLevel: ShouldLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_counter_total") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_counter_total") ts := results[0].TimeSeries require.NotEmpty(t, ts.Samples, "Timeseries test_counter_total must contain samples") require.Len(t, ts.Samples, 1, "Timeseries test_counter_total must contain a single sample") @@ -128,7 +129,7 @@ test_gauge_with_ts 2 %v RFCLevel: ShouldLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_histogram_count") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_histogram_count") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_histogram_count") ts := results[0].TimeSeries require.NotEmpty(t, ts.Samples, "Timeseries test_histogram_count must contain samples") require.Len(t, ts.Samples, 1, "Timeseries test_histogram_count must contain a single sample") @@ -169,28 +170,11 @@ test_counter_total 103.13 %v Validate: func(t *testing.T, res ReceiverResult) { require.GreaterOrEqual(t, len(res.Requests), 1, "Should receive at least 1 request") results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_counter_total") - // Prometheus will store 3 series, but support 3 batched samples too. - if len(results) == 3 { - require.Len(t, results[0].TimeSeries.Samples, 1) - require.Len(t, results[1].TimeSeries.Samples, 1) - require.Len(t, results[2].TimeSeries.Samples, 1) - require.True(t, slices.IsSorted([]int64{ - results[0].TimeSeries.Samples[0].Timestamp, - results[1].TimeSeries.Samples[0].Timestamp, - results[2].TimeSeries.Samples[0].Timestamp, - })) - return - } - if len(results) == 1 { - require.Len(t, results[0].TimeSeries.Samples, 3) - require.True(t, slices.IsSorted([]int64{ - results[0].TimeSeries.Samples[0].Timestamp, - results[0].TimeSeries.Samples[1].Timestamp, - results[0].TimeSeries.Samples[2].Timestamp, - })) - return - } - t.Fatal("Should receive exactly one timeseries for test_counter_total with 3 samples, or 3 timeseries with one sample each; results:", results) + ts := flattenTimeseriesResult(t, results) + require.GreaterOrEqual(t, len(ts.TimeSeries.Samples), 3) + require.True(t, slices.IsSortedFunc(ts.TimeSeries.Samples, func(a, b writev2.Sample) int { + return int(a.T() - b.T()) + })) }, }, { @@ -220,7 +204,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_float") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_float") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_float") require.Equal(t, 123.45, results[0].TimeSeries.Samples[0].Value, "Sample value must be correctly encoded") }, }, @@ -230,7 +214,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_integer") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_integer") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_integer") require.Equal(t, 42.0, results[0].TimeSeries.Samples[0].Value, "Integer value must be encoded as float") }, }, @@ -240,7 +224,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_zero") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_zero") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_zero") require.Equal(t, 0.0, results[0].TimeSeries.Samples[0].Value, "Zero value must be correctly encoded") }, }, @@ -250,7 +234,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_negative") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_negative") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_negative") require.Equal(t, -15.5, results[0].TimeSeries.Samples[0].Value, "Negative value must be correctly encoded") }, }, @@ -260,7 +244,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_pos_inf") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_pos_inf") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_pos_inf") require.True(t, math.IsInf(results[0].TimeSeries.Samples[0].Value, 1), "Positive infinity must be correctly encoded") }, }, @@ -270,7 +254,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_neg_inf") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_neg_inf") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_neg_inf") require.True(t, math.IsInf(results[0].TimeSeries.Samples[0].Value, -1), "Negative infinity must be correctly encoded") }, }, @@ -280,7 +264,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_nan") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_nan") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_nan") require.True(t, math.IsNaN(results[0].TimeSeries.Samples[0].Value), "NaN must be correctly encoded") }, }, @@ -290,7 +274,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_large") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_large") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_large") require.Greater(t, results[0].TimeSeries.Samples[0].Value, 1e307, "Large float value must be correctly encoded") }, }, @@ -300,7 +284,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_small") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_small") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_small") require.Less(t, results[0].TimeSeries.Samples[0].Value, 1e-307, "Small float value must be correctly encoded") require.Greater(t, results[0].TimeSeries.Samples[0].Value, 0.0, "Small float value must be positive") }, @@ -311,7 +295,7 @@ test_precision 0.123456789012345 RFCLevel: MustLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_scientific") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_scientific") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_scientific") require.InDelta(t, 0.000123, results[0].TimeSeries.Samples[0].Value, 0.0000001, "Scientific notation value must be correctly parsed and encoded") }, }, @@ -321,7 +305,7 @@ test_precision 0.123456789012345 RFCLevel: ShouldLevel, Validate: func(t *testing.T, res ReceiverResult) { results := requireTimeseriesByMetricName(t, res.Requests[0].RW2, "test_precision") - require.Len(t, results, 1, "Should receive exactly one timeseries for test_precision") + require.GreaterOrEqual(t, len(results), 1, "Should receive at least one timeseries for test_precision") require.NotEmpty(t, results[0].TimeSeries.Samples, "Timeseries must contain samples") }, },