feat: 实现 test/harness 集成测试启动器
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package harness
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Options 控制测试进程启动。
|
||||
type Options struct {
|
||||
// Listen 写入配置的 listen,默认 127.0.0.1:0。
|
||||
Listen string
|
||||
// AdminListen 可选;非空时写入 admin_listen,并读取 admin.addr。
|
||||
AdminListen string
|
||||
// AdminInit 可选;nil 时用 CLIAdminInit。不支持时忽略并继续启动。
|
||||
AdminInit AdminInitializer
|
||||
// KeepDir 为 true 时 Stop 不删除数据目录(命令行启动器在清理前可读)。
|
||||
KeepDir bool
|
||||
}
|
||||
|
||||
// Server 是一次集成测试用的真实 nixmsg 进程。
|
||||
type Server struct {
|
||||
BinPath string
|
||||
ConfigPath string
|
||||
DataDir string
|
||||
Addr string // 来自 listen.addr,形如 127.0.0.1:12345
|
||||
AdminAddr string // 来自 admin.addr(若有)
|
||||
AdminPassword string
|
||||
HTTPBase string
|
||||
AdminHTTPBase string
|
||||
|
||||
cmd *exec.Cmd
|
||||
keepDir bool
|
||||
stopped bool
|
||||
}
|
||||
|
||||
// Start 编译(如需)并启动服务:临时目录、listen 端口 0、读 listen.addr。
|
||||
func Start(opts Options) (*Server, error) {
|
||||
bin, err := Binary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dataDir, err := os.MkdirTemp("", "nixmsg-harness-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listen := opts.Listen
|
||||
if listen == "" {
|
||||
listen = "127.0.0.1:0"
|
||||
}
|
||||
cfgPath := filepath.Join(dataDir, "config.yaml")
|
||||
cfg := fmt.Sprintf("listen: %q\ndata_dir: %q\n", listen, filepath.ToSlash(dataDir))
|
||||
if opts.AdminListen != "" {
|
||||
cfg += fmt.Sprintf("admin_listen: %q\n", opts.AdminListen)
|
||||
}
|
||||
if err = os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil {
|
||||
_ = os.RemoveAll(dataDir)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
adminInit := opts.AdminInit
|
||||
if adminInit == nil {
|
||||
adminInit = CLIAdminInit{}
|
||||
}
|
||||
password, initErr := adminInit.Init(bin, cfgPath)
|
||||
if initErr != nil && !errors.Is(initErr, ErrAdminInitUnsupported) {
|
||||
_ = os.RemoveAll(dataDir)
|
||||
return nil, initErr
|
||||
}
|
||||
if errors.Is(initErr, ErrAdminInitUnsupported) {
|
||||
password = ""
|
||||
}
|
||||
|
||||
cmd := exec.Command(bin, "serve")
|
||||
cmd.Env = append(os.Environ(), "NIXMSG_CONFIG="+cfgPath)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
if err = cmd.Start(); err != nil {
|
||||
_ = os.RemoveAll(dataDir)
|
||||
return nil, fmt.Errorf("start serve: %w", err)
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
BinPath: bin,
|
||||
ConfigPath: cfgPath,
|
||||
DataDir: dataDir,
|
||||
AdminPassword: password,
|
||||
cmd: cmd,
|
||||
keepDir: opts.KeepDir,
|
||||
}
|
||||
|
||||
addr, err := waitAddrFile(filepath.Join(dataDir, "listen.addr"), 10*time.Second)
|
||||
if err != nil {
|
||||
_ = s.Stop()
|
||||
return nil, fmt.Errorf("wait listen.addr: %w", err)
|
||||
}
|
||||
s.Addr = addr
|
||||
s.HTTPBase = "http://" + addr
|
||||
|
||||
if opts.AdminListen != "" {
|
||||
adminAddr, adminErr := waitAddrFile(filepath.Join(dataDir, "admin.addr"), 10*time.Second)
|
||||
if adminErr == nil {
|
||||
s.AdminAddr = adminAddr
|
||||
s.AdminHTTPBase = "http://" + adminAddr
|
||||
}
|
||||
}
|
||||
if s.AdminHTTPBase == "" {
|
||||
s.AdminHTTPBase = s.HTTPBase
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// AdminClient 返回指向管理接口的 HTTP 客户端。
|
||||
func (s *Server) AdminClient() (*AdminClient, error) {
|
||||
base := s.AdminHTTPBase
|
||||
if base == "" {
|
||||
base = s.HTTPBase
|
||||
}
|
||||
return NewAdminClient(base)
|
||||
}
|
||||
|
||||
// Stop 结束进程并删除临时目录(除非 KeepDir)。
|
||||
func (s *Server) Stop() error {
|
||||
if s == nil || s.stopped {
|
||||
return nil
|
||||
}
|
||||
s.stopped = true
|
||||
var stopErr error
|
||||
if s.cmd != nil && s.cmd.Process != nil {
|
||||
_ = s.cmd.Process.Kill()
|
||||
_, _ = s.cmd.Process.Wait()
|
||||
}
|
||||
if !s.keepDir && s.DataDir != "" {
|
||||
if err := os.RemoveAll(s.DataDir); err != nil {
|
||||
stopErr = err
|
||||
}
|
||||
}
|
||||
return stopErr
|
||||
}
|
||||
|
||||
func waitAddrFile(path string, timeout time.Duration) (string, error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
addr := strings.TrimSpace(string(b))
|
||||
if addr != "" {
|
||||
return addr, nil
|
||||
}
|
||||
lastErr = fmt.Errorf("empty addr file")
|
||||
} else {
|
||||
lastErr = err
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
return "", lastErr
|
||||
}
|
||||
Reference in New Issue
Block a user