commit
a0a87b8520
@ -1,48 +0,0 @@ |
|||||||
load( |
|
||||||
"@io_bazel_rules_go//go:def.bzl", |
|
||||||
"go_library", |
|
||||||
) |
|
||||||
load( |
|
||||||
"@io_bazel_rules_go//proto:def.bzl", |
|
||||||
"go_proto_library", |
|
||||||
) |
|
||||||
|
|
||||||
go_library( |
|
||||||
name = "go_default_library", |
|
||||||
srcs = [], |
|
||||||
embed = [":proto_go_proto"], |
|
||||||
importpath = "go-common/library/cache/memcache/test", |
|
||||||
tags = ["automanaged"], |
|
||||||
visibility = ["//visibility:public"], |
|
||||||
deps = ["@com_github_golang_protobuf//proto:go_default_library"], |
|
||||||
) |
|
||||||
|
|
||||||
filegroup( |
|
||||||
name = "package-srcs", |
|
||||||
srcs = glob(["**"]), |
|
||||||
tags = ["automanaged"], |
|
||||||
visibility = ["//visibility:private"], |
|
||||||
) |
|
||||||
|
|
||||||
filegroup( |
|
||||||
name = "all-srcs", |
|
||||||
srcs = [":package-srcs"], |
|
||||||
tags = ["automanaged"], |
|
||||||
visibility = ["//visibility:public"], |
|
||||||
) |
|
||||||
|
|
||||||
proto_library( |
|
||||||
name = "test_proto", |
|
||||||
srcs = ["test.proto"], |
|
||||||
import_prefix = "go-common/library/cache/memcache/test", |
|
||||||
strip_import_prefix = "", |
|
||||||
tags = ["automanaged"], |
|
||||||
) |
|
||||||
|
|
||||||
go_proto_library( |
|
||||||
name = "proto_go_proto", |
|
||||||
compilers = ["@io_bazel_rules_go//proto:go_proto"], |
|
||||||
importpath = "go-common/library/cache/memcache/test", |
|
||||||
proto = ":test_proto", |
|
||||||
tags = ["automanaged"], |
|
||||||
) |
|
@ -0,0 +1,9 @@ |
|||||||
|
version: "3.7" |
||||||
|
|
||||||
|
services: |
||||||
|
mc: |
||||||
|
image: memcached:1 |
||||||
|
ports: |
||||||
|
- 11211:11211 |
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@ |
|||||||
|
## testing/lich 运行环境构建 |
||||||
|
基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。 |
||||||
|
|
||||||
|
使用说明参见:https://github.com/bilibili/kratos/tree/master/tool/testcli/README.md |
@ -0,0 +1,125 @@ |
|||||||
|
package lich |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"crypto/md5" |
||||||
|
"encoding/json" |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"path/filepath" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/bilibili/kratos/pkg/log" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
retry int |
||||||
|
noDown bool |
||||||
|
yamlPath string |
||||||
|
pathHash string |
||||||
|
services map[string]*Container |
||||||
|
) |
||||||
|
|
||||||
|
func init() { |
||||||
|
flag.StringVar(&yamlPath, "f", "docker-compose.yaml", "composer yaml path.") |
||||||
|
flag.BoolVar(&noDown, "nodown", false, "containers are not recycled.") |
||||||
|
} |
||||||
|
|
||||||
|
func runCompose(args ...string) (output []byte, err error) { |
||||||
|
if _, err = os.Stat(yamlPath); os.IsNotExist(err) { |
||||||
|
log.Error("os.Stat(%s) composer yaml is not exist!", yamlPath) |
||||||
|
return |
||||||
|
} |
||||||
|
if yamlPath, err = filepath.Abs(yamlPath); err != nil { |
||||||
|
log.Error("filepath.Abs(%s) error(%v)", yamlPath, err) |
||||||
|
return |
||||||
|
} |
||||||
|
pathHash = fmt.Sprintf("%x", md5.Sum([]byte(yamlPath)))[:9] |
||||||
|
args = append([]string{"-f", yamlPath, "-p", pathHash}, args...) |
||||||
|
if output, err = exec.Command("docker-compose", args...).CombinedOutput(); err != nil { |
||||||
|
log.Error("exec.Command(docker-compose) args(%v) stdout(%s) error(%v)", args, string(output), err) |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Setup setup UT related environment dependence for everything.
|
||||||
|
func Setup() (err error) { |
||||||
|
if _, err = runCompose("up", "-d"); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
defer func() { |
||||||
|
if err != err { |
||||||
|
go Teardown() |
||||||
|
} |
||||||
|
}() |
||||||
|
if _, err = getServices(); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
_, err = checkServices() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// Teardown unsetup all environment dependence.
|
||||||
|
func Teardown() (err error) { |
||||||
|
if !noDown { |
||||||
|
_, err = runCompose("down") |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func getServices() (output []byte, err error) { |
||||||
|
if output, err = runCompose("config", "--services"); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
services = make(map[string]*Container) |
||||||
|
output = bytes.TrimSpace(output) |
||||||
|
for _, svr := range bytes.Split(output, []byte("\n")) { |
||||||
|
if output, err = runCompose("ps", "-a", "-q", string(svr)); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
var ( |
||||||
|
id = string(bytes.TrimSpace(output)) |
||||||
|
args = []string{"inspect", id, "--format", "'{{json .}}'"} |
||||||
|
) |
||||||
|
if output, err = exec.Command("docker", args...).CombinedOutput(); err != nil { |
||||||
|
log.Error("exec.Command(docker) args(%v) stdout(%s) error(%v)", args, string(output), err) |
||||||
|
return |
||||||
|
} |
||||||
|
if output = bytes.TrimSpace(output); bytes.Equal(output, []byte("")) { |
||||||
|
err = fmt.Errorf("service: %s | container: %s fails to launch", svr, id) |
||||||
|
log.Error("exec.Command(docker) args(%v) error(%v)", args, err) |
||||||
|
return |
||||||
|
} |
||||||
|
var c = &Container{} |
||||||
|
if err = json.Unmarshal(bytes.Trim(output, "'"), c); err != nil { |
||||||
|
log.Error("json.Unmarshal(%s) error(%v)", string(output), err) |
||||||
|
return |
||||||
|
} |
||||||
|
services[string(svr)] = c |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func checkServices() (output []byte, err error) { |
||||||
|
defer func() { |
||||||
|
if err != nil && retry < 4 { |
||||||
|
retry++ |
||||||
|
getServices() |
||||||
|
time.Sleep(time.Second * 5) |
||||||
|
output, err = checkServices() |
||||||
|
return |
||||||
|
} |
||||||
|
retry = 0 |
||||||
|
}() |
||||||
|
for svr, c := range services { |
||||||
|
if err = c.Healthcheck(); err != nil { |
||||||
|
log.Error("healthcheck(%s) error(%v) retrying %d times...", svr, err, 5-retry) |
||||||
|
return |
||||||
|
} |
||||||
|
// TODO About container check and more...
|
||||||
|
} |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,85 @@ |
|||||||
|
package lich |
||||||
|
|
||||||
|
import ( |
||||||
|
"database/sql" |
||||||
|
"fmt" |
||||||
|
"net" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/bilibili/kratos/pkg/log" |
||||||
|
// Register go-sql-driver stuff
|
||||||
|
_ "github.com/go-sql-driver/mysql" |
||||||
|
) |
||||||
|
|
||||||
|
var healthchecks = map[string]func(*Container) error{"mysql": checkMysql, "mariadb": checkMysql} |
||||||
|
|
||||||
|
// Healthcheck check container health.
|
||||||
|
func (c *Container) Healthcheck() (err error) { |
||||||
|
if status, health := c.State.Status, c.State.Health.Status; !c.State.Running || (health != "" && health != "healthy") { |
||||||
|
err = fmt.Errorf("service: %s | container: %s not running", c.GetImage(), c.GetID()) |
||||||
|
log.Error("docker status(%s) health(%s) error(%v)", status, health, err) |
||||||
|
return |
||||||
|
} |
||||||
|
if check, ok := healthchecks[c.GetImage()]; ok { |
||||||
|
err = check(c) |
||||||
|
return |
||||||
|
} |
||||||
|
for proto, ports := range c.NetworkSettings.Ports { |
||||||
|
if id := c.GetID(); !strings.Contains(proto, "tcp") { |
||||||
|
log.Error("container: %s proto(%s) unsupported.", id, proto) |
||||||
|
continue |
||||||
|
} |
||||||
|
for _, publish := range ports { |
||||||
|
var ( |
||||||
|
ip = net.ParseIP(publish.HostIP) |
||||||
|
port, _ = strconv.Atoi(publish.HostPort) |
||||||
|
tcpAddr = &net.TCPAddr{IP: ip, Port: port} |
||||||
|
tcpConn *net.TCPConn |
||||||
|
) |
||||||
|
if tcpConn, err = net.DialTCP("tcp", nil, tcpAddr); err != nil { |
||||||
|
log.Error("net.DialTCP(%s:%s) error(%v)", publish.HostIP, publish.HostPort, err) |
||||||
|
return |
||||||
|
} |
||||||
|
tcpConn.Close() |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func checkMysql(c *Container) (err error) { |
||||||
|
var ip, port, user, passwd string |
||||||
|
for _, env := range c.Config.Env { |
||||||
|
splits := strings.Split(env, "=") |
||||||
|
if strings.Contains(splits[0], "MYSQL_ROOT_PASSWORD") { |
||||||
|
user, passwd = "root", splits[1] |
||||||
|
continue |
||||||
|
} |
||||||
|
if strings.Contains(splits[0], "MYSQL_ALLOW_EMPTY_PASSWORD") { |
||||||
|
user, passwd = "root", "" |
||||||
|
continue |
||||||
|
} |
||||||
|
if strings.Contains(splits[0], "MYSQL_USER") { |
||||||
|
user = splits[1] |
||||||
|
continue |
||||||
|
} |
||||||
|
if strings.Contains(splits[0], "MYSQL_PASSWORD") { |
||||||
|
passwd = splits[1] |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
var db *sql.DB |
||||||
|
if ports, ok := c.NetworkSettings.Ports["3306/tcp"]; ok { |
||||||
|
ip, port = ports[0].HostIP, ports[0].HostPort |
||||||
|
} |
||||||
|
var dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/", user, passwd, ip, port) |
||||||
|
if db, err = sql.Open("mysql", dsn); err != nil { |
||||||
|
log.Error("sql.Open(mysql) dsn(%s) error(%v)", dsn, err) |
||||||
|
return |
||||||
|
} |
||||||
|
if err = db.Ping(); err != nil { |
||||||
|
log.Error("ping(db) dsn(%s) error(%v)", dsn, err) |
||||||
|
} |
||||||
|
defer db.Close() |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,88 @@ |
|||||||
|
package lich |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Container docker inspect resp.
|
||||||
|
type Container struct { |
||||||
|
ID string `json:"Id"` |
||||||
|
Created time.Time `json:"Created"` |
||||||
|
Path string `json:"Path"` |
||||||
|
Args []string `json:"Args"` |
||||||
|
State struct { |
||||||
|
Status string `json:"Status"` |
||||||
|
Running bool `json:"Running"` |
||||||
|
Paused bool `json:"Paused"` |
||||||
|
Restarting bool `json:"Restarting"` |
||||||
|
OOMKilled bool `json:"OOMKilled"` |
||||||
|
Dead bool `json:"Dead"` |
||||||
|
Pid int `json:"Pid"` |
||||||
|
ExitCode int `json:"ExitCode"` |
||||||
|
Error string `json:"Error"` |
||||||
|
StartedAt time.Time `json:"StartedAt"` |
||||||
|
FinishedAt time.Time `json:"FinishedAt"` |
||||||
|
Health struct { |
||||||
|
Status string `json:"Status"` |
||||||
|
FailingStreak int `json:"FailingStreak"` |
||||||
|
Log []struct { |
||||||
|
Start time.Time `json:"Start"` |
||||||
|
End time.Time `json:"End"` |
||||||
|
ExitCode int `json:"ExitCode"` |
||||||
|
Output string `json:"Output"` |
||||||
|
} `json:"Log"` |
||||||
|
} `json:"Health"` |
||||||
|
} `json:"State"` |
||||||
|
Config struct { |
||||||
|
Hostname string `json:"Hostname"` |
||||||
|
Domainname string `json:"Domainname"` |
||||||
|
User string `json:"User"` |
||||||
|
Tty bool `json:"Tty"` |
||||||
|
OpenStdin bool `json:"OpenStdin"` |
||||||
|
StdinOnce bool `json:"StdinOnce"` |
||||||
|
Env []string `json:"Env"` |
||||||
|
Cmd []string `json:"Cmd"` |
||||||
|
Image string `json:"Image"` |
||||||
|
WorkingDir string `json:"WorkingDir"` |
||||||
|
Entrypoint []string `json:"Entrypoint"` |
||||||
|
} `json:"Config"` |
||||||
|
Image string `json:"Image"` |
||||||
|
ResolvConfPath string `json:"ResolvConfPath"` |
||||||
|
HostnamePath string `json:"HostnamePath"` |
||||||
|
HostsPath string `json:"HostsPath"` |
||||||
|
LogPath string `json:"LogPath"` |
||||||
|
Name string `json:"Name"` |
||||||
|
RestartCount int `json:"RestartCount"` |
||||||
|
Driver string `json:"Driver"` |
||||||
|
Platform string `json:"Platform"` |
||||||
|
MountLabel string `json:"MountLabel"` |
||||||
|
ProcessLabel string `json:"ProcessLabel"` |
||||||
|
AppArmorProfile string `json:"AppArmorProfile"` |
||||||
|
NetworkSettings struct { |
||||||
|
Bridge string `json:"Bridge"` |
||||||
|
SandboxID string `json:"SandboxID"` |
||||||
|
HairpinMode bool `json:"HairpinMode"` |
||||||
|
Ports map[string][]struct { |
||||||
|
HostIP string `json:"HostIp"` |
||||||
|
HostPort string `json:"HostPort"` |
||||||
|
} `json:"Ports"` |
||||||
|
} `json:"NetworkSettings"` |
||||||
|
} |
||||||
|
|
||||||
|
// GetImage get image name at container
|
||||||
|
func (c *Container) GetImage() (image string) { |
||||||
|
image = c.Config.Image |
||||||
|
if images := strings.Split(image, ":"); len(images) > 0 { |
||||||
|
image = images[0] |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// GetID get id at container
|
||||||
|
func (c *Container) GetID() (id string) { |
||||||
|
if id = c.ID; len(id) > 9 { |
||||||
|
id = id[0:9] |
||||||
|
} |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,154 @@ |
|||||||
|
## testcli UT运行环境构建工具 |
||||||
|
基于 docker-compose 实现跨平台跨语言环境的容器依赖管理方案,以解决运行ut场景下的 (mysql, redis, mc)容器依赖问题。 |
||||||
|
|
||||||
|
*这个是testing/lich的二进制工具版本(Go请直接使用库版本:github.com/bilibili/kratos/pkg/testing/lich)* |
||||||
|
|
||||||
|
### 功能和特性 |
||||||
|
- 自动读取 test 目录下的 yaml 并启动依赖 |
||||||
|
- 自动导入 test 目录下的 DB 初始化 SQL |
||||||
|
- 提供特定容器内的 healthcheck (mysql, mc, redis) |
||||||
|
- 提供一站式解决 UT 服务依赖的工具版本 (testcli) |
||||||
|
|
||||||
|
### 编译安装 |
||||||
|
*使用本工具/库需要前置安装好 docker & docker-compose@v1.24.1^* |
||||||
|
|
||||||
|
#### Method 1. With go get |
||||||
|
```shell |
||||||
|
go get -u github.com/bilibili/kratos/tool/testcli |
||||||
|
$GOPATH/bin/testcli -h |
||||||
|
``` |
||||||
|
#### Method 2. Build with Go |
||||||
|
```shell |
||||||
|
cd github.com/bilibili/kratos/tool/testcli |
||||||
|
go build -o $GOPATH/bin/testcli |
||||||
|
$GOPATH/bin/testcli -h |
||||||
|
``` |
||||||
|
#### Method 3. Import with Kratos pkg |
||||||
|
```Go |
||||||
|
import "github.com/bilibili/kratos/pkg/testing/lich" |
||||||
|
``` |
||||||
|
|
||||||
|
### 构建数据 |
||||||
|
#### Step 1. create docker-compose.yml |
||||||
|
创建依赖服务的 docker-compose.yml,并把它放在项目路径下的 test 文件夹下面。例如: |
||||||
|
```shell |
||||||
|
mkdir -p $YOUR_PROJECT/test |
||||||
|
``` |
||||||
|
```yaml |
||||||
|
version: "3.7" |
||||||
|
|
||||||
|
services: |
||||||
|
db: |
||||||
|
image: mysql:5.6 |
||||||
|
ports: |
||||||
|
- 3306:3306 |
||||||
|
environment: |
||||||
|
- MYSQL_ROOT_PASSWORD=root |
||||||
|
volumes: |
||||||
|
- .:/docker-entrypoint-initdb.d |
||||||
|
command: [ |
||||||
|
'--character-set-server=utf8', |
||||||
|
'--collation-server=utf8_unicode_ci' |
||||||
|
] |
||||||
|
|
||||||
|
redis: |
||||||
|
image: redis |
||||||
|
ports: |
||||||
|
- 6379:6379 |
||||||
|
``` |
||||||
|
一般来讲,我们推荐在项目根目录创建 test 目录,里面存放描述服务的yml,以及需要初始化的数据(database.sql等)。 |
||||||
|
|
||||||
|
同时也需要注意,正确的对容器内服务进行健康检测,testcli会在容器的health状态执行UT,其实我们也内置了针对几个较为通用镜像(mysql mariadb mc redis)的健康检测,也就是不写也没事(^^;; |
||||||
|
|
||||||
|
#### Step 2. export database.sql |
||||||
|
构造初始化的数据(database.sql等),当然也把它也在 test 文件夹里。 |
||||||
|
```sql |
||||||
|
CREATE DATABASE IF NOT EXISTS `YOUR_DATABASE_NAME`; |
||||||
|
|
||||||
|
SET NAMES 'utf8'; |
||||||
|
USE `YOUR_DATABASE_NAME`; |
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `YOUR_TABLE_NAME` ( |
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', |
||||||
|
PRIMARY KEY (`id`), |
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='YOUR_TABLE_NAME'; |
||||||
|
``` |
||||||
|
这里需要注意,在创建库/表的时候尽量加上 IF NOT EXISTS,以给予一定程度的容错,以及 SET NAMES 'utf8'; 用于解决客户端连接乱码问题。 |
||||||
|
|
||||||
|
#### Step 3. change your project mysql config |
||||||
|
```toml |
||||||
|
[mysql] |
||||||
|
addr = "127.0.0.1:3306" |
||||||
|
dsn = "root:root@tcp(127.0.0.1:3306)/YOUR_DATABASE?timeout=1s&readTimeout=1s&writeTimeout=1s&parseTime=true&loc=Local&charset=utf8mb4,utf8" |
||||||
|
active = 20 |
||||||
|
idle = 10 |
||||||
|
idleTimeout ="1s" |
||||||
|
queryTimeout = "1s" |
||||||
|
execTimeout = "1s" |
||||||
|
tranTimeout = "1s" |
||||||
|
``` |
||||||
|
在 *Step 1* 我们已经指定了服务对外暴露的端口为3306(这当然也可以是你指定的任何值),那理所应当的我们也要修改项目连接数据库的配置~ |
||||||
|
|
||||||
|
Great! 至此你已经完成了运行所需要用到的数据配置,接下来就来运行它。 |
||||||
|
|
||||||
|
### 运行 |
||||||
|
开头也说过本工具支持两种运行方式:testcli 二进制工具版本和 go package 源码包,业务方可以根据需求场景进行选择。 |
||||||
|
#### Method 1. With testcli tool |
||||||
|
*已支持的 flag: -f,--nodown,down,run* |
||||||
|
- -f,指定 docker-compose.yaml 文件路径,默认为当前目录下。 |
||||||
|
- --nodown,指定是否在UT执行完成后保留容器,以供下次复用。 |
||||||
|
- down,teardown 销毁当前项目下这个 compose 文件产生的容器。 |
||||||
|
- run,运行你当前语言的单测执行命令(如:golang为 go test -v ./) |
||||||
|
|
||||||
|
example: |
||||||
|
```shell |
||||||
|
testcli -f ../../test/docker-compose.yaml run go test -v ./ |
||||||
|
``` |
||||||
|
#### Method 2. Import with Kratos pkg |
||||||
|
- Step1. 在 Dao|Service 层中的 TestMain 单测主入口中,import "go-common/library/testing/lich" 引入testcli工具的go库版本。 |
||||||
|
- Step2. 使用 flag.Set("f", "../../test/docker-compose.yaml") 指定 docker-compose.yaml 文件的路径。 |
||||||
|
- Step3. 在 flag.Parse() 后即可使用 lich.Setup() 安装依赖&初始化数据(注意测试用例执行结束后 lich.Teardown() 回收下~) |
||||||
|
- Step4. 运行 `go test -v ./ `看看效果吧~ |
||||||
|
|
||||||
|
example: |
||||||
|
```Go |
||||||
|
package dao |
||||||
|
|
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/bilibili/kratos/pkg/conf/paladin" |
||||||
|
"github.com/bilibili/kratos/pkg/testing/lich" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
d *Dao |
||||||
|
) |
||||||
|
|
||||||
|
func TestMain(m *testing.M) { |
||||||
|
flag.Set("conf", "../../configs") |
||||||
|
flag.Set("f", "../../test/docker-compose.yaml") |
||||||
|
flag.Parse() |
||||||
|
if err := paladin.Init(); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if err := lich.Setup(); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
defer lich.Teardown() |
||||||
|
d = New() |
||||||
|
if code := m.Run(); code != 0 { |
||||||
|
panic(code) |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
## 注意 |
||||||
|
因为启动mysql容器较为缓慢,健康检测的机制会重试3次,每次暂留5秒钟,基本在10s内mysql就能从creating到服务正常启动! |
||||||
|
|
||||||
|
当然你也可以在使用 testcli 时加上 --nodown,使其不用每次跑都新建容器,只在第一次跑的时候会初始化容器,后面都进行复用,这样速度会快很多。 |
||||||
|
|
||||||
|
成功启动后就欢乐奔放的玩耍吧~ Good Lucky! |
@ -0,0 +1,26 @@ |
|||||||
|
version: "3.7" |
||||||
|
|
||||||
|
services: |
||||||
|
db: |
||||||
|
image: mysql:5.6 |
||||||
|
ports: |
||||||
|
- 3306:3306 |
||||||
|
environment: |
||||||
|
- MYSQL_ROOT_PASSWORD=root |
||||||
|
- TZ=Asia/Shanghai |
||||||
|
volumes: |
||||||
|
- .:/docker-entrypoint-initdb.d |
||||||
|
command: [ |
||||||
|
'--character-set-server=utf8', |
||||||
|
'--collation-server=utf8_unicode_ci' |
||||||
|
] |
||||||
|
|
||||||
|
redis: |
||||||
|
image: redis |
||||||
|
ports: |
||||||
|
- 6379:6379 |
||||||
|
|
||||||
|
memcached: |
||||||
|
image: memcached |
||||||
|
ports: |
||||||
|
- 11211:11211 |
@ -0,0 +1,50 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/bilibili/kratos/pkg/testing/lich" |
||||||
|
) |
||||||
|
|
||||||
|
func parseArgs() (flags map[string]string) { |
||||||
|
flags = make(map[string]string) |
||||||
|
for idx, arg := range os.Args { |
||||||
|
if idx == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
if arg == "down" { |
||||||
|
flags["down"] = "" |
||||||
|
return |
||||||
|
} |
||||||
|
if cmds := os.Args[idx+1:]; arg == "run" { |
||||||
|
flags["run"] = strings.Join(cmds, " ") |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func main() { |
||||||
|
flag.Parse() |
||||||
|
flags := parseArgs() |
||||||
|
if _, ok := flags["down"]; ok { |
||||||
|
lich.Teardown() |
||||||
|
return |
||||||
|
} |
||||||
|
if cmd, ok := flags["run"]; !ok || cmd == "" { |
||||||
|
panic("Your need 'run' flag assign to be run commands.") |
||||||
|
} |
||||||
|
if err := lich.Setup(); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
defer lich.Teardown() |
||||||
|
cmds := strings.Split(flags["run"], " ") |
||||||
|
cmd := exec.Command(cmds[0], cmds[1:]...) |
||||||
|
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr |
||||||
|
if err := cmd.Run(); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
## testgen UT代码自动生成器 |
||||||
|
解放你的双手,让你的UT一步到位! |
||||||
|
|
||||||
|
### 功能和特性 |
||||||
|
- 支持生成 Dao|Service 层UT代码功能(每个方法包含一个正向用例) |
||||||
|
- 支持生成 Dao|Service 层测试入口文件dao_test.go, service_test.go(用于控制初始化,控制测试流程等) |
||||||
|
- 支持生成Mock代码(使用GoMock框架) |
||||||
|
- 支持选择不同模式生成不同代码(使用"–m mode"指定) |
||||||
|
- 生成单元测试代码时,同时支持传入目录或文件 |
||||||
|
- 支持指定方法追加生成测试用例(使用"–func funcName"指定) |
||||||
|
|
||||||
|
### 编译安装 |
||||||
|
#### Method 1. With go get |
||||||
|
```shell |
||||||
|
go get -u github.com/bilibili/kratos/tool/testgen |
||||||
|
$GOPATH/bin/testgen -h |
||||||
|
``` |
||||||
|
#### Method 2. Build with Go |
||||||
|
```shell |
||||||
|
cd github.com/bilibili/kratos/tool/testgen |
||||||
|
go build -o $GOPATH/bin/testgen |
||||||
|
$GOPATH/bin/testgen -h |
||||||
|
``` |
||||||
|
### 运行 |
||||||
|
#### 生成Dao/Service层单元UT |
||||||
|
```shell |
||||||
|
$GOPATH/bin/testgen YOUR_PROJECT/dao # default mode |
||||||
|
$GOPATH/bin/testgen --m test path/to/your/pkg |
||||||
|
$GOPATH/bin/testgen --func functionName path/to/your/pkg |
||||||
|
``` |
||||||
|
|
||||||
|
#### 生成接口类型 |
||||||
|
```shell |
||||||
|
$GOPATH/bin/testgen --m interface YOUR_PROJECT/dao #当前仅支持传目录,如目录包含子目录也会做处理 |
||||||
|
``` |
||||||
|
|
||||||
|
#### 生成Mock代码 |
||||||
|
```shell |
||||||
|
$GOPATH/bin/testgen --m mock YOUR_PROJECT/dao #仅传入包路径即可 |
||||||
|
``` |
||||||
|
|
||||||
|
#### 生成Monkey代码 |
||||||
|
```shell |
||||||
|
$GOPATH/bin/testgen --m monkey yourCodeDirPath #仅传入包路径即可 |
||||||
|
``` |
||||||
|
### 赋诗一首 |
||||||
|
``` |
||||||
|
莫生气 莫生气 |
||||||
|
代码辣鸡非我意 |
||||||
|
自己动手分田地 |
||||||
|
谈笑风生活长命 |
||||||
|
``` |
@ -0,0 +1,419 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/otokaze/mock/mockgen" |
||||||
|
"github.com/otokaze/mock/mockgen/model" |
||||||
|
) |
||||||
|
|
||||||
|
func genTest(parses []*parse) (err error) { |
||||||
|
for _, p := range parses { |
||||||
|
switch { |
||||||
|
case strings.HasSuffix(p.Path, "_mock.go") || |
||||||
|
strings.HasSuffix(p.Path, ".intf.go"): |
||||||
|
continue |
||||||
|
case strings.HasSuffix(p.Path, "dao.go") || |
||||||
|
strings.HasSuffix(p.Path, "service.go"): |
||||||
|
err = p.genTestMain() |
||||||
|
default: |
||||||
|
err = p.genUTTest() |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (p *parse) genUTTest() (err error) { |
||||||
|
var ( |
||||||
|
buffer bytes.Buffer |
||||||
|
impts = strings.Join([]string{ |
||||||
|
`"context"`, |
||||||
|
`"testing"`, |
||||||
|
`. "github.com/smartystreets/goconvey/convey"`, |
||||||
|
}, "\n\t") |
||||||
|
content []byte |
||||||
|
) |
||||||
|
filename := strings.Replace(p.Path, ".go", "_test.go", -1) |
||||||
|
if _, err = os.Stat(filename); (_func == "" && err == nil) || |
||||||
|
(err != nil && os.IsExist(err)) { |
||||||
|
err = nil |
||||||
|
return |
||||||
|
} |
||||||
|
for _, impt := range p.Imports { |
||||||
|
impts += "\n\t\"" + impt.V + "\"" |
||||||
|
} |
||||||
|
if _func == "" { |
||||||
|
buffer.WriteString(fmt.Sprintf(tpPackage, p.Package)) |
||||||
|
buffer.WriteString(fmt.Sprintf(tpImport, impts)) |
||||||
|
} |
||||||
|
for _, parseFunc := range p.Funcs { |
||||||
|
if _func != "" && _func != parseFunc.Name { |
||||||
|
continue |
||||||
|
} |
||||||
|
var ( |
||||||
|
methodK string |
||||||
|
tpVars string |
||||||
|
vars []string |
||||||
|
val []string |
||||||
|
notice = "Then " |
||||||
|
reset string |
||||||
|
) |
||||||
|
if method := ConvertMethod(p.Path); method != "" { |
||||||
|
methodK = method + "." |
||||||
|
} |
||||||
|
tpTestFuncs := fmt.Sprintf(tpTestFunc, strings.Title(p.Package), parseFunc.Name, "", parseFunc.Name, "%s", "%s", "%s") |
||||||
|
tpTestFuncBeCall := methodK + parseFunc.Name + "(%s)\n\t\t\tConvey(\"%s\", func() {" |
||||||
|
if parseFunc.Result == nil { |
||||||
|
tpTestFuncBeCall = fmt.Sprintf(tpTestFuncBeCall, "%s", "No return values") |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", tpTestFuncBeCall, "%s") |
||||||
|
} |
||||||
|
for k, res := range parseFunc.Result { |
||||||
|
if res.K == "" { |
||||||
|
res.K = fmt.Sprintf("p%d", k+1) |
||||||
|
} |
||||||
|
var so string |
||||||
|
if res.V == "error" { |
||||||
|
res.K = "err" |
||||||
|
so = fmt.Sprintf("\tSo(%s, ShouldBeNil)", res.K) |
||||||
|
notice += "err should be nil." |
||||||
|
} else { |
||||||
|
so = fmt.Sprintf("\tSo(%s, ShouldNotBeNil)", res.K) |
||||||
|
val = append(val, res.K) |
||||||
|
} |
||||||
|
if len(parseFunc.Result) <= k+1 { |
||||||
|
if len(val) != 0 { |
||||||
|
notice += strings.Join(val, ",") + " should not be nil." |
||||||
|
} |
||||||
|
tpTestFuncBeCall = fmt.Sprintf(tpTestFuncBeCall, "%s", notice) |
||||||
|
res.K += " := " + tpTestFuncBeCall |
||||||
|
} else { |
||||||
|
res.K += ", %s" |
||||||
|
} |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", res.K+"\n\t\t\t%s", "%s") |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", "%s", so, "%s") |
||||||
|
} |
||||||
|
if parseFunc.Params == nil { |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", "", "%s") |
||||||
|
} |
||||||
|
for k, pType := range parseFunc.Params { |
||||||
|
if pType.K == "" { |
||||||
|
pType.K = fmt.Sprintf("a%d", k+1) |
||||||
|
} |
||||||
|
var ( |
||||||
|
init string |
||||||
|
params = pType.K |
||||||
|
) |
||||||
|
switch { |
||||||
|
case strings.HasPrefix(pType.V, "context"): |
||||||
|
init = params + " = context.Background()" |
||||||
|
case strings.HasPrefix(pType.V, "[]byte"): |
||||||
|
init = params + " = " + pType.V + "(\"\")" |
||||||
|
case strings.HasPrefix(pType.V, "[]"): |
||||||
|
init = params + " = " + pType.V + "{}" |
||||||
|
case strings.HasPrefix(pType.V, "int") || |
||||||
|
strings.HasPrefix(pType.V, "uint") || |
||||||
|
strings.HasPrefix(pType.V, "float") || |
||||||
|
strings.HasPrefix(pType.V, "double"): |
||||||
|
init = params + " = " + pType.V + "(0)" |
||||||
|
case strings.HasPrefix(pType.V, "string"): |
||||||
|
init = params + " = \"\"" |
||||||
|
case strings.Contains(pType.V, "*xsql.Tx"): |
||||||
|
init = params + ",_ = " + methodK + "BeginTran(c)" |
||||||
|
reset += "\n\t" + params + ".Commit()" |
||||||
|
case strings.HasPrefix(pType.V, "*"): |
||||||
|
init = params + " = " + strings.Replace(pType.V, "*", "&", -1) + "{}" |
||||||
|
case strings.Contains(pType.V, "chan"): |
||||||
|
init = params + " = " + pType.V |
||||||
|
case pType.V == "time.Time": |
||||||
|
init = params + " = time.Now()" |
||||||
|
case strings.Contains(pType.V, "chan"): |
||||||
|
init = params + " = " + pType.V |
||||||
|
default: |
||||||
|
init = params + " " + pType.V |
||||||
|
} |
||||||
|
vars = append(vars, "\t\t"+init) |
||||||
|
if len(parseFunc.Params) > k+1 { |
||||||
|
params += ", %s" |
||||||
|
} |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "%s", params, "%s") |
||||||
|
} |
||||||
|
if len(vars) > 0 { |
||||||
|
tpVars = fmt.Sprintf(tpVar, strings.Join(vars, "\n\t")) |
||||||
|
} |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, tpVars, "%s") |
||||||
|
if reset != "" { |
||||||
|
tpTestResets := fmt.Sprintf(tpTestReset, reset) |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, tpTestResets) |
||||||
|
} else { |
||||||
|
tpTestFuncs = fmt.Sprintf(tpTestFuncs, "") |
||||||
|
} |
||||||
|
buffer.WriteString(tpTestFuncs) |
||||||
|
} |
||||||
|
var ( |
||||||
|
file *os.File |
||||||
|
flag = os.O_RDWR | os.O_CREATE | os.O_APPEND |
||||||
|
) |
||||||
|
if file, err = os.OpenFile(filename, flag, 0644); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if _func == "" { |
||||||
|
content, _ = GoImport(filename, buffer.Bytes()) |
||||||
|
} else { |
||||||
|
content = buffer.Bytes() |
||||||
|
} |
||||||
|
if _, err = file.Write(content); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if err = file.Close(); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (p *parse) genTestMain() (err error) { |
||||||
|
var ( |
||||||
|
new bool |
||||||
|
buffer bytes.Buffer |
||||||
|
impts string |
||||||
|
vars, mainFunc string |
||||||
|
content []byte |
||||||
|
instance, confFunc string |
||||||
|
tomlPath = "**PUT PATH TO YOUR CONFIG FILES HERE**" |
||||||
|
filename = strings.Replace(p.Path, ".go", "_test.go", -1) |
||||||
|
) |
||||||
|
if p.Imports["paladin"] != nil { |
||||||
|
new = true |
||||||
|
} |
||||||
|
// if _intfMode {
|
||||||
|
// imptsList = append(imptsList, `"github.com/golang/mock/gomock"`)
|
||||||
|
// for _, field := range p.Structs {
|
||||||
|
// var hit bool
|
||||||
|
// pkgName := strings.Split(field.V, ".")[0]
|
||||||
|
// interfaceName := strings.Split(field.V, ".")[1]
|
||||||
|
// if p.Imports[pkgName] != nil {
|
||||||
|
// if hit, err = checkInterfaceMock(strings.Split(field.V, ".")[1], p.Imports[pkgName].V); err != nil {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if hit {
|
||||||
|
// imptsList = append(imptsList, "mock"+p.Imports[pkgName].K+" \""+p.Imports[pkgName].V+"/mock\"")
|
||||||
|
// pkgName = "mock" + strings.Title(pkgName)
|
||||||
|
// interfaceName = "Mock" + interfaceName
|
||||||
|
// varsList = append(varsList, "mock"+strings.Title(field.K)+" *"+pkgName+"."+interfaceName)
|
||||||
|
// mockStmt += "\tmock" + strings.Title(field.K) + " = " + pkgName + ".New" + interfaceName + "(mockCtrl)\n"
|
||||||
|
// newStmt += "\t\t" + field.K + ":\tmock" + strings.Title(field.K) + ",\n"
|
||||||
|
// } else {
|
||||||
|
// pkgName = subString(field.V, "*", ".")
|
||||||
|
// if p.Imports[pkgName] != nil && pkgName != "conf" {
|
||||||
|
// imptsList = append(imptsList, p.Imports[pkgName].K+" \""+p.Imports[pkgName].V+"\"")
|
||||||
|
// }
|
||||||
|
// switch {
|
||||||
|
// case strings.HasPrefix(field.V, "*conf."):
|
||||||
|
// newStmt += "\t\t" + field.K + ":\tconf.Conf,\n"
|
||||||
|
// case strings.HasPrefix(field.V, "*"):
|
||||||
|
// newStmt += "\t\t" + field.K + ":\t" + strings.Replace(field.V, "*", "&", -1) + "{},\n"
|
||||||
|
// default:
|
||||||
|
// newStmt += "\t\t" + field.K + ":\t" + field.V + ",\n"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// mockStmt = fmt.Sprintf(_tpTestServiceMainMockStmt, mockStmt)
|
||||||
|
// newStmt = fmt.Sprintf(_tpTestServiceMainNewStmt, newStmt)
|
||||||
|
// }
|
||||||
|
if instance = ConvertMethod(p.Path); instance == "s" { |
||||||
|
vars = strings.Join([]string{"s *Service"}, "\n\t") |
||||||
|
mainFunc = tpTestServiceMain |
||||||
|
} else { |
||||||
|
vars = strings.Join([]string{"d *Dao"}, "\n\t") |
||||||
|
mainFunc = tpTestDaoMain |
||||||
|
} |
||||||
|
if new { |
||||||
|
impts = strings.Join([]string{`"os"`, `"flag"`, `"testing"`, p.Imports["paladin"].V}, "\n\t") |
||||||
|
confFunc = fmt.Sprintf(tpTestMainNew, instance+" = New()") |
||||||
|
} else { |
||||||
|
impts = strings.Join(append([]string{`"os"`, `"flag"`, `"testing"`}), "\n\t") |
||||||
|
confFunc = fmt.Sprintf(tpTestMainOld, instance+" = New(conf.Conf)") |
||||||
|
} |
||||||
|
if _, err := os.Stat(filename); os.IsNotExist(err) { |
||||||
|
buffer.WriteString(fmt.Sprintf(tpPackage, p.Package)) |
||||||
|
buffer.WriteString(fmt.Sprintf(tpImport, impts)) |
||||||
|
buffer.WriteString(fmt.Sprintf(tpVar, vars)) |
||||||
|
buffer.WriteString(fmt.Sprintf(mainFunc, tomlPath, confFunc)) |
||||||
|
content, _ = GoImport(filename, buffer.Bytes()) |
||||||
|
ioutil.WriteFile(filename, content, 0644) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func genInterface(parses []*parse) (err error) { |
||||||
|
var ( |
||||||
|
parse *parse |
||||||
|
pkg = make(map[string]string) |
||||||
|
) |
||||||
|
for _, parse = range parses { |
||||||
|
if strings.Contains(parse.Path, ".intf.go") { |
||||||
|
continue |
||||||
|
} |
||||||
|
dirPath := filepath.Dir(parse.Path) |
||||||
|
for _, parseFunc := range parse.Funcs { |
||||||
|
if (parseFunc.Method == nil) || |
||||||
|
!(parseFunc.Name[0] >= 'A' && parseFunc.Name[0] <= 'Z') { |
||||||
|
continue |
||||||
|
} |
||||||
|
var ( |
||||||
|
params string |
||||||
|
results string |
||||||
|
) |
||||||
|
for k, param := range parseFunc.Params { |
||||||
|
params += param.K + " " + param.P + param.V |
||||||
|
if len(parseFunc.Params) > k+1 { |
||||||
|
params += ", " |
||||||
|
} |
||||||
|
} |
||||||
|
for k, res := range parseFunc.Result { |
||||||
|
results += res.K + " " + res.P + res.V |
||||||
|
if len(parseFunc.Result) > k+1 { |
||||||
|
results += ", " |
||||||
|
} |
||||||
|
} |
||||||
|
if len(results) != 0 { |
||||||
|
results = "(" + results + ")" |
||||||
|
} |
||||||
|
pkg[dirPath] += "\t" + fmt.Sprintf(tpIntfcFunc, parseFunc.Name, params, results) |
||||||
|
} |
||||||
|
} |
||||||
|
for k, v := range pkg { |
||||||
|
var buffer bytes.Buffer |
||||||
|
pathSplit := strings.Split(k, "/") |
||||||
|
filename := k + "/" + pathSplit[len(pathSplit)-1] + ".intf.go" |
||||||
|
if _, exist := os.Stat(filename); os.IsExist(exist) { |
||||||
|
continue |
||||||
|
} |
||||||
|
buffer.WriteString(fmt.Sprintf(tpPackage, pathSplit[len(pathSplit)-1])) |
||||||
|
buffer.WriteString(fmt.Sprintf(tpInterface, strings.Title(pathSplit[len(pathSplit)-1]), v)) |
||||||
|
content, _ := GoImport(filename, buffer.Bytes()) |
||||||
|
err = ioutil.WriteFile(filename, content, 0644) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func genMock(files ...string) (err error) { |
||||||
|
for _, file := range files { |
||||||
|
var pkg *model.Package |
||||||
|
if pkg, err = mockgen.ParseFile(file); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if len(pkg.Interfaces) == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
var mockDir = pkg.SrcDir + "/mock" |
||||||
|
if _, err = os.Stat(mockDir); os.IsNotExist(err) { |
||||||
|
err = nil |
||||||
|
os.Mkdir(mockDir, 0744) |
||||||
|
} |
||||||
|
var mockPath = mockDir + "/" + pkg.Name + "_mock.go" |
||||||
|
if _, exist := os.Stat(mockPath); os.IsExist(exist) { |
||||||
|
continue |
||||||
|
} |
||||||
|
var g = &mockgen.Generator{Filename: file} |
||||||
|
if err = g.Generate(pkg, "mock", mockPath); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if err = ioutil.WriteFile(mockPath, g.Output(), 0644); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func genMonkey(parses []*parse) (err error) { |
||||||
|
var ( |
||||||
|
pkg = make(map[string]string) |
||||||
|
) |
||||||
|
for _, parse := range parses { |
||||||
|
if strings.Contains(parse.Path, "monkey.go") || |
||||||
|
strings.Contains(parse.Path, "/mock/") { |
||||||
|
continue |
||||||
|
} |
||||||
|
var ( |
||||||
|
path = strings.Split(filepath.Dir(parse.Path), "/") |
||||||
|
pack = ConvertHump(path[len(path)-1]) |
||||||
|
refer = path[len(path)-1] |
||||||
|
mockVar, mockType, srcDir string |
||||||
|
) |
||||||
|
for i := len(path) - 1; i > len(path)-4; i-- { |
||||||
|
if path[i] == "dao" || path[i] == "service" { |
||||||
|
srcDir = strings.Join(path[:i+1], "/") |
||||||
|
break |
||||||
|
} |
||||||
|
pack = ConvertHump(path[i-1]) + pack |
||||||
|
} |
||||||
|
if mockVar = ConvertMethod(parse.Path); mockType == "d" { |
||||||
|
mockType = "*" + refer + ".Dao" |
||||||
|
} else { |
||||||
|
mockType = "*" + refer + ".Service" |
||||||
|
} |
||||||
|
for _, parseFunc := range parse.Funcs { |
||||||
|
if (parseFunc.Method == nil) || (parseFunc.Result == nil) || |
||||||
|
!(parseFunc.Name[0] >= 'A' && parseFunc.Name[0] <= 'Z') { |
||||||
|
continue |
||||||
|
} |
||||||
|
var ( |
||||||
|
funcParams, funcResults, mockKey, mockValue, funcName string |
||||||
|
) |
||||||
|
funcName = pack + parseFunc.Name |
||||||
|
for k, param := range parseFunc.Params { |
||||||
|
funcParams += "_ " + param.V |
||||||
|
if len(parseFunc.Params) > k+1 { |
||||||
|
funcParams += ", " |
||||||
|
} |
||||||
|
} |
||||||
|
for k, res := range parseFunc.Result { |
||||||
|
if res.K == "" { |
||||||
|
if res.V == "error" { |
||||||
|
res.K = "err" |
||||||
|
} else { |
||||||
|
res.K = fmt.Sprintf("p%d", k+1) |
||||||
|
} |
||||||
|
} |
||||||
|
mockKey += res.K |
||||||
|
mockValue += res.V |
||||||
|
funcResults += res.K + " " + res.P + res.V |
||||||
|
if len(parseFunc.Result) > k+1 { |
||||||
|
mockKey += ", " |
||||||
|
mockValue += ", " |
||||||
|
funcResults += ", " |
||||||
|
} |
||||||
|
} |
||||||
|
pkg[srcDir+"."+refer] += fmt.Sprintf(tpMonkeyFunc, funcName, funcName, mockVar, mockType, funcResults, mockVar, parseFunc.Name, mockType, funcParams, mockValue, mockKey) |
||||||
|
} |
||||||
|
} |
||||||
|
for path, content := range pkg { |
||||||
|
var ( |
||||||
|
buffer bytes.Buffer |
||||||
|
dir = strings.Split(path, ".") |
||||||
|
mockDir = dir[0] + "/mock" |
||||||
|
filename = mockDir + "/monkey_" + dir[1] + ".go" |
||||||
|
) |
||||||
|
if _, err = os.Stat(mockDir); os.IsNotExist(err) { |
||||||
|
err = nil |
||||||
|
os.Mkdir(mockDir, 0744) |
||||||
|
} |
||||||
|
if _, err := os.Stat(filename); os.IsExist(err) { |
||||||
|
continue |
||||||
|
} |
||||||
|
buffer.WriteString(fmt.Sprintf(tpPackage, "mock")) |
||||||
|
buffer.WriteString(content) |
||||||
|
content, _ := GoImport(filename, buffer.Bytes()) |
||||||
|
ioutil.WriteFile(filename, content, 0644) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"flag" |
||||||
|
"fmt" |
||||||
|
"os" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
err error |
||||||
|
_mode, _func string |
||||||
|
files []string |
||||||
|
parses []*parse |
||||||
|
) |
||||||
|
|
||||||
|
func main() { |
||||||
|
flag.StringVar(&_mode, "m", "test", "Generating code by Working mode. [test|interface|mock...]") |
||||||
|
flag.StringVar(&_func, "func", "", "Generating code by function.") |
||||||
|
flag.Parse() |
||||||
|
if len(os.Args) == 1 { |
||||||
|
println("Creater is a tool for generating code.\n\nUsage: creater [-m]") |
||||||
|
flag.PrintDefaults() |
||||||
|
return |
||||||
|
} |
||||||
|
if err = parseArgs(os.Args[1:], &files, 0); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
switch _mode { |
||||||
|
case "monkey": |
||||||
|
if parses, err = parseFile(files...); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if err = genMonkey(parses); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
case "test": |
||||||
|
if parses, err = parseFile(files...); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if err = genTest(parses); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
case "interface": |
||||||
|
if parses, err = parseFile(files...); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
if err = genInterface(parses); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
case "mock": |
||||||
|
if err = genMock(files...); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
default: |
||||||
|
} |
||||||
|
fmt.Println(print) |
||||||
|
} |
@ -0,0 +1,193 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"go/ast" |
||||||
|
"go/parser" |
||||||
|
"go/token" |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
type param struct{ K, V, P string } |
||||||
|
|
||||||
|
type parse struct { |
||||||
|
Path string |
||||||
|
Package string |
||||||
|
// Imports []string
|
||||||
|
Imports map[string]*param |
||||||
|
// Structs []*param
|
||||||
|
// Interfaces []string
|
||||||
|
Funcs []*struct { |
||||||
|
Name string |
||||||
|
Method, Params, Result []*param |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func parseArgs(args []string, res *[]string, index int) (err error) { |
||||||
|
if len(args) <= index { |
||||||
|
return |
||||||
|
} |
||||||
|
if strings.HasPrefix(args[index], "-") { |
||||||
|
index += 2 |
||||||
|
parseArgs(args, res, index) |
||||||
|
return |
||||||
|
} |
||||||
|
var f os.FileInfo |
||||||
|
if f, err = os.Stat(args[index]); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if f.IsDir() { |
||||||
|
if !strings.HasSuffix(args[index], "/") { |
||||||
|
args[index] += "/" |
||||||
|
} |
||||||
|
var fs []os.FileInfo |
||||||
|
if fs, err = ioutil.ReadDir(args[index]); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
for _, f = range fs { |
||||||
|
path, _ := filepath.Abs(args[index] + f.Name()) |
||||||
|
args = append(args, path) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if strings.HasSuffix(args[index], ".go") && |
||||||
|
!strings.HasSuffix(args[index], "_test.go") { |
||||||
|
*res = append(*res, args[index]) |
||||||
|
} |
||||||
|
} |
||||||
|
index++ |
||||||
|
return parseArgs(args, res, index) |
||||||
|
} |
||||||
|
|
||||||
|
func parseFile(files ...string) (parses []*parse, err error) { |
||||||
|
for _, file := range files { |
||||||
|
var ( |
||||||
|
astFile *ast.File |
||||||
|
fSet = token.NewFileSet() |
||||||
|
parse = &parse{ |
||||||
|
Imports: make(map[string]*param), |
||||||
|
} |
||||||
|
) |
||||||
|
if astFile, err = parser.ParseFile(fSet, file, nil, 0); err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
if astFile.Name != nil { |
||||||
|
parse.Path = file |
||||||
|
parse.Package = astFile.Name.Name |
||||||
|
} |
||||||
|
for _, decl := range astFile.Decls { |
||||||
|
switch decl.(type) { |
||||||
|
case *ast.GenDecl: |
||||||
|
if specs := decl.(*ast.GenDecl).Specs; len(specs) > 0 { |
||||||
|
parse.Imports = parseImports(specs) |
||||||
|
} |
||||||
|
case *ast.FuncDecl: |
||||||
|
var ( |
||||||
|
dec = decl.(*ast.FuncDecl) |
||||||
|
parseFunc = &struct { |
||||||
|
Name string |
||||||
|
Method, Params, Result []*param |
||||||
|
}{Name: dec.Name.Name} |
||||||
|
) |
||||||
|
if dec.Recv != nil { |
||||||
|
parseFunc.Method = parserParams(dec.Recv.List) |
||||||
|
} |
||||||
|
if dec.Type.Params != nil { |
||||||
|
parseFunc.Params = parserParams(dec.Type.Params.List) |
||||||
|
} |
||||||
|
if dec.Type.Results != nil { |
||||||
|
parseFunc.Result = parserParams(dec.Type.Results.List) |
||||||
|
} |
||||||
|
parse.Funcs = append(parse.Funcs, parseFunc) |
||||||
|
} |
||||||
|
} |
||||||
|
parses = append(parses, parse) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func parserParams(fields []*ast.Field) (params []*param) { |
||||||
|
for _, field := range fields { |
||||||
|
p := ¶m{} |
||||||
|
p.V = parseType(field.Type) |
||||||
|
if field.Names == nil { |
||||||
|
params = append(params, p) |
||||||
|
} |
||||||
|
for _, name := range field.Names { |
||||||
|
sp := ¶m{} |
||||||
|
sp.K = name.Name |
||||||
|
sp.V = p.V |
||||||
|
sp.P = p.P |
||||||
|
params = append(params, sp) |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func parseType(expr ast.Expr) string { |
||||||
|
switch expr.(type) { |
||||||
|
case *ast.Ident: |
||||||
|
return expr.(*ast.Ident).Name |
||||||
|
case *ast.StarExpr: |
||||||
|
return "*" + parseType(expr.(*ast.StarExpr).X) |
||||||
|
case *ast.ArrayType: |
||||||
|
return "[" + parseType(expr.(*ast.ArrayType).Len) + "]" + parseType(expr.(*ast.ArrayType).Elt) |
||||||
|
case *ast.SelectorExpr: |
||||||
|
return parseType(expr.(*ast.SelectorExpr).X) + "." + expr.(*ast.SelectorExpr).Sel.Name |
||||||
|
case *ast.MapType: |
||||||
|
return "map[" + parseType(expr.(*ast.MapType).Key) + "]" + parseType(expr.(*ast.MapType).Value) |
||||||
|
case *ast.StructType: |
||||||
|
return "struct{}" |
||||||
|
case *ast.InterfaceType: |
||||||
|
return "interface{}" |
||||||
|
case *ast.FuncType: |
||||||
|
var ( |
||||||
|
pTemp string |
||||||
|
rTemp string |
||||||
|
) |
||||||
|
pTemp = parseFuncType(pTemp, expr.(*ast.FuncType).Params) |
||||||
|
if expr.(*ast.FuncType).Results != nil { |
||||||
|
rTemp = parseFuncType(rTemp, expr.(*ast.FuncType).Results) |
||||||
|
return fmt.Sprintf("func(%s) (%s)", pTemp, rTemp) |
||||||
|
} |
||||||
|
return fmt.Sprintf("func(%s)", pTemp) |
||||||
|
case *ast.ChanType: |
||||||
|
return fmt.Sprintf("make(chan %s)", parseType(expr.(*ast.ChanType).Value)) |
||||||
|
case *ast.Ellipsis: |
||||||
|
return parseType(expr.(*ast.Ellipsis).Elt) |
||||||
|
} |
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
func parseFuncType(temp string, data *ast.FieldList) string { |
||||||
|
var params = parserParams(data.List) |
||||||
|
for i, param := range params { |
||||||
|
if i == 0 { |
||||||
|
temp = param.K + " " + param.V |
||||||
|
continue |
||||||
|
} |
||||||
|
t := param.K + " " + param.V |
||||||
|
temp = fmt.Sprintf("%s, %s", temp, t) |
||||||
|
} |
||||||
|
return temp |
||||||
|
} |
||||||
|
|
||||||
|
func parseImports(specs []ast.Spec) (params map[string]*param) { |
||||||
|
params = make(map[string]*param) |
||||||
|
for _, spec := range specs { |
||||||
|
switch spec.(type) { |
||||||
|
case *ast.ImportSpec: |
||||||
|
p := ¶m{V: strings.Replace(spec.(*ast.ImportSpec).Path.Value, "\"", "", -1)} |
||||||
|
if spec.(*ast.ImportSpec).Name != nil { |
||||||
|
p.K = spec.(*ast.ImportSpec).Name.Name |
||||||
|
params[p.K] = p |
||||||
|
} else { |
||||||
|
vs := strings.Split(p.V, "/") |
||||||
|
params[vs[len(vs)-1]] = p |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
var ( |
||||||
|
tpPackage = "package %s\n\n" |
||||||
|
tpImport = "import (\n\t%s\n)\n\n" |
||||||
|
tpVar = "var (\n\t%s\n)\n" |
||||||
|
tpInterface = "type %sInterface interface {\n%s}\n" |
||||||
|
tpIntfcFunc = "%s(%s) %s\n" |
||||||
|
tpMonkeyFunc = "// Mock%s .\nfunc Mock%s(%s %s,%s) (guard *monkey.PatchGuard) {\n\treturn monkey.PatchInstanceMethod(reflect.TypeOf(%s), \"%s\", func(_ %s, %s) (%s) {\n\t\treturn %s\n\t})\n}\n\n" |
||||||
|
tpTestReset = "\n\t\tReset(func() {%s\n\t\t})" |
||||||
|
tpTestFunc = "func Test%s%s(t *testing.T){%s\n\tConvey(\"%s\", t, func(){\n\t\t%s\tConvey(\"When everything goes positive\", func(){\n\t\t\t%s\n\t\t\t})\n\t\t})%s\n\t})\n}\n\n" |
||||||
|
tpTestDaoMain = `func TestMain(m *testing.M) { |
||||||
|
flag.Set("conf", "%s") |
||||||
|
flag.Parse() |
||||||
|
%s |
||||||
|
os.Exit(m.Run()) |
||||||
|
} |
||||||
|
` |
||||||
|
tpTestServiceMain = `func TestMain(m *testing.M){ |
||||||
|
flag.Set("conf", "%s") |
||||||
|
flag.Parse() |
||||||
|
%s |
||||||
|
os.Exit(m.Run()) |
||||||
|
} |
||||||
|
` |
||||||
|
tpTestMainNew = `if err := paladin.Init(); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
%s` |
||||||
|
tpTestMainOld = `if err := conf.Init(); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
%s` |
||||||
|
print = `Generation success!
|
||||||
|
莫生气 |
||||||
|
代码辣鸡非我意, |
||||||
|
自己动手分田地; |
||||||
|
你若气死谁如意? |
||||||
|
谈笑风生活长命. |
||||||
|
// Release 1.2.3. Powered by Kratos`
|
||||||
|
) |
@ -0,0 +1,42 @@ |
|||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"golang.org/x/tools/imports" |
||||||
|
) |
||||||
|
|
||||||
|
// GoImport Use golang.org/x/tools/imports auto import pkg
|
||||||
|
func GoImport(file string, bytes []byte) (res []byte, err error) { |
||||||
|
options := &imports.Options{ |
||||||
|
TabWidth: 8, |
||||||
|
TabIndent: true, |
||||||
|
Comments: true, |
||||||
|
Fragment: true, |
||||||
|
} |
||||||
|
if res, err = imports.Process(file, bytes, options); err != nil { |
||||||
|
fmt.Printf("GoImport(%s) error(%v)", file, err) |
||||||
|
res = bytes |
||||||
|
return |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
// ConvertMethod checkout the file belongs to dao or not
|
||||||
|
func ConvertMethod(path string) (method string) { |
||||||
|
switch { |
||||||
|
case strings.Contains(path, "/dao"): |
||||||
|
method = "d" |
||||||
|
case strings.Contains(path, "/service"): |
||||||
|
method = "s" |
||||||
|
default: |
||||||
|
method = "" |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
//ConvertHump convert words to hump style
|
||||||
|
func ConvertHump(words string) string { |
||||||
|
return strings.ToUpper(words[0:1]) + words[1:] |
||||||
|
} |
Loading…
Reference in new issue