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 }