diff --git a/load/pacer.go b/load/pacer.go index f7223b6a..02b267c0 100644 --- a/load/pacer.go +++ b/load/pacer.go @@ -34,9 +34,9 @@ type ConstantPacer struct { } // String returns a pretty-printed description of the ConstantPacer's behaviour: -// ConstantPacer{Freq: 1} => Constant{1 hits/1s} +// ConstantPacer{Freq: 1} => Constant{1 hits / 1s} func (cp *ConstantPacer) String() string { - return fmt.Sprintf("Constant{%d hits/1s}", cp.Freq) + return fmt.Sprintf("Constant{%d hits / 1s}", cp.Freq) } // Pace determines the length of time to sleep until the next hit is sent. @@ -258,9 +258,9 @@ func (p *StepPacer) hits(t time.Duration) float64 { } // String returns a pretty-printed description of the StepPacer's behaviour: -// StepPacer{Step: 1, StepDuration: 5s} => Step{1 hits/5s} +// StepPacer{Step: 1, StepDuration: 5s} => Step{Step:1 hits/5s} func (p *StepPacer) String() string { - return fmt.Sprintf("Step{%d hits/%s}", p.Step, p.StepDuration.String()) + return fmt.Sprintf("Step{Step: %d hits / %s}", p.Step, p.StepDuration.String()) } // LinearPacer paces an attack by starting at a given request rate diff --git a/load/pacer_test.go b/load/pacer_test.go index 2ce92880..d9350abd 100644 --- a/load/pacer_test.go +++ b/load/pacer_test.go @@ -14,6 +14,7 @@ func TestConstantPacer(t *testing.T) { for _, tc := range []struct { freq uint64 + max uint64 elapsed time.Duration hits uint64 wait time.Duration @@ -108,8 +109,25 @@ func TestConstantPacer(t *testing.T) { wait: 0, stop: false, }, + // Max + { + freq: 1, + elapsed: 1 * time.Second, + hits: 10, + wait: 10 * time.Second, + stop: false, + max: 0, + }, + { + freq: 1, + elapsed: 1 * time.Second, + hits: 10, + wait: 0, + stop: true, + max: 7, + }, } { - cp := ConstantPacer{Freq: tc.freq} + cp := ConstantPacer{Freq: tc.freq, Max: tc.max} wait, stop := cp.Pace(tc.elapsed, tc.hits) assert.Equal(t, tc.wait, wait) assert.Equal(t, tc.stop, stop) @@ -141,22 +159,10 @@ func TestConstantPacer_Rate(t *testing.T) { } } -// Stolen from https://github.com/google/go-cmp/cmp/cmpopts/equate.go -// to avoid an unwieldy dependency. Both fraction and margin set at 1e-6. -func floatEqual(x, y float64) bool { - relMarg := 1e-6 * math.Min(math.Abs(x), math.Abs(y)) - return math.Abs(x-y) <= math.Max(1e-6, relMarg) -} - -// A similar function to the above because SinePacer.Pace has discrete -// inputs and outputs but uses floats internally, and sometimes the -// floating point imprecision leaks out :-( -func durationEqual(x, y time.Duration) bool { - diff := x - y - if diff < 0 { - diff = -diff - } - return diff <= time.Microsecond +func TestConstantPacer_String(t *testing.T) { + cp := ConstantPacer{Freq: 5} + actual := cp.String() + assert.Equal(t, "Constant{5 hits / 1s}", actual) } func TestLinearPacer(t *testing.T) { @@ -772,6 +778,7 @@ func TestStepPacer(t *testing.T) { stepDuration time.Duration stop uint64 stopDuration time.Duration + max uint64 // params elapsed time.Duration hits uint64 @@ -1003,10 +1010,36 @@ func TestStepPacer(t *testing.T) { wait: 100 * time.Millisecond, stopResult: false, }, + // Max + { + start: 5, + step: 5, + stepDuration: 5 * time.Second, + stop: 25, + stopDuration: 0 * time.Second, + max: 100, + elapsed: 5000 * time.Millisecond, + hits: 25, + wait: 100 * time.Millisecond, + stopResult: false, + }, + { + start: 5, + step: 5, + stepDuration: 5 * time.Second, + stop: 25, + max: 10, + stopDuration: 0 * time.Second, + elapsed: 5000 * time.Millisecond, + hits: 25, + wait: 0, + stopResult: true, + }, } { t.Run(strconv.Itoa(ti), func(t *testing.T) { p := StepPacer{ - Start: ConstantPacer{Freq: tc.start}, + Start: ConstantPacer{Freq: tc.start, Max: tc.max}, + Max: tc.max, Step: tc.step, StepDuration: tc.stepDuration, LoadDuration: tc.stopDuration, Stop: ConstantPacer{Freq: tc.stop}} @@ -1017,3 +1050,32 @@ func TestStepPacer(t *testing.T) { }) } } + +func TestStepPacer_String(t *testing.T) { + p := StepPacer{ + Start: ConstantPacer{Freq: 5, Max: 100}, + Max: 100, + Step: 2, StepDuration: 5 * time.Second, + LoadDuration: 25 * time.Second, Stop: ConstantPacer{Freq: 25}} + + actual := p.String() + assert.Equal(t, "Step{Step: 2 hits / 5s}", actual) +} + +// Stolen from https://github.com/google/go-cmp/cmp/cmpopts/equate.go +// to avoid an unwieldy dependency. Both fraction and margin set at 1e-6. +func floatEqual(x, y float64) bool { + relMarg := 1e-6 * math.Min(math.Abs(x), math.Abs(y)) + return math.Abs(x-y) <= math.Max(1e-6, relMarg) +} + +// A similar function to the above because SinePacer.Pace has discrete +// inputs and outputs but uses floats internally, and sometimes the +// floating point imprecision leaks out :-( +func durationEqual(x, y time.Duration) bool { + diff := x - y + if diff < 0 { + diff = -diff + } + return diff <= time.Microsecond +} diff --git a/load/worker_ticker.go b/load/worker_ticker.go index 8ab01845..1bb4420c 100644 --- a/load/worker_ticker.go +++ b/load/worker_ticker.go @@ -14,6 +14,7 @@ type WorkerTicker interface { // TickValue is the delta value type TickValue struct { Delta int + Done bool } // ConstWorkerTicker is the const worker @@ -69,15 +70,31 @@ func (c *StepWorkerTicker) Run() { go func() { for range ticker.C { + // we have load duration and we eclipsed it if c.LoadDuration > 0 && time.Since(begin) > c.LoadDuration { - if c.Stop > 0 { - c.C <- TickValue{Delta: int(c.Stop - uint(wc))} + if stepUp && c.Stop > 0 && c.Stop > uint(wc) { + // if we have step up and stop value is > current count + // send the final diff + c.C <- TickValue{Delta: int(c.Stop - uint(wc)), Done: true} + } else if !stepUp && c.Stop > 0 && c.Stop < uint(wc) { + // if we have step down and stop value is < current count + // send the final diff + c.C <- TickValue{Delta: int(c.Stop - uint(wc)), Done: true} + } else { + // send done signal + c.C <- TickValue{Delta: 0, Done: true} } done <- true return - } else if (c.Stop > 0 && stepUp && wc >= int(c.Stop)) || - (!stepUp && wc <= int(c.Stop)) || wc <= 0 { + } else if (c.LoadDuration == 0) && ((c.Stop > 0 && stepUp && wc >= int(c.Stop)) || + (!stepUp && wc <= int(c.Stop))) { + // we do not have load duration + // if we have stop and are step up and current count >= stop + // or if we have stop and are step down and current count <= stop + // send done signal + + c.C <- TickValue{Delta: 0, Done: true} done <- true return } else { diff --git a/load/worker_ticker_test.go b/load/worker_ticker_test.go new file mode 100644 index 00000000..7d0bef9e --- /dev/null +++ b/load/worker_ticker_test.go @@ -0,0 +1,124 @@ +package load + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestConstWorkerTicker(t *testing.T) { + wt := ConstWorkerTicker{N: 5, C: make(chan TickValue)} + defer wt.Finish() + + wct := wt.Ticker() + + assert.NotNil(t, wct) + + go func() { + wt.Run() + }() + + tv := <-wct + + assert.NotEmpty(t, tv) + assert.Equal(t, 5, tv.Delta) +} + +func TestStepWorkerTicker(t *testing.T) { + t.Parallel() + + t.Run("step increase load duration", func(t *testing.T) { + wt := StepWorkerTicker{ + C: make(chan TickValue), + Start: 5, + Step: 2, + Stop: 0, + StepDuration: 2 * time.Second, + LoadDuration: 5 * time.Second, + } + + defer wt.Finish() + + wct := wt.Ticker() + + assert.NotNil(t, wct) + + go func() { + wt.Run() + }() + + tv := <-wct + assert.NotEmpty(t, tv) + assert.Equal(t, 5, tv.Delta) + assert.False(t, tv.Done) + + start := time.Now() + tv = <-wct + end := time.Since(start) + assert.NotEmpty(t, tv) + assert.Equal(t, 2, tv.Delta) + assert.False(t, tv.Done) + expected := 2 * time.Second + assert.True(t, durationEqual(expected, end.Round(time.Second)), "expected %s to equal %s", expected, end) + + tv = <-wct + end = time.Since(start) + assert.NotEmpty(t, tv) + assert.Equal(t, 2, tv.Delta) + assert.False(t, tv.Done) + assert.True(t, durationEqual(2*expected, end.Round(time.Second)), "expected %s to equal %s", expected, end) + + tv = <-wct + assert.Equal(t, 0, tv.Delta) + assert.True(t, tv.Done) + }) + + t.Run("step increase load duration with stop", func(t *testing.T) { + wt := StepWorkerTicker{ + C: make(chan TickValue), + Start: 5, + Step: 2, + Stop: 15, + StepDuration: 2 * time.Second, + LoadDuration: 5 * time.Second, + } + + defer wt.Finish() + + wct := wt.Ticker() + + assert.NotNil(t, wct) + + go func() { + wt.Run() + }() + + tv := <-wct + assert.NotEmpty(t, tv) + assert.Equal(t, 5, tv.Delta) + assert.False(t, tv.Done) + + start := time.Now() + tv = <-wct + end := time.Since(start) + assert.NotEmpty(t, tv) + assert.Equal(t, 2, tv.Delta) + assert.False(t, tv.Done) + expected := 2 * time.Second + assert.True(t, durationEqual(expected, end.Round(time.Second)), "expected %s to equal %s", expected, end) + + tv = <-wct + end = time.Since(start) + assert.NotEmpty(t, tv) + assert.Equal(t, 2, tv.Delta) + assert.False(t, tv.Done) + assert.True(t, durationEqual(2*expected, end.Round(time.Second)), "expected %s to equal %s", expected, end) + + tv = <-wct + end = time.Since(start) + assert.Equal(t, 6, tv.Delta) + assert.True(t, tv.Done) + assert.True(t, durationEqual(3*expected, end.Round(time.Second)), "expected %s to equal %s", expected, end) + }) +} diff --git a/runner/requester.go b/runner/requester.go index 2e44aa16..43463645 100644 --- a/runner/requester.go +++ b/runner/requester.go @@ -394,7 +394,7 @@ func (b *Requester) runWorkers(wt load.WorkerTicker, p load.Pacer) error { errC <- w.runWorker() }() } - } else { + } else if tv.Delta < 0 { nd := -1 * tv.Delta wm.Lock() wdc := 0