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
|
||||
```
|
||||
|
||||
## 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
|
||||
$ vegeta -h
|
||||
Usage of vegeta:
|
||||
-cpus=n: Number of CPUs to use
|
||||
-duration=10s: Duration of the test
|
||||
-ordering="random": Attack ordering [sequential, random]
|
||||
-output="stdout": Reporter output file
|
||||
-rate=50: Requests per second
|
||||
-reporter="text": Reporter to use [text, json, plot:timings]
|
||||
-targets="targets.txt": Targets file
|
||||
Usage: vegeta [globals] <command> [options]
|
||||
|
||||
Commands:
|
||||
attack Hit the targets
|
||||
report Report the results
|
||||
|
||||
Globals:
|
||||
-cpus=8 Number of CPUs to use
|
||||
```
|
||||
|
||||
#### -cpus
|
||||
Specifies the number of CPUs to be used internally.
|
||||
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
|
||||
Specifies the amount of time to issue request to the targets.
|
||||
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.
|
||||
|
||||
#### -output
|
||||
Specifies the output file to which the report will be written to.
|
||||
The default is stdout.
|
||||
Specifies the output file to which the binary results will be written
|
||||
to. Made to be piped to the report command input. Defaults to stdout.
|
||||
|
||||
#### -rate
|
||||
Specifies the requests per second rate to issue against
|
||||
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.
|
||||
|
||||
#### -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
|
||||
Specifies the reporting type to display the results with.
|
||||
The default is the text report printed to stdout.
|
||||
##### -reporter=text
|
||||
Specifies the kind of report to be generated. It defaults to text.
|
||||
|
||||
##### text
|
||||
```
|
||||
Time(avg) Requests Success Bytes(rx/tx)
|
||||
152.341ms 200 17.00% 251.00/0.00
|
||||
@@ -75,7 +120,7 @@ Error Set:
|
||||
Server Timeout
|
||||
Page Not Found
|
||||
```
|
||||
##### -reporter=json
|
||||
##### json
|
||||
```json
|
||||
{
|
||||
"total_requests": 50,
|
||||
@@ -93,19 +138,9 @@ Page Not Found
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
##### -reporter=plot:timings
|
||||
Plots the request timings in SVG format.
|
||||
##### plot:timings
|
||||

|
||||
|
||||
#### -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)
|
||||
```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
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"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 error
|
||||
}
|
||||
|
||||
// Attack hits the passed Targets (http.Requests) at the rate specified for
|
||||
// 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.
|
||||
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())
|
||||
hits := make(chan *http.Request, total)
|
||||
res := make(chan Result, total)
|
||||
results := make(results, total)
|
||||
results := make(Results, total)
|
||||
// Scatter
|
||||
go drill(rate, hits, res)
|
||||
for i := 0; i < cap(hits); i++ {
|
||||
@@ -39,18 +26,9 @@ func Attack(targets Targets, rate uint64, duration time.Duration) []Result {
|
||||
}
|
||||
close(res)
|
||||
|
||||
sort.Sort(results)
|
||||
|
||||
return results
|
||||
return results.Sort()
|
||||
}
|
||||
|
||||
// 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.
|
||||
// It is throttled to the rate specified.
|
||||
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,
|
||||
Timing: time.Since(began),
|
||||
BytesOut: uint64(req.ContentLength),
|
||||
Error: err,
|
||||
}
|
||||
if err == nil {
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
} else {
|
||||
result.Code = uint16(r.StatusCode)
|
||||
if body, err := ioutil.ReadAll(r.Body); err != nil {
|
||||
if result.Code < 200 || result.Code >= 300 {
|
||||
result.Error = errors.New(string(body))
|
||||
result.Error = string(body)
|
||||
}
|
||||
} else {
|
||||
result.BytesIn = uint64(len(body))
|
||||
|
||||
@@ -37,8 +37,8 @@ func NewMetrics(results []Result) *Metrics {
|
||||
if result.Code >= 200 && result.Code < 300 {
|
||||
m.TotalSuccess++
|
||||
}
|
||||
if result.Error != nil {
|
||||
errorSet[result.Error.Error()] = struct{}{}
|
||||
if result.Error != "" {
|
||||
errorSet[result.Error] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package vegeta
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewMetrics(t *testing.T) {
|
||||
m := NewMetrics([]Result{
|
||||
Result{500, time.Now(), 100 * time.Millisecond, 10, 30, errors.New("Internal server error")},
|
||||
Result{200, time.Now(), 20 * time.Millisecond, 20, 20, nil},
|
||||
Result{200, time.Now(), 30 * time.Millisecond, 30, 10, nil},
|
||||
Result{500, time.Now(), 100 * time.Millisecond, 10, 30, "Internal server error"},
|
||||
Result{200, time.Now(), 20 * time.Millisecond, 20, 20, ""},
|
||||
Result{200, time.Now(), 30 * time.Millisecond, 30, 10, ""},
|
||||
})
|
||||
|
||||
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"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Targets represents the http.Requests which will be issued during the test
|
||||
type Targets []*http.Request
|
||||
|
||||
// NewTargetsFromFile reads and parses targets from a text file
|
||||
func NewTargetsFromFile(filename string) (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) {
|
||||
// NewTargetsFrom reads targets out of a line separated source skipping empty lines
|
||||
func NewTargetsFrom(source io.Reader) (Targets, error) {
|
||||
scanner := bufio.NewScanner(source)
|
||||
lines := make([]string, 0)
|
||||
for scanner.Scan() {
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"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")
|
||||
targets, err := readTargets(lines)
|
||||
targets, err := NewTargetsFrom(lines)
|
||||
if err != nil {
|
||||
t.Fatalf("Couldn't parse valid source: %s", err)
|
||||
}
|
||||
|
||||
122
main.go
122
main.go
@@ -3,104 +3,48 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
vegeta "github.com/tsenart/vegeta/lib"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
rate = flag.Uint64("rate", 50, "Requests per second")
|
||||
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()
|
||||
// command is a closure function which each command constructor
|
||||
// builds and returns
|
||||
type command func() error
|
||||
|
||||
if flag.NFlag() == 0 {
|
||||
flag.Usage()
|
||||
return
|
||||
var usage = fmt.Sprintf(
|
||||
`Usage: vegeta [globals] <command> [options]
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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