Merge pull request #1283 from httprunner/httpstat

feat: stat HTTP request latencies (DNSLookup, TCP Connection and so on)
This commit is contained in:
debugtalk
2022-05-05 21:52:01 +08:00
committed by GitHub
21 changed files with 422 additions and 17 deletions

View File

@@ -27,7 +27,7 @@
## 核心特性
- 网络协议:完整支持 HTTP(S)/1.1 和 HTTP/2可扩展支持 WebSocket/TCP/RPC 等更多协议
- 网络协议:完整支持 HTTP(S)/HTTP2/WebSocket可扩展支持 TCP/UDP/RPC 等更多协议
- 多格式可选:测试用例支持 YAML/JSON/go test/pytest 格式,并且支持格式互相转换
- 双执行引擎:同时支持 golang/python 两个执行引擎,兼具 go 的高性能和 [pytest] 的丰富生态
- 录制 & 生成:可使用 [HAR]/Postman/Swagger/curl 等生成测试用例;基于链式调用的方法提示也可快速编写测试用例

View File

@@ -1,9 +1,10 @@
# Release History
## v4.0.0-beta2 (2022-05-03)
## v4.0.0 (2022-05-05)
**go version**
- feat: stat HTTP request latencies (DNSLookup, TCP Connection and so on)
- feat: add builtin function `environ`/`ENV`
- fix: demo function compatibility
- fix #1240: losing host port in har2case

View File

@@ -36,4 +36,4 @@ Copyright 2017 debugtalk
* [hrp run](hrp_run.md) - run API test with go engine
* [hrp startproject](hrp_startproject.md) - create a scaffold project
###### Auto generated by spf13/cobra on 3-May-2022
###### Auto generated by spf13/cobra on 5-May-2022

View File

