github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/prometheus/native/common_test.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package native
    22  
    23  import (
    24  	"bytes"
    25  	"math"
    26  	"net/http"
    27  	"net/http/httptest"
    28  	"net/url"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    34  	"github.com/m3db/m3/src/query/block"
    35  	"github.com/m3db/m3/src/query/executor"
    36  	"github.com/m3db/m3/src/query/models"
    37  	"github.com/m3db/m3/src/query/test"
    38  	"github.com/m3db/m3/src/query/ts"
    39  	"github.com/m3db/m3/src/query/util/json"
    40  	xerrors "github.com/m3db/m3/src/x/errors"
    41  	xjson "github.com/m3db/m3/src/x/json"
    42  	xhttp "github.com/m3db/m3/src/x/net/http"
    43  	xtest "github.com/m3db/m3/src/x/test"
    44  	xtime "github.com/m3db/m3/src/x/time"
    45  
    46  	"github.com/golang/mock/gomock"
    47  	"github.com/stretchr/testify/assert"
    48  	"github.com/stretchr/testify/require"
    49  )
    50  
    51  const (
    52  	promQuery = `http_requests_total{job="prometheus",group="canary"}`
    53  )
    54  
    55  func defaultParams() url.Values {
    56  	vals := url.Values{}
    57  	now := time.Now()
    58  	vals.Add(QueryParam, promQuery)
    59  	vals.Add(startParam, now.Format(time.RFC3339))
    60  	vals.Add(endParam, string(now.Add(time.Hour).Format(time.RFC3339)))
    61  	vals.Add(handleroptions.StepParam, (time.Duration(10) * time.Second).String())
    62  	return vals
    63  }
    64  
    65  func testParseParams(req *http.Request) (models.RequestParams, error) {
    66  	fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(
    67  		handleroptions.FetchOptionsBuilderOptions{
    68  			Timeout: 15 * time.Second,
    69  		})
    70  	if err != nil {
    71  		return models.RequestParams{}, err
    72  	}
    73  
    74  	_, fetchOpts, err := fetchOptsBuilder.NewFetchOptions(req.Context(), req)
    75  	if err != nil {
    76  		return models.RequestParams{}, err
    77  	}
    78  
    79  	return parseParams(req, executor.NewEngineOptions(), fetchOpts)
    80  }
    81  
    82  func TestParamParsing(t *testing.T) {
    83  	req := httptest.NewRequest("GET", PromReadURL, nil)
    84  	req.URL.RawQuery = defaultParams().Encode()
    85  
    86  	r, err := testParseParams(req)
    87  	require.Nil(t, err, "unable to parse request")
    88  	require.Equal(t, promQuery, r.Query)
    89  }
    90  
    91  func TestParamParsing_POST(t *testing.T) {
    92  	params := defaultParams().Encode()
    93  	req := httptest.NewRequest("POST", PromReadURL, strings.NewReader(params))
    94  	req.Header.Add(xhttp.HeaderContentType, xhttp.ContentTypeFormURLEncoded)
    95  
    96  	r, err := testParseParams(req)
    97  	require.NoError(t, err, "unable to parse request")
    98  	require.Equal(t, promQuery, r.Query)
    99  }
   100  
   101  func TestInstantaneousParamParsing(t *testing.T) {
   102  	req := httptest.NewRequest("GET", PromReadURL, nil)
   103  	params := url.Values{}
   104  	now := time.Now()
   105  	params.Add(QueryParam, promQuery)
   106  	params.Add(timeParam, now.Format(time.RFC3339))
   107  	req.URL.RawQuery = params.Encode()
   108  	fetchOptsBuilder, err := handleroptions.NewFetchOptionsBuilder(
   109  		handleroptions.FetchOptionsBuilderOptions{
   110  			Timeout: 10 * time.Second,
   111  		})
   112  	require.NoError(t, err)
   113  	_, fetchOpts, err := fetchOptsBuilder.NewFetchOptions(req.Context(), req)
   114  	require.NoError(t, err)
   115  
   116  	r, err := parseInstantaneousParams(req, executor.NewEngineOptions(),
   117  		fetchOpts)
   118  	require.NoError(t, err, "unable to parse request")
   119  	require.Equal(t, promQuery, r.Query)
   120  }
   121  
   122  func TestInvalidStart(t *testing.T) {
   123  	req := httptest.NewRequest("GET", PromReadURL, nil)
   124  	vals := defaultParams()
   125  	vals.Del(startParam)
   126  	req.URL.RawQuery = vals.Encode()
   127  	_, err := testParseParams(req)
   128  	require.NotNil(t, err, "unable to parse request")
   129  	require.True(t, xerrors.IsInvalidParams(err))
   130  }
   131  
   132  func TestInvalidTarget(t *testing.T) {
   133  	req := httptest.NewRequest("GET", PromReadURL, nil)
   134  	vals := defaultParams()
   135  	vals.Del(QueryParam)
   136  	req.URL.RawQuery = vals.Encode()
   137  
   138  	p, err := testParseParams(req)
   139  	require.NotNil(t, err, "unable to parse request")
   140  	assert.NotNil(t, p.Start)
   141  	require.True(t, xerrors.IsInvalidParams(err))
   142  }
   143  
   144  func TestParseBlockType(t *testing.T) {
   145  	for _, test := range []struct {
   146  		input    string
   147  		expected models.FetchedBlockType
   148  		err      bool
   149  	}{
   150  		{
   151  			input:    "0",
   152  			expected: models.TypeSingleBlock,
   153  		},
   154  		{
   155  			input: "1",
   156  			err:   true,
   157  		},
   158  		{
   159  			input: "2",
   160  			err:   true,
   161  		},
   162  		{
   163  			input: "foo",
   164  			err:   true,
   165  		},
   166  	} {
   167  		t.Run(test.input, func(t *testing.T) {
   168  			req := httptest.NewRequest("GET", PromReadURL, nil)
   169  			p := defaultParams()
   170  			p.Set("block-type", test.input)
   171  			req.URL.RawQuery = p.Encode()
   172  
   173  			r, err := testParseParams(req)
   174  			if !test.err {
   175  				require.NoError(t, err, "should be no block error")
   176  				require.Equal(t, test.expected, r.BlockType)
   177  			} else {
   178  				require.Error(t, err, "should be block error")
   179  			}
   180  		})
   181  	}
   182  }
   183  
   184  func TestRenderResultsJSON(t *testing.T) {
   185  	buffer := bytes.NewBuffer(nil)
   186  	jw := json.NewWriter(buffer)
   187  	series := testSeries(2)
   188  
   189  	start := series[0].Values().DatapointAt(0).Timestamp
   190  	params := models.RequestParams{
   191  		Start: start,
   192  		End:   start.Add(time.Hour * 1),
   193  	}
   194  
   195  	readResult := ReadResult{Series: series}
   196  	rr := RenderResultsJSON(json.NewNoopWriter(), readResult, RenderResultsOptions{
   197  		Start:    params.Start,
   198  		End:      params.End,
   199  		KeepNaNs: true,
   200  	})
   201  	require.Equal(t, false, rr.LimitedMaxReturnedData)
   202  	require.Equal(t, 6, rr.Datapoints)
   203  	require.Equal(t, 3, rr.Series)
   204  	require.Equal(t, 3, rr.TotalSeries)
   205  
   206  	rr = RenderResultsJSON(jw, readResult, RenderResultsOptions{
   207  		Start:    params.Start,
   208  		End:      params.End,
   209  		KeepNaNs: true,
   210  	})
   211  	require.Equal(t, false, rr.LimitedMaxReturnedData)
   212  	require.Equal(t, 6, rr.Datapoints)
   213  	require.Equal(t, 3, rr.Series)
   214  	require.Equal(t, 3, rr.TotalSeries)
   215  	require.NoError(t, jw.Close())
   216  
   217  	expected := xtest.MustPrettyJSONMap(t, xjson.Map{
   218  		"status": "success",
   219  		"warnings": xjson.Array{
   220  			"m3db exceeded query limit: results not exhaustive",
   221  		},
   222  		"data": xjson.Map{
   223  			"resultType": "matrix",
   224  			"result": xjson.Array{
   225  				xjson.Map{
   226  					"metric": xjson.Map{
   227  						"bar": "baz",
   228  						"qux": "qaz",
   229  					},
   230  					"values": xjson.Array{
   231  						xjson.Array{
   232  							1535948880,
   233  							"1",
   234  						},
   235  						xjson.Array{
   236  							1535948890,
   237  							"NaN",
   238  						},
   239  					},
   240  					"step_size_ms": 10000,
   241  				},
   242  				xjson.Map{
   243  					"metric": xjson.Map{
   244  						"baz": "bar",
   245  						"qaz": "qux",
   246  					},
   247  					"values": xjson.Array{
   248  						xjson.Array{
   249  							1535948880,
   250  							"2",
   251  						},
   252  						xjson.Array{
   253  							1535948890,
   254  							"2",
   255  						},
   256  					},
   257  					"step_size_ms": 10000,
   258  				},
   259  				xjson.Map{
   260  					"metric": xjson.Map{
   261  						"biz": "baz",
   262  						"qux": "qaz",
   263  					},
   264  					"values": xjson.Array{
   265  						xjson.Array{
   266  							1535948880,
   267  							"NaN",
   268  						},
   269  						xjson.Array{
   270  							1535948890,
   271  							"NaN",
   272  						},
   273  					},
   274  					"step_size_ms": 10000,
   275  				},
   276  			},
   277  		},
   278  	})
   279  
   280  	actual := xtest.MustPrettyJSONString(t, buffer.String())
   281  	assert.Equal(t, expected, actual, xtest.Diff(expected, actual))
   282  }
   283  
   284  func TestRenderResultsJSONWithDroppedNaNs(t *testing.T) {
   285  	var (
   286  		start       = xtime.FromSeconds(1535948880)
   287  		buffer      = bytes.NewBuffer(nil)
   288  		step        = 10 * time.Second
   289  		valsWithNaN = ts.NewFixedStepValues(step, 2, 1, start)
   290  		params      = models.RequestParams{
   291  			Start: start,
   292  			End:   start.Add(2 * step),
   293  		}
   294  	)
   295  
   296  	jw := json.NewWriter(buffer)
   297  
   298  	valsWithNaN.SetValueAt(1, math.NaN())
   299  	series := []*ts.Series{
   300  		ts.NewSeries([]byte("foo"),
   301  			valsWithNaN, test.TagSliceToTags([]models.Tag{
   302  				{Name: []byte("bar"), Value: []byte("baz")},
   303  				{Name: []byte("qux"), Value: []byte("qaz")},
   304  			})),
   305  		ts.NewSeries([]byte("foobar"),
   306  			ts.NewFixedStepValues(step, 2, math.NaN(), start),
   307  			test.TagSliceToTags([]models.Tag{
   308  				{Name: []byte("biz"), Value: []byte("baz")},
   309  				{Name: []byte("qux"), Value: []byte("qaz")},
   310  			})),
   311  		ts.NewSeries([]byte("bar"),
   312  			ts.NewFixedStepValues(step, 2, 2, start),
   313  			test.TagSliceToTags([]models.Tag{
   314  				{Name: []byte("baz"), Value: []byte("bar")},
   315  				{Name: []byte("qaz"), Value: []byte("qux")},
   316  			})),
   317  	}
   318  
   319  	meta := block.NewResultMetadata()
   320  	meta.AddWarning("foo", "bar")
   321  	meta.AddWarning("baz", "qux")
   322  	readResult := ReadResult{
   323  		Series: series,
   324  		Meta:   meta,
   325  	}
   326  
   327  	// Ensure idempotent by running first once with noop render.
   328  	rr := RenderResultsJSON(json.NewNoopWriter(), readResult, RenderResultsOptions{
   329  		Start:    params.Start,
   330  		End:      params.End,
   331  		KeepNaNs: false,
   332  	})
   333  	require.Equal(t, false, rr.LimitedMaxReturnedData)
   334  	require.Equal(t, 3, rr.Datapoints)
   335  	require.Equal(t, 2, rr.Series)
   336  	require.Equal(t, 3, rr.TotalSeries)
   337  
   338  	rr = RenderResultsJSON(jw, readResult, RenderResultsOptions{
   339  		Start:    params.Start,
   340  		End:      params.End,
   341  		KeepNaNs: false,
   342  	})
   343  	require.Equal(t, false, rr.LimitedMaxReturnedData)
   344  	require.Equal(t, 3, rr.Datapoints)
   345  	require.Equal(t, 2, rr.Series)
   346  	require.Equal(t, 3, rr.TotalSeries)
   347  	require.NoError(t, jw.Close())
   348  
   349  	expected := xtest.MustPrettyJSONMap(t, xjson.Map{
   350  		"status": "success",
   351  		"warnings": xjson.Array{
   352  			"foo_bar",
   353  			"baz_qux",
   354  		},
   355  		"data": xjson.Map{
   356  			"resultType": "matrix",
   357  			"result": xjson.Array{
   358  				xjson.Map{
   359  					"metric": xjson.Map{
   360  						"bar": "baz",
   361  						"qux": "qaz",
   362  					},
   363  					"values": xjson.Array{
   364  						xjson.Array{
   365  							1535948880,
   366  							"1",
   367  						},
   368  					},
   369  					"step_size_ms": 10000,
   370  				},
   371  				xjson.Map{
   372  					"metric": xjson.Map{
   373  						"baz": "bar",
   374  						"qaz": "qux",
   375  					},
   376  					"values": xjson.Array{
   377  						xjson.Array{
   378  							1535948880,
   379  							"2",
   380  						},
   381  						xjson.Array{
   382  							1535948890,
   383  							"2",
   384  						},
   385  					},
   386  					"step_size_ms": 10000,
   387  				},
   388  			},
   389  		},
   390  	})
   391  
   392  	actual := xtest.MustPrettyJSONString(t, buffer.String())
   393  	assert.Equal(t, expected, actual, xtest.Diff(expected, actual))
   394  }
   395  
   396  func TestRenderInstantaneousResultsJSONVector(t *testing.T) {
   397  	start := xtime.FromSeconds(1535948880)
   398  
   399  	series := []*ts.Series{
   400  		ts.NewSeries([]byte("foo"),
   401  			ts.NewFixedStepValues(10*time.Second, 1, 1, start),
   402  			test.TagSliceToTags([]models.Tag{
   403  				{Name: []byte("bar"), Value: []byte("baz")},
   404  				{Name: []byte("qux"), Value: []byte("qaz")},
   405  			})),
   406  		ts.NewSeries([]byte("nan"),
   407  			ts.NewFixedStepValues(10*time.Second, 1, math.NaN(), start),
   408  			test.TagSliceToTags([]models.Tag{
   409  				{Name: []byte("baz"), Value: []byte("bar")},
   410  			})),
   411  		ts.NewSeries([]byte("bar"),
   412  			ts.NewFixedStepValues(10*time.Second, 1, 2, start),
   413  			test.TagSliceToTags([]models.Tag{
   414  				{Name: []byte("baz"), Value: []byte("bar")},
   415  				{Name: []byte("qaz"), Value: []byte("qux")},
   416  			})),
   417  	}
   418  
   419  	readResult := ReadResult{
   420  		Series: series,
   421  		Meta:   block.NewResultMetadata(),
   422  	}
   423  
   424  	foo := xjson.Map{
   425  		"metric": xjson.Map{
   426  			"bar": "baz",
   427  			"qux": "qaz",
   428  		},
   429  		"value": xjson.Array{
   430  			1535948880,
   431  			"1",
   432  		},
   433  	}
   434  
   435  	bar := xjson.Map{
   436  		"metric": xjson.Map{
   437  			"baz": "bar",
   438  			"qaz": "qux",
   439  		},
   440  		"value": xjson.Array{
   441  			1535948880,
   442  			"2",
   443  		},
   444  	}
   445  
   446  	nan := xjson.Map{
   447  		"metric": xjson.Map{
   448  			"baz": "bar",
   449  		},
   450  		"value": xjson.Array{
   451  			1535948880,
   452  			"NaN",
   453  		},
   454  	}
   455  
   456  	// Ensure idempotent by running first once with noop render.
   457  	r := renderResultsInstantaneousJSON(json.NewNoopWriter(), readResult, RenderResultsOptions{KeepNaNs: true})
   458  	require.Equal(t, false, r.LimitedMaxReturnedData)
   459  	require.Equal(t, 3, r.Datapoints)
   460  	require.Equal(t, 3, r.Series)
   461  	require.Equal(t, 3, r.TotalSeries)
   462  
   463  	buffer := bytes.NewBuffer(nil)
   464  	jw := json.NewWriter(buffer)
   465  	r = renderResultsInstantaneousJSON(jw, readResult, RenderResultsOptions{KeepNaNs: true})
   466  	require.NoError(t, jw.Close())
   467  	require.Equal(t, false, r.LimitedMaxReturnedData)
   468  	require.Equal(t, 3, r.Datapoints)
   469  	require.Equal(t, 3, r.Series)
   470  	require.Equal(t, 3, r.TotalSeries)
   471  
   472  	expectedWithNaN := xtest.MustPrettyJSONMap(t, xjson.Map{
   473  		"status": "success",
   474  		"data": xjson.Map{
   475  			"resultType": "vector",
   476  			"result":     xjson.Array{foo, nan, bar},
   477  		},
   478  	})
   479  	actualWithNaN := xtest.MustPrettyJSONString(t, buffer.String())
   480  	assert.Equal(t, expectedWithNaN, actualWithNaN, xtest.Diff(expectedWithNaN, actualWithNaN))
   481  
   482  	// Ensure idempotent by running first once with noop render.
   483  	r = renderResultsInstantaneousJSON(json.NewNoopWriter(),
   484  		readResult,
   485  		RenderResultsOptions{KeepNaNs: false})
   486  	require.NoError(t, jw.Close())
   487  	require.Equal(t, false, r.LimitedMaxReturnedData)
   488  	require.Equal(t, 2, r.Datapoints)
   489  	require.Equal(t, 2, r.Series)
   490  	require.Equal(t, 3, r.TotalSeries) // This is > rendered series due to keepNaN: false.
   491  
   492  	buffer = bytes.NewBuffer(nil)
   493  	jw = json.NewWriter(buffer)
   494  	r = renderResultsInstantaneousJSON(jw, readResult, RenderResultsOptions{KeepNaNs: false})
   495  	require.NoError(t, jw.Close())
   496  	require.Equal(t, false, r.LimitedMaxReturnedData)
   497  	require.Equal(t, 2, r.Datapoints)
   498  	require.Equal(t, 2, r.Series)
   499  	require.Equal(t, 3, r.TotalSeries) // This is > rendered series due to keepNaN: false.
   500  	expectedWithoutNaN := xtest.MustPrettyJSONMap(t, xjson.Map{
   501  		"status": "success",
   502  		"data": xjson.Map{
   503  			"resultType": "vector",
   504  			"result":     xjson.Array{foo, bar},
   505  		},
   506  	})
   507  	actualWithoutNaN := xtest.MustPrettyJSONString(t, buffer.String())
   508  	assert.Equal(t, expectedWithoutNaN, actualWithoutNaN, xtest.Diff(expectedWithoutNaN, actualWithoutNaN))
   509  }
   510  
   511  func TestRenderInstantaneousResultsNansOnlyJSON(t *testing.T) {
   512  	start := xtime.FromSeconds(1535948880)
   513  
   514  	series := []*ts.Series{
   515  		ts.NewSeries([]byte("nan"),
   516  			ts.NewFixedStepValues(10*time.Second, 1, math.NaN(), start),
   517  			test.TagSliceToTags([]models.Tag{
   518  				{Name: []byte("qux"), Value: []byte("qaz")},
   519  			})),
   520  		ts.NewSeries([]byte("nan"),
   521  			ts.NewFixedStepValues(10*time.Second, 1, math.NaN(), start),
   522  			test.TagSliceToTags([]models.Tag{
   523  				{Name: []byte("baz"), Value: []byte("bar")},
   524  			})),
   525  	}
   526  
   527  	readResult := ReadResult{
   528  		Series: series,
   529  		Meta:   block.NewResultMetadata(),
   530  	}
   531  
   532  	nan1 := xjson.Map{
   533  		"metric": xjson.Map{
   534  			"qux": "qaz",
   535  		},
   536  		"value": xjson.Array{
   537  			1535948880,
   538  			"NaN",
   539  		},
   540  	}
   541  
   542  	nan2 := xjson.Map{
   543  		"metric": xjson.Map{
   544  			"baz": "bar",
   545  		},
   546  		"value": xjson.Array{
   547  			1535948880,
   548  			"NaN",
   549  		},
   550  	}
   551  
   552  	// Ensure idempotent by running first once with noop render.
   553  	r := renderResultsInstantaneousJSON(json.NewNoopWriter(), readResult, RenderResultsOptions{KeepNaNs: true})
   554  	require.Equal(t, false, r.LimitedMaxReturnedData)
   555  	require.Equal(t, 2, r.Datapoints)
   556  	require.Equal(t, 2, r.Series)
   557  	require.Equal(t, 2, r.TotalSeries)
   558  
   559  	buffer := bytes.NewBuffer(nil)
   560  	jw := json.NewWriter(buffer)
   561  	r = renderResultsInstantaneousJSON(jw, readResult, RenderResultsOptions{KeepNaNs: true})
   562  	require.NoError(t, jw.Close())
   563  	require.Equal(t, false, r.LimitedMaxReturnedData)
   564  	require.Equal(t, 2, r.Datapoints)
   565  	require.Equal(t, 2, r.Series)
   566  	require.Equal(t, 2, r.TotalSeries)
   567  
   568  	expectedWithNaN := xtest.MustPrettyJSONMap(t, xjson.Map{
   569  		"status": "success",
   570  		"data": xjson.Map{
   571  			"resultType": "vector",
   572  			"result":     xjson.Array{nan1, nan2},
   573  		},
   574  	})
   575  	actualWithNaN := xtest.MustPrettyJSONString(t, buffer.String())
   576  	assert.Equal(t, expectedWithNaN, actualWithNaN, xtest.Diff(expectedWithNaN, actualWithNaN))
   577  
   578  	// Ensure idempotent by running first once with noop render.
   579  	r = renderResultsInstantaneousJSON(json.NewNoopWriter(), readResult, RenderResultsOptions{KeepNaNs: false})
   580  	require.Equal(t, false, r.LimitedMaxReturnedData)
   581  	require.Equal(t, 0, r.Datapoints)
   582  	require.Equal(t, 0, r.Series)
   583  	require.Equal(t, 2, r.TotalSeries) // This is > rendered series due to keepNaN: false.
   584  
   585  	buffer = bytes.NewBuffer(nil)
   586  	jw = json.NewWriter(buffer)
   587  	r = renderResultsInstantaneousJSON(jw, readResult, RenderResultsOptions{KeepNaNs: false})
   588  	require.NoError(t, jw.Close())
   589  	require.Equal(t, false, r.LimitedMaxReturnedData)
   590  	require.Equal(t, 0, r.Datapoints)
   591  	require.Equal(t, 0, r.Series)
   592  	require.Equal(t, 2, r.TotalSeries) // This is > rendered series due to keepNaN: false.
   593  	expectedWithoutNaN := xtest.MustPrettyJSONMap(t, xjson.Map{
   594  		"status": "success",
   595  		"data": xjson.Map{
   596  			"resultType": "vector",
   597  			"result":     xjson.Array{},
   598  		},
   599  	})
   600  	actualWithoutNaN := xtest.MustPrettyJSONString(t, buffer.String())
   601  	assert.Equal(t, expectedWithoutNaN, actualWithoutNaN, xtest.Diff(expectedWithoutNaN, actualWithoutNaN))
   602  }
   603  
   604  func TestRenderInstantaneousResultsJSONScalar(t *testing.T) {
   605  	start := xtime.FromSeconds(1535948880)
   606  
   607  	series := []*ts.Series{
   608  		ts.NewSeries(
   609  			[]byte("foo"),
   610  			ts.NewFixedStepValues(10*time.Second, 1, 5, start),
   611  			test.TagSliceToTags([]models.Tag{})),
   612  	}
   613  
   614  	readResult := ReadResult{
   615  		Series:    series,
   616  		Meta:      block.NewResultMetadata(),
   617  		BlockType: block.BlockScalar,
   618  	}
   619  
   620  	// Ensure idempotent by running first once with noop render.
   621  	r := renderResultsInstantaneousJSON(json.NewNoopWriter(), readResult, RenderResultsOptions{KeepNaNs: false})
   622  	require.Equal(t, false, r.LimitedMaxReturnedData)
   623  	require.Equal(t, 1, r.Datapoints)
   624  	require.Equal(t, 1, r.Series)
   625  	require.Equal(t, 1, r.TotalSeries)
   626  
   627  	buffer := bytes.NewBuffer(nil)
   628  	jw := json.NewWriter(buffer)
   629  	r = renderResultsInstantaneousJSON(jw, readResult, RenderResultsOptions{KeepNaNs: false})
   630  	require.NoError(t, jw.Close())
   631  	require.Equal(t, false, r.LimitedMaxReturnedData)
   632  	require.Equal(t, 1, r.Datapoints)
   633  	require.Equal(t, 1, r.Series)
   634  	require.Equal(t, 1, r.TotalSeries)
   635  
   636  	expected := xtest.MustPrettyJSONMap(t, xjson.Map{
   637  		"status": "success",
   638  		"data": xjson.Map{
   639  			"resultType": "scalar",
   640  			"result": xjson.Array{
   641  				1535948880,
   642  				"5",
   643  			},
   644  		},
   645  	})
   646  
   647  	actual := xtest.MustPrettyJSONString(t, buffer.String())
   648  	assert.Equal(t, expected, actual, xtest.Diff(expected, actual))
   649  }
   650  
   651  func TestSanitizeSeries(t *testing.T) {
   652  	ctrl := gomock.NewController(t)
   653  	defer ctrl.Finish()
   654  
   655  	nan := math.NaN()
   656  	testData := []struct {
   657  		name string
   658  		data []float64
   659  	}{
   660  		{"1", []float64{nan, nan, nan, nan}},
   661  		{"2", []float64{nan, nan, nan, 1}},
   662  		{"3", []float64{nan, nan, nan, nan}},
   663  		{"4", []float64{nan, nan, 1, nan}},
   664  		{"5", []float64{1, 1, 1, 1}},
   665  		{"6", []float64{nan, nan, nan, nan}},
   666  		{"no values", []float64{}},
   667  		{"non-nan point is too early", []float64{1, nan, nan, nan, nan}},
   668  		{"non-nan point is too late ", []float64{nan, nan, nan, nan, 1}},
   669  	}
   670  
   671  	var (
   672  		series = make([]*ts.Series, 0, len(testData))
   673  		tags   = models.NewTags(0, models.NewTagOptions())
   674  		now    = xtime.FromSeconds(1535948880)
   675  		step   = time.Minute
   676  		start  = now.Add(step)
   677  		end    = now.Add(step * 3)
   678  	)
   679  
   680  	for _, d := range testData {
   681  		vals := ts.NewMockValues(ctrl)
   682  		dps := make(ts.Datapoints, 0, len(d.data))
   683  		for i, p := range d.data {
   684  			timestamp := now.Add(time.Duration(i) * step)
   685  			dps = append(dps, ts.Datapoint{Value: p, Timestamp: timestamp})
   686  			vals.EXPECT().DatapointAt(i).Return(dps[i])
   687  		}
   688  
   689  		vals.EXPECT().Len().Return(len(dps))
   690  		series = append(series, ts.NewSeries([]byte(d.name), vals, tags))
   691  	}
   692  
   693  	buffer := bytes.NewBuffer(nil)
   694  	jw := json.NewWriter(buffer)
   695  	r := RenderResultsJSON(jw,
   696  		ReadResult{Series: series},
   697  		RenderResultsOptions{Start: start, End: end})
   698  	require.NoError(t, jw.Close())
   699  
   700  	require.Equal(t, false, r.LimitedMaxReturnedData)
   701  	require.Equal(t, 3, r.Series)
   702  	require.Equal(t, 9, r.TotalSeries)
   703  	require.Equal(t, 5, r.Datapoints)
   704  
   705  	expected := xtest.MustPrettyJSONMap(t, xjson.Map{
   706  		"status": "success",
   707  		"data": xjson.Map{
   708  			"resultType": "matrix",
   709  			"result": xjson.Array{
   710  				xjson.Map{
   711  					"metric": xjson.Map{},
   712  					"values": xjson.Array{
   713  						xjson.Array{
   714  							1535949060,
   715  							"1",
   716  						},
   717  					},
   718  				},
   719  				xjson.Map{
   720  					"metric": xjson.Map{},
   721  					"values": xjson.Array{
   722  						xjson.Array{
   723  							1535949000,
   724  							"1",
   725  						},
   726  					},
   727  				},
   728  				xjson.Map{
   729  					"metric": xjson.Map{},
   730  					"values": xjson.Array{
   731  						xjson.Array{
   732  							1535948940,
   733  							"1",
   734  						},
   735  						xjson.Array{
   736  							1535949000,
   737  							"1",
   738  						},
   739  						xjson.Array{
   740  							1535949060,
   741  							"1",
   742  						},
   743  					},
   744  				},
   745  			},
   746  		},
   747  		"warnings": xjson.Array{
   748  			"m3db exceeded query limit: results not exhaustive",
   749  		},
   750  	})
   751  	actual := xtest.MustPrettyJSONString(t, buffer.String())
   752  	assert.Equal(t, expected, actual, xtest.Diff(expected, actual))
   753  }
   754  
   755  func TestRenderResultsJSONWithLimits(t *testing.T) {
   756  	buffer := bytes.NewBuffer(nil)
   757  	jw := json.NewWriter(buffer)
   758  	defer require.NoError(t, jw.Close())
   759  	series := testSeries(5)
   760  
   761  	start := series[0].Values().DatapointAt(0).Timestamp
   762  	params := models.RequestParams{
   763  		Start: start,
   764  		End:   start.Add(time.Hour * 24),
   765  	}
   766  
   767  	intPrt := func(v int) *int {
   768  		return &v
   769  	}
   770  
   771  	tests := []struct {
   772  		name               string
   773  		limit              *int
   774  		expectedDatapoints int
   775  		expectedSeries     int
   776  		expectedLimited    bool
   777  	}{
   778  		{
   779  			name:               "Omit limit",
   780  			expectedDatapoints: 15,
   781  			expectedSeries:     3,
   782  			expectedLimited:    false,
   783  		},
   784  		{
   785  			name:               "Below limit",
   786  			limit:              intPrt(16),
   787  			expectedDatapoints: 15,
   788  			expectedSeries:     3,
   789  			expectedLimited:    false,
   790  		},
   791  		{
   792  			name:               "At limit",
   793  			limit:              intPrt(15),
   794  			expectedDatapoints: 15,
   795  			expectedSeries:     3,
   796  			expectedLimited:    false,
   797  		},
   798  		{
   799  			name:               "Above limit - skip 1 series high",
   800  			limit:              intPrt(14),
   801  			expectedDatapoints: 10,
   802  			expectedSeries:     2,
   803  			expectedLimited:    true,
   804  		},
   805  		{
   806  			name:               "Above limit - skip 1 series low",
   807  			limit:              intPrt(11),
   808  			expectedDatapoints: 10,
   809  			expectedSeries:     2,
   810  			expectedLimited:    true,
   811  		},
   812  		{
   813  			name:               "Above limit - skip 1 series equal",
   814  			limit:              intPrt(10),
   815  			expectedDatapoints: 10,
   816  			expectedSeries:     2,
   817  			expectedLimited:    true,
   818  		},
   819  		{
   820  			name:               "Above limit - skip 2 series",
   821  			limit:              intPrt(9),
   822  			expectedDatapoints: 5,
   823  			expectedSeries:     1,
   824  			expectedLimited:    true,
   825  		},
   826  		{
   827  			name:               "Above limit - skip 3 series",
   828  			limit:              intPrt(4),
   829  			expectedDatapoints: 0,
   830  			expectedSeries:     0,
   831  			expectedLimited:    true,
   832  		},
   833  		{
   834  			name:               "Zero enforces no limit",
   835  			limit:              intPrt(0),
   836  			expectedDatapoints: 15,
   837  			expectedSeries:     3,
   838  			expectedLimited:    false,
   839  		},
   840  	}
   841  
   842  	for _, test := range tests {
   843  		t.Run(test.name, func(t *testing.T) {
   844  			readResult := ReadResult{Series: series}
   845  			o := RenderResultsOptions{
   846  				Start:    params.Start,
   847  				End:      params.End,
   848  				KeepNaNs: true,
   849  			}
   850  			if test.limit != nil {
   851  				o.ReturnedDatapointsLimit = *test.limit
   852  			}
   853  			r := RenderResultsJSON(jw, readResult, o)
   854  			require.Equal(t, 3, r.TotalSeries)
   855  			require.Equal(t, test.expectedSeries, r.Series)
   856  			require.Equal(t, test.expectedDatapoints, r.Datapoints)
   857  			require.Equal(t, test.expectedLimited, r.LimitedMaxReturnedData)
   858  		})
   859  	}
   860  }
   861  
   862  func testSeries(datapointsPerSeries int) []*ts.Series {
   863  	start := xtime.FromSeconds(1535948880)
   864  	valsWithNaN := ts.NewFixedStepValues(10*time.Second, datapointsPerSeries, 1, start)
   865  	valsWithNaN.SetValueAt(1, math.NaN())
   866  	return []*ts.Series{
   867  		ts.NewSeries([]byte("foo"),
   868  			valsWithNaN, test.TagSliceToTags([]models.Tag{
   869  				{Name: []byte("bar"), Value: []byte("baz")},
   870  				{Name: []byte("qux"), Value: []byte("qaz")},
   871  			})),
   872  		ts.NewSeries([]byte("bar"),
   873  			ts.NewFixedStepValues(10*time.Second, datapointsPerSeries, 2, start),
   874  			test.TagSliceToTags([]models.Tag{
   875  				{Name: []byte("baz"), Value: []byte("bar")},
   876  				{Name: []byte("qaz"), Value: []byte("qux")},
   877  			})),
   878  		ts.NewSeries([]byte("foobar"),
   879  			ts.NewFixedStepValues(10*time.Second, datapointsPerSeries, math.NaN(), start),
   880  			test.TagSliceToTags([]models.Tag{
   881  				{Name: []byte("biz"), Value: []byte("baz")},
   882  				{Name: []byte("qux"), Value: []byte("qaz")},
   883  			})),
   884  	}
   885  }