update id

add tpl
support reload on watched file changes
This commit is contained in:
Star 2024-11-30 10:44:02 +08:00
parent d416c61a6a
commit 7bb6938cb7
9 changed files with 483 additions and 181 deletions

2
.gitignore vendored
View File

@ -2,8 +2,6 @@
!.gitignore
go.sum
/build
/node_modules
/package.json
/bak
node_modules
package.json

24
go.mod
View File

@ -3,29 +3,32 @@ module apigo.cc/gojs/service
go 1.18
require (
apigo.cc/gojs v0.0.4
apigo.cc/gojs/console v0.0.1
apigo.cc/gojs v0.0.7
apigo.cc/gojs/console v0.0.2
apigo.cc/gojs/http v0.0.3
apigo.cc/gojs/util v0.0.3
apigo.cc/gojs/util v0.0.7
github.com/gorilla/websocket v1.5.3
github.com/ssgo/config v1.7.8
github.com/ssgo/config v1.7.9
github.com/ssgo/discover v1.7.9
github.com/ssgo/httpclient v1.7.8
github.com/ssgo/log v1.7.7
github.com/ssgo/redis v1.7.7
github.com/ssgo/s v1.7.18
github.com/ssgo/s v1.7.20
github.com/ssgo/standard v1.7.7
github.com/ssgo/u v1.7.9
github.com/ssgo/u v1.7.12
)
require (
github.com/ZZMarquis/gm v1.3.2 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/emmansun/gmsm v0.29.4 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
@ -33,8 +36,9 @@ require (
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,15 +1,19 @@
package service
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
"sync"
"syscall"
"text/template"
"time"
"apigo.cc/gojs"
@ -39,6 +43,16 @@ var poolActionRegistered = map[string]bool{}
var poolsLock = sync.RWMutex{}
var waitChan chan bool
type TplCache struct {
FileModTime map[string]int64
Tpl *template.Template
}
var tplFunc = map[string]any{}
var tplFuncLock = sync.RWMutex{}
var tplCache = map[string]*TplCache{}
var tplCacheLock = sync.RWMutex{}
type LimiterConfig struct {
From string
Time int
@ -69,112 +83,117 @@ var onStop goja.Callable
var limiters = map[string]*s.Limiter{}
var configed = false
func initConfig(opt *gojs.Obj, logger *log.Logger, vm *goja.Runtime) {
configed = true
s.InitConfig()
if startPath, ok := vm.GoData["startPath"]; ok {
s.SetWorkPath(u.String(startPath))
}
// 处理配置
serviceConfig = Config{"Session", "Device", "Client", "userId", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", map[string]string{}, map[string]string{}, map[string]string{}}
if errs := config.LoadConfig("service", &serviceConfig); errs != nil && len(errs) > 0 {
panic(vm.NewGoError(errs[0]))
}
config.LoadConfig("service", &discover.Config)
// var auth goja.Callable
if opt != nil {
u.Convert(opt.O.Export(), &serviceConfig)
u.Convert(opt.O.Export(), &s.Config)
u.Convert(opt.O.Export(), &discover.Config)
// auth = conf.Func("auth")
onStop = opt.Func("onStop")
if serviceConfig.SessionProvider != "" {
sessionRedis = redis.GetRedis(serviceConfig.SessionProvider, logger)
}
sessionTimeout = serviceConfig.SessionTimeout
if sessionTimeout < 0 {
sessionTimeout = 0
}
}
// 身份验证和Session
authAccessToken := len(s.Config.AccessTokens) > 0
s.SetClientKeys(serviceConfig.DeviceKey, serviceConfig.ClientKey, serviceConfig.SessionKey)
if serviceConfig.SessionKey != "" {
s.SetAuthChecker(func(authLevel int, logger *log.Logger, url *string, args map[string]any, request *s.Request, response *s.Response, options *s.WebServiceOptions) (pass bool, object any) {
var session *Session
setAuthLevel := 0
if serviceConfig.SessionKey != "" {
sessionID := request.GetSessionId()
if sessionID != "" {
session = NewSession(sessionID, logger)
}
if userId, ok := session.data[serviceConfig.UserIdKey]; ok {
request.SetUserId(u.String(userId))
}
// 优先使用session中的authLevel
if authLevel > 0 {
if authLevelBySession, ok := session.data["_authLevel"]; ok {
setAuthLevel = u.Int(authLevelBySession)
}
}
}
// 如果没有session中的authLevel验证失败则使用Access-Token中的authLevel服务间调用
if authAccessToken && setAuthLevel < authLevel {
setAuthLevel = s.GetAuthTokenLevel(request.Header.Get("Access-Token"))
}
if setAuthLevel >= authLevel {
return true, session
} else {
msg := serviceConfig.AuthFieldMessage
if strings.Contains(msg, "{{") {
msg = strings.ReplaceAll(msg, "{{TARGET_AUTHLEVEL}}", u.String(authLevel))
msg = strings.ReplaceAll(msg, "{{USER_AUTHLEVEL}}", u.String(setAuthLevel))
}
var obj any
if json.Unmarshal([]byte(msg), &obj) == nil {
return false, obj
} else {
return false, msg
}
}
})
s.Init()
}
// 限流器
if serviceConfig.Limiters != nil {
var limiterRedis *redis.Redis
if serviceConfig.LimiterRedis != "" {
limiterRedis = redis.GetRedis(serviceConfig.LimiterRedis, logger)
}
for name, limiter := range serviceConfig.Limiters {
switch limiter.From {
case "ip":
limiter.From = "header." + standard.DiscoverHeaderClientIp
case "user":
limiter.From = "header." + standard.DiscoverHeaderUserId
case "device":
limiter.From = "header." + standard.DiscoverHeaderDeviceId
}
if limiterRedis != nil {
limiters[name] = s.NewLimiter(name, limiter.From, time.Duration(limiter.Time)*time.Millisecond, limiter.Times, limiterRedis)
} else {
limiters[name] = s.NewLocalLimiter(name, limiter.From, time.Duration(limiter.Time)*time.Millisecond, limiter.Times)
}
}
}
}
func init() {
s.Config.KeepKeyCase = true
obj := map[string]any{
"config": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
configed = true
s.InitConfig()
if startPath, ok := vm.GoData["startPath"]; ok {
s.SetWorkPath(u.String(startPath))
}
// 处理配置
args := gojs.MakeArgs(&argsIn, vm)
serviceConfig = Config{"Session", "Device", "Client", "userId", "", 3600, "auth failed", "verify failed", "too many requests", nil, "", map[string]string{}, map[string]string{}, map[string]string{}}
if errs := config.LoadConfig("service", &serviceConfig); errs != nil && len(errs) > 0 {
panic(vm.NewGoError(errs[0]))
}
config.LoadConfig("service", &discover.Config)
// var auth goja.Callable
if conf := args.Obj(0); conf != nil {
u.Convert(conf.O.Export(), &serviceConfig)
u.Convert(conf.O.Export(), &s.Config)
u.Convert(conf.O.Export(), &discover.Config)
// auth = conf.Func("auth")
onStop = conf.Func("onStop")
if serviceConfig.SessionProvider != "" {
sessionRedis = redis.GetRedis(serviceConfig.SessionProvider, args.Logger)
}
sessionTimeout = serviceConfig.SessionTimeout
if sessionTimeout < 0 {
sessionTimeout = 0
}
}
// 身份验证和Session
authAccessToken := len(s.Config.AccessTokens) > 0
s.SetClientKeys(serviceConfig.DeviceKey, serviceConfig.ClientKey, serviceConfig.SessionKey)
if serviceConfig.SessionKey != "" {
s.SetAuthChecker(func(authLevel int, logger *log.Logger, url *string, args map[string]any, request *s.Request, response *s.Response, options *s.WebServiceOptions) (pass bool, object any) {
var session *Session
setAuthLevel := 0
if serviceConfig.SessionKey != "" {
sessionID := request.GetSessionId()
if sessionID != "" {
session = NewSession(sessionID, logger)
}
if userId, ok := session.data[serviceConfig.UserIdKey]; ok {
request.SetUserId(u.String(userId))
}
// 优先使用session中的authLevel
if authLevel > 0 {
if authLevelBySession, ok := session.data["_authLevel"]; ok {
setAuthLevel = u.Int(authLevelBySession)
}
}
}
// 如果没有session中的authLevel验证失败则使用Access-Token中的authLevel服务间调用
if authAccessToken && setAuthLevel < authLevel {
setAuthLevel = s.GetAuthTokenLevel(request.Header.Get("Access-Token"))
}
if setAuthLevel >= authLevel {
return true, session
} else {
msg := serviceConfig.AuthFieldMessage
if strings.Contains(msg, "{{") {
msg = strings.ReplaceAll(msg, "{{TARGET_AUTHLEVEL}}", u.String(authLevel))
msg = strings.ReplaceAll(msg, "{{USER_AUTHLEVEL}}", u.String(setAuthLevel))
}
var obj any
if json.Unmarshal([]byte(msg), &obj) == nil {
return false, obj
} else {
return false, msg
}
}
})
s.Init()
}
// 限流器
if serviceConfig.Limiters != nil {
var limiterRedis *redis.Redis
if serviceConfig.LimiterRedis != "" {
limiterRedis = redis.GetRedis(serviceConfig.LimiterRedis, args.Logger)
}
for name, limiter := range serviceConfig.Limiters {
switch limiter.From {
case "ip":
limiter.From = "header." + standard.DiscoverHeaderClientIp
case "user":
limiter.From = "header." + standard.DiscoverHeaderUserId
case "device":
limiter.From = "header." + standard.DiscoverHeaderDeviceId
}
if limiterRedis != nil {
limiters[name] = s.NewLimiter(name, limiter.From, time.Duration(limiter.Time)*time.Millisecond, limiter.Times, limiterRedis)
} else {
limiters[name] = s.NewLocalLimiter(name, limiter.From, time.Duration(limiter.Time)*time.Millisecond, limiter.Times)
}
}
}
initConfig(args.Obj(0), args.Logger, vm)
return nil
},
"start": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
if !configed {
panic(vm.NewGoError(errors.New("must run service.config frist")))
initConfig(nil, gojs.GetLogger(vm), vm)
// panic(vm.NewGoError(errors.New("must run service.config frist")))
}
if server != nil {
panic(vm.NewGoError(errors.New("server already started")))
@ -192,6 +211,55 @@ func init() {
s.SetProxyBy(proxy)
}
// 处理Watch
if vm.GoData["inWatch"] == true {
onWatchConn := map[string]*websocket.Conn{}
onWatchLock := sync.RWMutex{}
vm.GoData["onWatch"] = func(filename string) {
conns := map[string]*websocket.Conn{}
onWatchLock.RLock()
for id, conn := range onWatchConn {
conns[id] = conn
}
onWatchLock.RUnlock()
for id, conn := range conns {
if err := conn.WriteMessage(websocket.TextMessage, []byte(filename)); err != nil {
onWatchLock.Lock()
delete(onWatchConn, id)
onWatchLock.Unlock()
} else {
}
}
}
s.AddShutdownHook(func() {
for _, conn := range onWatchConn {
conn.Close()
}
})
s.RegisterSimpleWebsocket(0, "/_watch", func(request *s.Request, conn *websocket.Conn) {
onWatchLock.Lock()
onWatchConn[request.Id] = conn
onWatchLock.Unlock()
}, "")
s.SetOutFilter(func(in map[string]any, request *s.Request, response *s.Response, out any, logger *log.Logger) (newOut any, isOver bool) {
if strings.HasPrefix(response.Header().Get("Content-Type"), "text/html") {
outStr := u.String(out)
// 注入自动刷新的代码
outStr = strings.ReplaceAll(outStr, "</html>", `<script>
function connect() {
let ws = new WebSocket(location.protocol.replace('http', 'ws') + '//' + location.host + '/_watch')
ws.onmessage = () => { location.reload() }
ws.onclose = () => { setTimeout(connect, 1000) }
}
connect()
</script>
</html>`)
return []byte(outStr), false
}
return nil, false
})
}
// 启动服务
server = s.AsyncStart()
waitChan = make(chan bool, 1)
@ -203,6 +271,8 @@ func init() {
server.OnStopped(func() {
ClearRewritesAndProxies()
pools = map[string]*gojs.Pool{}
poolExists = map[string]bool{}
poolActionRegistered = map[string]bool{}
server = nil
if waitChan != nil {
waitChan <- true
@ -217,6 +287,72 @@ func init() {
server.Stop()
return nil
},
"uniqueId": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm)
size := args.Int(0)
var id string
if size >= 20 {
id = s.UniqueId20()
} else if size >= 16 {
id = s.UniqueId16()
} else if size >= 14 {
id = s.UniqueId14()
} else if size >= 12 {
id = s.UniqueId14()[0:12]
} else {
id = s.UniqueId()
}
return vm.ToValue(id)
},
"uniqueIdL": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm)
size := args.Int(0)
var id string
if size >= 20 {
id = s.UniqueId20()
} else if size >= 16 {
id = s.UniqueId16()
} else if size >= 14 {
id = s.UniqueId14()
} else if size >= 12 {
id = s.UniqueId14()[0:12]
} else {
id = s.UniqueId()
}
return vm.ToValue(strings.ToLower(id))
},
"id": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
space := args.Str(0)
size := args.Int(1)
var id string
if size >= 12 {
id = s.Id12(space)
} else if size >= 10 {
id = s.Id10(space)
} else if size >= 8 {
id = s.Id8(space)
} else {
id = s.Id6(space)
}
return vm.ToValue(id)
},
"idL": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
space := args.Str(0)
size := args.Int(1)
var id string
if size >= 12 {
id = s.Id12(space)
} else if size >= 10 {
id = s.Id10(space)
} else if size >= 8 {
id = s.Id8(space)
} else {
id = s.Id6(space)
}
return vm.ToValue(strings.ToLower(id))
},
"register": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2)
o := args.Obj(0)
@ -391,6 +527,100 @@ func init() {
})
return nil
},
"setTplFunc": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(1)
fnObj := args.Obj(0)
if fnObj != nil {
fnList := map[string]any{}
for _, k := range fnObj.O.Keys() {
if jsFunc := fnObj.Func(k); jsFunc != nil {
fn := func(args ...any) any {
jsArgs := make([]goja.Value, len(args))
for i := 0; i < len(args); i++ {
jsArgs[i] = vm.ToValue(args[i])
}
if r, err := jsFunc(argsIn.This, jsArgs...); err == nil {
return r.Export()
} else {
panic(vm.NewGoError(err))
}
}
fnList[k] = fn
}
}
if len(fnList) > 0 {
tplFuncLock.Lock()
for k, v := range fnList {
tplFunc[k] = v
}
tplFuncLock.Unlock()
}
}
return nil
},
"tpl": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
args := gojs.MakeArgs(&argsIn, vm).Check(2)
filename := args.Path(0)
info := u.GetFileInfo(filename)
if info == nil {
panic(vm.NewGoError(errors.New("tpl file " + filename + " not exists")))
}
data := args.Any(1)
tplCacheLock.RLock()
t := tplCache[filename]
tplCacheLock.RUnlock()
if t != nil {
for f, tm := range t.FileModTime {
info := u.GetFileInfo(f)
if info == nil || info.ModTime.UnixMilli() != tm {
t = nil
break
}
}
}
if t == nil {
tpl := template.New("main")
if len(tplFunc) > 0 {
tpl = tpl.Funcs(tplFunc)
}
fileModTime := map[string]int64{
filename: info.ModTime.UnixMilli(),
}
var err error
for _, m := range tplIncludeMatcher.FindAllStringSubmatch(u.ReadFileN(filename), -1) {
includeFilename := m[1]
info2 := u.GetFileInfo(includeFilename)
if info2 == nil {
includeFilename = filepath.Join(filepath.Dir(filename), m[1])
info2 = u.GetFileInfo(includeFilename)
}
if info2 != nil {
tpl, err = tpl.Parse(`{{ define "` + m[1] + `" }}` + u.ReadFileN(includeFilename) + `{{ end }}`)
if err != nil {
panic(vm.NewGoError(err))
}
fileModTime[includeFilename] = info2.ModTime.UnixMilli()
}
}
tpl, err = tpl.ParseFiles(filename)
if err != nil {
panic(vm.NewGoError(err))
}
t = &TplCache{
Tpl: tpl,
FileModTime: fileModTime,
}
}
buf := bytes.NewBuffer(make([]byte, 0))
err := t.Tpl.ExecuteTemplate(buf, filepath.Base(filename), data)
if err != nil {
panic(vm.NewGoError(err))
}
return vm.ToValue(buf.String())
},
"dataSet": DataSet,
"dataGet": DataGet,
"dataKeys": DataKeys,
@ -413,6 +643,12 @@ func init() {
server.Stop()
}
},
OnSignal: func(sig os.Signal) {
switch sig {
case syscall.SIGUSR1:
}
},
WaitForStop: func() {
if waitChan != nil {
<-waitChan
@ -421,6 +657,8 @@ func init() {
})
}
var tplIncludeMatcher = regexp.MustCompile(`{{\s*template\s+"([^"]+)"`)
func verifyRegexp(regexpStr string) func(any, *goja.Runtime) bool {
if rx, err := regexp.Compile(regexpStr); err != nil {
return func(value any, vm *goja.Runtime) bool {

View File

@ -18,6 +18,12 @@ export default {
listPop,
listCount,
listRemove,
id,
idL,
uniqueId,
uniqueIdL,
setTplFunc,
tpl,
}
function config(config?: Config): void { }
@ -41,7 +47,12 @@ function listPush(scope: string, key: string, value: any): void { }
function listPop(scope: string, key: string): any { return null }
function listCount(scope: string): number { return 0 }
function listRemove(scope: string): void { }
function id(space: string, size?: number): string { return '' }
function idL(space: string, size?: number): string { return '' }
function uniqueId(size?: number): string { return '' }
function uniqueIdL(size?: number): string { return '' }
function setTplFunc(fnList: Object): void { }
function tpl(file: string, data: Object): string { return '' }
interface Config {
// github.com/ssgo/s 的配置参数

View File

@ -1,6 +1,7 @@
package service_test
import (
"fmt"
"testing"
"time"
@ -9,6 +10,7 @@ import (
_ "apigo.cc/gojs/http"
_ "apigo.cc/gojs/service"
_ "apigo.cc/gojs/util"
"github.com/ssgo/httpclient"
"github.com/ssgo/u"
)
@ -41,80 +43,80 @@ func TestStatic(t *testing.T) {
}
}
// func TestJsEcho(t *testing.T) {
// for i := 0; i < runTimes; i++ {
// name := u.UniqueId()
// r, err := rt.RunCode("test('" + name + "')")
// if err != nil {
// t.Fatal("test js get failed, got error", err)
// } else if r != name {
// t.Fatal("test js get failed, name not match", r, name)
// }
// }
// }
func TestJsEcho(t *testing.T) {
for i := 0; i < runTimes; i++ {
name := u.UniqueId()
r, err := rt.RunCode("test('" + name + "')")
if err != nil {
t.Fatal("test js get failed, got error", err)
} else if r != name {
t.Fatal("test js get failed, name not match", r, name)
}
}
}
// func TestGoEcho(t *testing.T) {
// hc := httpclient.GetClientH2C(0)
// for i := 0; i < runTimes; i++ {
// name := u.UniqueId()
// r := hc.Get("http://" + addr + "/echo?name=" + name)
// if r.Error != nil {
// t.Fatal("test go get failed, got error", r.Error)
// } else if r.String() != name {
// t.Fatal("test go get failed, name not match", r, name)
// }
// }
// }
func TestGoEcho(t *testing.T) {
hc := httpclient.GetClientH2C(0)
for i := 0; i < runTimes; i++ {
name := u.UniqueId()
r := hc.Get("http://" + addr + "/echo?name=" + name)
if r.Error != nil {
t.Fatal("test go get failed, got error", r.Error)
} else if r.String() != name {
t.Fatal("test go get failed, name not match", r, name)
}
}
}
// func TestJsAsyncEcho(t *testing.T) {
// ch := make(chan bool, runTimes)
// t1 := time.Now().UnixMilli()
// for i := 0; i < runTimes; i++ {
// go func() {
// name := u.UniqueId()
// r, err := rt.RunCode("test('" + name + "')")
// ch <- true
// if err != nil {
// t.Fatal("test js async get failed, got error", err)
// } else if r != name {
// t.Fatal("test js async get failed, name not match", r, name)
// }
// }()
// }
// for i := 0; i < runTimes; i++ {
// <-ch
// }
// t2 := time.Now().UnixMilli() - t1
// fmt.Println(u.BGreen("js async test time:"), t2, "ms")
// }
func TestJsAsyncEcho(t *testing.T) {
ch := make(chan bool, runTimes)
t1 := time.Now().UnixMilli()
for i := 0; i < runTimes; i++ {
go func() {
name := u.UniqueId()
r, err := rt.RunCode("test('" + name + "')")
ch <- true
if err != nil {
t.Fatal("test js async get failed, got error", err)
} else if r != name {
t.Fatal("test js async get failed, name not match", r, name)
}
}()
}
for i := 0; i < runTimes; i++ {
<-ch
}
t2 := time.Now().UnixMilli() - t1
fmt.Println(u.BGreen("js async test time:"), t2, "ms")
}
// func TestGoAsyncEcho(t *testing.T) {
// hc := httpclient.GetClientH2C(0)
// ch := make(chan bool, runTimes*10)
// t1 := time.Now().UnixMilli()
// lastName := ""
// lastResult := ""
// for i := 0; i < runTimes*10; i++ {
// name := fmt.Sprint("N", i)
// lastName = name
// go func() {
// r := hc.Get("http://" + addr + "/echo?name=" + name)
// lastResult = r.String()
// ch <- true
// if r.Error != nil {
// t.Fatal("test go async get failed, got error", r.Error)
// } else if r.String() != name {
// t.Fatal("test go async get failed, name not match", r, name)
// }
// }()
// }
// for i := 0; i < runTimes*10; i++ {
// <-ch
// }
// t2 := time.Now().UnixMilli() - t1
// fmt.Println(u.BGreen("go async test time:"), t2, "ms")
// fmt.Println(u.BGreen("last name:"), lastName, lastResult)
// }
func TestGoAsyncEcho(t *testing.T) {
hc := httpclient.GetClientH2C(0)
ch := make(chan bool, runTimes*10)
t1 := time.Now().UnixMilli()
lastName := ""
lastResult := ""
for i := 0; i < runTimes*10; i++ {
name := fmt.Sprint("N", i)
lastName = name
go func() {
r := hc.Get("http://" + addr + "/echo?name=" + name)
lastResult = r.String()
ch <- true
if r.Error != nil {
t.Fatal("test go async get failed, got error", r.Error)
} else if r.String() != name {
t.Fatal("test go async get failed, name not match", r, name)
}
}()
}
for i := 0; i < runTimes*10; i++ {
<-ch
}
t2 := time.Now().UnixMilli() - t1
fmt.Println(u.BGreen("go async test time:"), t2, "ms")
fmt.Println(u.BGreen("last name:"), lastName, lastResult)
}
func TestStop(t *testing.T) {
go func() {

8
tests/tpl.js Normal file
View File

@ -0,0 +1,8 @@
import s from "apigo.cc/gojs/service"
function main(args) {
s.setTplFunc({
bb: text => { return '<b>' + text + '</b>' }
})
return s.tpl('tpl/page.html', { title: 'Abc' })
}

3
tests/tpl/header.html Normal file
View File

@ -0,0 +1,3 @@
<header>
<h1>Welcome to {{bb .title}}</h1>
</header>

18
tests/tpl/page.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{.title}}</title>
</head>
<body>
{{template "header.html" .}}
<pre>
hello world
</pre>
</body>
</html>

20
tests/tpl_test.go Normal file
View File

@ -0,0 +1,20 @@
package service_test
import (
"strings"
"testing"
"apigo.cc/gojs"
_ "apigo.cc/gojs/service"
"github.com/ssgo/u"
)
func TestTpl(t *testing.T) {
r, err := gojs.RunFile("tpl.js")
if err != nil {
t.Fatal("test static failed, got error", err)
}
if !strings.Contains(u.String(r), "<h1>Welcome to <b>Abc</b></h1>") {
t.Fatal("test tpl failed, name not match", r)
}
}