diff --git a/js/internal/modules/modules.go b/js/internal/modules/modules.go index 058fe459ae0..2dcfb38f34c 100644 --- a/js/internal/modules/modules.go +++ b/js/internal/modules/modules.go @@ -35,7 +35,13 @@ var ( func Get(name string) interface{} { mx.RLock() defer mx.RUnlock() - return modules[name] + mod := modules[name] + if i, ok := mod.(interface { + NewGlobalModule() interface{ NewModuleInstance() interface{} } + }); ok { + return i.NewGlobalModule().NewModuleInstance() + } + return mod } // Register the given mod as a JavaScript module, available diff --git a/js/modules/k6/http/http.go b/js/modules/k6/http/http.go index 8aa6cdab2dc..1ff41b2200c 100644 --- a/js/modules/k6/http/http.go +++ b/js/modules/k6/http/http.go @@ -30,7 +30,7 @@ import ( ) func init() { - modules.Register("k6/http", New()) + modules.Register("k6/http", new(RootModule)) } const ( @@ -46,6 +46,44 @@ const ( // 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") +// RootModule is only used to be registered and then to create a global (per k6 instance) module instance +type RootModule struct{} + +// NewGlobalModule return a new GlobalHTTP +func (r RootModule) NewGlobalModule() interface{ NewModuleInstance() interface{} } { + return &GlobalHTTP{} +} + +// GlobalHTTP is a global HTTP module for a k6 instance/test run +type GlobalHTTP struct{} + +// NewModuleInstance returns an HTTP instance for each VU +func (g *GlobalHTTP) NewModuleInstance() interface{} { + return &HTTP{ // change the below fields to be not writable or not fields + SSL_3_0: netext.SSL_3_0, + 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, + + responseCallback: defaultExpectedStatuses.match, + } +} + //nolint: golint type HTTP struct { SSL_3_0 string `js:"SSL_3_0"` @@ -67,10 +105,14 @@ type HTTP struct { 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"` + + responseCallback func(int) bool } +// New ... +// TODO deprecate this method func New() *HTTP { - //TODO: move this as an anonymous struct somewhere... + // TODO: move this as an anonymous struct somewhere... return &HTTP{ SSL_3_0: netext.SSL_3_0, TLS_1_0: netext.TLS_1_0, diff --git a/js/modules/k6/http/request.go b/js/modules/k6/http/request.go index f8604c894ee..22f18ab9dac 100644 --- a/js/modules/k6/http/request.go +++ b/js/modules/k6/http/request.go @@ -113,7 +113,7 @@ func (h *HTTP) Request(ctx context.Context, method string, url goja.Value, args if err != nil { return nil, err } - return responseFromHttpext(resp), nil + return h.responseFromHttpext(resp), nil } //TODO break this function up @@ -139,7 +139,7 @@ func (h *HTTP) parseRequest( Redirects: state.Options.MaxRedirects, Cookies: make(map[string]*httpext.HTTPRequestCookie), Tags: make(map[string]string), - ResponseCallback: state.HTTPResponseCallback, + ResponseCallback: h.responseCallback, } if state.Options.DiscardResponseBodies.Bool { @@ -388,7 +388,7 @@ func (h *HTTP) prepareBatchArray( ParsedHTTPRequest: parsedReq, Response: response, } - results[i] = &Response{response} + results[i] = h.responseFromHttpext(response) } return batchReqs, results, nil @@ -412,7 +412,7 @@ func (h *HTTP) prepareBatchObject( ParsedHTTPRequest: parsedReq, Response: response, } - results[key] = &Response{response} + results[key] = h.responseFromHttpext(response) i++ } diff --git a/js/modules/k6/http/response.go b/js/modules/k6/http/response.go index 8deb1b2462c..34506d7225c 100644 --- a/js/modules/k6/http/response.go +++ b/js/modules/k6/http/response.go @@ -36,11 +36,11 @@ import ( // Response is a representation of an HTTP response to be returned to the goja VM type Response struct { *httpext.Response `js:"-"` + h *HTTP } -func responseFromHttpext(resp *httpext.Response) *Response { - res := Response{resp} - return &res +func (h *HTTP) responseFromHttpext(resp *httpext.Response) *Response { + return &Response{Response: resp, h: h} } // JSON parses the body of a response as json and returns it to the goja VM @@ -159,9 +159,9 @@ func (res *Response) SubmitForm(args ...goja.Value) (*Response, error) { q.Add(k, v.String()) } requestURL.RawQuery = q.Encode() - return New().Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), goja.Null(), requestParams) + return res.h.Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), goja.Null(), requestParams) } - return New().Request(res.GetCtx(), requestMethod, rt.ToValue(requestURL.String()), rt.ToValue(values), requestParams) + return res.h.Request(res.GetCtx(), 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 @@ -202,5 +202,5 @@ func (res *Response) ClickLink(args ...goja.Value) (*Response, error) { } requestURL := responseURL.ResolveReference(hrefURL) - return New().Get(res.GetCtx(), rt.ToValue(requestURL.String()), requestParams) + return res.h.Get(res.GetCtx(), rt.ToValue(requestURL.String()), requestParams) } diff --git a/js/modules/k6/http/response_callback.go b/js/modules/k6/http/response_callback.go index bbe9168c29b..cd604f9661d 100644 --- a/js/modules/k6/http/response_callback.go +++ b/js/modules/k6/http/response_callback.go @@ -27,7 +27,6 @@ import ( "github.com/dop251/goja" "github.com/loadimpact/k6/js/common" - "github.com/loadimpact/k6/lib" ) //nolint:gochecknoglobals @@ -35,11 +34,6 @@ var defaultExpectedStatuses = expectedStatuses{ minmax: [][2]int{{200, 399}}, } -// DefaultHTTPResponseCallback ... -func DefaultHTTPResponseCallback() func(int) bool { - return defaultExpectedStatuses.match -} - type expectedStatuses struct { minmax [][2]int exact []int // this can be done with the above and vice versa @@ -102,10 +96,15 @@ func checkNumber(a goja.Value, rt *goja.Runtime) bool { } // SetResponseCallback .. -func (h HTTP) SetResponseCallback(ctx context.Context, es *expectedStatuses) { - if es != nil { - lib.GetState(ctx).HTTPResponseCallback = es.match +func (h *HTTP) SetResponseCallback(ctx context.Context, val goja.Value) { + if val != nil && !goja.IsNull(val) { + if es, ok := val.Export().(*expectedStatuses); ok { + h.responseCallback = es.match + } else { + //nolint:golint + common.Throw(common.GetRuntime(ctx), fmt.Errorf("unsupported argument, expected http.expectedStatuses")) + } } else { - lib.GetState(ctx).HTTPResponseCallback = nil + h.responseCallback = nil } } diff --git a/js/modules/k6/http/response_callback_test.go b/js/modules/k6/http/response_callback_test.go index 1b123561535..e5a0409855b 100644 --- a/js/modules/k6/http/response_callback_test.go +++ b/js/modules/k6/http/response_callback_test.go @@ -41,7 +41,7 @@ func TestExpectedStatuses(t *testing.T) { ctx := context.Background() ctx = common.WithRuntime(ctx, rt) - rt.Set("http", common.Bind(rt, New(), &ctx)) + rt.Set("http", common.Bind(rt, new(RootModule).NewGlobalModule().NewModuleInstance(), &ctx)) cases := map[string]struct { code, err string expected expectedStatuses @@ -107,9 +107,11 @@ type expectedSample struct { func TestResponseCallbackInAction(t *testing.T) { t.Parallel() - tb, state, samples, rt, _ := newRuntime(t) + tb, _, samples, rt, ctx := newRuntime(t) defer tb.Cleanup() sr := tb.Replacer.Replace + httpModule := new(RootModule).NewGlobalModule().NewModuleInstance().(*HTTP) + rt.Set("http", common.Bind(rt, httpModule, ctx)) HTTPMetricsWithoutFailed := []*stats.Metric{ metrics.HTTPReqs, @@ -280,7 +282,7 @@ func TestResponseCallbackInAction(t *testing.T) { for name, testCase := range testCases { testCase := testCase t.Run(name, func(t *testing.T) { - state.HTTPResponseCallback = DefaultHTTPResponseCallback() + httpModule.responseCallback = defaultExpectedStatuses.match _, err := rt.RunString(sr(testCase.code)) assert.NoError(t, err) @@ -306,7 +308,7 @@ func TestResponseCallbackInAction(t *testing.T) { func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) { t.Parallel() - tb, state, samples, rt, _ := newRuntime(t) + tb, state, samples, rt, ctx := newRuntime(t) defer tb.Cleanup() sr := tb.Replacer.Replace allHTTPMetrics := []*stats.Metric{ @@ -321,8 +323,8 @@ func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) { metrics.HTTPReqTLSHandshaking, } deleteSystemTag(state, stats.TagPassed.String()) - - state.HTTPResponseCallback = DefaultHTTPResponseCallback() + httpModule := new(RootModule).NewGlobalModule().NewModuleInstance().(*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) @@ -364,10 +366,11 @@ func TestResponseCallbackInActionWithoutPassedTag(t *testing.T) { func TestDigestWithResponseCallback(t *testing.T) { t.Parallel() - tb, state, samples, rt, _ := newRuntime(t) + tb, _, samples, rt, ctx := newRuntime(t) defer tb.Cleanup() - state.HTTPResponseCallback = DefaultHTTPResponseCallback() + httpModule := new(RootModule).NewGlobalModule().NewModuleInstance().(*HTTP) + rt.Set("http", common.Bind(rt, httpModule, ctx)) 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 94a5ddac42e..6b346e11754 100644 --- a/js/modules/k6/http/response_test.go +++ b/js/modules/k6/http/response_test.go @@ -55,6 +55,7 @@ const testGetFormHTML = ` ` + const jsonData = `{"glossary": { "friends": [ {"first": "Dale", "last": "Murphy", "age": 44}, @@ -413,7 +414,7 @@ func BenchmarkResponseJson(b *testing.B) { tc := tc b.Run(fmt.Sprintf("Selector %s ", tc.selector), func(b *testing.B) { for n := 0; n < b.N; n++ { - resp := responseFromHttpext(&httpext.Response{Body: jsonData}) + resp := new(HTTP).responseFromHttpext(&httpext.Response{Body: jsonData}) resp.JSON(tc.selector) } }) @@ -421,7 +422,7 @@ func BenchmarkResponseJson(b *testing.B) { b.Run("Without selector", func(b *testing.B) { for n := 0; n < b.N; n++ { - resp := responseFromHttpext(&httpext.Response{Body: jsonData}) + resp := new(HTTP).responseFromHttpext(&httpext.Response{Body: jsonData}) resp.JSON() } }) diff --git a/js/runner.go b/js/runner.go index ed72dfeeb88..9651c3e32b9 100644 --- a/js/runner.go +++ b/js/runner.go @@ -43,7 +43,6 @@ import ( "golang.org/x/time/rate" "github.com/loadimpact/k6/js/common" - k6http "github.com/loadimpact/k6/js/modules/k6/http" "github.com/loadimpact/k6/lib" "github.com/loadimpact/k6/lib/consts" "github.com/loadimpact/k6/lib/netext" @@ -218,20 +217,19 @@ func (r *Runner) newVU(id int64, samplesOut chan<- stats.SampleContainer) (*VU, } vu.state = &lib.State{ - Logger: vu.Runner.Logger, - Options: vu.Runner.Bundle.Options, - Transport: vu.Transport, - Dialer: vu.Dialer, - TLSConfig: vu.TLSConfig, - CookieJar: cookieJar, - RPSLimit: vu.Runner.RPSLimit, - BPool: vu.BPool, - Vu: vu.ID, - Samples: vu.Samples, - Iteration: vu.Iteration, - Tags: vu.Runner.Bundle.Options.RunTags.CloneTags(), - Group: r.defaultGroup, - HTTPResponseCallback: k6http.DefaultHTTPResponseCallback(), // TODO maybe move it to lib after all :sign: + Logger: vu.Runner.Logger, + Options: vu.Runner.Bundle.Options, + Transport: vu.Transport, + Dialer: vu.Dialer, + TLSConfig: vu.TLSConfig, + CookieJar: cookieJar, + RPSLimit: vu.Runner.RPSLimit, + BPool: vu.BPool, + Vu: vu.ID, + Samples: vu.Samples, + Iteration: vu.Iteration, + Tags: vu.Runner.Bundle.Options.RunTags.CloneTags(), + Group: r.defaultGroup, } vu.Runtime.Set("console", common.Bind(vu.Runtime, vu.Console, vu.Context)) diff --git a/lib/state.go b/lib/state.go index eb428caee88..63c6f93a83f 100644 --- a/lib/state.go +++ b/lib/state.go @@ -69,8 +69,6 @@ type State struct { Vu, Iteration int64 Tags map[string]string - - HTTPResponseCallback func(int) bool } // CloneTags makes a copy of the tags map and returns it.