Merge pull request #17 from tsenart/split-stages
Split test pipeline stages
This commit is contained in:
87
README.md
87
README.md
@@ -21,23 +21,42 @@ $ go get github.com/tsenart/vegeta
|
|||||||
$ go install github.com/tsenart/vegeta
|
$ go install github.com/tsenart/vegeta
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage (CLI)
|
## Usage examples
|
||||||
|
```shell
|
||||||
|
$ echo "GET http://localhost/" | vegeta attack -rate=100 -duration=5s | vegeta report
|
||||||
|
$ vegeta attack -targets=targets.txt > results.vr
|
||||||
|
$ vegeta report -input=results.vr -reporter=json > metrics.json
|
||||||
|
$ vegeta report -input=results.vr -reporter=plot:timings > plot.svg
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage manual
|
||||||
```shell
|
```shell
|
||||||
$ vegeta -h
|
$ vegeta -h
|
||||||
Usage of vegeta:
|
Usage: vegeta [globals] <command> [options]
|
||||||
-cpus=n: Number of CPUs to use
|
|
||||||
-duration=10s: Duration of the test
|
Commands:
|
||||||
-ordering="random": Attack ordering [sequential, random]
|
attack Hit the targets
|
||||||
-output="stdout": Reporter output file
|
report Report the results
|
||||||
-rate=50: Requests per second
|
|
||||||
-reporter="text": Reporter to use [text, json, plot:timings]
|
Globals:
|
||||||
-targets="targets.txt": Targets file
|
-cpus=8 Number of CPUs to use
|
||||||
```
|
```
|
||||||
|
|
||||||
#### -cpus
|
#### -cpus
|
||||||
Specifies the number of CPUs to be used internally.
|
Specifies the number of CPUs to be used internally.
|
||||||
It defaults to the amount of CPUs available in the system.
|
It defaults to the amount of CPUs available in the system.
|
||||||
|
|
||||||
|
### attack
|
||||||
|
```shell
|
||||||
|
$ vegeta attack -h
|
||||||
|
Usage of attack:
|
||||||
|
-duration=10s: Duration of the test
|
||||||
|
-ordering="random": Attack ordering [sequential, random]
|
||||||
|
-output="stdout": Output file
|
||||||
|
-rate=50: Requests per second
|
||||||
|
-targets="stdin": Targets file
|
||||||
|
```
|
||||||
|
|
||||||
#### -duration
|
#### -duration
|
||||||
Specifies the amount of time to issue request to the targets.
|
Specifies the amount of time to issue request to the targets.
|
||||||
The internal concurrency structure's setup has this value as a variable.
|
The internal concurrency structure's setup has this value as a variable.
|
||||||
@@ -52,18 +71,44 @@ The other option is `sequential` and it does what you would expect it to
|
|||||||
do.
|
do.
|
||||||
|
|
||||||
#### -output
|
#### -output
|
||||||
Specifies the output file to which the report will be written to.
|
Specifies the output file to which the binary results will be written
|
||||||
The default is stdout.
|
to. Made to be piped to the report command input. Defaults to stdout.
|
||||||
|
|
||||||
#### -rate
|
#### -rate
|
||||||
Specifies the requests per second rate to issue against
|
Specifies the requests per second rate to issue against
|
||||||
the targets. The actual request rate can vary slightly due to things like
|
the targets. The actual request rate can vary slightly due to things like
|
||||||
garbage collection, but overall it should stay very close to the specified.
|
garbage collection, but overall it should stay very close to the specified.
|
||||||
|
|
||||||
|
#### -targets
|
||||||
|
Specifies the attack targets in a line sepated file, defaulting to stdin.
|
||||||
|
The format should be as follows.
|
||||||
|
```
|
||||||
|
GET http://goku:9090/path/to/dragon?item=balls
|
||||||
|
GET http://user:password@goku:9090/path/to
|
||||||
|
HEAD http://goku:9090/path/to/success
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### report
|
||||||
|
```
|
||||||
|
$ vegeta report -h
|
||||||
|
Usage of report:
|
||||||
|
-input="stdin": Input file
|
||||||
|
-output="stdout": Output file
|
||||||
|
-reporter="text": Reporter [text, json, plot:timings]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### -input
|
||||||
|
Specifies the input file from which the attack command binary results
|
||||||
|
are saved. Defaults to stdin.
|
||||||
|
|
||||||
|
#### -output
|
||||||
|
Specifies the output file to which the report will be written to.
|
||||||
|
|
||||||
#### -reporter
|
#### -reporter
|
||||||
Specifies the reporting type to display the results with.
|
Specifies the kind of report to be generated. It defaults to text.
|
||||||
The default is the text report printed to stdout.
|
|
||||||
##### -reporter=text
|
##### text
|
||||||
```
|
```
|
||||||
Time(avg) Requests Success Bytes(rx/tx)
|
Time(avg) Requests Success Bytes(rx/tx)
|
||||||
152.341ms 200 17.00% 251.00/0.00
|
152.341ms 200 17.00% 251.00/0.00
|
||||||
@@ -75,7 +120,7 @@ Error Set:
|
|||||||
Server Timeout
|
Server Timeout
|
||||||
Page Not Found
|
Page Not Found
|
||||||
```
|
```
|
||||||
##### -reporter=json
|
##### json
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"total_requests": 50,
|
"total_requests": 50,
|
||||||
@@ -93,19 +138,9 @@ Page Not Found
|
|||||||
"errors": []
|
"errors": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
##### -reporter=plot:timings
|
##### plot:timings
|
||||||
Plots the request timings in SVG format.
|
|
||||||

|

|
||||||
|
|
||||||
#### -targets
|
|
||||||
Specifies the attack targets in a line sepated file. The format should
|
|
||||||
be as follows:
|
|
||||||
```
|
|
||||||
GET http://goku:9090/path/to/dragon?item=balls
|
|
||||||
GET http://user:password@goku:9090/path/to
|
|
||||||
HEAD http://goku:9090/path/to/success
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage (Library)
|
## Usage (Library)
|
||||||
```go
|
```go
|
||||||
|
|||||||
78
attack.go
Normal file
78
attack.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
vegeta "github.com/tsenart/vegeta/lib"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func attackCmd(args []string) command {
|
||||||
|
fs := flag.NewFlagSet("attack", flag.ExitOnError)
|
||||||
|
rate := fs.Uint64("rate", 50, "Requests per second")
|
||||||
|
targetsf := fs.String("targets", "stdin", "Targets file")
|
||||||
|
ordering := fs.String("ordering", "random", "Attack ordering [sequential, random]")
|
||||||
|
duration := fs.Duration("duration", 10*time.Second, "Duration of the test")
|
||||||
|
output := fs.String("output", "stdout", "Output file")
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
return func() error {
|
||||||
|
return attack(*rate, *duration, *targetsf, *ordering, *output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attack validates the attack arguments, sets up the
|
||||||
|
// required resources, launches the attack and writes the results
|
||||||
|
func attack(rate uint64, duration time.Duration, targetsf, ordering, output string) error {
|
||||||
|
if rate == 0 {
|
||||||
|
return fmt.Errorf(errRatePrefix + "can't be zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
if duration == 0 {
|
||||||
|
return fmt.Errorf(errDurationPrefix + "can't be zero")
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := file(targetsf, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errTargetsFilePrefix+"(%s): %s", targetsf, err)
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
targets, err := vegeta.NewTargetsFrom(in)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errTargetsFilePrefix+"(%s): %s", targetsf, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ordering {
|
||||||
|
case "random":
|
||||||
|
targets.Shuffle(time.Now().UnixNano())
|
||||||
|
case "sequential":
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
return fmt.Errorf(errOrderingPrefix+"`%s` is invalid", ordering)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := file(output, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errOutputFilePrefix+"(%s): %s", output, err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
log.Printf("Vegeta is attacking %d targets in %s order for %s...\n", len(targets), ordering, duration)
|
||||||
|
results := vegeta.Attack(targets, rate, duration)
|
||||||
|
log.Println("Done!")
|
||||||
|
log.Printf("Writing results to '%s'...", output)
|
||||||
|
if err := results.WriteTo(out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
errRatePrefix = "Rate: "
|
||||||
|
errDurationPrefix = "Duration: "
|
||||||
|
errOutputFilePrefix = "Output file: "
|
||||||
|
errTargetsFilePrefix = "Targets file: "
|
||||||
|
errOrderingPrefix = "Ordering: "
|
||||||
|
errReportingPrefix = "Reporting: "
|
||||||
|
)
|
||||||
74
attack_test.go
Normal file
74
attack_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Discard default log output
|
||||||
|
log.SetOutput(ioutil.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRateValidation(t *testing.T) {
|
||||||
|
rate, duration, targetsf, ordering, output := defaultArguments()
|
||||||
|
rate = 0
|
||||||
|
|
||||||
|
err := attack(rate, duration, targetsf, ordering, output)
|
||||||
|
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errRatePrefix)) {
|
||||||
|
t.Errorf("Rate 0 shouldn't be valid: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurationValidation(t *testing.T) {
|
||||||
|
rate, duration, targetsf, ordering, output := defaultArguments()
|
||||||
|
duration = 0
|
||||||
|
|
||||||
|
err := attack(rate, duration, targetsf, ordering, output)
|
||||||
|
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errDurationPrefix)) {
|
||||||
|
t.Errorf("Duration 0 shouldn't be valid: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetsValidation(t *testing.T) {
|
||||||
|
rate, duration, goodFile, ordering, output := defaultArguments()
|
||||||
|
|
||||||
|
// Good case
|
||||||
|
err := attack(rate, duration, goodFile, ordering, output)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Targets file `%s` should be valid: %s", goodFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad case
|
||||||
|
badFile := "randomInexistingFile12345.txt"
|
||||||
|
err = attack(rate, duration, badFile, ordering, output)
|
||||||
|
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errTargetsFilePrefix)) {
|
||||||
|
t.Errorf("Targets file `%s` shouldn't be valid: %s", badFile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOrderingValidation(t *testing.T) {
|
||||||
|
rate, duration, targetsf, _, output := defaultArguments()
|
||||||
|
|
||||||
|
// Good cases
|
||||||
|
for _, ordering := range []string{"random", "sequential"} {
|
||||||
|
err := attack(rate, duration, targetsf, ordering, output)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Ordering `%s` should be valid: %s", ordering, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad case
|
||||||
|
badOrdering := "lolcat"
|
||||||
|
err := attack(rate, duration, targetsf, badOrdering, output)
|
||||||
|
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errOrderingPrefix)) {
|
||||||
|
t.Errorf("Ordering `%s` shouldn't be valid: %s", badOrdering, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultArguments() (uint64, time.Duration, string, string, string) {
|
||||||
|
return uint64(1000), 5 * time.Millisecond, ".targets.txt", "random", "/dev/null"
|
||||||
|
}
|
||||||
26
file.go
Normal file
26
file.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func file(filename string, create bool) (*os.File, error) {
|
||||||
|
switch filename {
|
||||||
|
case "stdin":
|
||||||
|
return os.Stdin, nil
|
||||||
|
case "stdout":
|
||||||
|
return os.Stdout, nil
|
||||||
|
default:
|
||||||
|
var file *os.File
|
||||||
|
var err error
|
||||||
|
if create {
|
||||||
|
file, err = os.Create(filename)
|
||||||
|
} else {
|
||||||
|
file, err = os.Open(filename)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,19 @@
|
|||||||
package vegeta
|
package vegeta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Result represents the metrics defined out of an http.Response
|
|
||||||
// generated by each target hit
|
|
||||||
type Result struct {
|
|
||||||
Code uint16
|
|
||||||
Timestamp time.Time
|
|
||||||
Timing time.Duration
|
|
||||||
BytesOut uint64
|
|
||||||
BytesIn uint64
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attack hits the passed Targets (http.Requests) at the rate specified for
|
// Attack hits the passed Targets (http.Requests) at the rate specified for
|
||||||
// duration time and then waits for all the requests to come back.
|
// duration time and then waits for all the requests to come back.
|
||||||
// The results of the attack are put into a slice which is returned.
|
// The results of the attack are put into a slice which is returned.
|
||||||
func Attack(targets Targets, rate uint64, duration time.Duration) []Result {
|
func Attack(targets Targets, rate uint64, duration time.Duration) Results {
|
||||||
total := rate * uint64(duration.Seconds())
|
total := rate * uint64(duration.Seconds())
|
||||||
hits := make(chan *http.Request, total)
|
hits := make(chan *http.Request, total)
|
||||||
res := make(chan Result, total)
|
res := make(chan Result, total)
|
||||||
results := make(results, total)
|
results := make(Results, total)
|
||||||
// Scatter
|
// Scatter
|
||||||
go drill(rate, hits, res)
|
go drill(rate, hits, res)
|
||||||
for i := 0; i < cap(hits); i++ {
|
for i := 0; i < cap(hits); i++ {
|
||||||
@@ -39,18 +26,9 @@ func Attack(targets Targets, rate uint64, duration time.Duration) []Result {
|
|||||||
}
|
}
|
||||||
close(res)
|
close(res)
|
||||||
|
|
||||||
sort.Sort(results)
|
return results.Sort()
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// results is a slice of Result defined only to be sortable with sort.Interface
|
|
||||||
type results []Result
|
|
||||||
|
|
||||||
func (r results) Len() int { return len(r) }
|
|
||||||
func (r results) Less(i, j int) bool { return r[i].Timestamp.Before(r[j].Timestamp) }
|
|
||||||
func (r results) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
|
||||||
|
|
||||||
// drill loops over the passed reqs channel and executes each request.
|
// drill loops over the passed reqs channel and executes each request.
|
||||||
// It is throttled to the rate specified.
|
// It is throttled to the rate specified.
|
||||||
func drill(rate uint64, reqs chan *http.Request, res chan Result) {
|
func drill(rate uint64, reqs chan *http.Request, res chan Result) {
|
||||||
@@ -71,13 +49,14 @@ func hit(req *http.Request, res chan Result) {
|
|||||||
Timestamp: began,
|
Timestamp: began,
|
||||||
Timing: time.Since(began),
|
Timing: time.Since(began),
|
||||||
BytesOut: uint64(req.ContentLength),
|
BytesOut: uint64(req.ContentLength),
|
||||||
Error: err,
|
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
} else {
|
||||||
result.Code = uint16(r.StatusCode)
|
result.Code = uint16(r.StatusCode)
|
||||||
if body, err := ioutil.ReadAll(r.Body); err != nil {
|
if body, err := ioutil.ReadAll(r.Body); err != nil {
|
||||||
if result.Code < 200 || result.Code >= 300 {
|
if result.Code < 200 || result.Code >= 300 {
|
||||||
result.Error = errors.New(string(body))
|
result.Error = string(body)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result.BytesIn = uint64(len(body))
|
result.BytesIn = uint64(len(body))
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ func NewMetrics(results []Result) *Metrics {
|
|||||||
if result.Code >= 200 && result.Code < 300 {
|
if result.Code >= 200 && result.Code < 300 {
|
||||||
m.TotalSuccess++
|
m.TotalSuccess++
|
||||||
}
|
}
|
||||||
if result.Error != nil {
|
if result.Error != "" {
|
||||||
errorSet[result.Error.Error()] = struct{}{}
|
errorSet[result.Error] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package vegeta
|
package vegeta
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewMetrics(t *testing.T) {
|
func TestNewMetrics(t *testing.T) {
|
||||||
m := NewMetrics([]Result{
|
m := NewMetrics([]Result{
|
||||||
Result{500, time.Now(), 100 * time.Millisecond, 10, 30, errors.New("Internal server error")},
|
Result{500, time.Now(), 100 * time.Millisecond, 10, 30, "Internal server error"},
|
||||||
Result{200, time.Now(), 20 * time.Millisecond, 20, 20, nil},
|
Result{200, time.Now(), 20 * time.Millisecond, 20, 20, ""},
|
||||||
Result{200, time.Now(), 30 * time.Millisecond, 30, 10, nil},
|
Result{200, time.Now(), 30 * time.Millisecond, 30, 10, ""},
|
||||||
})
|
})
|
||||||
|
|
||||||
for field, values := range map[string][]float64{
|
for field, values := range map[string][]float64{
|
||||||
|
|||||||
46
lib/results.go
Normal file
46
lib/results.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package vegeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result represents the metrics defined out of an http.Response
|
||||||
|
// generated by each target hit
|
||||||
|
type Result struct {
|
||||||
|
Code uint16
|
||||||
|
Timestamp time.Time
|
||||||
|
Timing time.Duration
|
||||||
|
BytesOut uint64
|
||||||
|
BytesIn uint64
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Results is a slice of Result structs with encoding,
|
||||||
|
// decoding and sorting behavior attached
|
||||||
|
type Results []Result
|
||||||
|
|
||||||
|
// WriteTo encodes the results and writes it to an io.Writer
|
||||||
|
// returning an error in case of failure
|
||||||
|
func (r Results) WriteTo(out io.Writer) error {
|
||||||
|
return gob.NewEncoder(out).Encode(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom reads data from an io.Reader and decodes it into a Results struct
|
||||||
|
// returning an error in case of failure
|
||||||
|
func (r *Results) ReadFrom(in io.Reader) error {
|
||||||
|
return gob.NewDecoder(in).Decode(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort sorts Results by Timestamp in ascending order and returns
|
||||||
|
// the sorted slice
|
||||||
|
func (r Results) Sort() Results {
|
||||||
|
sort.Sort(r)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Results) Len() int { return len(r) }
|
||||||
|
func (r Results) Less(i, j int) bool { return r[i].Timestamp.Before(r[j].Timestamp) }
|
||||||
|
func (r Results) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
||||||
50
lib/results_test.go
Normal file
50
lib/results_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package vegeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncoding(t *testing.T) {
|
||||||
|
results := Results{
|
||||||
|
Result{200, time.Now(), 100 * time.Millisecond, 10, 30, ""},
|
||||||
|
Result{200, time.Now(), 20 * time.Millisecond, 20, 20, ""},
|
||||||
|
Result{200, time.Now(), 30 * time.Millisecond, 30, 10, ""},
|
||||||
|
}
|
||||||
|
buffer := &bytes.Buffer{}
|
||||||
|
|
||||||
|
if err := results.WriteTo(buffer); err != nil {
|
||||||
|
t.Fatalf("Failed WriteTo: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded := Results{}
|
||||||
|
if err := decoded.ReadFrom(buffer); err != nil {
|
||||||
|
t.Fatalf("Failed ReadFrom: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded) != len(results) {
|
||||||
|
t.Fatalf("Length mismatch. Want: %d, Got: %d", len(results), len(decoded))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, result := range results {
|
||||||
|
if decoded[i].Timestamp != result.Timestamp {
|
||||||
|
t.Fatalf("Expected result with timestamp: %s, got: %s", result.Timestamp, decoded[i].Timestamp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSort(t *testing.T) {
|
||||||
|
results := Results{
|
||||||
|
Result{Timestamp: time.Date(2013, 9, 10, 20, 4, 0, 3, time.UTC)},
|
||||||
|
Result{Timestamp: time.Date(2013, 9, 10, 20, 4, 0, 2, time.UTC)},
|
||||||
|
Result{Timestamp: time.Date(2013, 9, 10, 20, 4, 0, 1, time.UTC)},
|
||||||
|
}
|
||||||
|
|
||||||
|
results.Sort()
|
||||||
|
|
||||||
|
if !sort.IsSorted(results) {
|
||||||
|
t.Fatalf("Sort failed: %v", results)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,25 +6,14 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Targets represents the http.Requests which will be issued during the test
|
// Targets represents the http.Requests which will be issued during the test
|
||||||
type Targets []*http.Request
|
type Targets []*http.Request
|
||||||
|
|
||||||
// NewTargetsFromFile reads and parses targets from a text file
|
// NewTargetsFrom reads targets out of a line separated source skipping empty lines
|
||||||
func NewTargetsFromFile(filename string) (Targets, error) {
|
func NewTargetsFrom(source io.Reader) (Targets, error) {
|
||||||
file, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return Targets{}, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
return readTargets(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// readTargets reads targets out of a line separated source skipping empty lines
|
|
||||||
func readTargets(source io.Reader) (Targets, error) {
|
|
||||||
scanner := bufio.NewScanner(source)
|
scanner := bufio.NewScanner(source)
|
||||||
lines := make([]string, 0)
|
lines := make([]string, 0)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadTargets(t *testing.T) {
|
func TestNewTargetsFrom(t *testing.T) {
|
||||||
lines := bytes.NewBufferString("GET http://lolcathost:9999/\n\n // HEAD http://lolcathost.com this is a comment \nHEAD http://lolcathost:9999/\n")
|
lines := bytes.NewBufferString("GET http://lolcathost:9999/\n\n // HEAD http://lolcathost.com this is a comment \nHEAD http://lolcathost:9999/\n")
|
||||||
targets, err := readTargets(lines)
|
targets, err := NewTargetsFrom(lines)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Couldn't parse valid source: %s", err)
|
t.Fatalf("Couldn't parse valid source: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
122
main.go
122
main.go
@@ -3,104 +3,48 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
vegeta "github.com/tsenart/vegeta/lib"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
// command is a closure function which each command constructor
|
||||||
var (
|
// builds and returns
|
||||||
rate = flag.Uint64("rate", 50, "Requests per second")
|
type command func() error
|
||||||
targetsf = flag.String("targets", "targets.txt", "Targets file")
|
|
||||||
ordering = flag.String("ordering", "random", "Attack ordering [sequential, random]")
|
|
||||||
duration = flag.Duration("duration", 10*time.Second, "Duration of the test")
|
|
||||||
reporter = flag.String("reporter", "text", "Reporter to use [text, json, plot:timings]")
|
|
||||||
output = flag.String("output", "stdout", "Reporter output file")
|
|
||||||
cpus = flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use")
|
|
||||||
)
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if flag.NFlag() == 0 {
|
var usage = fmt.Sprintf(
|
||||||
flag.Usage()
|
`Usage: vegeta [globals] <command> [options]
|
||||||
return
|
|
||||||
|
Commands:
|
||||||
|
attack Hit the targets
|
||||||
|
report Report the results
|
||||||
|
|
||||||
|
Globals:
|
||||||
|
-cpus=%d Number of CPUs to use
|
||||||
|
`, runtime.NumCPU())
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
flag.Usage = func() { fmt.Print(usage) }
|
||||||
|
cpus := flag.Int("cpus", runtime.NumCPU(), "Number of CPUs to use")
|
||||||
|
flag.Parse()
|
||||||
|
runtime.GOMAXPROCS(*cpus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
commands := map[string]func([]string) command{
|
||||||
|
"attack": attackCmd,
|
||||||
|
"report": reportCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
runtime.GOMAXPROCS(*cpus)
|
args := flag.Args()
|
||||||
|
if len(args) == 0 {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
if err := run(*rate, *duration, *targetsf, *ordering, *reporter, *output); err != nil {
|
if cmd, ok := commands[args[0]]; !ok {
|
||||||
|
log.Fatalf("Unknown command: %s", args[0])
|
||||||
|
} else if err := cmd(args[1:])(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
errRatePrefix = "Rate: "
|
|
||||||
errDurationPrefix = "Duration: "
|
|
||||||
errOutputFilePrefix = "Output file: "
|
|
||||||
errTargetsFilePrefix = "Targets file: "
|
|
||||||
errOrderingPrefix = "Ordering: "
|
|
||||||
errReportingPrefix = "Reporting: "
|
|
||||||
)
|
|
||||||
|
|
||||||
// run is an utility function that validates the attack arguments, sets up the
|
|
||||||
// required resources, launches the attack and reports the results
|
|
||||||
func run(rate uint64, duration time.Duration, targetsf, ordering, reporter, output string) error {
|
|
||||||
if rate == 0 {
|
|
||||||
return fmt.Errorf(errRatePrefix + "can't be zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
if duration == 0 {
|
|
||||||
return fmt.Errorf(errDurationPrefix + "can't be zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
var out io.Writer
|
|
||||||
switch output {
|
|
||||||
case "stdout":
|
|
||||||
out = os.Stdout
|
|
||||||
default:
|
|
||||||
file, err := os.Create(output)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(errOutputFilePrefix+"(%s): %s", output, err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
out = file
|
|
||||||
}
|
|
||||||
|
|
||||||
var rep vegeta.Reporter
|
|
||||||
switch reporter {
|
|
||||||
case "text":
|
|
||||||
rep = vegeta.ReportText
|
|
||||||
case "json":
|
|
||||||
rep = vegeta.ReportJSON
|
|
||||||
case "plot:timings":
|
|
||||||
rep = vegeta.ReportTimingsPlot
|
|
||||||
default:
|
|
||||||
log.Println("Reporter provided is not supported. Using text")
|
|
||||||
rep = vegeta.ReportText
|
|
||||||
}
|
|
||||||
|
|
||||||
targets, err := vegeta.NewTargetsFromFile(targetsf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(errTargetsFilePrefix+"(%s): %s", targetsf, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ordering {
|
|
||||||
case "random":
|
|
||||||
targets.Shuffle(time.Now().UnixNano())
|
|
||||||
case "sequential":
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return fmt.Errorf(errOrderingPrefix+"`%s` is invalid", ordering)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Vegeta is attacking %d targets in %s order for %s...\n", len(targets), ordering, duration)
|
|
||||||
results := vegeta.Attack(targets, rate, duration)
|
|
||||||
log.Println("Done!")
|
|
||||||
log.Printf("Writing report to '%s'...", output)
|
|
||||||
if err = rep(results, out); err != nil {
|
|
||||||
return fmt.Errorf(errReportingPrefix+"%s", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
104
main_test.go
104
main_test.go
@@ -1,104 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Discard default log output
|
|
||||||
log.SetOutput(ioutil.Discard)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRateValidation(t *testing.T) {
|
|
||||||
rate, duration, targetsf, ordering, reporter, output := defaultArguments()
|
|
||||||
rate = 0
|
|
||||||
|
|
||||||
err := run(rate, duration, targetsf, ordering, reporter, output)
|
|
||||||
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errRatePrefix)) {
|
|
||||||
t.Errorf("Rate 0 shouldn't be valid: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDurationValidation(t *testing.T) {
|
|
||||||
rate, duration, targetsf, ordering, reporter, output := defaultArguments()
|
|
||||||
duration = 0
|
|
||||||
|
|
||||||
err := run(rate, duration, targetsf, ordering, reporter, output)
|
|
||||||
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errDurationPrefix)) {
|
|
||||||
t.Errorf("Duration 0 shouldn't be valid: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOutputValidation(t *testing.T) {
|
|
||||||
rate, duration, targetsf, ordering, reporter, _ := defaultArguments()
|
|
||||||
|
|
||||||
// Good cases
|
|
||||||
for _, output := range []string{"stdout", "/dev/null"} {
|
|
||||||
err := run(rate, duration, targetsf, ordering, reporter, output)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Output file `%s` should be valid: %s", output, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bad case
|
|
||||||
badOutput := ""
|
|
||||||
err := run(rate, duration, targetsf, ordering, reporter, badOutput)
|
|
||||||
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errOutputFilePrefix)) {
|
|
||||||
t.Errorf("Output file `%s` shouldn't be valid: %s", badOutput, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReporter(t *testing.T) {
|
|
||||||
rate, duration, targetsf, ordering, _, output := defaultArguments()
|
|
||||||
|
|
||||||
for _, reporter := range []string{"text", "plot:timings"} {
|
|
||||||
err := run(rate, duration, targetsf, ordering, reporter, output)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Reporter `%s` shouldn't return an error: %s", reporter, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTargetsValidation(t *testing.T) {
|
|
||||||
rate, duration, goodFile, ordering, reporter, output := defaultArguments()
|
|
||||||
|
|
||||||
// Good case
|
|
||||||
err := run(rate, duration, goodFile, ordering, reporter, output)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Targets file `%s` should be valid: %s", goodFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bad case
|
|
||||||
badFile := "randomInexistingFile12345.txt"
|
|
||||||
err = run(rate, duration, badFile, ordering, reporter, output)
|
|
||||||
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errTargetsFilePrefix)) {
|
|
||||||
t.Errorf("Targets file `%s` shouldn't be valid: %s", badFile, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOrderingValidation(t *testing.T) {
|
|
||||||
rate, duration, targetsf, _, reporter, output := defaultArguments()
|
|
||||||
|
|
||||||
// Good cases
|
|
||||||
for _, ordering := range []string{"random", "sequential"} {
|
|
||||||
err := run(rate, duration, targetsf, ordering, reporter, output)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Ordering `%s` should be valid: %s", ordering, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bad case
|
|
||||||
badOrdering := "lolcat"
|
|
||||||
err := run(rate, duration, targetsf, badOrdering, reporter, output)
|
|
||||||
if err == nil || (err != nil && !strings.HasPrefix(err.Error(), errOrderingPrefix)) {
|
|
||||||
t.Errorf("Ordering `%s` shouldn't be valid: %s", badOrdering, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaultArguments() (uint64, time.Duration, string, string, string, string) {
|
|
||||||
return uint64(1000), 5 * time.Millisecond, ".targets.txt", "random", "text", "/dev/null"
|
|
||||||
}
|
|
||||||
59
report.go
Normal file
59
report.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
vegeta "github.com/tsenart/vegeta/lib"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func reportCmd(args []string) command {
|
||||||
|
fs := flag.NewFlagSet("report", flag.ExitOnError)
|
||||||
|
reporter := fs.String("reporter", "text", "Reporter [text, json, plot:timings]")
|
||||||
|
input := fs.String("input", "stdin", "Input file")
|
||||||
|
output := fs.String("output", "stdout", "Output file")
|
||||||
|
fs.Parse(args)
|
||||||
|
|
||||||
|
return func() error {
|
||||||
|
return report(*reporter, *input, *output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// report validates the report arguments, sets up the required resources
|
||||||
|
// and writes the report
|
||||||
|
func report(reporter, input, output string) error {
|
||||||
|
var rep vegeta.Reporter
|
||||||
|
switch reporter {
|
||||||
|
case "text":
|
||||||
|
rep = vegeta.ReportText
|
||||||
|
case "json":
|
||||||
|
rep = vegeta.ReportJSON
|
||||||
|
case "plot:timings":
|
||||||
|
rep = vegeta.ReportTimingsPlot
|
||||||
|
default:
|
||||||
|
log.Println("Reporter provided is not supported. Using text")
|
||||||
|
rep = vegeta.ReportText
|
||||||
|
}
|
||||||
|
|
||||||
|
in, err := file(input, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
out, err := file(output, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
results := vegeta.Results{}
|
||||||
|
if err := results.ReadFrom(in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rep(results, out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user