diff --git a/README.md b/README.md index a81af23..ddf9e48 100644 --- a/README.md +++ b/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] [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 ![plot](https://dl.dropboxusercontent.com/u/83217940/plot.svg) -#### -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 diff --git a/attack.go b/attack.go new file mode 100644 index 0000000..8d7e83b --- /dev/null +++ b/attack.go @@ -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: " +) diff --git a/attack_test.go b/attack_test.go new file mode 100644 index 0000000..f9b1216 --- /dev/null +++ b/attack_test.go @@ -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" +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..539c313 --- /dev/null +++ b/file.go @@ -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 + } +} diff --git a/lib/attack.go b/lib/attack.go index c66fc33..2aa9b9e 100644 --- a/lib/attack.go +++ b/lib/attack.go @@ -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)) diff --git a/lib/metrics.go b/lib/metrics.go index 1588b1f..6592156 100644 --- a/lib/metrics.go +++ b/lib/metrics.go @@ -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{}{} } } diff --git a/lib/metrics_test.go b/lib/metrics_test.go index 8d17ed5..94571ab 100644 --- a/lib/metrics_test.go +++ b/lib/metrics_test.go @@ -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{ diff --git a/lib/results.go b/lib/results.go new file mode 100644 index 0000000..5d8a530 --- /dev/null +++ b/lib/results.go @@ -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] } diff --git a/lib/results_test.go b/lib/results_test.go new file mode 100644 index 0000000..c42847a --- /dev/null +++ b/lib/results_test.go @@ -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) + } +} diff --git a/lib/targets.go b/lib/targets.go index 467b899..9864f86 100644 --- a/lib/targets.go +++ b/lib/targets.go @@ -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() { diff --git a/lib/targets_test.go b/lib/targets_test.go index 6b7e99e..9337928 100644 --- a/lib/targets_test.go +++ b/lib/targets_test.go @@ -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) } diff --git a/main.go b/main.go index 63363f6..7184bb2 100644 --- a/main.go +++ b/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] [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 -} diff --git a/main_test.go b/main_test.go deleted file mode 100644 index 9cd75a0..0000000 --- a/main_test.go +++ /dev/null @@ -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" -} diff --git a/report.go b/report.go new file mode 100644 index 0000000..e428e7b --- /dev/null +++ b/report.go @@ -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 +}