mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-31 21:39:41 +08:00
fix: typo
change: do not add console output by default feat: add GA for docs refactor: move builtin to internal refactor: relocate sentry sdk feat: report events with ga change: use http client session fix: report GA events change: sentry
This commit is contained in:
85
internal/builtin/assertion.go
Normal file
85
internal/builtin/assertion.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var Assertions = map[string]func(t assert.TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool{
|
||||
"equals": assert.EqualValues,
|
||||
"equal": assert.EqualValues, // alias for equals
|
||||
"greater_than": assert.Greater,
|
||||
"less_than": assert.Less,
|
||||
"greater_or_equals": assert.GreaterOrEqual,
|
||||
"less_or_equals": assert.LessOrEqual,
|
||||
"not_equal": assert.NotEqual,
|
||||
"contains": assert.Contains,
|
||||
"regex_match": assert.Regexp,
|
||||
// custom assertions
|
||||
"startswith": StartsWith, // check if string starts with substring
|
||||
"endswith": EndsWith, // check if string ends with substring
|
||||
"length_equals": EqualLength,
|
||||
"length_equal": EqualLength, // alias for length_equals
|
||||
}
|
||||
|
||||
func StartsWith(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.HasPrefix(actualString, expectedString), msgAndArgs...)
|
||||
}
|
||||
|
||||
func EndsWith(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
if !assert.IsType(t, "string", actual, fmt.Sprintf("actual is %v", actual)) {
|
||||
return false
|
||||
}
|
||||
if !assert.IsType(t, "string", expected, fmt.Sprintf("expected is %v", expected)) {
|
||||
return false
|
||||
}
|
||||
actualString := actual.(string)
|
||||
expectedString := expected.(string)
|
||||
return assert.True(t, strings.HasSuffix(actualString, expectedString), msgAndArgs...)
|
||||
}
|
||||
|
||||
func EqualLength(t assert.TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
|
||||
length, err := convertInt(expected)
|
||||
if err != nil {
|
||||
return assert.Fail(t, fmt.Sprintf("expected type is not int, got %#v", expected), msgAndArgs...)
|
||||
}
|
||||
|
||||
return assert.Len(t, actual, length, msgAndArgs...)
|
||||
}
|
||||
|
||||
func convertInt(value interface{}) (int, error) {
|
||||
switch v := value.(type) {
|
||||
case int:
|
||||
return v, nil
|
||||
case int8:
|
||||
return int(v), nil
|
||||
case int16:
|
||||
return int(v), nil
|
||||
case int32:
|
||||
return int(v), nil
|
||||
case int64:
|
||||
return int(v), nil
|
||||
case uint:
|
||||
return int(v), nil
|
||||
case uint8:
|
||||
return int(v), nil
|
||||
case uint16:
|
||||
return int(v), nil
|
||||
case uint32:
|
||||
return int(v), nil
|
||||
case uint64:
|
||||
return int(v), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported int convertion for %v(%T)", v, v)
|
||||
}
|
||||
}
|
||||
63
internal/builtin/assertion_test.go
Normal file
63
internal/builtin/assertion_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStartsWith(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abc", "a"},
|
||||
{"abc", "ab"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, StartsWith(t, data.expected, data.raw)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndsWith(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw string
|
||||
expected string
|
||||
}{
|
||||
{"", ""},
|
||||
{"a", "a"},
|
||||
{"abc", "c"},
|
||||
{"abc", "bc"},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EndsWith(t, data.expected, data.raw)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEqualLength(t *testing.T) {
|
||||
testData := []struct {
|
||||
raw interface{}
|
||||
expected int
|
||||
}{
|
||||
{"", 0},
|
||||
{[]string{}, 0},
|
||||
{map[string]interface{}{}, 0},
|
||||
{"a", 1},
|
||||
{[]string{"a"}, 1},
|
||||
{map[string]interface{}{"a": 123}, 1},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
if !assert.True(t, EqualLength(t, data.expected, data.raw)) {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
46
internal/builtin/function.go
Normal file
46
internal/builtin/function.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Functions = map[string]interface{}{
|
||||
"get_timestamp": getTimestamp, // call without arguments
|
||||
"sleep": sleep, // call with one argument
|
||||
"gen_random_string": genRandomString, // call with one argument
|
||||
"max": math.Max, // call with two arguments
|
||||
"md5": MD5,
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func getTimestamp() int64 {
|
||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func sleep(nSecs int) {
|
||||
time.Sleep(time.Duration(nSecs) * time.Second)
|
||||
}
|
||||
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
func genRandomString(n int) string {
|
||||
lettersLen := len(letters)
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(lettersLen)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func MD5(str string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(str))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
79
internal/ga/client.go
Normal file
79
internal/ga/client.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package ga
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
gaAPIDebugURL = "https://www.google-analytics.com/debug/collect" // used for debug
|
||||
gaAPIURL = "https://www.google-analytics.com/collect"
|
||||
)
|
||||
|
||||
type GAClient struct {
|
||||
TrackingID string `form:"tid"` // Tracking ID / Property ID, XX-XXXXXXX-X
|
||||
ClientID string `form:"cid"` // Anonymous Client ID
|
||||
Version string `form:"v"` // Version
|
||||
httpClient *http.Client // http client session
|
||||
}
|
||||
|
||||
// NewGAClient creates a new GAClient object with the trackingID and clientID.
|
||||
func NewGAClient(trackingID string) *GAClient {
|
||||
nodeUUID, _ := uuid.NewUUID()
|
||||
return &GAClient{
|
||||
TrackingID: trackingID,
|
||||
ClientID: nodeUUID.String(),
|
||||
Version: "1", // constant v1
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SendEvent sends one event to Google Analytics
|
||||
func (g *GAClient) SendEvent(e IEvent) error {
|
||||
var data url.Values
|
||||
if event, ok := e.(UserTimingTracking); ok {
|
||||
event.duration = time.Since(event.startTime)
|
||||
data = event.ToUrlValues()
|
||||
} else {
|
||||
data = e.ToUrlValues()
|
||||
}
|
||||
|
||||
// append common params
|
||||
data.Add("v", g.Version)
|
||||
data.Add("tid", g.TrackingID)
|
||||
data.Add("cid", g.ClientID)
|
||||
|
||||
resp, err := g.httpClient.PostForm(gaAPIURL, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("response status: %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func structToUrlValues(i interface{}) (values url.Values) {
|
||||
values = url.Values{}
|
||||
iVal := reflect.ValueOf(i)
|
||||
for i := 0; i < iVal.NumField(); i++ {
|
||||
formTagName := iVal.Type().Field(i).Tag.Get("form")
|
||||
if formTagName == "" {
|
||||
continue
|
||||
}
|
||||
if iVal.Field(i).IsZero() {
|
||||
continue
|
||||
}
|
||||
values.Set(formTagName, fmt.Sprint(iVal.Field(i)))
|
||||
}
|
||||
return
|
||||
}
|
||||
28
internal/ga/client_test.go
Normal file
28
internal/ga/client_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package ga
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSendEvents(t *testing.T) {
|
||||
event := EventTracking{
|
||||
Category: "unittest",
|
||||
Action: "SendEvents",
|
||||
}
|
||||
err := gaClient.SendEvent(event)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStructToUrlValues(t *testing.T) {
|
||||
event := EventTracking{
|
||||
Category: "unittest",
|
||||
Action: "convert",
|
||||
Label: "StructToUrlValues",
|
||||
}
|
||||
val := structToUrlValues(event)
|
||||
if val.Encode() != "ea=convert&ec=unittest&el=StructToUrlValues" {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
67
internal/ga/events.go
Normal file
67
internal/ga/events.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package ga
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IEvent interface {
|
||||
ToUrlValues() url.Values
|
||||
}
|
||||
|
||||
type EventTracking struct {
|
||||
HitType string `form:"t"` // Event hit type = event
|
||||
Category string `form:"ec"` // Required. Event Category.
|
||||
Action string `form:"ea"` // Required. Event Action.
|
||||
Label string `form:"el"` // Optional. Event label
|
||||
Value int `form:"ev"` // Optional. Event value
|
||||
}
|
||||
|
||||
func (e EventTracking) StartTiming(variable string) UserTimingTracking {
|
||||
return UserTimingTracking{
|
||||
HitType: "timing",
|
||||
Category: e.Category,
|
||||
Variable: variable,
|
||||
Label: e.Label,
|
||||
startTime: time.Now(), // starts the timer
|
||||
}
|
||||
}
|
||||
|
||||
func (e EventTracking) ToUrlValues() url.Values {
|
||||
e.HitType = "event"
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
|
||||
type UserTimingTracking struct {
|
||||
HitType string `form:"t"` // Timing hit type
|
||||
Category string `form:"utc"` // Required. user timing category. e.g. jsonLoader
|
||||
Variable string `form:"utv"` // Required. timing variable. e.g. load
|
||||
Duration string `form:"utt"` // Required. time took duration. Required.
|
||||
Label string `form:"utl"` // Optional. user timing label. e.g jQuery
|
||||
startTime time.Time
|
||||
duration time.Duration // time took duration
|
||||
}
|
||||
|
||||
func (e UserTimingTracking) ToUrlValues() url.Values {
|
||||
e.HitType = "timing"
|
||||
e.Duration = fmt.Sprintf("%d", int64(e.duration.Seconds()*1000))
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
|
||||
type Exception struct {
|
||||
HitType string `form:"t"` // Hit Type = exception
|
||||
Description string `form:"exd"` // exception description. i.e. IOException
|
||||
IsFatal string `form:"exf"` // if the exception was fatal
|
||||
isFatal bool
|
||||
}
|
||||
|
||||
func (e Exception) ToUrlValues() url.Values {
|
||||
e.HitType = "exception"
|
||||
if e.isFatal {
|
||||
e.IsFatal = "1"
|
||||
} else {
|
||||
e.IsFatal = "0"
|
||||
}
|
||||
return structToUrlValues(e)
|
||||
}
|
||||
15
internal/ga/init.go
Normal file
15
internal/ga/init.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package ga
|
||||
|
||||
const (
|
||||
trackingID = "UA-114587036-1" // Tracking ID for Google Analytics
|
||||
)
|
||||
|
||||
var gaClient *GAClient
|
||||
|
||||
func init() {
|
||||
gaClient = NewGAClient(trackingID)
|
||||
}
|
||||
|
||||
func SendEvent(e IEvent) error {
|
||||
return gaClient.SendEvent(e)
|
||||
}
|
||||
27
internal/sentry/init.go
Normal file
27
internal/sentry/init.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package sentry
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
|
||||
"github.com/httprunner/hrp/internal/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// init sentry sdk
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: "https://cff5efc69b1a4325a4cf873f1e70c13a@o334324.ingest.sentry.io/6070292",
|
||||
Release: version.VERSION,
|
||||
})
|
||||
if err != nil {
|
||||
panic("init sentry sdk failed!")
|
||||
}
|
||||
sentry.ConfigureScope(func(scope *sentry.Scope) {
|
||||
scope.SetLevel(sentry.LevelError)
|
||||
})
|
||||
}
|
||||
|
||||
func Flush() {
|
||||
sentry.Flush(3 * time.Second)
|
||||
}
|
||||
3
internal/version/init.go
Normal file
3
internal/version/init.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package version
|
||||
|
||||
const VERSION = "v0.2.1"
|
||||
Reference in New Issue
Block a user