Skip to content
Merged
42 changes: 22 additions & 20 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ func init() {
}

type RunConfig struct {
Target string
ParsedTarget *url.URL
Requests int
Concurrency int
Timeout time.Duration
Duration time.Duration
Method string
Body string
BodyFile string
Headers []string
Verbose bool
Target string
ParsedTarget *url.URL
Requests int
Concurrency int
Timeout time.Duration
Duration time.Duration
Method string
Body string
BodyFile string
Headers []string
Verbose bool
DisableRedirects bool
}

var validMethods = map[string]bool{
Expand Down Expand Up @@ -99,14 +100,15 @@ func (c *RunConfig) Validate() error {

func (c *RunConfig) ToHTTPConfig() httpclient.Config {
return httpclient.Config{
Target: c.Target,
Requests: c.Requests,
Concurrency: c.Concurrency,
Timeout: c.Timeout,
Duration: c.Duration,
Method: c.Method,
Body: c.Body,
Headers: c.Headers,
Verbose: c.Verbose,
Target: c.Target,
Requests: c.Requests,
Concurrency: c.Concurrency,
Timeout: c.Timeout,
Duration: c.Duration,
Method: c.Method,
Body: c.Body,
Headers: c.Headers,
Verbose: c.Verbose,
DisableRedirects: c.DisableRedirects,
}
}
41 changes: 23 additions & 18 deletions cmd/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,29 +300,31 @@ func TestRunConfig_Validate(t *testing.T) {

func TestRunConfig_ToHTTPConfig(t *testing.T) {
rc := RunConfig{
Target: "https://example.com/api",
Requests: 100,
Concurrency: 10,
Timeout: 5 * time.Second,
Duration: 30 * time.Second,
Method: "POST",
Body: `{"key":"value"}`,
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
Verbose: true,
Target: "https://example.com/api",
Requests: 100,
Concurrency: 10,
Timeout: 5 * time.Second,
Duration: 30 * time.Second,
Method: "POST",
Body: `{"key":"value"}`,
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
Verbose: true,
DisableRedirects: true,
}

got := rc.ToHTTPConfig()

want := httpclient.Config{
Target: "https://example.com/api",
Requests: 100,
Concurrency: 10,
Timeout: 5 * time.Second,
Duration: 30 * time.Second,
Method: "POST",
Body: `{"key":"value"}`,
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
Verbose: true,
Target: "https://example.com/api",
Requests: 100,
Concurrency: 10,
Timeout: 5 * time.Second,
Duration: 30 * time.Second,
Method: "POST",
Body: `{"key":"value"}`,
Headers: []string{"Authorization: Bearer token", "X-Custom: val:with:colons"},
Verbose: true,
DisableRedirects: true,
}

if got.Target != want.Target {
Expand All @@ -331,6 +333,9 @@ func TestRunConfig_ToHTTPConfig(t *testing.T) {
if got.Verbose != want.Verbose {
t.Errorf("Verbose: got %v, want %v", got.Verbose, want.Verbose)
}
if got.DisableRedirects != want.DisableRedirects {
t.Errorf("DisableRedirects: got %v, want %v", got.DisableRedirects, want.DisableRedirects)
}
if got.Requests != want.Requests {
t.Errorf("Requests: got %d, want %d", got.Requests, want.Requests)
}
Expand Down
25 changes: 15 additions & 10 deletions cmd/configfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ import (
)

type fileConfig struct {
Target *string `json:"target" yaml:"target"`
Requests *int `json:"requests" yaml:"requests"`
Concurrency *int `json:"concurrency" yaml:"concurrency"`
Timeout *string `json:"timeout" yaml:"timeout"`
Duration *string `json:"duration" yaml:"duration"`
Method *string `json:"method" yaml:"method"`
Body *string `json:"body" yaml:"body"`
BodyFile *string `json:"body_file" yaml:"body_file"`
Headers []string `json:"headers" yaml:"headers"`
Verbose *bool `json:"verbose" yaml:"verbose"`
Target *string `json:"target" yaml:"target"`
Requests *int `json:"requests" yaml:"requests"`
Concurrency *int `json:"concurrency" yaml:"concurrency"`
Timeout *string `json:"timeout" yaml:"timeout"`
Duration *string `json:"duration" yaml:"duration"`
Method *string `json:"method" yaml:"method"`
Body *string `json:"body" yaml:"body"`
BodyFile *string `json:"body_file" yaml:"body_file"`
Headers []string `json:"headers" yaml:"headers"`
Verbose *bool `json:"verbose" yaml:"verbose"`
DisableRedirects *bool `json:"disable_redirects" yaml:"disable_redirects"`
}

func loadConfig(path string) (*fileConfig, error) {
Expand Down Expand Up @@ -130,5 +131,9 @@ func mergeConfig(file *fileConfig, cli RunConfig, changed map[string]bool) (RunC
merged.Verbose = *file.Verbose
}

if file.DisableRedirects != nil && !changed["disable-redirects"] {
merged.DisableRedirects = *file.DisableRedirects
}

return merged, nil
}
68 changes: 39 additions & 29 deletions cmd/configfile_merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ func TestMergeConfig_NilFileConfig(t *testing.T) {

func TestMergeConfig_FileValuesUsedWhenCLIUnchanged(t *testing.T) {
file := &fileConfig{
Target: strPtr("https://file.example.com"),
Requests: intPtr(200),
Concurrency: intPtr(20),
Timeout: strPtr("30s"),
Duration: strPtr("1m"),
Method: strPtr("POST"),
Body: strPtr(`{"data":"test"}`),
Headers: []string{"X-From: file"},
Verbose: func() *bool { b := true; return &b }(),
Target: strPtr("https://file.example.com"),
Requests: intPtr(200),
Concurrency: intPtr(20),
Timeout: strPtr("30s"),
Duration: strPtr("1m"),
Method: strPtr("POST"),
Body: strPtr(`{"data":"test"}`),
Headers: []string{"X-From: file"},
Verbose: func() *bool { b := true; return &b }(),
DisableRedirects: func() *bool { b := true; return &b }(),
}

cli := RunConfig{
Expand All @@ -64,6 +65,9 @@ func TestMergeConfig_FileValuesUsedWhenCLIUnchanged(t *testing.T) {
if got.Verbose != true {
t.Errorf("Verbose: got %v, want true", got.Verbose)
}
if got.DisableRedirects != true {
t.Errorf("DisableRedirects: got %v, want true", got.DisableRedirects)
}
if got.Requests != 200 {
t.Errorf("Requests: got %d, want 200", got.Requests)
}
Expand All @@ -89,32 +93,35 @@ func TestMergeConfig_FileValuesUsedWhenCLIUnchanged(t *testing.T) {

func TestMergeConfig_CLIOverridesFileValues(t *testing.T) {
file := &fileConfig{
Target: strPtr("https://file.example.com"),
Requests: intPtr(200),
Concurrency: intPtr(20),
Timeout: strPtr("30s"),
Method: strPtr("POST"),
Body: strPtr(`{"data":"file"}`),
Headers: []string{"X-From: file"},
Target: strPtr("https://file.example.com"),
Requests: intPtr(200),
Concurrency: intPtr(20),
Timeout: strPtr("30s"),
Method: strPtr("POST"),
Body: strPtr(`{"data":"file"}`),
Headers: []string{"X-From: file"},
DisableRedirects: func() *bool { b := true; return &b }(),
}

cli := RunConfig{
Target: "https://cli.example.com",
Requests: 500,
Concurrency: 50,
Timeout: 5 * time.Second,
Method: "PUT",
Body: `{"data":"cli"}`,
Headers: []string{"X-From: cli"},
Target: "https://cli.example.com",
Requests: 500,
Concurrency: 50,
Timeout: 5 * time.Second,
Method: "PUT",
Body: `{"data":"cli"}`,
Headers: []string{"X-From: cli"},
DisableRedirects: false,
}

changed := map[string]bool{
"requests": true,
"concurrency": true,
"timeout": true,
"method": true,
"body": true,
"header": true,
"requests": true,
"concurrency": true,
"timeout": true,
"method": true,
"body": true,
"header": true,
"disable-redirects": true,
}

got, err := mergeConfig(file, cli, changed)
Expand Down Expand Up @@ -143,6 +150,9 @@ func TestMergeConfig_CLIOverridesFileValues(t *testing.T) {
if len(got.Headers) != 1 || got.Headers[0] != "X-From: cli" {
t.Errorf("Headers: got %v, want [X-From: cli] (CLI override)", got.Headers)
}
if got.DisableRedirects != false {
t.Errorf("DisableRedirects: got %v, want false (CLI override)", got.DisableRedirects)
}
}

func TestMergeConfig_PartialFileConfig(t *testing.T) {
Expand Down
23 changes: 13 additions & 10 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Latency Percentiles:
bodyFile, _ := f.GetString("body-file")
headers, _ := f.GetStringArray("header")
verbose, _ := f.GetBool("verbose")
disableRedirects, _ := f.GetBool("disable-redirects")
outputFormat, _ := f.GetString("output")

if outputFormat != "text" && outputFormat != "json" {
Expand All @@ -69,16 +70,17 @@ Latency Percentiles:
}

cliConfig := RunConfig{
Target: target,
Requests: requests,
Concurrency: concurrency,
Timeout: timeout,
Duration: duration,
Method: strings.ToUpper(method),
Body: body,
BodyFile: bodyFile,
Headers: headers,
Verbose: verbose,
Target: target,
Requests: requests,
Concurrency: concurrency,
Timeout: timeout,
Duration: duration,
Method: strings.ToUpper(method),
Body: body,
BodyFile: bodyFile,
Headers: headers,
Verbose: verbose,
DisableRedirects: disableRedirects,
}

changed := make(map[string]bool)
Expand Down Expand Up @@ -142,6 +144,7 @@ Latency Percentiles:
cmd.Flags().StringArrayP("header", "H", []string{}, "HTTP header in 'Key: Value' format (can be repeated)")
cmd.Flags().StringP("config", "f", "", "Path to configuration file (JSON/YAML)")
cmd.Flags().BoolP("verbose", "v", false, "Enable verbose output")
cmd.Flags().Bool("disable-redirects", false, "Do not follow HTTP redirects")
cmd.Flags().StringP("output", "o", "text", "Output format (text or json)")

return cmd
Expand Down
1 change: 1 addition & 0 deletions cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func TestFlagRegistration(t *testing.T) {
{"header", "H", []string{}},
{"config", "f", ""},
{"verbose", "v", false},
{"disable-redirects", "", false},
{"output", "o", "text"},
}

Expand Down
41 changes: 24 additions & 17 deletions internal/httpclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,34 @@ import (
"github.com/infraspecdev/goperf/internal/stats"
)

func NewHTTPClient(concurrency int) *http.Client {
return &http.Client{
func NewHTTPClient(concurrency int, disableRedirects bool) *http.Client {
client := &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: concurrency,
DisableCompression: true,
},
}
if disableRedirects {
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
}
return client
}

type Config struct {
Target string
Requests int
Concurrency int
Timeout time.Duration
Duration time.Duration
Method string
Body string
Headers []string
Verbose bool
Version string
Stderr io.Writer
Target string
Requests int
Concurrency int
Timeout time.Duration
Duration time.Duration
Method string
Body string
Headers []string
Verbose bool
Version string
Stderr io.Writer
DisableRedirects bool
}

type HTTPDoer interface {
Expand Down Expand Up @@ -126,7 +133,7 @@ func formatErrorForStats(err error) string {
return err.Error()
}

func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verboseWriter io.Writer, statusCode int, latency time.Duration, err error) {
func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verboseWriter io.Writer, statusCode int, latency time.Duration, err error, disableRedirects bool) {
if err != nil && ctx.Err() != nil && errors.Is(err, ctx.Err()) {
return
}
Expand All @@ -139,7 +146,7 @@ func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verbos
}
if err != nil {
recorder.RecordErrorResult(statusCode, formatErrorForStats(err))
} else if statusCode >= 200 && statusCode < 300 {
} else if (statusCode >= 200 && statusCode < 300) || (disableRedirects && statusCode >= 300 && statusCode < 400) {
if statusCode > 0 {
recorder.RecordStatusCode(statusCode)
}
Expand All @@ -150,7 +157,7 @@ func recordResult(ctx context.Context, recorder *stats.HistogramRecorder, verbos
}

func Run(ctx context.Context, cfg Config) *stats.HistogramRecorder {
Comment thread
11eshaannegi marked this conversation as resolved.
client := NewHTTPClient(cfg.Concurrency)
client := NewHTTPClient(cfg.Concurrency, cfg.DisableRedirects)
recorder := stats.NewHistogramRecorder(cfg.Timeout)

var verboseWriter io.Writer
Expand Down Expand Up @@ -224,7 +231,7 @@ func Run(ctx context.Context, cfg Config) *stats.HistogramRecorder {
}
}
statusCode, d, err := MakeRequest(reqCtx, client, cfg)
recordResult(reqCtx, recorder, verboseWriter, statusCode, d, err)
recordResult(reqCtx, recorder, verboseWriter, statusCode, d, err, cfg.DisableRedirects)
}
}()
}
Expand Down
Loading