Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support html report #21

Merged
merged 2 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ autotest extract -x "//title" -j '[
}
]'
```
## Test Report
![report](https://github.com/vearne/autotest/raw/main/img/result_html.jpg)

## TODO
* [x] 1) support utilizing the script language Lua to ascertain the conformity of HTTP responses with expectations.
Expand Down
2 changes: 2 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ autotest extract -x "//title" -j '[
}
]'
```
## 测试报告
![report](https://github.com/vearne/autotest/raw/main/img/result_html.jpg)

## TODO
* [x] 1) 支持使用脚本语言Lua判断HTTP response是否符合预期
Expand Down
6 changes: 3 additions & 3 deletions config_files/autotest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ global:
worker_num: 5
# default: true
ignore_testcase_fail: true
debug: true
debug: false
# Timeout setting for each request
request_timeout: 5s

Expand All @@ -20,8 +20,8 @@ global:
http_rule_files:
- "./config_files/my_http_api.yml"

grpc_rule_files:
- "./config_files/my_grpc_api.yml"
#grpc_rule_files:
# - "./config_files/my_grpc_api.yml"



2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/golang/protobuf v1.5.4
github.com/jhump/protoreflect v1.16.0
github.com/lianggaoqiang/progress v0.0.1
github.com/spf13/cast v1.6.0
github.com/spf13/cast v1.7.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v3 v3.0.0-alpha9
github.com/vearne/executor v0.0.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
Expand Down
Binary file added img/result_html.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
90 changes: 87 additions & 3 deletions internal/command/http_automate.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package command

