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  }