github.com/m3db/m3@v1.5.0/src/query/graphite/native/aggregation_functions_test.go (about)

     1  // Copyright (c) 2019 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  	"fmt"
    25  	"math"
    26  	"sort"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/m3db/m3/src/query/block"
    31  	"github.com/m3db/m3/src/query/graphite/common"
    32  	"github.com/m3db/m3/src/query/graphite/context"
    33  	"github.com/m3db/m3/src/query/graphite/storage"
    34  	"github.com/m3db/m3/src/query/graphite/ts"
    35  	xgomock "github.com/m3db/m3/src/x/test"
    36  	"github.com/stretchr/testify/assert"
    37  	"github.com/stretchr/testify/require"
    38  
    39  	"github.com/golang/mock/gomock"
    40  )
    41  
    42  var (
    43  	consolidationStartTime = time.Now().Truncate(time.Minute).Add(10 * time.Second)
    44  	consolidationEndTime   = consolidationStartTime.Add(1 * time.Minute)
    45  )
    46  
    47  func newConsolidationTestSeries() (*common.Context, []*ts.Series) {
    48  	ctx := common.NewContext(common.ContextOptions{Start: consolidationStartTime, End: consolidationEndTime})
    49  
    50  	testSeries := []*ts.Series{
    51  		ts.NewSeries(ctx, "a", consolidationStartTime,
    52  			ts.NewConstantValues(ctx, 10, 6, 10000)),
    53  		ts.NewSeries(ctx, "b", consolidationStartTime.Add(-30*time.Second),
    54  			ts.NewConstantValues(ctx, 15, 6, 10000)),
    55  		ts.NewSeries(ctx, "c", consolidationStartTime.Add(30*time.Second),
    56  			ts.NewConstantValues(ctx, 17, 6, 10000)),
    57  		ts.NewSeries(ctx, "d", consolidationStartTime,
    58  			ts.NewConstantValues(ctx, 3, 60, 1000)),
    59  	}
    60  
    61  	return ctx, testSeries
    62  }
    63  
    64  func testAggregatedSeries(
    65  	t *testing.T,
    66  	f func(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error),
    67  	ev1, ev2, ev3, ev4 float64,
    68  	errorMessage string,
    69  ) {
    70  	ctx, consolidationTestSeries := newConsolidationTestSeries()
    71  	defer ctx.Close()
    72  
    73  	input := ts.SeriesList{Values: consolidationTestSeries}
    74  
    75  	r, err := f(ctx, multiplePathSpecs(input))
    76  	require.Nil(t, err)
    77  
    78  	series := r.Values
    79  	require.Equal(t, 1, len(series))
    80  
    81  	require.Equal(t, consolidationTestSeries[1].StartTime(), series[0].StartTime())
    82  	require.Equal(t, consolidationTestSeries[2].EndTime(), series[0].EndTime())
    83  	require.Equal(t, 12, series[0].Len())
    84  	require.Equal(t, 10000, series[0].MillisPerStep())
    85  	for i := 0; i < 3; i++ {
    86  		n := series[0].ValueAt(i)
    87  		assert.Equal(t, ev1, n, errorMessage, i)
    88  	}
    89  	for i := 3; i < 6; i++ {
    90  		n := series[0].ValueAt(i)
    91  		assert.Equal(t, ev2, n, errorMessage, i)
    92  	}
    93  	for i := 6; i < 9; i++ {
    94  		n := series[0].ValueAt(i)
    95  		assert.Equal(t, ev3, n, errorMessage, i)
    96  	}
    97  	for i := 9; i < 12; i++ {
    98  		n := series[0].ValueAt(i)
    99  		assert.Equal(t, ev4, n, errorMessage, i)
   100  	}
   101  
   102  	// nil input -> nil output
   103  	for _, in := range [][]*ts.Series{nil, {}} {
   104  		series, err := f(ctx, multiplePathSpecs(ts.SeriesList{
   105  			Values: in,
   106  		}))
   107  		require.Nil(t, err)
   108  		require.Equal(t, in, series.Values)
   109  	}
   110  
   111  	// single input -> same output
   112  	singleSeries := []*ts.Series{consolidationTestSeries[0]}
   113  	r, err = f(ctx, multiplePathSpecs(ts.SeriesList{
   114  		Values: singleSeries,
   115  	}))
   116  	require.Nil(t, err)
   117  
   118  	series = r.Values
   119  	require.Equal(t, singleSeries[0].Len(), series[0].Len())
   120  	for i := 0; i < series[0].Len(); i++ {
   121  		assert.Equal(t, singleSeries[0].ValueAt(i), series[0].ValueAt(i))
   122  	}
   123  }
   124  
   125  func TestMinSeries(t *testing.T) {
   126  	testAggregatedSeries(t, minSeries, 15.0, 3.0, 3.0, 17.0, "invalid min value for step %d")
   127  }
   128  
   129  func TestMaxSeries(t *testing.T) {
   130  	testAggregatedSeries(t, maxSeries, 15.0, 15.0, 17.0, 17.0, "invalid max value for step %d")
   131  }
   132  
   133  func TestSumSeries(t *testing.T) {
   134  	testAggregatedSeries(t, func(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) {
   135  		return sumSeries(ctx, series)
   136  	}, 15.0, 28.0, 30.0, 17.0, "invalid sum value for step %d")
   137  }
   138  
   139  func TestStdDevSeries(t *testing.T) {
   140  	var (
   141  		ctrl          = xgomock.NewController(t)
   142  		store         = storage.NewMockStorage(ctrl)
   143  		engine        = NewEngine(store, CompileOptions{})
   144  		start, _      = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:41:19 GMT")
   145  		end, _        = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:43:19 GMT")
   146  		ctx           = common.NewContext(common.ContextOptions{Start: start, End: end, Engine: engine})
   147  		millisPerStep = 60000
   148  		inputs        = []*ts.Series{
   149  			ts.NewSeries(ctx, "servers.s2", start,
   150  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{10, 20, 30})),
   151  			ts.NewSeries(ctx, "servers.s1", start,
   152  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{90, 80, 70})),
   153  		}
   154  	)
   155  
   156  	expectedResults := []common.TestSeries{
   157  		{
   158  			Name: "stddevSeries(servers.s2,servers.s1)",
   159  			Data: []float64{40, 30, 20},
   160  		},
   161  	}
   162  	result, err := stddevSeries(ctx, multiplePathSpecs{
   163  		Values: inputs,
   164  	})
   165  	require.NoError(t, err)
   166  	common.CompareOutputsAndExpected(t, 60000, start, expectedResults, result.Values)
   167  }
   168  
   169  func TestPowSeries(t *testing.T) {
   170  	var (
   171  		ctrl      = xgomock.NewController(t)
   172  		store     = storage.NewMockStorage(ctrl)
   173  		now       = time.Now().Truncate(time.Hour)
   174  		engine    = NewEngine(store, CompileOptions{})
   175  		startTime = now.Add(-3 * time.Minute)
   176  		endTime   = now.Add(-time.Minute)
   177  		ctx       = common.NewContext(common.ContextOptions{
   178  			Start:  startTime,
   179  			End:    endTime,
   180  			Engine: engine,
   181  		})
   182  	)
   183  
   184  	fakeSeries1 := ts.NewSeries(ctx, "foo.bar.g.zed.g", startTime,
   185  		common.NewTestSeriesValues(ctx, 60000, []float64{0, 1, 2, 3, 4}))
   186  	fakeSeries2 := ts.NewSeries(ctx, "foo.bar.g.zed.g", startTime,
   187  		common.NewTestSeriesValues(ctx, 60000, []float64{2, 4, 1, 3, 3}))
   188  	fakeSeries3 := ts.NewSeries(ctx, "foo.bar.g.zed.g", startTime,
   189  		common.NewTestSeriesValues(ctx, 60000, []float64{5, 4, 3, 2, 1}))
   190  
   191  	listOfFakeSeries := []*ts.Series{fakeSeries1, fakeSeries2, fakeSeries3}
   192  
   193  	expectedValues := []float64{0, 1, 8, 729, 64}
   194  	result, err := powSeries(ctx, multiplePathSpecs(singlePathSpec{Values: listOfFakeSeries}))
   195  	if err != nil {
   196  		fmt.Println(err)
   197  	}
   198  	for i := 0; i < result.Values[0].Len(); i++ {
   199  		require.Equal(t, result.Values[0].ValueAt(i), expectedValues[i])
   200  	}
   201  }
   202  
   203  func TestAggregate(t *testing.T) {
   204  	testAggregatedSeries(t, func(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) {
   205  		return aggregate(ctx, singlePathSpec(series), "sum")
   206  	}, 15.0, 28.0, 30.0, 17.0, "invalid sum value for step %d")
   207  
   208  	testAggregatedSeries(t, func(ctx *common.Context, series multiplePathSpecs) (ts.SeriesList, error) {
   209  		return aggregate(ctx, singlePathSpec(series), "maxSeries")
   210  	}, 15.0, 15.0, 17.0, 17.0, "invalid max value for step %d")
   211  }
   212  
   213  func TestAggregateSeriesMedian(t *testing.T) {
   214  	var (
   215  		ctrl          = xgomock.NewController(t)
   216  		store         = storage.NewMockStorage(ctrl)
   217  		engine        = NewEngine(store, CompileOptions{})
   218  		start, _      = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:41:19 GMT")
   219  		end, _        = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:43:19 GMT")
   220  		ctx           = common.NewContext(common.ContextOptions{Start: start, End: end, Engine: engine})
   221  		millisPerStep = 60000
   222  		inputs        = []*ts.Series{
   223  			ts.NewSeries(ctx, "servers.s2", start,
   224  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{10, 20, 30})),
   225  			ts.NewSeries(ctx, "servers.s1", start,
   226  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{90, 80, 70})),
   227  			ts.NewSeries(ctx, "servers.s3", start,
   228  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{5, 100, 45})),
   229  		}
   230  	)
   231  
   232  	expectedResults := []common.TestSeries{
   233  		{
   234  			Name: "medianSeries(servers.s2,servers.s1,servers.s3)",
   235  			Data: []float64{10, 80, 45},
   236  		},
   237  	}
   238  	result, err := aggregate(ctx, singlePathSpec{
   239  		Values: inputs,
   240  	}, "median")
   241  	require.NoError(t, err)
   242  	common.CompareOutputsAndExpected(t, 60000, start, expectedResults, result.Values)
   243  }
   244  
   245  type mockEngine struct {
   246  	fn func(
   247  		ctx context.Context,
   248  		query string,
   249  		options storage.FetchOptions,
   250  	) (*storage.FetchResult, error)
   251  
   252  	storage storage.Storage
   253  }
   254  
   255  func (e mockEngine) FetchByQuery(
   256  	ctx context.Context,
   257  	query string,
   258  	opts storage.FetchOptions,
   259  ) (*storage.FetchResult, error) {
   260  	return e.fn(ctx, query, opts)
   261  }
   262  
   263  func (e mockEngine) Storage() storage.Storage {
   264  	return nil
   265  }
   266  
   267  func TestVariadicSumSeries(t *testing.T) {
   268  	expr, err := Compile("sumSeries(foo.bar.*, foo.baz.*)", CompileOptions{})
   269  	require.NoError(t, err)
   270  	ctx := common.NewTestContext()
   271  	ctx.Engine = mockEngine{fn: func(
   272  		ctx context.Context,
   273  		query string,
   274  		options storage.FetchOptions,
   275  	) (*storage.FetchResult, error) {
   276  		start := options.StartTime
   277  		switch query {
   278  		case "foo.bar.*":
   279  			return storage.NewFetchResult(ctx, []*ts.Series{
   280  				ts.NewSeries(ctx, "foo.bar.a", start, ts.NewConstantValues(ctx, 1, 3, 1000)),
   281  				ts.NewSeries(ctx, "foo.bar.b", start, ts.NewConstantValues(ctx, 2, 3, 1000)),
   282  			}, block.NewResultMetadata()), nil
   283  		case "foo.baz.*":
   284  			return storage.NewFetchResult(ctx, []*ts.Series{
   285  				ts.NewSeries(ctx, "foo.baz.a", start, ts.NewConstantValues(ctx, 3, 3, 1000)),
   286  				ts.NewSeries(ctx, "foo.baz.b", start, ts.NewConstantValues(ctx, 4, 3, 1000)),
   287  			}, block.ResultMetadata{
   288  				Exhaustive: false,
   289  				LocalOnly:  false,
   290  				Warnings:   []block.Warning{{Name: "foo", Message: "bar"}},
   291  			}), nil
   292  		}
   293  		return nil, fmt.Errorf("unexpected query: %s", query)
   294  	}}
   295  
   296  	r, err := expr.Execute(ctx)
   297  	require.NoError(t, err)
   298  
   299  	require.Equal(t, 1, r.Len())
   300  	assert.Equal(t, []float64{10, 10, 10}, r.Values[0].SafeValues())
   301  	assert.False(t, r.Metadata.Exhaustive)
   302  	assert.False(t, r.Metadata.LocalOnly)
   303  	require.Equal(t, 1, len(r.Metadata.Warnings))
   304  	assert.Equal(t, "foo_bar", r.Metadata.Warnings[0].Header())
   305  }
   306  
   307  func TestDiffSeries(t *testing.T) {
   308  	testAggregatedSeries(t, diffSeries, -15.0, -8.0, -10.0, -17.0, "invalid diff value for step %d")
   309  }
   310  
   311  func TestMultiplySeries(t *testing.T) {
   312  	testAggregatedSeries(t, multiplySeries, 15.0, 450.0, 510.0, 17.0, "invalid product value for step %d")
   313  }
   314  
   315  func TestAverageSeries(t *testing.T) {
   316  	testAggregatedSeries(t, averageSeries, 15.0, 28.0/3, 10.0, 17.0, "invalid avg value for step %d")
   317  }
   318  
   319  func TestDivideSeries(t *testing.T) {
   320  	ctx, consolidationTestSeries := newConsolidationTestSeries()
   321  	defer ctx.Close()
   322  
   323  	// multiple series, different start/end times
   324  	nan := math.NaN()
   325  	series, err := divideSeries(ctx, singlePathSpec{
   326  		Values: consolidationTestSeries[0:2],
   327  	}, singlePathSpec{
   328  		Values: consolidationTestSeries[2:3],
   329  	})
   330  	require.Nil(t, err)
   331  	expected := []common.TestSeries{
   332  		{
   333  			Name: "divideSeries(a,c)",
   334  			Data: []float64{nan, nan, nan, 0.5882, 0.5882, 0.5882, nan, nan, nan},
   335  		},
   336  		{
   337  			Name: "divideSeries(b,c)",
   338  			Data: []float64{nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan},
   339  		},
   340  	}
   341  
   342  	common.CompareOutputsAndExpected(t, 10000, consolidationStartTime,
   343  		[]common.TestSeries{expected[0]}, []*ts.Series{series.Values[0]})
   344  	common.CompareOutputsAndExpected(t, 10000, consolidationStartTime.Add(-30*time.Second),
   345  		[]common.TestSeries{expected[1]}, []*ts.Series{series.Values[1]})
   346  
   347  	// different millisPerStep, same start/end times
   348  	series, err = divideSeries(ctx, singlePathSpec{
   349  		Values: consolidationTestSeries[0:1],
   350  	}, singlePathSpec{
   351  		Values: consolidationTestSeries[3:4],
   352  	})
   353  	require.Nil(t, err)
   354  	expected = []common.TestSeries{
   355  		{
   356  			Name: "divideSeries(a,d)",
   357  			Data: []float64{3.3333, 3.3333, 3.3333, 3.3333, 3.33333, 3.3333},
   358  		},
   359  	}
   360  	common.CompareOutputsAndExpected(t, 10000, consolidationStartTime,
   361  		[]common.TestSeries{expected[0]}, []*ts.Series{series.Values[0]})
   362  
   363  	// empty series
   364  	series, err = divideSeries(ctx, singlePathSpec{
   365  		Values: []*ts.Series{},
   366  	}, singlePathSpec{
   367  		Values: consolidationTestSeries,
   368  	})
   369  	require.Nil(t, err)
   370  	require.Equal(t, series, ts.NewSeriesList())
   371  }
   372  
   373  func TestDivideSeriesError(t *testing.T) {
   374  	ctx, consolidationTestSeries := newConsolidationTestSeries()
   375  	defer ctx.Close()
   376  
   377  	// error - multiple divisor series
   378  	_, err := divideSeries(ctx, singlePathSpec{
   379  		Values: consolidationTestSeries,
   380  	}, singlePathSpec{
   381  		Values: consolidationTestSeries,
   382  	})
   383  	require.Error(t, err)
   384  	require.Equal(t, err.Error(), "divideSeries second argument must reference exactly one series but instead has 4")
   385  }
   386  
   387  func TestDivideSeriesLists(t *testing.T) {
   388  	ctx, consolidationTestSeries := newConsolidationTestSeries()
   389  	defer ctx.Close()
   390  
   391  	// multiple series, different start/end times
   392  	nan := math.NaN()
   393  	series, err := divideSeriesLists(ctx, singlePathSpec{
   394  		Values: consolidationTestSeries[:2],
   395  	}, singlePathSpec{
   396  		Values: consolidationTestSeries[2:],
   397  	})
   398  	require.Nil(t, err)
   399  	expected := []common.TestSeries{
   400  		{
   401  			Name: "divideSeries(a,c)",
   402  			Data: []float64{nan, nan, nan, 0.5882, 0.5882, 0.5882, nan, nan, nan},
   403  		},
   404  		{
   405  			Name: "divideSeries(b,d)",
   406  			Data: []float64{nan, nan, nan, 5, 5, 5, nan, nan, nan},
   407  		},
   408  	}
   409  
   410  	common.CompareOutputsAndExpected(t, 10000, consolidationStartTime,
   411  		[]common.TestSeries{expected[0]}, []*ts.Series{series.Values[0]})
   412  	common.CompareOutputsAndExpected(t, 10000, consolidationStartTime.Add(-30*time.Second),
   413  		[]common.TestSeries{expected[1]}, []*ts.Series{series.Values[1]})
   414  
   415  	// different millisPerStep, same start/end times
   416  	consolidationTestSeries[0], consolidationTestSeries[2] = consolidationTestSeries[2], consolidationTestSeries[0]
   417  	consolidationTestSeries[1], consolidationTestSeries[3] = consolidationTestSeries[3], consolidationTestSeries[1]
   418  	series, err = divideSeriesLists(ctx, singlePathSpec{
   419  		Values: consolidationTestSeries[:2],
   420  	}, singlePathSpec{
   421  		Values: consolidationTestSeries[2:],
   422  	})
   423  	require.Nil(t, err)
   424  	expected = []common.TestSeries{
   425  		{
   426  			Name: "divideSeries(c,a)",
   427  			Data: []float64{nan, nan, nan, 1.7, 1.7, 1.7, nan, nan, nan},
   428  		},
   429  		{
   430  			Name: "divideSeries(d,b)",
   431  			Data: []float64{nan, nan, nan, 0.2, 0.2, 0.2, nan, nan, nan},
   432  		},
   433  	}
   434  	common.CompareOutputsAndExpected(t, 10000, consolidationStartTime,
   435  		[]common.TestSeries{expected[0]}, []*ts.Series{series.Values[0]})
   436  
   437  	// error - multiple divisor series
   438  	series, err = divideSeries(ctx, singlePathSpec{
   439  		Values: consolidationTestSeries,
   440  	}, singlePathSpec{
   441  		Values: consolidationTestSeries,
   442  	})
   443  	require.Error(t, err)
   444  }
   445  
   446  // TestDivideSeriesListsWithUnsortedSeriesInput ensures that if input into
   447  // the function wasn't sorted as input that it becomes sorted before dividing
   448  // two series lists (to ensure deterministic results).
   449  func TestDivideSeriesListsWithUnsortedSeriesInput(t *testing.T) {
   450  	start := time.Now().Truncate(time.Minute).Add(-10 * time.Minute)
   451  	end := start.Add(5 * time.Minute)
   452  	ctx := common.NewContext(common.ContextOptions{Start: start, End: end})
   453  
   454  	dividend := []*ts.Series{
   455  		ts.NewSeries(ctx, "a", start,
   456  			ts.NewConstantValues(ctx, 1, 5, 60000)),
   457  		ts.NewSeries(ctx, "c", start,
   458  			ts.NewConstantValues(ctx, 3, 5, 60000)),
   459  		ts.NewSeries(ctx, "b", start,
   460  			ts.NewConstantValues(ctx, 2, 5, 60000)),
   461  	}
   462  
   463  	divisor := []*ts.Series{
   464  		ts.NewSeries(ctx, "b", start,
   465  			ts.NewConstantValues(ctx, 2, 5, 60000)),
   466  		ts.NewSeries(ctx, "a", start,
   467  			ts.NewConstantValues(ctx, 1, 5, 60000)),
   468  		ts.NewSeries(ctx, "d", start,
   469  			ts.NewConstantValues(ctx, 3, 5, 60000)),
   470  	}
   471  
   472  	actual, err := divideSeriesLists(ctx, singlePathSpec{
   473  		Values: dividend,
   474  	}, singlePathSpec{
   475  		Values: divisor,
   476  	})
   477  	require.Nil(t, err)
   478  	expected := []common.TestSeries{
   479  		{
   480  			Name: "divideSeries(a,a)",
   481  			Data: []float64{1, 1, 1, 1, 1},
   482  		},
   483  		{
   484  			Name: "divideSeries(b,b)",
   485  			Data: []float64{1, 1, 1, 1, 1},
   486  		},
   487  		{
   488  			Name: "divideSeries(c,d)",
   489  			Data: []float64{1, 1, 1, 1, 1},
   490  		},
   491  	}
   492  
   493  	common.CompareOutputsAndExpected(t, 60000, start, expected, actual.Values)
   494  }
   495  
   496  //nolint:govet
   497  func TestAverageSeriesWithWildcards(t *testing.T) {
   498  	ctx, _ := newConsolidationTestSeries()
   499  	defer ctx.Close()
   500  
   501  	input := []common.TestSeries{
   502  		{"web.host-1.avg-response.value", []float64{70.0, 20.0, 30.0, 40.0, 50.0}},
   503  		{"web.host-2.avg-response.value", []float64{20.0, 30.0, 40.0, 50.0, 60.0}},
   504  		{"web.host-3.avg-response.value", []float64{30.0, 40.0, 80.0, 60.0, 70.0}},
   505  		{"web.host-4.num-requests.value", []float64{10.0, 10.0, 15.0, 10.0, 15.0}},
   506  	}
   507  	expected := []common.TestSeries{
   508  		{"web.avg-response", []float64{40.0, 30.0, 50.0, 50.0, 60.0}},
   509  		{"web.num-requests", []float64{10.0, 10.0, 15.0, 10.0, 15.0}},
   510  	}
   511  
   512  	start := consolidationStartTime
   513  	step := 12000
   514  	timeSeries := generateSeriesList(ctx, start, input, step)
   515  	output, err := averageSeriesWithWildcards(ctx, singlePathSpec{
   516  		Values: timeSeries,
   517  	}, 1, 3)
   518  	require.NoError(t, err)
   519  	sort.Sort(TimeSeriesPtrVector(output.Values))
   520  	common.CompareOutputsAndExpected(t, step, start, expected, output.Values)
   521  }
   522  
   523  func createTestSeriesForAggregation() (*common.Context, []*ts.Series) {
   524  	var (
   525  		start, _ = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:41:19 GMT")
   526  		end, _   = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:43:19 GMT")
   527  		ctx      = common.NewContext(common.ContextOptions{Start: start, End: end})
   528  		series   = []*ts.Series{
   529  			ts.NewSeries(ctx, "servers.foo-1.pod1.status.500", start,
   530  				ts.NewConstantValues(ctx, 2, 12, 10000)),
   531  			ts.NewSeries(ctx, "servers.foo-2.pod1.status.500", start,
   532  				ts.NewConstantValues(ctx, 4, 12, 10000)),
   533  			ts.NewSeries(ctx, "servers.foo-3.pod1.status.500", start,
   534  				ts.NewConstantValues(ctx, 6, 12, 10000)),
   535  			ts.NewSeries(ctx, "servers.foo-1.pod2.status.500", start,
   536  				ts.NewConstantValues(ctx, 8, 12, 10000)),
   537  			ts.NewSeries(ctx, "servers.foo-2.pod2.status.500", start,
   538  				ts.NewConstantValues(ctx, 10, 12, 10000)),
   539  
   540  			ts.NewSeries(ctx, "servers.foo-1.pod1.status.400", start,
   541  				ts.NewConstantValues(ctx, 20, 12, 10000)),
   542  			ts.NewSeries(ctx, "servers.foo-2.pod1.status.400", start,
   543  				ts.NewConstantValues(ctx, 30, 12, 10000)),
   544  			ts.NewSeries(ctx, "servers.foo-3.pod2.status.400", start,
   545  				ts.NewConstantValues(ctx, 40, 12, 10000)),
   546  		}
   547  	)
   548  	return ctx, series
   549  }
   550  
   551  func TestSumSeriesWithWildcards(t *testing.T) {
   552  	ctx, inputs := createTestSeriesForAggregation()
   553  	defer ctx.Close()
   554  
   555  	outSeries, err := sumSeriesWithWildcards(ctx, singlePathSpec{
   556  		Values: inputs,
   557  	}, 1, 2)
   558  	require.NoError(t, err)
   559  	require.Equal(t, 2, len(outSeries.Values))
   560  
   561  	outSeries, _ = sortByName(ctx, singlePathSpec(outSeries), false, false)
   562  
   563  	expectedOutputs := []struct {
   564  		name      string
   565  		sumOfVals float64
   566  	}{
   567  		{"servers.status.400", 90 * 12},
   568  		{"servers.status.500", 30 * 12},
   569  	}
   570  
   571  	for i, expected := range expectedOutputs {
   572  		series := outSeries.Values[i]
   573  		assert.Equal(t, expected.name, series.Name())
   574  		assert.Equal(t, expected.sumOfVals, series.SafeSum())
   575  	}
   576  }
   577  
   578  func TestMultiplySeriesWithWildcards(t *testing.T) {
   579  	ctx, inputs := createTestSeriesForAggregation()
   580  	defer func() { _ = ctx.Close() }()
   581  
   582  	outSeries, err := multiplySeriesWithWildcards(ctx, singlePathSpec{
   583  		Values: inputs,
   584  	}, 1, 2)
   585  	require.NoError(t, err)
   586  	require.Equal(t, 2, len(outSeries.Values))
   587  
   588  	outSeries, _ = sortByName(ctx, singlePathSpec(outSeries), false, false)
   589  
   590  	expectedOutputs := []struct {
   591  		name      string
   592  		sumOfVals float64
   593  	}{
   594  		{"servers.status.400", 20 * 30 * 40 * 12},
   595  		{"servers.status.500", 2 * 4 * 6 * 8 * 10 * 12},
   596  	}
   597  
   598  	for i, expected := range expectedOutputs {
   599  		series := outSeries.Values[i]
   600  		assert.Equal(t, expected.name, series.Name())
   601  		assert.Equal(t, expected.sumOfVals, series.SafeSum())
   602  	}
   603  }
   604  
   605  func TestApplyByNode(t *testing.T) {
   606  	var (
   607  		ctrl          = xgomock.NewController(t)
   608  		store         = storage.NewMockStorage(ctrl)
   609  		engine        = NewEngine(store, CompileOptions{})
   610  		start, _      = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:41:19 GMT")
   611  		end, _        = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:43:19 GMT")
   612  		ctx           = common.NewContext(common.ContextOptions{Start: start, End: end, Engine: engine})
   613  		millisPerStep = 60000
   614  		inputs        = []*ts.Series{
   615  			ts.NewSeries(ctx, "servers.s1.disk.bytes_used", start,
   616  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{10, 20, 30})),
   617  			ts.NewSeries(ctx, "servers.s1.disk.bytes_free", start,
   618  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{90, 80, 70})),
   619  			ts.NewSeries(ctx, "servers.s2.disk.bytes_used", start,
   620  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{1, 2, 3})),
   621  			ts.NewSeries(ctx, "servers.s2.disk.bytes_free", start,
   622  				common.NewTestSeriesValues(ctx, millisPerStep, []float64{99, 98, 97})),
   623  		}
   624  	)
   625  
   626  	defer ctrl.Finish()
   627  	defer ctx.Close()
   628  
   629  	store.EXPECT().FetchByQuery(gomock.Any(), "servers.s1.disk.bytes_used", gomock.Any()).Return(
   630  		&storage.FetchResult{SeriesList: []*ts.Series{ts.NewSeries(ctx, "servers.s1.disk.bytes_used", start,
   631  			common.NewTestSeriesValues(ctx, 60000, []float64{10, 20, 30}))}}, nil).Times(2)
   632  
   633  	store.EXPECT().FetchByQuery(gomock.Any(), "servers.s1.disk.bytes_*", gomock.Any()).Return(
   634  		&storage.FetchResult{SeriesList: []*ts.Series{
   635  			ts.NewSeries(ctx, "servers.s1.disk.bytes_free", start,
   636  				common.NewTestSeriesValues(ctx, 60000, []float64{90, 80, 70})),
   637  			ts.NewSeries(ctx, "servers.s1.disk.bytes_used", start,
   638  				common.NewTestSeriesValues(ctx, 60000, []float64{10, 20, 30})),
   639  		}}, nil).Times(2)
   640  
   641  	store.EXPECT().FetchByQuery(gomock.Any(), "servers.s2.disk.bytes_used", gomock.Any()).Return(
   642  		&storage.FetchResult{SeriesList: []*ts.Series{ts.NewSeries(ctx, "servers.s2.disk.bytes_used", start,
   643  			common.NewTestSeriesValues(ctx, 60000, []float64{1, 2, 3}))}}, nil).Times(2)
   644  
   645  	store.EXPECT().FetchByQuery(gomock.Any(), "servers.s2.disk.bytes_*", gomock.Any()).Return(
   646  		&storage.FetchResult{SeriesList: []*ts.Series{
   647  			ts.NewSeries(ctx, "servers.s2.disk.bytes_free", start,
   648  				common.NewTestSeriesValues(ctx, 60000, []float64{99, 98, 97})),
   649  			ts.NewSeries(ctx, "servers.s2.disk.bytes_used", start,
   650  				common.NewTestSeriesValues(ctx, 60000, []float64{1, 2, 3})),
   651  		}}, nil).Times(2)
   652  
   653  	tests := []struct {
   654  		nodeNum          int
   655  		templateFunction string
   656  		newName          string
   657  		expectedResults  []common.TestSeries
   658  	}{
   659  		{
   660  			nodeNum:          1,
   661  			templateFunction: "divideSeries(%.disk.bytes_used, sumSeries(%.disk.bytes_*))",
   662  			newName:          "",
   663  			expectedResults: []common.TestSeries{
   664  				{
   665  					Name: "divideSeries(servers.s1.disk.bytes_used,sumSeries(servers.s1.disk.bytes_*))",
   666  					Data: []float64{0.10, 0.20, 0.30},
   667  				},
   668  				{
   669  					Name: "divideSeries(servers.s2.disk.bytes_used,sumSeries(servers.s2.disk.bytes_*))",
   670  					Data: []float64{0.01, 0.02, 0.03},
   671  				},
   672  			},
   673  		},
   674  		{
   675  			nodeNum:          1,
   676  			templateFunction: "divideSeries(%.disk.bytes_used, sumSeries(%.disk.bytes_*))",
   677  			newName:          "%.disk.pct_used",
   678  			expectedResults: []common.TestSeries{
   679  				{
   680  					Name: "servers.s1.disk.pct_used",
   681  					Data: []float64{0.10, 0.20, 0.30},
   682  				},
   683  				{
   684  					Name: "servers.s2.disk.pct_used",
   685  					Data: []float64{0.01, 0.02, 0.03},
   686  				},
   687  			},
   688  		},
   689  	}
   690  
   691  	for _, test := range tests {
   692  		outSeries, err := applyByNode(
   693  			ctx,
   694  			singlePathSpec{
   695  				Values: inputs,
   696  			},
   697  			test.nodeNum,
   698  			test.templateFunction,
   699  			test.newName,
   700  		)
   701  		require.NoError(t, err)
   702  		require.Equal(t, len(test.expectedResults), len(outSeries.Values))
   703  
   704  		outSeries, _ = sortByName(ctx, singlePathSpec(outSeries), false, false)
   705  		common.CompareOutputsAndExpected(t, 60000, start, test.expectedResults, outSeries.Values)
   706  	}
   707  }
   708  
   709  func TestAggregateWithWildcards(t *testing.T) {
   710  	ctx, inputs := createTestSeriesForAggregation()
   711  	defer ctx.Close()
   712  
   713  	type result struct {
   714  		name      string
   715  		sumOfVals float64
   716  	}
   717  
   718  	tests := []struct {
   719  		fname           string
   720  		nodes           []int
   721  		expectedResults []result
   722  	}{
   723  		{"avg", []int{1, 2}, []result{
   724  			{"servers.status.400", ((20 + 30 + 40) / 3) * 12},
   725  			{"servers.status.500", ((2 + 4 + 6 + 8 + 10) / 5) * 12},
   726  		}},
   727  		{"max", []int{2, 4}, []result{
   728  			{"servers.status.400", 40 * 12},
   729  			{"servers.status.500", 10 * 12},
   730  		}},
   731  		{"min", []int{2, -1}, []result{
   732  			{"servers.status.400", 20 * 12},
   733  			{"servers.status.500", 2 * 12},
   734  		}},
   735  		{"median", []int{1, 2}, []result{
   736  			{"servers.status.400", 30 * 12},
   737  			{"servers.status.500", 6 * 12},
   738  		}},
   739  	}
   740  
   741  	for _, test := range tests {
   742  		outSeries, err := aggregateWithWildcards(ctx, singlePathSpec{
   743  			Values: inputs,
   744  		}, test.fname, 1, 2)
   745  		require.NoError(t, err)
   746  		require.Equal(t, 2, len(outSeries.Values))
   747  
   748  		outSeries, _ = sortByName(ctx, singlePathSpec(outSeries), false, false)
   749  
   750  		for i, expected := range test.expectedResults {
   751  			series := outSeries.Values[i]
   752  			assert.Equal(t, expected.name, series.Name(), "wrong name for %v %s (%d)", test.nodes, test.fname, i)
   753  
   754  			assert.Equal(t, expected.sumOfVals, series.SafeSum(),
   755  				"wrong result for %v %s (%d)", test.nodes, test.fname, i)
   756  		}
   757  	}
   758  }
   759  
   760  func TestGroupByNode(t *testing.T) {
   761  	ctx, inputs := createTestSeriesForAggregation()
   762  	defer ctx.Close()
   763  
   764  	type result struct {
   765  		name      string
   766  		sumOfVals float64
   767  	}
   768  
   769  	tests := []struct {
   770  		fname           string
   771  		node            int
   772  		expectedResults []result
   773  	}{
   774  		{"avg", 4, []result{
   775  			{"400", ((20 + 30 + 40) / 3) * 12},
   776  			{"500", ((2 + 4 + 6 + 8 + 10) / 5) * 12},
   777  		}},
   778  		{"max", 2, []result{
   779  			{"pod1", 30 * 12},
   780  			{"pod2", 40 * 12},
   781  		}},
   782  		{"min", -1, []result{
   783  			{"400", 20 * 12},
   784  			{"500", 2 * 12},
   785  		}},
   786  	}
   787  
   788  	for _, test := range tests {
   789  		outSeries, err := groupByNode(ctx, singlePathSpec{
   790  			Values: inputs,
   791  		}, test.node, test.fname)
   792  		require.NoError(t, err)
   793  		require.Equal(t, len(test.expectedResults), len(outSeries.Values))
   794  
   795  		outSeries, _ = sortByName(ctx, singlePathSpec(outSeries), false, false)
   796  
   797  		for i, expected := range test.expectedResults {
   798  			series := outSeries.Values[i]
   799  			assert.Equal(t, expected.name, series.Name(),
   800  				"wrong name for %d %s (%d)", test.node, test.fname, i)
   801  			assert.Equal(t, expected.sumOfVals, series.SafeSum(),
   802  				"wrong result for %d %s (%d)", test.node, test.fname, i)
   803  		}
   804  	}
   805  }
   806  
   807  func TestGroupByNodes(t *testing.T) {
   808  	var (
   809  		start, _ = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:41:19 GMT")
   810  		end, _   = time.Parse(time.RFC1123, "Mon, 27 Jul 2015 19:43:19 GMT")
   811  		ctx      = common.NewContext(common.ContextOptions{Start: start, End: end})
   812  		inputs   = []*ts.Series{
   813  			ts.NewSeries(ctx, "transformNull(servers.foo-1.pod1.status.500)", start,
   814  				ts.NewConstantValues(ctx, 2, 12, 10000)),
   815  			ts.NewSeries(ctx, "scaleToSeconds(servers.foo-2.pod1.status.500,1)", start,
   816  				ts.NewConstantValues(ctx, 4, 12, 10000)),
   817  			ts.NewSeries(ctx, "servers.foo-3.pod1.status.500", start,
   818  				ts.NewConstantValues(ctx, 6, 12, 10000)),
   819  			ts.NewSeries(ctx, "servers.foo-1.pod2.status.500", start,
   820  				ts.NewConstantValues(ctx, 8, 12, 10000)),
   821  			ts.NewSeries(ctx, "servers.foo-2.pod2.status.500", start,
   822  				ts.NewConstantValues(ctx, 10, 12, 10000)),
   823  
   824  			ts.NewSeries(ctx, "servers.foo-1.pod1.status.400", start,
   825  				ts.NewConstantValues(ctx, 20, 12, 10000)),
   826  			ts.NewSeries(ctx, "servers.foo-2.pod1.status.400", start,
   827  				ts.NewConstantValues(ctx, 30, 12, 10000)),
   828  			ts.NewSeries(ctx, "servers.foo-3.pod2.status.400", start,
   829  				ts.NewConstantValues(ctx, 40, 12, 10000)),
   830  		}
   831  	)
   832  
   833  	defer ctx.Close()
   834  
   835  	type result struct {
   836  		name      string
   837  		sumOfVals float64
   838  	}
   839  
   840  	tests := []struct {
   841  		fname           string
   842  		nodes           []int
   843  		expectedResults []result
   844  	}{
   845  		{"avg", []int{2, 4}, []result{ // test normal group by nodes
   846  			{"pod1.400", ((20 + 30) / 2) * 12},
   847  			{"pod1.500", ((2 + 4 + 6) / 3) * 12},
   848  			{"pod2.400", (40 / 1) * 12},
   849  			{"pod2.500", ((8 + 10) / 2) * 12},
   850  		}},
   851  		{"max", []int{2, 4}, []result{ // test with different function
   852  			{"pod1.400", 30 * 12},
   853  			{"pod1.500", 6 * 12},
   854  			{"pod2.400", 40 * 12},
   855  			{"pod2.500", 10 * 12},
   856  		}},
   857  		{"median", []int{2, 4}, []result{ // test with different function
   858  			{"pod1.400", ((20 + 30) / 2) * 12},
   859  			{"pod1.500", 4 * 12},
   860  			{"pod2.400", 40 * 12},
   861  			{"pod2.500", ((8 + 10) / 2) * 12},
   862  		}},
   863  		{"max", []int{2, 4, 100}, []result{ // test with a node number that exceeds num parts
   864  			{"pod1.400.", 30 * 12},
   865  			{"pod1.500.", 6 * 12},
   866  			{"pod2.400.", 40 * 12},
   867  			{"pod2.500.", 10 * 12},
   868  		}},
   869  		{"min", []int{2, -1}, []result{ // test negative index handling
   870  			{"pod1.400", 20 * 12},
   871  			{"pod1.500", 2 * 12},
   872  			{"pod2.400", 40 * 12},
   873  			{"pod2.500", 8 * 12},
   874  		}},
   875  		{"sum", []int{}, []result{ // test empty slice handing.
   876  			{
   877  				"sumSeries(transformNull(servers.foo-1.pod1.status.500)," +
   878  					"scaleToSeconds(servers.foo-2.pod1.status.500,1)," +
   879  					"servers.foo-3.pod1.status.500,servers.foo-1.pod2.status.500," +
   880  					"servers.foo-2.pod2.status.500,servers.foo-1.pod1.status.400," +
   881  					"servers.foo-2.pod1.status.400,servers.foo-3.pod2.status.400)",
   882  				(2 + 4 + 6 + 8 + 10 + 20 + 30 + 40) * 12,
   883  			},
   884  		}},
   885  		{"sum", []int{100}, []result{ // test all nodes out of bounds
   886  			{"", (2 + 4 + 6 + 8 + 10 + 20 + 30 + 40) * 12},
   887  		}},
   888  	}
   889  
   890  	for _, test := range tests {
   891  		outSeries, err := groupByNodes(ctx, singlePathSpec{
   892  			Values: inputs,
   893  		}, test.fname, test.nodes...)
   894  		require.NoError(t, err)
   895  		require.Equal(t, len(test.expectedResults), len(outSeries.Values))
   896  
   897  		outSeries, _ = sortByName(ctx, singlePathSpec(outSeries), false, false)
   898  
   899  		for i, expected := range test.expectedResults {
   900  			series := outSeries.Values[i]
   901  			assert.Equal(t, expected.name, series.Name(),
   902  				"wrong name for %v %s (%d)", test.nodes, test.fname, i)
   903  			assert.Equal(t, expected.sumOfVals, series.SafeSum(),
   904  				"wrong result for %v %s (%d)", test.nodes, test.fname, i)
   905  		}
   906  	}
   907  }
   908  
   909  func TestWeightedAverage(t *testing.T) {
   910  	ctx, _ := newConsolidationTestSeries()
   911  	defer ctx.Close()
   912  
   913  	means := []common.TestSeries{
   914  		{Name: "web.host-1.avg-response.mean", Data: []float64{70.0, 20.0, 30.0, 0.0, 50.0}},
   915  		{Name: "web.host-2.avg-response.mean", Data: []float64{20.0, 30.0, 40.0, 50.0, 60.0}},
   916  		{Name: "web.host-3.avg-response.mean", Data: []float64{20.0, 30.0, 40.0, 50.0, 60.0}}, // no match
   917  	}
   918  	counts := []common.TestSeries{
   919  		{Name: "web.host-1.avg-response.count", Data: []float64{1, 2, 3, 4, 5}},
   920  		{Name: "web.host-2.avg-response.count", Data: []float64{10, 20, 30, 40, 50}},
   921  		{Name: "web.host-4.avg-response.count", Data: []float64{10, 20, 30, 40, 50}}, // no match
   922  	}
   923  	expected := []common.TestSeries{
   924  		{Name: "weightedAverage", Data: []float64{24.5454, 29.0909, 39.0909, 45.4545, 59.0909}},
   925  	}
   926  
   927  	// normal series
   928  	start := consolidationStartTime
   929  	step := 12000
   930  	values := ts.SeriesList{Values: generateSeriesList(ctx, start, means, step)}
   931  	weights := ts.SeriesList{Values: generateSeriesList(ctx, start, counts, step)}
   932  	output, err := weightedAverage(ctx, singlePathSpec(values), singlePathSpec(weights), 1)
   933  	require.NoError(t, err)
   934  	sort.Sort(TimeSeriesPtrVector(output.Values))
   935  	common.CompareOutputsAndExpected(t, step, start, expected, output.Values)
   936  
   937  	// one series as input, should return the same as output no matter what the weight
   938  	values = ts.SeriesList{Values: generateSeriesList(ctx, start, means[:1], step)}
   939  	weights = ts.SeriesList{Values: generateSeriesList(ctx, start, counts[:1], step)}
   940  	output, err = weightedAverage(ctx, singlePathSpec(values), singlePathSpec(weights), 1)
   941  	require.NoError(t, err)
   942  	common.CompareOutputsAndExpected(t, step, start,
   943  		[]common.TestSeries{{Name: "weightedAverage", Data: means[0].Data}}, output.Values)
   944  
   945  	// different steps should lead to error -- not supported yet
   946  	values = ts.SeriesList{Values: generateSeriesList(ctx, start, means, step)}
   947  	weights = ts.SeriesList{Values: generateSeriesList(ctx, start, counts, step*2)}
   948  	output, err = weightedAverage(ctx, singlePathSpec(values), singlePathSpec(weights), 1)
   949  	require.EqualError(t, err, "different step sizes in input series not supported")
   950  }
   951  
   952  func TestCountSeries(t *testing.T) {
   953  	ctx, input := newConsolidationTestSeries()
   954  	defer ctx.Close()
   955  
   956  	results, err := countSeries(ctx, multiplePathSpecs(ts.SeriesList{
   957  		Values: input,
   958  	}))
   959  	expected := common.TestSeries{
   960  		Name: "countSeries(a,b,c,d)",
   961  		Data: []float64{4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4},
   962  	}
   963  	require.Nil(t, err)
   964  	common.CompareOutputsAndExpected(t, input[1].MillisPerStep(), input[1].StartTime(),
   965  		[]common.TestSeries{expected}, results.Values)
   966  }