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
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: go

go:
- 1.7.x
- 1.8

install:
Expand All @@ -21,6 +20,7 @@ script:
- structcheck .
- varcheck .
- misspell -error .
- go test -v -cover ./...
# recompile with glide AFTER doing code checks, as errors in dependencies in vendor/ will get caught ¯\_(ツ)_/¯
- go get -v github.com/Masterminds/glide
- cd $GOPATH/src/github.com/Masterminds/glide && git checkout tags/v0.12.3 && go install && cd -
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ Pewpew is under active development. Since Pewpew is pre-1.0, minor version chang
## Installing
Pre-compiled binaries are available on [Releases](https://github.com/bengadbois/pewpew/releases).

If you want to get the latest or build from source: install Go 1.7+, `go get github.com/bengadbois/pewpew`, and install dependencies with [Glide](http://glide.sh/).
If you want to get the latest or build from source: install Go 1.8+, `go get github.com/bengadbois/pewpew`, and install dependencies with [Glide](http://glide.sh/).

## Examples
```
pewpew stress -n 50 http://www.example.com
pewpew stress -n 50 www.example.com
```
Make 50 requests to http://www.example.com

Expand All @@ -46,7 +46,7 @@ For the full list of command line options, run `pewpew help` or `pewpew help str
### Using Regular Expression Targets
Pewpew supports using regular expressions (Perl syntax) to nondeterministically generate targets.
```
pewpew stress -r "http://localhost/pages/[0-9]{1,3}"
pewpew stress -r "localhost/pages/[0-9]{1,3}"
```
This example will generate target URLs such as:
```
Expand All @@ -63,7 +63,7 @@ http://localhost/pages/3
```

```
pewpew stress -r "http://localhost/pages/[0-9]+\?cache=(true|false)(\&referrer=[0-9]{3})?"
pewpew stress -r "localhost/pages/[0-9]+\?cache=(true|false)(\&referrer=[0-9]{3})?"
```
This example will generate target URLs such as:
```
Expand Down
6 changes: 6 additions & 0 deletions lib/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ func printStat(stat RequestStat, w io.Writer) {

//print tons of info about the request, response and response body
func printVerbose(req *http.Request, response *http.Response, w io.Writer) {
if req == nil {
return
}
if response == nil {
return
}
var requestInfo string
//request details
requestInfo = requestInfo + fmt.Sprintf("Request:\n%+v\n\n", &req)
Expand Down
68 changes: 68 additions & 0 deletions lib/printer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package pewpew

import (
"errors"
"io/ioutil"
"net/http"
"testing"
"time"
)

func TestCreateTextSummary(t *testing.T) {
cases := []struct {
s requestStatSummary
}{
{requestStatSummary{}}, //empty
{requestStatSummary{
avgRPS: 12.34,
avgDuration: 1234,
minDuration: 1234,
maxDuration: 1234,
statusCodes: map[int]int{100: 1, 200: 2, 300: 3, 400: 4, 500: 5, 0: 1},
startTime: time.Now(),
endTime: time.Now(),
avgDataTransferred: 2345,
maxDataTransferred: 12345,
minDataTransferred: 1234,
totalDataTransferred: 123456,
}}, //nonzero values for everything
}
for _, c := range cases {
//could check for the exact string, but that's super tedious and brittle
_ = CreateTextSummary(c.s)
}
}

func TestPrintStat(t *testing.T) {
cases := []struct {
r RequestStat
}{
{RequestStat{}}, //empty
//status codes
{RequestStat{StatusCode: 100}},
{RequestStat{StatusCode: 200}},
{RequestStat{StatusCode: 300}},
{RequestStat{StatusCode: 400}},
{RequestStat{StatusCode: 500}},
//error case
{RequestStat{Error: errors.New("this is an error")}},
}
for _, c := range cases {
printStat(c.r, ioutil.Discard)
}
}

func TestPrintVerbose(t *testing.T) {
cases := []struct {
req *http.Request
resp *http.Response
}{
{nil, nil},
{nil, &http.Response{}},
{&http.Request{}, nil},
{&http.Request{}, &http.Response{Body: http.NoBody}},
}
for _, c := range cases {
printVerbose(c.req, c.resp, ioutil.Discard)
}
}
34 changes: 19 additions & 15 deletions lib/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ func CreateRequestsStats(requestStats []RequestStat) requestStatSummary {
}

requestCodes := make(map[int]int)
summary := requestStatSummary{maxDuration: requestStats[0].Duration,
summary := requestStatSummary{
maxDuration: requestStats[0].Duration,
minDuration: requestStats[0].Duration,
minDataTransferred: requestStats[0].DataTransferred,
statusCodes: requestCodes,
Expand All @@ -37,10 +38,14 @@ func CreateRequestsStats(requestStats []RequestStat) requestStatSummary {
var totalDurations time.Duration //total time of all requests (concurrent is counted)
nonErrCount := 0
for i := 0; i < len(requestStats); i++ {
if requestStats[i].Error != nil {
continue
}
nonErrCount++
if requestStats[i].Duration > summary.maxDuration {
summary.maxDuration = requestStats[i].Duration
}
if requestStats[i].Duration < summary.minDuration {
if requestStats[i].Duration < summary.minDuration || summary.minDuration == 0 { //in case was set to 0 due to an error req
summary.minDuration = requestStats[i].Duration
}
if requestStats[i].StartTime.Before(summary.startTime) {
Expand All @@ -49,36 +54,35 @@ func CreateRequestsStats(requestStats []RequestStat) requestStatSummary {
if requestStats[i].EndTime.After(summary.endTime) {
summary.endTime = requestStats[i].EndTime
}
totalDurations += requestStats[i].Duration

if requestStats[i].DataTransferred > summary.maxDataTransferred {
summary.maxDataTransferred = requestStats[i].DataTransferred
}
if requestStats[i].DataTransferred < summary.minDataTransferred {
if requestStats[i].DataTransferred < summary.minDataTransferred || summary.minDataTransferred == 0 { //in case was set to 0 due to an error req
summary.minDataTransferred = requestStats[i].DataTransferred
}
summary.totalDataTransferred += requestStats[i].DataTransferred

totalDurations += requestStats[i].Duration
summary.statusCodes[requestStats[i].StatusCode]++
summary.totalDataTransferred += requestStats[i].DataTransferred
if requestStats[i].Error == nil {
nonErrCount++
}
}
//kinda ugly to calculate average, then convert into nanoseconds
if nonErrCount == 0 {
summary.avgDuration = 0
summary.maxDuration = 0
summary.minDuration = 0
summary.minDataTransferred = 0
summary.maxDataTransferred = 0
summary.totalDataTransferred = 0
return summary
} else {
//kinda ugly to calculate average, then convert into nanoseconds
avgNs := totalDurations.Nanoseconds() / int64(nonErrCount)
newAvg, _ := time.ParseDuration(fmt.Sprintf("%d", avgNs) + "ns")
summary.avgDuration = newAvg
}

if nonErrCount == 0 {
summary.avgDataTransferred = 0
} else {
summary.avgDataTransferred = summary.totalDataTransferred / nonErrCount
}
summary.avgDataTransferred = summary.totalDataTransferred / nonErrCount

summary.avgRPS = float64(len(requestStats)) / float64(summary.endTime.Sub(summary.startTime))
summary.avgRPS = float64(nonErrCount) / float64(summary.endTime.Sub(summary.startTime))
return summary
}
115 changes: 115 additions & 0 deletions lib/stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package pewpew

import (
"errors"
"reflect"
"testing"
"time"
)

func TestCreateRequestsStats(t *testing.T) {
cases := []struct {
requestStats []RequestStat
want requestStatSummary
}{
{requestStats: make([]RequestStat, 0), want: requestStatSummary{}},
//check basic
{requestStats: []RequestStat{
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, StatusCode: 200},
},
want: requestStatSummary{
avgRPS: 0.000000000001,
avgDuration: 1000,
maxDuration: 1000,
minDuration: 1000,
startTime: time.Unix(1000, 0),
endTime: time.Unix(2000, 0),
statusCodes: map[int]int{200: 1},
},
},
//check multiple
{requestStats: []RequestStat{
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, StatusCode: 200},
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, StatusCode: 200},
},
want: requestStatSummary{
avgRPS: 0.000000000002,
avgDuration: 1000,
maxDuration: 1000,
minDuration: 1000,
startTime: time.Unix(1000, 0),
endTime: time.Unix(2000, 0),
statusCodes: map[int]int{200: 2},
},
},
//checking errors
{requestStats: []RequestStat{
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, Error: errors.New("test error 1")},
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, Error: errors.New("test error 1")},
},
want: requestStatSummary{
avgRPS: 0,
avgDuration: 0,
maxDuration: 0,
minDuration: 0,
startTime: time.Unix(1000, 0),
endTime: time.Unix(2000, 0),
statusCodes: map[int]int{},
},
},
//mix of timings, mix of data transferred, mix of status codes
{requestStats: []RequestStat{
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, Error: errors.New("test error 1")},
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, StatusCode: 200, DataTransferred: 100},
{StartTime: time.Unix(2000, 0), EndTime: time.Unix(3000, 0), Duration: 1000, StatusCode: 200, DataTransferred: 200},
{StartTime: time.Unix(3000, 0), EndTime: time.Unix(4000, 0), Duration: 1000, StatusCode: 400, DataTransferred: 300},
{StartTime: time.Unix(4000, 0), EndTime: time.Unix(6000, 0), Duration: 2000, StatusCode: 400, DataTransferred: 400},
{StartTime: time.Unix(5000, 0), EndTime: time.Unix(7000, 0), Duration: 2000, StatusCode: 400, DataTransferred: 500},
{StartTime: time.Unix(6000, 0), EndTime: time.Unix(7000, 0), Duration: 2000, StatusCode: 400, DataTransferred: 600},
},
want: requestStatSummary{
avgRPS: 0.000000000001,
avgDuration: 1500,
maxDuration: 2000,
minDuration: 1000,
startTime: time.Unix(1000, 0),
endTime: time.Unix(7000, 0),
statusCodes: map[int]int{200: 2, 400: 4},
avgDataTransferred: 350,
maxDataTransferred: 600,
minDataTransferred: 100,
totalDataTransferred: 2100,
},
},
//like test case from above, but differently ordered
{requestStats: []RequestStat{
{StartTime: time.Unix(6000, 0), EndTime: time.Unix(7000, 0), Duration: 2000, StatusCode: 400, DataTransferred: 600},
{StartTime: time.Unix(5000, 0), EndTime: time.Unix(7000, 0), Duration: 2000, StatusCode: 400, DataTransferred: 500},
{StartTime: time.Unix(4000, 0), EndTime: time.Unix(6000, 0), Duration: 2000, StatusCode: 400, DataTransferred: 400},
{StartTime: time.Unix(3000, 0), EndTime: time.Unix(4000, 0), Duration: 1000, StatusCode: 400, DataTransferred: 300},
{StartTime: time.Unix(2000, 0), EndTime: time.Unix(3000, 0), Duration: 1000, StatusCode: 200, DataTransferred: 200},
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, StatusCode: 200, DataTransferred: 100},
{StartTime: time.Unix(1000, 0), EndTime: time.Unix(2000, 0), Duration: 1000, Error: errors.New("test error 1")},
},
want: requestStatSummary{
avgRPS: 0.000000000001,
avgDuration: 1500,
maxDuration: 2000,
minDuration: 1000,
startTime: time.Unix(1000, 0),
endTime: time.Unix(7000, 0),
statusCodes: map[int]int{200: 2, 400: 4},
avgDataTransferred: 350,
maxDataTransferred: 600,
minDataTransferred: 100,
totalDataTransferred: 2100,
},
},
}
for _, c := range cases {
summary := CreateRequestsStats(c.requestStats)
if !reflect.DeepEqual(summary, c.want) {
t.Errorf("CreateRequestsStats(%+v) == %+v wanted %+v", c.requestStats, summary, c.want)
}
}
}
28 changes: 22 additions & 6 deletions lib/stress.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ func validateTargets(s StressConfig) error {
if target.Concurrency <= 0 {
return errors.New("concurrency must be greater than zero")
}
if target.Method == "" {
return errors.New("method cannot be empty string")
}
if target.Timeout != "" {
//TODO should save this parsed duration so don't have to inefficiently reparse later
timeout, err := time.ParseDuration(target.Timeout)
Expand All @@ -272,6 +275,17 @@ func validateTargets(s StressConfig) error {

//build the http request out of the target's config
func buildRequest(t Target) (http.Request, error) {
if t.URL == "" {
return http.Request{}, errors.New("empty URL")
}
if len(t.URL) < 8 {
return http.Request{}, errors.New("URL too short")
}
//prepend "http://" if scheme not provided
//maybe a cleaner way to do this via net.url?
if t.URL[:7] != "http://" && t.URL[:8] != "https://" {
t.URL = "http://" + t.URL
}
var urlStr string
var err error
//when regex set, generate urls
Expand All @@ -287,9 +301,8 @@ func buildRequest(t Target) (http.Request, error) {
if err != nil {
return http.Request{}, errors.New("failed to parse URL " + urlStr + " : " + err.Error())
}
//default to http if not specified
if URL.Scheme == "" {
URL.Scheme = "http"
if URL.Host == "" {
return http.Request{}, errors.New("empty hostname")
}

//setup the request
Expand Down Expand Up @@ -347,21 +360,24 @@ func buildRequest(t Target) (http.Request, error) {

//splits on delim into parts and trims whitespace
//delim1 splits the pairs, delim2 splits amongst the pairs
//like parseKeyValString("key1: val2, key3 : val4,key5:key6 ", ",", ":") becomes
//like parseKeyValString("key1: val2, key3 : val4,key5:val6 ", ",", ":") becomes
//["key1"]->"val2"
//["key3"]->"val4"
//["key5"]->"val6"
func parseKeyValString(keyValStr, delim1, delim2 string) (map[string]string, error) {
m := make(map[string]string)
if delim1 == delim2 {
return m, errors.New("delimiters can't be equal")
}
pairs := strings.SplitN(keyValStr, delim1, -1)
for _, pair := range pairs {
parts := strings.SplitN(pair, delim2, 2)
if len(parts) != 2 {
return m, fmt.Errorf("failed to parse into two parts")
return m, errors.New("failed to parse into two parts")
}
key, val := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
if key == "" || val == "" {
return m, fmt.Errorf("key or value is empty")
return m, errors.New("key or value is empty")
}
m[key] = val
}
Expand Down
Loading