From 5585317f075a09d3b6d68781f760c375b4c955d9 Mon Sep 17 00:00:00 2001 From: Nikolai Vladimirov Date: Sun, 8 Mar 2026 05:54:44 +0200 Subject: [PATCH] feat: add Virtual User ID (VUID) to iteration context Assign a distinct integer VUID to each pool worker when creating the iteration state pool. Enables correlating iterations with user-specific test data (e.g. in the "users" trigger mode). - Add VUID field and WithVUID option to testing.T - Assign VUID -1 for setup phase, 0-based for pool workers - Include VUID in error and panic log output via VUIDAttr - Add tests for WithVUID and VUID in error logs Co-authored-by: James Dunne Closes #309 Refs #310 --- internal/log/attrs.go | 4 ++++ internal/workers/active_scenario.go | 4 +++- internal/workers/pool_manager.go | 2 +- pkg/f1/testing/t.go | 28 +++++++++++++++++++++------- pkg/f1/testing/t_test.go | 27 +++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/internal/log/attrs.go b/internal/log/attrs.go index 86a2e92c..181d31fc 100644 --- a/internal/log/attrs.go +++ b/internal/log/attrs.go @@ -29,6 +29,10 @@ func IterationAttr(iteration string) slog.Attr { return slog.String("iteration", iteration) } +func VUIDAttr(vuid int) slog.Attr { + return slog.Int("vuid", vuid) +} + func DurationAttr(duration time.Duration) slog.Attr { return slog.Duration("duration", duration) } diff --git a/internal/workers/active_scenario.go b/internal/workers/active_scenario.go index c24a25d9..99cb6622 100644 --- a/internal/workers/active_scenario.go +++ b/internal/workers/active_scenario.go @@ -33,6 +33,7 @@ func NewActiveScenario( ) *ActiveScenario { t, teardown := testing.NewTWithOptions(scenario.Name, testing.WithIteration("setup"), + testing.WithVUID(-1), testing.WithLogger(logger), testing.WithLogrusLogger(logrusLogger), ) @@ -93,8 +94,9 @@ func (s *ActiveScenario) RecordDroppedIteration() { s.progress.Record(metrics.DroppedResult, instantDuration) } -func (s *ActiveScenario) newIterationState() *iterationState { +func (s *ActiveScenario) newIterationState(id int) *iterationState { t, teardown := testing.NewTWithOptions(s.scenario.Name, + testing.WithVUID(id), testing.WithLogger(s.logger), testing.WithLogrusLogger(s.logrusLogger), ) diff --git a/internal/workers/pool_manager.go b/internal/workers/pool_manager.go index b2f64616..61227c36 100644 --- a/internal/workers/pool_manager.go +++ b/internal/workers/pool_manager.go @@ -68,7 +68,7 @@ func (m *PoolManager) NewContinuousPool(numWorkers int) *ContinuousPool { func (m *PoolManager) makeIterationStatePool(numWorkers int) []*iterationState { statePool := make([]*iterationState, numWorkers) for i := range numWorkers { - statePool[i] = m.activeScenario.newIterationState() + statePool[i] = m.activeScenario.newIterationState(i) } return statePool diff --git a/pkg/f1/testing/t.go b/pkg/f1/testing/t.go index 55d4539d..aa470081 100644 --- a/pkg/f1/testing/t.go +++ b/pkg/f1/testing/t.go @@ -23,11 +23,15 @@ var errFailNow = errors.New("FailNow") // reporting methods, such as the variations of Log and Error, may be called simultaneously from // multiple goroutines. type T struct { - logrusLogger *logrus.Logger - logger *slog.Logger - require *require.Assertions - Iteration string // iteration number or "setup" - Scenario string + logrusLogger *logrus.Logger + logger *slog.Logger + require *require.Assertions + Iteration string // iteration number or "setup" + Scenario string + // VUID is the Virtual User ID - a stable identifier for the pool worker running this iteration. + // Useful for correlating iterations with user-specific test data (e.g. in the "users" trigger mode). + // VUID is -1 for setup; 0-based for pool workers. + VUID int teardownStack []func() failed atomic.Bool teardownFailed atomic.Bool @@ -57,6 +61,14 @@ func WithIteration(iteration string) TOption { } } +// WithVUID sets the Virtual User ID for the test context. +// Use -1 for setup phase; 0-based integers for pool workers. +func WithVUID(id int) TOption { + return func(t *T) { + t.VUID = id + } +} + // NewT returns a new T state // // Deprecated: Will be removed in favour of NewTWithOptions @@ -148,7 +160,7 @@ func (t *T) Errorf(format string, args ...any) { // Error is equivalent to Log followed by Fail. func (t *T) Error(err error) { - t.logger.Error("iteration failed", log.IterationAttr(t.Iteration), log.ErrorAttr(err)) + t.logger.Error("iteration failed", log.IterationAttr(t.Iteration), log.VUIDAttr(t.VUID), log.ErrorAttr(err)) t.Fail() } @@ -160,7 +172,7 @@ func (t *T) Fatalf(format string, args ...any) { // Fatal is equivalent to Log followed by FailNow. func (t *T) Fatal(err error) { - t.logger.Error("iteration failed", log.IterationAttr(t.Iteration), log.ErrorAttr(err)) + t.logger.Error("iteration failed", log.IterationAttr(t.Iteration), log.VUIDAttr(t.VUID), log.ErrorAttr(err)) t.FailNow() } @@ -220,6 +232,7 @@ func handlePanic(t *T, recovered any) { t.logger.Error("recovered panic in scenario", log.StackTraceAttr(stack), log.IterationAttr(t.Iteration), + log.VUIDAttr(t.VUID), log.ErrorAttr(err), ) t.Fail() @@ -228,6 +241,7 @@ func handlePanic(t *T, recovered any) { t.logger.Error("recovered panic in scenario", log.StackTraceAttr(stack), log.IterationAttr(t.Iteration), + log.VUIDAttr(t.VUID), log.ErrorAnyAttr(recovered), ) t.Fail() diff --git a/pkg/f1/testing/t_test.go b/pkg/f1/testing/t_test.go index 3528236e..f67b49a8 100644 --- a/pkg/f1/testing/t_test.go +++ b/pkg/f1/testing/t_test.go @@ -169,6 +169,33 @@ func TestNameReturnsScenarioName(t *testing.T) { require.Equal(t, "test", newT.Name()) } +func TestWithVUIDSetsVirtualUserID(t *testing.T) { + t.Parallel() + + newT, teardown := f1testing.NewTWithOptions("test", f1testing.WithVUID(42)) + defer teardown() + + require.Equal(t, 42, newT.VUID) +} + +func TestWithVUIDIncludedInErrorLogs(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + logger := slog.New(slog.NewTextHandler(&buf, nil)) + + newT, teardown := f1testing.NewTWithOptions("test", + f1testing.WithVUID(7), + f1testing.WithLogger(logger), + ) + defer teardown() + + newT.Error(errors.New("test error")) + logs := buf.String() + require.Contains(t, logs, "vuid=7") + require.Contains(t, logs, "test error") +} + func catchPanics(done chan<- struct{}) { _ = recover() close(done)