Merge branch 'master' into master

This commit is contained in:
debugtalk
2022-04-08 09:38:37 +08:00
committed by GitHub
50 changed files with 1258 additions and 999 deletions

96
hrp/README.md Normal file
View File

@@ -0,0 +1,96 @@
# 代码阅读指南golang 部分)
## 核心数据结构
HttpRunner 以 `TestCase` 为核心,将任意测试场景抽象为有序步骤的集合。
```go
type TestCase struct {
Config *TConfig
TestSteps []IStep
}
```
其中,测试步骤 `IStep` 采用了 `go interface` 的设计理念,支持进行任意拓展;步骤内容统一在 `Run` 方法中进行实现。
```go
type IStep interface {
Name() string
Type() StepType
Struct() *TStep
Run(*SessionRunner) (*StepResult, error)
}
```
我们只需遵循 `IStep` 的接口定义,即可实现各种类型的测试步骤类型。当前 hrp 已支持的步骤类型包括:
- [request](step_request.go):发起单次 HTTP 请求
- [api](step_api.go):引用执行其它 API 文件
- [testcase](step_testcase.go):引用执行其它测试用例文件
- [thinktime](step_thinktime.go):思考时间,按照配置的逻辑进行等待
- [transaction](step_transaction.go):事务机制,用于压测
- [rendezvous](step_rendezvous.go):集合点机制,用于压测
基于该机制,我们可以扩展支持新的协议类型,例如 HTTP2/WebSocket/RPC 等;同时也可以支持新的测试类型,例如 UI 自动化。甚至我们还可以在一个测试用例中混合调用多种不同的 Step 类型,例如实现 HTTP/RPC/UI 混合场景。
## 运行主流程
### 整体控制器 HRPRunner
执行接口测试时,会初始化一个 `HRPRunner`,用于控制测试的执行策略。
```go
type HRPRunner struct {
t *testing.T
failfast bool
requestsLogOn bool
pluginLogOn bool
saveTests bool
genHTMLReport bool
client *http.Client
}
func (r *HRPRunner) Run(testcases ...ITestCase) error
func (r *HRPRunner) NewSessionRunner(testcase *TestCase) *SessionRunner
```
重点关注两个方法:
- Run测试执行的主入口支持运行一个或多个测试用例
- NewSessionRunner针对给定的测试用例初始化一个 SessionRunner
### 用例执行器 SessionRunner
测试用例的具体执行都由 `SessionRunner` 完成,每个 TestCase 对应一个实例,在该实例中除了包含测试用例自身内容外,还会包含测试过程的 session 数据和最终测试结果 summary。
```go
type SessionRunner struct {
testCase *TestCase
hrpRunner *HRPRunner
parser *Parser
sessionVariables map[string]interface{}
transactions map[string]map[transactionType]time.Time
startTime time.Time // record start time of the testcase
summary *TestCaseSummary // record test case summary
}
```
重点关注一个方法:
- Start启动执行用例依次执行所有测试步骤
```go
func (r *SessionRunner) Start() error {
...
// run step in sequential order
for _, step := range r.testCase.TestSteps {
_, err := step.Run(r)
if err != nil && r.hrpRunner.failfast {
return errors.Wrap(err, "abort running due to failfast setting")
}
}
...
}
```
在主流程中SessionRunner 并不需要关注 step 的具体类型,统一都是调用 `step.Run(r)`,具体实现逻辑都在对应 step 的 `Run(*SessionRunner)` 方法中。

View File

