diff --git a/.golangci.yml b/.golangci.yml index 9d0fdc98773..956ab758f2a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -38,6 +38,8 @@ linters-settings: min-confidence: 0 gocyclo: min-complexity: 25 + cyclop: #TODO: see https://github.com/grafana/k6/issues/2294, leave only one of these? + max-complexity: 25 maligned: suggest-new: true dupl: diff --git a/js/modules/k6/html/element_test.go b/js/modules/k6/html/element_test.go index 58cb3f15da0..354ce175f70 100644 --- a/js/modules/k6/html/element_test.go +++ b/js/modules/k6/html/element_test.go @@ -21,13 +21,11 @@ package html import ( - "context" "testing" "github.com/dop251/goja" "github.com/stretchr/testify/assert" - - "go.k6.io/k6/js/common" + "github.com/stretchr/testify/require" ) const testHTMLElem = ` @@ -55,17 +53,13 @@ const testHTMLElem = ` ` func TestElement(t *testing.T) { - rt := goja.New() - rt.SetFieldNameMapper(common.FieldNameMapper{}) - - ctx := common.WithRuntime(context.Background(), rt) - rt.Set("src", testHTMLElem) - rt.Set("html", common.Bind(rt, &HTML{}, &ctx)) - // compileProtoElem() + t.Parallel() + rt, _ := getTestModuleInstance(t) + require.NoError(t, rt.Set("src", testHTMLElem)) _, err := rt.RunString(`var doc = html.parseHTML(src)`) - assert.NoError(t, err) + require.NoError(t, err) assert.IsType(t, Selection{}, rt.Get("doc").Export()) t.Run("NodeName", func(t *testing.T) { diff --git a/js/modules/k6/html/elements_gen_test.go b/js/modules/k6/html/elements_gen_test.go index 6d275d598c1..ff3d20466b0 100644 --- a/js/modules/k6/html/elements_gen_test.go +++ b/js/modules/k6/html/elements_gen_test.go @@ -21,13 +21,10 @@ package html import ( - "context" "testing" - "github.com/dop251/goja" "github.com/stretchr/testify/assert" - - "go.k6.io/k6/js/common" + "github.com/stretchr/testify/require" ) var textTests = []struct { @@ -404,16 +401,13 @@ const testGenElems = ` ` func TestGenElements(t *testing.T) { - rt := goja.New() - rt.SetFieldNameMapper(common.FieldNameMapper{}) - - ctx := common.WithRuntime(context.Background(), rt) - rt.Set("src", testGenElems) - rt.Set("html", common.Bind(rt, &HTML{}, &ctx)) + t.Parallel() + rt, mi := getTestModuleInstance(t) + require.NoError(t, rt.Set("src", testGenElems)) _, err := rt.RunString("var doc = html.parseHTML(src)") - assert.NoError(t, err) + require.NoError(t, err) assert.IsType(t, Selection{}, rt.Get("doc").Export()) t.Run("Test text properties", func(t *testing.T) { @@ -468,8 +462,7 @@ func TestGenElements(t *testing.T) { }) t.Run("Test url properties", func(t *testing.T) { - html := HTML{} - sel, parseError := html.ParseHTML(ctx, testGenElems) + sel, parseError := mi.parseHTML(testGenElems) if parseError != nil { t.Errorf("Unable to parse html") } diff --git a/js/modules/k6/html/elements_test.go b/js/modules/k6/html/elements_test.go index 8244265573d..51a5f54a6ea 100644 --- a/js/modules/k6/html/elements_test.go +++ b/js/modules/k6/html/elements_test.go @@ -21,13 +21,11 @@ package html import ( - "context" "testing" "github.com/dop251/goja" "github.com/stretchr/testify/assert" - - "go.k6.io/k6/js/common" + "github.com/stretchr/testify/require" ) const testHTMLElems = ` @@ -86,16 +84,13 @@ const testHTMLElems = ` ` func TestElements(t *testing.T) { - rt := goja.New() - rt.SetFieldNameMapper(common.FieldNameMapper{}) - - ctx := common.WithRuntime(context.Background(), rt) - rt.Set("src", testHTMLElems) - rt.Set("html", common.Bind(rt, &HTML{}, &ctx)) + t.Parallel() + rt, _ := getTestModuleInstance(t) + require.NoError(t, rt.Set("src", testHTMLElems)) _, err := rt.RunString(`var doc = html.parseHTML(src)`) - assert.NoError(t, err) + require.NoError(t, err) assert.IsType(t, Selection{}, rt.Get("doc").Export()) t.Run("AnchorElement", func(t *testing.T) { diff --git a/js/modules/k6/html/html.go b/js/modules/k6/html/html.go index cc902b33d0c..0aa22324b62 100644 --- a/js/modules/k6/html/html.go +++ b/js/modules/k6/html/html.go @@ -21,7 +21,6 @@ package html import ( - "context" "errors" "fmt" "strings" @@ -31,20 +30,62 @@ import ( gohtml "golang.org/x/net/html" "go.k6.io/k6/js/common" + "go.k6.io/k6/js/modules" ) -type HTML struct{} +// RootModule is the global module object type. It is instantiated once per test +// run and will be used to create k6/html module instances for each VU. +type RootModule struct{} -func New() *HTML { - return &HTML{} +// ModuleInstance represents an instance of the HTML module for every VU. +type ModuleInstance struct { + vu modules.VU + rootModule *RootModule + exports *goja.Object } -func (HTML) ParseHTML(ctx context.Context, src string) (Selection, error) { +var ( + _ modules.Module = &RootModule{} + _ modules.Instance = &ModuleInstance{} +) + +// New returns a pointer to a new HTML RootModule. +func New() *RootModule { + return &RootModule{} +} + +// Exports returns the JS values this module exports. +func (mi *ModuleInstance) Exports() modules.Exports { + return modules.Exports{ + Default: mi.exports, + } +} + +// NewModuleInstance returns an HTML module instance for each VU. +func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance { + rt := vu.Runtime() + mi := &ModuleInstance{ + vu: vu, + rootModule: r, + exports: rt.NewObject(), + } + if err := mi.exports.Set("parseHTML", mi.parseHTML); err != nil { + common.Throw(rt, err) + } + return mi +} + +func (mi *ModuleInstance) parseHTML(src string) (Selection, error) { + return ParseHTML(mi.vu.Runtime(), src) +} + +// ParseHTML parses the provided HTML source into a Selection object. +func ParseHTML(rt *goja.Runtime, src string) (Selection, error) { doc, err := goquery.NewDocumentFromReader(strings.NewReader(src)) if err != nil { return Selection{}, err } - return Selection{rt: common.GetRuntime(ctx), sel: doc.Selection}, nil + return Selection{rt: rt, sel: doc.Selection}, nil } type Selection struct { diff --git a/js/modules/k6/html/html_test.go b/js/modules/k6/html/html_test.go index 5c8f8e20d86..6f1c6797a87 100644 --- a/js/modules/k6/html/html_test.go +++ b/js/modules/k6/html/html_test.go @@ -26,8 +26,11 @@ import ( "github.com/dop251/goja" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.k6.io/k6/js/common" + "go.k6.io/k6/js/modulestest" + "go.k6.io/k6/lib/metrics" ) const testHTML = ` @@ -63,18 +66,42 @@ const testHTML = ` ` -func TestParseHTML(t *testing.T) { +func getTestModuleInstance(t testing.TB) (*goja.Runtime, *ModuleInstance) { rt := goja.New() rt.SetFieldNameMapper(common.FieldNameMapper{}) - ctx := common.WithRuntime(context.Background(), rt) - rt.Set("src", testHTML) - rt.Set("html", common.Bind(rt, New(), &ctx)) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + root := New() + mockVU := &modulestest.VU{ + RuntimeField: rt, + InitEnvField: &common.InitEnvironment{ + Registry: metrics.NewRegistry(), + }, + CtxField: ctx, + StateField: nil, + } + mi, ok := root.NewModuleInstance(mockVU).(*ModuleInstance) + require.True(t, ok) + + require.NoError(t, rt.Set("html", mi.Exports().Default)) + + return rt, mi +} + +// TODO: split apart? +// nolint: cyclop, tparallel +func TestParseHTML(t *testing.T) { + t.Parallel() + rt, _ := getTestModuleInstance(t) + require.NoError(t, rt.Set("src", testHTML)) // TODO: I literally cannot think of a snippet that makes goquery error. // I'm not sure if it's even possible without like, an invalid reader or something, which would // be impossible to cause from the JS side. _, err := rt.RunString(`var doc = html.parseHTML(src)`) - assert.NoError(t, err) + require.NoError(t, err) assert.IsType(t, Selection{}, rt.Get("doc").Export()) t.Run("Find", func(t *testing.T) { diff --git a/js/modules/k6/html/serialize_test.go b/js/modules/k6/html/serialize_test.go index 04b2c2524ac..0517b1200a7 100644 --- a/js/modules/k6/html/serialize_test.go +++ b/js/modules/k6/html/serialize_test.go @@ -21,13 +21,11 @@ package html import ( - "context" "testing" "github.com/dop251/goja" "github.com/stretchr/testify/assert" - - "go.k6.io/k6/js/common" + "github.com/stretchr/testify/require" ) const testSerializeHTML = ` @@ -69,14 +67,12 @@ const testSerializeHTML = ` ` func TestSerialize(t *testing.T) { - rt := goja.New() - rt.SetFieldNameMapper(common.FieldNameMapper{}) - ctx := common.WithRuntime(context.Background(), rt) - rt.Set("src", testSerializeHTML) - rt.Set("html", common.Bind(rt, New(), &ctx)) + t.Parallel() + rt, _ := getTestModuleInstance(t) + require.NoError(t, rt.Set("src", testSerializeHTML)) _, err := rt.RunString(`var doc = html.parseHTML(src)`) - assert.NoError(t, err) + require.NoError(t, err) assert.IsType(t, Selection{}, rt.Get("doc").Export()) t.Run("SerializeArray", func(t *testing.T) { diff --git a/js/modules/k6/http/cookiejar.go b/js/modules/k6/http/cookiejar.go index 1ccebf38450..108f14dd6c2 100644 --- a/js/modules/k6/http/cookiejar.go +++ b/js/modules/k6/http/cookiejar.go @@ -21,7 +21,6 @@ package http import ( - "context" "fmt" "net/http" "net/http/cookiejar" @@ -30,27 +29,23 @@ import ( "time" "github.com/dop251/goja" - "go.k6.io/k6/js/common" ) -// HTTPCookieJar is cookiejar.Jar wrapper to be used in js scripts -type HTTPCookieJar struct { - // js is to make it not be accessible from inside goja/js, the json is because it's used if we return it from setup - Jar *cookiejar.Jar `js:"-" json:"-"` - ctx *context.Context -} +// ErrJarForbiddenInInitContext is used when a cookie jar was made in the init context +// TODO: unexport this? there's no reason for this to be exported +var ErrJarForbiddenInInitContext = common.NewInitContextError("Making cookie jars in the init context is not supported") -func newCookieJar(ctxPtr *context.Context) *HTTPCookieJar { - jar, err := cookiejar.New(nil) - if err != nil { - common.Throw(common.GetRuntime(*ctxPtr), err) - } - return &HTTPCookieJar{jar, ctxPtr} +// CookieJar is cookiejar.Jar wrapper to be used in js scripts +type CookieJar struct { + moduleInstance *ModuleInstance + // js is to make it not be accessible from inside goja/js, the json is + // for when it is returned from setup(). + Jar *cookiejar.Jar `js:"-" json:"-"` } // CookiesForURL return the cookies for a given url as a map of key and values -func (j HTTPCookieJar) CookiesForURL(url string) map[string][]string { +func (j CookieJar) CookiesForURL(url string) map[string][]string { u, err := neturl.Parse(url) if err != nil { panic(err) @@ -65,8 +60,8 @@ func (j HTTPCookieJar) CookiesForURL(url string) map[string][]string { } // Set sets a cookie for a particular url with the given name value and additional opts -func (j HTTPCookieJar) Set(url, name, value string, opts goja.Value) (bool, error) { - rt := common.GetRuntime(*j.ctx) +func (j CookieJar) Set(url, name, value string, opts goja.Value) (bool, error) { + rt := j.moduleInstance.vu.Runtime() u, err := neturl.Parse(url) if err != nil { diff --git a/js/modules/k6/http/file.go b/js/modules/k6/http/file.go index 89ab236007f..03d438e9a35 100644 --- a/js/modules/k6/http/file.go +++ b/js/modules/k6/http/file.go @@ -21,7 +21,6 @@ package http import ( - "context" "fmt" "strings" "time" @@ -42,8 +41,8 @@ func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } -// File returns a FileData parameter -func (h *HTTP) File(ctx context.Context, data interface{}, args ...string) FileData { +// File returns a FileData object. +func (mi *ModuleInstance) file(data interface{}, args ...string) FileData { // supply valid default if filename and content-type are not specified fname, ct := fmt.Sprintf("%d", time.Now().UnixNano()), "application/octet-stream" @@ -57,7 +56,7 @@ func (h *HTTP) File(ctx context.Context, data interface{}, args ...string) FileD dt, err := common.ToBytes(data) if err != nil { - common.Throw(common.GetRuntime(ctx), err) + common.Throw(mi.vu.Runtime(), err) } return FileData{ diff --git a/js/modules/k6/http/file_test.go b/js/modules/k6/http/file_test.go index f37c7f367b7..ba22524e026 100644 --- a/js/modules/k6/http/file_test.go +++ b/js/modules/k6/http/file_test.go @@ -21,20 +21,17 @@ package http import ( - "context" "fmt" "testing" "github.com/dop251/goja" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "go.k6.io/k6/js/common" ) func TestHTTPFile(t *testing.T) { t.Parallel() - rt := goja.New() + rt, mi := getTestModuleInstance(t, nil, nil) input := []byte{104, 101, 108, 108, 111} testCases := []struct { @@ -79,9 +76,7 @@ func TestHTTPFile(t *testing.T) { require.EqualError(t, val.(error), tc.expErr) }() } - h := new(GlobalHTTP).NewModuleInstancePerVU().(*HTTP) - ctx := common.WithRuntime(context.Background(), rt) - out := h.File(ctx, tc.input, tc.args...) + out := mi.file(tc.input, tc.args...) assert.Equal(t, tc.expected, out) }) } diff --git a/js/modules/k6/http/http.go b/js/modules/k6/http/http.go index 24bc72e9f9b..91679976dc5 100644 --- a/js/modules/k6/http/http.go +++ b/js/modules/k6/http/http.go @@ -21,101 +21,159 @@ package http import ( - "context" + "net/http" + "net/http/cookiejar" + "github.com/dop251/goja" "go.k6.io/k6/js/common" - "go.k6.io/k6/lib" + "go.k6.io/k6/js/modules" "go.k6.io/k6/lib/netext" "go.k6.io/k6/lib/netext/httpext" ) -const ( - HTTP_METHOD_GET = "GET" - HTTP_METHOD_POST = "POST" - HTTP_METHOD_PUT = "PUT" - HTTP_METHOD_DELETE = "DELETE" - HTTP_METHOD_HEAD = "HEAD" - HTTP_METHOD_PATCH = "PATCH" - HTTP_METHOD_OPTIONS = "OPTIONS" -) +// RootModule is the global module object type. It is instantiated once per test +// run and will be used to create HTTP module instances for each VU. +// +// TODO: add sync.Once for all of the deprecation warnings we might want to do +// for the old k6/http APIs here, so they are shown only once in a test run. +type RootModule struct{} + +// ModuleInstance represents an instance of the HTTP module for every VU. +type ModuleInstance struct { + vu modules.VU + rootModule *RootModule + defaultClient *Client + exports *goja.Object +} -// ErrJarForbiddenInInitContext is used when a cookie jar was made in the init context -var ErrJarForbiddenInInitContext = common.NewInitContextError("Making cookie jars in the init context is not supported") +var ( + _ modules.Module = &RootModule{} + _ modules.Instance = &ModuleInstance{} +) -// New returns a new global module instance -func New() *GlobalHTTP { - return &GlobalHTTP{} +// New returns a pointer to a new HTTP RootModule. +func New() *RootModule { + return &RootModule{} } -// GlobalHTTP is a global HTTP module for a k6 instance/test run -type GlobalHTTP struct{} - -// NewModuleInstancePerVU returns an HTTP instance for each VU -func (g *GlobalHTTP) NewModuleInstancePerVU() interface{} { // this here needs to return interface{} - return &HTTP{ // change the below fields to be not writable or not fields - TLS_1_0: netext.TLS_1_0, - TLS_1_1: netext.TLS_1_1, - TLS_1_2: netext.TLS_1_2, - TLS_1_3: netext.TLS_1_3, - OCSP_STATUS_GOOD: netext.OCSP_STATUS_GOOD, - OCSP_STATUS_REVOKED: netext.OCSP_STATUS_REVOKED, - OCSP_STATUS_SERVER_FAILED: netext.OCSP_STATUS_SERVER_FAILED, - OCSP_STATUS_UNKNOWN: netext.OCSP_STATUS_UNKNOWN, - OCSP_REASON_UNSPECIFIED: netext.OCSP_REASON_UNSPECIFIED, - OCSP_REASON_KEY_COMPROMISE: netext.OCSP_REASON_KEY_COMPROMISE, - OCSP_REASON_CA_COMPROMISE: netext.OCSP_REASON_CA_COMPROMISE, - OCSP_REASON_AFFILIATION_CHANGED: netext.OCSP_REASON_AFFILIATION_CHANGED, - OCSP_REASON_SUPERSEDED: netext.OCSP_REASON_SUPERSEDED, - OCSP_REASON_CESSATION_OF_OPERATION: netext.OCSP_REASON_CESSATION_OF_OPERATION, - OCSP_REASON_CERTIFICATE_HOLD: netext.OCSP_REASON_CERTIFICATE_HOLD, - OCSP_REASON_REMOVE_FROM_CRL: netext.OCSP_REASON_REMOVE_FROM_CRL, - OCSP_REASON_PRIVILEGE_WITHDRAWN: netext.OCSP_REASON_PRIVILEGE_WITHDRAWN, - OCSP_REASON_AA_COMPROMISE: netext.OCSP_REASON_AA_COMPROMISE, +// NewModuleInstance returns an HTTP module instance for each VU. +func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance { + rt := vu.Runtime() + mi := &ModuleInstance{ + vu: vu, + rootModule: r, + exports: rt.NewObject(), + } + mi.defineConstants() + mi.defaultClient = &Client{ + // TODO: configure this from lib.Options and get rid of some of the + // things in the VU State struct that should be here. See + // https://github.com/grafana/k6/issues/2293 + moduleInstance: mi, responseCallback: defaultExpectedStatuses.match, } + + mustExport := func(name string, value interface{}) { + if err := mi.exports.Set(name, value); err != nil { + common.Throw(rt, err) + } + } + + mustExport("url", mi.URL) + mustExport("CookieJar", mi.newCookieJar) + mustExport("cookieJar", mi.getVUCookieJar) + mustExport("file", mi.file) // TODO: deprecate or refactor? + + // TODO: refactor so the Client actually has better APIs and these are + // wrappers (facades) that convert the old k6 idiosyncratic APIs to the new + // proper Client ones that accept Request objects and don't suck + mustExport("get", func(url goja.Value, args ...goja.Value) (*Response, error) { + args = append([]goja.Value{goja.Undefined()}, args...) // sigh + return mi.defaultClient.Request(http.MethodGet, url, args...) + }) + mustExport("head", mi.defaultClient.getMethodClosure(http.MethodHead)) + mustExport("post", mi.defaultClient.getMethodClosure(http.MethodPost)) + mustExport("put", mi.defaultClient.getMethodClosure(http.MethodPut)) + mustExport("patch", mi.defaultClient.getMethodClosure(http.MethodPatch)) + mustExport("del", mi.defaultClient.getMethodClosure(http.MethodDelete)) + mustExport("options", mi.defaultClient.getMethodClosure(http.MethodOptions)) + mustExport("request", mi.defaultClient.Request) + mustExport("batch", mi.defaultClient.Batch) + mustExport("setResponseCallback", mi.defaultClient.SetResponseCallback) + + mustExport("expectedStatuses", mi.expectedStatuses) // TODO: refactor? + + // TODO: actually expose the default client as k6/http.defaultClient when we + // have a better HTTP API (e.g. proper Client constructor, an actual Request + // object, custom Transport implementations you can pass the Client, etc.). + // This will allow us to find solutions to many of the issues with the + // current HTTP API that plague us: + // https://github.com/grafana/k6/issues?q=is%3Aopen+is%3Aissue+label%3Anew-http + + return mi } -//nolint: golint -type HTTP struct { - TLS_1_0 string `js:"TLS_1_0"` - TLS_1_1 string `js:"TLS_1_1"` - TLS_1_2 string `js:"TLS_1_2"` - TLS_1_3 string `js:"TLS_1_3"` - OCSP_STATUS_GOOD string `js:"OCSP_STATUS_GOOD"` - OCSP_STATUS_REVOKED string `js:"OCSP_STATUS_REVOKED"` - OCSP_STATUS_SERVER_FAILED string `js:"OCSP_STATUS_SERVER_FAILED"` - OCSP_STATUS_UNKNOWN string `js:"OCSP_STATUS_UNKNOWN"` - OCSP_REASON_UNSPECIFIED string `js:"OCSP_REASON_UNSPECIFIED"` - OCSP_REASON_KEY_COMPROMISE string `js:"OCSP_REASON_KEY_COMPROMISE"` - OCSP_REASON_CA_COMPROMISE string `js:"OCSP_REASON_CA_COMPROMISE"` - OCSP_REASON_AFFILIATION_CHANGED string `js:"OCSP_REASON_AFFILIATION_CHANGED"` - OCSP_REASON_SUPERSEDED string `js:"OCSP_REASON_SUPERSEDED"` - OCSP_REASON_CESSATION_OF_OPERATION string `js:"OCSP_REASON_CESSATION_OF_OPERATION"` - OCSP_REASON_CERTIFICATE_HOLD string `js:"OCSP_REASON_CERTIFICATE_HOLD"` - OCSP_REASON_REMOVE_FROM_CRL string `js:"OCSP_REASON_REMOVE_FROM_CRL"` - OCSP_REASON_PRIVILEGE_WITHDRAWN string `js:"OCSP_REASON_PRIVILEGE_WITHDRAWN"` - OCSP_REASON_AA_COMPROMISE string `js:"OCSP_REASON_AA_COMPROMISE"` +// Exports returns the JS values this module exports. +func (mi *ModuleInstance) Exports() modules.Exports { + return modules.Exports{ + Default: mi.exports, + // TODO: add new HTTP APIs like Client, Request (see above comment in + // NewModuleInstance()), etc. as named exports? + } +} - responseCallback func(int) bool +func (mi *ModuleInstance) defineConstants() { + rt := mi.vu.Runtime() + mustAddProp := func(name, val string) { + err := mi.exports.DefineDataProperty( + name, rt.ToValue(val), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_TRUE, + ) + if err != nil { + common.Throw(rt, err) + } + } + mustAddProp("TLS_1_0", netext.TLS_1_0) + mustAddProp("TLS_1_1", netext.TLS_1_1) + mustAddProp("TLS_1_2", netext.TLS_1_2) + mustAddProp("TLS_1_3", netext.TLS_1_3) + mustAddProp("OCSP_STATUS_GOOD", netext.OCSP_STATUS_GOOD) + mustAddProp("OCSP_STATUS_REVOKED", netext.OCSP_STATUS_REVOKED) + mustAddProp("OCSP_STATUS_SERVER_FAILED", netext.OCSP_STATUS_SERVER_FAILED) + mustAddProp("OCSP_STATUS_UNKNOWN", netext.OCSP_STATUS_UNKNOWN) + mustAddProp("OCSP_REASON_UNSPECIFIED", netext.OCSP_REASON_UNSPECIFIED) + mustAddProp("OCSP_REASON_KEY_COMPROMISE", netext.OCSP_REASON_KEY_COMPROMISE) + mustAddProp("OCSP_REASON_CA_COMPROMISE", netext.OCSP_REASON_CA_COMPROMISE) + mustAddProp("OCSP_REASON_AFFILIATION_CHANGED", netext.OCSP_REASON_AFFILIATION_CHANGED) + mustAddProp("OCSP_REASON_SUPERSEDED", netext.OCSP_REASON_SUPERSEDED) + mustAddProp("OCSP_REASON_CESSATION_OF_OPERATION", netext.OCSP_REASON_CESSATION_OF_OPERATION) + mustAddProp("OCSP_REASON_CERTIFICATE_HOLD", netext.OCSP_REASON_CERTIFICATE_HOLD) + mustAddProp("OCSP_REASON_REMOVE_FROM_CRL", netext.OCSP_REASON_REMOVE_FROM_CRL) + mustAddProp("OCSP_REASON_PRIVILEGE_WITHDRAWN", netext.OCSP_REASON_PRIVILEGE_WITHDRAWN) + mustAddProp("OCSP_REASON_AA_COMPROMISE", netext.OCSP_REASON_AA_COMPROMISE) } -// XCookieJar creates a new cookie jar object. -func (*HTTP) XCookieJar(ctx *context.Context) *HTTPCookieJar { - return newCookieJar(ctx) +func (mi *ModuleInstance) newCookieJar(call goja.ConstructorCall) *goja.Object { + rt := mi.vu.Runtime() + jar, err := cookiejar.New(nil) + if err != nil { + common.Throw(rt, err) + } + return rt.ToValue(&CookieJar{mi, jar}).ToObject(rt) } -// CookieJar returns the active cookie jar for the current VU. -func (*HTTP) CookieJar(ctx context.Context) (*HTTPCookieJar, error) { - state := lib.GetState(ctx) - if state == nil { - return nil, ErrJarForbiddenInInitContext +// getVUCookieJar returns the active cookie jar for the current VU. +func (mi *ModuleInstance) getVUCookieJar(call goja.FunctionCall) goja.Value { + rt := mi.vu.Runtime() + if state := mi.vu.State(); state != nil { + return rt.ToValue(&CookieJar{mi, state.CookieJar}) } - return &HTTPCookieJar{state.CookieJar, &ctx}, nil + common.Throw(rt, ErrJarForbiddenInInitContext) + return nil } -// URL creates a new URL from the provided parts -func (*HTTP) URL(parts []string, pieces ...string) (httpext.URL, error) { +// URL creates a new URL wrapper from the provided parts. +func (mi *ModuleInstance) URL(parts []string, pieces ...string) (httpext.URL, error) { var name, urlstr string for i, part := range parts { name += part @@ -127,3 +185,11 @@ func (*HTTP) URL(parts []string, pieces ...string) (httpext.URL, error) { } return httpext.NewURL(urlstr, name) } + +// Client represents a stand-alone HTTP client. +// +// TODO: move to its own file +type Client struct { + moduleInstance *ModuleInstance + responseCallback func(int) bool +} diff --git a/js/modules/k6/http/http_test.go b/js/modules/k6/http/http_test.go index bbacca83107..3dcb0102d33 100644 --- a/js/modules/k6/http/http_test.go +++ b/js/modules/k6/http/http_test.go @@ -21,6 +21,7 @@ package http import ( + "context" "testing" "github.com/dop251/goja" @@ -28,13 +29,44 @@ import ( "github.com/stretchr/testify/require" "go.k6.io/k6/js/common" + "go.k6.io/k6/js/modulestest" + "go.k6.io/k6/lib" + "go.k6.io/k6/lib/metrics" "go.k6.io/k6/lib/netext/httpext" ) -func TestTagURL(t *testing.T) { +//nolint: golint, revive +func getTestModuleInstance( + t testing.TB, ctx context.Context, state *lib.State, +) (*goja.Runtime, *ModuleInstance) { rt := goja.New() rt.SetFieldNameMapper(common.FieldNameMapper{}) - rt.Set("http", common.Bind(rt, new(GlobalHTTP).NewModuleInstancePerVU(), nil)) + + if ctx == nil { + dummyCtx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + ctx = dummyCtx + } + + root := New() + mockVU := &modulestest.VU{ + RuntimeField: rt, + InitEnvField: &common.InitEnvironment{ + Registry: metrics.NewRegistry(), + }, + CtxField: ctx, + StateField: state, + } + mi, ok := root.NewModuleInstance(mockVU).(*ModuleInstance) + require.True(t, ok) + + require.NoError(t, rt.Set("http", mi.Exports().Default)) + + return rt, mi +} + +func TestTagURL(t *testing.T) { + t.Parallel() testdata := map[string]struct{ u, n string }{ `http://localhost/anything/`: {"http://localhost/anything/", "http://localhost/anything/"}, @@ -46,6 +78,8 @@ func TestTagURL(t *testing.T) { for expr, data := range testdata { expr, data := expr, data t.Run("expr="+expr, func(t *testing.T) { + t.Parallel() + rt, _ := getTestModuleInstance(t, nil, nil) tag, err := httpext.NewURL(data.u, data.n) require.NoError(t, err) v, err := rt.RunString("http.url`" + expr + "`") diff --git a/js/modules/k6/http/request.go b/js/modules/k6/http/request.go index 5317220fc41..be51e84ade7 100644 --- a/js/modules/k6/http/request.go +++ b/js/modules/k6/http/request.go @@ -22,7 +22,6 @@ package http import ( "bytes" - "context" "errors" "fmt" "mime/multipart" @@ -37,7 +36,6 @@ import ( "gopkg.in/guregu/null.v3" "go.k6.io/k6/js/common" - "go.k6.io/k6/lib" "go.k6.io/k6/lib/netext/httpext" "go.k6.io/k6/lib/types" ) @@ -48,49 +46,16 @@ var ErrHTTPForbiddenInInitContext = common.NewInitContextError("Making http requ // ErrBatchForbiddenInInitContext is used when batch was made in the init context var ErrBatchForbiddenInInitContext = common.NewInitContextError("Using batch in the init context is not supported") -// Get makes an HTTP GET request and returns a corresponding response by taking goja.Values as arguments -func (h *HTTP) Get(ctx context.Context, url goja.Value, args ...goja.Value) (*Response, error) { - // The body argument is always undefined for GETs and HEADs. - args = append([]goja.Value{goja.Undefined()}, args...) - return h.Request(ctx, HTTP_METHOD_GET, url, args...) -} - -// Head makes an HTTP HEAD request and returns a corresponding response by taking goja.Values as arguments -func (h *HTTP) Head(ctx context.Context, url goja.Value, args ...goja.Value) (*Response, error) { - // The body argument is always undefined for GETs and HEADs. - args = append([]goja.Value{goja.Undefined()}, args...) - return h.Request(ctx, HTTP_METHOD_HEAD, url, args...) -} - -// Post makes an HTTP POST request and returns a corresponding response by taking goja.Values as arguments -func (h *HTTP) Post(ctx context.Context, url goja.Value, args ...goja.Value) (*Response, error) { - return h.Request(ctx, HTTP_METHOD_POST, url, args...) -} - -// Put makes an HTTP PUT request and returns a corresponding response by taking goja.Values as arguments -func (h *HTTP) Put(ctx context.Context, url goja.Value, args ...goja.Value) (*Response, error) { - return h.Request(ctx, HTTP_METHOD_PUT, url, args...) -} - -// Patch makes a patch request and returns a corresponding response by taking goja.Values as arguments -func (h *HTTP) Patch(ctx context.Context, url goja.Value, args ...goja.Value) (*Response, error) { - return h.Request(ctx, HTTP_METHOD_PATCH, url, args...) -} - -// Del makes an HTTP DELETE and returns a corresponding response by taking goja.Values as arguments -func (h *HTTP) Del(ctx context.Context, url goja.Value, args ...goja.Value) (*Response, error) { - return h.Request(ctx, HTTP_METHOD_DELETE, url, args...) -} - -// Options makes an HTTP OPTIONS request and returns a corresponding response by taking goja.Values as arguments -func (h *HTTP) Options(ctx context.Context, url goja.Value, args ...goja.Value) (*Response, error) { - return h.Request(ctx, HTTP_METHOD_OPTIONS, url, args...) +func (c *Client) getMethodClosure(method string) func(url goja.Value, args ...goja.Value) (*Response, error) { + return func(url goja.Value, args ...goja.Value) (*Response, error) { + return c.Request(method, url, args...) + } } // Request makes an http request of the provided `method` and returns a corresponding response by // taking goja.Values as arguments -func (h *HTTP) Request(ctx context.Context, method string, url goja.Value, args ...goja.Value) (*Response, error) { - state := lib.GetState(ctx) +func (c *Client) Request(method string, url goja.Value, args ...goja.Value) (*Response, error) { + state := c.moduleInstance.vu.State() if state == nil { return nil, ErrHTTPForbiddenInInitContext } @@ -105,36 +70,49 @@ func (h *HTTP) Request(ctx context.Context, method string, url goja.Value, args params = args[1] } - req, err := h.parseRequest(ctx, method, url, body, params) + req, err := c.parseRequest(method, url, body, params) if err != nil { if state.Options.Throw.Bool { return nil, err } state.Logger.WithField("error", err).Warn("Request Failed") - r := httpext.NewResponse(ctx) + r := httpext.NewResponse() r.Error = err.Error() var k6e httpext.K6Error if errors.As(err, &k6e) { r.ErrorCode = int(k6e.Code) } - return &Response{Response: r}, nil + return &Response{Response: r, client: c}, nil } - resp, err := httpext.MakeRequest(ctx, req) + resp, err := httpext.MakeRequest(c.moduleInstance.vu.Context(), state, req) if err != nil { return nil, err } - processResponse(ctx, resp, req.ResponseType) - return h.responseFromHttpext(resp), nil + c.processResponse(resp, req.ResponseType) + return c.responseFromHTTPext(resp), nil } -//TODO break this function up -//nolint: gocyclo -func (h *HTTP) parseRequest( - ctx context.Context, method string, reqURL, body interface{}, params goja.Value, +// processResponse stores the body as an ArrayBuffer if indicated by +// respType. This is done here instead of in httpext.readResponseBody to avoid +// a reverse dependency on js/common or goja. +func (c *Client) processResponse(resp *httpext.Response, respType httpext.ResponseType) { + if respType == httpext.ResponseTypeBinary && resp.Body != nil { + resp.Body = c.moduleInstance.vu.Runtime().NewArrayBuffer(resp.Body.([]byte)) + } +} + +func (c *Client) responseFromHTTPext(resp *httpext.Response) *Response { + return &Response{Response: resp, client: c} +} + +// TODO: break this function up +//nolint: gocyclo, cyclop, funlen, gocognit +func (c *Client) parseRequest( + method string, reqURL, body interface{}, params goja.Value, ) (*httpext.ParsedHTTPRequest, error) { - rt := common.GetRuntime(ctx) - state := lib.GetState(ctx) + rt := c.moduleInstance.vu.Runtime() + state := c.moduleInstance.vu.State() if state == nil { return nil, ErrHTTPForbiddenInInitContext } @@ -159,7 +137,7 @@ func (h *HTTP) parseRequest( Redirects: state.Options.MaxRedirects, Cookies: make(map[string]*httpext.HTTPRequestCookie), Tags: make(map[string]string), - ResponseCallback: h.responseCallback, + ResponseCallback: c.responseCallback, } if state.Options.DiscardResponseBodies.Bool { @@ -328,7 +306,7 @@ func (h *HTTP) parseRequest( continue } switch v := jarV.Export().(type) { - case *HTTPCookieJar: + case *CookieJar: result.ActiveJar = v.Jar } case "compression": @@ -397,60 +375,60 @@ func (h *HTTP) parseRequest( return result, nil } -func (h *HTTP) prepareBatchArray( - ctx context.Context, requests []interface{}, -) ([]httpext.BatchParsedHTTPRequest, []*Response, error) { +func (c *Client) prepareBatchArray(requests []interface{}) ( + []httpext.BatchParsedHTTPRequest, []*Response, error, +) { reqCount := len(requests) batchReqs := make([]httpext.BatchParsedHTTPRequest, reqCount) results := make([]*Response, reqCount) for i, req := range requests { - resp := httpext.NewResponse(ctx) - parsedReq, err := h.parseBatchRequest(ctx, i, req) + resp := httpext.NewResponse() + parsedReq, err := c.parseBatchRequest(i, req) if err != nil { resp.Error = err.Error() var k6e httpext.K6Error if errors.As(err, &k6e) { resp.ErrorCode = int(k6e.Code) } - results[i] = h.responseFromHttpext(resp) + results[i] = c.responseFromHTTPext(resp) return batchReqs, results, err } batchReqs[i] = httpext.BatchParsedHTTPRequest{ ParsedHTTPRequest: parsedReq, Response: resp, } - results[i] = h.responseFromHttpext(resp) + results[i] = c.responseFromHTTPext(resp) } return batchReqs, results, nil } -func (h *HTTP) prepareBatchObject( - ctx context.Context, requests map[string]interface{}, -) ([]httpext.BatchParsedHTTPRequest, map[string]*Response, error) { +func (c *Client) prepareBatchObject(requests map[string]interface{}) ( + []httpext.BatchParsedHTTPRequest, map[string]*Response, error, +) { reqCount := len(requests) batchReqs := make([]httpext.BatchParsedHTTPRequest, reqCount) results := make(map[string]*Response, reqCount) i := 0 for key, req := range requests { - resp := httpext.NewResponse(ctx) - parsedReq, err := h.parseBatchRequest(ctx, key, req) + resp := httpext.NewResponse() + parsedReq, err := c.parseBatchRequest(key, req) if err != nil { resp.Error = err.Error() var k6e httpext.K6Error if errors.As(err, &k6e) { resp.ErrorCode = int(k6e.Code) } - results[key] = h.responseFromHttpext(resp) + results[key] = c.responseFromHTTPext(resp) return batchReqs, results, err } batchReqs[i] = httpext.BatchParsedHTTPRequest{ ParsedHTTPRequest: parsedReq, Response: resp, } - results[key] = h.responseFromHttpext(resp) + results[key] = c.responseFromHTTPext(resp) i++ } @@ -459,8 +437,8 @@ func (h *HTTP) prepareBatchObject( // Batch makes multiple simultaneous HTTP requests. The provideds reqsV should be an array of request // objects. Batch returns an array of responses and/or error -func (h *HTTP) Batch(ctx context.Context, reqsV goja.Value) (interface{}, error) { - state := lib.GetState(ctx) +func (c *Client) Batch(reqsV goja.Value) (interface{}, error) { + state := c.moduleInstance.vu.State() if state == nil { return nil, ErrBatchForbiddenInInitContext } @@ -473,9 +451,9 @@ func (h *HTTP) Batch(ctx context.Context, reqsV goja.Value) (interface{}, error) switch v := reqsV.Export().(type) { case []interface{}: - batchReqs, results, err = h.prepareBatchArray(ctx, v) + batchReqs, results, err = c.prepareBatchArray(v) case map[string]interface{}: - batchReqs, results, err = h.prepareBatchObject(ctx, v) + batchReqs, results, err = c.prepareBatchObject(v) default: return nil, fmt.Errorf("invalid http.batch() argument type %T", v) } @@ -490,9 +468,9 @@ func (h *HTTP) Batch(ctx context.Context, reqsV goja.Value) (interface{}, error) reqCount := len(batchReqs) errs := httpext.MakeBatchRequests( - ctx, batchReqs, reqCount, + c.moduleInstance.vu.Context(), state, batchReqs, reqCount, int(state.Options.Batch.Int64), int(state.Options.BatchPerHost.Int64), - processResponse, + c.processResponse, ) for i := 0; i < reqCount; i++ { @@ -503,15 +481,13 @@ func (h *HTTP) Batch(ctx context.Context, reqsV goja.Value) (interface{}, error) return results, err } -func (h *HTTP) parseBatchRequest( - ctx context.Context, key interface{}, val interface{}, -) (*httpext.ParsedHTTPRequest, error) { +func (c *Client) parseBatchRequest(key interface{}, val interface{}) (*httpext.ParsedHTTPRequest, error) { var ( - method = HTTP_METHOD_GET + method = http.MethodGet ok bool body, reqURL interface{} params goja.Value - rt = common.GetRuntime(ctx) + rt = c.moduleInstance.vu.Runtime() ) switch data := val.(type) { @@ -547,7 +523,7 @@ func (h *HTTP) parseBatchRequest( return nil, fmt.Errorf("invalid method type '%#v'", newMethod) } method = strings.ToUpper(method) - if method == HTTP_METHOD_GET || method == HTTP_METHOD_HEAD { + if method == http.MethodGet || method == http.MethodHead { body = nil } } @@ -559,7 +535,7 @@ func (h *HTTP) parseBatchRequest( reqURL = val } - return h.parseRequest(ctx, method, reqURL, body, params) + return c.parseRequest(method, reqURL, body, params) } func requestContainsFile(data map[string]interface{}) bool { diff --git a/js/modules/k6/http/request_test.go b/js/modules/k6/http/request_test.go index 24f4aab643c..06922a46f88 100644 --- a/js/modules/k6/http/request_test.go +++ b/js/modules/k6/http/request_test.go @@ -48,7 +48,7 @@ import ( "github.com/stretchr/testify/require" "gopkg.in/guregu/null.v3" - "go.k6.io/k6/js/common" + "go.k6.io/k6/js/modulestest" "go.k6.io/k6/lib" "go.k6.io/k6/lib/metrics" "go.k6.io/k6/lib/testutils" @@ -129,9 +129,9 @@ func assertRequestMetricsEmittedSingle(t *testing.T, sampleContainer stats.Sampl } } -func newRuntime( - t testing.TB, -) (*httpmultibin.HTTPMultiBin, *lib.State, chan stats.SampleContainer, *goja.Runtime, *context.Context) { +func newRuntime(t testing.TB) ( + *httpmultibin.HTTPMultiBin, *lib.State, chan stats.SampleContainer, *goja.Runtime, *ModuleInstance, +) { tb := httpmultibin.NewHTTPMultiBin(t) root, err := lib.NewGroup("", nil) @@ -141,9 +141,6 @@ func newRuntime( logger := logrus.New() logger.Level = logrus.DebugLevel - rt := goja.New() - rt.SetFieldNameMapper(common.FieldNameMapper{}) - options := lib.Options{ MaxRedirects: null.IntFrom(10), UserAgent: null.StringFrom("TestUserAgent"), @@ -169,17 +166,13 @@ func newRuntime( BuiltinMetrics: metrics.RegisterBuiltinMetrics(registry), } - ctx := new(context.Context) - *ctx = lib.WithState(tb.Context, state) - *ctx = common.WithRuntime(*ctx, rt) - rt.Set("http", common.Bind(rt, new(GlobalHTTP).NewModuleInstancePerVU(), ctx)) - - return tb, state, samples, rt, ctx + rt, mi := getTestModuleInstance(t, tb.Context, state) + return tb, state, samples, rt, mi } func TestRequestAndBatch(t *testing.T) { t.Parallel() - tb, state, samples, rt, ctx := newRuntime(t) + tb, state, samples, rt, _ := newRuntime(t) sr := tb.Replacer.Replace // Handle paths with custom logic @@ -485,20 +478,6 @@ func TestRequestAndBatch(t *testing.T) { assert.NoError(t, err) }) }) - t.Run("Cancelled", func(t *testing.T) { - hook := logtest.NewLocal(state.Logger) - defer hook.Reset() - - oldctx := *ctx - newctx, cancel := context.WithCancel(oldctx) - cancel() - *ctx = newctx - defer func() { *ctx = oldctx }() - - _, err := rt.RunString(sr(`http.get("HTTPBIN_URL/get/");`)) - assert.Error(t, err) - assert.Nil(t, hook.LastEntry()) - }) t.Run("HTTP/2", func(t *testing.T) { stats.GetBufferedSamples(samples) // Clean up buffered samples from previous tests _, err := rt.RunString(sr(` @@ -1580,6 +1559,26 @@ func TestRequestAndBatch(t *testing.T) { }) } +func TestRequestCancellation(t *testing.T) { + t.Parallel() + tb, state, _, rt, mi := newRuntime(t) + sr := tb.Replacer.Replace + + hook := logtest.NewLocal(state.Logger) + defer hook.Reset() + + testVU, ok := mi.vu.(*modulestest.VU) + require.True(t, ok) + + newctx, cancel := context.WithCancel(mi.vu.Context()) + testVU.CtxField = newctx + cancel() + + _, err := rt.RunString(sr(`http.get("HTTPBIN_URL/get/");`)) + assert.Error(t, err) + assert.Nil(t, hook.LastEntry()) +} + func TestRequestArrayBufferBody(t *testing.T) { t.Parallel() tb, _, _, rt, _ := newRuntime(t) //nolint: dogsled diff --git a/js/modules/k6/http/response.go b/js/modules/k6/http/response.go index e7e96e9c6bd..452f1f6c0b9 100644 --- a/js/modules/k6/http/response.go +++ b/js/modules/k6/http/response.go @@ -21,10 +21,10 @@ package http import ( - "context" "encoding/json" "errors" "fmt" + "net/http" "net/url" "strings" @@ -39,7 +39,7 @@ import ( // Response is a representation of an HTTP response to be returned to the goja VM type Response struct { *httpext.Response `js:"-"` - h *HTTP + client *Client cachedJSON interface{} validatedJSON bool @@ -56,36 +56,23 @@ func (j jsonError) Error() string { return fmt.Sprintf("%s %d, character %d , error: %v", errMessage, j.line, j.character, j.err) } -// processResponse stores the body as an ArrayBuffer if indicated by -// respType. This is done here instead of in httpext.readResponseBody to avoid -// a reverse dependency on js/common or goja. -func processResponse(ctx context.Context, resp *httpext.Response, respType httpext.ResponseType) { - if respType == httpext.ResponseTypeBinary && resp.Body != nil { - rt := common.GetRuntime(ctx) - resp.Body = rt.NewArrayBuffer(resp.Body.([]byte)) - } -} - -func (h *HTTP) responseFromHttpext(resp *httpext.Response) *Response { - return &Response{Response: resp, h: h, cachedJSON: nil, validatedJSON: false} -} - // HTML returns the body as an html.Selection func (res *Response) HTML(selector ...string) html.Selection { + rt := res.client.moduleInstance.vu.Runtime() if res.Body == nil { err := fmt.Errorf("the body is null so we can't transform it to HTML" + " - this likely was because of a request error getting the response") - common.Throw(common.GetRuntime(res.GetCtx()), err) + common.Throw(rt, err) } body, err := common.ToString(res.Body) if err != nil { - common.Throw(common.GetRuntime(res.GetCtx()), err) + common.Throw(rt, err) } - sel, err := html.HTML{}.ParseHTML(res.GetCtx(), body) + sel, err := html.ParseHTML(rt, body) if err != nil { - common.Throw(common.GetRuntime(res.GetCtx()), err) + common.Throw(rt, err) } sel.URL = res.URL if len(selector) > 0 { @@ -96,7 +83,7 @@ func (res *Response) HTML(selector ...string) html.Selection { // JSON parses the body of a response as JSON and returns it to the goja VM. func (res *Response) JSON(selector ...string) goja.Value { - rt := common.GetRuntime(res.GetCtx()) + rt := res.client.moduleInstance.vu.Runtime() if res.Body == nil { err := fmt.Errorf("the body is null so we can't transform it to JSON" + @@ -168,7 +155,7 @@ func checkErrorInJSON(input []byte, offset int, err error) error { // SubmitForm parses the body as an html looking for a from and then submitting it // TODO: document the actual arguments that can be provided func (res *Response) SubmitForm(args ...goja.Value) (*Response, error) { - rt := common.GetRuntime(res.GetCtx()) + rt := res.client.moduleInstance.vu.Runtime() formSelector := "form" submitSelector := "[type=\"submit\"]" @@ -201,7 +188,7 @@ func (res *Response) SubmitForm(args ...goja.Value) (*Response, error) { var requestMethod string if methodAttr == goja.Undefined() { // Use GET by default - requestMethod = HTTP_METHOD_GET + requestMethod = http.MethodGet } else { requestMethod = strings.ToUpper(methodAttr.String()) } @@ -240,21 +227,24 @@ func (res *Response) SubmitForm(args ...goja.Value) (*Response, error) { values[k] = v } - if requestMethod == HTTP_METHOD_GET { + if requestMethod == http.MethodGet { q := url.Values{} for k, v := range values { q.Add(k, v.String()) } requestURL.RawQuery = q.Encode() - return res.h.Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), goja.Null(), requestParams) + return res.client.Request(requestMethod, rt.ToValue(requestURL.String()), goja.Null(), requestParams) } - return res.h.Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), rt.ToValue(values), requestParams) + return res.client.Request( + requestMethod, rt.ToValue(requestURL.String()), + rt.ToValue(values), requestParams, + ) } // ClickLink parses the body as an html, looks for a link and than makes a request as if the link was // clicked func (res *Response) ClickLink(args ...goja.Value) (*Response, error) { - rt := common.GetRuntime(res.GetCtx()) + rt := res.client.moduleInstance.vu.Runtime() selector := "a[href]" requestParams := goja.Null() @@ -289,5 +279,5 @@ func (res *Response) ClickLink(args ...goja.Value) (*Response, error) { } requestURL := responseURL.ResolveReference(hrefURL) - return res.h.Get(res.GetCtx(), rt.ToValue(requestURL.String()), requestParams) + return res.client.Request(http.MethodGet, rt.ToValue(requestURL.String()), goja.Undefined(), requestParams) } diff --git a/js/modules/k6/http/response_callback.go b/js/modules/k6/http/response_callback.go index 4b91e19a365..6a29d7d5a22 100644 --- a/js/modules/k6/http/response_callback.go +++ b/js/modules/k6/http/response_callback.go @@ -21,7 +21,6 @@ package http import ( - "context" "errors" "fmt" @@ -57,11 +56,11 @@ func (e expectedStatuses) match(status int) bool { return false } -// ExpectedStatuses returns expectedStatuses object based on the provided arguments. +// expectedStatuses returns expectedStatuses object based on the provided arguments. // The arguments must be either integers or object of `{min: , max: }` // kind. The "integer"ness is checked by the Number.isInteger. -func (*HTTP) ExpectedStatuses(ctx context.Context, args ...goja.Value) *expectedStatuses { //nolint: golint - rt := common.GetRuntime(ctx) +func (mi *ModuleInstance) expectedStatuses(args ...goja.Value) *expectedStatuses { + rt := mi.vu.Runtime() if len(args) == 0 { common.Throw(rt, errors.New("no arguments")) @@ -102,16 +101,18 @@ func (*HTTP) ExpectedStatuses(ctx context.Context, args ...goja.Value) *expected // SetResponseCallback sets the responseCallback to the value provided. Supported values are // expectedStatuses object or a `null` which means that metrics shouldn't be tagged as failed and // `http_req_failed` should not be emitted - the behaviour previous to this -func (h *HTTP) SetResponseCallback(ctx context.Context, val goja.Value) { +func (c *Client) SetResponseCallback(val goja.Value) { if val != nil && !goja.IsNull(val) { // This is done this way as ExportTo exports functions to empty structs without an error if es, ok := val.Export().(*expectedStatuses); ok { - h.responseCallback = es.match + c.responseCallback = es.match } else { - //nolint:golint - common.Throw(common.GetRuntime(ctx), fmt.Errorf("unsupported argument, expected http.expectedStatuses")) + common.Throw( + c.moduleInstance.vu.Runtime(), + fmt.Errorf("unsupported argument, expected http.expectedStatuses"), + ) } } else { - h.responseCallback = nil + c.responseCallback = nil } } diff --git a/js/modules/k6/http/response_callback_test.go b/js/modules/k6/http/response_callback_test.go index 939dc6dec4d..f0438513ec8 100644 --- a/js/modules/k6/http/response_callback_test.go +++ b/js/modules/k6/http/response_callback_test.go @@ -21,7 +21,6 @@ package http import ( - "context" "fmt" "sort" "testing" @@ -29,7 +28,6 @@ import ( "github.com/dop251/goja" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.k6.io/k6/js/common" "go.k6.io/k6/lib" "go.k6.io/k6/lib/metrics" "go.k6.io/k6/stats" @@ -37,12 +35,8 @@ import ( func TestExpectedStatuses(t *testing.T) { t.Parallel() - rt := goja.New() - rt.SetFieldNameMapper(common.FieldNameMapper{}) - ctx := context.Background() + rt, _ := getTestModuleInstance(t, nil, nil) - ctx = common.WithRuntime(ctx, rt) - rt.Set("http", common.Bind(rt, new(GlobalHTTP).NewModuleInstancePerVU(), &ctx)) cases := map[string]struct { code, err string expected expectedStatuses @@ -108,10 +102,8 @@ type expectedSample struct { func TestResponseCallbackInAction(t *testing.T) { t.Parallel() - tb, _, samples, rt, ctx := newRuntime(t) + tb, _, samples, rt, mii := newRuntime(t) sr := tb.Replacer.Replace - httpModule := new(GlobalHTTP).NewModuleInstancePerVU().(*HTTP) - rt.Set("http", common.Bind(rt, httpModule, ctx)) HTTPMetricsWithoutFailed := []string{ metrics.HTTPReqsName, @@ -282,7 +274,7 @@ func TestResponseCallbackInAction(t *testing.T) { for name, testCase := range testCases { testCase := testCase t.Run(name, func(t *testing.T) { - httpModule.responseCallback = defaultExpectedStatuses.match + mii.defaultClient.responseCallback = defaultExpectedStatuses.match _, err := rt.RunString(sr(testCase.code)) assert.NoError(t, err) @@ -308,10 +300,8 @@ func TestResponseCallbackInAction(t *testing.T) { func TestResponseCallbackBatch(t *testing.T) { t.Parallel() - tb, _, samples, rt, ctx := newRuntime(t) + tb, _, samples, rt, mii := newRuntime(t) sr := tb.Replacer.Replace - httpModule := new(GlobalHTTP).NewModuleInstancePerVU().(*HTTP) - rt.Set("http", common.Bind(rt, httpModule, ctx)) HTTPMetricsWithoutFailed := []string{ metrics.HTTPReqsName, @@ -393,7 +383,7 @@ func TestResponseCallbackBatch(t *testing.T) { for name, testCase := range testCases { testCase := testCase t.Run(name, func(t *testing.T) { - httpModule.responseCallback = defaultExpectedStatuses.match + mii.defaultClient.responseCallback = defaultExpectedStatuses.match _, err := rt.RunString(sr(testCase.code)) assert.NoError(t, err) @@ -424,7 +414,7 @@ func TestResponseCallbackBatch(t *testing.T) { func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) { t.Parallel() - tb, state, samples, rt, ctx := newRuntime(t) + tb, state, samples, rt, _ := newRuntime(t) sr := tb.Replacer.Replace allHTTPMetrics := []string{ metrics.HTTPReqsName, @@ -438,8 +428,6 @@ func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) { metrics.HTTPReqTLSHandshakingName, } deleteSystemTag(state, stats.TagExpectedResponse.String()) - httpModule := new(GlobalHTTP).NewModuleInstancePerVU().(*HTTP) - rt.Set("http", common.Bind(rt, httpModule, ctx)) _, err := rt.RunString(sr(`http.request("GET", "HTTPBIN_URL/redirect/1", null, {responseCallback: http.expectedStatuses(200)});`)) assert.NoError(t, err) @@ -481,10 +469,7 @@ func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) { func TestDigestWithResponseCallback(t *testing.T) { t.Parallel() - tb, _, samples, rt, ctx := newRuntime(t) - - httpModule := new(GlobalHTTP).NewModuleInstancePerVU().(*HTTP) - rt.Set("http", common.Bind(rt, httpModule, ctx)) + tb, _, samples, rt, _ := newRuntime(t) urlWithCreds := tb.Replacer.Replace( "http://testuser:testpwd@HTTPBIN_IP:HTTPBIN_PORT/digest-auth/auth/testuser/testpwd", diff --git a/js/modules/k6/http/response_test.go b/js/modules/k6/http/response_test.go index 3e49041398b..0b1f9c74a41 100644 --- a/js/modules/k6/http/response_test.go +++ b/js/modules/k6/http/response_test.go @@ -28,8 +28,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "go.k6.io/k6/lib/netext/httpext" "go.k6.io/k6/stats" ) @@ -187,6 +187,7 @@ func TestResponse(t *testing.T) { t.Run("NoResponseBody", func(t *testing.T) { _, err := rt.RunString(sr(`http.get("HTTPBIN_URL/html", {responseType: 'none'}).html();`)) + require.NotNil(t, err) assert.Contains(t, err.Error(), "the body is null so we can't transform it to HTML"+ " - this likely was because of a request error getting the response") }) @@ -356,7 +357,7 @@ func TestResponse(t *testing.T) { data.textarea[0] !== "Lorem ipsum dolor sit amet" ) { throw new Error("incorrect body: " + JSON.stringify(data, null, 4) ); } `)) - assert.NoError(t, err) + require.NoError(t, err) assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/myforms/get"), "", 200, "") }) }) @@ -407,35 +408,3 @@ func TestResponse(t *testing.T) { }) }) } - -func BenchmarkResponseJson(b *testing.B) { - b.Skipf("We need to have context in the response") - testCases := []struct { - selector string - }{ - {"glossary.GlossDiv.GlossList.GlossEntry.title"}, - {"glossary.GlossDiv.GlossList.GlossEntry.int"}, - {"glossary.GlossDiv.GlossList.GlossEntry.intArray"}, - {"glossary.GlossDiv.GlossList.GlossEntry.mixedArray"}, - {"glossary.friends"}, - {"glossary.friends.#.first"}, - {"glossary.GlossDiv.GlossList.GlossEntry.GlossDef"}, - {"glossary"}, - } - for _, tc := range testCases { - tc := tc - b.Run(fmt.Sprintf("Selector %s ", tc.selector), func(b *testing.B) { - for n := 0; n < b.N; n++ { - resp := new(HTTP).responseFromHttpext(&httpext.Response{Body: jsonData}) - resp.JSON(tc.selector) - } - }) - } - - b.Run("Without selector", func(b *testing.B) { - for n := 0; n < b.N; n++ { - resp := new(HTTP).responseFromHttpext(&httpext.Response{Body: jsonData}) - resp.JSON() - } - }) -} diff --git a/js/modules/k6/http/tls_test.go b/js/modules/k6/http/tls_test.go index a7e7668698d..8b604d90246 100644 --- a/js/modules/k6/http/tls_test.go +++ b/js/modules/k6/http/tls_test.go @@ -1,5 +1,3 @@ -// +build go1.12 - /* * * k6 - a next-generation load testing tool diff --git a/js/modules/k6/ws/ws.go b/js/modules/k6/ws/ws.go index 90960d0641b..a1d95331285 100644 --- a/js/modules/k6/ws/ws.go +++ b/js/modules/k6/ws/ws.go @@ -191,7 +191,7 @@ func (mi *WS) Connect(url string, args ...goja.Value) (*WSHTTPResponse, error) { if goja.IsUndefined(jarV) || goja.IsNull(jarV) { continue } - if v, ok := jarV.Export().(*httpModule.HTTPCookieJar); ok { + if v, ok := jarV.Export().(*httpModule.CookieJar); ok { jar = v.Jar } case "compression": diff --git a/js/modules/k6/ws/ws_test.go b/js/modules/k6/ws/ws_test.go index 6e852c9c634..a2f15785ebc 100644 --- a/js/modules/k6/ws/ws_test.go +++ b/js/modules/k6/ws/ws_test.go @@ -1075,9 +1075,9 @@ func TestCompression(t *testing.T) { })) _, err := ts.rt.RunString(sr(` - // if client supports compression, it has to send the header + // if client supports compression, it has to send the header // 'Sec-Websocket-Extensions:permessage-deflate; server_no_context_takeover; client_no_context_takeover' to server. - // if compression is negotiated successfully, server will reply with header + // if compression is negotiated successfully, server will reply with header // 'Sec-Websocket-Extensions:permessage-deflate; server_no_context_takeover; client_no_context_takeover' var params = { @@ -1274,7 +1274,14 @@ func TestCookieJar(t *testing.T) { t.Logf("error while closing connection in /ws-echo-someheader: %v", err) } })) - err := ts.rt.Set("http", common.Bind(ts.rt, httpModule.New().NewModuleInstancePerVU(), ts.ctxPtr)) + + mii := &modulestest.VU{ + RuntimeField: ts.rt, + InitEnvField: &common.InitEnvironment{Registry: metrics.NewRegistry()}, + CtxField: context.Background(), + StateField: ts.state, + } + err := ts.rt.Set("http", httpModule.New().NewModuleInstance(mii).Exports().Default) require.NoError(t, err) ts.state.CookieJar, _ = cookiejar.New(nil) diff --git a/js/modules/modules.go b/js/modules/modules.go index 34f8d436ebe..acff2edfa27 100644 --- a/js/modules/modules.go +++ b/js/modules/modules.go @@ -29,7 +29,6 @@ import ( "github.com/dop251/goja" "go.k6.io/k6/js/common" - "go.k6.io/k6/js/modules/k6/http" "go.k6.io/k6/lib" ) @@ -72,10 +71,6 @@ type Module interface { NewModuleInstance(VU) Instance } -// checks that modules implement HasModuleInstancePerVU -// this is done here as otherwise there will be a loop if the module imports this package -var _ HasModuleInstancePerVU = http.New() - // GetJSModules returns a map of all registered js modules func GetJSModules() map[string]interface{} { mx.Lock() diff --git a/lib/netext/httpext/batch.go b/lib/netext/httpext/batch.go index 00a4fdee4c6..47edc6702c8 100644 --- a/lib/netext/httpext/batch.go +++ b/lib/netext/httpext/batch.go @@ -45,10 +45,10 @@ type BatchParsedHTTPRequest struct { // The processResponse callback can be used to modify the response, e.g. // to replace the body. func MakeBatchRequests( - ctx context.Context, + ctx context.Context, state *lib.State, requests []BatchParsedHTTPRequest, reqCount, globalLimit, perHostLimit int, - processResponse func(context.Context, *Response, ResponseType), + processResponse func(*Response, ResponseType), ) <-chan error { workers := globalLimit if reqCount < workers { @@ -63,9 +63,9 @@ func MakeBatchRequests( defer hl.End() } - resp, err := MakeRequest(ctx, req.ParsedHTTPRequest) + resp, err := MakeRequest(ctx, state, req.ParsedHTTPRequest) if resp != nil { - processResponse(ctx, resp, req.ParsedHTTPRequest.ResponseType) + processResponse(resp, req.ParsedHTTPRequest.ResponseType) *req.Response = *resp } result <- err diff --git a/lib/netext/httpext/request.go b/lib/netext/httpext/request.go index bc16d63bb2f..44ef0622644 100644 --- a/lib/netext/httpext/request.go +++ b/lib/netext/httpext/request.go @@ -127,10 +127,11 @@ func updateK6Response(k6Response *Response, finishedReq *finishedRequest) { } } -// MakeRequest makes http request for tor the provided ParsedHTTPRequest -func MakeRequest(ctx context.Context, preq *ParsedHTTPRequest) (*Response, error) { - state := lib.GetState(ctx) - +// MakeRequest makes http request for tor the provided ParsedHTTPRequest. +// +// TODO: split apart... +// nolint: cyclop, gocyclo, funlen, gocognit +func MakeRequest(ctx context.Context, state *lib.State, preq *ParsedHTTPRequest) (*Response, error) { respReq := &Request{ Method: preq.Req.Method, URL: preq.Req.URL.String(), @@ -249,7 +250,7 @@ func MakeRequest(ctx context.Context, preq *ParsedHTTPRequest) (*Response, error transport = ntlmssp.Negotiator{RoundTripper: transport} } - resp := &Response{ctx: ctx, URL: preq.URL.URL, Request: respReq} + resp := &Response{URL: preq.URL.URL, Request: respReq} client := http.Client{ Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { diff --git a/lib/netext/httpext/request_test.go b/lib/netext/httpext/request_test.go index 863ff979f3d..3fd2c25b254 100644 --- a/lib/netext/httpext/request_test.go +++ b/lib/netext/httpext/request_test.go @@ -118,7 +118,13 @@ func TestMakeRequestError(t *testing.T) { Body: new(bytes.Buffer), Compressions: []CompressionType{badCompressionType}, } - _, err = MakeRequest(ctx, preq) + state := &lib.State{ + Options: lib.Options{RunTags: &stats.SampleTags{}}, + Transport: http.DefaultTransport, + Logger: logrus.New(), + Tags: lib.NewTagMap(nil), + } + _, err = MakeRequest(ctx, state, preq) require.Error(t, err) require.Equal(t, err.Error(), "unknown compressionType CompressionType(13)") }) @@ -141,7 +147,6 @@ func TestMakeRequestError(t *testing.T) { Logger: logger, Tags: lib.NewTagMap(nil), } - ctx = lib.WithState(ctx, state) req, _ := http.NewRequest("GET", srv.URL, nil) preq := &ParsedHTTPRequest{ Req: req, @@ -150,7 +155,7 @@ func TestMakeRequestError(t *testing.T) { Timeout: 10 * time.Second, } - res, err := MakeRequest(ctx, preq) + res, err := MakeRequest(ctx, state, preq) assert.Nil(t, res) assert.EqualError(t, err, "unsupported response status: 101 Switching Protocols") @@ -195,7 +200,6 @@ func TestResponseStatus(t *testing.T) { BuiltinMetrics: metrics.RegisterBuiltinMetrics(registry), Tags: lib.NewTagMap(nil), } - ctx := lib.WithState(context.Background(), state) req, err := http.NewRequest("GET", server.URL, nil) require.NoError(t, err) @@ -207,7 +211,9 @@ func TestResponseStatus(t *testing.T) { ResponseType: ResponseTypeNone, } - res, err := MakeRequest(ctx, preq) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + res, err := MakeRequest(ctx, state, preq) require.NoError(t, err) assert.Equal(t, tc.statusCodeExpected, res.Status) assert.Equal(t, tc.statusCodeStringExpected, res.StatusText) @@ -276,7 +282,6 @@ func TestMakeRequestTimeoutInTheMiddle(t *testing.T) { BuiltinMetrics: metrics.RegisterBuiltinMetrics(registry), Tags: lib.NewTagMap(nil), } - ctx = lib.WithState(ctx, state) req, _ := http.NewRequest("GET", srv.URL, nil) preq := &ParsedHTTPRequest{ Req: req, @@ -286,7 +291,7 @@ func TestMakeRequestTimeoutInTheMiddle(t *testing.T) { ResponseCallback: func(i int) bool { return i == 0 }, } - res, err := MakeRequest(ctx, preq) + res, err := MakeRequest(ctx, state, preq) require.NoError(t, err) assert.NotNil(t, res) assert.Len(t, samples, 1) @@ -354,7 +359,6 @@ func TestTrailFailed(t *testing.T) { BuiltinMetrics: metrics.RegisterBuiltinMetrics(registry), Tags: lib.NewTagMap(nil), } - ctx = lib.WithState(ctx, state) req, _ := http.NewRequest("GET", srv.URL, nil) preq := &ParsedHTTPRequest{ Req: req, @@ -363,7 +367,7 @@ func TestTrailFailed(t *testing.T) { Timeout: 10 * time.Millisecond, ResponseCallback: responseCallback, } - res, err := MakeRequest(ctx, preq) + res, err := MakeRequest(ctx, state, preq) require.NoError(t, err) require.NotNil(t, res) @@ -422,7 +426,6 @@ func TestMakeRequestDialTimeout(t *testing.T) { Tags: lib.NewTagMap(nil), } - ctx = lib.WithState(ctx, state) req, _ := http.NewRequest("GET", "http://"+addr.String(), nil) preq := &ParsedHTTPRequest{ Req: req, @@ -432,7 +435,7 @@ func TestMakeRequestDialTimeout(t *testing.T) { ResponseCallback: func(i int) bool { return i == 0 }, } - res, err := MakeRequest(ctx, preq) + res, err := MakeRequest(ctx, state, preq) require.NoError(t, err) assert.NotNil(t, res) assert.Len(t, samples, 1) @@ -477,7 +480,6 @@ func TestMakeRequestTimeoutInTheBegining(t *testing.T) { BuiltinMetrics: metrics.RegisterBuiltinMetrics(registry), Tags: lib.NewTagMap(nil), } - ctx = lib.WithState(ctx, state) req, _ := http.NewRequest("GET", srv.URL, nil) preq := &ParsedHTTPRequest{ Req: req, @@ -487,7 +489,7 @@ func TestMakeRequestTimeoutInTheBegining(t *testing.T) { ResponseCallback: func(i int) bool { return i == 0 }, } - res, err := MakeRequest(ctx, preq) + res, err := MakeRequest(ctx, state, preq) require.NoError(t, err) assert.NotNil(t, res) assert.Len(t, samples, 1) diff --git a/lib/netext/httpext/response.go b/lib/netext/httpext/response.go index 2e41148390c..468e048529a 100644 --- a/lib/netext/httpext/response.go +++ b/lib/netext/httpext/response.go @@ -21,7 +21,6 @@ package httpext import ( - "context" "crypto/tls" "go.k6.io/k6/lib/netext" @@ -74,8 +73,6 @@ type HTTPCookie struct { // Response is a representation of an HTTP response type Response struct { - ctx context.Context - RemoteIP string `json:"remote_ip"` RemotePort int `json:"remote_port"` URL string `json:"url"` @@ -95,9 +92,8 @@ type Response struct { } // NewResponse returns an empty Response instance. -func NewResponse(ctx context.Context) *Response { +func NewResponse() *Response { return &Response{ - ctx: ctx, Body: []byte{}, } } @@ -108,8 +104,3 @@ func (res *Response) setTLSInfo(tlsState *tls.ConnectionState) { res.TLSCipherSuite = tlsInfo.CipherSuite res.OCSP = oscp } - -// GetCtx return the response context -func (res *Response) GetCtx() context.Context { - return res.ctx -} diff --git a/lib/state.go b/lib/state.go index a80012465d7..c314f055fd3 100644 --- a/lib/state.go +++ b/lib/state.go @@ -54,8 +54,11 @@ type State struct { Group *Group // Networking equipment. + Dialer DialContexter + + // TODO: move a lot of the things below to the k6/http ModuleInstance, see + // https://github.com/grafana/k6/issues/2293. Transport http.RoundTripper - Dialer DialContexter CookieJar *cookiejar.Jar TLSConfig *tls.Config diff --git a/samples/http_get.js b/samples/http_get.js index 6af937d0c6e..2eb256e407c 100644 --- a/samples/http_get.js +++ b/samples/http_get.js @@ -1,5 +1,5 @@ import http from 'k6/http'; export default function () { - const response = http.get("https://test-api.k6.io/"); + http.get('https://test-api.k6.io/'); };