Skip to content
Draft

v3 #334

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a6ed684
chore: remove Fluentd integration
nvloff-f3 Mar 8, 2026
4f925b2
chore: remove go-chart dependency
nvloff-f3 Mar 8, 2026
db8598b
chore: remove go-chart and asciigraph dependencies
nvloff-f3 Mar 8, 2026
2fcf3f9
refactor!: remove logrus dependency
nvloff-f3 Mar 8, 2026
750a606
feat!: remove exposed pkg/f1/metrics package
nvloff-f3 Mar 8, 2026
d8e6c09
feat!: remove T.Time and metrics coupling from testing package
nvloff-f3 Mar 9, 2026
c70a0c6
feat!: remove deprecated NewT
nvloff-f3 Mar 9, 2026
ae6b0e5
feat!: rename pkg/f1/testing to pkg/f1/f1testing
nvloff-f3 Mar 9, 2026
8d7f8c0
feat!: remove deprecated --verbose-fail flag
nvloff-f3 Mar 9, 2026
5ca96fa
feat(cli): add flag grouping and improve help text
nvloff-f3 Mar 9, 2026
0e965a6
chore: bump module to v3
nvloff-f3 Mar 9, 2026
c2e07fb
feat!: add context.Context to ScenarioFn and RunFn
nvloff-f3 Mar 9, 2026
d5ef6b6
feat!: add Run API and propagate context through CLI
nvloff-f3 Mar 9, 2026
70dbc72
feat!: rename Add to AddScenario
nvloff-f3 Mar 9, 2026
7cb1b4e
feat!: NewRun accepts ...RunOption directly
nvloff-f3 Mar 10, 2026
7bc3b8b
feat!: make Error and Fatal compatible with testing.T
nvloff-f3 Mar 10, 2026
9ad4bb3
docs: add MIGRATION.md for v2 to v3 upgrade
nvloff-f3 Mar 10, 2026
1e0afa6
feat!: change T.Iteration from string to uint64
nvloff-f3 Mar 11, 2026
011958b
fix: error messages
nvloff-f3 Mar 11, 2026
be7e82d
refactor(scenarios): write ls output to cobra writer
nvloff-f3 Mar 12, 2026
b4f7aab
test(scenarios): add coverage for scenario registry builder
nvloff-f3 Mar 12, 2026
4549c45
test(scenarios): verify ls output ordering and help text
nvloff-f3 Mar 12, 2026
c99b6bf
chore: move migration file to docs/
nvloff-f3 Mar 13, 2026
931367d
feat(f1): allow programmatic overrides for env settings
nvloff-f3 Mar 13, 2026
28739da
docs: document settings overrides and env precedence
nvloff-f3 Mar 13, 2026
4ae97ad
refactor(f1): unify configuration and options
nvloff-f3 Mar 13, 2026
d6e3e8e
test(f1): assert configuration precedence and behavior
nvloff-f3 Mar 13, 2026
de20655
docs: unify configuration docs and precedence
nvloff-f3 Mar 13, 2026
d1d6dd1
chore: update migration docs
nvloff-f3 Mar 14, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
/vendor
/bin
*.pprof
*.csv
/local
10 changes: 4 additions & 6 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ linters:
exhaustruct:
# Structs that must have all fields explicitly set
include:
- github.com/form3tech-oss/f1/v2/internal/run/views.*
- github.com/form3tech-oss/f1/v3/internal/run/views.*

nolintlint:
require-explanation: true # nolint directives must explain why
Expand All @@ -79,14 +79,12 @@ linters:
- std-error-handling

rules:
# Deprecated Logger/WithLogrusLogger - legacy API support
- linters: [staticcheck]
path: \.go
text: "SA1019: (.*.Logger|testing.WithLogrusLogger)"

# Test files - relaxed rules for readability
- linters: [dupword, lll, unparam, wrapcheck]
path: _test\.go
# cobra.AddTemplateFunc modifies a global map; sync.Once is required
- linters: [gochecknoglobals]
path: internal/run/help\.go
- linters: [staticcheck]
path: _test\.go
text: ST1003
Expand Down
112 changes: 90 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<a href="https://pkg.go.dev/github.com/form3tech-oss/f1/v2/pkg/f1"><img align="right" src="https://pkg.go.dev/badge/github.com/form3tech-oss/f1/v2/pkg/f1.svg" alt="Go Reference"></a>
<a href="https://pkg.go.dev/github.com/form3tech-oss/f1/v3/pkg/f1"><img align="right" src="https://pkg.go.dev/badge/github.com/form3tech-oss/f1/v3/pkg/f1.svg" alt="Go Reference"></a>
# f1
`f1` is a flexible load testing framework using the `go` language for test scenarios. This allows test scenarios to be developed as code, utilising full development principles such as test driven development. Test scenarios with multiple stages and multiple modes are ideally suited to this environment.

Expand All @@ -15,12 +15,12 @@ These `ScenarioFn` and `RunFn` functions are defined as types in `f1`:

```golang
// ScenarioFn initialises a scenario and returns the iteration function (RunFn) to be invoked for every iteration
// of the tests.
type ScenarioFn func(t *T) RunFn
// of the tests. ctx is cancelled when the run is interrupted or times out.
type ScenarioFn func(ctx context.Context, t *T) RunFn

// RunFn performs a single iteration of the scenario. 't' may be used for asserting
// results or failing the scenario.
type RunFn func(t *T)
// results or failing the scenario. ctx is cancelled when the run is stopped; check ctx.Done() for cancellation.
type RunFn func(ctx context.Context, t *T)
```

Writing tests is simply a case of implementing the types and registering them with `f1`:
Expand All @@ -29,31 +29,32 @@ Writing tests is simply a case of implementing the types and registering them wi
package main

import (
"context"
"fmt"

"github.com/form3tech-oss/f1/v2/pkg/f1"
"github.com/form3tech-oss/f1/v2/pkg/f1/testing"
"github.com/form3tech-oss/f1/v3/pkg/f1"
"github.com/form3tech-oss/f1/v3/pkg/f1/f1testing"
)

func main() {
// Create a new f1 instance, add all the scenarios and execute the f1 tool.
// Any scenario that is added here can be executed like: `go run main.go run constant mySuperFastLoadTest`
f1.New().Add("mySuperFastLoadTest", setupMySuperFastLoadTest).Execute()
f1.New().AddScenario("mySuperFastLoadTest", setupMySuperFastLoadTest).Execute()
}

