Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revamp the end-of-test summary #4089

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
55617fa
Work in progress: new end-of-test summary
joanlopez Oct 21, 2024
0d6eab2
More improvements for the new end-of-test summary
joanlopez Oct 22, 2024
a6d0c68
Dummy summary output
joanlopez Oct 23, 2024
f324c2c
Start collecting metrics on summary output
joanlopez Oct 26, 2024
a6b496b
Include groups & scenarios metrics on summary
joanlopez Oct 28, 2024
89379a7
Display nested groups in the summary
joanlopez Oct 30, 2024
40f3e1c
Fix summary report metric values
joanlopez Oct 30, 2024
e088fab
Push multi-scenario script example
joanlopez Oct 30, 2024
589b0e6
Fix end-of-test summary when there are no checks
joanlopez Nov 4, 2024
099d0c4
Include nested checks to the end-of-test summary
joanlopez Nov 5, 2024
9beea69
Rename storeSample and addSample methods for clarity
oleiade Nov 5, 2024
ce95d36
WIP
oleiade Dec 3, 2024
6bceb9c
Store Threholds to Summary output and its report
joanlopez Dec 4, 2024
b0215a7
Print Threholds as part of the summary output
joanlopez Dec 4, 2024
5d30088
Add JSDoc documentation to summary.js
oleiade Dec 10, 2024
b80a09c
Apply JS linter recommendations to summary.js
oleiade Dec 10, 2024
b2b5823
Refactor metrics and thresholds rendering in summary.js
oleiade Dec 11, 2024
4153ba7
Import prettier+eslint configurations from docs and format summary.js
oleiade Dec 16, 2024
af4d6f4
Factor decoration in a ANSIFormatter class
oleiade Dec 16, 2024
8b1da1d
Refactor text summary generation for simplicity and maintainability
oleiade Dec 17, 2024
126a188
Reorganize the summary.js file for easier maintenance
oleiade Dec 17, 2024
63d89e0
Fulfill JSDoc documentation of summary.js
oleiade Dec 18, 2024
21e18dd
Merge branch 'master' into new-end-of-test-summary-output
joanlopez Jan 15, 2025
644623b
Enable --summary-extended mode
joanlopez Jan 15, 2025
061e60e
Add a custom metric to the example script
joanlopez Jan 15, 2025
8b75bed
Merge branch 'new-end-of-test-summary-output' of github.com:grafana/k…
joanlopez Jan 15, 2025
037c16a
Clean up the old summary data (except for the user-defined handler)
joanlopez Jan 17, 2025
3a6a7d4
Merge remote-tracking branch 'upstream/master' into new-end-of-test-s…
joanlopez Jan 27, 2025
d8e2805
Merge remote-tracking branch 'upstream/master' into new-end-of-test-s…
joanlopez Jan 27, 2025
04cd35b
Par the end-of-test summary with the design
joanlopez Jan 30, 2025
f0aa13a
Align metrics sections within the same block
joanlopez Jan 30, 2025
28fd762
Replace --summary-extended with --with-summary
joanlopez Feb 3, 2025
27769d8
Consistent naming: report => summary
joanlopez Feb 3, 2025
ce4b92f
Keep support for the 'legacy' summary
joanlopez Feb 4, 2025
cd633bc
(Try to) fix some tests
joanlopez Feb 5, 2025
f62c2d4
Fix linter complaints
joanlopez Feb 6, 2025
eee0c99
Fix xk6 test
joanlopez Feb 6, 2025
da116ac
Merge remote-tracking branch 'upstream/master' into new-end-of-test-s…
joanlopez Feb 6, 2025
eaf59f5
Fix more linter complaints
joanlopez Feb 6, 2025
8cb412c
Small refactor
joanlopez Feb 7, 2025
52ce9db
Merge branch 'new-end-of-test-summary-output' of github.com:grafana/k…
joanlopez Feb 7, 2025
cf87057
Move full-summary example to internal/cmd/testdata
joanlopez Feb 7, 2025
1bb7aaf
Pass render options missing to ANSIFormatter
joanlopez Feb 7, 2025
bf19d6c
Accommodate tests to the new summary format
joanlopez Feb 7, 2025
7a057b6
Merge remote-tracking branch 'upstream/master' into new-end-of-test-s…
joanlopez Feb 7, 2025
75af9ab
Refine flaky test with undetermined amount of iterations
joanlopez Feb 7, 2025
9c1ed70
Refactor runtime options init func
joanlopez Feb 7, 2025
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: 1 addition & 1 deletion .github/workflows/xk6-tests/xk6-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export let options = {

export function handleSummary(data) {
return {
'summary-results.txt': data.metrics.foos.values.count.toString(),
'summary-results.txt': data.metrics.custom.foos.values.count.toString(),
};
}

Expand Down
12 changes: 8 additions & 4 deletions internal/cmd/builtin_output_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/cmd/outputs.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
builtinOutputKafka
builtinOutputStatsd
builtinOutputExperimentalOpentelemetry
builtinOutputSummary
)

// TODO: move this to an output sub-module after we get rid of the old collectors?
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/outputs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func TestBuiltinOutputString(t *testing.T) {
exp := []string{
"cloud", "csv", "datadog", "experimental-prometheus-rw",
"influxdb", "json", "kafka", "statsd", "experimental-opentelemetry",
"summary",
}
assert.Equal(t, exp, builtinOutputStrings())
}
87 changes: 69 additions & 18 deletions internal/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"go.k6.io/k6/lib/fsext"
"go.k6.io/k6/metrics"
"go.k6.io/k6/output"
"go.k6.io/k6/output/summary"
)

// cmdRun handles the `k6 run` sub-command
Expand Down Expand Up @@ -189,26 +190,76 @@ func (c *cmdRun) run(cmd *cobra.Command, args []string) (err error) {
}

executionState := execScheduler.GetState()
if !testRunState.RuntimeOptions.NoSummary.Bool {
defer func() {
logger.Debug("Generating the end-of-test summary...")
summaryResult, hsErr := test.initRunner.HandleSummary(globalCtx, &lib.Summary{
Metrics: metricsEngine.ObservedMetrics,
RootGroup: testRunState.GroupSummary.Group(),
TestRunDuration: executionState.GetCurrentTestRunDuration(),
NoColor: c.gs.Flags.NoColor,
UIState: lib.UIState{
IsStdOutTTY: c.gs.Stdout.IsTTY,
IsStdErrTTY: c.gs.Stderr.IsTTY,
},
if !testRunState.RuntimeOptions.NoSummary.Bool { //nolint:nestif
sm, err := lib.ValidateSummaryMode(testRunState.RuntimeOptions.SummaryMode.String)
if err != nil {
logger.WithError(err).Error("invalid summary mode, falling back to \"compact\" (default)")
}

switch sm {
// TODO: Remove this code block once we stop supporting the legacy summary, and just leave the default.
case lib.SummaryModeLegacy:
// At the end of the test run
defer func() {
logger.Debug("Generating the end-of-test summary...")

legacySummary := &lib.LegacySummary{
Metrics: metricsEngine.ObservedMetrics,
RootGroup: testRunState.GroupSummary.Group(),
TestRunDuration: executionState.GetCurrentTestRunDuration(),
NoColor: c.gs.Flags.NoColor,
UIState: lib.UIState{
IsStdOutTTY: c.gs.Stdout.IsTTY,
IsStdErrTTY: c.gs.Stderr.IsTTY,
},
}

summaryResult, hsErr := test.initRunner.HandleSummary(globalCtx, legacySummary, nil)
if hsErr == nil {
hsErr = handleSummaryResult(c.gs.FS, c.gs.Stdout, c.gs.Stderr, summaryResult)
}
if hsErr != nil {
logger.WithError(hsErr).Error("failed to handle the end-of-test summary")
}
}()
default:
// Instantiates the summary output
summaryOutput, err := summary.New(output.Params{
RuntimeOptions: testRunState.RuntimeOptions,
Logger: c.gs.Logger,
})
if hsErr == nil {
hsErr = handleSummaryResult(c.gs.FS, c.gs.Stdout, c.gs.Stderr, summaryResult)
}
if hsErr != nil {
logger.WithError(hsErr).Error("failed to handle the end-of-test summary")
if err != nil {
logger.WithError(err).Error("failed to initialize the end-of-test summary output")
}
}()
outputs = append(outputs, summaryOutput)

// At the end of the test run
defer func() {
logger.Debug("Generating the end-of-test summary...")

summary := summaryOutput.Summary(
executionState,
metricsEngine.ObservedMetrics,
test.initRunner.GetOptions(),
)

// TODO: We should probably try to move these out of the summary,
// likely as an additional argument like options.
summary.NoColor = c.gs.Flags.NoColor
summary.UIState = lib.UIState{
IsStdOutTTY: c.gs.Stdout.IsTTY,
IsStdErrTTY: c.gs.Stderr.IsTTY,
}

summaryResult, hsErr := test.initRunner.HandleSummary(globalCtx, nil, summary)
if hsErr == nil {
hsErr = handleSummaryResult(c.gs.FS, c.gs.Stdout, c.gs.Stderr, summaryResult)
}
if hsErr != nil {
logger.WithError(hsErr).Error("failed to handle the end-of-test summary")
}
}()
}
}

waitInitDone := emitEvent(&event.Event{Type: event.Init})
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ func TestThresholdsRuntimeBehavior(t *testing.T) {
name: "#2518: submetrics without values should be rendered under their parent metric #2518",
testFilename: "thresholds/thresholds_on_submetric_without_samples.js",
expExitCode: 0,
expStdoutContains: " one..................: 0 0/s\n { tag:xyz }........: 0 0/s\n",
expStdoutContains: " one....................................: 0 0/s\n { tag:xyz }..........................: 0 0/s\n",
},
{
name: "#2512: parsing threshold names containing parsable tokens should be valid",
Expand All @@ -337,7 +337,7 @@ func TestThresholdsRuntimeBehavior(t *testing.T) {
name: "#2520: thresholds over metrics without values should avoid division by zero and displaying NaN values",
testFilename: "thresholds/empty_sink_no_nan.js",
expExitCode: 0,
expStdoutContains: "rate.................: 0.00%",
expStdoutContains: "rate...................................: 0.00%",
expStdoutNotContains: "NaN",
},
}
Expand Down
117 changes: 72 additions & 45 deletions internal/cmd/runtime_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ extended: base + sets "global" as alias for "globalThis"
flags.StringArrayP("env", "e", nil, "add/override environment variable with `VAR=value`")
flags.Bool("no-thresholds", false, "don't run thresholds")
flags.Bool("no-summary", false, "don't show the summary at the end of the test")
flags.String("with-summary", lib.SummaryModeCompact.String(), "determine the summary mode,"+
" \"compact\", \"full\" or \"legacy\"")
flags.String(
"summary-export",
"",
Expand All @@ -41,94 +43,119 @@ extended: base + sets "global" as alias for "globalThis"
return flags
}

func saveBoolFromEnv(env map[string]string, varName string, placeholder *null.Bool) error {
strValue, ok := env[varName]
if !ok {
return nil
func getRuntimeOptions(
flags *pflag.FlagSet,
environment map[string]string,
) (lib.RuntimeOptions, error) {
// TODO: refactor with composable helpers as a part of #883, to reduce copy-paste
// TODO: get these options out of the JSON config file as well?
opts, err := populateRuntimeOptionsFromEnv(runtimeOptionsFromFlags(flags), environment)
if err != nil {
return opts, err
}
val, err := strconv.ParseBool(strValue)

// Set/overwrite environment variables with custom user-supplied values
envVars, err := flags.GetStringArray("env")
if err != nil {
return fmt.Errorf("env var '%s' is not a valid boolean value: %w", varName, err)
return opts, err
}
// Only override if not explicitly set via the CLI flag
if !placeholder.Valid {
*placeholder = null.BoolFrom(val)

for _, kv := range envVars {
k, v := state.ParseEnvKeyValue(kv)
// Allow only alphanumeric ASCII variable names for now
if !userEnvVarName.MatchString(k) {
return opts, fmt.Errorf("invalid environment variable name '%s'", k)
}
opts.Env[k] = v
}
return nil

return opts, nil
}

func getRuntimeOptions(flags *pflag.FlagSet, environment map[string]string) (lib.RuntimeOptions, error) {
// TODO: refactor with composable helpers as a part of #883, to reduce copy-paste
// TODO: get these options out of the JSON config file as well?
func runtimeOptionsFromFlags(flags *pflag.FlagSet) lib.RuntimeOptions {
opts := lib.RuntimeOptions{
TestType: getNullString(flags, "type"),
IncludeSystemEnvVars: getNullBool(flags, "include-system-env-vars"),
CompatibilityMode: getNullString(flags, "compatibility-mode"),
NoThresholds: getNullBool(flags, "no-thresholds"),
NoSummary: getNullBool(flags, "no-summary"),
SummaryMode: getNullString(flags, "with-summary"),
SummaryExport: getNullString(flags, "summary-export"),
TracesOutput: getNullString(flags, "traces-output"),
Env: make(map[string]string),
}
return opts
}

func populateRuntimeOptionsFromEnv(opts lib.RuntimeOptions, environment map[string]string) (lib.RuntimeOptions, error) {
// Only override if not explicitly set via the CLI flag

if envVar, ok := environment["K6_TYPE"]; ok && !opts.TestType.Valid {
// Only override if not explicitly set via the CLI flag
if envVar, ok := environment["K6_TYPE"]; !opts.TestType.Valid && ok {
opts.TestType = null.StringFrom(envVar)
}
if envVar, ok := environment["K6_COMPATIBILITY_MODE"]; ok && !opts.CompatibilityMode.Valid {
// Only override if not explicitly set via the CLI flag

if envVar, ok := environment["K6_COMPATIBILITY_MODE"]; !opts.CompatibilityMode.Valid && ok {
opts.CompatibilityMode = null.StringFrom(envVar)
}
if _, err := lib.ValidateCompatibilityMode(opts.CompatibilityMode.String); err != nil {
// some early validation
return opts, err

if envVar, ok := environment["K6_WITH_SUMMARY"]; !opts.SummaryMode.Valid && ok {
opts.SummaryMode = null.StringFrom(envVar)
}

if err := saveBoolFromEnv(environment, "K6_INCLUDE_SYSTEM_ENV_VARS", &opts.IncludeSystemEnvVars); err != nil {
return opts, err
}

if err := saveBoolFromEnv(environment, "K6_NO_THRESHOLDS", &opts.NoThresholds); err != nil {
return opts, err
}

if err := saveBoolFromEnv(environment, "K6_NO_SUMMARY", &opts.NoSummary); err != nil {
return opts, err
}

if envVar, ok := environment["K6_SUMMARY_EXPORT"]; ok {
if !opts.SummaryExport.Valid {
opts.SummaryExport = null.StringFrom(envVar)
}
if _, err := lib.ValidateCompatibilityMode(opts.CompatibilityMode.String); err != nil {
// some early validation
return opts, err
}

if envVar, ok := environment["SSLKEYLOGFILE"]; ok {
if !opts.KeyWriter.Valid {
opts.KeyWriter = null.StringFrom(envVar)
}
if _, err := lib.ValidateSummaryMode(opts.SummaryMode.String); err != nil {
// some early validation
return opts, err
}

if envVar, ok := environment["K6_TRACES_OUTPUT"]; ok {
if !opts.TracesOutput.Valid {
opts.TracesOutput = null.StringFrom(envVar)
}
if envVar, ok := environment["K6_SUMMARY_EXPORT"]; !opts.SummaryExport.Valid && ok {
opts.SummaryExport = null.StringFrom(envVar)
}

if opts.IncludeSystemEnvVars.Bool { // If enabled, gather the actual system environment variables
opts.Env = environment
if envVar, ok := environment["SSLKEYLOGFILE"]; !opts.KeyWriter.Valid && ok {
opts.KeyWriter = null.StringFrom(envVar)
}

// Set/overwrite environment variables with custom user-supplied values
envVars, err := flags.GetStringArray("env")
if err != nil {
return opts, err
if envVar, ok := environment["K6_TRACES_OUTPUT"]; !opts.TracesOutput.Valid && ok {
opts.TracesOutput = null.StringFrom(envVar)
}
for _, kv := range envVars {
k, v := state.ParseEnvKeyValue(kv)
// Allow only alphanumeric ASCII variable names for now
if !userEnvVarName.MatchString(k) {
return opts, fmt.Errorf("invalid environment variable name '%s'", k)
}
opts.Env[k] = v

// If enabled, gather the actual system environment variables
if opts.IncludeSystemEnvVars.Bool {
opts.Env = environment
}

return opts, nil
}

func saveBoolFromEnv(env map[string]string, varName string, placeholder *null.Bool) error {
strValue, ok := env[varName]
if !ok {
return nil
}
val, err := strconv.ParseBool(strValue)
if err != nil {
return fmt.Errorf("env var '%s' is not a valid boolean value: %w", varName, err)
}
// Only override if not explicitly set via the CLI flag
if !placeholder.Valid {
*placeholder = null.BoolFrom(val)
}
return nil
}
Loading
Loading