github.com/go-graphite/carbonapi@v0.17.0/cmd/mockbackend/e2etesting.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "context" 6 "crypto/sha256" 7 "encoding/base64" 8 "encoding/json" 9 "fmt" 10 "io" 11 "math" 12 "net/http" 13 "net/url" 14 "os" 15 "reflect" 16 "sort" 17 "strconv" 18 "strings" 19 "sync" 20 "time" 21 22 merry2 "github.com/ansel1/merry" 23 "go.uber.org/zap" 24 ) 25 26 type TestSchema struct { 27 Apps []App 28 Queries []Query 29 } 30 31 type App struct { 32 Name string 33 Binary string 34 Args []string 35 } 36 37 type Query struct { 38 Endpoint string `yaml:"endpoint"` 39 Delay int `yaml:"delay"` 40 URL string `yaml:"URL"` 41 Type string `yaml:"type"` 42 Body string `yaml:"body"` 43 ExpectedResponse ExpectedResponse `yaml:"expectedResponse"` 44 } 45 46 type ExpectedResponse struct { 47 HttpCode int `yaml:"httpCode"` 48 ContentType string `yaml:"contentType"` 49 ErrBody string `yaml:"errBody"` 50 ErrSort bool `yaml:"errSort"` 51 ExpectedResults []ExpectedResult `yaml:"expectedResults"` 52 } 53 54 type ExpectedResult struct { 55 SHA256 []string `yaml:"sha256"` 56 Metrics []RenderResponse 57 MetricsFind []MetricsFindResponse `json:"metricsFind" yaml:"metricsFind"` 58 TagsAutocompelete []string `json:"tagsAutocompelete" yaml:"tagsAutocompelete"` 59 } 60 61 type MetricsFindResponse struct { 62 AllowChildren int `json:"allowChildren" yaml:"allowChildren"` 63 Expandable int `json:"expandable" yaml:"expandable"` 64 Leaf int `json:"leaf" yaml:"leaf"` 65 Id string `json:"id" yaml:"id"` 66 Text string `json:"text" yaml:"text"` 67 Context map[string]string `json:"context" yaml:"context"` 68 } 69 70 type RenderResponse struct { 71 Target string `json:"target" yaml:"target"` 72 Datapoints []Datapoint `json:"datapoints" yaml:"datapoints"` 73 Tags map[string]string `json:"tags" yaml:"tags"` 74 } 75 76 type Datapoint struct { 77 Timestamp int 78 Value float64 79 } 80 81 func (d *Datapoint) UnmarshalJSON(data []byte) error { 82 pieces := strings.Split(string(data), ",") 83 if len(pieces) != 2 { 84 return fmt.Errorf("too many parameters in the Datapoint, got %v, expected 2", len(pieces)) 85 } 86 87 var err error 88 valueStr := pieces[0][1:] 89 tsStr := pieces[1][:len(pieces[1])-1] 90 91 d.Timestamp, err = strconv.Atoi(tsStr) 92 if err != nil { 93 return fmt.Errorf("failed to parse Timestamp: %v", err) 94 } 95 96 if valueStr == "null" || valueStr == "\"null\"" { 97 d.Value = math.NaN() 98 return nil 99 } 100 d.Value, err = strconv.ParseFloat(valueStr, 64) 101 if err != nil { 102 return fmt.Errorf("failed to parse Value: %v", err) 103 } 104 105 return nil 106 } 107 108 func (d *Datapoint) UnmarshalYAML(unmarshal func(interface{}) error) error { 109 yamlData := make([]string, 0) 110 err := unmarshal(&yamlData) 111 if err != nil { 112 return err 113 } 114 115 if len(yamlData) != 2 { 116 return fmt.Errorf("too many parameters in the Datapoint, got %v, expected 2", len(yamlData)) 117 } 118 119 valueStr := yamlData[0] 120 tsStr := yamlData[1] 121 122 d.Timestamp, err = strconv.Atoi(tsStr) 123 if err != nil { 124 return fmt.Errorf("failed to parse Timestamp: %v", err) 125 } 126 127 if valueStr == "null" || valueStr == "\"null\"" { 128 d.Value = math.NaN() 129 return nil 130 } 131 d.Value, err = strconv.ParseFloat(valueStr, 64) 132 if err != nil { 133 return fmt.Errorf("failed to parse Value: %v", err) 134 } 135 136 return nil 137 } 138 139 func isRenderEqual(m1, m2 RenderResponse) error { 140 if m1.Target != m2.Target { 141 return fmt.Errorf("target mismatch, got '%v', expected '%v'", m1.Target, m2.Target) 142 } 143 144 if len(m1.Datapoints) != len(m2.Datapoints) { 145 return fmt.Errorf("response have unexpected length, got '%v', expected '%v'", m1.Datapoints, m2.Datapoints) 146 } 147 148 if len(m1.Datapoints) > 1 { 149 step1 := m1.Datapoints[1].Timestamp - m1.Datapoints[2].Timestamp 150 step2 := m2.Datapoints[1].Timestamp - m2.Datapoints[2].Timestamp 151 if step1 != step2 { 152 return fmt.Errorf("series has unexpected step, got '%v', expected '%v'", step1, step2) 153 } 154 } 155 datapointsMismatch := false 156 for i := range m1.Datapoints { 157 if math.IsNaN(m1.Datapoints[i].Value) && math.IsNaN(m2.Datapoints[i].Value) { 158 continue 159 } 160 if m1.Datapoints[i].Value != m2.Datapoints[i].Value { 161 datapointsMismatch = true 162 break 163 } 164 if m1.Datapoints[i].Timestamp != m2.Datapoints[i].Timestamp { 165 datapointsMismatch = true 166 break 167 } 168 } 169 if datapointsMismatch { 170 return fmt.Errorf("data in response is different, got '%v', expected '%v'", m1.Datapoints, m2.Datapoints) 171 } 172 173 return nil 174 } 175 176 func max(a, b int) int { 177 if a > b { 178 return a 179 } 180 return b 181 } 182 183 func resortErr(errStr string) string { 184 first := strings.Index(errStr, "\n") 185 if first >= 0 && first != len(errStr)-1 { 186 // resort error string 187 errs := strings.Split(errStr, "\n") 188 if errs[len(errs)-1] == "" { 189 errs = errs[:len(errs)-1] 190 } 191 sort.Strings(errs) 192 errStr = strings.Join(errs, "\n") + "\n" 193 } 194 return errStr 195 } 196 197 func doTest(logger *zap.Logger, t *Query, verbose bool) []error { 198 client := http.Client{} 199 failures := make([]error, 0) 200 d, err := time.ParseDuration(fmt.Sprintf("%v", t.Delay) + "s") 201 if err != nil { 202 err = merry2.Prepend(err, "failed parse duration") 203 failures = append(failures, err) 204 return failures 205 } 206 time.Sleep(d) 207 ctx := context.Background() 208 var body io.Reader 209 if t.Type != "GET" { 210 body = strings.NewReader(t.Body) 211 } 212 var resp *http.Response 213 var contentType string 214 u, err := url.Parse(t.Endpoint + t.URL) 215 if err != nil { 216 err = merry2.Prepend(err, "failed to parse URL") 217 failures = append(failures, err) 218 return failures 219 } 220 221 logger.Info("sending request", 222 zap.String("endpoint", t.Endpoint), 223 zap.String("original_URL", t.URL), 224 ) 225 226 req, err := http.NewRequestWithContext(ctx, t.Type, t.Endpoint+u.Path+"/?"+u.Query().Encode(), body) 227 if err != nil { 228 err = merry2.Prepend(err, "failed to prepare the request") 229 failures = append(failures, err) 230 return failures 231 } 232 233 resp, err = client.Do(req) 234 if err != nil { 235 err = merry2.Prepend(err, "failed to perform the request") 236 failures = append(failures, err) 237 return failures 238 } 239 240 contentType = resp.Header.Get("Content-Type") 241 if t.ExpectedResponse.ContentType != contentType { 242 failures = append(failures, 243 merry2.Errorf("unexpected content-type, got %v (code %d), expected %v", 244 contentType, resp.StatusCode, 245 t.ExpectedResponse.ContentType, 246 ), 247 ) 248 } 249 250 b, err := io.ReadAll(resp.Body) 251 if err != nil { 252 err = merry2.Prepend(err, "failed to read body") 253 failures = append(failures, err) 254 return failures 255 } 256 257 if resp.StatusCode != t.ExpectedResponse.HttpCode { 258 failures = append(failures, merry2.Errorf("unexpected status code, got %v, expected %v", 259 resp.StatusCode, 260 t.ExpectedResponse.HttpCode, 261 ), 262 ) 263 } 264 265 // We don't need to actually check body of response if we expect any sort of error (4xx/5xx), but for check error handling do this 266 if t.ExpectedResponse.HttpCode >= 300 { 267 if t.ExpectedResponse.ErrBody != "" { 268 errStr := string(b) 269 if t.ExpectedResponse.ErrSort { 270 errStr = resortErr(errStr) 271 } 272 if t.ExpectedResponse.ErrBody != errStr { 273 failures = append(failures, merry2.Errorf("mismatch error body, got '%s', expected '%s'", string(b), t.ExpectedResponse.ErrBody)) 274 } 275 } 276 return failures 277 } 278 279 switch contentType { 280 case "image/png": 281 case "image/svg+xml": 282 hash := sha256.Sum256(b) 283 hashStr := fmt.Sprintf("%x", hash) 284 sha256matched := false 285 for _, sha256sum := range t.ExpectedResponse.ExpectedResults[0].SHA256 { 286 if hashStr == sha256sum { 287 sha256matched = true 288 break 289 } 290 } 291 if !sha256matched { 292 encodedBody := base64.StdEncoding.EncodeToString(b) 293 failures = append(failures, merry2.Errorf("sha256 mismatch, got '%v', expected '%v', encodedBody: '%v'", hashStr, t.ExpectedResponse.ExpectedResults[0].SHA256, encodedBody)) 294 return failures 295 } 296 case "application/json": 297 if strings.HasPrefix(t.URL, "/metrics/find") { 298 res := make([]MetricsFindResponse, 0, 1) 299 err := json.Unmarshal(b, &res) 300 if err != nil { 301 err = merry2.Prepend(err, "failed to parse response") 302 failures = append(failures, err) 303 return failures 304 } 305 306 if len(t.ExpectedResponse.ExpectedResults) == 0 { 307 return failures 308 } 309 310 if len(res) != len(t.ExpectedResponse.ExpectedResults[0].MetricsFind) { 311 failures = append(failures, merry2.Errorf("unexpected amount of metrics find, got %v, expected %v", 312 len(res), 313 len(t.ExpectedResponse.ExpectedResults[0].MetricsFind))) 314 if verbose { 315 length := max(len(t.ExpectedResponse.ExpectedResults[0].MetricsFind), len(res)) 316 for i := 0; i < length; i++ { 317 if i >= len(res) { 318 err = fmt.Errorf("metrics find[%d] want=`%+v`", i, t.ExpectedResponse.ExpectedResults[0].MetricsFind[i]) 319 failures = append(failures, err) 320 } else if i >= len(t.ExpectedResponse.ExpectedResults[0].MetricsFind) { 321 err = fmt.Errorf("metrics find[%d] got unexpected=`%+v`", i, res[i]) 322 failures = append(failures, err) 323 } else if !reflect.DeepEqual(res[i], t.ExpectedResponse.ExpectedResults[0].MetricsFind[i]) { 324 err = fmt.Errorf("metrics find[%d] are not equal, got=`%+v`, expected=`%+v`", i, res[i], t.ExpectedResponse.ExpectedResults[0].MetricsFind[i]) 325 failures = append(failures, err) 326 } 327 } 328 } 329 return failures 330 } 331 332 for i := range res { 333 if !reflect.DeepEqual(res[i], t.ExpectedResponse.ExpectedResults[0].MetricsFind[i]) { 334 err = fmt.Errorf("metrics find[%d] are not equal, got=`%+v`, expected=`%+v`", i, res[i], t.ExpectedResponse.ExpectedResults[0].MetricsFind[i]) 335 failures = append(failures, err) 336 } 337 } 338 } else if strings.HasPrefix(t.URL, "/tags/autoComplete/") { 339 // tags/autoComplete 340 res := make([]string, 0, 1) 341 err := json.Unmarshal(b, &res) 342 if err != nil { 343 err = merry2.Prepend(err, "failed to parse response") 344 failures = append(failures, err) 345 return failures 346 } 347 348 if len(t.ExpectedResponse.ExpectedResults) == 0 { 349 return failures 350 } 351 352 if len(res) != len(t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete) { 353 failures = append(failures, merry2.Errorf("unexpected amount of results, got %v, expected %v", 354 len(res), 355 len(t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete))) 356 if verbose { 357 length := max(len(t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete), len(res)) 358 for i := 0; i < length; i++ { 359 if i >= len(res) { 360 err = fmt.Errorf("tags[%d] want=`%+v`", i, t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete[i]) 361 failures = append(failures, err) 362 } else if i >= len(t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete) { 363 err = fmt.Errorf("tags[%d] got unexpected=`%+v`", i, res[i]) 364 failures = append(failures, err) 365 } else if !reflect.DeepEqual(res[i], t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete[i]) { 366 err = fmt.Errorf("tags[%d] are not equal, got=`%+v`, expected=`%+v`", i, res[i], t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete[i]) 367 failures = append(failures, err) 368 } 369 } 370 } 371 return failures 372 } 373 374 for i := range res { 375 if res[i] != t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete[i] { 376 err = merry2.Prependf(err, "tags[%d] are not equal, got=`%+v`, expected=`%+v`", i, res[i], t.ExpectedResponse.ExpectedResults[0].TagsAutocompelete[i]) 377 failures = append(failures, err) 378 } 379 } 380 381 } else { 382 // render 383 res := make([]RenderResponse, 0, 1) 384 err := json.Unmarshal(b, &res) 385 if err != nil { 386 err = merry2.Prepend(err, "failed to parse response") 387 failures = append(failures, err) 388 return failures 389 } 390 391 if len(t.ExpectedResponse.ExpectedResults) == 0 { 392 return failures 393 } 394 395 if len(res) != len(t.ExpectedResponse.ExpectedResults[0].Metrics) { 396 failures = append(failures, merry2.Errorf("unexpected amount of results, got %v, expected %v", 397 len(res), 398 len(t.ExpectedResponse.ExpectedResults[0].Metrics))) 399 if verbose { 400 length := max(len(t.ExpectedResponse.ExpectedResults[0].Metrics), len(res)) 401 for i := 0; i < length; i++ { 402 if i >= len(res) { 403 err = fmt.Errorf("metrics[%d] want=`%+v`", i, t.ExpectedResponse.ExpectedResults[0].Metrics[i]) 404 failures = append(failures, err) 405 } else if i >= len(t.ExpectedResponse.ExpectedResults[0].Metrics) { 406 err = fmt.Errorf("metrics[%d] got unexpected=`%+v`", i, res[i]) 407 failures = append(failures, err) 408 } else if !reflect.DeepEqual(res[i], t.ExpectedResponse.ExpectedResults[0].Metrics[i]) { 409 err = fmt.Errorf("metrics[%d] are not equal, got=`%+v`, expected=`%+v`", i, res[i], t.ExpectedResponse.ExpectedResults[0].Metrics[i]) 410 failures = append(failures, err) 411 } 412 } 413 } 414 return failures 415 } 416 417 for i := range res { 418 err := isRenderEqual(res[i], t.ExpectedResponse.ExpectedResults[0].Metrics[i]) 419 if err != nil { 420 err = merry2.Prependf(err, "metrics are not equal, got=`%+v`, expected=`%+v`", res[i], t.ExpectedResponse.ExpectedResults[0].Metrics[i]) 421 failures = append(failures, err) 422 } 423 } 424 } 425 default: 426 if resp.StatusCode == http.StatusOK { 427 // if !strings.HasPrefix(t.URL, "/tags/autoComplete/") || 428 // (contentType == "text/plain; charset=utf-8" && 429 // resp.StatusCode == http.StatusNotFound && 430 // t.ExpectedResponse.HttpCode == http.StatusNotFound) { 431 failures = append(failures, merry2.Errorf("unsupported content-type: got '%v'", contentType)) 432 } 433 } 434 435 return failures 436 } 437 438 func e2eTest(logger *zap.Logger, noapp, breakOnError, verbose bool) bool { 439 failed := false 440 logger.Info("will run test", 441 zap.Any("config", cfg.Test), 442 ) 443 runningApps := make(map[string]*runner) 444 if !noapp { 445 wgStart := sync.WaitGroup{} 446 for i, c := range cfg.Test.Apps { 447 r := new(&cfg.Test.Apps[i], logger) 448 wgStart.Add(1) 449 runningApps[c.Name] = r 450 go func() { 451 wgStart.Done() 452 r.Run() 453 }() 454 } 455 456 wgStart.Wait() 457 logger.Info("will sleep for 1 seconds to start all required apps") 458 time.Sleep(1 * time.Second) 459 } 460 461 for _, t := range cfg.Test.Queries { 462 failures := doTest(logger, &t, verbose) 463 if len(failures) != 0 { 464 failed = true 465 logger.Error("test failed", 466 zap.Errors("failures", failures), 467 zap.String("url", t.URL), zap.String("type", t.Type), zap.String("body", t.Body), 468 ) 469 for _, v := range runningApps { 470 if !v.IsRunning() { 471 logger.Error("unexpected app crash", zap.Any("app", v)) 472 } 473 } 474 if breakOnError { 475 for { 476 fmt.Print("Some queries was failed, press y for continue after debug test:") 477 in := bufio.NewScanner(os.Stdin) 478 in.Scan() 479 s := in.Text() 480 if s == "y" || s == "Y" { 481 break 482 } 483 } 484 } 485 } else { 486 logger.Info("test OK") 487 } 488 } 489 490 logger.Info("shutting down running application") 491 for _, v := range runningApps { 492 v.Finish() 493 } 494 495 if failed { 496 logger.Error("tests failed") 497 for _, v := range runningApps { 498 logger.Info("app out", zap.Any("app", v), zap.String("out", v.Out())) 499 } 500 } else { 501 logger.Info("All tests OK") 502 } 503 504 return failed 505 }