k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/measurement/common/generic_query_measurement_test.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package common
    18  
    19  import (
    20  	"encoding/json"
    21  	"testing"
    22  	"time"
    23  
    24  	"github.com/prometheus/common/model"
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  	"k8s.io/perf-tests/clusterloader2/pkg/measurement"
    28  	measurementutil "k8s.io/perf-tests/clusterloader2/pkg/measurement/util"
    29  )
    30  
    31  type fakeQueryExecutor struct {
    32  	samples map[string][]*model.Sample
    33  }
    34  
    35  func (f fakeQueryExecutor) Query(query string, _ time.Time) ([]*model.Sample, error) {
    36  	return f.samples[query], nil
    37  }
    38  
    39  func TestGather(t *testing.T) {
    40  	testCases := []struct {
    41  		desc             string
    42  		params           map[string]interface{}
    43  		samples          map[string][]*model.Sample
    44  		wantDataItems    []measurementutil.DataItem
    45  		wantConfigureErr string
    46  		wantErr          string
    47  	}{
    48  		{
    49  			desc: "happy path",
    50  			params: map[string]interface{}{
    51  				"metricName":    "happy-path",
    52  				"metricVersion": "v1",
    53  				"unit":          "ms",
    54  				"queries": []map[string]interface{}{
    55  					{
    56  						"name":      "no-samples",
    57  						"query":     "no-samples-query[%v]",
    58  						"threshold": 42,
    59  					},
    60  					{
    61  						"name":      "below-threshold",
    62  						"query":     "below-threshold-query[%v]",
    63  						"threshold": 30,
    64  					},
    65  					{
    66  						"name":  "no-threshold",
    67  						"query": "no-threshold-query[%v]",
    68  					},
    69  					{
    70  						"name":  "multiple-duration-placeholders",
    71  						"query": "placeholder-a[%v] + placeholder-b[%v]",
    72  					},
    73  				},
    74  			},
    75  			samples: map[string][]*model.Sample{
    76  				"below-threshold-query[60s]":              {{Value: model.SampleValue(7)}},
    77  				"no-threshold-query[60s]":                 {{Value: model.SampleValue(120)}},
    78  				"placeholder-a[60s] + placeholder-b[60s]": {{Value: model.SampleValue(5)}},
    79  			},
    80  			wantDataItems: []measurementutil.DataItem{
    81  				{
    82  					Unit: "ms",
    83  					Data: map[string]float64{
    84  						"below-threshold":                7.0,
    85  						"no-threshold":                   120.0,
    86  						"multiple-duration-placeholders": 5.0,
    87  					},
    88  				},
    89  			},
    90  		},
    91  		{
    92  			desc: "no samples, but samples not required",
    93  			params: map[string]interface{}{
    94  				"metricName":    "no-samples",
    95  				"metricVersion": "v1",
    96  				"unit":          "ms",
    97  				"queries": []map[string]interface{}{
    98  					{
    99  						"name":  "no-samples",
   100  						"query": "no-samples-query[%v]",
   101  					},
   102  				},
   103  			},
   104  		},
   105  		{
   106  			desc: "no samples, but samples required",
   107  			params: map[string]interface{}{
   108  				"metricName":    "no-samples",
   109  				"metricVersion": "v1",
   110  				"unit":          "ms",
   111  				"queries": []map[string]interface{}{
   112  					{
   113  						"name":           "no-samples",
   114  						"query":          "no-samples-query[%v]",
   115  						"requireSamples": true,
   116  					},
   117  				},
   118  			},
   119  			wantErr: "no samples",
   120  		},
   121  		{
   122  			desc: "too many samples",
   123  			params: map[string]interface{}{
   124  				"metricName":    "many-samples",
   125  				"metricVersion": "v1",
   126  				"unit":          "ms",
   127  				"queries": []map[string]interface{}{
   128  					{
   129  						"name":  "many-samples",
   130  						"query": "many-samples-query[%v]",
   131  					},
   132  				},
   133  			},
   134  			samples: map[string][]*model.Sample{
   135  				"many-samples-query[60s]": {
   136  					{Value: model.SampleValue(1)},
   137  					{Value: model.SampleValue(2)},
   138  				},
   139  			},
   140  			wantErr: "too many samples",
   141  			// When too many samples, first value is returned and error is raised.
   142  			wantDataItems: []measurementutil.DataItem{
   143  				{
   144  					Unit: "ms",
   145  					Data: map[string]float64{
   146  						"many-samples": 1.0,
   147  					},
   148  				},
   149  			},
   150  		},
   151  		{
   152  			desc: "sample above threshold",
   153  			params: map[string]interface{}{
   154  				"metricName":    "above-threshold",
   155  				"metricVersion": "v1",
   156  				"unit":          "ms",
   157  				"queries": []map[string]interface{}{
   158  					{
   159  						"name":      "above-threshold",
   160  						"query":     "above-threshold-query[%v]",
   161  						"threshold": 60,
   162  					},
   163  				},
   164  			},
   165  			samples: map[string][]*model.Sample{
   166  				"above-threshold-query[60s]": {{Value: model.SampleValue(123)}},
   167  			},
   168  			wantErr: "sample above threshold: want: less or equal than 60, got: 123",
   169  			wantDataItems: []measurementutil.DataItem{
   170  				{
   171  					Unit: "ms",
   172  					Data: map[string]float64{
   173  						"above-threshold": 123.0,
   174  					},
   175  				},
   176  			},
   177  		},
   178  		{
   179  			desc: "sample above lower bound",
   180  			params: map[string]interface{}{
   181  				"metricName":    "below-threshold",
   182  				"metricVersion": "v1",
   183  				"unit":          "ms",
   184  				"queries": []map[string]interface{}{
   185  					{
   186  						"name":       "above-threshold",
   187  						"query":      "above-threshold-query[%v]",
   188  						"threshold":  60,
   189  						"lowerBound": true,
   190  					},
   191  				},
   192  			},
   193  			samples: map[string][]*model.Sample{
   194  				"above-threshold-query[60s]": {{Value: model.SampleValue(74)}},
   195  			},
   196  			wantDataItems: []measurementutil.DataItem{
   197  				{
   198  					Unit: "ms",
   199  					Data: map[string]float64{
   200  						"above-threshold": 74.0,
   201  					},
   202  				},
   203  			},
   204  		},
   205  		{
   206  			desc: "sample below lower bound",
   207  			params: map[string]interface{}{
   208  				"metricName":    "below-threshold",
   209  				"metricVersion": "v1",
   210  				"unit":          "ms",
   211  				"queries": []map[string]interface{}{
   212  					{
   213  						"name":       "below-threshold",
   214  						"query":      "below-threshold-query[%v]",
   215  						"threshold":  60,
   216  						"lowerBound": true,
   217  					},
   218  				},
   219  			},
   220  			samples: map[string][]*model.Sample{
   221  				"below-threshold-query[60s]": {{Value: model.SampleValue(42)}},
   222  			},
   223  			wantErr: "sample below threshold: want: greater or equal than 60, got: 42",
   224  			wantDataItems: []measurementutil.DataItem{
   225  				{
   226  					Unit: "ms",
   227  					Data: map[string]float64{
   228  						"below-threshold": 42.0,
   229  					},
   230  				},
   231  			},
   232  		},
   233  		{
   234  			desc: "missing field metricName",
   235  			params: map[string]interface{}{
   236  				"metricVersion": "v1",
   237  				"unit":          "ms",
   238  				"queries": []map[string]interface{}{
   239  					{
   240  						"name":      "no-samples",
   241  						"query":     "no-samples-query[%v]",
   242  						"threshold": 42,
   243  					},
   244  					{
   245  						"name":      "below-threshold",
   246  						"query":     "below-threshold-query[%v]",
   247  						"threshold": 30,
   248  					},
   249  					{
   250  						"name":  "no-threshold",
   251  						"query": "no-threshold-query[%v]",
   252  					},
   253  				},
   254  			},
   255  			wantConfigureErr: "metricName is required",
   256  		},
   257  		{
   258  			desc: "dimensions",
   259  			params: map[string]interface{}{
   260  				"metricName":    "dimensions",
   261  				"metricVersion": "v1",
   262  				"unit":          "ms",
   263  				"dimensions": []interface{}{
   264  					"d1",
   265  					"d2",
   266  				},
   267  				"queries": []map[string]interface{}{
   268  					{
   269  						"name":  "perc99",
   270  						"query": "query-perc99[%v]",
   271  					},
   272  					{
   273  						"name":  "perc90",
   274  						"query": "query-perc90[%v]",
   275  					},
   276  				},
   277  			},
   278  			samples: map[string][]*model.Sample{
   279  				"query-perc99[60s]": {
   280  					{
   281  						Metric: model.Metric{
   282  							model.LabelName("d1"): model.LabelValue("d1-val1"),
   283  							model.LabelName("d2"): model.LabelValue("d2-val1"),
   284  							model.LabelName("d3"): model.LabelValue("d3-val1"), // Ignored
   285  						},
   286  						Value: model.SampleValue(1),
   287  					},
   288  					{
   289  						Metric: model.Metric{
   290  							model.LabelName("d1"): model.LabelValue("d1-val1"),
   291  							model.LabelName("d2"): model.LabelValue("d2-val2"),
   292  						},
   293  						Value: model.SampleValue(2),
   294  					},
   295  				},
   296  				"query-perc90[60s]": {
   297  					{
   298  						Metric: model.Metric{
   299  							model.LabelName("d1"): model.LabelValue("d1-val1"),
   300  							model.LabelName("d2"): model.LabelValue("d2-val1"),
   301  						},
   302  						Value: model.SampleValue(3),
   303  					},
   304  					{
   305  						Metric: model.Metric{
   306  							model.LabelName("d1"): model.LabelValue("d1-val1"),
   307  							model.LabelName("d2"): model.LabelValue("d2-val2"),
   308  						},
   309  						Value: model.SampleValue(4),
   310  					},
   311  					{
   312  						Metric: model.Metric{
   313  							model.LabelName("d1"): model.LabelValue("d1-val1"),
   314  							// d2 not set
   315  						},
   316  						Value: model.SampleValue(5),
   317  					},
   318  				},
   319  			},
   320  			wantDataItems: []measurementutil.DataItem{
   321  				{
   322  					Labels: map[string]string{
   323  						"d1": "d1-val1",
   324  						"d2": "d2-val1",
   325  					},
   326  					Unit: "ms",
   327  					Data: map[string]float64{
   328  						"perc99": 1.0,
   329  						"perc90": 3.0,
   330  					},
   331  				},
   332  				{
   333  					Labels: map[string]string{
   334  						"d1": "d1-val1",
   335  						"d2": "d2-val2",
   336  					},
   337  					Unit: "ms",
   338  					Data: map[string]float64{
   339  						"perc99": 2.0,
   340  						"perc90": 4.0,
   341  					},
   342  				},
   343  				{
   344  					Labels: map[string]string{
   345  						"d1": "d1-val1",
   346  						"d2": "",
   347  					},
   348  					Unit: "ms",
   349  					Data: map[string]float64{
   350  						// perc99 doesn't return this combination.
   351  						"perc90": 5.0,
   352  					},
   353  				},
   354  			},
   355  		},
   356  		{
   357  			desc: "multiple values for single dimension",
   358  			params: map[string]interface{}{
   359  				"metricName":    "dimensions",
   360  				"metricVersion": "v1",
   361  				"unit":          "ms",
   362  				"dimensions": []interface{}{
   363  					"d1",
   364  					"d2",
   365  				},
   366  				"queries": []map[string]interface{}{
   367  					{
   368  						"name":  "perc99",
   369  						"query": "query-perc99[%v]",
   370  					},
   371  				},
   372  			},
   373  			samples: map[string][]*model.Sample{
   374  				"query-perc99[60s]": {
   375  					{
   376  						Metric: model.Metric{
   377  							model.LabelName("d1"): model.LabelValue("d1-val1"),
   378  							model.LabelName("d2"): model.LabelValue("d2-val1"),
   379  						},
   380  						Value: model.SampleValue(1),
   381  					},
   382  					{
   383  						Metric: model.Metric{
   384  							model.LabelName("d1"): model.LabelValue("d1-val1"),
   385  							model.LabelName("d2"): model.LabelValue("d2-val1"),
   386  						},
   387  						Value: model.SampleValue(2),
   388  					},
   389  				},
   390  			},
   391  			wantErr: "too many samples for [d1-val1 d2-val1]",
   392  			wantDataItems: []measurementutil.DataItem{
   393  				{
   394  					Labels: map[string]string{
   395  						"d1": "d1-val1",
   396  						"d2": "d2-val1",
   397  					},
   398  					Unit: "ms",
   399  					Data: map[string]float64{
   400  						"perc99": 1.0,
   401  					},
   402  				},
   403  			},
   404  		},
   405  	}
   406  
   407  	for _, tc := range testCases {
   408  		t.Run(tc.desc, func(t *testing.T) {
   409  			gatherer := &genericQueryGatherer{}
   410  			err := gatherer.Configure(&measurement.Config{Params: tc.params})
   411  			if tc.wantConfigureErr != "" {
   412  				assert.Contains(t, err.Error(), tc.wantConfigureErr)
   413  				return
   414  			}
   415  			assert.Nil(t, err)
   416  			startTime := time.Now()
   417  			endTime := startTime.Add(1 * time.Minute)
   418  			executor := fakeQueryExecutor{tc.samples}
   419  
   420  			summaries, err := gatherer.Gather(executor, startTime, endTime, nil)
   421  			if tc.wantErr != "" {
   422  				assert.Contains(t, err.Error(), tc.wantErr)
   423  			} else {
   424  				assert.Nil(t, err)
   425  			}
   426  			require.Len(t, summaries, 1)
   427  			content := summaries[0].SummaryContent()
   428  			perfData := measurementutil.PerfData{}
   429  			err = json.Unmarshal([]byte(content), &perfData)
   430  			require.Nil(t, err)
   431  			assert.ElementsMatch(t, perfData.DataItems, tc.wantDataItems)
   432  		})
   433  	}
   434  }