Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,29 @@ It provides the following information:
- `(20/s)` (attempted) rate,
- `avg: 72ns, min: 125ns, max: 27.590042ms` average, min and max iteration times.

### Chart Command

The `chart` command allows you to plot a chart of the test scenarios that would be triggered over time with the provided run function. It supports various subcommands corresponding to different trigger modes.

Usage:
```
f1 chart <subcommand>
```

Flags:
- `--chart-start`: Optional start time for the chart (default: current time in RFC3339 format)
- `--chart-duration`: Duration for the chart (default: 10 minutes)
- `--filename`: Filename for optional detailed chart, e.g. `<trigger_mode>.png`

The command generates an ASCII graph in the console and optionally creates a detailed PNG chart if a filename is provided.

Example:
```
f1 chart constant --chart-duration=5m --filename=constant_chart.png
```

This will generate an ASCII graph in the console and save a detailed PNG chart to `constant_chart.png`.

### Environment variables

| Name | Format | Default | Description |
Expand Down
31 changes: 18 additions & 13 deletions internal/chart/chart_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func chartCmdExecute(
return fmt.Errorf("creating builder: %w", err)
}

if trig.DryRun == nil {
if trig.Rate == nil {
return fmt.Errorf("%s does not support charting predicted load", cmd.Name())
}

Expand All @@ -82,7 +82,7 @@ func chartCmdExecute(
rates := []float64{0.0}
times := []time.Time{current}
for ; current.Add(sampleInterval).Before(end); current = current.Add(sampleInterval) {
rate := trig.DryRun(current)
rate := trig.Rate(current)
rates = append(rates, float64(rate))
times = append(times, current)
}
Expand All @@ -95,26 +95,31 @@ func chartCmdExecute(
return nil
}
graph := chart.Chart{
Title: trig.Description,
TitleStyle: chart.StyleTextDefaults(),
Width: 1920,
Height: 1024,
Title: trig.Description,
TitleStyle: chart.Style{
TextWrap: chart.TextWrapWord,
},
Width: 1920,
Height: 1024,
Background: chart.Style{
Padding: chart.Box{
Top: 50,
},
},
YAxis: chart.YAxis{
Name: "Triggered Test Iterations",
NameStyle: chart.StyleTextDefaults(),
Style: chart.StyleTextDefaults(),
AxisType: chart.YAxisSecondary,
Name: "Triggered Test Iterations",
AxisType: chart.YAxisSecondary,
ValueFormatter: chart.IntValueFormatter,
},
XAxis: chart.XAxis{
Name: "Time",
NameStyle: chart.StyleTextDefaults(),
ValueFormatter: chart.TimeMinuteValueFormatter,
Style: chart.StyleTextDefaults(),
},
Series: []chart.Series{
chart.TimeSeries{
Style: chart.Style{
StrokeColor: chart.GetDefaultColor(0).WithAlpha(64),
StrokeColor: chart.GetDefaultColor(0),
StrokeWidth: 2.0,
},
Name: "testing",
XValues: times,
Expand Down
84 changes: 65 additions & 19 deletions internal/chart/chart_cmd_stage_test.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,41 @@
package chart_test

import (
"strconv"
"testing"
"time"

"github.com/stretchr/testify/assert"
"os"
"strings"
"testing"

"github.com/form3tech-oss/f1/v2/internal/chart"
"github.com/form3tech-oss/f1/v2/internal/log"
"github.com/form3tech-oss/f1/v2/internal/trigger"
"github.com/form3tech-oss/f1/v2/internal/ui"
)

type ChartTestStage struct {
t *testing.T
assert *assert.Assertions
err error
args []string
t *testing.T
assert *assert.Assertions
err error
args []string
output *ui.Output
results *lineStringBuilder
expectedOutput string
}

func NewChartTestStage(t *testing.T) (*ChartTestStage, *ChartTestStage, *ChartTestStage) {
t.Helper()

sb := newLineStringBuilder()
p := ui.Printer{
Writer: sb,
ErrWriter: sb,
}

stage := &ChartTestStage{
t: t,
assert: assert.New(t),
t: t,
assert: assert.New(t),
output: ui.NewOutput(log.NewDiscardLogger(), &p, true, true),
results: sb,
}
return stage, stage, stage
}
Expand All @@ -34,8 +45,7 @@ func (s *ChartTestStage) and() *ChartTestStage {
}

func (s *ChartTestStage) i_execute_the_chart_command() *ChartTestStage {
outputer := ui.NewDiscardOutput()
cmd := chart.Cmd(trigger.GetBuilders(outputer), outputer)
cmd := chart.Cmd(trigger.GetBuilders(s.output), s.output)
cmd.SetArgs(s.args)
s.err = cmd.Execute()
return s
Expand All @@ -46,8 +56,16 @@ func (s *ChartTestStage) the_command_is_successful() *ChartTestStage {
return s
}

func (s *ChartTestStage) the_output_is_correct() *ChartTestStage {
s.assert.Equal(s.expectedOutput, s.results.String())
return s
}

func (s *ChartTestStage) the_load_style_is_constant() *ChartTestStage {
s.args = append(s.args, "constant", "--rate", "10/s", "--distribution", "none")
f, err := os.ReadFile("../testdata/expected-constant-chart-output.txt")
s.assert.NoError(err)
s.expectedOutput = string(f)
return s
}

Expand All @@ -56,27 +74,55 @@ func (s *ChartTestStage) jitter_is_applied() *ChartTestStage {
return s
}

func (s *ChartTestStage) the_load_style_is_staged(stages string) *ChartTestStage {
s.args = append(s.args, "staged", "--stages", stages, "--distribution", "none")
func (s *ChartTestStage) the_load_style_is_staged() *ChartTestStage {
s.args = append(s.args, "staged", "--stages", "5m:100,2m:0,10s:100", "--distribution", "none")
f, err := os.ReadFile("../testdata/expected-staged-chart-output.txt")
s.assert.NoError(err)
s.expectedOutput = string(f)
return s
}

func (s *ChartTestStage) the_load_style_is_ramp() *ChartTestStage {
s.args = append(s.args, "ramp", "--start-rate", "0/s", "--end-rate", "10/s", "--ramp-duration", "10s", "--chart-duration", "10s", "--distribution", "none")
f, err := os.ReadFile("../testdata/expected-ramp-chart-output.txt")
s.assert.NoError(err)
s.expectedOutput = string(f)
return s
}

func (s *ChartTestStage) the_load_style_is_gaussian_with_a_volume_of(volume int) *ChartTestStage {
s.args = append(s.args, "gaussian", "--peak", "5m", "--repeat", "10m", "--volume", strconv.Itoa(volume), "--standard-deviation", "1m", "--distribution", "none")
func (s *ChartTestStage) the_load_style_is_gaussian_with_a_volume_of() *ChartTestStage {
s.args = append(s.args, "gaussian", "--peak", "5m", "--repeat", "10m", "--volume", "100000", "--standard-deviation", "1m", "--distribution", "none")
f, err := os.ReadFile("../testdata/expected-gaussian-chart-output.txt")
s.assert.NoError(err)
s.expectedOutput = string(f)
return s
}

func (s *ChartTestStage) the_chart_starts_at_a_fixed_time() *ChartTestStage {
s.args = append(s.args, "--chart-start", time.Now().Truncate(10*time.Minute).Format(time.RFC3339))
s.args = append(s.args, "--chart-start", "2024-09-19T17:00:00Z")
return s
}

func (s *ChartTestStage) the_load_style_is_defined_in_the_config_file(filename string) *ChartTestStage {
s.args = append(s.args, "file", filename, "--chart-duration", "5s")
func (s *ChartTestStage) the_load_style_is_defined_in_the_config_file() *ChartTestStage {
s.args = append(s.args, "file", "../testdata/config-file.yaml", "--chart-duration", "5s")
f, err := os.ReadFile("../testdata/expected-file-chart-output.txt")
s.assert.NoError(err)
s.expectedOutput = string(f)
return s
}

type lineStringBuilder struct {
sb *strings.Builder
}

func newLineStringBuilder() *lineStringBuilder {
return &lineStringBuilder{sb: &strings.Builder{}}
}

func (l *lineStringBuilder) Write(p []byte) (n int, err error) {
return l.sb.Write(p)
}

func (l *lineStringBuilder) String() string {
return strings.Replace(l.sb.String(), "\\n", "\n", -1)
}
23 changes: 14 additions & 9 deletions internal/chart/chart_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ func TestChartConstantNoJitter(t *testing.T) {
i_execute_the_chart_command()

then.
the_command_is_successful()
the_command_is_successful().and().
the_output_is_correct()
}

func TestChartStaged(t *testing.T) {
Expand All @@ -41,13 +42,14 @@ func TestChartStaged(t *testing.T) {
given, when, then := NewChartTestStage(t)

given.
the_load_style_is_staged("5m:100,2m:0,10s:100")
the_load_style_is_staged()

when.
i_execute_the_chart_command()

then.
the_command_is_successful()
the_command_is_successful().and().
the_output_is_correct()
}

func TestChartGaussian(t *testing.T) {
Expand All @@ -56,14 +58,15 @@ func TestChartGaussian(t *testing.T) {
given, when, then := NewChartTestStage(t)

given.
the_load_style_is_gaussian_with_a_volume_of(100000).and().
the_load_style_is_gaussian_with_a_volume_of().and().
the_chart_starts_at_a_fixed_time()

when.
i_execute_the_chart_command()

then.
the_command_is_successful()
the_command_is_successful().and().
the_output_is_correct()
}

func TestChartGaussianWithJitter(t *testing.T) {
Expand All @@ -72,7 +75,7 @@ func TestChartGaussianWithJitter(t *testing.T) {
given, when, then := NewChartTestStage(t)

given.
the_load_style_is_gaussian_with_a_volume_of(100000).and().
the_load_style_is_gaussian_with_a_volume_of().and().
jitter_is_applied().and().
the_chart_starts_at_a_fixed_time()

Expand All @@ -95,7 +98,8 @@ func TestChartRamp(t *testing.T) {
i_execute_the_chart_command()

then.
the_command_is_successful()
the_command_is_successful().and().
the_output_is_correct()
}

func TestChartFileConfig(t *testing.T) {
Expand All @@ -104,11 +108,12 @@ func TestChartFileConfig(t *testing.T) {
given, when, then := NewChartTestStage(t)

given.
the_load_style_is_defined_in_the_config_file("../testdata/config-file.yaml")
the_load_style_is_defined_in_the_config_file()

when.
i_execute_the_chart_command()

then.
the_command_is_successful()
the_command_is_successful().and().
the_output_is_correct()
}
16 changes: 16 additions & 0 deletions internal/testdata/expected-constant-chart-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
10.00 ┤╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
9.33 ┤│
8.67 ┤│
8.00 ┤│
7.33 ┤│
6.67 ┤│
6.00 ┤│
5.33 ┤│
4.67 ┤│
4.00 ┤│
3.33 ┤│
2.67 ┤│
2.00 ┤│
1.33 ┤│
0.67 ┤│
0.00 ┼╯
16 changes: 16 additions & 0 deletions internal/testdata/expected-file-chart-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
10.00 ┤╭────────────────╮
9.33 ┤│ │ ╭╮
8.67 ┤│ │ ││
8.00 ┤│ │ │╰╮
7.33 ┤│ │ ╭╯ │
6.67 ┤│ │ │ │
6.00 ┤│ │ ╭╯ │
5.33 ┤│ ╰────────────╮ │ ╰╮
4.67 ┤│ │ │ │
4.00 ┤│ │ ╭╯ │
3.33 ┤│ │ ╭╯ │
2.67 ┤│ │ │ │
2.00 ┤│ │ │ ╰╮
1.33 ┤│ │╭╯ │ ╭───╮
0.67 ┤│ ││ │ │ │
0.00 ┼╯ ╰╯ ╰───────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────
16 changes: 16 additions & 0 deletions internal/testdata/expected-gaussian-chart-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
665 ┤ ╭───────╮
621 ┤ ╭──╯ ╰──╮
576 ┤ ╭──╯ ╰──╮
532 ┤ ╭─╯ ╰─╮
488 ┤ ╭╯ ╰╮
443 ┤ ╭─╯ ╰─╮
399 ┤ ╭─╯ ╰─╮
355 ┤ ╭─╯ ╰─╮
310 ┤ ╭─╯ ╰─╮
266 ┤ ╭─╯ ╰─╮
222 ┤ ╭─╯ ╰─╮
177 ┤ ╭─╯ ╰─╮
133 ┤ ╭──╯ ╰──╮
89 ┤ ╭───╯ ╰───╮
44 ┤ ╭──────╯ ╰──────╮
0 ┼───────────────────────────────────────╯ ╰─────────────────────────────────────
16 changes: 16 additions & 0 deletions internal/testdata/expected-ramp-chart-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
9.00 ┤ ╭──────────────
8.40 ┤ │
7.80 ┤ ╭───────────────╯
7.20 ┤ ╭───────────────╯
6.60 ┤ │
6.00 ┤ ╭───────────────╯
5.40 ┤ │
4.80 ┤ ╭───────────────╯
4.20 ┤ ╭───────────────╯
3.60 ┤ │
3.00 ┤ ╭───────────────╯
2.40 ┤ │
1.80 ┤ ╭───────────────╯
1.20 ┤ ╭───────────────╯
0.60 ┤ │
0.00 ┼────────────────╯
16 changes: 16 additions & 0 deletions internal/testdata/expected-staged-chart-output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
99.00 ┤ ╭────╮
92.40 ┤ ╭────╯ ╰╮
85.80 ┤ ╭─────╯ ╰──╮
79.20 ┤ ╭────╯ ╰─╮
72.60 ┤ ╭────╯ ╰─╮
66.00 ┤ ╭────╯ ╰─╮ ╭╮
59.40 ┤ ╭────╯ ╰─╮ ││
52.80 ┤ ╭─────╯ ╰─╮ ││
46.20 ┤ ╭────╯ ╰─╮ ││
39.60 ┤ ╭────╯ ╰─╮ ││
33.00 ┤ ╭─────╯ ╰──╮ ││
26.40 ┤ ╭───╯ ╰╮ ╭╯│
19.80 ┤ ╭─────╯ ╰──╮ │ │
13.20 ┤ ╭─────╯ ╰─╮ │ │
6.60 ┤ ╭───╯ ╰─╮│ │
0.00 ┼────╯ ╰╯ ╰────────────────────────────────────────────
Loading