Skip to content

Commit 649b0d8

Browse files
authored
feat: add writeFile function to expr (#277)
1 parent 7124dc4 commit 649b0d8

25 files changed

+686
-97
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fmt:
1717
build:
1818
mkdir -p bin
1919
rm -rf bin/atest
20-
go build ${TOOLEXEC} -a ${BUILD_FLAG} -o bin/${BINARY} main.go
20+
CGO_ENABLED=0 go build ${TOOLEXEC} -a ${BUILD_FLAG} -o bin/${BINARY} main.go
2121
build-ext: build-ext-git build-ext-orm build-ext-s3 build-ext-etcd
2222
build-ext-git:
2323
CGO_ENABLED=0 go build -ldflags "-w -s" -o bin/atest-store-git extensions/store-git/main.go

cmd/run.go

+2
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ func (o *runOption) runSuite(loader testing.Loader, dataContext map[string]inter
261261
runner := runner.GetTestSuiteRunner(testSuite)
262262
runner.WithTestReporter(o.reporter)
263263
runner.WithSecure(testSuite.Spec.Secure)
264+
runner.WithOutputWriter(os.Stdout)
265+
runner.WithWriteLevel(o.level)
264266
if output, err = runner.RunTestCase(&testCase, dataContext, ctxWithTimeout); err != nil && !o.requestIgnoreError {
265267
err = fmt.Errorf("failed to run '%s', %v", testCase.Name, err)
266268
return

pkg/runner/expr_function.go

+84-21
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,21 @@ SOFTWARE.
2626
package runner
2727

2828
import (
29+
"encoding/json"
2930
"fmt"
31+
"io"
32+
"log"
3033
"net/http"
34+
"os"
3135
"os/exec"
3236
"time"
3337

38+
"github.com/antonmedv/expr"
3439
"github.com/antonmedv/expr/ast"
40+
"github.com/antonmedv/expr/builtin"
41+
"github.com/antonmedv/expr/vm"
3542
)
3643

37-
var extensionFuncs = []*ast.Function{
38-
{
39-
Name: "sleep",
40-
Func: ExprFuncSleep,
41-
},
42-
{
43-
Name: "httpReady",
44-
Func: ExprFuncHTTPReady,
45-
},
46-
{
47-
Name: "exec",
48-
Func: func(params ...interface{}) (res any, err error) {
49-
exec.Command("sh", "-c", params[0].(string)).Run()
50-
return
51-
},
52-
},
53-
}
54-
5544
// ExprFuncSleep is an expr function for sleeping
5645
func ExprFuncSleep(params ...interface{}) (res interface{}, err error) {
5746
if len(params) < 1 {
@@ -90,14 +79,88 @@ func ExprFuncHTTPReady(params ...interface{}) (res interface{}, err error) {
9079
return
9180
}
9281

82+
var resp *http.Response
9383
for i := 0; i < retry; i++ {
94-
var resp *http.Response
95-
if resp, err = http.Get(api); err == nil && resp != nil && resp.StatusCode == http.StatusOK {
84+
resp, err = http.Get(api)
85+
alive := err == nil && resp != nil && resp.StatusCode == http.StatusOK
86+
87+
if alive && len(params) >= 3 {
88+
log.Println("checking the response")
89+
exprText := params[2].(string)
90+
91+
// check the response
92+
var data []byte
93+
if data, err = io.ReadAll(resp.Body); err == nil {
94+
unstruct := make(map[string]interface{})
95+
96+
if err = json.Unmarshal(data, &unstruct); err != nil {
97+
log.Printf("failed to unmarshal the response data: %v\n", err)
98+
return
99+
}
100+
101+
unstruct["data"] = unstruct
102+
var program *vm.Program
103+
if program, err = expr.Compile(exprText, expr.Env(unstruct)); err != nil {
104+
log.Printf("failed to compile: %s, %v\n", exprText, err)
105+
return
106+
}
107+
108+
var result interface{}
109+
if result, err = expr.Run(program, unstruct); err != nil {
110+
log.Printf("failed to Run: %s, %v\n", exprText, err)
111+
return
112+
}
113+
114+
if val, ok := result.(bool); ok {
115+
if val {
116+
return
117+
}
118+
} else {
119+
err = fmt.Errorf("the result of %s should be a bool", exprText)
120+
return
121+
}
122+
}
123+
} else if alive {
96124
return
97125
}
98-
fmt.Println("waiting for", api)
126+
127+
log.Println("waiting for", api)
99128
time.Sleep(1 * time.Second)
100129
}
101130
err = fmt.Errorf("failed to wait for the API ready in %d times", retry)
102131
return
103132
}
133+
134+
func init() {
135+
builtin.Builtins = append(builtin.Builtins, []*ast.Function{
136+
{
137+
Name: "sleep",
138+
Func: ExprFuncSleep,
139+
},
140+
{
141+
Name: "httpReady",
142+
Func: ExprFuncHTTPReady,
143+
},
144+
{
145+
Name: "command",
146+
Func: func(params ...interface{}) (res any, err error) {
147+
var output []byte
148+
output, err = exec.Command("sh", "-c", params[0].(string)).CombinedOutput()
149+
if output != nil {
150+
res = string(output)
151+
}
152+
return
153+
},
154+
},
155+
{
156+
Name: "writeFile",
157+
Func: func(params ...interface{}) (res any, err error) {
158+
filename := params[0]
159+
content := params[1]
160+
161+
err = os.WriteFile(filename.(string), []byte(content.(string)), 0644)
162+
return
163+
},
164+
},
165+
}...)
166+
}

pkg/runner/expr_function_test.go

+98-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@ SOFTWARE.
2525
package runner_test
2626

2727
import (
28+
"fmt"
29+
"io"
2830
"net/http"
31+
"os"
2932
"testing"
3033

34+
"github.com/antonmedv/expr"
35+
"github.com/antonmedv/expr/vm"
3136
"github.com/h2non/gock"
3237
"github.com/linuxsuren/api-testing/pkg/runner"
3338
"github.com/stretchr/testify/assert"
@@ -60,15 +65,15 @@ func TestExprFuncSleep(t *testing.T) {
6065

6166
func TestExprFuncHTTPReady(t *testing.T) {
6267
t.Run("normal", func(t *testing.T) {
63-
defer gock.Clean()
68+
defer gock.Off()
6469
gock.New(urlFoo).Reply(http.StatusOK)
6570

6671
_, err := runner.ExprFuncHTTPReady(urlFoo, 1)
6772
assert.NoError(t, err)
6873
})
6974

7075
t.Run("failed", func(t *testing.T) {
71-
defer gock.Clean()
76+
defer gock.Off()
7277
gock.New(urlFoo).Reply(http.StatusNotFound)
7378

7479
_, err := runner.ExprFuncHTTPReady(urlFoo, 1)
@@ -89,4 +94,95 @@ func TestExprFuncHTTPReady(t *testing.T) {
8994
_, err := runner.ExprFuncHTTPReady(urlFoo, "two")
9095
assert.Error(t, err)
9196
})
97+
98+
t.Run("check the response", func(t *testing.T) {
99+
defer gock.Off()
100+
gock.New(urlFoo).Reply(http.StatusOK).BodyString(`{"name": "test"}`)
101+
_, err := runner.ExprFuncHTTPReady(urlFoo, 1, `data.name == "test"`)
102+
assert.NoError(t, err)
103+
})
104+
105+
t.Run("response is not JSON", func(t *testing.T) {
106+
defer gock.Off()
107+
gock.New(urlFoo).Reply(http.StatusOK).BodyString(`name: test`)
108+
_, err := runner.ExprFuncHTTPReady(urlFoo, 1, `data.name == "test"`)
109+
assert.Error(t, err)
110+
})
111+
112+
t.Run("response checking result is failed", func(t *testing.T) {
113+
defer gock.Off()
114+
gock.New(urlFoo).Reply(http.StatusOK).BodyString(`{"name": "test"}`)
115+
_, err := runner.ExprFuncHTTPReady(urlFoo, 1, `data.name == "test"`)
116+
assert.NoError(t, err)
117+
})
118+
119+
t.Run("not a bool expr", func(t *testing.T) {
120+
defer gock.Off()
121+
gock.New(urlFoo).Reply(http.StatusOK).BodyString(`{"name": "test"}`)
122+
_, err := runner.ExprFuncHTTPReady(urlFoo, 1, `name + "test"`)
123+
assert.Error(t, err)
124+
})
125+
126+
t.Run("failed to compile", func(t *testing.T) {
127+
defer gock.Off()
128+
gock.New(urlFoo).Reply(http.StatusOK).BodyString(`{"name": "test"}`)
129+
_, err := runner.ExprFuncHTTPReady(urlFoo, 1, `1~!@`)
130+
assert.Error(t, err)
131+
})
132+
}
133+
134+
func TestFunctions(t *testing.T) {
135+
tmpFile, err := os.CreateTemp(os.TempDir(), "test")
136+
if err != nil {
137+
t.Fatal("failed to create temp file")
138+
}
139+
defer os.Remove(tmpFile.Name())
140+
141+
tests := []struct {
142+
name string
143+
expr string
144+
syntaxErr bool
145+
verify func(t *testing.T, result any, resultErr error)
146+
}{{
147+
name: "invalid syntax",
148+
expr: "sleep 1",
149+
syntaxErr: true,
150+
}, {
151+
name: "command",
152+
expr: `command("echo 1")`,
153+
verify: func(t *testing.T, result any, resultErr error) {
154+
assert.NoError(t, resultErr)
155+
assert.Equal(t, "1\n", result)
156+
},
157+
}, {
158+
name: "writeFile",
159+
expr: fmt.Sprintf(`writeFile("%s", "hello")`, tmpFile.Name()),
160+
verify: func(t *testing.T, result any, resultErr error) {
161+
assert.NoError(t, resultErr)
162+
163+
data, err := io.ReadAll(tmpFile)
164+
assert.NoError(t, err)
165+
assert.Equal(t, "hello", string(data))
166+
},
167+
}}
168+
169+
for i, tt := range tests {
170+
t.Run(tt.name, func(t *testing.T) {
171+
var program *vm.Program
172+
program, err = expr.Compile(tt.expr, expr.Env(tt))
173+
if tt.syntaxErr {
174+
assert.Error(t, err, "%q %d", tt.name, i)
175+
return
176+
}
177+
if !assert.NotNil(t, program, "%q %d", tt.name, i) {
178+
return
179+
}
180+
181+
var result any
182+
result, err = expr.Run(program, tt)
183+
if tt.verify != nil {
184+
tt.verify(t, result, err)
185+
}
186+
})
187+
}
92188
}

pkg/runner/grpc.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (r *gRPCTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataContext
9393

9494
defer func() {
9595
if err == nil {
96-
err = runJob(testcase.After, dataContext)
96+
err = runJob(testcase.After, dataContext, output)
9797
}
9898
}()
9999

@@ -102,7 +102,7 @@ func (r *gRPCTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataContext
102102
return
103103
}
104104

105-
if err = runJob(testcase.Before, dataContext); err != nil {
105+
if err = runJob(testcase.Before, dataContext, nil); err != nil {
106106
return
107107
}
108108

0 commit comments

Comments
 (0)