Skip to content

Commit

Permalink
feat: support output the test report (#17)
Browse files Browse the repository at this point in the history
Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
  • Loading branch information
LinuxSuRen and LinuxSuRen authored Apr 1, 2023
1 parent 4a4554d commit 078d4c8
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 13 deletions.
5 changes: 5 additions & 0 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Print the code of lines:

```shell
git ls-files | xargs cloc
```
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ build:
copy: build
sudo cp bin/atest /usr/local/bin/
test:
go test ./...
go test ./... -cover
47 changes: 43 additions & 4 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strings"
Expand All @@ -28,18 +29,36 @@ type runOption struct {
burst int32
limiter limit.RateLimiter
startTime time.Time
reporter runner.TestReporter
reportWriter runner.ReportResultWriter
report string
}

func newDefaultRunOption() *runOption {
return &runOption{
reporter: runner.NewmemoryTestReporter(),
reportWriter: runner.NewResultWriter(os.Stdout),
}
}

func newDiskCardRunOption() *runOption {
return &runOption{
reporter: runner.NewDiscardTestReporter(),
reportWriter: runner.NewDiscardResultWriter(),
}
}

// CreateRunCommand returns the run command
func CreateRunCommand() (cmd *cobra.Command) {
opt := &runOption{}
opt := newDefaultRunOption()
cmd = &cobra.Command{
Use: "run",
Aliases: []string{"r"},
Example: `atest run -p sample.yaml
See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
Short: "Run the test suite",
RunE: opt.runE,
Short: "Run the test suite",
PreRunE: opt.preRunE,
RunE: opt.runE,
}

// set flags
Expand All @@ -52,6 +71,15 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
flags.Int64VarP(&opt.thread, "thread", "", 1, "Threads of the execution")
flags.Int32VarP(&opt.qps, "qps", "", 5, "QPS")
flags.Int32VarP(&opt.burst, "burst", "", 5, "burst")
flags.StringVarP(&opt.report, "report", "", "", "The type of target report")
return
}

func (o *runOption) preRunE(cmd *cobra.Command, args []string) (err error) {
switch o.report {
case "markdown", "md":
o.reportWriter = runner.NewMarkdownResultWriter(cmd.OutOrStdout())
}
return
}

Expand All @@ -73,6 +101,14 @@ func (o *runOption) runE(cmd *cobra.Command, args []string) (err error) {
}
}
}

// print the report
if err == nil {
var results []runner.ReportResult
if results, err = o.reporter.ExportAllReportResults(); err == nil {
err = o.reportWriter.Output(results)
}
}
return
}

Expand Down Expand Up @@ -165,7 +201,10 @@ func (o *runOption) runSuite(suite string, dataContext map[string]interface{}, c
o.limiter.Accept()

ctxWithTimeout, _ := context.WithTimeout(ctx, o.requestTimeout)
if output, err = runner.RunTestCase(&testCase, dataContext, ctxWithTimeout); err != nil && !o.requestIgnoreError {

simpleRunner := runner.NewSimpleTestCaseRunner()
simpleRunner.WithTestReporter(o.reporter)
if output, err = simpleRunner.RunTestCase(&testCase, dataContext, ctxWithTimeout); err != nil && !o.requestIgnoreError {
return
}
}
Expand Down
7 changes: 3 additions & 4 deletions cmd/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ func TestRunSuite(t *testing.T) {

tt.prepare()
ctx := getDefaultContext()
opt := &runOption{
requestTimeout: 30 * time.Second,
limiter: limit.NewDefaultRateLimiter(0, 0),
}
opt := newDiskCardRunOption()
opt.requestTimeout = 30 * time.Second
opt.limiter = limit.NewDefaultRateLimiter(0, 0)
stopSingal := make(chan struct{}, 1)

err := opt.runSuite(tt.suiteFile, ctx, context.TODO(), stopSingal)
Expand Down
5 changes: 5 additions & 0 deletions pkg/runner/data/report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
| API | Average | Max | Min | Count | Error |
|---|---|---|---|---|---|
{{- range $val := .}}
| {{$val.API}} | {{$val.Average}} | {{$val.Max}} | {{$val.Min}} | {{$val.Count}} | {{$val.Error}} |
{{- end}}
17 changes: 17 additions & 0 deletions pkg/runner/reporter_discard.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package runner

type discardTestReporter struct {
}

// NewDiscardTestReporter creates a test reporter which discard everything
func NewDiscardTestReporter() TestReporter {
return &discardTestReporter{}
}

func (r *discardTestReporter) PutRecord(*ReportRecord) {}
func (r *discardTestReporter) GetAllRecords() []*ReportRecord {
return nil
}
func (r *discardTestReporter) ExportAllReportResults() (ReportResultSlice, error) {
return nil, nil
}
20 changes: 20 additions & 0 deletions pkg/runner/reporter_discard_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package runner_test

import (
"testing"

"github.com/linuxsuren/api-testing/pkg/runner"
"github.com/stretchr/testify/assert"
)

func TestDiscardTestReporter(t *testing.T) {
reporter := runner.NewDiscardTestReporter()
assert.NotNil(t, reporter)
assert.Nil(t, reporter.GetAllRecords())

result, err := reporter.ExportAllReportResults()
assert.Nil(t, result)
assert.Nil(t, err)

reporter.PutRecord(&runner.ReportRecord{})
}
68 changes: 68 additions & 0 deletions pkg/runner/reporter_memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package runner

import (
"sort"
"time"
)

type memoryTestReporter struct {
records []*ReportRecord
}

// NewmemoryTestReporter creates a memory based test reporter
func NewmemoryTestReporter() TestReporter {
return &memoryTestReporter{
records: []*ReportRecord{},
}
}

type ReportResultWithTotal struct {
ReportResult
Total time.Duration
}

func (r *memoryTestReporter) PutRecord(record *ReportRecord) {
r.records = append(r.records, record)
}
func (r *memoryTestReporter) GetAllRecords() []*ReportRecord {
return r.records
}
func (r *memoryTestReporter) ExportAllReportResults() (result ReportResultSlice, err error) {
resultWithTotal := map[string]*ReportResultWithTotal{}
for _, record := range r.records {
api := record.Method + " " + record.API
duration := record.Duration()

if item, ok := resultWithTotal[api]; ok {
if item.Max < duration {
item.Max = duration
}

if item.Min > duration {
item.Min = duration
}
item.Error += record.ErrorCount()
item.Total += duration
item.Count += 1
} else {
resultWithTotal[api] = &ReportResultWithTotal{
ReportResult: ReportResult{
API: api,
Count: 1,
Max: duration,
Min: duration,
Error: record.ErrorCount(),
},
Total: duration,
}
}
}

for _, r := range resultWithTotal {
r.Average = r.Total / time.Duration(r.Count)
result = append(result, r.ReportResult)
}

sort.Sort(result)
return
}
112 changes: 108 additions & 4 deletions pkg/runner/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os"
"reflect"
"strings"
"time"

"github.com/andreyvit/diff"
"github.com/antonmedv/expr"
Expand All @@ -21,9 +22,94 @@ import (
unstructured "github.com/linuxsuren/unstructured/pkg"
)

// RunTestCase runs the test case
func RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx context.Context) (output interface{}, err error) {
fmt.Printf("start to run: '%s'\n", testcase.Name)
type TestCaseRunner interface {
RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx context.Context) (output interface{}, err error)
WithOutputWriter(io.Writer) TestCaseRunner
WithTestReporter(TestReporter) TestCaseRunner
}

type ReportRecord struct {
Method string
API string
BeginTime time.Time
EndTime time.Time
Error error
}

// Duration returns the duration between begin and end time
func (r *ReportRecord) Duration() time.Duration {
return r.EndTime.Sub(r.BeginTime)
}

func (r *ReportRecord) ErrorCount() int {
if r.Error == nil {
return 0
}
return 1
}

// NewReportRecord creates a record, and set the begin time to be now
func NewReportRecord() *ReportRecord {
return &ReportRecord{
BeginTime: time.Now(),
}
}

type ReportResult struct {
API string
Count int
Average time.Duration
Max time.Duration
Min time.Duration
Error int
}

type ReportResultSlice []ReportResult

func (r ReportResultSlice) Len() int {
return len(r)
}

func (r ReportResultSlice) Less(i, j int) bool {
return r[i].Average > r[j].Average
}

func (r ReportResultSlice) Swap(i, j int) {
tmp := r[i]
r[i] = r[j]
r[j] = tmp
}

type ReportResultWriter interface {
Output([]ReportResult) error
}

type TestReporter interface {
PutRecord(*ReportRecord)
GetAllRecords() []*ReportRecord
ExportAllReportResults() (ReportResultSlice, error)
}

type simpleTestCaseRunner struct {
testReporter TestReporter
writer io.Writer
}

// NewSimpleTestCaseRunner creates the instance of the simple test case runner
func NewSimpleTestCaseRunner() TestCaseRunner {
runner := &simpleTestCaseRunner{}
return runner.WithOutputWriter(io.Discard).WithTestReporter(NewDiscardTestReporter())
}

func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx context.Context) (output interface{}, err error) {
fmt.Fprintf(r.writer, "start to run: '%s'\n", testcase.Name)
record := NewReportRecord()
defer func(rr *ReportRecord) {
rr.EndTime = time.Now()
rr.Error = err
r.testReporter.PutRecord(rr)
}(record)

if err = doPrepare(testcase); err != nil {
err = fmt.Errorf("failed to prepare, error: %v", err)
return
Expand Down Expand Up @@ -77,13 +163,15 @@ func RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx contex
if request, err = http.NewRequestWithContext(ctx, testcase.Request.Method, testcase.Request.API, requestBody); err != nil {
return
}
record.API = testcase.Request.API
record.Method = testcase.Request.Method

// set headers
for key, val := range testcase.Request.Header {
request.Header.Add(key, val)
}

fmt.Println("start to send request to", testcase.Request.API)
fmt.Fprintf(r.writer, "start to send request to %s\n", testcase.Request.API)

// send the HTTP request
var resp *http.Response
Expand Down Expand Up @@ -178,6 +266,22 @@ func RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx contex
return
}

func (r *simpleTestCaseRunner) WithOutputWriter(writer io.Writer) TestCaseRunner {
r.writer = writer
return r
}

func (r *simpleTestCaseRunner) WithTestReporter(reporter TestReporter) TestCaseRunner {
r.testReporter = reporter
return r
}

// Deprecated
// RunTestCase runs the test case.
func RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx context.Context) (output interface{}, err error) {
return NewSimpleTestCaseRunner().WithOutputWriter(os.Stdout).RunTestCase(testcase, dataContext, ctx)
}

func doPrepare(testcase *testing.TestCase) (err error) {
for i := range testcase.Prepare.Kubernetes {
item := testcase.Prepare.Kubernetes[i]
Expand Down
Loading

0 comments on commit 078d4c8

Please sign in to comment.