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

feat: support to output the HTML report #88

Merged
merged 1 commit into from
Jun 12, 2023
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ collector-coverage.out
dist/
.vscode/launch.json
sample.yaml
.DS_Store
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This is a API testing tool.

## Features

* Multiple test report formats: Markdown, HTML, Stdout
* Response Body fields equation check
* Response Body [eval](https://expr.medv.io/)
* Verify the Kubernetes resources
Expand Down
8 changes: 5 additions & 3 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func newDefaultRunOption() *runOption {
}
}

func newDiskCardRunOption() *runOption {
func newDiscardRunOption() *runOption {
return &runOption{
reporter: runner.NewDiscardTestReporter(),
reportWriter: runner.NewDiscardResultWriter(),
Expand Down Expand Up @@ -74,7 +74,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
flags.DurationVarP(&opt.duration, "duration", "", 0, "Running duration")
flags.DurationVarP(&opt.requestTimeout, "request-timeout", "", time.Minute, "Timeout for per request")
flags.BoolVarP(&opt.requestIgnoreError, "request-ignore-error", "", false, "Indicate if ignore the request error")
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, discard, std")
flags.StringVarP(&opt.report, "report", "", "", "The type of target report. Supported: markdown, md, html, discard, std")
flags.StringVarP(&opt.reportFile, "report-file", "", "", "The file path of the report")
flags.BoolVarP(&opt.reportIgnore, "report-ignore", "", false, "Indicate if ignore the report output")
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
Expand All @@ -98,6 +98,8 @@ func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
switch o.report {
case "markdown", "md":
o.reportWriter = runner.NewMarkdownResultWriter(writer)
case "html":
o.reportWriter = runner.NewHTMLResultWriter(writer)
case "discard":
o.reportWriter = runner.NewDiscardResultWriter()
case "", "std":
Expand Down Expand Up @@ -178,7 +180,7 @@ func (o *runOption) runSuiteWithDuration(suite string) (err error) {
defer sem.Release(1)
defer wait.Done()
defer func() {
fmt.Println("routing end with", time.Now().Sub(now))
fmt.Println("routing end with", time.Since(now))
}()

dataContext := getDefaultContext()
Expand Down
2 changes: 1 addition & 1 deletion cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestRunSuite(t *testing.T) {
defer gock.Clean()
util.MakeSureNotNil(tt.prepare)()
ctx := getDefaultContext()
opt := newDiskCardRunOption()
opt := newDiscardRunOption()
opt.requestTimeout = 30 * time.Second
opt.limiter = limit.NewDefaultRateLimiter(0, 0)
stopSingal := make(chan struct{}, 1)
Expand Down
11 changes: 11 additions & 0 deletions pkg/render/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package render

import (
"bytes"
"fmt"
"html/template"
"io"
"strings"

"github.com/Masterminds/sprig/v3"
Expand Down Expand Up @@ -31,3 +33,12 @@ func FuncMap() template.FuncMap {
}
return funcs
}

// RenderThenPrint renders the template then prints the result
func RenderThenPrint(name, text string, ctx interface{}, w io.Writer) (err error) {
var report string
if report, err = Render(name, text, ctx); err == nil {
fmt.Fprint(w, report)
}
return
}
30 changes: 30 additions & 0 deletions pkg/render/template_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package render

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -56,3 +57,32 @@ func TestRender(t *testing.T) {
})
}
}

func TestRenderThenPrint(t *testing.T) {
tests := []struct {
name string
tplText string
ctx interface{}
buf *bytes.Buffer
expect string
}{{
name: "simple",
tplText: `{{max 1 2 3}}`,
ctx: nil,
buf: new(bytes.Buffer),
expect: `3`,
}, {
name: "with a map as context",
tplText: `{{.name}}`,
ctx: map[string]string{"name": "linuxsuren"},
buf: new(bytes.Buffer),
expect: "linuxsuren",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := RenderThenPrint(tt.name, tt.tplText, tt.ctx, tt.buf)
assert.NoError(t, err)
assert.Equal(t, tt.expect, tt.buf.String())
})
}
}
33 changes: 33 additions & 0 deletions pkg/runner/data/html.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE>
<html lang="zh">
<head>
<title>API Testing Report</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
[leading-7=""] {
line-height: 1.75rem;
}
.text-center, [text-center=""] {
text-align: center;
}
footer {
position: fixed;
bottom: 0;
width: 100%;
height: 60px;
}
</style>
</head>
<body>
<table>
<caption>API Testing Report</caption>
<tr><th>API</th><th>Average</th><th>Max</th><th>Min</th><th>Count</th><th>Error</th></tr>
{{- range $val := .}}
<tr><td>{{$val.API}}</td><td>{{$val.Average}}</td><td>{{$val.Max}}</td><td>{{$val.Min}}</td><td>{{$val.Count}}</td><td>{{$val.Error}}</td></tr>
{{- end}}
</table>
<footer text-center="" leading-7="">
<p text-sm=""><a href="https://github.com/LinuxSuRen/api-testing" target="_blank" rel="noopener">Powered by API Testing</a></p>
</footer>
</body>
</html>
23 changes: 17 additions & 6 deletions pkg/runner/reporter_memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,8 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
item.Total += duration
item.Count += 1

if record.EndTime.After(item.Last) {
item.Last = record.EndTime
}
if record.BeginTime.Before(item.First) {
item.First = record.BeginTime
}
item.Last = getLaterTime(record.EndTime, item.Last)
item.LastErrorMessage = getOriginalStringWhenEmpty(item.LastErrorMessage, record.GetErrorMessage())
} else {
resultWithTotal[api] = &ReportResultWithTotal{
ReportResult: ReportResult{
Expand All @@ -77,6 +73,7 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
Last: record.EndTime,
Total: duration,
}
resultWithTotal[api].LastErrorMessage = record.GetErrorMessage()
}
}

Expand All @@ -91,3 +88,17 @@ func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice,
sort.Sort(result)
return
}

