diff --git a/cmd/benchttp/run.go b/cmd/benchttp/run.go index 066a4ac..6c2bd68 100644 --- a/cmd/benchttp/run.go +++ b/cmd/benchttp/run.go @@ -8,9 +8,9 @@ import ( "io" "os" - "github.com/benchttp/engine/runner" + "github.com/benchttp/engine/benchttp" + "github.com/benchttp/engine/configio" - "github.com/benchttp/cli/internal/configfile" "github.com/benchttp/cli/internal/configflag" "github.com/benchttp/cli/internal/output" "github.com/benchttp/cli/internal/render" @@ -21,39 +21,26 @@ import ( type cmdRun struct { flagset *flag.FlagSet - // configFile is the parsed value for flag -configFile - configFile string + configFile string // parsed value for flag -configFile + silent bool // parsed value for flag -silent - // silent is the parsed value for flag -silent - silent bool - - // config is the runner config resulting from parsing CLI flags. - config runner.Config -} - -// init initializes cmdRun with default values. -func (cmd *cmdRun) init() { - cmd.config = runner.DefaultConfig() - cmd.configFile = configfile.Find([]string{ - "./.benchttp.yml", - "./.benchttp.yaml", - "./.benchttp.json", - }) + builder configio.Builder } // execute runs the benchttp runner: it parses CLI flags, loads config // from config file and parsed flags, then runs the benchmark and outputs // it according to the config. func (cmd *cmdRun) execute(args []string) error { - cmd.init() + if err := cmd.parseArgs(args); err != nil { + return err + } - // Generate merged config (default < config file < CLI flags) - cfg, err := cmd.makeConfig(args) + config, err := buildConfig(cmd.builder, cmd.configFile) if err != nil { return err } - report, err := runBenchmark(cfg, cmd.silent) + report, err := runBenchmark(config, cmd.silent) if err != nil { return err } @@ -61,85 +48,44 @@ func (cmd *cmdRun) execute(args []string) error { return renderReport(os.Stdout, report, cmd.silent) } -// parseArgs parses input args as config fields and returns -// a slice of fields that were set by the user. -func (cmd *cmdRun) parseArgs(args []string) []string { - // skip parsing if no flags are provided - if len(args) == 0 { - return []string{} - } - - // config file path - cmd.flagset.StringVar(&cmd.configFile, - "configFile", - cmd.configFile, - "Config file path", - ) - - // silent mode - cmd.flagset.BoolVar(&cmd.silent, - "silent", - false, - "Silent mode", - ) - - // attach config options flags to the flagset - // and bind their value to the config struct - configflag.Bind(cmd.flagset, &cmd.config) - - cmd.flagset.Parse(args) //nolint:errcheck // never occurs due to flag.ExitOnError - - return configflag.Which(cmd.flagset) +func (cmd *cmdRun) parseArgs(args []string) error { + cmd.flagset.StringVar(&cmd.configFile, "configFile", configio.FindFile(), "Config file path") + cmd.flagset.BoolVar(&cmd.silent, "silent", false, "Silent mode") + configflag.Bind(cmd.flagset, &cmd.builder) + return cmd.flagset.Parse(args) } -// makeConfig returns a runner.ConfigGlobal initialized with config file -// options if found, overridden with CLI options listed in fields -// slice param. -func (cmd *cmdRun) makeConfig(args []string) (cfg runner.Config, err error) { - // Set CLI config from flags and retrieve fields that were set - fields := cmd.parseArgs(args) - - // configFile not set and default ones not found: - // skip the merge and return the cli config - if cmd.configFile == "" { - return cmd.config, cmd.config.Validate() - } +func buildConfig( + b configio.Builder, + filePath string, +) (benchttp.Runner, error) { + // use default runner as a base + runner := benchttp.DefaultRunner() - fileConfig, err := configfile.Parse(cmd.configFile) - if err != nil && !errors.Is(err, configfile.ErrFileNotFound) { + // override with config file values + err := configio.UnmarshalFile(filePath, &runner) + if err != nil && !errors.Is(err, configio.ErrFileNotFound) { // config file is not mandatory: discard ErrFileNotFound. // other errors are critical - return + return runner, err } - mergedConfig := cmd.config.WithFields(fields...).Override(fileConfig) + // override with CLI flags values + b.Mutate(&runner) - return mergedConfig, mergedConfig.Validate() + return runner, nil } -func onRecordingProgress(silent bool) func(runner.RecordingProgress) { - if silent { - return func(runner.RecordingProgress) {} - } - - // hack: write a blank line as render.Progress always - // erases the previous line - fmt.Println() - - return func(progress runner.RecordingProgress) { - render.Progress(os.Stdout, progress) //nolint: errcheck - } -} - -func runBenchmark(cfg runner.Config, silent bool) (*runner.Report, error) { +func runBenchmark(runner benchttp.Runner, silent bool) (*benchttp.Report, error) { // Prepare graceful shutdown in case of os.Interrupt (Ctrl+C) ctx, cancel := context.WithCancel(context.Background()) go signals.ListenOSInterrupt(cancel) + // Stream progress to stdout + runner.OnProgress = onRecordingProgress(silent) + // Run the benchmark - report, err := runner. - New(onRecordingProgress(silent)). - Run(ctx, cfg) + report, err := runner.Run(ctx) if err != nil { return report, err } @@ -147,7 +93,21 @@ func runBenchmark(cfg runner.Config, silent bool) (*runner.Report, error) { return report, nil } -func renderReport(w io.Writer, report *runner.Report, silent bool) error { +func onRecordingProgress(silent bool) func(benchttp.RecordingProgress) { + if silent { + return func(benchttp.RecordingProgress) {} + } + + // hack: write a blank line as render.Progress always + // erases the previous line + fmt.Println() + + return func(progress benchttp.RecordingProgress) { + render.Progress(os.Stdout, progress) //nolint: errcheck + } +} + +func renderReport(w io.Writer, report *benchttp.Report, silent bool) error { writeIfNotSilent := output.ConditionalWriter{Writer: w}.If(!silent) if _, err := render.ReportSummary(writeIfNotSilent, report); err != nil { diff --git a/go.mod b/go.mod index b66e1f3..1bd77a2 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,10 @@ module github.com/benchttp/cli go 1.17 -require github.com/benchttp/engine v0.1.0 +require github.com/benchttp/engine v0.2.0 require ( + github.com/google/go-cmp v0.5.9 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 281e466..f5b6e2d 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,7 @@ -github.com/benchttp/engine v0.0.0-20221008174504-d1162e9ac007 h1:h6ON0tn3i/83eUMoXq0FHxBMLYIA/OIli3m9KYkcxPI= -github.com/benchttp/engine v0.0.0-20221008174504-d1162e9ac007/go.mod h1:FRfUnUjoL1s0aHVGlrxB3pdPAEDLNCnWh6cVOur24hM= -github.com/benchttp/engine v0.1.0 h1:FpQOwHklBITuRd7B/AGKqr0mAmbXgTwzQiPHlhUktbQ= -github.com/benchttp/engine v0.1.0/go.mod h1:FRfUnUjoL1s0aHVGlrxB3pdPAEDLNCnWh6cVOur24hM= -github.com/drykit-go/cond v0.1.0 h1:y7MNxREQLT83vGfcfSKjyFPLC/ZDjYBNp6KuaVVjOg4= -github.com/drykit-go/cond v0.1.0/go.mod h1:7MXBFjjaB5ZCEB8Q4w2euNOaWuTqf7NjOFZAyV1Jpfg= -github.com/drykit-go/strcase v0.2.0/go.mod h1:cWK0/az2f09UPIbJ42Sb8Iqdv01uENrFX+XXKGjPo+8= -github.com/drykit-go/testx v0.1.0/go.mod h1:qGXb49a8CzQ82crBeCVW8R3kGU1KRgWHnI+Q6CNVbz8= -github.com/drykit-go/testx v1.2.0 h1:UsH+tFd24z3Xu+mwvwPY+9eBEg9CUyMsUeMYyUprG0o= -github.com/drykit-go/testx v1.2.0/go.mod h1:qTzXJgnAg8n31woklBzNTaWzLMJrnFk93x/aeaIpc20= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/benchttp/engine v0.2.0 h1:r3zKs7ZOgRC0uDsHabD76sIoiF57MwGl0ouhFGpbu+A= +github.com/benchttp/engine v0.2.0/go.mod h1:70imLQ2ONTEMGcbJimOirjz57uw3aix9RI8yZVZxLdc= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/configfile/error.go b/internal/configfile/error.go deleted file mode 100644 index 61fdcf6..0000000 --- a/internal/configfile/error.go +++ /dev/null @@ -1,26 +0,0 @@ -package configfile - -import ( - "errors" -) - -var ( - // ErrFileNotFound signals a config file not found. - ErrFileNotFound = errors.New("file not found") - - // ErrFileRead signals an error trying to read a config file. - // It can be due to a corrupted file or an invalid permission - // for instance. - ErrFileRead = errors.New("invalid file") - - // ErrFileExt signals an unsupported extension for the config file. - ErrFileExt = errors.New("invalid extension") - - // ErrParse signals an error parsing a retrieved config file. - // It is returned if it contains an unexpected field or an unexpected - // value for a field. - ErrParse = errors.New("parsing error: invalid config file") - - // ErrCircularExtends signals a circular reference in the config file. - ErrCircularExtends = errors.New("circular reference detected") -) diff --git a/internal/configfile/find.go b/internal/configfile/find.go deleted file mode 100644 index 755e39a..0000000 --- a/internal/configfile/find.go +++ /dev/null @@ -1,14 +0,0 @@ -package configfile - -import "os" - -// Find returns the first name tham matches a file path. -// If no match is found, it returns an empty string. -func Find(names []string) string { - for _, path := range names { - if _, err := os.Stat(path); err == nil { // err IS nil: file exists - return path - } - } - return "" -} diff --git a/internal/configfile/find_test.go b/internal/configfile/find_test.go deleted file mode 100644 index df6490d..0000000 --- a/internal/configfile/find_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package configfile_test - -import ( - "testing" - - "github.com/benchttp/cli/internal/configfile" -) - -var ( - goodFileYML = configPath("valid/benchttp.yml") - goodFileJSON = configPath("valid/benchttp.json") - badFile = configPath("does-not-exist.json") -) - -func TestFind(t *testing.T) { - t.Run("return first existing file", func(t *testing.T) { - files := []string{badFile, goodFileYML, goodFileJSON} - - if got := configfile.Find(files); got != goodFileYML { - t.Errorf("did not retrieve good file: exp %s, got %s", goodFileYML, got) - } - }) - - t.Run("return empty string when no match", func(t *testing.T) { - files := []string{badFile} - - if got := configfile.Find(files); got != "" { - t.Errorf("retrieved unexpected file: %s", got) - } - }) -} diff --git a/internal/configfile/parse.go b/internal/configfile/parse.go deleted file mode 100644 index 5217600..0000000 --- a/internal/configfile/parse.go +++ /dev/null @@ -1,119 +0,0 @@ -package configfile - -import ( - "errors" - "os" - "path/filepath" - - "github.com/benchttp/engine/configparse" - "github.com/benchttp/engine/runner" - - "github.com/benchttp/cli/internal/errorutil" -) - -// Parse parses a benchttp runner config file into a runner.ConfigGlobal -// and returns it or the first non-nil error occurring in the process, -// which can be any of the values declared in the package. -func Parse(filename string) (cfg runner.Config, err error) { - uconfs, err := parseFileRecursive(filename, []configparse.Representation{}, set{}) - if err != nil { - return - } - return parseAndMergeConfigs(uconfs) -} - -// set is a collection of unique string values. -type set map[string]bool - -// add adds v to the receiver. If v is already set, it returns a non-nil -// error instead. -func (s set) add(v string) error { - if _, exists := s[v]; exists { - return errors.New("value already set") - } - s[v] = true - return nil -} - -// parseFileRecursive parses a config file and its parent found from key -// "extends" recursively until the root config file is reached. -// It returns the list of all parsed configs or the first non-nil error -// occurring in the process. -func parseFileRecursive( - filename string, - reprs []configparse.Representation, - seen set, -) ([]configparse.Representation, error) { - // avoid infinite recursion caused by circular reference - if err := seen.add(filename); err != nil { - return reprs, ErrCircularExtends - } - - // parse current file, append parsed config - repr, err := parseFile(filename) - if err != nil { - return reprs, err - } - reprs = append(reprs, repr) - - // root config reached: stop now and return the parsed configs - if repr.Extends == nil { - return reprs, nil - } - - // config has parent: resolve its path and parse it recursively - parentPath := filepath.Join(filepath.Dir(filename), *repr.Extends) - return parseFileRecursive(parentPath, reprs, seen) -} - -// parseFile parses a single config file and returns the result as an -// configparse.Representation and an appropriate error predeclared in the package. -func parseFile(filename string) (repr configparse.Representation, err error) { - b, err := os.ReadFile(filename) - switch { - case err == nil: - case errors.Is(err, os.ErrNotExist): - return repr, errorutil.WithDetails(ErrFileNotFound, filename) - default: - return repr, errorutil.WithDetails(ErrFileRead, filename, err) - } - - ext := extension(filepath.Ext(filename)) - parser, err := newParser(ext) - if err != nil { - return repr, errorutil.WithDetails(ErrFileExt, ext, err) - } - - if err = parser.Parse(b, &repr); err != nil { - return repr, errorutil.WithDetails(ErrParse, filename, err) - } - - return repr, nil -} - -// parseAndMergeConfigs iterates backwards over uconfs, parsing them -// as runner.ConfigGlobal and merging them into a single one. -// It returns the merged result or the first non-nil error occurring in the -// process. -func parseAndMergeConfigs(reprs []configparse.Representation) (cfg runner.Config, err error) { - if len(reprs) == 0 { // supposedly catched upstream, should not occur - return cfg, errors.New( - "an unacceptable error occurred parsing the config file, " + - "please visit https://github.com/benchttp/runner/issues/new " + - "and insult us properly", - ) - } - - cfg = runner.DefaultConfig() - - for i := len(reprs) - 1; i >= 0; i-- { - repr := reprs[i] - currentConfig, err := configparse.ParseRepresentation(repr) - if err != nil { - return cfg, errorutil.WithDetails(ErrParse, err) - } - cfg = currentConfig.Override(cfg) - } - - return cfg, nil -} diff --git a/internal/configfile/parse_test.go b/internal/configfile/parse_test.go deleted file mode 100644 index 06f7de2..0000000 --- a/internal/configfile/parse_test.go +++ /dev/null @@ -1,261 +0,0 @@ -package configfile_test - -import ( - "errors" - "fmt" - "net/http" - "net/url" - "path/filepath" - "reflect" - "testing" - "time" - - "github.com/benchttp/engine/runner" - - "github.com/benchttp/cli/internal/configfile" -) - -const ( - testdataConfigPath = "./testdata" - testURL = "http://localhost:9999?fib=30&delay=200ms" // value from testdata files -) - -var supportedExt = []string{ - ".yml", - ".yaml", - ".json", -} - -// TestParse ensures the config file is open, read, and correctly parsed. -func TestParse(t *testing.T) { - t.Run("return file errors early", func(t *testing.T) { - testcases := []struct { - label string - path string - expErr error - }{ - { - label: "not found", - path: configPath("invalid/bad path"), - expErr: configfile.ErrFileNotFound, - }, - { - label: "unsupported extension", - path: configPath("invalid/badext.yams"), - expErr: configfile.ErrFileExt, - }, - { - label: "yaml invalid fields", - path: configPath("invalid/badfields.yml"), - expErr: configfile.ErrParse, - }, - { - label: "json invalid fields", - path: configPath("invalid/badfields.json"), - expErr: configfile.ErrParse, - }, - { - label: "self reference", - path: configPath("extends/extends-circular-self.yml"), - expErr: configfile.ErrCircularExtends, - }, - { - label: "circular reference", - path: configPath("extends/extends-circular-0.yml"), - expErr: configfile.ErrCircularExtends, - }, - } - - for _, tc := range testcases { - t.Run(tc.label, func(t *testing.T) { - gotCfg, gotErr := configfile.Parse(tc.path) - - if gotErr == nil { - t.Fatal("exp non-nil error, got nil") - } - - if !errors.Is(gotErr, tc.expErr) { - t.Errorf("\nexp %v\ngot %v", tc.expErr, gotErr) - } - - if !reflect.DeepEqual(gotCfg, runner.Config{}) { - t.Errorf("\nexp empty config\ngot %v", gotCfg) - } - }) - } - }) - - t.Run("happy path for all extensions", func(t *testing.T) { - for _, ext := range supportedExt { - expCfg := newExpConfig() - fname := configPath("valid/benchttp" + ext) - - gotCfg, err := configfile.Parse(fname) - if err != nil { - // critical error, stop the test - t.Fatal(err) - } - - expURL, gotURL := expCfg.Request.URL, gotCfg.Request.URL - - // compare *url.URLs separately, as they contain unpredictable values - // they need special checks - if !sameURL(gotURL, expURL) { - t.Errorf("unexpected parsed URL:\nexp %v, got %v", expURL, gotURL) - } - - // replace unpredictable values (undetermined query params order) - restoreGotCfg := setTempValue(&gotURL.RawQuery, "replaced by test") - restoreExpCfg := setTempValue(&expURL.RawQuery, "replaced by test") - - if !gotCfg.Equal(expCfg) { - t.Errorf("unexpected parsed config for %s file:\nexp %v\ngot %v", ext, expCfg, gotCfg) - } - - restoreExpCfg() - restoreGotCfg() - } - }) - - t.Run("override default values", func(t *testing.T) { - const ( - expRequests = 0 // default is -1 - expGlobalTimeout = 42 * time.Millisecond - ) - - fname := configPath("valid/benchttp-zeros.yml") - - cfg, err := configfile.Parse(fname) - if err != nil { - t.Fatal(err) - } - - if gotRequests := cfg.Runner.Requests; gotRequests != expRequests { - t.Errorf("did not override Requests: exp %d, got %d", expRequests, gotRequests) - } - - if gotGlobalTimeout := cfg.Runner.GlobalTimeout; gotGlobalTimeout != expGlobalTimeout { - t.Errorf("did not override GlobalTimeout: exp %d, got %d", expGlobalTimeout, gotGlobalTimeout) - } - - t.Log(cfg) - }) - - t.Run("extend config files", func(t *testing.T) { - testcases := []struct { - label string - cfname string - cfpath string - }{ - { - label: "same directory", - cfname: "child", - cfpath: configPath("extends/extends-valid-child.yml"), - }, - { - label: "nested directory", - cfname: "nested", - cfpath: configPath("extends/nest-0/nest-1/extends-valid-nested.yml"), - }, - } - - for _, tc := range testcases { - t.Run(tc.label, func(t *testing.T) { - cfg, err := configfile.Parse(tc.cfpath) - if err != nil { - t.Fatal(err) - } - - var ( - expMethod = "POST" - expURL = fmt.Sprintf("http://%s.config", tc.cfname) - ) - - if gotMethod := cfg.Request.Method; gotMethod != expMethod { - t.Errorf("method: exp %s, got %s", expMethod, gotMethod) - } - - if gotURL := cfg.Request.URL.String(); gotURL != expURL { - t.Errorf("url: exp %s, got %s", expURL, gotURL) - } - }) - } - }) -} - -// helpers - -// newExpConfig returns the expected runner.ConfigConfig result after parsing -// one of the config files in testdataConfigPath. -func newExpConfig() runner.Config { - u, _ := url.ParseRequestURI(testURL) - return runner.Config{ - Request: runner.RequestConfig{ - Method: "POST", - URL: u, - Header: http.Header{ - "key0": []string{"val0", "val1"}, - "key1": []string{"val0"}, - }, - Body: runner.NewRequestBody("raw", `{"key0":"val0","key1":"val1"}`), - }, - Runner: runner.RecorderConfig{ - Requests: 100, - Concurrency: 1, - Interval: 50 * time.Millisecond, - RequestTimeout: 2 * time.Second, - GlobalTimeout: 60 * time.Second, - }, - Tests: []runner.TestCase{ - { - Name: "minimum response time", - Field: "ResponseTimes.Min", - Predicate: "GT", - Target: 80 * time.Millisecond, - }, - { - Name: "maximum response time", - Field: "ResponseTimes.Max", - Predicate: "LTE", - Target: 120 * time.Millisecond, - }, - { - Name: "100% availability", - Field: "RequestFailureCount", - Predicate: "EQ", - Target: 0, - }, - }, - } -} - -// sameURL returns true if a and b are the same *url.URL, taking into account -// the undeterministic nature of their RawQuery. -func sameURL(a, b *url.URL) bool { - // check query params equality via Query() rather than RawQuery - if !reflect.DeepEqual(a.Query(), b.Query()) { - return false - } - - // temporarily set RawQuery to a determined value - for _, u := range []*url.URL{a, b} { - defer setTempValue(&u.RawQuery, "replaced by test")() - } - - // we can now rely on deep equality check - return reflect.DeepEqual(a, b) -} - -// setTempValue sets *ptr to val and returns a restore func that sets *ptr -// back to its previous value. -func setTempValue(ptr *string, val string) (restore func()) { - previousValue := *ptr - *ptr = val - return func() { - *ptr = previousValue - } -} - -func configPath(name string) string { - return filepath.Join(testdataConfigPath, name) -} diff --git a/internal/configfile/parser.go b/internal/configfile/parser.go deleted file mode 100644 index c50876e..0000000 --- a/internal/configfile/parser.go +++ /dev/null @@ -1,35 +0,0 @@ -package configfile - -import ( - "errors" - - "github.com/benchttp/engine/configparse" -) - -type extension string - -const ( - extYML extension = ".yml" - extYAML extension = ".yaml" - extJSON extension = ".json" -) - -// configParser exposes a method parse to read bytes as a raw config. -type configParser interface { - // parse parses a raw bytes input as a raw config and stores - // the resulting value into dst. - Parse(in []byte, dst *configparse.Representation) error -} - -// newParser returns an appropriate parser according to ext, or a non-nil -// error if ext is not an expected extension. -func newParser(ext extension) (configParser, error) { - switch ext { - case extYML, extYAML: - return configparse.YAMLParser{}, nil - case extJSON: - return configparse.JSONParser{}, nil - default: - return nil, errors.New("unsupported config format") - } -} diff --git a/internal/configfile/testdata/extends/extends-circular-0.yml b/internal/configfile/testdata/extends/extends-circular-0.yml deleted file mode 100644 index 55c7ac7..0000000 --- a/internal/configfile/testdata/extends/extends-circular-0.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-1.yml diff --git a/internal/configfile/testdata/extends/extends-circular-1.yml b/internal/configfile/testdata/extends/extends-circular-1.yml deleted file mode 100644 index f451260..0000000 --- a/internal/configfile/testdata/extends/extends-circular-1.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-2.yml diff --git a/internal/configfile/testdata/extends/extends-circular-2.yml b/internal/configfile/testdata/extends/extends-circular-2.yml deleted file mode 100644 index b862fa4..0000000 --- a/internal/configfile/testdata/extends/extends-circular-2.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-0.yml diff --git a/internal/configfile/testdata/extends/extends-circular-self.yml b/internal/configfile/testdata/extends/extends-circular-self.yml deleted file mode 100644 index 2fa66ac..0000000 --- a/internal/configfile/testdata/extends/extends-circular-self.yml +++ /dev/null @@ -1 +0,0 @@ -extends: ./extends-circular-self.yml diff --git a/internal/configfile/testdata/extends/extends-valid-child.yml b/internal/configfile/testdata/extends/extends-valid-child.yml deleted file mode 100644 index a344080..0000000 --- a/internal/configfile/testdata/extends/extends-valid-child.yml +++ /dev/null @@ -1,4 +0,0 @@ -extends: ./extends-valid-parent.yml - -request: - url: http://child.config diff --git a/internal/configfile/testdata/extends/extends-valid-parent.yml b/internal/configfile/testdata/extends/extends-valid-parent.yml deleted file mode 100644 index 7f3b136..0000000 --- a/internal/configfile/testdata/extends/extends-valid-parent.yml +++ /dev/null @@ -1,3 +0,0 @@ -request: - method: POST - url: http://parent.config diff --git a/internal/configfile/testdata/extends/nest-0/nest-1/extends-valid-nested.yml b/internal/configfile/testdata/extends/nest-0/nest-1/extends-valid-nested.yml deleted file mode 100644 index 7810890..0000000 --- a/internal/configfile/testdata/extends/nest-0/nest-1/extends-valid-nested.yml +++ /dev/null @@ -1,4 +0,0 @@ -extends: ../../extends-valid-parent.yml - -request: - url: http://nested.config diff --git a/internal/configfile/testdata/invalid/badext.yams b/internal/configfile/testdata/invalid/badext.yams deleted file mode 100644 index f7aba57..0000000 --- a/internal/configfile/testdata/invalid/badext.yams +++ /dev/null @@ -1,2 +0,0 @@ -request: - url: https://benchttp.app diff --git a/internal/configfile/testdata/invalid/badfields.json b/internal/configfile/testdata/invalid/badfields.json deleted file mode 100644 index 9b45468..0000000 --- a/internal/configfile/testdata/invalid/badfields.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "runner": { - "requests": [123], - "concurrency": "123" - }, - "notafield": 123 -} diff --git a/internal/configfile/testdata/invalid/badfields.yml b/internal/configfile/testdata/invalid/badfields.yml deleted file mode 100644 index 6899cd8..0000000 --- a/internal/configfile/testdata/invalid/badfields.yml +++ /dev/null @@ -1,4 +0,0 @@ -runner: - requests: [123] # error: invalid type for field requests - concurrency: "123" # error: invalid type for field concurrency -notafield: 123 # error: field does not exist diff --git a/internal/configfile/testdata/valid/benchttp-zeros.yml b/internal/configfile/testdata/valid/benchttp-zeros.yml deleted file mode 100644 index 38b1bdb..0000000 --- a/internal/configfile/testdata/valid/benchttp-zeros.yml +++ /dev/null @@ -1,3 +0,0 @@ -runner: - requests: 0 - globalTimeout: 42ms diff --git a/internal/configfile/testdata/valid/benchttp.json b/internal/configfile/testdata/valid/benchttp.json deleted file mode 100644 index 714506f..0000000 --- a/internal/configfile/testdata/valid/benchttp.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "request": { - "method": "POST", - "url": "http://localhost:9999?delay=200ms", - "queryParams": { - "fib": "30" - }, - "header": { - "key0": ["val0", "val1"], - "key1": ["val0"] - }, - "body": { - "type": "raw", - "content": "{\"key0\":\"val0\",\"key1\":\"val1\"}" - } - }, - "runner": { - "requests": 100, - "concurrency": 1, - "interval": "50ms", - "requestTimeout": "2s", - "globalTimeout": "60s" - }, - "tests": [ - { - "name": "minimum response time", - "field": "ResponseTimes.Min", - "predicate": "GT", - "target": "80ms" - }, - { - "name": "maximum response time", - "field": "ResponseTimes.Max", - "predicate": "LTE", - "target": "120ms" - }, - { - "name": "100% availability", - "field": "RequestFailureCount", - "predicate": "EQ", - "target": "0" - } - ] -} diff --git a/internal/configfile/testdata/valid/benchttp.yaml b/internal/configfile/testdata/valid/benchttp.yaml deleted file mode 100644 index 2ee790c..0000000 --- a/internal/configfile/testdata/valid/benchttp.yaml +++ /dev/null @@ -1,35 +0,0 @@ -x-custom: &data - method: POST - url: http://localhost:9999?delay=200ms - -request: - <<: *data - queryParams: - fib: 30 - header: - key0: [val0, val1] - key1: [val0] - body: - type: raw - content: '{"key0":"val0","key1":"val1"}' - -runner: - requests: 100 - concurrency: 1 - interval: 50ms - requestTimeout: 2s - globalTimeout: 60s - -tests: - - name: minimum response time - field: ResponseTimes.Min - predicate: GT - target: 80ms - - name: maximum response time - field: ResponseTimes.Max - predicate: LTE - target: 120ms - - name: 100% availability - field: RequestFailureCount - predicate: EQ - target: 0 diff --git a/internal/configfile/testdata/valid/benchttp.yml b/internal/configfile/testdata/valid/benchttp.yml deleted file mode 100644 index 27a2fc9..0000000 --- a/internal/configfile/testdata/valid/benchttp.yml +++ /dev/null @@ -1,32 +0,0 @@ -request: - method: POST - url: http://localhost:9999?delay=200ms - queryParams: - fib: 30 - header: - key0: [val0, val1] - key1: [val0] - body: - type: raw - content: '{"key0":"val0","key1":"val1"}' - -runner: - requests: 100 - concurrency: 1 - interval: 50ms - requestTimeout: 2s - globalTimeout: 60s - -tests: - - name: minimum response time - field: ResponseTimes.Min - predicate: GT - target: 80ms - - name: maximum response time - field: ResponseTimes.Max - predicate: LTE - target: 120ms - - name: 100% availability - field: RequestFailureCount - predicate: EQ - target: 0 diff --git a/internal/configflag/bind.go b/internal/configflag/bind.go index 359cae8..2cfb717 100644 --- a/internal/configflag/bind.go +++ b/internal/configflag/bind.go @@ -1,76 +1,139 @@ package configflag import ( + "bytes" + "errors" "flag" + "fmt" + "io" "net/http" "net/url" + "strconv" + "strings" + "time" - "github.com/benchttp/engine/runner" + "github.com/benchttp/engine/configio" ) -// Bind reads arguments provided to flagset as config.Fields and binds -// their value to the appropriate fields of given *config.Global. +// Bind reads arguments provided to flagset as config fields +// and binds their value to the appropriate fields of dst. // The provided *flag.Flagset must not have been parsed yet, otherwise // bindings its values would fail. -func Bind(flagset *flag.FlagSet, dst *runner.Config) { - // avoid nil pointer dereferences - if dst.Request.URL == nil { - dst.Request.URL = &url.URL{} - } - if dst.Request.Header == nil { - dst.Request.Header = http.Header{} +func Bind(flagset *flag.FlagSet, dst *configio.Builder) { + for field, bind := range bindings { + flagset.Func(field, flagsUsage[field], bind(dst)) } +} - // request url - flagset.Var(urlValue{url: dst.Request.URL}, - runner.ConfigFieldURL, - runner.ConfigFieldsUsage[runner.ConfigFieldURL], - ) - // request method - flagset.StringVar(&dst.Request.Method, - runner.ConfigFieldMethod, - dst.Request.Method, - runner.ConfigFieldsUsage[runner.ConfigFieldMethod], - ) - // request header - flagset.Var(headerValue{header: &dst.Request.Header}, - runner.ConfigFieldHeader, - runner.ConfigFieldsUsage[runner.ConfigFieldHeader], - ) - // request body - flagset.Var(bodyValue{body: &dst.Request.Body}, - runner.ConfigFieldBody, - runner.ConfigFieldsUsage[runner.ConfigFieldBody], - ) - // requests number - flagset.IntVar(&dst.Runner.Requests, - runner.ConfigFieldRequests, - dst.Runner.Requests, - runner.ConfigFieldsUsage[runner.ConfigFieldRequests], - ) +type setter = func(string) error - // concurrency - flagset.IntVar(&dst.Runner.Concurrency, - runner.ConfigFieldConcurrency, - dst.Runner.Concurrency, - runner.ConfigFieldsUsage[runner.ConfigFieldConcurrency], - ) - // non-conurrent requests interval - flagset.DurationVar(&dst.Runner.Interval, - runner.ConfigFieldInterval, - dst.Runner.Interval, - runner.ConfigFieldsUsage[runner.ConfigFieldInterval], - ) - // request timeout - flagset.DurationVar(&dst.Runner.RequestTimeout, - runner.ConfigFieldRequestTimeout, - dst.Runner.RequestTimeout, - runner.ConfigFieldsUsage[runner.ConfigFieldRequestTimeout], - ) - // global timeout - flagset.DurationVar(&dst.Runner.GlobalTimeout, - runner.ConfigFieldGlobalTimeout, - dst.Runner.GlobalTimeout, - runner.ConfigFieldsUsage[runner.ConfigFieldGlobalTimeout], - ) +var bindings = map[string]func(*configio.Builder) setter{ + flagMethod: func(b *configio.Builder) setter { + return func(in string) error { + b.SetRequestMethod(in) + return nil + } + }, + flagURL: func(b *configio.Builder) setter { + return func(in string) error { + u, err := url.ParseRequestURI(in) + if err != nil { + return err + } + b.SetRequestURL(u) + return nil + } + }, + flagHeader: func(b *configio.Builder) setter { + return func(in string) error { + keyval := strings.SplitN(in, ":", 2) + if len(keyval) != 2 { + return errors.New(`-header: expect format ":"`) + } + key, val := keyval[0], keyval[1] + b.SetRequestHeaderFunc(func(h http.Header) http.Header { + if h == nil { + h = http.Header{} + } + h[key] = append(h[key], val) + return h + }) + return nil + } + }, + flagBody: func(b *configio.Builder) setter { + return func(in string) error { + errFormat := fmt.Errorf(`expect format ":", got %q`, in) + if in == "" { + return errFormat + } + split := strings.SplitN(in, ":", 2) + if len(split) != 2 { + return errFormat + } + btype, bcontent := split[0], split[1] + if bcontent == "" { + return errFormat + } + switch btype { + case "raw": + b.SetRequestBody(io.NopCloser(bytes.NewBufferString(bcontent))) + // case "file": + // // TODO + default: + return fmt.Errorf(`unsupported type: %s (only "raw" accepted)`, btype) + } + return nil + } + }, + flagRequests: func(b *configio.Builder) setter { + return func(in string) error { + n, err := strconv.Atoi(in) + if err != nil { + return err + } + b.SetRequests(n) + return nil + } + }, + flagConcurrency: func(b *configio.Builder) setter { + return func(in string) error { + n, err := strconv.Atoi(in) + if err != nil { + return err + } + b.SetConcurrency(n) + return nil + } + }, + flagInterval: func(b *configio.Builder) setter { + return func(in string) error { + d, err := time.ParseDuration(in) + if err != nil { + return err + } + b.SetInterval(d) + return nil + } + }, + flagRequestTimeout: func(b *configio.Builder) setter { + return func(in string) error { + d, err := time.ParseDuration(in) + if err != nil { + return err + } + b.SetRequestTimeout(d) + return nil + } + }, + flagGlobalTimeout: func(b *configio.Builder) setter { + return func(in string) error { + d, err := time.ParseDuration(in) + if err != nil { + return err + } + b.SetGlobalTimeout(d) + return nil + } + }, } diff --git a/internal/configflag/bind_test.go b/internal/configflag/bind_test.go index 5c54727..f9e9fd4 100644 --- a/internal/configflag/bind_test.go +++ b/internal/configflag/bind_test.go @@ -1,39 +1,43 @@ package configflag_test import ( + "bytes" "flag" + "io" "net/http" - "reflect" + "net/url" "testing" "time" - "github.com/benchttp/engine/runner" + "github.com/benchttp/engine/benchttp" + "github.com/benchttp/engine/benchttptest" + "github.com/benchttp/engine/configio" "github.com/benchttp/cli/internal/configflag" ) func TestBind(t *testing.T) { - t.Run("default to base config", func(t *testing.T) { - flagset := flag.NewFlagSet("run", flag.ExitOnError) + t.Run("default to zero runner", func(t *testing.T) { + flagset := flag.NewFlagSet("", flag.ExitOnError) args := []string{} // no args - cfg := runner.DefaultConfig() - configflag.Bind(flagset, &cfg) + b := configio.Builder{} + configflag.Bind(flagset, &b) if err := flagset.Parse(args); err != nil { t.Fatal(err) // critical error, stop the test } - if exp := runner.DefaultConfig(); !reflect.DeepEqual(cfg, exp) { - t.Errorf("\nexp %#v\ngot %#v", exp, cfg) - } + benchttptest.AssertEqualRunners(t, benchttp.Runner{}, b.Runner()) }) t.Run("set config with flags values", func(t *testing.T) { - flagset := flag.NewFlagSet("run", flag.ExitOnError) + flagset := flag.NewFlagSet("", flag.ExitOnError) args := []string{ "-method", "POST", - "-url", "https://benchttp.app?cool=yes", - "-header", "Content-Type:application/json", + "-url", "https://example.com?a=b", + "-header", "API_KEY:abc", + "-header", "Accept:text/html", + "-header", "Accept:application/json", "-body", "raw:hello", "-requests", "1", "-concurrency", "2", @@ -42,29 +46,38 @@ func TestBind(t *testing.T) { "-globalTimeout", "5s", } - cfg := runner.Config{} - configflag.Bind(flagset, &cfg) + b := configio.Builder{} + configflag.Bind(flagset, &b) if err := flagset.Parse(args); err != nil { t.Fatal(err) // critical error, stop the test } - exp := runner.Config{ - Request: runner.RequestConfig{ - Method: "POST", - Header: http.Header{"Content-Type": {"application/json"}}, - Body: runner.RequestBody{Type: "raw", Content: []byte("hello")}, - }.WithURL("https://benchttp.app?cool=yes"), - Runner: runner.RecorderConfig{ + benchttptest.AssertEqualRunners(t, + benchttp.Runner{ + Request: &http.Request{ + Method: "POST", + URL: mustParseURL("https://example.com?a=b"), + Header: http.Header{ + "API_KEY": []string{"abc"}, + "Accept": []string{"text/html", "application/json"}, + }, + Body: io.NopCloser(bytes.NewBufferString("hello")), + }, Requests: 1, Concurrency: 2, Interval: 3 * time.Second, RequestTimeout: 4 * time.Second, GlobalTimeout: 5 * time.Second, }, - } - - if !reflect.DeepEqual(cfg, exp) { - t.Errorf("\nexp %#v\ngot %#v", exp, cfg) - } + b.Runner(), + ) }) } + +func mustParseURL(v string) *url.URL { + u, err := url.ParseRequestURI(v) + if err != nil { + panic("mustParseURL: " + err.Error()) + } + return u +} diff --git a/internal/configflag/body.go b/internal/configflag/body.go deleted file mode 100644 index f3f0dfb..0000000 --- a/internal/configflag/body.go +++ /dev/null @@ -1,50 +0,0 @@ -package configflag - -import ( - "fmt" - "strings" - - "github.com/benchttp/engine/runner" -) - -// bodyValue implements flag.Value -type bodyValue struct { - body *runner.RequestBody -} - -// String returns a string representation of the referenced body. -func (v bodyValue) String() string { - return fmt.Sprint(v.body) -} - -// Set reads input string in format "type:content" and sets -// the referenced body accordingly. -// -// Note: only type "raw" is supported at the moment. -func (v bodyValue) Set(raw string) error { - errFormat := fmt.Errorf(`expect format ":", got "%s"`, raw) - - if raw == "" { - return errFormat - } - - split := strings.SplitN(raw, ":", 2) - if len(split) != 2 { - return errFormat - } - - btype, bcontent := split[0], split[1] - if bcontent == "" { - return errFormat - } - - switch btype { - case "raw": - *v.body = runner.NewRequestBody(btype, bcontent) - // case "file": - // // TODO - default: - return fmt.Errorf(`unsupported type: %s (only "raw" accepted)`, btype) - } - return nil -} diff --git a/internal/configflag/configflag.go b/internal/configflag/configflag.go new file mode 100644 index 0000000..e841413 --- /dev/null +++ b/internal/configflag/configflag.go @@ -0,0 +1,28 @@ +package configflag + +const ( + flagMethod = "method" + flagURL = "url" + flagHeader = "header" + flagBody = "body" + flagRequests = "requests" + flagConcurrency = "concurrency" + flagInterval = "interval" + flagRequestTimeout = "requestTimeout" + flagGlobalTimeout = "globalTimeout" + flagTests = "tests" +) + +// flagsUsage is a record of all available config flags and their usage. +var flagsUsage = map[string]string{ + flagMethod: "HTTP request method", + flagURL: "HTTP request url", + flagHeader: "HTTP request header", + flagBody: "HTTP request body", + flagRequests: "Number of requests to run, use duration as exit condition if omitted", + flagConcurrency: "Number of connections to run concurrently", + flagInterval: "Minimum duration between two non concurrent requests", + flagRequestTimeout: "Timeout for each HTTP request", + flagGlobalTimeout: "Max duration of test", + flagTests: "Test suite", +} diff --git a/internal/configflag/header.go b/internal/configflag/header.go deleted file mode 100644 index 232180e..0000000 --- a/internal/configflag/header.go +++ /dev/null @@ -1,30 +0,0 @@ -package configflag - -import ( - "errors" - "fmt" - "net/http" - "strings" -) - -// headerValue implements flag.Value -type headerValue struct { - header *http.Header -} - -// String returns a string representation of the referenced header. -func (v headerValue) String() string { - return fmt.Sprint(v.header) -} - -// Set reads input string in format "key:value" and appends value -// to the key's values of the referenced header. -func (v headerValue) Set(raw string) error { - keyval := strings.SplitN(raw, ":", 2) - if len(keyval) != 2 { - return errors.New(`expect format ":"`) - } - key, val := keyval[0], keyval[1] - (*v.header)[key] = append((*v.header)[key], val) - return nil -} diff --git a/internal/configflag/url.go b/internal/configflag/url.go deleted file mode 100644 index c1eb907..0000000 --- a/internal/configflag/url.go +++ /dev/null @@ -1,29 +0,0 @@ -package configflag - -import ( - "fmt" - "net/url" -) - -// urlValue implements flag.Value -type urlValue struct { - url *url.URL -} - -// String returns a string representation of urlValue.url. -func (v urlValue) String() string { - if v.url == nil { - return "" - } - return v.url.String() -} - -// Set parses input string as a URL and sets the referenced URL accordingly. -func (v urlValue) Set(in string) error { - urlURL, err := url.ParseRequestURI(in) - if err != nil { - return fmt.Errorf(`invalid url: "%s"`, in) - } - *v.url = *urlURL - return nil -} diff --git a/internal/configflag/which.go b/internal/configflag/which.go deleted file mode 100644 index c084ecd..0000000 --- a/internal/configflag/which.go +++ /dev/null @@ -1,19 +0,0 @@ -package configflag - -import ( - "flag" - - "github.com/benchttp/engine/runner" -) - -// Which returns a slice of all config fields set via the CLI -// for the given *flag.FlagSet. -func Which(flagset *flag.FlagSet) []string { - var fields []string - flagset.Visit(func(f *flag.Flag) { - if name := f.Name; runner.IsConfigField(name) { - fields = append(fields, name) - } - }) - return fields -} diff --git a/internal/configflag/which_test.go b/internal/configflag/which_test.go deleted file mode 100644 index 80cf4ae..0000000 --- a/internal/configflag/which_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package configflag_test - -import ( - "flag" - "reflect" - "testing" - - "github.com/benchttp/engine/runner" - - "github.com/benchttp/cli/internal/configflag" -) - -func TestWhich(t *testing.T) { - for _, tc := range []struct { - label string - args []string - exp []string - }{ - { - label: "return all config flags set", - args: []string{ - "-method", "POST", - "-url", "https://benchttp.app?cool=yes", - "-concurrency", "2", - "-requests", "3", - "-requestTimeout", "1s", - "-globalTimeout", "4s", - }, - exp: []string{ - "concurrency", "globalTimeout", "method", - "requestTimeout", "requests", "url", - }, - }, - { - label: "do not return config flags not set", - args: []string{"-requests", "3"}, - exp: []string{"requests"}, - }, - } { - flagset := flag.NewFlagSet("run", flag.ExitOnError) - - configflag.Bind(flagset, &runner.Config{}) - - if err := flagset.Parse(tc.args); err != nil { - t.Fatal(err) // critical error, stop the test - } - - if got := configflag.Which(flagset); !reflect.DeepEqual(got, tc.exp) { - t.Errorf("\nexp %v\ngot %v", tc.exp, got) - } - } -} diff --git a/internal/render/progress.go b/internal/render/progress.go index 2495e88..4f5c673 100644 --- a/internal/render/progress.go +++ b/internal/render/progress.go @@ -6,14 +6,14 @@ import ( "strconv" "strings" - "github.com/benchttp/engine/runner" + "github.com/benchttp/engine/benchttp" "github.com/benchttp/cli/internal/render/ansi" ) // Progress renders a fancy representation of a runner.RecordingProgress // and writes the result to w. -func Progress(w io.Writer, p runner.RecordingProgress) (int, error) { +func Progress(w io.Writer, p benchttp.RecordingProgress) (int, error) { return fmt.Fprint(w, progressString(p)) } @@ -21,7 +21,7 @@ func Progress(w io.Writer, p runner.RecordingProgress) (int, error) { // for a fancy display in a CLI: // // RUNNING ◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎◼︎ 50% | 50/100 requests | 27s timeout -func progressString(p runner.RecordingProgress) string { +func progressString(p benchttp.RecordingProgress) string { var ( countdown = p.Timeout - p.Elapsed reqmax = strconv.Itoa(p.MaxCount) @@ -68,20 +68,20 @@ func renderTimeline(pctdone int) string { // renderStatus returns a string representing the status, // depending on whether the run is done or not and the value // of its context error. -func renderStatus(status runner.RecordingStatus) string { +func renderStatus(status benchttp.RecordingStatus) string { styled := statusStyle(status) return styled(string(status)) } -func statusStyle(status runner.RecordingStatus) ansi.StyleFunc { +func statusStyle(status benchttp.RecordingStatus) ansi.StyleFunc { switch status { - case runner.StatusRunning: + case benchttp.StatusRunning: return ansi.Yellow - case runner.StatusDone: + case benchttp.StatusDone: return ansi.Green - case runner.StatusCanceled: + case benchttp.StatusCanceled: return ansi.Red - case runner.StatusTimeout: + case benchttp.StatusTimeout: return ansi.Cyan } return ansi.Grey // should not occur diff --git a/internal/render/report.go b/internal/render/report.go index d5f996f..ced37b7 100644 --- a/internal/render/report.go +++ b/internal/render/report.go @@ -7,17 +7,17 @@ import ( "strings" "time" - "github.com/benchttp/engine/runner" + "github.com/benchttp/engine/benchttp" "github.com/benchttp/cli/internal/render/ansi" ) -func ReportSummary(w io.Writer, rep *runner.Report) (int, error) { +func ReportSummary(w io.Writer, rep *benchttp.Report) (int, error) { return w.Write([]byte(ReportSummaryString(rep))) } // String returns a default summary of the Report as a string. -func ReportSummaryString(rep *runner.Report) string { +func ReportSummaryString(rep *benchttp.Report) string { var b strings.Builder line := func(name string, value interface{}) string { @@ -37,15 +37,13 @@ func ReportSummaryString(rep *runner.Report) string { return fmt.Sprintf("%d/%s", n, maxString) } - var ( - m = rep.Metrics - cfg = rep.Metadata.Config - ) + m := rep.Metrics + r := rep.Metadata.Runner b.WriteString(ansi.Bold("→ Summary")) b.WriteString("\n") - b.WriteString(line("Endpoint", cfg.Request.URL)) - b.WriteString(line("Requests", formatRequests(len(m.Records), cfg.Runner.Requests))) + b.WriteString(line("Endpoint", r.Request.URL)) + b.WriteString(line("Requests", formatRequests(len(m.Records), r.Requests))) b.WriteString(line("Errors", len(m.RequestFailures))) b.WriteString(line("Min response time", msString(m.ResponseTimes.Min))) b.WriteString(line("Max response time", msString(m.ResponseTimes.Max))) diff --git a/internal/render/report_test.go b/internal/render/report_test.go index 1fad710..3771798 100644 --- a/internal/render/report_test.go +++ b/internal/render/report_test.go @@ -1,10 +1,11 @@ package render_test import ( + "net/http" "testing" "time" - "github.com/benchttp/engine/runner" + "github.com/benchttp/engine/benchttp" "github.com/benchttp/cli/internal/render" "github.com/benchttp/cli/internal/render/ansi" @@ -13,12 +14,12 @@ import ( func TestReport_String(t *testing.T) { t.Run("returns metrics summary", func(t *testing.T) { metrics, duration := metricsStub() - cfg := configStub() + runner := runnerStub() - rep := &runner.Report{ + rep := &benchttp.Report{ Metrics: metrics, - Metadata: runner.ReportMetadata{ - Config: cfg, + Metadata: benchttp.Metadata{ + Runner: runner, TotalDuration: duration, }, } @@ -28,13 +29,13 @@ func TestReport_String(t *testing.T) { // helpers -func metricsStub() (agg runner.MetricsAggregate, total time.Duration) { - return runner.MetricsAggregate{ +func metricsStub() (agg benchttp.MetricsAggregate, total time.Duration) { + return benchttp.MetricsAggregate{ RequestFailures: make([]struct { Reason string }, 1), Records: make([]struct{ ResponseTime time.Duration }, 3), - ResponseTimes: runner.MetricsTimeStats{ + ResponseTimes: benchttp.MetricsTimeStats{ Min: 4 * time.Second, Max: 6 * time.Second, Mean: 5 * time.Second, @@ -42,11 +43,11 @@ func metricsStub() (agg runner.MetricsAggregate, total time.Duration) { }, 15 * time.Second } -func configStub() runner.Config { - cfg := runner.Config{} - cfg.Request = cfg.Request.WithURL("https://a.b.com") - cfg.Runner.Requests = -1 - return cfg +func runnerStub() benchttp.Runner { + runner := benchttp.Runner{} + runner.Request = mustMakeRequest("https://a.b.com") + runner.Requests = -1 + return runner } func checkSummary(t *testing.T, summary string) { @@ -67,3 +68,11 @@ Total duration 15000ms t.Errorf("\nexp summary:\n%q\ngot summary:\n%q", expSummary, summary) } } + +func mustMakeRequest(uri string) *http.Request { + req, err := http.NewRequest("GET", uri, nil) + if err != nil { + panic(err) + } + return req +} diff --git a/internal/render/testsuite.go b/internal/render/testsuite.go index 648ad17..185965c 100644 --- a/internal/render/testsuite.go +++ b/internal/render/testsuite.go @@ -4,17 +4,17 @@ import ( "io" "strings" - "github.com/benchttp/engine/runner" + "github.com/benchttp/engine/benchttp" "github.com/benchttp/cli/internal/render/ansi" ) -func TestSuite(w io.Writer, suite runner.TestSuiteResults) (int, error) { +func TestSuite(w io.Writer, suite benchttp.TestSuiteResults) (int, error) { return w.Write([]byte(TestSuiteString(suite))) } // String returns a default summary of the Report as a string. -func TestSuiteString(suite runner.TestSuiteResults) string { +func TestSuiteString(suite benchttp.TestSuiteResults) string { if len(suite.Results) == 0 { return "" } diff --git a/internal/testutil/http.go b/internal/testutil/http.go new file mode 100644 index 0000000..34b660b --- /dev/null +++ b/internal/testutil/http.go @@ -0,0 +1,15 @@ +package testutil + +import ( + "bytes" + "net/http" +) + +func MustMakeRequest(method, uri string, header http.Header, body []byte) *http.Request { + req, err := http.NewRequest(method, uri, bytes.NewReader(body)) + if err != nil { + panic("testutil.MustMakeRequest: " + err.Error()) + } + req.Header = header + return req +}