From edac6cf717a69b6869c32cc63368762e0654790e Mon Sep 17 00:00:00 2001 From: openmohan Date: Thu, 24 Jan 2019 17:43:05 +0530 Subject: [PATCH] instead of dumping entire JSON, offset, line number and column numbers are displayed --- .golangci.yml | 2 + js/modules/k6/http/response_test.go | 3 +- lib/netext/httpext/response.go | 40 ++- lib/netext/httpext/response_test.go | 488 ++++++++++++++++++++++++++++ 4 files changed, 530 insertions(+), 3 deletions(-) create mode 100644 lib/netext/httpext/response_test.go diff --git a/.golangci.yml b/.golangci.yml index f7f3a4f1a137..e153d0a1ea40 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -24,6 +24,8 @@ linters-settings: goconst: min-len: 5 min-occurrences: 4 + lll: + line-length: 180 linters: enable-all: true diff --git a/js/modules/k6/http/response_test.go b/js/modules/k6/http/response_test.go index 36033729bd7a..e3edb2f9d97a 100644 --- a/js/modules/k6/http/response_test.go +++ b/js/modules/k6/http/response_test.go @@ -27,7 +27,6 @@ import ( "net/url" "testing" - "github.com/dop251/goja" "github.com/loadimpact/k6/js/common" "github.com/loadimpact/k6/lib/netext/httpext" "github.com/loadimpact/k6/stats" @@ -179,7 +178,7 @@ func TestResponse(t *testing.T) { t.Run("Invalid", func(t *testing.T) { _, err := common.RunString(rt, sr(`http.request("GET", "HTTPBIN_URL/html").json();`)) - assert.EqualError(t, err, "GoError: invalid character '<' looking for beginning of value\n Invalid character at offset 1\n <<--(Invalid Character)!DOCTYPE html>\n\n \n \n \n

Herman Melville - Moby-Dick

\n\n
\n

\n Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Perth, the begrimed, blistered old blacksmith, had not removed his portable forge to the hold again, after concluding his contributory work for Ahab's leg, but still retained it on deck, fast lashed to ringbolts by the foremast; being now almost incessantly invoked by the headsmen, and harpooneers, and bowsmen to do some little job for them; altering, or repairing, or new shaping their various weapons and boat furniture. Often he would be surrounded by an eager circle, all waiting to be served; holding boat-spades, pike-heads, harpoons, and lances, and jealously watching his every sooty movement, as he toiled. Nevertheless, this old man's was a patient hammer wielded by a patient arm. No murmur, no impatience, no petulance did come from him. Silent, slow, and solemn; bowing over still further his chronically broken back, he toiled away, as if toil were life itself, and the heavy beating of his hammer the heavy beating of his heart. And so it was.—Most miserable! A peculiar walk in this old man, a certain slight but painful appearing yawing in his gait, had at an early period of the voyage excited the curiosity of the mariners. And to the importunity of their persisted questionings he had finally given in; and so it came to pass that every one now knew the shameful story of his wretched fate. Belated, and not innocently, one bitter winter's midnight, on the road running between two country towns, the blacksmith half-stupidly felt the deadly numbness stealing over him, and sought refuge in a leaning, dilapidated barn. The issue was, the loss of the extremities of both feet. Out of this revelation, part by part, at last came out the four acts of the gladness, and the one long, and as yet uncatastrophied fifth act of the grief of his life's drama. He was an old man, who, at the age of nearly sixty, had postponedly encountered that thing in sorrow's technicals called ruin. He had been an artisan of famed excellence, and with plenty to do; owned a house and garden; embraced a youthful, daughter-like, loving wife, and three blithe, ruddy children; every Sunday went to a cheerful-looking church, planted in a grove. But one night, under cover of darkness, and further concealed in a most cunning disguisement, a desperate burglar slid into his happy home, and robbed them all of everything. And darker yet to tell, the blacksmith himself did ignorantly conduct this burglar into his family's heart. It was the Bottle Conjuror! Upon the opening of that fatal cork, forth flew the fiend, and shrivelled up his home. Now, for prudent, most wise, and economic reasons, the blacksmith's shop was in the basement of his dwelling, but with a separate entrance to it; so that always had the young and loving healthy wife listened with no unhappy nervousness, but with vigorous pleasure, to the stout ringing of her young-armed old husband's hammer; whose reverberations, muffled by passing through the floors and walls, came up to her, not unsweetly, in her nursery; and so, to stout Labor's iron lullaby, the blacksmith's infants were rocked to slumber. Oh, woe on woe! Oh, Death, why canst thou not sometimes be timely? Hadst thou taken this old blacksmith to thyself ere his full ruin came upon him, then had the young widow had a delicious grief, and her orphans a truly venerable, legendary sire to dream of in their after years; and all of them a care-killing competency.\n

\n
\n \n\n") + assert.EqualError(t, err, "GoError: cannot parse json schema due to a error at line 1, character 2 , error: invalid character '<' looking for beginning of value") }) }) t.Run("JsonSelector", func(t *testing.T) { diff --git a/lib/netext/httpext/response.go b/lib/netext/httpext/response.go index 0cb9a83a517b..b48c738efc76 100644 --- a/lib/netext/httpext/response.go +++ b/lib/netext/httpext/response.go @@ -24,6 +24,7 @@ import ( "context" "crypto/tls" "encoding/json" + "fmt" "net/http" "net/http/httputil" @@ -140,8 +141,19 @@ func (res *Response) JSON(selector ...string) (interface{}, error) { return nil, errors.New("invalid response type") } - if hasSelector { + if err := json.Unmarshal(body, &v); err != nil { + switch t := err.(type) { + case *json.SyntaxError: + err = checkErrorInJSON(body, int(t.Offset), err) + case *json.UnmarshalTypeError: + err = checkErrorInJSON(body, int(t.Offset), err) + default: + break + } + return nil, err + } + if hasSelector { if !res.validatedJSON { if !gjson.ValidBytes(body) { return nil, nil @@ -166,3 +178,29 @@ func (res *Response) JSON(selector ...string) (interface{}, error) { return res.cachedJSON, nil } + +func checkErrorInJSON(input []byte, offset int, err error) error { + lf := '\n' + str := string(input) + + if offset > len(str) || offset < 0 { + return fmt.Errorf("couldn't find line and character for '%s'", err.Error()) + } + + // Humans tend to count from 1. + line := 1 + character := 0 + + for i, b := range str { + if b == lf { + line++ + character = 0 + } + character++ + if i == offset { + break + } + } + + return fmt.Errorf("cannot parse json schema due to a error at line %d, character %d , error: %v", line, character, err) +} diff --git a/lib/netext/httpext/response_test.go b/lib/netext/httpext/response_test.go new file mode 100644 index 000000000000..96c18dc6eb4c --- /dev/null +++ b/lib/netext/httpext/response_test.go @@ -0,0 +1,488 @@ +package httpext_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "testing" + + "github.com/dop251/goja" + "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/metrics" + "github.com/loadimpact/k6/lib/netext/httpext" + "github.com/loadimpact/k6/lib/testutils" + "github.com/loadimpact/k6/stats" + "github.com/oxtoacart/bpool" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + null "gopkg.in/guregu/null.v3" +) + +const testGetFormHTML = ` + + + This is the title + + +
+ + + + + +
+ +` +const jsonData = `{"glossary": { + "friends": [ + {"first": "Dale", "last": "Murphy", "age": 44}, + {"first": "Roger", "last": "Craig", "age": 68}, + {"first": "Jane", "last": "Murphy", "age": 47}], + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Markup Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "int": 1123456, + "null": null, + "intArray": [1,2,3], + "mixedArray": ["123",123,true,null], + "boolean": true, + "title": "example glossary", + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML","XML"]}, + "GlossSee": "markup"}}}}}` + +func myFormHandler(w http.ResponseWriter, r *http.Request) { + var body []byte + var err error + if r.URL.RawQuery != "" { + body, err = json.Marshal(struct { + Query url.Values `json:"query"` + }{ + Query: r.URL.Query(), + }) + if err != nil { + body = []byte(`{"error": "failed serializing json"}`) + } + w.Header().Set("Content-Type", "application/json") + } else { + w.Header().Set("Content-Type", "text/html") + body = []byte(testGetFormHTML) + } + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) + w.WriteHeader(200) + _, _ = w.Write(body) +} + +func jsonHandler(w http.ResponseWriter, r *http.Request) { + body := []byte(jsonData) + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) + w.WriteHeader(200) + _, _ = w.Write(body) +} + +func TestResponse(t *testing.T) { + tb, state, samples, rt, _ := newRuntime(t) + defer tb.Cleanup() + root := state.Group + sr := tb.Replacer.Replace + + tb.Mux.HandleFunc("/myforms/get", myFormHandler) + tb.Mux.HandleFunc("/json", jsonHandler) + + t.Run("Html", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/html"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + if (res.body.indexOf("Herman Melville - Moby-Dick") == -1) { throw new Error("wrong body: " + res.body); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/html"), "", 200, "") + + t.Run("html", func(t *testing.T) { + _, err := common.RunString(rt, ` + if (res.html().find("h1").text() != "Herman Melville - Moby-Dick") { throw new Error("wrong title: " + res.body); } + `) + assert.NoError(t, err) + + t.Run("shorthand", func(t *testing.T) { + _, err := common.RunString(rt, ` + if (res.html("h1").text() != "Herman Melville - Moby-Dick") { throw new Error("wrong title: " + res.body); } + `) + assert.NoError(t, err) + }) + + t.Run("url", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + if (res.html().url != "HTTPBIN_URL/html") { throw new Error("url incorrect: " + res.html().url); } + `)) + assert.NoError(t, err) + }) + }) + + t.Run("group", func(t *testing.T) { + g, err := root.Group("my group") + if assert.NoError(t, err) { + old := state.Group + state.Group = g + defer func() { state.Group = old }() + } + + _, err = common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/html"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + if (res.body.indexOf("Herman Melville - Moby-Dick") == -1) { throw new Error("wrong body: " + res.body); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/html"), "", 200, "::my group") + }) + }) + t.Run("Json", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/get?a=1&b=2"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + if (res.json().args.a != "1") { throw new Error("wrong ?a: " + res.json().args.a); } + if (res.json().args.b != "2") { throw new Error("wrong ?b: " + res.json().args.b); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/get?a=1&b=2"), "", 200, "") + + t.Run("Invalid", func(t *testing.T) { + _, err := common.RunString(rt, sr(`http.request("GET", "HTTPBIN_URL/html").json();`)) + assert.EqualError(t, err, "GoError: cannot parse json schema due to a error at line 1, character 2 , error: invalid character '<' looking for beginning of value") + }) + }) + t.Run("JsonSelector", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/json"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + + var value = res.json("glossary.friends.1") + if (typeof value != "object") + { throw new Error("wrong type of result value: " + value); } + if (value["first"] != "Roger") + { throw new Error("Expected Roger for key first but got: " + value["first"]); } + + value = res.json("glossary.int1") + if (value != undefined) + { throw new Error("Expected undefined, but got: " + value); } + + value = res.json("glossary.null") + if (value != null) + { throw new Error("Expected null, but got: " + value); } + + value = res.json("glossary.GlossDiv.GlossList.GlossEntry.GlossDef.intArray.#") + if (value != 3) + { throw new Error("Expected num 3, but got: " + value); } + + value = res.json("glossary.GlossDiv.GlossList.GlossEntry.GlossDef.intArray")[2] + if (value != 3) + { throw new Error("Expected, num 3, but got: " + value); } + + value = res.json("glossary.GlossDiv.GlossList.GlossEntry.GlossDef.boolean") + if (value != true) + { throw new Error("Expected boolean true, but got: " + value); } + + value = res.json("glossary.GlossDiv.GlossList.GlossEntry.GlossDef.title") + if (value != "example glossary") + { throw new Error("Expected 'example glossary'', but got: " + value); } + + value = res.json("glossary.friends.#.first")[0] + if (value != "Dale") + { throw new Error("Expected 'Dale', but got: " + value); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/json"), "", 200, "") + }) + + t.Run("SubmitForm", func(t *testing.T) { + t.Run("withoutArgs", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/forms/post"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.submitForm() + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + let data = res.json().form + if (data.custname[0] !== "" || + data.extradata !== undefined || + data.comments[0] !== "" || + data.custemail[0] !== "" || + data.custtel[0] !== "" || + data.delivery[0] !== "" + ) { throw new Error("incorrect body: " + JSON.stringify(data, null, 4) ); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "POST", sr("HTTPBIN_URL/post"), "", 200, "") + }) + + t.Run("withFields", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/forms/post"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.submitForm({ fields: { custname: "test", extradata: "test2" } }) + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + let data = res.json().form + if (data.custname[0] !== "test" || + data.extradata[0] !== "test2" || + data.comments[0] !== "" || + data.custemail[0] !== "" || + data.custtel[0] !== "" || + data.delivery[0] !== "" + ) { throw new Error("incorrect body: " + JSON.stringify(data, null, 4) ); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "POST", sr("HTTPBIN_URL/post"), "", 200, "") + }) + + t.Run("withRequestParams", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/forms/post"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.submitForm({ params: { headers: { "My-Fancy-Header": "SomeValue" } }}) + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + let headers = res.json().headers + if (headers["My-Fancy-Header"][0] !== "SomeValue" ) { throw new Error("incorrect headers: " + JSON.stringify(headers)); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "POST", sr("HTTPBIN_URL/post"), "", 200, "") + }) + + t.Run("withFormSelector", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/forms/post"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.submitForm({ formSelector: 'form[method="post"]' }) + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + let data = res.json().form + if (data.custname[0] !== "" || + data.extradata !== undefined || + data.comments[0] !== "" || + data.custemail[0] !== "" || + data.custtel[0] !== "" || + data.delivery[0] !== "" + ) { throw new Error("incorrect body: " + JSON.stringify(data, null, 4) ); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "POST", sr("HTTPBIN_URL/post"), "", 200, "") + }) + + t.Run("withNonExistentForm", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/forms/post"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res.submitForm({ formSelector: "#doesNotExist" }) + `)) + assert.EqualError(t, err, sr("GoError: no form found for selector '#doesNotExist' in response 'HTTPBIN_URL/forms/post'")) + }) + + t.Run("withGetMethod", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/myforms/get"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.submitForm() + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + let data = res.json().query + if (data.input_with_value[0] !== "value" || + data.input_without_value[0] !== "" || + data.select_one[0] !== "yes this option" || + data.select_multi[0] !== "option 2,option 3" || + data.textarea[0] !== "Lorem ipsum dolor sit amet" + ) { throw new Error("incorrect body: " + JSON.stringify(data, null, 4) ); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/myforms/get"), "", 200, "") + }) + }) + + t.Run("ClickLink", func(t *testing.T) { + t.Run("withoutArgs", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/links/10/0"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.clickLink() + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/links/10/1"), "", 200, "") + }) + + t.Run("withSelector", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/links/10/0"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.clickLink({ selector: 'a:nth-child(4)' }) + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/links/10/4"), "", 200, "") + }) + + t.Run("withNonExistentLink", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL/links/10/0"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.clickLink({ selector: 'a#doesNotExist' }) + `)) + assert.EqualError(t, err, sr("GoError: no element found for selector 'a#doesNotExist' in response 'HTTPBIN_URL/links/10/0'")) + }) + + t.Run("withRequestParams", func(t *testing.T) { + _, err := common.RunString(rt, sr(` + let res = http.request("GET", "HTTPBIN_URL"); + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + res = res.clickLink({ selector: 'a[href="/get"]', params: { headers: { "My-Fancy-Header": "SomeValue" } } }) + if (res.status != 200) { throw new Error("wrong status: " + res.status); } + let headers = res.json().headers + if (headers["My-Fancy-Header"][0] !== "SomeValue" ) { throw new Error("incorrect headers: " + JSON.stringify(headers)); } + `)) + assert.NoError(t, err) + assertRequestMetricsEmitted(t, stats.GetBufferedSamples(samples), "GET", sr("HTTPBIN_URL/get"), "", 200, "") + }) + }) +} + +func BenchmarkResponseJson(b *testing.B) { + 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 { + 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.JSON(tc.selector) + } + }) + } + + b.Run("Without selector", func(b *testing.B) { + for n := 0; n < b.N; n++ { + resp := responseFromHttpext(&httpext.Response{Body: jsonData}) + resp.JSON() + } + }) +} + +func newRuntime( + t *testing.T, +) (*testutils.HTTPMultiBin, *lib.State, chan stats.SampleContainer, *goja.Runtime, *context.Context) { + tb := testutils.NewHTTPMultiBin(t) + + root, err := lib.NewGroup("", nil) + require.NoError(t, err) + + 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"), + Throw: null.BoolFrom(true), + SystemTags: lib.GetTagSet(lib.DefaultSystemTagList...), + //HttpDebug: null.StringFrom("full"), + } + samples := make(chan stats.SampleContainer, 1000) + + state := &lib.State{ + Options: options, + Logger: logger, + Group: root, + TLSConfig: tb.TLSClientConfig, + Transport: tb.HTTPTransport, + BPool: bpool.NewBufferPool(1), + Samples: samples, + } + + ctx := new(context.Context) + *ctx = context.Background() + *ctx = lib.WithState(*ctx, state) + *ctx = common.WithRuntime(*ctx, rt) + rt.Set("http", common.Bind(rt, k6http.New(), ctx)) + + return tb, state, samples, rt, ctx +} + +func assertRequestMetricsEmitted(t *testing.T, sampleContainers []stats.SampleContainer, method, url, name string, status int, group string) { + if name == "" { + name = url + } + + seenDuration := false + seenBlocked := false + seenConnecting := false + seenTLSHandshaking := false + seenSending := false + seenWaiting := false + seenReceiving := false + for _, sampleContainer := range sampleContainers { + for _, sample := range sampleContainer.GetSamples() { + tags := sample.Tags.CloneTags() + if tags["url"] == url { + switch sample.Metric { + case metrics.HTTPReqDuration: + seenDuration = true + case metrics.HTTPReqBlocked: + seenBlocked = true + case metrics.HTTPReqConnecting: + seenConnecting = true + case metrics.HTTPReqTLSHandshaking: + seenTLSHandshaking = true + case metrics.HTTPReqSending: + seenSending = true + case metrics.HTTPReqWaiting: + seenWaiting = true + case metrics.HTTPReqReceiving: + seenReceiving = true + } + + assert.Equal(t, strconv.Itoa(status), tags["status"]) + assert.Equal(t, method, tags["method"]) + assert.Equal(t, group, tags["group"]) + assert.Equal(t, name, tags["name"]) + } + } + } + assert.True(t, seenDuration, "url %s didn't emit Duration", url) + assert.True(t, seenBlocked, "url %s didn't emit Blocked", url) + assert.True(t, seenConnecting, "url %s didn't emit Connecting", url) + assert.True(t, seenTLSHandshaking, "url %s didn't emit TLSHandshaking", url) + assert.True(t, seenSending, "url %s didn't emit Sending", url) + assert.True(t, seenWaiting, "url %s didn't emit Waiting", url) + assert.True(t, seenReceiving, "url %s didn't emit Receiving", url) +} + +func responseFromHttpext(resp *httpext.Response) *httpext.Response { + res := httpext.Response(*resp) + return &res +}