diff --git a/cmd/run.go b/cmd/run.go index a1c2b7fd0b4..81f05cce4ae 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -299,7 +299,7 @@ a commandline interface for interacting with it.`, TestRunDuration: executionState.GetCurrentTestRunDuration(), }) if err == nil { - err = handleSummaryResult(afero.NewOsFs(), os.Stdout, os.Stderr, summaryResult) + err = handleSummaryResult(afero.NewOsFs(), stdout, stderr, summaryResult) } if err != nil { logger.WithError(err).Error("failed to handle the end-of-test summary") diff --git a/cmd/run_test.go b/cmd/run_test.go new file mode 100644 index 00000000000..d4ea327728d --- /dev/null +++ b/cmd/run_test.go @@ -0,0 +1,126 @@ +/* + * + * k6 - a next-generation load testing tool + * Copyright (C) 2020 Load Impact + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package cmd + +import ( + "bytes" + "errors" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/loadimpact/k6/lib/fsext" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockWriter struct { + err error + errAfter int +} + +func (fw mockWriter) Write(p []byte) (n int, err error) { + if fw.err != nil { + return fw.errAfter, fw.err + } + return len(p), nil +} + +var _ io.Writer = mockWriter{} + +func getFiles(t *testing.T, fs afero.Fs) map[string]*bytes.Buffer { + result := map[string]*bytes.Buffer{} + walkFn := func(filePath string, info os.FileInfo, err error) error { + if filePath == "/" || filePath == "\\" { + return nil + } + require.NoError(t, err) + contents, err := afero.ReadFile(fs, filePath) + require.NoError(t, err) + result[filePath] = bytes.NewBuffer(contents) + return nil + } + + err := fsext.Walk(fs, afero.FilePathSeparator, filepath.WalkFunc(walkFn)) + require.NoError(t, err) + + return result +} + +func assertEqual(t *testing.T, exp string, actual io.Reader) { + act, err := ioutil.ReadAll(actual) + require.NoError(t, err) + assert.Equal(t, []byte(exp), act) +} + +func initVars() ( + content map[string]io.Reader, stdout *bytes.Buffer, stderr *bytes.Buffer, fs afero.Fs, +) { + return map[string]io.Reader{}, bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{}), afero.NewMemMapFs() +} + +func TestHandleSummaryResultSimple(t *testing.T) { + t.Parallel() + content, stdout, stderr, fs := initVars() + + // Test noop + assert.NoError(t, handleSummaryResult(fs, stdout, stderr, content)) + require.Empty(t, getFiles(t, fs)) + require.Empty(t, stdout.Bytes()) + require.Empty(t, stderr.Bytes()) + + // Test stdout only + content["stdout"] = bytes.NewBufferString("some stdout summary") + assert.NoError(t, handleSummaryResult(fs, stdout, stderr, content)) + require.Empty(t, getFiles(t, fs)) + assertEqual(t, "some stdout summary", stdout) + require.Empty(t, stderr.Bytes()) +} + +func TestHandleSummaryResultError(t *testing.T) { + t.Parallel() + content, _, stderr, fs := initVars() + + expErr := errors.New("test error") + stdout := mockWriter{err: expErr, errAfter: 10} + + filePath1 := "/path/file1" + filePath2 := "/path/file2" + if runtime.GOOS == "windows" { + filePath1 = "\\path\\file1" + filePath2 = "\\path\\file2" + } + + content["stdout"] = bytes.NewBufferString("some stdout summary") + content["stderr"] = bytes.NewBufferString("some stderr summary") + content[filePath1] = bytes.NewBufferString("file summary 1") + content[filePath2] = bytes.NewBufferString("file summary 2") + err := handleSummaryResult(fs, stdout, stderr, content) + assert.Error(t, err) + assert.Contains(t, err.Error(), expErr.Error()) + files := getFiles(t, fs) + assertEqual(t, "file summary 1", files[filePath1]) + assertEqual(t, "file summary 2", files[filePath2]) +} diff --git a/js/summary.go b/js/summary.go index dc89e856713..aaf00b69c31 100644 --- a/js/summary.go +++ b/js/summary.go @@ -36,10 +36,12 @@ import ( // TODO: move this to a separate JS file and use go.rice to embed it const summaryWrapperLambdaCode = ` (function() { - var forEach = function(obj, callback) { + var forEach = function (obj, callback) { for (var key in obj) { if (obj.hasOwnProperty(key)) { - callback(key, obj[key]); + if (callback(key, obj[key])) { + break; + } } } } @@ -88,9 +90,7 @@ const summaryWrapperLambdaCode = ` return JSON.stringify(results, null, 4); }; - var oldTextSummary = function(data) { - // TODO: implement something like the current end of test summary - }; + // TODO: bundle the text summary generation from jslib and get rid of oldCallback return function(exportedSummaryCallback, jsonSummaryPath, data, oldCallback) { var result = {}; @@ -99,12 +99,10 @@ const summaryWrapperLambdaCode = ` result = exportedSummaryCallback(data, oldCallback); } catch (e) { console.error('handleSummary() failed with error "' + e + '", falling back to the default summary'); - //result["stdout"] = oldTextSummary(data); - result["stdout"] = oldCallback(); // TODO: delete + result["stdout"] = oldCallback(); // TODO: replace with JS function } } else { - // result["stdout"] = oldTextSummary(data); - result["stdout"] = oldCallback(); // TODO: delete + result["stdout"] = oldCallback(); // TODO: replace with JS function } // TODO: ensure we're returning a map of strings or null/undefined... diff --git a/stats/stats.go b/stats/stats.go index 350b2dd0b69..e1487d4eaac 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -474,6 +474,8 @@ var unitMap = map[string][]interface{}{ "us": {"µs", time.Microsecond}, } +// HumanizeValue makes the value human-readable +// TODO: get rid of this after we remove the Go-based summary func (m *Metric) HumanizeValue(v float64, timeUnit string) string { switch m.Type { case Rate: