Skip to content

Commit

Permalink
fix: chart subcommand not rendering correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
joshabbott-f3 committed Sep 20, 2024
1 parent b125e2e commit 3a59e87
Show file tree
Hide file tree
Showing 17 changed files with 225 additions and 63 deletions.
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)

Check failure on line 67 in internal/chart/chart_cmd_stage_test.go

View workflow job for this annotation

GitHub Actions / Test

require-error: for error assertions use require (testifylint)
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)

Check failure on line 80 in internal/chart/chart_cmd_stage_test.go

View workflow job for this annotation

GitHub Actions / Test

require-error: for error assertions use require (testifylint)
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)

Check failure on line 88 in internal/chart/chart_cmd_stage_test.go

View workflow job for this annotation

GitHub Actions / Test

require-error: for error assertions use require (testifylint)
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)

Check failure on line 96 in internal/chart/chart_cmd_stage_test.go

View workflow job for this annotation

GitHub Actions / Test

require-error: for error assertions use require (testifylint)
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)

Check failure on line 109 in internal/chart/chart_cmd_stage_test.go

View workflow job for this annotation

GitHub Actions / Test

require-error: for error assertions use require (testifylint)
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) {

Check failure on line 122 in internal/chart/chart_cmd_stage_test.go

View workflow job for this annotation

GitHub Actions / Test

named return "n" with type "int" found (nonamedreturns)
return l.sb.Write(p)
}

func (l *lineStringBuilder) String() string {
return strings.Replace(l.sb.String(), "\\n", "\n", -1)

Check failure on line 127 in internal/chart/chart_cmd_stage_test.go

View workflow job for this annotation

GitHub Actions / Test

wrapperFunc: use strings.ReplaceAll method in `strings.Replace(l.sb.String(), "\\n", "\n", -1)` (gocritic)
}
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

0 comments on commit 3a59e87

Please sign in to comment.