From 0fb07285369f838b23430f4c23c47c478d7dd230 Mon Sep 17 00:00:00 2001 From: xushiwei Date: Mon, 11 Mar 2024 13:08:49 +0800 Subject: [PATCH 1/2] ytest match, README --- README.md | 2 +- gop.mod | 2 +- test/match.go | 30 ++++++ ytest/README.md | 112 ++++++++++++++++++++++ ytest/case.go | 30 ++++-- ytest/classfile.go | 4 +- ytest/demo/basic/gop_autogen_test.go | 4 +- ytest/demo/foo/gop_autogen_test.go | 8 +- ytest/demo/jwtdemo/gop_autogen_test.go | 4 +- ytest/demo/match/complex/complex_yapt.gox | 13 +++ ytest/demo/match/complex/gop_autogen.go | 39 ++++++++ ytest/demo/match/hello/get_p_#id.yap | 3 + ytest/demo/match/hello/gop_autogen.go | 63 ++++++++++++ ytest/demo/match/hello/hello_yapt.gox | 9 ++ ytest/demo/match/simple/gop_autogen.go | 41 ++++++++ ytest/demo/match/simple/simple_yapt.gox | 6 ++ ytest/request.go | 4 +- 17 files changed, 352 insertions(+), 22 deletions(-) create mode 100644 ytest/README.md create mode 100644 ytest/demo/match/complex/complex_yapt.gox create mode 100644 ytest/demo/match/complex/gop_autogen.go create mode 100644 ytest/demo/match/hello/get_p_#id.yap create mode 100644 ytest/demo/match/hello/gop_autogen.go create mode 100644 ytest/demo/match/hello/hello_yapt.gox create mode 100644 ytest/demo/match/simple/gop_autogen.go create mode 100644 ytest/demo/match/simple/simple_yapt.gox diff --git a/README.md b/README.md index 30fc103..d45cb46 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ run ":8080" ### yaptest: HTTP Test Framework -This classfile has the file suffix `_ytest.gox`. +yaptest is a web server testing framework. This classfile has the file suffix `_ytest.gox`. Suppose we have a web server ([foo/get_p_#id.yap](ytest/demo/foo/get_p_%23id.yap)): diff --git a/gop.mod b/gop.mod index e656abd..750bc58 100644 --- a/gop.mod +++ b/gop.mod @@ -14,7 +14,7 @@ import github.com/goplus/yap/ytest/auth/jwt project _ytest.gox App github.com/goplus/yap/ytest github.com/goplus/yap/test -class _ytest.gox Case +class _ytest.gox CaseApp import github.com/goplus/yap/ytest/auth/jwt diff --git a/test/match.go b/test/match.go index 78c22e4..5d18c1c 100644 --- a/test/match.go +++ b/test/match.go @@ -40,6 +40,27 @@ func toMapAny[T basetype](val map[string]T) map[string]any { return ret } +func tryToMapAny(val any) (ret map[string]any, ok bool) { + v := reflect.ValueOf(val) + return castMapAny(v) +} + +func castMapAny(v reflect.Value) (ret map[string]any, ok bool) { + if v.Kind() != reflect.Map || v.Type().Key() != tyString { + return + } + ret, ok = make(map[string]any, v.Len()), true + for it := v.MapRange(); it.Next(); { + key := it.Key().String() + ret[key] = it.Value().Interface() + } + return +} + +var ( + tyString = reflect.TypeOf("") +) + // ----------------------------------------------------------------------------- type baseelem interface { @@ -234,6 +255,11 @@ retry: case *Var__1[map[string]any]: Gopt_Case_MatchMap(t, ev, gv.Val(), name...) return + default: + if gv, ok := tryToMapAny(got); ok { + Gopt_Case_MatchMap(t, ev, gv, name...) + return + } } case []any: switch gv := got.(type) { @@ -351,6 +377,10 @@ retry: // other types: default: + if v, ok := tryToMapAny(expected); ok { + expected = v + goto retry + } if reflect.DeepEqual(expected, got) { return } diff --git a/ytest/README.md b/ytest/README.md new file mode 100644 index 0000000..26f3cb2 --- /dev/null +++ b/ytest/README.md @@ -0,0 +1,112 @@ +yaptest - Go+ HTTP Test Framework +===== + +yaptest is a web server testing framework. This classfile has the file suffix `_ytest.gox`. + +Before using `yaptest`, you need to add `github.com/goplus/yap` to `go.mod`: + +``` +gop get github.com/goplus/yap@latest +``` + +Suppose we have a web server ([foo/get_p_#id.yap](demo/foo/get_p_%23id.yap)): + +```go +json { + "id": ${id}, +} +``` + +Then we create a yaptest file ([foo/foo_ytest.gox](demo/foo/foo_ytest.gox)): + +```go +mock "foo.com", new(AppV2) // name of any YAP v2 web server is `AppV2` + +id := "123" +get "http://foo.com/p/${id}" +ret 200 +json { + "id": id, +} +``` + +The directive `mock` creates the web server by [mockhttp](https://pkg.go.dev/github.com/qiniu/x/mockhttp). Then we write test code directly. + +You can change the directive `mock` to `testServer` (see [foo/bar_ytest.gox](demo/foo/bar_ytest.gox)), and keep everything else unchanged: + +```go +testServer "foo.com", new(AppV2) + +id := "123" +get "http://foo.com/p/${id}" +ret 200 +json { + "id": id, +} +``` + +The directive `testServer` creates the web server by [net/http/httptest](https://pkg.go.dev/net/http/httptest#NewServer) and obtained a random port as the service address. Then it calls the directive [host](https://pkg.go.dev/github.com/goplus/yap/ytest#App.Host) to map the random service address to `foo.com`. This makes all other code no need to changed. + +## yaptest User Manual + +### match + +This is almost the core concept in `yaptest`. It matches two objects. + +Let’s look at [a simple example](demo/match/simple/simple_yapt.gox) first: + +```go +id := Var(int) +match id, 1+2 +echo id +``` + +Here we define a variable called `id` and match it with expression `1+2`. If the variable is unbound, it is assigned the value of the expression. + +So far, you've seen `match` like the assignment side. But you cannot assign a different value to a variable that has been bound: + +```go +id := Var(int) +match id, 1+2 +match id, 3 +echo id + +match id, 5 // unmatched value - expected: 3, got: 5 +``` + +In the second match statement, the variable `id` has been bound. At this time, it will be compared with the expression value. If it is equal, it will succeed, otherwise an error will be reported (such as the third match statement above). + +The `match` statement [can be complex](demo/match/complex/complex_yap.gox), such as: + +```go +d := Var(string) + +match { + "c": {"d": d}, +}, { + "a": 1, + "b": 3.14, + "c": {"d": "hello", "e": "world"}, + "f": 1, +} + +echo d +match d, "hello" +``` + +Generally, the syntax of the match command is: + +```go +match +``` + +Unbound variables are allowed in ``, but cannot appear in ``. `` and `` do not have to be exactly the same, but what appears in `` must also appear in ``. That is, it is required to be a subset relationship (`` is a subset of ``). If a variable in `` has not been bound, it will be bound according to the value of the corresponding ``; if the variable has been bound, the values on both sides must match. + +The cornerstone of `yaptest` is matching grammar. Let's look at the next example you saw at the beginning: + +```go +ret 200 +json { + "id": id, +} +``` diff --git a/ytest/case.go b/ytest/case.go index e5b2897..67639da 100644 --- a/ytest/case.go +++ b/ytest/case.go @@ -29,21 +29,14 @@ type CaseT = test.CaseT type Case struct { *Request - *App test.Case + app *App DefaultHeader http.Header } -// Gopt_Case_TestMain is required by Go+ compiler as the entry of a YAP test case. -func Gopt_Case_TestMain(c interface{ initCase(*App, CaseT) }, t *testing.T) { - app := new(App).initApp() - c.initCase(app, test.NewT(t)) - c.(interface{ Main() }).Main() -} - func (p *Case) initCase(app *App, t CaseT) { - p.App = app + p.app = app p.CaseT = t p.DefaultHeader = make(http.Header) } @@ -133,3 +126,22 @@ func (p *Case) DELETE(url string) *Request { } // ----------------------------------------------------------------------------- + +type CaseApp struct { + Case + *App +} + +// Gopt_CaseApp_TestMain is required by Go+ compiler as the entry of a YAP test case. +func Gopt_CaseApp_TestMain(c interface{ initCaseApp(*App, CaseT) }, t *testing.T) { + app := new(App).initApp() + c.initCaseApp(app, test.NewT(t)) + c.(interface{ Main() }).Main() +} + +func (p *CaseApp) initCaseApp(app *App, t CaseT) { + p.initCase(app, t) + p.App = app +} + +// ----------------------------------------------------------------------------- diff --git a/ytest/classfile.go b/ytest/classfile.go index 092e826..ecca909 100644 --- a/ytest/classfile.go +++ b/ytest/classfile.go @@ -26,6 +26,7 @@ import ( "github.com/goplus/yap" "github.com/goplus/yap/test" + "github.com/goplus/yap/test/logt" "github.com/qiniu/x/mockhttp" ) @@ -96,8 +97,9 @@ func Gopt_App_Main(app interface{ initApp() *App }, workers ...interface{ initCa if me, ok := app.(interface{ MainEntry() }); ok { me.MainEntry() } + t := logt.New() for _, worker := range workers { - worker.initCase(a, nil) + worker.initCase(a, t) worker.(interface{ Main() }).Main() } } diff --git a/ytest/demo/basic/gop_autogen_test.go b/ytest/demo/basic/gop_autogen_test.go index 4dfb8b0..4dfa69a 100644 --- a/ytest/demo/basic/gop_autogen_test.go +++ b/ytest/demo/basic/gop_autogen_test.go @@ -9,7 +9,7 @@ import ( ) type case_foo struct { - ytest.Case + ytest.CaseApp } //line ytest/demo/basic/foo_ytest.gox:1 func (this *case_foo) Main() { @@ -35,5 +35,5 @@ func (this *case_foo) Classfname() string { return "foo" } func Test_foo(t *testing.T) { - ytest.Gopt_Case_TestMain(new(case_foo), t) + ytest.Gopt_CaseApp_TestMain(new(case_foo), t) } diff --git a/ytest/demo/foo/gop_autogen_test.go b/ytest/demo/foo/gop_autogen_test.go index cbb0056..bff1043 100644 --- a/ytest/demo/foo/gop_autogen_test.go +++ b/ytest/demo/foo/gop_autogen_test.go @@ -10,10 +10,10 @@ import ( ) type case_bar struct { - ytest.Case + ytest.CaseApp } type case_foo struct { - ytest.Case + ytest.CaseApp } //line ytest/demo/foo/bar_ytest.gox:1 func (this *case_bar) Main() { @@ -50,8 +50,8 @@ func (this *case_foo) Classfname() string { return "foo" } func Test_bar(t *testing.T) { - ytest.Gopt_Case_TestMain(new(case_bar), t) + ytest.Gopt_CaseApp_TestMain(new(case_bar), t) } func Test_foo(t *testing.T) { - ytest.Gopt_Case_TestMain(new(case_foo), t) + ytest.Gopt_CaseApp_TestMain(new(case_foo), t) } diff --git a/ytest/demo/jwtdemo/gop_autogen_test.go b/ytest/demo/jwtdemo/gop_autogen_test.go index 3eb64ab..d565d4f 100644 --- a/ytest/demo/jwtdemo/gop_autogen_test.go +++ b/ytest/demo/jwtdemo/gop_autogen_test.go @@ -11,7 +11,7 @@ import ( ) type case_jwtdemo struct { - ytest.Case + ytest.CaseApp } //line ytest/demo/jwtdemo/jwtdemo_ytest.gox:7 func (this *case_jwtdemo) Main() { @@ -34,5 +34,5 @@ func (this *case_jwtdemo) Classfname() string { return "jwtdemo" } func Test_jwtdemo(t *testing.T) { - ytest.Gopt_Case_TestMain(new(case_jwtdemo), t) + ytest.Gopt_CaseApp_TestMain(new(case_jwtdemo), t) } diff --git a/ytest/demo/match/complex/complex_yapt.gox b/ytest/demo/match/complex/complex_yapt.gox new file mode 100644 index 0000000..a35fa2b --- /dev/null +++ b/ytest/demo/match/complex/complex_yapt.gox @@ -0,0 +1,13 @@ +d := Var(string) + +match { + "c": {"d": d}, +}, { + "a": 1, + "b": 3.14, + "c": {"d": "hello", "e": "world"}, + "f": 1, +} + +echo d +match d, "hello" diff --git a/ytest/demo/match/complex/gop_autogen.go b/ytest/demo/match/complex/gop_autogen.go new file mode 100644 index 0000000..d5a6b21 --- /dev/null +++ b/ytest/demo/match/complex/gop_autogen.go @@ -0,0 +1,39 @@ +// Code generated by gop (Go+); DO NOT EDIT. + +package main + +import ( + "fmt" + "github.com/goplus/yap/test" + "github.com/goplus/yap/ytest" +) + +const _ = true + +type complex struct { + ytest.Case + *App +} +type App struct { + ytest.App +} +//line ytest/demo/match/complex/complex_yapt.gox:1 +func (this *complex) Main() { +//line ytest/demo/match/complex/complex_yapt.gox:1:1 + d := test.Gopx_Var_Cast__0[string]() +//line ytest/demo/match/complex/complex_yapt.gox:3:1 + test.Gopt_Case_MatchAny(this, map[string]map[string]*test.Var__0[string]{"c": map[string]*test.Var__0[string]{"d": d}}, map[string]interface{}{"a": 1, "b": 3.14, "c": map[string]string{"d": "hello", "e": "world"}, "f": 1}) +//line ytest/demo/match/complex/complex_yapt.gox:12:1 + fmt.Println(d) +//line ytest/demo/match/complex/complex_yapt.gox:13:1 + test.Gopt_Case_MatchAny(this, d, "hello") +} +func (this *complex) Classfname() string { + return "complex" +} +func (this *App) Main() { + ytest.Gopt_App_Main(this, new(complex)) +} +func main() { + new(App).Main() +} diff --git a/ytest/demo/match/hello/get_p_#id.yap b/ytest/demo/match/hello/get_p_#id.yap new file mode 100644 index 0000000..c0f07b6 --- /dev/null +++ b/ytest/demo/match/hello/get_p_#id.yap @@ -0,0 +1,3 @@ +json { + "id": ${id}, +} diff --git a/ytest/demo/match/hello/gop_autogen.go b/ytest/demo/match/hello/gop_autogen.go new file mode 100644 index 0000000..4327e9d --- /dev/null +++ b/ytest/demo/match/hello/gop_autogen.go @@ -0,0 +1,63 @@ +// Code generated by gop (Go+); DO NOT EDIT. + +package main + +import ( + "fmt" + "github.com/goplus/yap" + "github.com/goplus/yap/ytest" + "github.com/qiniu/x/stringutil" +) + +const _ = true + +type get_p_id struct { + yap.Handler + *AppV2 +} +type hello struct { + ytest.Case + *App +} +type AppV2 struct { + yap.AppV2 +} +//line ytest/demo/match/hello/get_p_#id.yap:1 +func (this *get_p_id) Main(_gop_arg0 *yap.Context) { + this.Handler.Main(_gop_arg0) +//line ytest/demo/match/hello/get_p_#id.yap:1:1 + this.Json__1(map[string]string{"id": this.Gop_Env("id")}) +} +func (this *get_p_id) Classfname() string { + return "get_p_#id" +} + +type App struct { + ytest.App +} +//line ytest/demo/match/hello/hello_yapt.gox:1 +func (this *hello) Main() { +//line ytest/demo/match/hello/hello_yapt.gox:1:1 + this.Mock("foo.com", new(AppV2)) +//line ytest/demo/match/hello/hello_yapt.gox:3:1 + id := "123" +//line ytest/demo/match/hello/hello_yapt.gox:4:1 + this.Get(stringutil.Concat("http://foo.com/p/", id)) +//line ytest/demo/match/hello/hello_yapt.gox:5:1 + this.RetWith(200) +//line ytest/demo/match/hello/hello_yapt.gox:6:1 + this.Json(map[string]string{"id": id}) +//line ytest/demo/match/hello/hello_yapt.gox:9:1 + fmt.Println("OK") +} +func (this *hello) Classfname() string { + return "hello" +} +func (this *AppV2) Main() { + yap.Gopt_AppV2_Main(this, new(get_p_id)) +} +func (this *App) Main() { + ytest.Gopt_App_Main(this, new(hello)) +} +func main() { +} diff --git a/ytest/demo/match/hello/hello_yapt.gox b/ytest/demo/match/hello/hello_yapt.gox new file mode 100644 index 0000000..e661cdb --- /dev/null +++ b/ytest/demo/match/hello/hello_yapt.gox @@ -0,0 +1,9 @@ +mock "foo.com", new(AppV2) + +id := "123" +get "http://foo.com/p/${id}" +ret 200 +json { + "id": id, +} +echo "OK" diff --git a/ytest/demo/match/simple/gop_autogen.go b/ytest/demo/match/simple/gop_autogen.go new file mode 100644 index 0000000..6943216 --- /dev/null +++ b/ytest/demo/match/simple/gop_autogen.go @@ -0,0 +1,41 @@ +// Code generated by gop (Go+); DO NOT EDIT. + +package main + +import ( + "fmt" + "github.com/goplus/yap/test" + "github.com/goplus/yap/ytest" +) + +const _ = true + +type simple struct { + ytest.Case + *App +} +type App struct { + ytest.App +} +//line ytest/demo/match/simple/simple_yapt.gox:1 +func (this *simple) Main() { +//line ytest/demo/match/simple/simple_yapt.gox:1:1 + id := test.Gopx_Var_Cast__0[int]() +//line ytest/demo/match/simple/simple_yapt.gox:2:1 + test.Gopt_Case_MatchAny(this, id, 1+2) +//line ytest/demo/match/simple/simple_yapt.gox:3:1 + test.Gopt_Case_MatchAny(this, id, 3) +//line ytest/demo/match/simple/simple_yapt.gox:4:1 + fmt.Println(id) +//line ytest/demo/match/simple/simple_yapt.gox:6:1 + test.Gopt_Case_MatchAny(this, id, 5) +} +func (this *simple) Classfname() string { + return "simple" +} +func (this *App) Main() { + ytest.Gopt_App_Main(this, new(simple)) +} +func main() { + new(App).Main() +} diff --git a/ytest/demo/match/simple/simple_yapt.gox b/ytest/demo/match/simple/simple_yapt.gox new file mode 100644 index 0000000..3466a48 --- /dev/null +++ b/ytest/demo/match/simple/simple_yapt.gox @@ -0,0 +1,6 @@ +id := Var(int) +match id, 1+2 +match id, 3 +echo id + +match id, 5 // unmatched value - expected: 3, got: 5 diff --git a/ytest/request.go b/ytest/request.go index 254762c..29d3fe1 100644 --- a/ytest/request.go +++ b/ytest/request.go @@ -243,7 +243,7 @@ func mergeHeader(to, from http.Header) { func (p *Request) doSend() (resp *http.Response, err error) { body := p.body - req, err := p.ctx.newRequest(p.method, p.url, body) + req, err := p.ctx.app.newRequest(p.method, p.url, body) if err != nil { log.Fatalf("newRequest(%s, %s) failed: %v\n", p.method, p.url, err) } @@ -257,7 +257,7 @@ func (p *Request) doSend() (resp *http.Response, err error) { } req.ContentLength = body.Size() } - tr := p.ctx.transport + tr := p.ctx.app.transport if p.auth != nil { tr = p.auth.Compose(tr) } From 020b89b3c3309d75f2c0e8b910b1bc9e26b2c364 Mon Sep 17 00:00:00 2001 From: xushiwei Date: Mon, 11 Mar 2024 14:44:08 +0800 Subject: [PATCH 2/2] ytest: GopTestClass --- ytest/classfile.go | 5 ++++- ytest/demo/match/hello/gop_autogen.go | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ytest/classfile.go b/ytest/classfile.go index ecca909..746419e 100644 --- a/ytest/classfile.go +++ b/ytest/classfile.go @@ -21,6 +21,7 @@ import ( "net/http" "net/http/httptest" "os" + "reflect" "strings" "testing" @@ -31,7 +32,8 @@ import ( ) const ( - GopPackage = "github.com/goplus/yap/test" + GopPackage = "github.com/goplus/yap/test" + GopTestClass = true ) // ----------------------------------------------------------------------------- @@ -100,6 +102,7 @@ func Gopt_App_Main(app interface{ initApp() *App }, workers ...interface{ initCa t := logt.New() for _, worker := range workers { worker.initCase(a, t) + reflect.ValueOf(worker).Elem().Field(1).Set(reflect.ValueOf(app)) // (*worker).App = app worker.(interface{ Main() }).Main() } } diff --git a/ytest/demo/match/hello/gop_autogen.go b/ytest/demo/match/hello/gop_autogen.go index 4327e9d..6e47be0 100644 --- a/ytest/demo/match/hello/gop_autogen.go +++ b/ytest/demo/match/hello/gop_autogen.go @@ -60,4 +60,5 @@ func (this *App) Main() { ytest.Gopt_App_Main(this, new(hello)) } func main() { + new(App).Main() }