From a116f5e7c1d3ec55fd3e782e4f938aba76a4939f Mon Sep 17 00:00:00 2001
From: Artem Navoiev <tenmozes@gmail.com>
Date: Mon, 30 Sep 2019 11:25:54 +0300
Subject: [PATCH] Add regression test for query apo (#194)

Part of https://github.com/VictoriaMetrics/VictoriaMetrics/issues/187
cover:
- https://github.com/VictoriaMetrics/VictoriaMetrics/issues/184
---
 app/victoria-metrics/main_test.go             | 134 +++++++++++-------
 app/victoria-metrics/test/parser.go           |  51 +++++++
 app/victoria-metrics/test/prom_types.go       |   2 -
 .../testdata/graphite/basic.json              |   4 +-
 .../graphite/subquery-aggregation.json        |  14 ++
 .../testdata/influxdb/basic.json              |   2 +-
 .../testdata/opentsdb/basic.json              |   4 +-
 .../testdata/opentsdbhttp/basic.json          |   4 +-
 .../testdata/opentsdbhttp/multi_line.json     |   6 +-
 .../testdata/prometheus/basic.json            |   2 +-
 .../prometheus/case-sensitive-regex.json      |   2 +-
 .../testdata/prometheus/duplicate-label.json  |   2 +-
 .../testdata/prometheus/match-series.json     |   2 +-
 13 files changed, 161 insertions(+), 68 deletions(-)
 create mode 100644 app/victoria-metrics/test/parser.go
 create mode 100644 app/victoria-metrics/testdata/graphite/subquery-aggregation.json

diff --git a/app/victoria-metrics/main_test.go b/app/victoria-metrics/main_test.go
index b0fc39c3eb..995ed8221c 100644
--- a/app/victoria-metrics/main_test.go
+++ b/app/victoria-metrics/main_test.go
@@ -50,13 +50,6 @@ const (
 	testStorageInitTimeout = 10 * time.Second
 )
 
-const (
-	tplWordTime              = "{TIME}"
-	tplQuotedWordTime        = `"{TIME}"`
-	tplQuotedWordTimeSeconds = `"{TIME_S}"`
-	tplQuotedWordTimeMillis  = `"{TIME_MS}"`
-)
-
 var (
 	storagePath   string
 	insertionTime = time.Now().UTC()
@@ -64,10 +57,11 @@ var (
 
 type test struct {
 	Name          string   `json:"name"`
-	Data          string   `json:"data"`
+	Data          []string `json:"data"`
 	Query         []string `json:"query"`
 	ResultMetrics []Metric `json:"result_metrics"`
 	ResultSeries  Series   `json:"result_series"`
+	ResultQuery   Query    `json:"result_query"`
 	Issue         string   `json:"issue"`
 }
 
@@ -77,28 +71,32 @@ type Metric struct {
 	Timestamps []int64           `json:"timestamps"`
 }
 
+func (r *Metric) UnmarshalJSON(b []byte) error {
+	type plain Metric
+	return json.Unmarshal(testutil.PopulateTimeTpl(b, insertionTime), (*plain)(r))
+}
+
 type Series struct {
 	Status string              `json:"status"`
 	Data   []map[string]string `json:"data"`
 }
-
-func (r *Metric) UnmarshalJSON(b []byte) error {
-	type plain Metric
-	return json.Unmarshal(populateTimeTpl(b), (*plain)(r))
+type Query struct {
+	Status string    `json:"status"`
+	Data   QueryData `json:"data"`
+}
+type QueryData struct {
+	ResultType string            `json:"resultType"`
+	Result     []QueryDataResult `json:"result"`
 }
 
-func populateTimeTpl(b []byte) []byte {
-	var (
-		tplTimeToQuotedMS = [2][]byte{[]byte(tplQuotedWordTimeMillis), []byte(fmt.Sprintf("%d", timeToMillis(insertionTime)))}
-		tpsTimeToQuotedS  = [2][]byte{[]byte(tplQuotedWordTimeSeconds), []byte(fmt.Sprintf("%d", insertionTime.Unix()*1e3))}
-	)
-	tpls := [][2][]byte{
-		tplTimeToQuotedMS, tpsTimeToQuotedS,
-	}
-	for i := range tpls {
-		b = bytes.ReplaceAll(b, tpls[i][0], tpls[i][1])
-	}
-	return b
+type QueryDataResult struct {
+	Metric map[string]string `json:"metric"`
+	Value  []interface{}     `json:"value"`
+}
+
+func (r *QueryDataResult) UnmarshalJSON(b []byte) error {
+	type plain QueryDataResult
+	return json.Unmarshal(testutil.PopulateTimeTpl(b, insertionTime), (*plain)(r))
 }
 
 func TestMain(m *testing.M) {
@@ -182,10 +180,10 @@ func TestWriteRead(t *testing.T) {
 
 func testWrite(t *testing.T) {
 	t.Run("prometheus", func(t *testing.T) {
-		for _, test := range readIn("prometheus", t, fmt.Sprintf("%d", timeToMillis(insertionTime))) {
+		for _, test := range readIn("prometheus", t, insertionTime) {
 			s := newSuite(t)
 			r := testutil.WriteRequest{}
-			s.noError(json.Unmarshal([]byte(test.Data), &r.Timeseries))
+			s.noError(json.Unmarshal([]byte(strings.Join(test.Data, "\n")), &r.Timeseries))
 			data, err := testutil.Compress(r)
 			s.greaterThan(len(r.Timeseries), 0)
 			if err != nil {
@@ -197,39 +195,39 @@ func testWrite(t *testing.T) {
 	})
 
 	t.Run("influxdb", func(t *testing.T) {
-		for _, x := range readIn("influxdb", t, fmt.Sprintf("%d", insertionTime.UnixNano())) {
+		for _, x := range readIn("influxdb", t, insertionTime) {
 			test := x
 			t.Run(test.Name, func(t *testing.T) {
 				t.Parallel()
-				httpWrite(t, testWriteHTTPPath, bytes.NewBufferString(test.Data))
+				httpWrite(t, testWriteHTTPPath, bytes.NewBufferString(strings.Join(test.Data, "\n")))
 			})
 		}
 	})
 	t.Run("graphite", func(t *testing.T) {
-		for _, x := range readIn("graphite", t, fmt.Sprintf("%d", insertionTime.Unix())) {
+		for _, x := range readIn("graphite", t, insertionTime) {
 			test := x
 			t.Run(test.Name, func(t *testing.T) {
 				t.Parallel()
-				tcpWrite(t, "127.0.0.1"+testStatsDListenAddr, test.Data)
+				tcpWrite(t, "127.0.0.1"+testStatsDListenAddr, strings.Join(test.Data, "\n"))
 			})
 		}
 	})
 	t.Run("opentsdb", func(t *testing.T) {
-		for _, x := range readIn("opentsdb", t, fmt.Sprintf("%d", insertionTime.Unix())) {
+		for _, x := range readIn("opentsdb", t, insertionTime) {
 			test := x
 			t.Run(test.Name, func(t *testing.T) {
 				t.Parallel()
-				tcpWrite(t, "127.0.0.1"+testOpenTSDBListenAddr, test.Data)
+				tcpWrite(t, "127.0.0.1"+testOpenTSDBListenAddr, strings.Join(test.Data, "\n"))
 			})
 		}
 	})
 	t.Run("opentsdbhttp", func(t *testing.T) {
-		for _, x := range readIn("opentsdbhttp", t, fmt.Sprintf("%d", insertionTime.Unix())) {
+		for _, x := range readIn("opentsdbhttp", t, insertionTime) {
 			test := x
 			t.Run(test.Name, func(t *testing.T) {
 				t.Parallel()
 				logger.Infof("writing %s", test.Data)
-				httpWrite(t, testOpenTSDBWriteHTTPPath, bytes.NewBufferString(test.Data))
+				httpWrite(t, testOpenTSDBWriteHTTPPath, bytes.NewBufferString(strings.Join(test.Data, "\n")))
 			})
 		}
 	})
@@ -238,16 +236,25 @@ func testWrite(t *testing.T) {
 func testRead(t *testing.T) {
 	for _, engine := range []string{"prometheus", "graphite", "opentsdb", "influxdb", "opentsdbhttp"} {
 		t.Run(engine, func(t *testing.T) {
-			for _, x := range readIn(engine, t, fmt.Sprintf("%d", insertionTime.UnixNano())) {
+			for _, x := range readIn(engine, t, insertionTime) {
 				test := x
 				t.Run(test.Name, func(t *testing.T) {
 					t.Parallel()
 					for _, q := range test.Query {
+						q = testutil.PopulateTimeTplString(q, insertionTime)
 						switch true {
 						case strings.HasPrefix(q, "/api/v1/export"):
 							checkMetricsResult(t, httpReadMetrics(t, testReadHTTPPath, q), test.ResultMetrics, q, test.Issue)
 						case strings.HasPrefix(q, "/api/v1/series"):
-							checkSeriesResult(t, httpReadSeries(t, testReadHTTPPath, q), test.ResultSeries, q, test.Issue)
+							s := Series{}
+							httpReadStruct(t, testReadHTTPPath, q, &s)
+							checkSeriesResult(t, s, test.ResultSeries, q, test.Issue)
+						case strings.HasPrefix(q, "/api/v1/query_range"):
+							t.Fatalf("unsupported read query %s", q)
+						case strings.HasPrefix(q, "/api/v1/query"):
+							queryResult := Query{}
+							httpReadStruct(t, testReadHTTPPath, q, &queryResult)
+							checkQueryResult(t, queryResult, test.ResultQuery, q, test.Issue)
 						default:
 							t.Fatalf("unsupported read query %s", q)
 						}
@@ -258,7 +265,7 @@ func testRead(t *testing.T) {
 	}
 }
 
-func readIn(readFor string, t *testing.T, timeStr string) []test {
+func readIn(readFor string, t *testing.T, insertTime time.Time) []test {
 	t.Helper()
 	s := newSuite(t)
 	var tt []test
@@ -270,8 +277,9 @@ func readIn(readFor string, t *testing.T, timeStr string) []test {
 		s.noError(err)
 		item := test{}
 		s.noError(json.Unmarshal(b, &item))
-		item.Data = strings.Replace(item.Data, tplQuotedWordTime, timeStr, -1)
-		item.Data = strings.Replace(item.Data, tplWordTime, timeStr, -1)
+		for i := range item.Data {
+			item.Data[i] = testutil.PopulateTimeTplString(item.Data[i], insertTime)
+		}
 		tt = append(tt, item)
 		return nil
 	}))
@@ -316,19 +324,14 @@ func httpReadMetrics(t *testing.T, address, query string) []Metric {
 	}
 	return rows
 }
-
-func httpReadSeries(t *testing.T, address, query string) Series {
+func httpReadStruct(t *testing.T, address, query string, dst interface{}) {
 	t.Helper()
 	s := newSuite(t)
 	resp, err := http.Get(address + query)
 	s.noError(err)
 	defer resp.Body.Close()
 	s.equalInt(resp.StatusCode, 200)
-	var Series Series
-	if err := json.NewDecoder(resp.Body).Decode(&Series); err != nil {
-		s.noError(err)
-	}
-	return Series
+	s.noError(json.NewDecoder(resp.Body).Decode(dst))
 }
 
 func checkMetricsResult(t *testing.T, got, want []Metric, query, issue string) {
@@ -358,7 +361,7 @@ func removeIfFoundMetrics(r Metric, contains []Metric) []Metric {
 func checkSeriesResult(t *testing.T, got, want Series, query, issue string) {
 	t.Helper()
 	if got.Status != want.Status {
-		t.Fatalf("query %s. Result ResultSeries status mismatch %q - %q. %s", query, want.Status, got.Status, issue)
+		t.Fatalf("query %s. Result Series status mismatch %q - %q. %s", query, want.Status, got.Status, issue)
 	}
 	wantData := append([]map[string]string(nil), want.Data...)
 	for _, r := range got.Data {
@@ -368,7 +371,7 @@ func checkSeriesResult(t *testing.T, got, want Series, query, issue string) {
 		if issue != "" {
 			issue = "Regression in " + issue
 		}
-		t.Fatalf("query %s. Result series %+v not found in %+v.%s", query, wantData, got.Data, issue)
+		t.Fatalf("query %s. Result Series %+v not found in %+v.%s", query, wantData, got.Data, issue)
 	}
 }
 
@@ -382,6 +385,37 @@ func removeIfFoundSeries(r map[string]string, contains []map[string]string) []ma
 	return contains
 }
 
+func checkQueryResult(t *testing.T, got, want Query, query, issue string) {
+	t.Helper()
+	if got.Status != want.Status {
+		t.Fatalf("query %s. Result Query status mismatch %q - %q. %s", query, want.Status, got.Status, issue)
+	}
+	if got.Data.ResultType != want.Data.ResultType {
+		t.Fatalf("query %s. Result Query Data ResultType status mismatch %q - %q. %s", query, want.Data.ResultType, got.Data.ResultType, issue)
+	}
+
+	wantData := append([]QueryDataResult(nil), want.Data.Result...)
+	for _, r := range got.Data.Result {
+		wantData = removeIfFoundQueryData(r, wantData)
+	}
+	if len(wantData) > 0 {
+		if issue != "" {
+			issue = "Regression in " + issue
+		}
+		t.Fatalf("query %s. Result query %+v not found in %+v.%s", query, wantData, got.Data.Result, issue)
+	}
+}
+
+func removeIfFoundQueryData(r QueryDataResult, contains []QueryDataResult) []QueryDataResult {
+	for i, item := range contains {
+		if reflect.DeepEqual(r.Metric, item.Metric) && reflect.DeepEqual(r.Value[0], item.Value[0]) && reflect.DeepEqual(r.Value[1], item.Value[1]) {
+			contains[i] = contains[len(contains)-1]
+			return contains[:len(contains)-1]
+		}
+	}
+	return contains
+}
+
 type suite struct{ t *testing.T }
 
 func newSuite(t *testing.T) *suite { return &suite{t: t} }
@@ -409,7 +443,3 @@ func (s *suite) greaterThan(a, b int) {
 		s.t.FailNow()
 	}
 }
-
-func timeToMillis(t time.Time) int64 {
-	return t.UnixNano() / 1e6
-}
diff --git a/app/victoria-metrics/test/parser.go b/app/victoria-metrics/test/parser.go
new file mode 100644
index 0000000000..1cecc29f3d
--- /dev/null
+++ b/app/victoria-metrics/test/parser.go
@@ -0,0 +1,51 @@
+// +build integration
+
+package test
+
+import (
+	"fmt"
+	"log"
+	"regexp"
+	"strings"
+	"time"
+)
+
+var (
+	parseTimeExpRegex = regexp.MustCompile(`"?{TIME[^}]*}"?`)
+	extractRegex      = regexp.MustCompile(`"?{([^}]*)}"?`)
+)
+
+func PopulateTimeTplString(s string, t time.Time) string {
+	return string(PopulateTimeTpl([]byte(s), t))
+}
+
+func PopulateTimeTpl(b []byte, t time.Time) []byte {
+	return parseTimeExpRegex.ReplaceAllFunc(b, func(repl []byte) []byte {
+		repl = extractRegex.FindSubmatch(repl)[1]
+		parts := strings.SplitN(string(repl), "-", 2)
+		if len(parts) == 2 {
+			duration, err := time.ParseDuration(strings.TrimSpace(parts[1]))
+			if err != nil {
+				log.Fatalf("error %s parsing duration %s in %s", err, parts[1], repl)
+			}
+			t = t.Add(-duration)
+		}
+		switch strings.TrimSpace(parts[0]) {
+		case `TIME_S`:
+			return []byte(fmt.Sprintf("%d", t.Unix()))
+		case `TIME_MSZ`:
+			return []byte(fmt.Sprintf("%d", t.Unix()*1e3))
+		case `TIME_MS`:
+			return []byte(fmt.Sprintf("%d", timeToMillis(t)))
+		case `TIME_NS`:
+			return []byte(fmt.Sprintf("%d", t.UnixNano()))
+		default:
+			log.Fatalf("unkown time pattern %s in %s", parts[0], repl)
+		}
+		return repl
+	})
+}
+
+func timeToMillis(t time.Time) int64 {
+	return t.UnixNano() / 1e6
+}
diff --git a/app/victoria-metrics/test/prom_types.go b/app/victoria-metrics/test/prom_types.go
index c52b5639c6..8aaf1eddab 100644
--- a/app/victoria-metrics/test/prom_types.go
+++ b/app/victoria-metrics/test/prom_types.go
@@ -5,7 +5,6 @@ package test
 
 import (
 	"encoding/binary"
-	"log"
 	"math"
 	"math/bits"
 )
@@ -124,7 +123,6 @@ func (m *Sample) MarshalTo(dAtA []byte) (int, error) {
 func (m *Sample) MarshalToSizedBuffer(dAtA []byte) (int, error) {
 	i := len(dAtA)
 	if m.Timestamp != 0 {
-		log.Printf("prom types %d", m.Timestamp)
 		i = encodeVarintTypes(dAtA, i, uint64(m.Timestamp))
 		i--
 		dAtA[i] = 0x10
diff --git a/app/victoria-metrics/testdata/graphite/basic.json b/app/victoria-metrics/testdata/graphite/basic.json
index 8aab533660..5a93c15fc7 100644
--- a/app/victoria-metrics/testdata/graphite/basic.json
+++ b/app/victoria-metrics/testdata/graphite/basic.json
@@ -1,8 +1,8 @@
 {
   "name": "basic_insertion",
-  "data": "graphite.foo.bar.baz;tag1=value1;tag2=value2 123 {TIME}",
+  "data": ["graphite.foo.bar.baz;tag1=value1;tag2=value2 123 {TIME_S}"],
   "query": ["/api/v1/export?match={__name__!=''}"],
   "result_metrics": [
-    {"metric":{"__name__":"graphite.foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123], "timestamps": ["{TIME_S}"]}
+    {"metric":{"__name__":"graphite.foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123], "timestamps": ["{TIME_MSZ}"]}
   ]
 }
diff --git a/app/victoria-metrics/testdata/graphite/subquery-aggregation.json b/app/victoria-metrics/testdata/graphite/subquery-aggregation.json
new file mode 100644
index 0000000000..882f8b5df2
--- /dev/null
+++ b/app/victoria-metrics/testdata/graphite/subquery-aggregation.json
@@ -0,0 +1,14 @@
+{
+  "name": "subquery-aggregation",
+  "issue": "https://github.com/VictoriaMetrics/VictoriaMetrics/issues/184",
+  "data": [
+    "forms_daily_count;item=x 1 {TIME_S-1m}",
+    "forms_daily_count;item=x 2 {TIME_S-2m}",
+    "forms_daily_count;item=y 3 {TIME_S-1m}",
+    "forms_daily_count;item=y 4 {TIME_S-2m}"],
+  "query": ["/api/v1/query?query=min%20by%20(item)%20(min_over_time(forms_daily_count[10m:1m]))&time={TIME_S-1m}"],
+  "result_query": {
+    "status":"success",
+    "data":{"resultType":"vector","result":[{"metric":{"item":"x"},"value":["{TIME_S-1m}","1"]},{"metric":{"item":"y"},"value":["{TIME_S-1m}","3"]}]}
+  }
+}
diff --git a/app/victoria-metrics/testdata/influxdb/basic.json b/app/victoria-metrics/testdata/influxdb/basic.json
index 09d44b0b94..6127e69566 100644
--- a/app/victoria-metrics/testdata/influxdb/basic.json
+++ b/app/victoria-metrics/testdata/influxdb/basic.json
@@ -1,6 +1,6 @@
 {
   "name": "basic_insertion",
-  "data": "measurement,tag1=value1,tag2=value2 field1=1.23,field2=123 {TIME}",
+  "data": ["measurement,tag1=value1,tag2=value2 field1=1.23,field2=123 {TIME_NS}"],
   "query": ["/api/v1/export?match={__name__!=''}"],
   "result_metrics": [
     {"metric":{"__name__":"measurement_field2","tag1":"value1","tag2":"value2"},"values":[123], "timestamps": ["{TIME_MS}"]},
diff --git a/app/victoria-metrics/testdata/opentsdb/basic.json b/app/victoria-metrics/testdata/opentsdb/basic.json
index 010b40ffbf..3250c19798 100644
--- a/app/victoria-metrics/testdata/opentsdb/basic.json
+++ b/app/victoria-metrics/testdata/opentsdb/basic.json
@@ -1,8 +1,8 @@
 {
   "name": "basic_insertion",
-  "data": "put openstdb.foo.bar.baz {TIME} 123 tag1=value1 tag2=value2",
+  "data": ["put openstdb.foo.bar.baz {TIME_S} 123 tag1=value1 tag2=value2"],
   "query": ["/api/v1/export?match={__name__!=''}"],
   "result_metrics": [
-    {"metric":{"__name__":"openstdb.foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123], "timestamps": ["{TIME_S}"]}
+    {"metric":{"__name__":"openstdb.foo.bar.baz","tag1":"value1","tag2":"value2"},"values":[123], "timestamps": ["{TIME_MSZ}"]}
   ]
 }
diff --git a/app/victoria-metrics/testdata/opentsdbhttp/basic.json b/app/victoria-metrics/testdata/opentsdbhttp/basic.json
index e8eb44c8d4..343b88db9a 100644
--- a/app/victoria-metrics/testdata/opentsdbhttp/basic.json
+++ b/app/victoria-metrics/testdata/opentsdbhttp/basic.json
@@ -1,8 +1,8 @@
 {
   "name": "basic_insertion",
-  "data": "{\"metric\": \"opentsdbhttp.foo\", \"value\": 1001, \"timestamp\": {TIME}, \"tags\": {\"bar\":\"baz\", \"x\": \"y\"}}",
+  "data": ["{\"metric\": \"opentsdbhttp.foo\", \"value\": 1001, \"timestamp\": {TIME_S}, \"tags\": {\"bar\":\"baz\", \"x\": \"y\"}}"],
   "query": ["/api/v1/export?match={__name__!=''}"],
   "result_metrics": [
-    {"metric":{"__name__":"opentsdbhttp.foo","bar":"baz","x":"y"},"values":[1001], "timestamps": ["{TIME_S}"]}
+    {"metric":{"__name__":"opentsdbhttp.foo","bar":"baz","x":"y"},"values":[1001], "timestamps": ["{TIME_MSZ}"]}
   ]
 }
diff --git a/app/victoria-metrics/testdata/opentsdbhttp/multi_line.json b/app/victoria-metrics/testdata/opentsdbhttp/multi_line.json
index f1f444f759..35780861a4 100644
--- a/app/victoria-metrics/testdata/opentsdbhttp/multi_line.json
+++ b/app/victoria-metrics/testdata/opentsdbhttp/multi_line.json
@@ -1,9 +1,9 @@
 {
   "name": "multiline",
-  "data": "[{\"metric\": \"opentsdbhttp.multiline1\", \"value\": 1001, \"timestamp\": \"{TIME}\", \"tags\": {\"bar\":\"baz\", \"x\": \"y\"}}, {\"metric\": \"opentsdbhttp.multiline2\", \"value\": 1002, \"timestamp\": {TIME}}]",
+  "data": ["[{\"metric\": \"opentsdbhttp.multiline1\", \"value\": 1001, \"timestamp\": \"{TIME_S}\", \"tags\": {\"bar\":\"baz\", \"x\": \"y\"}}, {\"metric\": \"opentsdbhttp.multiline2\", \"value\": 1002, \"timestamp\": {TIME_S}}]"],
   "query": ["/api/v1/export?match={__name__!=''}"],
   "result_metrics": [
-    {"metric":{"__name__":"opentsdbhttp.multiline1","bar":"baz","x":"y"},"values":[1001], "timestamps": ["{TIME_S}"]},
-    {"metric":{"__name__":"opentsdbhttp.multiline2"},"values":[1002], "timestamps": ["{TIME_S}"]}
+    {"metric":{"__name__":"opentsdbhttp.multiline1","bar":"baz","x":"y"},"values":[1001], "timestamps": ["{TIME_MSZ}"]},
+    {"metric":{"__name__":"opentsdbhttp.multiline2"},"values":[1002], "timestamps": ["{TIME_MSZ}"]}
   ]
 }
diff --git a/app/victoria-metrics/testdata/prometheus/basic.json b/app/victoria-metrics/testdata/prometheus/basic.json
index 02cd92d3e2..6bd295787a 100644
--- a/app/victoria-metrics/testdata/prometheus/basic.json
+++ b/app/victoria-metrics/testdata/prometheus/basic.json
@@ -1,6 +1,6 @@
 {
   "name": "basic_insertion",
-  "data": "[{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.bar\"},{\"name\":\"baz\",\"value\":\"qux\"}],\"samples\":[{\"value\":100000,\"timestamp\":\"{TIME}\"}]}]",
+  "data": ["[{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.bar\"},{\"name\":\"baz\",\"value\":\"qux\"}],\"samples\":[{\"value\":100000,\"timestamp\":\"{TIME_MS}\"}]}]"],
   "query": ["/api/v1/export?match={__name__!=''}"],
   "result_metrics": [
     {"metric":{"__name__":"prometheus.bar","baz":"qux"},"values":[100000], "timestamps": ["{TIME_MS}"]}
diff --git a/app/victoria-metrics/testdata/prometheus/case-sensitive-regex.json b/app/victoria-metrics/testdata/prometheus/case-sensitive-regex.json
index f0241f8f40..997163e07e 100644
--- a/app/victoria-metrics/testdata/prometheus/case-sensitive-regex.json
+++ b/app/victoria-metrics/testdata/prometheus/case-sensitive-regex.json
@@ -1,7 +1,7 @@
 {
   "name": "case-sensitive-regex",
   "issue": "https://github.com/VictoriaMetrics/VictoriaMetrics/issues/161",
-  "data": "[{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.sensitiveRegex\"},{\"name\":\"label\",\"value\":\"sensitiveRegex\"}],\"samples\":[{\"value\":2,\"timestamp\":\"{TIME}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.sensitiveRegex\"},{\"name\":\"label\",\"value\":\"SensitiveRegex\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME}\"}]}]",
+  "data": ["[{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.sensitiveRegex\"},{\"name\":\"label\",\"value\":\"sensitiveRegex\"}],\"samples\":[{\"value\":2,\"timestamp\":\"{TIME_MS}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.sensitiveRegex\"},{\"name\":\"label\",\"value\":\"SensitiveRegex\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME_MS}\"}]}]"],
   "query": ["/api/v1/export?match={label=~'(?i)sensitiveregex'}"],
   "result_metrics": [
     {"metric":{"__name__":"prometheus.sensitiveRegex","label":"sensitiveRegex"},"values":[2], "timestamps": ["{TIME_MS}"]},
diff --git a/app/victoria-metrics/testdata/prometheus/duplicate-label.json b/app/victoria-metrics/testdata/prometheus/duplicate-label.json
index 0da251329d..1f27afaf8b 100644
--- a/app/victoria-metrics/testdata/prometheus/duplicate-label.json
+++ b/app/victoria-metrics/testdata/prometheus/duplicate-label.json
@@ -1,7 +1,7 @@
 {
   "name": "duplicate_label",
   "issue": "https://github.com/VictoriaMetrics/VictoriaMetrics/issues/172",
-  "data": "[{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.duplicate_label\"},{\"name\":\"duplicate\",\"value\":\"label\"},{\"name\":\"duplicate\",\"value\":\"label\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME}\"}]}]",
+  "data": ["[{\"labels\":[{\"name\":\"__name__\",\"value\":\"prometheus.duplicate_label\"},{\"name\":\"duplicate\",\"value\":\"label\"},{\"name\":\"duplicate\",\"value\":\"label\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME_MS}\"}]}]"],
   "query": ["/api/v1/export?match={__name__!=''}"],
   "result_metrics": [
     {"metric":{"__name__":"prometheus.duplicate_label","duplicate":"label"},"values":[1], "timestamps": ["{TIME_MS}"]}
diff --git a/app/victoria-metrics/testdata/prometheus/match-series.json b/app/victoria-metrics/testdata/prometheus/match-series.json
index 9d1547347a..67164535b5 100644
--- a/app/victoria-metrics/testdata/prometheus/match-series.json
+++ b/app/victoria-metrics/testdata/prometheus/match-series.json
@@ -1,7 +1,7 @@
 {
   "name": "match_series",
   "issue": "https://github.com/VictoriaMetrics/VictoriaMetrics/issues/155",
-  "data": "[{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"1\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"2\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"3\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"4\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME}\"}]}]",
+  "data": ["[{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"1\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME_MS}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"2\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME_MS}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"3\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME_MS}\"}]},{\"labels\":[{\"name\":\"__name__\",\"value\":\"MatchSeries\"},{\"name\":\"db\",\"value\":\"TenMinute\"},{\"name\":\"TurbineType\",\"value\":\"V112\"},{\"name\":\"Park\",\"value\":\"4\"}],\"samples\":[{\"value\":1,\"timestamp\":\"{TIME_MS}\"}]}]"],
   "query": ["/api/v1/series?match[]={__name__='MatchSeries'}", "/api/v1/series?match[]={__name__=~'MatchSeries.*'}"],
   "result_series": {
     "status": "success",