import (
"context"
"embed"
"fmt"
"github.com/lianggaoqiang/progress"
"github.com/vearne/autotest/internal/config"
"github.com/vearne/autotest/internal/model"
Expand All @@ -11,19 +13,32 @@ import (
slog "github.com/vearne/simplelog"
"github.com/vearne/zaplog"
"go.uber.org/zap"
"html/template"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
)

//go:embed template/*.tpl
var mytpl embed.FS

type ResultInfo struct {
Total int
SuccessCount int
FailedCount int
}

type CaseShow struct {
ID uint64
Description string
State string
Reason string
Link string
}

func HttpAutomateTest(httpTestCases map[string][]*config.TestCaseHttp) {
total := 0
for _, testcases := range httpTestCases {
Expand Down Expand Up @@ -54,12 +69,12 @@ func HttpAutomateTest(httpTestCases map[string][]*config.TestCaseHttp) {
slog.Info("HttpTestCases, total:%v, finishCount:%v, successCount:%v, failedCount:%v",
total, finishCount, successCount, failedCount)
// generate report file
GenReportFileHttp(filePath, tcResultList)
GenReportFileHttp(filePath, tcResultList, info)
}
slog.Info("[end]HttpTestCases, total:%v, cost:%v", total, time.Since(begin))
}

func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResult) {
func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResult, info *ResultInfo) {
filename := filepath.Base(testCasefilePath)
name := strings.TrimSuffix(filename, filepath.Ext(filename))
filename = name + ".csv"
Expand All @@ -69,8 +84,9 @@ func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResul
sort.Slice(tcResultList, func(i, j int) bool {
return tcResultList[i].ID < tcResultList[j].ID
})
// 1. csv file
var records [][]string
records = append(records, []string{"id", "desc", "state", "reason"})
records = append(records, []string{"id", "description", "state", "reason"})
for _, item := range tcResultList {
reasonStr := item.Reason.String()
if item.Reason == model.ReasonSuccess {
Expand All @@ -80,6 +96,74 @@ func GenReportFileHttp(testCasefilePath string, tcResultList []HttpTestCaseResul
item.Desc, item.State.String(), reasonStr})
}
util.WriterCSV(reportPath, records)
// 2. html file
dirName := util.MD5(reportDirPath + name)

var caseResults []CaseShow
for _, item := range tcResultList {
caseResults = append(caseResults, CaseShow{ID: item.ID, Description: item.Desc,
State: item.State.String(), Reason: item.Reason.String(),
Link: fmt.Sprintf("./%v/%v.html", dirName, item.ID)})
}

obj := map[string]any{
"info": info,
"tcResultList": caseResults,
}
// index file
err := RenderTpl(mytpl, "template/index.tpl", obj, filepath.Join(reportDirPath, name+".html"))
if err != nil {
slog.Error("RenderTpl, %v", err)
return
}

for _, item := range tcResultList {
data := map[string]any{
"Error": item.Error,
"reqDetail": item.ReqDetail(),
"respDetail": item.RespDetail(),
}
err := RenderTpl(mytpl, "template/case.tpl", data,
filepath.Join(reportDirPath, dirName, strconv.Itoa(int(item.ID))+".html"))
if err != nil {
slog.Error("RenderTpl, %v", err)
return
}
}
}

func RenderTpl(fs embed.FS, key string, obj map[string]any, targetPath string) error {
data, err := fs.ReadFile(key)
if err != nil {
slog.Error("mytpl.ReadFile, %v", err)
return err
}
t, err := template.New("index").Parse(string(data))
if err != nil {
slog.Error("template Parse, %v", err)
return err
}
dirPath := filepath.Dir(targetPath)
if !pathExists(dirPath) {
err = os.Mkdir(dirPath, 0755)
if err != nil {
return err
}
}

file, err := os.Create(targetPath)
if err != nil {
slog.Error("Create file, %v", err)
return err
}
defer file.Close()
return t.Execute(file, obj)
}

func pathExists(path string) bool {
_, err := os.Stat(path)
// os.IsNotExist 判断错误是否为文件或目录不存在
return !os.IsNotExist(err)
}

func HandleSingleFileHttp(workerNum int, filePath string) (*ResultInfo, []HttpTestCaseResult) {
Expand Down
68 changes: 68 additions & 0 deletions internal/command/http_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package command

import (
"context"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/vearne/autotest/internal/config"
"github.com/vearne/autotest/internal/model"
"github.com/vearne/autotest/internal/resource"
"github.com/vearne/executor"
"github.com/vearne/zaplog"
"go.uber.org/zap"
"net/url"
"strings"
"time"
)
Expand All @@ -22,6 +24,66 @@ type HttpTestCaseResult struct {
Request config.RequestHttp
TestCase *config.TestCaseHttp
KeyValues map[string]any
Error error
Response *resty.Response
}

/*
POST /api/order/subscribe HTTP/1.1
HOST: localhost:8080
HEADERS:
Content-Type: application/json
User-Agent: go-resty/2.12.0 (https://github.com/go-resty/resty)
BODY:
{
"title": "book3_title",
"author": "book3_author"
}
*/

func (t *HttpTestCaseResult) ReqDetail() string {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("%v %v\n", strings.ToUpper(t.Request.Method), t.Request.URL))
u, _ := url.Parse(t.Request.URL)
builder.WriteString(fmt.Sprintf("HOST: %v\n", u.Host))
builder.WriteString("HEADERS:\n")
for _, item := range t.Request.Headers {
builder.WriteString(fmt.Sprintf("%v\n", item))
}
builder.WriteString("BODY:\n")
builder.WriteString(fmt.Sprintf("%v\n", t.Request.Body))
return builder.String()
}

/*
STATUS : 200 OK
PROTO : HTTP/1.1
RECEIVED AT : 2024-10-17T17:05:05.156315+08:00
TIME DURATION: 38.354958ms
HEADERS :
Content-Length: 17
Content-Type: application/json; charset=utf-8
Date: Thu, 17 Oct 2024 09:05:05 GMT
BODY :
{
"code": "E000"
}
*/

func (t *HttpTestCaseResult) RespDetail() string {
if t.Response == nil {
return ""
}

var builder strings.Builder
builder.WriteString(fmt.Sprintf("STATUS: %v\n", t.Response.Status()))
builder.WriteString("HEADERS:\n")
for key, values := range t.Response.Header() {
builder.WriteString(fmt.Sprintf("%v: %v\n", key, strings.Join(values, ",")))
}
builder.WriteString("BODY:\n")
builder.WriteString(fmt.Sprintf("%v\n", t.Response.String()))
return builder.String()
}

type HttpTestCallable struct {
Expand All @@ -40,6 +102,8 @@ func (m *HttpTestCallable) Call(ctx context.Context) *executor.GPResult {
Reason: model.ReasonSuccess,
TestCase: m.testcase,
KeyValues: map[string]any{},
Error: nil,
Response: nil,
}

// 1. Check other test cases of dependencies
Expand Down Expand Up @@ -75,6 +139,7 @@ func (m *HttpTestCallable) Call(ctx context.Context) *executor.GPResult {
if err != nil {
tcResult.State = model.StateFailed
tcResult.Reason = model.ReasonTemplateRenderError
tcResult.Error = err
r.Value = tcResult
r.Err = err
return &r
Expand Down Expand Up @@ -119,11 +184,14 @@ func (m *HttpTestCallable) Call(ctx context.Context) *executor.GPResult {
)
tcResult.State = model.StateFailed
tcResult.Reason = model.ReasonRequestFailed
tcResult.Error = err
r.Value = tcResult
r.Err = err
return &r
}

tcResult.Response = out

// 6. export
if m.testcase.Export != nil {
exportConfig := m.testcase.Export
Expand Down
32 changes: 32 additions & 0 deletions internal/command/template/case.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTTP Request and Response Display</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h2 { color: #333; }
.container { margin-bottom: 20px; }
.section-title { font-weight: bold; margin-top: 20px; }
pre { background-color: #f4f4f4; padding: 10px; border: 1px solid #ddd; overflow: auto; white-space: pre-wrap; }
</style>
</head>
<body>
<h2>HTTP Request and Response Display</h2>

<div class="container">
<div class="section-title">~~~ REQUEST ~~~</div>
<pre>{{ .reqDetail }}</pre>
</div>

<div class="container">
<div class="section-title">~~~ RESPONSE ~~~</div>
{{ if .Error }}
{{ .Error }}
{{else}}
<pre>{{ .respDetail }}</pre>
{{end}}
</div>
</body>
</html>
Loading
Loading