-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Add JS function to abort test #2093
Changes from all commits
3dd633d
c24e04e
4e21b36
b7ab4d2
1bab74e
52eed6b
8abac3e
dfab9bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,19 +22,28 @@ package cmd | |
|
||
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/sirupsen/logrus" | ||
"github.com/spf13/afero" | ||
"github.com/spf13/pflag" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
|
||
"go.k6.io/k6/errext" | ||
"go.k6.io/k6/errext/exitcodes" | ||
"go.k6.io/k6/js/common" | ||
"go.k6.io/k6/lib/fsext" | ||
"go.k6.io/k6/lib/testutils" | ||
) | ||
|
||
type mockWriter struct { | ||
|
@@ -125,3 +134,85 @@ func TestHandleSummaryResultError(t *testing.T) { | |
assertEqual(t, "file summary 1", files[filePath1]) | ||
assertEqual(t, "file summary 2", files[filePath2]) | ||
} | ||
|
||
func TestAbortTest(t *testing.T) { //nolint: tparallel | ||
t.Parallel() | ||
|
||
testCases := []struct { | ||
testFilename, expLogOutput string | ||
}{ | ||
{ | ||
testFilename: "abort.js", | ||
}, | ||
{ | ||
testFilename: "abort_initerr.js", | ||
}, | ||
{ | ||
testFilename: "abort_initvu.js", | ||
}, | ||
{ | ||
testFilename: "abort_teardown.js", | ||
expLogOutput: "Calling teardown function after test.abort()", | ||
}, | ||
Comment on lines
+145
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: for these really small testfiles I kind of feel like moving them to files just adds extra steps for whoever looks at the code. AFAIK the reason for this to be the case for other tests is that the testdata is quite ... big and hard to put in a string in the code, none of this is true for this couple of lines here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, but these tests test the |
||
} | ||
|
||
for _, tc := range testCases { //nolint: paralleltest | ||
tc := tc | ||
t.Run(tc.testFilename, func(t *testing.T) { | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
logger := logrus.New() | ||
logger.SetLevel(logrus.InfoLevel) | ||
logger.Out = ioutil.Discard | ||
hook := testutils.SimpleLogrusHook{ | ||
HookedLevels: []logrus.Level{logrus.InfoLevel}, | ||
} | ||
logger.AddHook(&hook) | ||
|
||
cmd := getRunCmd(ctx, logger) | ||
// Redefine the flag to avoid a nil pointer panic on lookup. | ||
cmd.Flags().AddFlag(&pflag.Flag{ | ||
Name: "address", | ||
Hidden: true, | ||
}) | ||
a, err := filepath.Abs(path.Join("testdata", tc.testFilename)) | ||
require.NoError(t, err) | ||
cmd.SetArgs([]string{a}) | ||
err = cmd.Execute() | ||
var e errext.HasExitCode | ||
require.ErrorAs(t, err, &e) | ||
assert.Equalf(t, exitcodes.ScriptAborted, e.ExitCode(), | ||
"Status code must be %d", exitcodes.ScriptAborted) | ||
assert.Contains(t, e.Error(), common.AbortTest) | ||
|
||
if tc.expLogOutput != "" { | ||
var gotMsg bool | ||
for _, entry := range hook.Drain() { | ||
if strings.Contains(entry.Message, tc.expLogOutput) { | ||
gotMsg = true | ||
break | ||
} | ||
} | ||
assert.True(t, gotMsg) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestInitErrExitCode(t *testing.T) { //nolint: paralleltest | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
logger := testutils.NewLogger(t) | ||
|
||
cmd := getRunCmd(ctx, logger) | ||
a, err := filepath.Abs("testdata/initerr.js") | ||
require.NoError(t, err) | ||
cmd.SetArgs([]string{a}) | ||
err = cmd.Execute() | ||
var e errext.HasExitCode | ||
require.ErrorAs(t, err, &e) | ||
assert.Equalf(t, exitcodes.ScriptException, e.ExitCode(), | ||
"Status code must be %d", exitcodes.ScriptException) | ||
assert.Contains(t, err.Error(), "ReferenceError: someUndefinedVar is not defined") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import exec from 'k6/execution'; | ||
|
||
export default function () { | ||
exec.test.abort(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import exec from 'k6/execution'; | ||
exec.test.abort(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import exec from 'k6/execution'; | ||
|
||
// This won't fail on initial parsing of the script, but on VU initialization. | ||
if (__VU == 1) { | ||
exec.test.abort(); | ||
} | ||
|
||
export default function() {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import exec from 'k6/execution'; | ||
|
||
export default function () { | ||
exec.test.abort(); | ||
} | ||
|
||
export function teardown() { | ||
console.log('Calling teardown function after test.abort()'); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
someUndefinedVar |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,7 +30,9 @@ import ( | |
"github.com/sirupsen/logrus" | ||
|
||
"go.k6.io/k6/errext" | ||
"go.k6.io/k6/js/common" | ||
"go.k6.io/k6/lib" | ||
"go.k6.io/k6/lib/executor" | ||
"go.k6.io/k6/lib/metrics" | ||
"go.k6.io/k6/stats" | ||
"go.k6.io/k6/ui/pb" | ||
|
@@ -346,7 +348,13 @@ func (e *ExecutionScheduler) Run( | |
executorsCount := len(e.executors) | ||
logger := e.logger.WithField("phase", "local-execution-scheduler-run") | ||
e.initProgress.Modify(pb.WithConstLeft("Run")) | ||
defer e.state.MarkEnded() | ||
var interrupted bool | ||
defer func() { | ||
e.state.MarkEnded() | ||
if interrupted { | ||
e.state.SetExecutionStatus(lib.ExecutionStatusInterrupted) | ||
} | ||
Comment on lines
+352
to
+356
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we get a comment that we on purpose first hit the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤷♂️ Not sure, doesn't the code speak for itself? |
||
}() | ||
|
||
if e.state.IsPaused() { | ||
logger.Debug("Execution is paused, waiting for resume or interrupt...") | ||
|
@@ -386,8 +394,14 @@ func (e *ExecutionScheduler) Run( | |
// Start all executors at their particular startTime in a separate goroutine... | ||
logger.Debug("Start all executors...") | ||
e.state.SetExecutionStatus(lib.ExecutionStatusRunning) | ||
|
||
// We are using this context to allow lib.Executor implementations to cancel | ||
// this context effectively stopping all executions. | ||
// | ||
// This is for addressing test.abort(). | ||
execCtx := executor.Context(runSubCtx) | ||
for _, exec := range e.executors { | ||
go e.runExecutor(runSubCtx, runResults, engineOut, exec, builtinMetrics) | ||
go e.runExecutor(execCtx, runResults, engineOut, exec, builtinMetrics) | ||
} | ||
|
||
// Wait for all executors to finish | ||
|
@@ -414,7 +428,10 @@ func (e *ExecutionScheduler) Run( | |
return err | ||
} | ||
} | ||
|
||
if err := executor.CancelReason(execCtx); err != nil && common.IsInterruptError(err) { | ||
interrupted = true | ||
return err | ||
} | ||
return firstErr | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/* | ||
* | ||
* k6 - a next-generation load testing tool | ||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. | ||
* | ||
*/ | ||
|
||
package common | ||
|
||
import ( | ||
"errors" | ||
|
||
"github.com/dop251/goja" | ||
"go.k6.io/k6/errext" | ||
"go.k6.io/k6/errext/exitcodes" | ||
) | ||
|
||
// InterruptError is an error that halts engine execution | ||
type InterruptError struct { | ||
Reason string | ||
} | ||
|
||
var _ errext.HasExitCode = &InterruptError{} | ||
|
||
// Error returns the reason of the interruption. | ||
func (i *InterruptError) Error() string { | ||
return i.Reason | ||
} | ||
|
||
// ExitCode returns the status code used when the k6 process exits. | ||
func (i *InterruptError) ExitCode() errext.ExitCode { | ||
return exitcodes.ScriptAborted | ||
} | ||
|
||
// AbortTest is the reason emitted when a test script calls test.abort() | ||
const AbortTest = "test aborted" | ||
|
||
// IsInterruptError returns true if err is *InterruptError. | ||
func IsInterruptError(err error) bool { | ||
if err == nil { | ||
return false | ||
} | ||
var intErr *InterruptError | ||
return errors.As(err, &intErr) | ||
} | ||
|
||
// UnwrapGojaInterruptedError returns the internal error handled by goja. | ||
func UnwrapGojaInterruptedError(err error) error { | ||
var gojaErr *goja.InterruptedError | ||
if errors.As(err, &gojaErr) { | ||
if e, ok := gojaErr.Value().(error); ok { | ||
return e | ||
} | ||
} | ||
return err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this
nolint
required 🤔There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can just add
t.Parallel
to the ones below? Or just split them in two separate tests?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, missed this originally.
To answer your first question: yes, it's needed, otherwise
tparallel
fails withTestAbortTest's subtests should call t.Parallel
. And bothtparallel
andparalleltest
are enabled by default... It's confusing, I know 😞 Maybe we should disabletparallel
globally?We can't add
t.Parallel()
to either test because of several data races... And I prefer having them as subtests in this case. :)