Skip to content
Open
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
155 changes: 39 additions & 116 deletions internal/cmd/tests/cmd_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3177,122 +3177,45 @@ func TestMachineReadableSummary(t *testing.T) {
assertSummaryExport := func(t *testing.T, out string) {
t.Helper()

// Configuration block: duration is dynamic.
configPattern := `"config": \{
"duration": [\d.]+,
"execution": "local",
"script": "(?:[^"\\]|\\.)*"
\}`
assert.Regexp(t, regexp.MustCompile(configPattern), out)

// Metadata block: generated_at is dynamic.
metadataPattern := `"metadata": \{
"generated_at": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(Z|[+-]\d{2}:\d{2})",
"k6_version": "` + regexp.QuoteMeta(build.Version) + `"
\}`
assert.Regexp(t, regexp.MustCompile(metadataPattern), out)

// Checks block: checks.metrics preserve the order.
checksPattern := `"checks": \{
"metrics": \[
\{
"contains": "default",
"name": "checks_total",
"type": "counter",
"values": \{
"count": 1
\}
\},
\{
"contains": "default",
"name": "checks_failed",
"type": "rate",
"values": \{
"matches": 0,
"rate": 0,
"total": 1
\}
\},
\{
"contains": "default",
"name": "checks_succeeded",
"type": "rate",
"values": \{
"matches": 1,
"rate": 1,
"total": 1
\}
\}
\],
"results": \[
\{
"fails": 0,
"name": "TRUE is TRUE",
"passes": 1
\}
\]
\}`
assert.Regexp(t, regexp.MustCompile(checksPattern), out)

// Metrics block: asserted individual because order may vary between executions.
iterationDurationPattern := `\{
"contains": "time",
"name": "iteration_duration",
"type": "trend",
"values": \{
"avg": [\d.]+,
"max": [\d.]+,
"med": [\d.]+,
"min": [\d.]+,
"p90": [\d.]+,
"p95": [\d.]+
\}
\}`
assert.Regexp(t, regexp.MustCompile(iterationDurationPattern), out)

iterationsPattern := `\{
"contains": "default",
"name": "iterations",
"type": "counter",
"values": \{
"count": 1
\}
\}`
assert.Regexp(t, regexp.MustCompile(iterationsPattern), out)

dataSentPattern := `\{
"contains": "data",
"name": "data_sent",
"type": "counter",
"values": \{
"count": 0
\}
\}`
assert.Regexp(t, regexp.MustCompile(dataSentPattern), out)

dataReceivedPattern := `\{
"contains": "data",
"name": "data_received",
"type": "counter",
"values": \{
"count": 0
\}
\}`
assert.Regexp(t, regexp.MustCompile(dataReceivedPattern), out)

customIterationsPattern := `\{
"contains": "default",
"name": "custom_iterations",
"type": "counter",
"values": \{
"count": 1
\}
\}`
assert.Regexp(t, regexp.MustCompile(customIterationsPattern), out)

// Schema version: statically defined, as changes in the schema will likely require changes in code.
versionPattern := `"version": "1\.0\.0"`
assert.Regexp(t, regexp.MustCompile(versionPattern), out)
assert.Equal(t, "local", gjson.Get(out, "config.execution").String())
assert.NotEmpty(t, gjson.Get(out, "config.script").String())
assert.True(t, gjson.Get(out, "config.duration").Float() >= 0)
assert.Equal(t, build.Version, gjson.Get(out, "metadata.k6Version").String())
assert.Regexp(
t,
regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(Z|[+-]\d{2}:\d{2})$`),
gjson.Get(out, "metadata.generatedAt").String(),
)

assert.Equal(t, int64(3), gjson.Get(out, `results.checks.metrics.#`).Int())
assert.Equal(t, "checks_total", gjson.Get(out, `results.checks.metrics.0.name`).String())
assert.Equal(t, int64(1), gjson.Get(out, `results.checks.metrics.0.values.count`).Int())
assert.Equal(t, "checks_failed", gjson.Get(out, `results.checks.metrics.1.name`).String())
assert.Equal(t, float64(0), gjson.Get(out, `results.checks.metrics.1.values.matches`).Float())
assert.Equal(t, float64(0), gjson.Get(out, `results.checks.metrics.1.values.rate`).Float())
assert.Equal(t, float64(1), gjson.Get(out, `results.checks.metrics.1.values.total`).Float())
assert.Equal(t, "checks_succeeded", gjson.Get(out, `results.checks.metrics.2.name`).String())
assert.Equal(t, float64(1), gjson.Get(out, `results.checks.metrics.2.values.matches`).Float())
assert.Equal(t, float64(1), gjson.Get(out, `results.checks.metrics.2.values.rate`).Float())
assert.Equal(t, float64(1), gjson.Get(out, `results.checks.metrics.2.values.total`).Float())

assert.Equal(t, int64(1), gjson.Get(out, `results.checks.results.#`).Int())
assert.Equal(t, "TRUE is TRUE", gjson.Get(out, `results.checks.results.0.name`).String())
assert.Equal(t, int64(1), gjson.Get(out, `results.checks.results.0.passes`).Int())
assert.Equal(t, int64(0), gjson.Get(out, `results.checks.results.0.fails`).Int())

assert.True(t, gjson.Get(out, `results.metrics.#(name=="iteration_duration").values.avg`).Exists())
assert.True(t, gjson.Get(out, `results.metrics.#(name=="iteration_duration").values.max`).Exists())
assert.True(t, gjson.Get(out, `results.metrics.#(name=="iteration_duration").values.med`).Exists())
assert.True(t, gjson.Get(out, `results.metrics.#(name=="iteration_duration").values.min`).Exists())
assert.True(t, gjson.Get(out, `results.metrics.#(name=="iteration_duration").values.p\(90\)`).Exists())
assert.True(t, gjson.Get(out, `results.metrics.#(name=="iteration_duration").values.p\(95\)`).Exists())
assert.Equal(t, int64(1), gjson.Get(out, `results.metrics.#(name=="iterations").values.count`).Int())
assert.Equal(t, int64(0), gjson.Get(out, `results.metrics.#(name=="data_sent").values.count`).Int())
assert.Equal(t, int64(0), gjson.Get(out, `results.metrics.#(name=="data_received").values.count`).Int())
assert.Equal(t, int64(1), gjson.Get(out, `results.metrics.#(name=="custom_iterations").values.count`).Int())

assert.Equal(t, "1.0.0", gjson.Get(out, "version").String())
}

t.Run("--summary-export", func(t *testing.T) {
Expand Down
5 changes: 3 additions & 2 deletions internal/js/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,9 @@ func prepareHandleSummaryCall(

if s.NewMachineReadableSummary {
mrSummary, mrsErr := summary.ToMachineReadable(s, meta)
handleSummaryData = rt.ToValue(mrSummary)
err = errors.Join(srErr, mrsErr)
jsonCompatibleSummary, jsonCompatibleErr := toJSONCompatibleValue(mrSummary)
handleSummaryData = rt.ToValue(jsonCompatibleSummary)
err = errors.Join(srErr, mrsErr, jsonCompatibleErr)
} else {
handleSummaryData = rt.ToValue(summarizeMetricsToObject(legacy, r.Bundle.Options, r.setupData))
}
Expand Down
17 changes: 17 additions & 0 deletions internal/js/summary.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,23 @@ func summarizeMetricsToObject(data *lib.LegacySummary, options lib.Options, setu
return m
}

// toJSONCompatibleValue converts a Go value into a JSON-compatible structure made only of
// maps, slices, numbers, strings, booleans, and nils. This preserves JSON tags when passing
// data through the JS runtime, which otherwise uses the Sobek field mapper for structs.
func toJSONCompatibleValue(v any) (any, error) {
raw, err := json.Marshal(v)
if err != nil {
return nil, err
}

var decoded any
if err := json.Unmarshal(raw, &decoded); err != nil {
return nil, err
}

return decoded, nil
}

func exportGroup(group *lib.Group) map[string]any {
subGroups := make([]map[string]any, len(group.OrderedGroups))
for i, subGroup := range group.OrderedGroups {
Expand Down
44 changes: 44 additions & 0 deletions internal/js/summary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,50 @@ func TestRawHandleSummaryPromise(t *testing.T) {
assert.JSONEq(t, expectedHandleSummaryDataWithSetup, string(dataWithSetup))
}

func TestMachineReadableHandleSummaryUsesCamelCaseKeys(t *testing.T) {
t.Parallel()

runner, err := getSimpleRunner(
t, "/script.js",
`
exports.default = function() { /* we don't run this, metrics are mocked */ };
exports.handleSummary = function(data) {
return {'rawdata.json': JSON.stringify(data, null, 4)};
};
`,
lib.RuntimeOptions{
CompatibilityMode: null.NewString("base", true),
SummaryExport: null.StringFrom("summary.json"),
},
)
require.NoError(t, err)

s := summary.New()
s.NewMachineReadableSummary = true
s.TestRunDuration = time.Second

result, err := runner.HandleSummary(t.Context(), nil, s, summary.Meta{Script: "export default function() {}\n"})
require.NoError(t, err)

rawData, err := io.ReadAll(result["rawdata.json"])
require.NoError(t, err)
rawDataString := string(rawData)
assert.Contains(t, rawDataString, `"generatedAt"`)
assert.Contains(t, rawDataString, `"k6Version"`)
assert.NotContains(t, rawDataString, `"generated_at"`)
assert.NotContains(t, rawDataString, `"k6_version"`)

summaryExport, err := io.ReadAll(result["summary.json"])
require.NoError(t, err)
summaryExportString := string(summaryExport)
assert.Contains(t, summaryExportString, `"generatedAt"`)
assert.Contains(t, summaryExportString, `"k6Version"`)
assert.NotContains(t, summaryExportString, `"generated_at"`)
assert.NotContains(t, summaryExportString, `"k6_version"`)
assert.Contains(t, summaryExportString, `"script": "export default function() {}\n"`)
assert.Contains(t, summaryExportString, `"version": "1.0.0"`)
}

func TestWrongSummaryHandlerExportTypes(t *testing.T) {
t.Parallel()
testCases := []string{"{}", `"foo"`, "null", "undefined", "123"}
Expand Down
Loading