Files
httprunner/runner.go

931 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package hrp
import (
"bufio"
"bytes"
"compress/flate"
"compress/gzip"
"crypto/tls"
_ "embed"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"syscall"
"testing"
"time"
"github.com/google/brotli/go/cbrotli"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/hrp/internal/builtin"
"github.com/httprunner/hrp/internal/ga"
"github.com/httprunner/hrp/plugin/common"
)
const (
summaryPath string = "reports/summary-%v.json"
reportPath string = "reports/report-%v.html"
)
// Run starts to run API test with default configs.
func Run(testcases ...ITestCase) error {
t := &testing.T{}
return NewRunner(t).SetDebug(true).Run(testcases...)
}
// NewRunner constructs a new runner instance.
func NewRunner(t *testing.T) *HRPRunner {
if t == nil {
t = &testing.T{}
}
return &HRPRunner{
t: t,
failfast: true, // default to failfast
debug: false, // default to turn off debug
genHTMLReport: false,
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
Timeout: 30 * time.Second,
},
}
}
type HRPRunner struct {
t *testing.T
failfast bool
debug bool
saveTests bool
genHTMLReport bool
client *http.Client
}
// SetFailfast configures whether to stop running when one step fails.
func (r *HRPRunner) SetFailfast(failfast bool) *HRPRunner {
log.Info().Bool("failfast", failfast).Msg("[init] SetFailfast")
r.failfast = failfast
return r
}
// SetDebug configures whether to log HTTP request and response content.
func (r *HRPRunner) SetDebug(debug bool) *HRPRunner {
log.Info().Bool("debug", debug).Msg("[init] SetDebug")
r.debug = debug
return r
}
// SetProxyUrl configures the proxy URL, which is usually used to capture HTTP packets for debugging.
func (r *HRPRunner) SetProxyUrl(proxyUrl string) *HRPRunner {
log.Info().Str("proxyUrl", proxyUrl).Msg("[init] SetProxyUrl")
p, err := url.Parse(proxyUrl)
if err != nil {
log.Error().Err(err).Str("proxyUrl", proxyUrl).Msg("[init] invalid proxyUrl")
return r
}
r.client.Transport = &http.Transport{
Proxy: http.ProxyURL(p),
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return r
}
// SetSaveTests configures whether to save summary of tests.
func (r *HRPRunner) SetSaveTests(saveTests bool) *HRPRunner {
log.Info().Bool("saveTests", saveTests).Msg("[init] SetSaveTests")
r.saveTests = saveTests
return r
}
// GenHTMLReport configures whether to gen html report of api tests.
func (r *HRPRunner) GenHTMLReport() *HRPRunner {
log.Info().Bool("genHTMLReport", true).Msg("[init] SetgenHTMLReport")
r.genHTMLReport = true
return r
}
// Run starts to execute one or multiple testcases.
func (r *HRPRunner) Run(testcases ...ITestCase) error {
event := ga.EventTracking{
Category: "RunAPITests",
Action: "hrp run",
}
// report start event
go ga.SendEvent(event)
// report execution timing event
defer ga.SendEvent(event.StartTiming("execution"))
// record execution data to summary
s := newOutSummary()
for _, iTestCase := range testcases {
testcase, err := iTestCase.ToTestCase()
if err != nil {
log.Error().Err(err).Msg("[Run] convert ITestCase interface to TestCase struct failed")
return err
}
cfg := testcase.Config
// parse config parameters
err = initParameterIterator(cfg, "runner")
if err != nil {
log.Error().Interface("parameters", cfg.Parameters).Err(err).Msg("parse config parameters failed")
return err
}
// 在runner模式下指定整体策略cfg.ParametersSetting.Iterators仅包含一个CartesianProduct的迭代器
for it := cfg.ParametersSetting.Iterators[0]; it.HasNext(); {
// iterate through all parameter iterators and update case variables
for _, it := range cfg.ParametersSetting.Iterators {
if it.HasNext() {
cfg.Variables = mergeVariables(it.Next(), cfg.Variables)
}
}
caseRunnerObj := r.newCaseRunner(testcase)
if err = caseRunnerObj.run(); err != nil {
log.Error().Err(err).Msg("[Run] run testcase failed")
return err
}
caseSummary := caseRunnerObj.getSummary()
s.appendCaseSummary(caseSummary)
}
}
s.Time.Duration = time.Since(s.Time.StartAt).Seconds()
// save summary
if r.saveTests {
dir, _ := filepath.Split(summaryPath)
err := builtin.EnsureFolderExists(dir)
if err != nil {
return err
}
err = builtin.Dump2JSON(s, fmt.Sprintf(summaryPath, s.Time.StartAt.Unix()))
if err != nil {
return err
}
}
// generate HTML report
if r.genHTMLReport {
err := s.genHTMLReport()
if err != nil {
return err
}
}
return nil
}
func (r *HRPRunner) newCaseRunner(testcase *TestCase) *caseRunner {
caseRunner := &caseRunner{
TestCase: testcase,
hrpRunner: r,
parser: newParser(),
summary: newSummary(),
}
caseRunner.reset()
return caseRunner
}
// caseRunner is used to run testcase and its steps.
// each testcase has its own caseRunner instance and share session variables.
type caseRunner struct {
*TestCase
hrpRunner *HRPRunner
parser *parser
sessionVariables map[string]interface{}
// transactions stores transaction timing info.
// key is transaction name, value is map of transaction type and time, e.g. start time and end time.
transactions map[string]map[transactionType]time.Time
startTime time.Time // record start time of the testcase
summary *testCaseSummary // record test case summary
}
// reset clears runner session variables.
func (r *caseRunner) reset() *caseRunner {
log.Info().Msg("[init] Reset session variables")
r.sessionVariables = make(map[string]interface{})
r.transactions = make(map[string]map[transactionType]time.Time)
r.startTime = time.Now()
r.summary.Name = r.Config.Name
return r
}
func (r *caseRunner) run() error {
config := r.TestCase.Config
// init plugin
var err error
if r.parser.plugin, err = initPlugin(config.Path); err != nil {
return err
}
defer func() {
if r.parser.plugin != nil {
r.parser.plugin.Quit()
}
}()
if err := r.parseConfig(config); err != nil {
return err
}
log.Info().Str("testcase", config.Name).Msg("run testcase start")
r.startTime = time.Now()
for index := range r.TestCase.TestSteps {
stepDataObj, err := r.runStep(index, config)
if stepDataObj == nil {
stepDataObj = &stepData{
Name: r.TestCase.TestSteps[index].Name(),
Success: false,
}
}
if stepDataObj.StepType == stepTypeTestCase {
// merge test case if the step is test case
summary, ok := stepDataObj.Data.(*testCaseSummary)
if ok {
for _, rc := range summary.Records {
r.summary.Records = append(r.summary.Records, rc)
}
r.summary.Stat.Total += summary.Stat.Total
r.summary.Stat.Successes += summary.Stat.Successes
r.summary.Stat.Failures += summary.Stat.Failures
}
} else if stepDataObj.StepType == stepTypeRequest {
// only record that the test step is the request step
r.summary.Records = append(r.summary.Records, stepDataObj)
r.summary.Stat.Total += 1
if stepDataObj.Success {
r.summary.Stat.Successes += 1
} else {
r.summary.Stat.Failures += 1
}
}
r.summary.Success = r.summary.Success && stepDataObj.Success
if err != nil {
stepDataObj.Attachment = err.Error()
if r.hrpRunner.failfast {
return errors.Wrap(err, "abort running due to failfast setting")
}
}
}
log.Info().Str("testcase", config.Name).Msg("run testcase end")
return nil
}
func initPlugin(path string) (plugin common.Plugin, err error) {
plugin, err = common.Init(path)
if plugin == nil {
return
}
// catch Interrupt and SIGTERM signals to ensure plugin quitted
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
plugin.Quit()
os.Exit(0)
}()
// report event for initializing plugin
var pluginType string
if _, ok := plugin.(*common.GoPlugin); ok {
pluginType = "go"
} else {
pluginType = "hashicorp"
}
event := ga.EventTracking{
Category: "InitPlugin",
Action: fmt.Sprintf("Init %s plugin", pluginType),
}
if err != nil {
event.Value = 1 // failed
}
go ga.SendEvent(event)
return
}
func (r *caseRunner) runStep(index int, caseConfig *TConfig) (stepResult *stepData, err error) {
step := r.TestCase.TestSteps[index]
// step type priority order: transaction > rendezvous > testcase > request
if stepTran, ok := step.(*StepTransaction); ok {
// transaction step
return r.runStepTransaction(stepTran.step.Transaction)
} else if stepRend, ok := step.(*StepRendezvous); ok {
// rendezvous step
return r.runStepRendezvous(stepRend.step.Rendezvous)
}
log.Info().Str("step", step.Name()).Msg("run step start")
// copy step and config to avoid data racing
copiedStep := &TStep{}
if err = copier.Copy(copiedStep, step.ToStruct()); err != nil {
log.Error().Err(err).Msg("copy step data failed")
return nil, err
}
stepVariables := copiedStep.Variables
// override variables
// step variables > session variables (extracted variables from previous steps)
stepVariables = mergeVariables(stepVariables, r.sessionVariables)
// step variables > testcase config variables
stepVariables = mergeVariables(stepVariables, caseConfig.Variables)
// parse step variables
parsedVariables, err := r.parser.parseVariables(stepVariables)
if err != nil {
log.Error().Interface("variables", caseConfig.Variables).Err(err).Msg("parse step variables failed")
return nil, err
}
copiedStep.Variables = parsedVariables // avoid data racing
// step type priority order: testcase > request
if _, ok := step.(*StepTestCaseWithOptionalArgs); ok {
// run referenced testcase
log.Info().Str("testcase", copiedStep.Name).Msg("run referenced testcase")
// TODO: override testcase config
stepResult, err = r.runStepTestCase(copiedStep)
if err != nil {
log.Error().Err(err).Msg("run referenced testcase step failed")
}
} else {
// parse step request url
var requestUrl interface{}
requestUrl, err = r.parser.parseString(copiedStep.Request.URL, copiedStep.Variables)
if err != nil {
log.Error().Err(err).Msg("parse request url failed")
requestUrl = copiedStep.Variables
}
copiedStep.Request.URL = buildURL(caseConfig.BaseURL, convertString(requestUrl)) // avoid data racing
// run request
stepResult, err = r.runStepRequest(copiedStep)
if err != nil {
log.Error().Err(err).Msg("run request step failed")
}
}
// update extracted variables
for k, v := range stepResult.ExportVars {
r.sessionVariables[k] = v
}
log.Info().
Str("step", step.Name()).
Bool("success", stepResult.Success).
Interface("exportVars", stepResult.ExportVars).
Msg("run step end")
return stepResult, err
}
func (r *caseRunner) runStepTransaction(transaction *Transaction) (stepResult *stepData, err error) {
log.Info().
Str("name", transaction.Name).
Str("type", string(transaction.Type)).
Msg("transaction")
stepResult = &stepData{
Name: transaction.Name,
StepType: stepTypeTransaction,
Success: true,
Elapsed: 0,
ContentSize: 0, // TODO: record transaction total response length
}
// create transaction if not exists
if _, ok := r.transactions[transaction.Name]; !ok {
r.transactions[transaction.Name] = make(map[transactionType]time.Time)
}
// record transaction start time, override if already exists
if transaction.Type == transactionStart {
r.transactions[transaction.Name][transactionStart] = time.Now()
}
// record transaction end time, override if already exists
if transaction.Type == transactionEnd {
r.transactions[transaction.Name][transactionEnd] = time.Now()
// if transaction start time not exists, use testcase start time instead
if _, ok := r.transactions[transaction.Name][transactionStart]; !ok {
r.transactions[transaction.Name][transactionStart] = r.startTime
}
// calculate transaction duration
duration := r.transactions[transaction.Name][transactionEnd].Sub(
r.transactions[transaction.Name][transactionStart])
stepResult.Elapsed = duration.Milliseconds()
log.Info().Str("name", transaction.Name).Dur("elapsed", duration).Msg("transaction")
}
return stepResult, nil
}
func (r *caseRunner) runStepRendezvous(rendezvous *Rendezvous) (stepResult *stepData, err error) {
log.Info().
Str("name", rendezvous.Name).
Float32("percent", rendezvous.Percent).
Int64("number", rendezvous.Number).
Int64("timeout", rendezvous.Timeout).
Msg("rendezvous")
stepResult = &stepData{
Name: rendezvous.Name,
StepType: stepTypeRendezvous,
Success: true,
}
// pass current rendezvous if already released, activate rendezvous sequentially after spawn done
if rendezvous.isReleased() || !r.isPreRendezvousAllReleased(rendezvous) || !rendezvous.isSpawnDone() {
return stepResult, nil
}
// activate the rendezvous only once during each cycle
rendezvous.once.Do(func() {
close(rendezvous.activateChan)
})
// check current cnt using double check lock before updating to avoid negative WaitGroup counter
if atomic.LoadInt64(&rendezvous.cnt) < rendezvous.Number {
rendezvous.lock.Lock()
if atomic.LoadInt64(&rendezvous.cnt) < rendezvous.Number {
atomic.AddInt64(&rendezvous.cnt, 1)
rendezvous.wg.Done()
rendezvous.timerResetChan <- struct{}{}
}
rendezvous.lock.Unlock()
}
// block until current rendezvous released
<-rendezvous.releaseChan
return stepResult, nil
}
func (r *caseRunner) isPreRendezvousAllReleased(rendezvous *Rendezvous) bool {
tCase, _ := r.ToTCase()
for _, step := range tCase.TestSteps {
preRendezvous := step.Rendezvous
if preRendezvous == nil {
continue
}
// meet current rendezvous, all previous rendezvous released, return true
if preRendezvous == rendezvous {
return true
}
if !preRendezvous.isReleased() {
return false
}
}
return true
}
func (r *Rendezvous) reset() {
r.cnt = 0
r.releasedFlag = 0
r.wg.Add(int(r.Number))
// timerResetChan channel will not be closed, thus init only once
if r.timerResetChan == nil {
r.timerResetChan = make(chan struct{})
}
r.activateChan = make(chan struct{})
r.releaseChan = make(chan struct{})
r.once = new(sync.Once)
}
func (r *Rendezvous) isSpawnDone() bool {
return atomic.LoadUint32(&r.spawnDoneFlag) == 1
}
func (r *Rendezvous) setSpawnDone() {
atomic.StoreUint32(&r.spawnDoneFlag, 1)
}
func (r *Rendezvous) isReleased() bool {
return atomic.LoadUint32(&r.releasedFlag) == 1
}
func (r *Rendezvous) setReleased() {
atomic.StoreUint32(&r.releasedFlag, 1)
}
func initRendezvous(testcase *TestCase, total int64) []*Rendezvous {
tCase, _ := testcase.ToTCase()
var rendezvousList []*Rendezvous
for _, step := range tCase.TestSteps {
if step.Rendezvous == nil {
continue
}
rendezvous := step.Rendezvous
// either number or percent should be correctly put, otherwise set to default (total)
if rendezvous.Number == 0 && rendezvous.Percent > 0 && rendezvous.Percent <= defaultRendezvousPercent {
rendezvous.Number = int64(rendezvous.Percent * float32(total))
} else if rendezvous.Number > 0 && rendezvous.Number <= total && rendezvous.Percent == 0 {
rendezvous.Percent = float32(rendezvous.Number) / float32(total)
} else {
log.Warn().
Str("name", rendezvous.Name).
Int64("default number", total).
Float32("default percent", defaultRendezvousPercent).
Msg("rendezvous parameter not defined or error, set to default value")
rendezvous.Number = total
rendezvous.Percent = defaultRendezvousPercent
}
if rendezvous.Timeout <= 0 {
rendezvous.Timeout = defaultRendezvousTimeout
}
rendezvous.reset()
rendezvousList = append(rendezvousList, rendezvous)
}
return rendezvousList
}
func waitRendezvous(rendezvousList []*Rendezvous) {
if rendezvousList != nil {
lastRendezvous := rendezvousList[len(rendezvousList)-1]
for _, rendezvous := range rendezvousList {
go waitSingleRendezvous(rendezvous, rendezvousList, lastRendezvous)
}
}
}
func waitSingleRendezvous(rendezvous *Rendezvous, rendezvousList []*Rendezvous, lastRendezvous *Rendezvous) {
for {
// cycle start: block current checking until current rendezvous activated
<-rendezvous.activateChan
stop := make(chan struct{})
timeout := time.Duration(rendezvous.Timeout) * time.Millisecond
timer := time.NewTimer(timeout)
go func() {
defer close(stop)
rendezvous.wg.Wait()
}()
for !rendezvous.isReleased() {
select {
case <-rendezvous.timerResetChan:
timer.Reset(timeout)
case <-stop:
rendezvous.setReleased()
close(rendezvous.releaseChan)
log.Info().
Str("name", rendezvous.Name).
Float32("percent", rendezvous.Percent).
Int64("number", rendezvous.Number).
Int64("timeout(ms)", rendezvous.Timeout).
Int64("cnt", rendezvous.cnt).
Str("reason", "rendezvous release condition satisfied").
Msg("rendezvous released")
case <-timer.C:
rendezvous.setReleased()
close(rendezvous.releaseChan)
log.Info().
Str("name", rendezvous.Name).
Float32("percent", rendezvous.Percent).
Int64("number", rendezvous.Number).
Int64("timeout(ms)", rendezvous.Timeout).
Int64("cnt", rendezvous.cnt).
Str("reason", "time's up").
Msg("rendezvous released")
}
}
// cycle end: reset all previous rendezvous after last rendezvous released
// otherwise, block current checker until the last rendezvous end
if rendezvous == lastRendezvous {
for _, r := range rendezvousList {
r.reset()
}
} else {
<-lastRendezvous.releaseChan
}
}
}
func (r *caseRunner) runStepRequest(step *TStep) (stepResult *stepData, err error) {
stepResult = &stepData{
Name: step.Name,
StepType: stepTypeRequest,
Success: false,
ContentSize: 0,
}
sessionData := newSessionData()
// convert request struct to map
jsonRequest, _ := json.Marshal(&step.Request)
var requestMap map[string]interface{}
_ = json.Unmarshal(jsonRequest, &requestMap)
rawUrl := step.Request.URL
method := step.Request.Method
req := &http.Request{
Method: method,
Header: make(http.Header),
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Close: true, // prevent the connection from being re-used
}
// prepare request headers
if len(step.Request.Headers) > 0 {
headers, err := r.parser.parseHeaders(step.Request.Headers, step.Variables)
if err != nil {
return stepResult, errors.Wrap(err, "parse headers failed")
}
for key, value := range headers {
// omit pseudo header names for HTTP/1, e.g. :authority, :method, :path, :scheme
if strings.HasPrefix(key, ":") {
continue
}
req.Header.Add(key, value)
// prepare content length
if strings.EqualFold(key, "Content-Length") && value != "" {
if l, err := strconv.ParseInt(value, 10, 64); err == nil {
req.ContentLength = l
}
}
}
}
// prepare request params
var queryParams url.Values
if len(step.Request.Params) > 0 {
params, err := r.parser.parseData(step.Request.Params, step.Variables)
if err != nil {
return stepResult, errors.Wrap(err, "parse data failed")
}
parsedParams := params.(map[string]interface{})
requestMap["params"] = parsedParams
if len(parsedParams) > 0 {
queryParams = make(url.Values)
for k, v := range parsedParams {
queryParams.Add(k, fmt.Sprint(v))
}
}
}
if queryParams != nil {
// append params to url
paramStr := queryParams.Encode()
if strings.IndexByte(rawUrl, '?') == -1 {
rawUrl = rawUrl + "?" + paramStr
} else {
rawUrl = rawUrl + "&" + paramStr
}
}
// prepare request cookies
for cookieName, cookieValue := range step.Request.Cookies {
req.AddCookie(&http.Cookie{
Name: cookieName,
Value: cookieValue,
})
}
// prepare request body
if step.Request.Body != nil {
data, err := r.parser.parseData(step.Request.Body, step.Variables)
if err != nil {
return stepResult, err
}
requestMap["body"] = data
var dataBytes []byte
switch vv := data.(type) {
case map[string]interface{}:
contentType := req.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
// post form data
formData := make(url.Values)
for k, v := range vv {
formData.Add(k, fmt.Sprint(v))
}
dataBytes = []byte(formData.Encode())
} else {
// post json
dataBytes, err = json.Marshal(vv)
if err != nil {
return stepResult, err
}
if contentType == "" {
req.Header.Set("Content-Type", "application/json; charset=utf-8")
}
}
case string:
dataBytes = []byte(vv)
case []byte:
dataBytes = vv
case bytes.Buffer:
dataBytes = vv.Bytes()
default: // unexpected body type
return stepResult, errors.New("unexpected request body type")
}
setBodyBytes(req, dataBytes)
}
// update header
headers := make(map[string]string)
for key, value := range req.Header {
headers[key] = value[0]
}
requestMap["headers"] = headers
// prepare url
u, err := url.Parse(rawUrl)
if err != nil {
return stepResult, errors.Wrap(err, "parse url failed")
}
req.URL = u
req.Host = u.Host
// log & print request
if r.hrpRunner.debug {
reqDump, err := httputil.DumpRequest(req, true)
if err != nil {
return stepResult, errors.Wrap(err, "dump request failed")
}
fmt.Println("-------------------- request --------------------")
fmt.Println(string(reqDump))
}
// do request action
start := time.Now()
resp, err := r.hrpRunner.client.Do(req)
stepResult.Elapsed = time.Since(start).Milliseconds()
if err != nil {
return stepResult, errors.Wrap(err, "do request failed")
}
defer resp.Body.Close()
// decode response body in br/gzip/deflate formats
err = decodeResponseBody(resp)
if err != nil {
return stepResult, errors.Wrap(err, "decode response body failed")
}
// log & print response
if r.hrpRunner.debug {
fmt.Println("==================== response ===================")
respDump, err := httputil.DumpResponse(resp, true)
if err != nil {
return stepResult, errors.Wrap(err, "dump response failed")
}
fmt.Println(string(respDump))
fmt.Println("--------------------------------------------------")
}
// new response object
respObj, err := newResponseObject(r.hrpRunner.t, r.parser, resp)
if err != nil {
err = errors.Wrap(err, "init ResponseObject error")
return
}
sessionData.ReqResps.Request = requestMap
sessionData.ReqResps.Response = builtin.FormatResponse(respObj.respObjMeta)
// extract variables from response
extractors := step.Extract
extractMapping := respObj.Extract(extractors)
stepResult.ExportVars = extractMapping
// override step variables with extracted variables
stepVariables := mergeVariables(step.Variables, extractMapping)
// validate response
err = respObj.Validate(step.Validators, stepVariables)
sessionData.Validators = respObj.validationResults
if err == nil {
sessionData.Success = true
stepResult.Success = true
}
stepResult.ContentSize = resp.ContentLength
stepResult.Data = sessionData
return stepResult, err
}
func decodeResponseBody(resp *http.Response) error {
switch resp.Header.Get("Content-Encoding") {
case "br":
resp.Body = cbrotli.NewReader(resp.Body)
case "gzip":
gr, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}
resp.Body = gr
case "deflate":
resp.Body = flate.NewReader(resp.Body)
}
return nil
}
func (r *caseRunner) runStepTestCase(step *TStep) (stepResult *stepData, err error) {
stepResult = &stepData{
Name: step.Name,
StepType: stepTypeTestCase,
Success: false,
}
testcase := step.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")
return stepResult, err
}
start := time.Now()
caseRunnerObj := r.hrpRunner.newCaseRunner(copiedTestCase)
err = caseRunnerObj.run()
stepResult.Elapsed = time.Since(start).Milliseconds()
if err != nil {
return stepResult, err
}
stepResult.Data = caseRunnerObj.getSummary()
stepResult.Success = true
return stepResult, nil
}
func (r *caseRunner) parseConfig(cfg *TConfig) error {
// parse config variables
parsedVariables, err := r.parser.parseVariables(cfg.Variables)
if err != nil {
log.Error().Interface("variables", cfg.Variables).Err(err).Msg("parse config variables failed")
return err
}
cfg.Variables = parsedVariables
// parse config name
parsedName, err := r.parser.parseString(cfg.Name, cfg.Variables)
if err != nil {
return err
}
cfg.Name = convertString(parsedName)
// parse config base url
parsedBaseURL, err := r.parser.parseString(cfg.BaseURL, cfg.Variables)
if err != nil {
return err
}
cfg.BaseURL = convertString(parsedBaseURL)
return nil
}
func newSummary() *testCaseSummary {
return &testCaseSummary{
Success: true,
Stat: &testStepStat{},
Time: &testCaseTime{},
InOut: &testCaseInOut{},
}
}
func (r *caseRunner) getSummary() *testCaseSummary {
caseSummary := r.summary
caseSummary.Time.StartAt = r.startTime
caseSummary.Time.Duration = time.Since(r.startTime).Seconds()
exportVars := make(map[string]interface{})
for _, value := range r.Config.Export {
exportVars[value] = r.Config.Variables[value]
}
caseSummary.InOut.ExportVars = exportVars
caseSummary.InOut.ConfigVars = r.Config.Variables
return caseSummary
}
func setBodyBytes(req *http.Request, data []byte) {
req.Body = ioutil.NopCloser(bytes.NewReader(data))
req.ContentLength = int64(len(data))
}
//go:embed internal/report/template.html
var reportTemplate string
func (s *Summary) genHTMLReport() error {
dir, _ := filepath.Split(reportPath)
err := builtin.EnsureFolderExists(dir)
if err != nil {
return err
}
file, err := os.OpenFile(fmt.Sprintf(reportPath, s.Time.StartAt.Unix()), os.O_WRONLY|os.O_CREATE, 0666)
defer file.Close()
if err != nil {
log.Error().Err(err).Msg("open file failed")
return err
}
writer := bufio.NewWriter(file)
tmpl := template.Must(template.New("report").Parse(reportTemplate))
err = tmpl.Execute(writer, s)
if err != nil {
log.Error().Err(err).Msg("execute applies a parsed template to the specified data object failed")
return err
}
err = writer.Flush()
return err
}