// Performs any setup steps and returns a function to run on every iteration of the scenario
func setupMySuperFastLoadTest(t *testing.T) testing.RunFn {
func setupMySuperFastLoadTest(ctx context.Context, t *f1testing.T) f1testing.RunFn {
fmt.Println("Setup the scenario")

// Register clean up function which will be invoked at the end of the scenario execution to clean up the setup
t.Cleanup(func() {
fmt.Println("Clean up the setup of the scenario")
})

runFn := func(t *testing.T) {
fmt.Println("Run the test")

// Register clean up function for each test which will be invoked in LIFO order after each iteration
runFn := func(ctx context.Context, t *f1testing.T) {
fmt.Println("Run the test")

// Register clean up function for each test which will be invoked in LIFO order after each iteration
t.Cleanup(func() {
fmt.Println("Clean up the test execution")
})
Expand Down Expand Up @@ -87,16 +88,83 @@ It provides the following information:
- `(20/s)` (attempted) rate,
- `avg: 72ns, min: 125ns, max: 27.590042ms` average, min and max iteration times.

### Environment variables
### Configuration

f1 can be configured via environment variables, programmatic options, or both. By default, environment variables are read at construction time. Programmatic options override env vars for the fields they set.

#### Settings reference

| Name | Format | Default | Description |
| Setting | Environment variable | Programmatic option | Default |
| --- | --- | --- | --- |
| `PROMETHEUS_PUSH_GATEWAY` | string - `host:port` or `ip:port` | `""` | Configures the address of a [Prometheus Push Gateway](https://prometheus.io/docs/instrumenting/pushing/) for exposing metrics. The prometheus job name configured will be `f1-{scenario_name}`. Disabled by default.|
| `PROMETHEUS_NAMESPACE` | string | `""` | Sets the metric label `namespace` to the specified value. Label is omitted if the value provided is empty.|
| `PROMETHEUS_LABEL_ID` | string | `""` | Sets the metric label `id` to the specified value. Label is omitted if the value provided is empty.|
| `LOG_FILE_PATH` | string | `""`| Specify the log file path used if `--verbose` is disabled. The logfile path will be an automatically generated temp file if not specified. |
| `F1_LOG_LEVEL` | string | `"info"`| Specify the log level of the default logger, one of: `debug`, `warn`, `error` |
| `F1_LOG_FORMAT` | string | `""`| Specify the log format of the default logger, defaults to `text` formatter, allows `json` |
| Prometheus push gateway | `PROMETHEUS_PUSH_GATEWAY` | `f1.WithPrometheusPushGateway(url)` | disabled |
| Prometheus namespace label | `PROMETHEUS_NAMESPACE` | `f1.WithPrometheusNamespace(ns)` | `""` |
| Prometheus ID label | `PROMETHEUS_LABEL_ID` | `f1.WithPrometheusLabelID(id)` | `""` |
| Log file path | `LOG_FILE_PATH` | `f1.WithLogFilePath(path)` | auto temp file |
| Log level | `F1_LOG_LEVEL` | `f1.WithLogLevel(slog.LevelDebug)` | `slog.LevelInfo` |
| Log format | `F1_LOG_FORMAT` | `f1.WithLogFormat(f1.LogFormatJSON)` | `f1.LogFormatText` |

Log level and format options use Go's standard `slog.Level` and f1's `LogFormat` type for compile-time safety. Use `f1.ParseLogLevel(string)` and `f1.ParseLogFormat(string)` to convert from strings (e.g. from config files).

#### Configuring without environment variables

Use `f1.WithSettings(f1.Settings{})` to start from zero values, ignoring all environment variables. Fine-grained options (`WithLogLevel`, `WithPrometheusPushGateway`, etc.) still apply after the baseline:

```golang
f1.New(
f1.WithSettings(f1.Settings{}),
f1.WithLogLevel(slog.LevelWarn),
f1.WithLogFormat(f1.LogFormatJSON),
).AddScenario("myScenario", mySetup).Execute()
```

For full control, pass a complete `f1.Settings` struct:

```golang
f1.New(
f1.WithSettings(f1.Settings{
Prometheus: f1.PrometheusSettings{
PushGateway: "http://pushgateway:9091",
Namespace: "my-namespace",
},
Logging: f1.LoggingSettings{
Level: slog.LevelDebug,
Format: f1.LogFormatJSON,
},
}),
).AddScenario("myScenario", mySetup).Execute()
```

#### Precedence

Settings are resolved in this order (highest priority first):

1. **Programmatic options** — values passed to `f1.New()` (applied in order)
2. **Environment variables** — read at construction time (baseline when no `WithSettings` is used)
3. **Defaults** — `slog.LevelInfo`, `LogFormatText`, no Prometheus push

When `f1.WithLogger(logger)` is used, the caller owns the logger entirely. `WithLogLevel`, `WithLogFormat`, and the corresponding env vars have no effect:

```golang
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
f1.New(
f1.WithLogger(logger),
).AddScenario("myScenario", mySetup).Execute()
```

#### Default env-backed behaviour

When no `WithSettings` is provided, environment variables are used as the baseline (backward-compatible with previous releases):

```golang
// Env vars like PROMETHEUS_PUSH_GATEWAY are read automatically
f1.New().AddScenario("myScenario", mySetup).Execute()

// Fine-grained options override individual env var values
f1.New(
f1.WithPrometheusPushGateway("http://pushgateway:9091"),
f1.WithLogLevel(slog.LevelDebug),
).AddScenario("myScenario", mySetup).Execute()
```

## Contributions
If you'd like to help improve `f1`, please fork this repo and raise a PR!
45 changes: 21 additions & 24 deletions benchcmd/main.go
Original file line number Diff line number Diff line change
@@ -1,60 +1,57 @@
package main

import (
"context"
"os"
"strconv"
"time"

"github.com/form3tech-oss/f1/v2/pkg/f1"
"github.com/form3tech-oss/f1/v2/pkg/f1/testing"
"github.com/form3tech-oss/f1/v3/pkg/f1"
"github.com/form3tech-oss/f1/v3/pkg/f1/f1testing"
)

func main() {
f1.New().
Add("emptyScenario", emptyScenario).
Add("failingScenario", failingScenario).
Add("sleepScenario", sleepScenario).
Add("logScenario", logScenario).
AddScenario("emptyScenario", emptyScenario).
AddScenario("failingScenario", failingScenario).
AddScenario("sleepScenario", sleepScenario).
AddScenario("logScenario", logScenario).
Execute()
}

func emptyScenario(*testing.T) testing.RunFn {
runFn := func(t *testing.T) {
func emptyScenario(context.Context, *f1testing.T) f1testing.RunFn {
return func(_ context.Context, t *f1testing.T) {
t.Require().True(true)
}

return runFn
}

func sleepScenario(t *testing.T) testing.RunFn {
func sleepScenario(_ context.Context, t *f1testing.T) f1testing.RunFn {
msString := os.Getenv("MS_SLEEP")
ms, err := strconv.ParseInt(msString, 10, 64)
t.Require().NoError(err)

runFn := func(*testing.T) {
runFn := func(_ context.Context, _ *f1testing.T) {
time.Sleep(time.Duration(ms) * time.Millisecond)
}

return runFn
}

func failingScenario(*testing.T) testing.RunFn {
runFn := func(t *testing.T) {
func failingScenario(context.Context, *f1testing.T) f1testing.RunFn {
return func(_ context.Context, t *f1testing.T) {
t.Require().True(false)
}

return runFn
}

func logScenario(t *testing.T) testing.RunFn {
func logScenario(_ context.Context, t *f1testing.T) f1testing.RunFn {
t.Log("Setup")
runFn := func(t *testing.T) {
t.Logf("Iteration: %s", t.Iteration)
t.Logger().WithField("iteration", t.Iteration).Trace("Trace log")
t.Logger().WithField("iteration", t.Iteration).Debug("Debug log")
t.Logger().WithField("iteration", t.Iteration).Info("Info log")
t.Logger().WithField("iteration", t.Iteration).Warn("Warn log")
t.Logger().WithField("iteration", t.Iteration).Error("Error log")
runFn := func(_ context.Context, t *f1testing.T) {
t.Logf("Iteration: %d", t.Iteration)
t.Logger().With("iteration", t.Iteration).Debug("Trace log")
t.Logger().With("iteration", t.Iteration).Debug("Debug log")
t.Logger().With("iteration", t.Iteration).Info("Info log")
t.Logger().With("iteration", t.Iteration).Warn("Warn log")
t.Logger().With("iteration", t.Iteration).Error("Error log")

panic("panic message")
}
Expand Down
Loading
Loading