@@ -41,4 +41,4 @@ hrp boom [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 3-May-2022
###### Auto generated by spf13/cobra on 5-May-2022

View File

@@ -18,4 +18,4 @@ hrp convert $path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 3-May-2022
###### Auto generated by spf13/cobra on 5-May-2022

View File

@@ -24,4 +24,4 @@ hrp har2case $har_path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 3-May-2022
###### Auto generated by spf13/cobra on 5-May-2022

View File

@@ -16,4 +16,4 @@ hrp pytest $path ... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 3-May-2022
###### Auto generated by spf13/cobra on 5-May-2022

View File

@@ -24,6 +24,7 @@ hrp run $path... [flags]
-c, --continue-on-failure continue running next step when failure occurs
-g, --gen-html-report generate html report
-h, --help help for run
--http-stat turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)
--log-plugin turn on plugin logging
--log-requests-off turn off request & response details logging
-p, --proxy-url string set proxy url
@@ -34,4 +35,4 @@ hrp run $path... [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 3-May-2022
###### Auto generated by spf13/cobra on 5-May-2022

View File

@@ -20,4 +20,4 @@ hrp startproject $project_name [flags]
* [hrp](hrp.md) - Next-Generation API Testing Solution.
###### Auto generated by spf13/cobra on 3-May-2022
###### Auto generated by spf13/cobra on 5-May-2022

View File

@@ -2,4 +2,4 @@ module plugin
go 1.16
require github.com/httprunner/funplugin v0.4.3 // indirect
require github.com/httprunner/funplugin v0.4.5 // indirect

View File

@@ -58,8 +58,8 @@ github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v
github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/httprunner/funplugin v0.4.3 h1:mxdxQh54NZLQnK/FXZxpZV0rhqZQzckrWKEnBW5w2Vg=
github.com/httprunner/funplugin v0.4.3/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/httprunner/funplugin v0.4.5 h1:2KCj5AZZA22OER6TN5P/PSBYFMiKpgTmCRbDmHB1tos=
github.com/httprunner/funplugin v0.4.5/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=

3
go.mod
View File

@@ -5,10 +5,11 @@ go 1.16
require (
github.com/andybalholm/brotli v1.0.4
github.com/denisbrodbeck/machineid v1.0.1
github.com/fatih/color v1.13.0
github.com/getsentry/sentry-go v0.13.0
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.1
github.com/httprunner/funplugin v0.4.4
github.com/httprunner/funplugin v0.4.5
github.com/jinzhu/copier v0.3.2
github.com/jmespath/go-jmespath v0.4.0
github.com/json-iterator/go v1.1.12

8
go.sum
View File

@@ -106,8 +106,9 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -241,8 +242,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/httprunner/funplugin v0.4.4 h1:IVt603Y57WfSbn6DZ0R4iLeGQJ1yj944gmYwEOSBzGo=
github.com/httprunner/funplugin v0.4.4/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/httprunner/funplugin v0.4.5 h1:2KCj5AZZA22OER6TN5P/PSBYFMiKpgTmCRbDmHB1tos=
github.com/httprunner/funplugin v0.4.5/go.mod h1:vPyeJIfbpGe0epZZtAV0wCn16gLY9+imSw/zfxq0Lcc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
@@ -303,6 +304,7 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=

View File

@@ -35,6 +35,9 @@ var runCmd = &cobra.Command{
if !requestsLogOff {
runner.SetRequestsLogOn()
}
if httpStatOn {
runner.SetHTTPStatOn()
}
if pluginLogOn {
runner.SetPluginLogOn()
}
@@ -51,6 +54,7 @@ var runCmd = &cobra.Command{
var (
continueOnFailure bool
requestsLogOff bool
httpStatOn bool
pluginLogOn bool
proxyUrl string
saveTests bool
@@ -61,6 +65,7 @@ func init() {
rootCmd.AddCommand(runCmd)
runCmd.Flags().BoolVarP(&continueOnFailure, "continue-on-failure", "c", false, "continue running next step when failure occurs")
runCmd.Flags().BoolVar(&requestsLogOff, "log-requests-off", false, "turn off request & response details logging")
runCmd.Flags().BoolVar(&httpStatOn, "http-stat", false, "turn on HTTP latency stat (DNSLookup, TCP Connection, etc.)")
runCmd.Flags().BoolVar(&pluginLogOn, "log-plugin", false, "turn on plugin logging")
runCmd.Flags().StringVarP(&proxyUrl, "proxy-url", "p", "", "set proxy url")
runCmd.Flags().BoolVarP(&saveTests, "save-tests", "s", false, "save tests summary")

View File

@@ -0,0 +1,268 @@
// Package httpstat traces HTTP latency infomation (DNSLookup, TCP Connection and so on) on any golang HTTP request.
// It uses `httptrace` package.
// Inspired by https://github.com/tcnksm/go-httpstat and https://github.com/davecheney/httpstat
package httpstat
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"net/http/httptrace"
"strconv"
"strings"
"time"
"github.com/fatih/color"
"github.com/rs/zerolog/log"
)
const (
httpsTemplate = "\n" +
` DNS Lookup TCP Connection TLS Handshake Server Processing Content Transfer` + "\n" +
`[%s | %s | %s | %s | %s ]` + "\n" +
` | | | | |` + "\n" +
` namelookup:%s | | | |` + "\n" +
` connect:%s | | |` + "\n" +
` pretransfer:%s | |` + "\n" +
` starttransfer:%s |` + "\n" +
` total:%s` + "\n\n"
httpTemplate = "\n" +
` DNS Lookup TCP Connection Server Processing Content Transfer` + "\n" +
`[ %s | %s | %s | %s ]` + "\n" +
` | | | |` + "\n" +
` namelookup:%s | | |` + "\n" +
` connect:%s | |` + "\n" +
` starttransfer:%s |` + "\n" +
` total:%s` + "\n\n"
)
func fmta(d time.Duration) string {
return color.BlueString("%7dms", int(d.Milliseconds()))
}
func fmtb(d time.Duration) string {
return color.MagentaString("%-9s", strconv.Itoa(int(d.Milliseconds()))+"ms")
}
func grayscale(code color.Attribute) func(string, ...interface{}) string {
return color.New(code + 232).SprintfFunc()
}
func colorize(s string) string {
v := strings.Split(s, "\n")
v[0] = grayscale(16)(v[0])
return strings.Join(v, "\n")
}
func printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(color.Output, format, a...)
}
// Stat stores httpstat info.
type Stat struct {
// The following are duration for each phase
// DNSLookup => TCPConnection => TLSHandshake => ServerProcessing => ContentTransfer
DNSLookup time.Duration
TCPConnection time.Duration
TLSHandshake time.Duration
ServerProcessing time.Duration
ContentTransfer time.Duration // from the first response byte to tansfer done.
// The followings are timeline of request
NameLookup time.Duration // = DNSLookup
Connect time.Duration // = DNSLookup + TCPConnection
Pretransfer time.Duration // = DNSLookup + TCPConnection + TLSHandshake
StartTransfer time.Duration // = DNSLookup + TCPConnection + TLSHandshake + ServerProcessing
Total time.Duration // = DNSLookup + TCPConnection + TLSHandshake + ServerProcessing + ContentTransfer
// internal timelines, including start and finish timestamps of each phase
dnsStart time.Time
dnsDone time.Time
tcpStart time.Time
tcpDone time.Time
tlsStart time.Time
tlsDone time.Time
serverStart time.Time
serverDone time.Time
transferStart time.Time
transferDone time.Time // need to be provided from outside
// isTLS is true when connection seems to use TLS
isTLS bool
// isReused is true when connection is reused (keep-alive)
isReused bool
// https or http
schema string
// connected network info
network, addr string
}
// Finish sets the time when reading response is done.
// This must be called after reading response body.
func (s *Stat) Finish() {
s.transferDone = time.Now()
// This means result is empty (it does nothing).
// Skip setting value (contentTransfer and total will be zero).
if s.dnsStart.IsZero() {
return
}
s.ContentTransfer = s.transferDone.Sub(s.transferStart)
s.Total = s.transferDone.Sub(s.dnsStart)
}
// Durations returns all durations and timelines of request latencies
func (s *Stat) Durations() map[string]int64 {
return map[string]int64{
"DNSLookup": s.DNSLookup.Milliseconds(),
"TCPConnection": s.TCPConnection.Milliseconds(),
"TLSHandshake": s.TLSHandshake.Milliseconds(),
"ServerProcessing": s.ServerProcessing.Milliseconds(),
"ContentTransfer": s.ContentTransfer.Milliseconds(),
"NameLookup": s.NameLookup.Milliseconds(),
"Connect": s.Connect.Milliseconds(),
"Pretransfer": s.Pretransfer.Milliseconds(),
"StartTransfer": s.StartTransfer.Milliseconds(),
"Total": s.Total.Milliseconds(),
}
}
func (s *Stat) Print() {
if s.network != "" && s.addr != "" {
printf("\n%s %s: %s\n",
color.CyanString("Connected to"),
color.YellowString(s.network),
color.BlueString(s.addr),
)
}
switch s.schema {
case "https":
printf(colorize(httpsTemplate),
fmta(s.DNSLookup), // dns lookup
fmta(s.TCPConnection), // tcp connection
fmta(s.TLSHandshake), // tls handshake
fmta(s.ServerProcessing), // server processing
fmta(s.ContentTransfer), // content transfer
fmtb(s.NameLookup), // namelookup
fmtb(s.Connect), // connect
fmtb(s.Pretransfer), // pretransfer
fmtb(s.StartTransfer), // starttransfer
fmtb(s.Total), // total
)
case "http":
printf(colorize(httpTemplate),
fmta(s.DNSLookup), // dns lookup
fmta(s.TCPConnection), // tcp connection
fmta(s.ServerProcessing), // server processing
fmta(s.ContentTransfer), // content transfer
fmtb(s.NameLookup), // namelookup
fmtb(s.Connect), // connect
fmtb(s.StartTransfer), // starttransfer
fmtb(s.Total), // total
)
}
log.Info().
Interface("httpstat(ms)", s.Durations()).
Msg("HTTP latency statistics")
}
// WithHTTPStat is a wrapper of httptrace.WithClientTrace.
// It records the time of each httptrace hooks.
func WithHTTPStat(req *http.Request, s *Stat) context.Context {
s.schema = req.URL.Scheme
return httptrace.WithClientTrace(req.Context(), &httptrace.ClientTrace{
DNSStart: func(i httptrace.DNSStartInfo) {
s.dnsStart = time.Now()
},
DNSDone: func(i httptrace.DNSDoneInfo) {
s.dnsDone = time.Now()
s.DNSLookup = s.dnsDone.Sub(s.dnsStart)
s.NameLookup = s.DNSLookup
},
ConnectStart: func(network, addr string) {
s.network = network
s.addr = addr
s.tcpStart = time.Now()
// When connecting to IP (When no DNS lookup)
if s.dnsStart.IsZero() {
s.dnsStart = s.tcpStart
s.dnsDone = s.tcpStart
}
},
ConnectDone: func(network, addr string, err error) {
s.tcpDone = time.Now()
s.TCPConnection = s.tcpDone.Sub(s.tcpStart)
s.Connect = s.tcpDone.Sub(s.dnsStart)
},
TLSHandshakeStart: func() {
s.isTLS = true
s.tlsStart = time.Now()
},
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
s.tlsDone = time.Now()
s.TLSHandshake = s.tlsDone.Sub(s.tlsStart)
s.Pretransfer = s.tlsDone.Sub(s.dnsStart)
},
GotConn: func(i httptrace.GotConnInfo) {
// Handle when keep alive is used and connection is reused.
// DNSStart(Done) and ConnectStart(Done) is skipped
if i.Reused {
s.isReused = true
}
},
WroteRequest: func(info httptrace.WroteRequestInfo) {
now := time.Now()
s.serverStart = now
// When client doesn't use DialContext, DNS/TCP/TLS hook is not called.
if s.dnsStart.IsZero() && s.tcpStart.IsZero() {
s.dnsStart = now
s.dnsDone = now
s.tcpStart = now
s.tcpDone = now
}
// When connection is re-used, DNS/TCP/TLS hook is not called.
if s.isReused {
s.dnsStart = now
s.dnsDone = now
s.tcpStart = now
s.tcpDone = now
s.tlsStart = now
s.tlsDone = now
}
if s.isTLS { // https
return
}
// http
s.TLSHandshake = 0
s.Pretransfer = s.Connect
},
GotFirstResponseByte: func() {
s.serverDone = time.Now()
s.ServerProcessing = s.serverDone.Sub(s.serverStart)
s.StartTransfer = s.serverDone.Sub(s.dnsStart)
s.transferStart = s.serverDone
},
})
}

View File

@@ -185,7 +185,7 @@ func createPythonPlugin(projectName string) error {
return errors.Wrap(err, "copy file failed")
}
_, err = builtin.EnsurePython3Venv(fmt.Sprintf("funppy>=%s", shared.Version))
_, err = builtin.EnsurePython3Venv("funppy")
if err != nil {
return err
}

View File

@@ -54,6 +54,7 @@ func NewRunner(t *testing.T) *HRPRunner {
type HRPRunner struct {
t *testing.T
failfast bool
httpStatOn bool
requestsLogOn bool
pluginLogOn bool
saveTests bool
@@ -100,6 +101,13 @@ func (r *HRPRunner) SetRequestsLogOn() *HRPRunner {
return r
}
// SetHTTPStatOn turns on HTTP latency stat.
func (r *HRPRunner) SetHTTPStatOn() *HRPRunner {
log.Info().Msg("[init] SetHTTPStatOn")
r.httpStatOn = true
return r
}
// SetPluginLogOn turns on plugin logging.
func (r *HRPRunner) SetPluginLogOn() *HRPRunner {
log.Info().Msg("[init] SetPluginLogOn")

View File

@@ -42,6 +42,10 @@ func (r *SessionRunner) GetConfig() *TConfig {
return r.parsedConfig
}
func (r *SessionRunner) HTTPStatOn() bool {
return r.hrpRunner.httpStatOn
}
func (r *SessionRunner) LogOn() bool {
return r.hrpRunner.requestsLogOn
}

View File

@@ -17,6 +17,7 @@ type StepResult struct {
StepType StepType `json:"step_type" yaml:"step_type"` // step type, testcase/request/transaction/rendezvous
Success bool `json:"success" yaml:"success"` // step execution result
Elapsed int64 `json:"elapsed_ms" yaml:"elapsed_ms"` // step execution time in millisecond(ms)
HttpStat map[string]int64 `json:"httpstat,omitempty" yaml:"httpstat,omitempty"` // httpstat in millisecond(ms)
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"` // session data or slice of step data
ContentSize int64 `json:"content_size" yaml:"content_size"` // response body length
ExportVars map[string]interface{} `json:"export_vars,omitempty" yaml:"export_vars,omitempty"` // extract variables

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"compress/gzip"
"compress/zlib"
"crypto/tls"
"fmt"
"io"
"net/http"
@@ -15,10 +16,12 @@ import (
"time"
"github.com/andybalholm/brotli"
"github.com/fatih/color"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/hrp/internal/builtin"
"github.com/httprunner/httprunner/hrp/internal/httpstat"
"github.com/httprunner/httprunner/hrp/internal/json"
)
@@ -311,6 +314,13 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
}
}
// stat HTTP request
var httpStat httpstat.Stat
if r.HTTPStatOn() {
ctx := httpstat.WithHTTPStat(rb.req, &httpStat)
rb.req = rb.req.WithContext(ctx)
}
// do request action
start := time.Now()
var resp *http.Response
@@ -346,6 +356,13 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
return
}
if r.HTTPStatOn() {
// resp.Body has been ReadAll
httpStat.Finish()
stepResult.HttpStat = httpStat.Durations()
httpStat.Print()
}
// add response object to step variables, could be used in teardown hooks
stepVariables["hrp_step_response"] = respObj.respObjMeta
@@ -408,8 +425,22 @@ func printRequest(req *http.Request) error {
return nil
}
func printf(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(color.Output, format, a...)
}
func printResponse(resp *http.Response) error {
fmt.Println("==================== response ====================")
connectedVia := "plaintext"
if resp.TLS != nil {
switch resp.TLS.Version {
case tls.VersionTLS12:
connectedVia = "TLSv1.2"
case tls.VersionTLS13:
connectedVia = "TLSv1.3"
}
}
printf("%s %s\n", color.CyanString("Connected via"), color.BlueString("%s", connectedVia))
respContentType := resp.Header.Get("Content-Type")
printBody := shouldPrintBody(respContentType)
respDump, err := httputil.DumpResponse(resp, printBody)

View File

@@ -2,6 +2,8 @@ package hrp
import (
"testing"
"github.com/stretchr/testify/assert"
)
var (
@@ -89,3 +91,84 @@ func TestRunRequestRun(t *testing.T) {
t.Fatalf("stepPOSTData.Run() error: %v", err)
}
}
func TestRunRequestStatOn(t *testing.T) {
testcase := &TestCase{
Config: NewConfig("test").SetBaseURL("https://postman-echo.com"),
TestSteps: []IStep{stepGET, stepPOSTData},
}
runner := NewRunner(t).SetHTTPStatOn()
sessionRunner, _ := runner.NewSessionRunner(testcase)
if err := sessionRunner.Start(nil); err != nil {
t.Fatal()
}
summary := sessionRunner.GetSummary()
stat := summary.Records[0].HttpStat
if !assert.GreaterOrEqual(t, stat["DNSLookup"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["TCPConnection"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["TLSHandshake"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["ServerProcessing"], int64(1)) {
t.Fatal()
}
if !assert.GreaterOrEqual(t, stat["ContentTransfer"], int64(0)) {
t.Fatal()
}
if !assert.GreaterOrEqual(t, stat["NameLookup"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["Connect"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["Pretransfer"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["StartTransfer"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["Total"], int64(5)) {
t.Fatal()
}
if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(3)) {
t.Fatal()
}
// reuse connection
stat = summary.Records[1].HttpStat
if !assert.Equal(t, int64(0), stat["DNSLookup"]) {
t.Fatal()
}
if !assert.Equal(t, int64(0), stat["TCPConnection"]) {
t.Fatal()
}
if !assert.Equal(t, int64(0), stat["TLSHandshake"]) {
t.Fatal()
}
if !assert.Greater(t, stat["ServerProcessing"], int64(1)) {
t.Fatal()
}
if !assert.Equal(t, int64(0), stat["NameLookup"]) {
t.Fatal()
}
if !assert.Equal(t, int64(0), stat["Connect"]) {
t.Fatal()
}
if !assert.Equal(t, int64(0), stat["Pretransfer"]) {
t.Fatal()
}
if !assert.Greater(t, stat["StartTransfer"], int64(0)) {
t.Fatal()
}
if !assert.Greater(t, stat["Total"], int64(1)) {
t.Fatal()
}
if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(3)) {
t.Fatal()
}
}