github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/query/api/v1/handler/graphite/find_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 graphite
    22  
    23  import (
    24  	"bytes"
    25  	"encoding/json"
    26  	"fmt"
    27  	"net/http"
    28  	"net/url"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/m3db/m3/src/m3ninx/doc"
    33  	"github.com/m3db/m3/src/query/api/v1/handler/prometheus/handleroptions"
    34  	"github.com/m3db/m3/src/query/api/v1/options"
    35  	"github.com/m3db/m3/src/query/block"
    36  	"github.com/m3db/m3/src/query/graphite/graphite"
    37  	"github.com/m3db/m3/src/query/models"
    38  	"github.com/m3db/m3/src/query/storage"
    39  	"github.com/m3db/m3/src/query/storage/m3/consolidators"
    40  	"github.com/m3db/m3/src/x/headers"
    41  	xtest "github.com/m3db/m3/src/x/test"
    42  	xtime "github.com/m3db/m3/src/x/time"
    43  
    44  	"github.com/golang/mock/gomock"
    45  	"github.com/stretchr/testify/assert"
    46  	"github.com/stretchr/testify/require"
    47  )
    48  
    49  // dates is a tuple of a date with a valid string representation
    50  type date struct {
    51  	t xtime.UnixNano
    52  	s string
    53  }
    54  
    55  var (
    56  	from = date{
    57  		s: "14:38_20150618",
    58  		t: xtime.ToUnixNano(time.Date(2015, time.June, 18, 14, 38, 0, 0, time.UTC)),
    59  	}
    60  	until = date{
    61  		s: "1432581620",
    62  		t: xtime.ToUnixNano(time.Date(2015, time.May, 25, 19, 20, 20, 0, time.UTC)),
    63  	}
    64  )
    65  
    66  type completeTagQueryMatcher struct {
    67  	matchers                 []models.Matcher
    68  	filterNameTagsIndexStart int
    69  	filterNameTagsIndexEnd   int
    70  }
    71  
    72  func (m *completeTagQueryMatcher) String() string {
    73  	q := storage.CompleteTagsQuery{
    74  		TagMatchers: m.matchers,
    75  	}
    76  	return q.String()
    77  }
    78  
    79  func (m *completeTagQueryMatcher) Matches(x interface{}) bool {
    80  	q, ok := x.(*storage.CompleteTagsQuery)
    81  	if !ok {
    82  		return false
    83  	}
    84  
    85  	if !q.Start.Equal(from.t) {
    86  		return false
    87  	}
    88  
    89  	if !q.End.Equal(until.t) {
    90  		return false
    91  	}
    92  
    93  	if q.CompleteNameOnly {
    94  		return false
    95  	}
    96  
    97  	if m.filterNameTagsIndexStart == 0 && m.filterNameTagsIndexEnd == 0 {
    98  		// Default query completing single graphite path index value.
    99  		if len(q.FilterNameTags) != 1 {
   100  			return false
   101  		}
   102  
   103  		// Both queries should filter on __g1__.
   104  		if !bytes.Equal(q.FilterNameTags[0], []byte("__g1__")) {
   105  			return false
   106  		}
   107  	} else {
   108  		// Unterminated query completing many grapth path index values.
   109  		n := m.filterNameTagsIndexEnd
   110  		expected := make([][]byte, 0, n)
   111  		for i := m.filterNameTagsIndexStart; i < m.filterNameTagsIndexEnd; i++ {
   112  			expected = append(expected, graphite.TagName(i))
   113  		}
   114  
   115  		if len(q.FilterNameTags) != len(expected) {
   116  			return false
   117  		}
   118  
   119  		for i := range expected {
   120  			if !bytes.Equal(q.FilterNameTags[i], expected[i]) {
   121  				return false
   122  			}
   123  		}
   124  	}
   125  
   126  	if len(q.TagMatchers) != len(m.matchers) {
   127  		return false
   128  	}
   129  
   130  	for i, qMatcher := range q.TagMatchers {
   131  		if !bytes.Equal(qMatcher.Name, m.matchers[i].Name) {
   132  			return false
   133  		}
   134  		if !bytes.Equal(qMatcher.Value, m.matchers[i].Value) {
   135  			return false
   136  		}
   137  		if qMatcher.Type != m.matchers[i].Type {
   138  			return false
   139  		}
   140  	}
   141  
   142  	return true
   143  }
   144  
   145  var _ gomock.Matcher = &completeTagQueryMatcher{}
   146  
   147  func b(s string) []byte { return []byte(s) }
   148  func bs(ss ...string) [][]byte {
   149  	bb := make([][]byte, len(ss))
   150  	for i, s := range ss {
   151  		bb[i] = b(s)
   152  	}
   153  	return bb
   154  }
   155  
   156  type writer struct {
   157  	results []string
   158  	header  http.Header
   159  }
   160  
   161  var _ http.ResponseWriter = &writer{}
   162  
   163  func (w *writer) WriteHeader(_ int) {}
   164  func (w *writer) Header() http.Header {
   165  	if w.header == nil {
   166  		w.header = make(http.Header)
   167  	}
   168  
   169  	return w.header
   170  }
   171  
   172  func (w *writer) Write(b []byte) (int, error) {
   173  	if w.results == nil {
   174  		w.results = make([]string, 0, 10)
   175  	}
   176  
   177  	w.results = append(w.results, string(b))
   178  	return len(b), nil
   179  }
   180  
   181  type result struct {
   182  	ID            string `json:"id"`
   183  	Text          string `json:"text"`
   184  	Leaf          int    `json:"leaf"`
   185  	Expandable    int    `json:"expandable"`
   186  	AllowChildren int    `json:"allowChildren"`
   187  }
   188  
   189  type results []result
   190  
   191  func makeNoChildrenResult(id, text string) result {
   192  	return result{
   193  		ID:            id,
   194  		Text:          text,
   195  		Leaf:          1,
   196  		Expandable:    0,
   197  		AllowChildren: 0,
   198  	}
   199  }
   200  
   201  func makeWithChildrenResult(id, text string) result {
   202  	return result{
   203  		ID:            id,
   204  		Text:          text,
   205  		Leaf:          0,
   206  		Expandable:    1,
   207  		AllowChildren: 1,
   208  	}
   209  }
   210  
   211  type limitTest struct {
   212  	name    string
   213  	ex, ex2 bool
   214  	header  string
   215  }
   216  
   217  var (
   218  	bothCompleteLimitTest = limitTest{"both complete", true, true, ""}
   219  	limitTests            = []limitTest{
   220  		bothCompleteLimitTest,
   221  		{
   222  			"both incomplete", false, false,
   223  			fmt.Sprintf("%s,%s_%s", headers.LimitHeaderSeriesLimitApplied, "foo", "bar"),
   224  		},
   225  		{
   226  			"with terminator incomplete", true, false,
   227  			"foo_bar",
   228  		},
   229  		{
   230  			"with children incomplete", false, true,
   231  			headers.LimitHeaderSeriesLimitApplied,
   232  		},
   233  	}
   234  )
   235  
   236  func TestFind(t *testing.T) {
   237  	for _, httpMethod := range FindHTTPMethods {
   238  		testFind(t, testFindOptions{
   239  			httpMethod: httpMethod,
   240  		})
   241  	}
   242  }
   243  
   244  type testFindOptions struct {
   245  	httpMethod string
   246  }
   247  
   248  type testFindQuery struct {
   249  	expectMatchers *completeTagQueryMatcher
   250  	mockResult     func(lt limitTest) *consolidators.CompleteTagsResult
   251  }
   252  
   253  func testFind(t *testing.T, opts testFindOptions) {
   254  	warningsFooBar := block.Warnings{
   255  		block.Warning{
   256  			Name:    "foo",
   257  			Message: "bar",
   258  		},
   259  	}
   260  
   261  	for _, test := range []struct {
   262  		query                                             string
   263  		limitTests                                        []limitTest
   264  		terminatedQuery                                   *testFindQuery
   265  		childQuery                                        *testFindQuery
   266  		expectedResultsWithoutExpandableAndLeafDuplicates results
   267  		expectedResultsWithExpandableAndLeafDuplicates    results
   268  	}{
   269  		{
   270  			query:      "foo.b*",
   271  			limitTests: limitTests,
   272  			terminatedQuery: &testFindQuery{
   273  				expectMatchers: &completeTagQueryMatcher{
   274  					matchers: []models.Matcher{
   275  						{Type: models.MatchEqual, Name: b("__g0__"), Value: b("foo")},
   276  						{Type: models.MatchRegexp, Name: b("__g1__"), Value: b(`b[^\.]*`)},
   277  						{Type: models.MatchNotField, Name: b("__g2__")},
   278  					},
   279  				},
   280  				mockResult: func(lt limitTest) *consolidators.CompleteTagsResult {
   281  					return &consolidators.CompleteTagsResult{
   282  						CompleteNameOnly: false,
   283  						CompletedTags: []consolidators.CompletedTag{
   284  							{Name: b("__g1__"), Values: bs("bug", "bar", "baz")},
   285  						},
   286  						Metadata: block.ResultMetadata{
   287  							LocalOnly:  true,
   288  							Exhaustive: lt.ex,
   289  						},
   290  					}
   291  				},
   292  			},
   293  			childQuery: &testFindQuery{
   294  				expectMatchers: &completeTagQueryMatcher{
   295  					matchers: []models.Matcher{
   296  						{Type: models.MatchEqual, Name: b("__g0__"), Value: b("foo")},
   297  						{Type: models.MatchRegexp, Name: b("__g1__"), Value: b(`b[^\.]*`)},
   298  						{Type: models.MatchField, Name: b("__g2__")},
   299  					},
   300  				},
   301  				mockResult: func(lt limitTest) *consolidators.CompleteTagsResult {
   302  					var warnings block.Warnings
   303  					if !lt.ex2 {
   304  						warnings = warningsFooBar
   305  					}
   306  					return &consolidators.CompleteTagsResult{
   307  						CompleteNameOnly: false,
   308  						CompletedTags: []consolidators.CompletedTag{
   309  							{Name: b("__g1__"), Values: bs("baz", "bix", "bug")},
   310  						},
   311  						Metadata: block.ResultMetadata{
   312  							LocalOnly:  false,
   313  							Exhaustive: true,
   314  							Warnings:   warnings,
   315  						},
   316  					}
   317  				},
   318  			},
   319  			expectedResultsWithoutExpandableAndLeafDuplicates: results{
   320  				makeNoChildrenResult("foo.bar", "bar"),
   321  				makeWithChildrenResult("foo.baz", "baz"),
   322  				makeWithChildrenResult("foo.bix", "bix"),
   323  				makeWithChildrenResult("foo.bug", "bug"),
   324  			},
   325  			expectedResultsWithExpandableAndLeafDuplicates: results{
   326  				makeNoChildrenResult("foo.bar", "bar"),
   327  				makeNoChildrenResult("foo.baz", "baz"),
   328  				makeWithChildrenResult("foo.baz", "baz"),
   329  				makeWithChildrenResult("foo.bix", "bix"),
   330  				makeNoChildrenResult("foo.bug", "bug"),
   331  				makeWithChildrenResult("foo.bug", "bug"),
   332  			},
   333  		},
   334  		{
   335  			query: "foo.**.*",
   336  			childQuery: &testFindQuery{
   337  				expectMatchers: &completeTagQueryMatcher{
   338  					matchers: []models.Matcher{
   339  						{
   340  							Type: models.MatchRegexp,
   341  							Name: b("__g0__"), Value: b(".*"),
   342  						},
   343  						{
   344  							Type:  models.MatchRegexp,
   345  							Name:  doc.IDReservedFieldName,
   346  							Value: b(`foo\.+.*[^\.]*`),
   347  						},
   348  					},
   349  					filterNameTagsIndexStart: 2,
   350  					filterNameTagsIndexEnd:   102,
   351  				},
   352  				mockResult: func(_ limitTest) *consolidators.CompleteTagsResult {
   353  					return &consolidators.CompleteTagsResult{
   354  						CompleteNameOnly: false,
   355  						CompletedTags: []consolidators.CompletedTag{
   356  							{Name: b("__g2__"), Values: bs("bar0", "bar1")},
   357  							{Name: b("__g3__"), Values: bs("baz0", "baz1", "baz2")},
   358  						},
   359  						Metadata: block.ResultMetadata{
   360  							LocalOnly:  true,
   361  							Exhaustive: true,
   362  						},
   363  					}
   364  				},
   365  			},
   366  			expectedResultsWithoutExpandableAndLeafDuplicates: results{
   367  				makeWithChildrenResult("foo.**.bar0", "bar0"),
   368  				makeWithChildrenResult("foo.**.bar1", "bar1"),
   369  				makeWithChildrenResult("foo.**.baz0", "baz0"),
   370  				makeWithChildrenResult("foo.**.baz1", "baz1"),
   371  				makeWithChildrenResult("foo.**.baz2", "baz2"),
   372  			},
   373  			expectedResultsWithExpandableAndLeafDuplicates: results{
   374  				makeWithChildrenResult("foo.**.bar0", "bar0"),
   375  				makeWithChildrenResult("foo.**.bar1", "bar1"),
   376  				makeWithChildrenResult("foo.**.baz0", "baz0"),
   377  				makeWithChildrenResult("foo.**.baz1", "baz1"),
   378  				makeWithChildrenResult("foo.**.baz2", "baz2"),
   379  			},
   380  		},
   381  	} {
   382  		// Set which limit tests should be performed for this query.
   383  		testCaseLimitTests := test.limitTests
   384  		if len(limitTests) == 0 {
   385  			// Just test case where both are complete.
   386  			testCaseLimitTests = []limitTest{bothCompleteLimitTest}
   387  		}
   388  
   389  		type testVariation struct {
   390  			limitTest                    limitTest
   391  			includeBothExpandableAndLeaf bool
   392  			expectedResults              results
   393  		}
   394  
   395  		var testVarations []testVariation
   396  		for _, limitTest := range testCaseLimitTests {
   397  			testVarations = append(testVarations,
   398  				// Test case with default find result options.
   399  				testVariation{
   400  					limitTest:                    limitTest,
   401  					includeBothExpandableAndLeaf: false,
   402  					expectedResults:              test.expectedResultsWithoutExpandableAndLeafDuplicates,
   403  				},
   404  				// Test case test for overloaded find result options.
   405  				testVariation{
   406  					limitTest:                    limitTest,
   407  					includeBothExpandableAndLeaf: true,
   408  					expectedResults:              test.expectedResultsWithExpandableAndLeafDuplicates,
   409  				})
   410  		}
   411  
   412  		for _, variation := range testVarations {
   413  			// nolint: govet
   414  			limitTest := variation.limitTest
   415  			includeBothExpandableAndLeaf := variation.includeBothExpandableAndLeaf
   416  			expectedResults := variation.expectedResults
   417  			t.Run(fmt.Sprintf("%s-%s", test.query, limitTest.name), func(t *testing.T) {
   418  				ctrl := xtest.NewController(t)
   419  				defer ctrl.Finish()
   420  
   421  				store := storage.NewMockStorage(ctrl)
   422  
   423  				if q := test.terminatedQuery; q != nil {
   424  					// Set up no children case.
   425  					store.EXPECT().
   426  						CompleteTags(gomock.Any(), q.expectMatchers, gomock.Any()).
   427  						Return(q.mockResult(limitTest), nil)
   428  				}
   429  
   430  				if q := test.childQuery; q != nil {
   431  					// Set up children case.
   432  					store.EXPECT().
   433  						CompleteTags(gomock.Any(), q.expectMatchers, gomock.Any()).
   434  						Return(q.mockResult(limitTest), nil)
   435  				}
   436  
   437  				builder, err := handleroptions.NewFetchOptionsBuilder(
   438  					handleroptions.FetchOptionsBuilderOptions{
   439  						Timeout: 15 * time.Second,
   440  					})
   441  				require.NoError(t, err)
   442  
   443  				handlerOpts := options.EmptyHandlerOptions().
   444  					SetGraphiteFindFetchOptionsBuilder(builder).
   445  					SetStorage(store)
   446  				// Set the relevant result options and save back to handler options.
   447  				graphiteStorageOpts := handlerOpts.GraphiteStorageOptions()
   448  				graphiteStorageOpts.FindResultsIncludeBothExpandableAndLeaf = includeBothExpandableAndLeaf
   449  				handlerOpts = handlerOpts.SetGraphiteStorageOptions(graphiteStorageOpts)
   450  
   451  				h := NewFindHandler(handlerOpts)
   452  
   453  				// Execute the query.
   454  				params := make(url.Values)
   455  				params.Set("query", test.query)
   456  				params.Set("from", from.s)
   457  				params.Set("until", until.s)
   458  
   459  				w := &writer{}
   460  				req := &http.Request{Method: opts.httpMethod}
   461  				switch opts.httpMethod {
   462  				case http.MethodGet:
   463  					req.URL = &url.URL{
   464  						RawQuery: params.Encode(),
   465  					}
   466  				case http.MethodPost:
   467  					req.Form = params
   468  				}
   469  
   470  				h.ServeHTTP(w, req)
   471  
   472  				// Convert results to comparable format.
   473  				require.Equal(t, 1, len(w.results))
   474  				r := make(results, 0)
   475  				decoder := json.NewDecoder(bytes.NewBufferString((w.results[0])))
   476  				require.NoError(t, decoder.Decode(&r))
   477  
   478  				require.Equal(t, expectedResults, r)
   479  				actual := w.Header().Get(headers.LimitHeader)
   480  				assert.Equal(t, limitTest.header, actual)
   481  			})
   482  		}
   483  	}
   484  }