diff --git a/cli/hrp/cmd/boom.go b/cli/hrp/cmd/boom.go index 1dc57447..a16657ab 100644 --- a/cli/hrp/cmd/boom.go +++ b/cli/hrp/cmd/boom.go @@ -43,6 +43,7 @@ var boomCmd = &cobra.Command{ hrpBoomer.SetDisableCompression(disableCompression) hrpBoomer.EnableCPUProfile(cpuProfile, cpuProfileDuration) hrpBoomer.EnableMemoryProfile(memoryProfile, memoryProfileDuration) + hrpBoomer.EnableGracefulQuit() hrpBoomer.Run(paths...) }, } diff --git a/internal/boomer/boomer.go b/internal/boomer/boomer.go index d204b7ed..6d22a10c 100644 --- a/internal/boomer/boomer.go +++ b/internal/boomer/boomer.go @@ -2,6 +2,9 @@ package boomer import ( "math" + "os" + "os/signal" + "syscall" "time" "github.com/rs/zerolog/log" @@ -95,6 +98,16 @@ func (b *Boomer) EnableMemoryProfile(memoryProfile string, duration time.Duratio b.memoryProfileDuration = duration } +// EnableGracefulQuit catch SIGINT and SIGTERM signals to quit gracefully +func (b *Boomer) EnableGracefulQuit() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) + go func() { + <-c + b.Quit() + }() +} + // Run accepts a slice of Task and connects to the locust master. func (b *Boomer) Run(tasks ...*Task) { if b.cpuProfile != "" { diff --git a/internal/boomer/runner.go b/internal/boomer/runner.go index 8b8d6444..c4da7cfd 100644 --- a/internal/boomer/runner.go +++ b/internal/boomer/runner.go @@ -5,10 +5,13 @@ import ( "math/rand" "os" "runtime/debug" + "strconv" "sync" "sync/atomic" "time" + "github.com/olekukonko/tablewriter" + "github.com/rs/zerolog/log" ) @@ -144,6 +147,36 @@ func (r *runner) reportStats() { r.outputOnEvent(data) } +func (r *runner) reportTestResult() { + // convert stats in total + var statsTotal interface{} = r.stats.total.serialize() + entryTotalOutput, err := deserializeStatsEntry(statsTotal) + if err != nil { + return + } + duration := time.Duration(entryTotalOutput.LastRequestTimestamp-entryTotalOutput.StartTime) * time.Second + currentTime := time.Now() + println(fmt.Sprint("=========================================== Statistics Summary ==========================================")) + println(fmt.Sprintf("Current time: %s, Users: %v, Duration: %v, Accumulated Transactions: %d Passed, %d Failed", + currentTime.Format("2006/01/02 15:04:05"), atomic.LoadInt32(&r.currentClientsNum), duration, r.stats.transactionPassed, r.stats.transactionFailed)) + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"Name", "# requests", "# fails", "Median", "Average", "Min", "Max", "Content Size", "# reqs/sec", "# fails/sec"}) + row := make([]string, 10) + row[0] = entryTotalOutput.Name + row[1] = strconv.FormatInt(entryTotalOutput.NumRequests, 10) + row[2] = strconv.FormatInt(entryTotalOutput.NumFailures, 10) + row[3] = strconv.FormatInt(entryTotalOutput.medianResponseTime, 10) + row[4] = strconv.FormatFloat(entryTotalOutput.avgResponseTime, 'f', 2, 64) + row[5] = strconv.FormatInt(entryTotalOutput.MinResponseTime, 10) + row[6] = strconv.FormatInt(entryTotalOutput.MaxResponseTime, 10) + row[7] = strconv.FormatInt(entryTotalOutput.avgContentLength, 10) + row[8] = strconv.FormatFloat(entryTotalOutput.currentRps, 'f', 2, 64) + row[9] = strconv.FormatFloat(entryTotalOutput.currentFailPerSec, 'f', 2, 64) + table.Append(row) + table.Render() + println() +} + func (r *localRunner) spawnWorkers(spawnCount int, spawnRate float64, quit chan bool, spawnCompleteFunc func()) { log.Info(). Int("spawnCount", spawnCount). @@ -324,6 +357,9 @@ func (r *localRunner) start() { r.rateLimiter.Stop() } + // report test result + r.reportTestResult() + // output teardown r.outputOnStop()