Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"net/url"
"os"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -30,6 +31,7 @@ type RunConfig struct {
Duration time.Duration
Method string
Body string
BodyFile string
Headers []string
Verbose bool
}
Expand Down Expand Up @@ -58,6 +60,17 @@ func (c *RunConfig) Validate() error {
}
c.ParsedTarget = u

if c.BodyFile != "" {
if c.Body != "" {
return fmt.Errorf("cannot use both -b (body string) and -D (body file)")
}
data, err := os.ReadFile(c.BodyFile)
if err != nil {
return fmt.Errorf("failed to read body file %q: %w", c.BodyFile, err)
}
c.Body = string(data)
}

if c.Concurrency <= 0 {
return fmt.Errorf("concurrency must be positive, got %d", c.Concurrency)
}
Expand Down
52 changes: 51 additions & 1 deletion cmd/config_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
package cmd

import (
"os"
"strings"
"testing"
"time"

"github.com/infraspecdev/goperf/internal/httpclient"
)

func TestRunConfig_Validate(t *testing.T) {
tmpFile, err := os.CreateTemp("", "body*.txt")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer func() {
_ = os.Remove(tmpFile.Name())
}()
if _, err := tmpFile.Write([]byte("hello file body")); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
if err := tmpFile.Close(); err != nil {
t.Fatalf("failed to close temp file: %v", err)
}

validConfig := func() RunConfig {
return RunConfig{
Target: "https://example.com/api",
Expand All @@ -25,6 +41,7 @@ func TestRunConfig_Validate(t *testing.T) {
mutate func(*RunConfig)
wantErr bool
errMsg string
check func(*testing.T, *RunConfig)
}{
{
name: "Valid configuration",
Expand Down Expand Up @@ -225,6 +242,35 @@ func TestRunConfig_Validate(t *testing.T) {
wantErr: true,
errMsg: `invalid header format ": some-value", expected 'Key: Value' without spaces in the key`,
},
{
name: "Both body and body file set",
mutate: func(c *RunConfig) {
c.Body = "some body text"
c.BodyFile = "some_file.txt"
},
wantErr: true,
errMsg: "cannot use both -b (body string) and -D (body file)",
},
{
name: "Nonexistent body file",
mutate: func(c *RunConfig) {
c.BodyFile = "does_not_exist_12345.txt"
},
wantErr: true,
errMsg: "failed to read body file \"does_not_exist_12345.txt\"",
},
{
name: "Valid body file",
mutate: func(c *RunConfig) {
c.BodyFile = tmpFile.Name()
},
wantErr: false,
check: func(t *testing.T, c *RunConfig) {
if c.Body != "hello file body" {
t.Errorf("expected Body to be %q, got %q", "hello file body", c.Body)
}
},
},
}

for _, tt := range tests {
Expand All @@ -236,14 +282,18 @@ func TestRunConfig_Validate(t *testing.T) {
if tt.wantErr {
if err == nil {
t.Errorf("expected error but got nil")
} else if tt.errMsg != "" && err.Error() != tt.errMsg {
} else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("expected error containing %q, got %q", tt.errMsg, err.Error())
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}

if tt.check != nil {
tt.check(t, &cfg)
}
})
}
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/configfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type fileConfig struct {
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"`
Comment thread
11eshaannegi marked this conversation as resolved.
Headers []string `json:"headers" yaml:"headers"`
Verbose *bool `json:"verbose" yaml:"verbose"`
}
Expand Down Expand Up @@ -116,6 +117,10 @@ func mergeConfig(file *fileConfig, cli RunConfig, changed map[string]bool) (RunC
merged.Body = *file.Body
}

if file.BodyFile != nil && !changed["body-file"] {
merged.BodyFile = *file.BodyFile
}

if len(file.Headers) > 0 && !changed["header"] {
merged.Headers = make([]string, len(file.Headers))
copy(merged.Headers, file.Headers)
Expand Down
3 changes: 3 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Latency Percentiles:
duration, _ := f.GetDuration("duration")
method, _ := f.GetString("method")
body, _ := f.GetString("body")
bodyFile, _ := f.GetString("body-file")
headers, _ := f.GetStringArray("header")
verbose, _ := f.GetBool("verbose")
outputFormat, _ := f.GetString("output")
Expand All @@ -75,6 +76,7 @@ Latency Percentiles:
Duration: duration,
Method: strings.ToUpper(method),
Body: body,
BodyFile: bodyFile,
Headers: headers,
Verbose: verbose,
}
Expand Down Expand Up @@ -135,6 +137,7 @@ Latency Percentiles:
cmd.Flags().DurationP("duration", "d", 0, "Duration to run the test. Overrides -n when set (e.g., 10s, 1m)")
cmd.Flags().StringP("method", "m", "GET", "HTTP method to use")
cmd.Flags().StringP("body", "b", "", "Request body content")
cmd.Flags().StringP("body-file", "D", "", "Path to file containing the request body")
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")
Expand Down