Skip to content
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: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.17 as builder
FROM golang:1.18 as builder

WORKDIR /workspace
COPY . .
Expand Down
2 changes: 2 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type runOption struct {
reportWriter runner.ReportResultWriter
report string
reportIgnore bool
level string
}

func newDefaultRunOption() *runOption {
Expand Down Expand Up @@ -66,6 +67,7 @@ See also https://github.com/LinuxSuRen/api-testing/tree/master/sample`,
flags := cmd.Flags()
flags.StringVarP(&opt.pattern, "pattern", "p", "test-suite-*.yaml",
"The file pattern which try to execute the test cases")
flags.StringVarP(&opt.level, "level", "l", "info", "Set the output log level")
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")
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/linuxsuren/api-testing

go 1.17
go 1.18

require (
github.com/Masterminds/sprig/v3 v3.2.3
Expand Down
83 changes: 77 additions & 6 deletions pkg/runner/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,68 @@ import (
unstructured "github.com/linuxsuren/unstructured/pkg"
)

// LevelWriter represents a writer with level
type LevelWriter interface {
Info(format string, a ...any)
Debug(format string, a ...any)
}

// FormatPrinter represents a formart printer with level
type FormatPrinter interface {
Fprintf(w io.Writer, level, format string, a ...any) (n int, err error)
}

type defaultLevelWriter struct {
level int
io.Writer
FormatPrinter
}

// NewDefaultLevelWriter creates a default LevelWriter instance
func NewDefaultLevelWriter(level string, writer io.Writer) LevelWriter {
result := &defaultLevelWriter{
Writer: writer,
}
switch level {
case "debug":
result.level = 7
case "info":
result.level = 3
}
return result
}

// Fprintf implements interface FormatPrinter
func (w *defaultLevelWriter) Fprintf(writer io.Writer, level int, format string, a ...any) (n int, err error) {
if level <= w.level {
return fmt.Fprintf(writer, format, a...)
}
return
}

// Info writes the info level message
func (w *defaultLevelWriter) Info(format string, a ...any) {
w.Fprintf(w.Writer, 3, format, a...)
}

// Debug writes the debug level message
func (w *defaultLevelWriter) Debug(format string, a ...any) {
w.Fprintf(w.Writer, 7, format, a...)
}

// TestCaseRunner represents a test case runner
type TestCaseRunner interface {
RunTestCase(testcase *testing.TestCase, dataContext interface{}, ctx context.Context) (output interface{}, err error)
WithOutputWriter(io.Writer) TestCaseRunner
WithWriteLevel(level string) TestCaseRunner
WithTestReporter(TestReporter) TestCaseRunner
}

// ReportRecord represents the raw data of a HTTP request
type ReportRecord struct {
Method string
API string
Body string
BeginTime time.Time
EndTime time.Time
Error error
Expand Down Expand Up @@ -103,17 +155,20 @@ type TestReporter interface {
type simpleTestCaseRunner struct {
testReporter TestReporter
writer io.Writer
log LevelWriter
}

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

// RunTestCase is the main entry point of a test case
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)
r.log.Info("start to run: '%s'\n", testcase.Name)
record := NewReportRecord()
defer func(rr *ReportRecord) {
rr.EndTime = time.Now()
Expand Down Expand Up @@ -182,7 +237,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
request.Header.Add(key, val)
}

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

// send the HTTP request
var resp *http.Response
Expand All @@ -194,6 +249,8 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
if responseBodyData, err = io.ReadAll(resp.Body); err != nil {
return
}
record.Body = string(responseBodyData)
r.log.Debug("response body: %s\n", record.Body)

if err = testcase.Expect.Render(nil); err != nil {
return
Expand All @@ -218,6 +275,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
}
}

var bodyMap map[string]interface{}
mapOutput := map[string]interface{}{}
if err = json.Unmarshal(responseBodyData, &mapOutput); err != nil {
switch b := err.(type) {
Expand All @@ -231,17 +289,22 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte
return
}
output = arrayOutput
mapOutput["data"] = arrayOutput
default:
return
}
} else {
bodyMap = mapOutput
output = mapOutput
mapOutput = map[string]interface{}{
"data": bodyMap,
}
}

for key, expectVal := range testcase.Expect.BodyFieldsExpect {
var val interface{}
var ok bool
if val, ok, err = unstructured.NestedField(mapOutput, strings.Split(key, "/")...); err != nil {
if val, ok, err = unstructured.NestedField(bodyMap, strings.Split(key, "/")...); err != nil {
err = fmt.Errorf("failed to get field: %s, %v", key, err)
return
} else if !ok {
Expand All @@ -260,12 +323,12 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte

for _, verify := range testcase.Expect.Verify {
var program *vm.Program
if program, err = expr.Compile(verify, expr.Env(output), expr.AsBool()); err != nil {
if program, err = expr.Compile(verify, expr.Env(mapOutput), expr.AsBool()); err != nil {
return
}

var result interface{}
if result, err = expr.Run(program, output); err != nil {
if result, err = expr.Run(program, mapOutput); err != nil {
return
}

Expand All @@ -283,6 +346,14 @@ func (r *simpleTestCaseRunner) WithOutputWriter(writer io.Writer) TestCaseRunner
return r
}

// WithWriteLevel sets the level writer
func (r *simpleTestCaseRunner) WithWriteLevel(level string) TestCaseRunner {
if level != "" {
r.log = NewDefaultLevelWriter(level, r.writer)
}
return r
}

// WithTestReporter sets the TestReporter
func (r *simpleTestCaseRunner) WithTestReporter(reporter TestReporter) TestCaseRunner {
r.testReporter = reporter
Expand Down
33 changes: 31 additions & 2 deletions pkg/runner/simple_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package runner

import (
"bytes"
"context"
"errors"
"net/http"
Expand Down Expand Up @@ -40,7 +41,7 @@ func TestTestCase(t *testing.T) {
"type": "generic",
},
Verify: []string{
`name == "linuxsuren"`,
`data.name == "linuxsuren"`,
},
},
},
Expand Down Expand Up @@ -242,7 +243,7 @@ func TestTestCase(t *testing.T) {
},
Expect: atest.Response{
Verify: []string{
"len(items) > 0",
"len(data.items) > 0",
},
},
},
Expand Down Expand Up @@ -372,5 +373,33 @@ func TestTestCase(t *testing.T) {
}
}

func TestLevelWriter(t *testing.T) {
tests := []struct {
name string
buf *bytes.Buffer
level string
expect string
}{{
name: "debug",
buf: new(bytes.Buffer),
level: "debug",
expect: "debuginfo",
}, {
name: "info",
buf: new(bytes.Buffer),
level: "info",
expect: "info",
}}
for _, tt := range tests {
writer := NewDefaultLevelWriter(tt.level, tt.buf)
if assert.NotNil(t, writer) {
writer.Debug("debug")
writer.Info("info")

assert.Equal(t, tt.expect, tt.buf.String())
}
}
}

//go:embed testdata/generic_response.json
var genericBody string
73 changes: 73 additions & 0 deletions pkg/server/fake_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package server

import (
context "context"
"log"
"net"

grpc "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
)

type fakeServer struct {
UnimplementedRunnerServer
version string
err error
}

// NewServer creates a fake server
func NewServer(version string, err error) RunnerServer {
t := &fakeServer{
version: version,
err: err,
}
return t
}

// Run runs the task
func (s *fakeServer) Run(ctx context.Context, in *TestTask) (*HelloReply, error) {
return &HelloReply{}, s.err
}

// GetVersion returns the version
func (s *fakeServer) GetVersion(ctx context.Context, in *Empty) (reply *HelloReply, err error) {
reply = &HelloReply{
Message: s.version,
}
err = s.err
return
}

// NewFakeClient creates a fake client
func NewFakeClient(ctx context.Context, version string, err error) (RunnerClient, func()) {
buffer := 101024 * 1024
lis := bufconn.Listen(buffer)

baseServer := grpc.NewServer()
RegisterRunnerServer(baseServer, NewServer(version, err))
go func() {
if err := baseServer.Serve(lis); err != nil {
log.Printf("error serving server: %v", err)
}
}()

conn, err := grpc.DialContext(ctx, "",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Printf("error connecting to server: %v", err)
}

closer := func() {
err := lis.Close()
if err != nil {
log.Printf("error closing listener: %v", err)
}
baseServer.Stop()
}

client := NewRunnerClient(conn)
return client, closer
}
22 changes: 17 additions & 5 deletions pkg/server/remote_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func NewRemoteServer() RunnerServer {

// Run start to run the test task
func (s *server) Run(ctx context.Context, task *TestTask) (reply *HelloReply, err error) {
if task.Level == "" {
task.Level = "info"
}

var suite *testing.TestSuite
if task.Env == nil {
task.Env = map[string]string{}
Expand Down Expand Up @@ -88,7 +92,8 @@ func (s *server) Run(ctx context.Context, task *TestTask) (reply *HelloReply, er
return
}

fmt.Println("prepare to run:", suite.Name)
fmt.Printf("prepare to run: %s, with level: %s\n", suite.Name, task.Level)
fmt.Printf("task kind: %s, %d to run\n", task.Kind, len(suite.Items))
dataContext := map[string]interface{}{}

var result string
Expand All @@ -104,6 +109,7 @@ func (s *server) Run(ctx context.Context, task *TestTask) (reply *HelloReply, er
for _, testCase := range suite.Items {
simpleRunner := runner.NewSimpleTestCaseRunner()
simpleRunner.WithOutputWriter(buf)
simpleRunner.WithWriteLevel(task.Level)

// reuse the API prefix
if strings.HasPrefix(testCase.Request.API, "/") {
Expand All @@ -128,20 +134,26 @@ func (s *server) GetVersion(ctx context.Context, in *Empty) (reply *HelloReply,
}

func findParentTestCases(testcase *testing.TestCase, suite *testing.TestSuite) (testcases []testing.TestCase) {
reg, matchErr := regexp.Compile(`.*\{\{\.\w*\..*}\}.*`)
targetReg, targetErr := regexp.Compile(`\{\{\.\w*\.`)
reg, matchErr := regexp.Compile(`.*\{\{.*\.\w*.*}\}.*`)
targetReg, targetErr := regexp.Compile(`\.\w*`)

if matchErr == nil && targetErr == nil {
expectName := ""
for _, val := range testcase.Request.Header {
if matched := reg.MatchString(val); matched {
expectName = targetReg.FindString(val)
expectName = strings.TrimPrefix(expectName, "{{.")
expectName = strings.TrimSuffix(expectName, ".")
expectName = strings.TrimPrefix(expectName, ".")
break
}
}

if expectName == "" {
if mached := reg.MatchString(testcase.Request.API); mached {
expectName = targetReg.FindString(testcase.Request.API)
expectName = strings.TrimPrefix(expectName, ".")
}
}

for _, item := range suite.Items {
if item.Name == expectName {
testcases = append(testcases, item)
Expand Down
Loading