@@ -1,6 +1,7 @@
package hrp
import (
"os"
"sync"
"time"
@@ -42,14 +43,16 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) {
// load all testcases
testCases, err := loadTestCases(testcases...)
if err != nil {
panic(err)
log.Error().Err(err).Msg("failed to load testcases")
os.Exit(1)
}
for _, testcase := range testCases {
cfg := testcase.Config
err = initParameterIterator(cfg, "boomer")
if err != nil {
panic(err)
log.Error().Err(err).Msg("failed to init parameter iterator")
os.Exit(1)
}
rendezvousList := initRendezvous(testcase, int64(b.GetSpawnCount()))
task := b.convertBoomerTask(testcase, rendezvousList)

View File

@@ -227,14 +227,14 @@ func loadFromCSV(path string) []map[string]interface{} {
file, err := readFile(path)
if err != nil {
log.Error().Err(err).Msg("read csv file failed")
panic(err)
os.Exit(1)
}
r := csv.NewReader(strings.NewReader(string(file)))
content, err := r.ReadAll()
if err != nil {
log.Error().Err(err).Msg("parse csv file failed")
panic(err)
os.Exit(1)
}
var result []map[string]interface{}
for i := 1; i < len(content); i++ {

View File

@@ -10,7 +10,7 @@ func TestSendEvents(t *testing.T) {
Action: "SendEvents",
Value: 123,
}
err := gaClient.SendEvent(event)
err := SendEvent(event)
if err != nil {
t.Fatal(err)
}

View File

@@ -2,6 +2,7 @@ package sdk
import (
"fmt"
"os"
"github.com/denisbrodbeck/machineid"
"github.com/getsentry/sentry-go"
@@ -46,5 +47,9 @@ func init() {
}
func SendEvent(e IEvent) error {
if os.Getenv("DISABLE_GA") == "true" {
// do not send GA events in CI environment
return nil
}
return gaClient.SendEvent(e)
}

View File

@@ -4,6 +4,7 @@ import (
builtinJSON "encoding/json"
"fmt"
"net/url"
"path"
"reflect"
"regexp"
"strings"
@@ -26,18 +27,29 @@ type Parser struct {
}
func buildURL(baseURL, stepURL string) string {
uConfig, err := url.Parse(baseURL)
uStep, err := url.Parse(stepURL)
if err != nil {
log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse baseURL failed")
log.Error().Str("stepURL", stepURL).Err(err).Msg("[buildURL] parse url failed")
return ""
}
uStep, err := uConfig.Parse(stepURL)
// step url is absolute url
if uStep.Host != "" {
return stepURL
}
// step url is relative, based on base url
uConfig, err := url.Parse(baseURL)
if err != nil {
log.Error().Str("stepURL", stepURL).Err(err).Msg("[buildURL] parse stepURL failed")
log.Error().Str("baseURL", baseURL).Err(err).Msg("[buildURL] parse url failed")
return ""
}
// merge url
uStep.Scheme = uConfig.Scheme
uStep.Host = uConfig.Host
uStep.Path = path.Join(uConfig.Path, uStep.Path)
// base url missed
return uStep.String()
}

View File

@@ -11,25 +11,44 @@ import (
func TestBuildURL(t *testing.T) {
var url string
url = buildURL("https://postman-echo.com", "/get")
if url != "https://postman-echo.com/get" {
t.Fatalf("buildURL error, %s != 'https://postman-echo.com/get'", url)
if !assert.Equal(t, url, "https://postman-echo.com/get") {
t.Fail()
}
url = buildURL("https://postman-echo.com", "get")
if !assert.Equal(t, url, "https://postman-echo.com/get") {
t.Fail()
}
url = buildURL("https://postman-echo.com/", "/get")
if !assert.Equal(t, url, "https://postman-echo.com/get") {
t.Fail()
}
url = buildURL("https://postman-echo.com/abc/", "/get?a=1&b=2")
if url != "https://postman-echo.com/get?a=1&b=2" {
t.Fatalf("buildURL error, %s != 'https://postman-echo.com/get'", url)
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
t.Fail()
}
url = buildURL("https://postman-echo.com/abc", "get?a=1&b=2")
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
t.Fail()
}
// omit query string in base url
url = buildURL("https://postman-echo.com/abc?x=6&y=9", "/get?a=1&b=2")
if !assert.Equal(t, url, "https://postman-echo.com/abc/get?a=1&b=2") {
t.Fail()
}
url = buildURL("", "https://postman-echo.com/get")
if url != "https://postman-echo.com/get" {
t.Fatalf("buildURL error, %s != 'https://postman-echo.com/get'", url)
if !assert.Equal(t, url, "https://postman-echo.com/get") {
t.Fail()
}
// notice: step request url > config base url
url = buildURL("https://postman-echo.com", "https://httpbin.org/get")
if url != "https://httpbin.org/get" {
t.Fatalf("buildURL error, %s != 'https://httpbin.org/get'", url)
if !assert.Equal(t, url, "https://httpbin.org/get") {
t.Fail()
}
}

View File

@@ -175,13 +175,13 @@ func (v *responseObject) Validate(iValidators []interface{}, variablesMapping ma
Msgf("validate %s", checkItem)
if !result {
v.t.Fail()
return errors.New(fmt.Sprintf(
"do assertion failed, checkExpr: %v, assertMethod: %v, checkValue: %v, expectValue: %v",
validator.Check,
assertMethod,
checkValue,
expectValue,
))
log.Error().
Str("checkExpr", validator.Check).
Str("assertMethod", assertMethod).
Interface("checkValue", checkValue).
Interface("expectValue", expectValue).
Msg("assert failed")
return errors.New("step validation failed")
}
}
return nil

View File

@@ -16,7 +16,8 @@ func buildHashicorpGoPlugin() {
cmd := exec.Command("go", "build",
"-o", templatesDir+"debugtalk.bin", templatesDir+"plugin/debugtalk.go")
if err := cmd.Run(); err != nil {
panic(err)
log.Error().Err(err).Msg("build hashicorp go plugin failed")
os.Exit(1)
}
}
@@ -30,7 +31,8 @@ func buildHashicorpPyPlugin() {
pluginFile := templatesDir + "debugtalk.py"
err := scaffold.CopyFile("templates/plugin/debugtalk.py", pluginFile)
if err != nil {
panic(err)
log.Error().Err(err).Msg("build hashicorp python plugin failed")
os.Exit(1)
}
}

View File

@@ -69,24 +69,40 @@ func (r *SessionRunner) Start() error {
r.startTime = time.Now()
// run step in sequential order
for _, step := range r.testCase.TestSteps {
_, err := step.Run(r)
log.Info().Str("step", step.Name()).
Str("type", string(step.Type())).Msg("run step start")
stepResult, err := step.Run(r)
if err != nil && r.hrpRunner.failfast {
log.Error().
Str("step", stepResult.Name).
Str("type", string(stepResult.StepType)).
Bool("success", false).
Msg("run step end")
return errors.Wrap(err, "abort running due to failfast setting")
}
// update extracted variables
for k, v := range stepResult.ExportVars {
r.sessionVariables[k] = v
}
// update testcase summary
r.updateSummary(stepResult)
log.Info().
Str("step", stepResult.Name).
Str("type", string(stepResult.StepType)).
Bool("success", stepResult.Success).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
}
log.Info().Str("testcase", config.Name).Msg("run testcase end")
return nil
}
func (r *SessionRunner) UpdateSession(vars map[string]interface{}) {
for k, v := range vars {
r.sessionVariables[k] = v
}
}
// UpdateSummary appends step result to summary
func (r *SessionRunner) UpdateSummary(stepResult *StepResult) {
// updateSummary appends step result to summary
func (r *SessionRunner) updateSummary(stepResult *StepResult) {
r.summary.Records = append(r.summary.Records, stepResult)
r.summary.Stat.Total += 1
if stepResult.Success {

View File

@@ -3,8 +3,6 @@ package hrp
import (
"fmt"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/hrp/internal/builtin"
)
@@ -93,8 +91,6 @@ func (s *StepAPIWithOptionalArgs) Struct() *TStep {
}
func (s *StepAPIWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) {
log.Info().Str("api", s.step.Name).Msg("run referenced api")
// extend request with referenced API
api, _ := s.step.API.(*API)
extendWithAPI(s.step, api)

View File

@@ -250,8 +250,6 @@ func (r *requestBuilder) prepareBody(stepVariables map[string]interface{}) error
}
func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) {
log.Info().Str("step", step.Name).Msg("run step start")
stepResult = &StepResult{
Name: step.Name,
StepType: stepTypeRequest,
@@ -262,20 +260,16 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
defer func() {
// update testcase summary
if err != nil {
log.Error().Err(err).Msg("run request step failed")
stepResult.Attachment = err.Error()
} else {
// update extracted variables
r.UpdateSession(stepResult.ExportVars)
log.Info().
Str("step", step.Name).
Bool("success", stepResult.Success).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
}
r.UpdateSummary(stepResult)
}()
// override step variables
stepVariables, err := r.MergeStepVariables(step.Variables)
if err != nil {
return
}
sessionData := newSessionData()
parser := r.GetParser()
config := r.GetConfig()
@@ -283,12 +277,6 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
rb := newRequestBuilder(parser, config, step.Request)
rb.req.Method = string(step.Request.Method)
// override step variables
stepVariables, err := r.MergeStepVariables(step.Variables)
if err != nil {
return
}
err = rb.prepareUrlParams(stepVariables)
if err != nil {
return

View File

@@ -44,26 +44,27 @@ func (s *StepTestCaseWithOptionalArgs) Struct() *TStep {
}
func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error) {
stepVariables, err := r.MergeStepVariables(s.step.Variables)
if err != nil {
return nil, err
}
s.step.Variables = stepVariables
log.Info().Str("testcase", s.step.Name).Msg("run referenced testcase")
stepResult := &StepResult{
Name: s.step.Name,
StepType: stepTypeTestCase,
Success: false,
}
testcase := s.step.TestCase.(*TestCase)
// copy testcase to avoid data racing
copiedTestCase := &TestCase{}
if err := copier.Copy(copiedTestCase, testcase); err != nil {
log.Error().Err(err).Msg("copy testcase failed")
stepVariables, err := r.MergeStepVariables(s.step.Variables)
if err != nil {
return stepResult, err
}
// copy step to avoid data racing
copiedStep := &TStep{}
if err := copier.Copy(copiedStep, s.step); err != nil {
log.Error().Err(err).Msg("copy step failed")
return stepResult, err
}
copiedStep.Variables = stepVariables
copiedTestCase := copiedStep.TestCase.(*TestCase)
// override testcase config
extendWithTestCase(s.step, copiedTestCase)
@@ -73,16 +74,14 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
err = sessionRunner.Start()
stepResult.Elapsed = time.Since(start).Milliseconds()
if err != nil {
log.Error().Err(err).Msg("run referenced testcase step failed")
log.Info().Str("step", s.step.Name).Bool("success", false).Msg("run step end")
stepResult.Attachment = err.Error()
r.summary.Success = false
return stepResult, err
}
summary := sessionRunner.GetSummary()
stepResult.Data = summary
stepResult.Data = summary.Records
// export testcase export variables
stepResult.ExportVars = sessionRunner.summary.InOut.ExportVars
stepResult.ExportVars = summary.InOut.ExportVars
stepResult.Success = true
// update extracted variables
@@ -96,12 +95,6 @@ func (s *StepTestCaseWithOptionalArgs) Run(r *SessionRunner) (*StepResult, error
r.summary.Stat.Successes += summary.Stat.Successes
r.summary.Stat.Failures += summary.Stat.Failures
log.Info().
Str("step", s.step.Name).
Bool("success", true).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
return stepResult, nil
}

View File

@@ -30,8 +30,7 @@ func (s *StepThinkTime) Struct() *TStep {
func (s *StepThinkTime) Run(r *SessionRunner) (*StepResult, error) {
thinkTime := s.step.ThinkTime
log.Info().Str("name", s.step.Name).
Float64("time", thinkTime.Time).Msg("think time")
log.Info().Float64("time", thinkTime.Time).Msg("think time")
stepResult := &StepResult{
Name: s.step.Name,