func getLaterTime(a, b time.Time) time.Time {
if a.After(b) {
return a
}
return b
}

func getOriginalStringWhenEmpty(a, b string) string {
if b == "" {
return a
}
return b
}
33 changes: 27 additions & 6 deletions pkg/runner/reporter_memory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func TestExportAllReportResults(t *testing.T) {
BeginTime: now,
EndTime: now.Add(time.Second * 4),
Error: errors.New("fake"),
Body: "fake",
}, {
API: urlFoo,
Method: http.MethodGet,
Expand All @@ -62,12 +63,13 @@ func TestExportAllReportResults(t *testing.T) {
Count: 1,
Error: 0,
}, {
API: "GET http://foo",
Average: time.Second * 3,
Max: time.Second * 4,
Min: time.Second * 2,
Count: 3,
Error: 1,
API: "GET http://foo",
Average: time.Second * 3,
Max: time.Second * 4,
Min: time.Second * 2,
Count: 3,
Error: 1,
LastErrorMessage: "fake",
}, {
API: "GET http://bar",
Average: time.Second,
Expand All @@ -77,6 +79,25 @@ func TestExportAllReportResults(t *testing.T) {
Count: 1,
Error: 0,
}},
}, {
name: "first record has error",
records: []*runner.ReportRecord{{
API: urlFoo,
Method: http.MethodGet,
BeginTime: now,
EndTime: now.Add(time.Second * 4),
Error: errors.New("fake"),
Body: "fake",
}},
expect: runner.ReportResultSlice{{
API: "GET http://foo",
Average: time.Second * 4,
Max: time.Second * 4,
Min: time.Second * 4,
Count: 1,
Error: 1,
LastErrorMessage: "fake",
}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
28 changes: 17 additions & 11 deletions pkg/runner/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ func (r *ReportRecord) ErrorCount() int {
return 1
}

// GetErrorMessage returns the error message
func (r *ReportRecord) GetErrorMessage() string {
if r.ErrorCount() > 0 {
return r.Body
} else {
return ""
}
}

// NewReportRecord creates a record, and set the begin time to be now
func NewReportRecord() *ReportRecord {
return &ReportRecord{
Expand All @@ -111,13 +120,14 @@ func NewReportRecord() *ReportRecord {

// ReportResult represents the report result of a set of the same API requests
type ReportResult struct {
API string
Count int
Average time.Duration
Max time.Duration
Min time.Duration
QPS int
Error int
API string
Count int
Average time.Duration
Max time.Duration
Min time.Duration
QPS int
Error int
LastErrorMessage string
}

// ReportResultSlice is the alias type of ReportResult slice
Expand Down Expand Up @@ -202,10 +212,6 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
}(record)

defer func() {
if testcase.Clean.CleanPrepare {
err = r.doCleanPrepare(testcase)
}

if err == nil {
err = runJob(testcase.After)
}
Expand Down
3 changes: 0 additions & 3 deletions pkg/runner/simple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ func TestTestCase(t *testing.T) {
Before: atest.Job{
Items: []string{"sleep(1)"},
},
Clean: atest.Clean{
CleanPrepare: true,
},
},
execer: fakeruntime.FakeExecer{},
prepare: func() {
Expand Down
31 changes: 31 additions & 0 deletions pkg/runner/testdata/report.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE>
<html lang="zh">
<head>
<title>API Testing Report</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
[leading-7=""] {
line-height: 1.75rem;
}
.text-center, [text-center=""] {
text-align: center;
}
footer {
position: fixed;
bottom: 0;
width: 100%;
height: 60px;
}
</style>
</head>
<body>
<table>
<caption>API Testing Report</caption>
<tr><th>API</th><th>Average</th><th>Max</th><th>Min</th><th>Count</th><th>Error</th></tr>
<tr><td>/foo</td><td>3ns</td><td>3ns</td><td>3ns</td><td>1</td><td>0</td></tr>
</table>
<footer text-center="" leading-7="">
<p text-sm=""><a href="https://github.com/LinuxSuRen/api-testing" target="_blank" rel="noopener">Powered by API Testing</a></p>
</footer>
</body>
</html>
25 changes: 25 additions & 0 deletions pkg/runner/writer_html.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package runner

import (
_ "embed"
"io"

"github.com/linuxsuren/api-testing/pkg/render"
)

type htmlResultWriter struct {
writer io.Writer
}

// NewHTMLResultWriter creates a new htmlResultWriter
func NewHTMLResultWriter(writer io.Writer) ReportResultWriter {
return &htmlResultWriter{writer: writer}
}

// Output writes the HTML base report to target writer
func (w *htmlResultWriter) Output(result []ReportResult) (err error) {
return render.RenderThenPrint("html-report", htmlReport, result, w.writer)
}

//go:embed data/html.html
var htmlReport string
Loading