
The core package uses global variables that keep from having more than one Tile38 instance runnning in the same process. Move the core variables in the server.Options type which are uniquely stated per Server instance. The build variables are still present in the core package.
331 lines
7.0 KiB
Go
331 lines
7.0 KiB
Go
package tests
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gomodule/redigo/redis"
|
|
"github.com/tidwall/sjson"
|
|
tlog "github.com/tidwall/tile38/internal/log"
|
|
"github.com/tidwall/tile38/internal/server"
|
|
)
|
|
|
|
var errTimeout = errors.New("timeout")
|
|
|
|
func mockCleanup(silent bool) {
|
|
if !silent {
|
|
fmt.Printf("Cleanup: may take some time... ")
|
|
}
|
|
files, _ := os.ReadDir(".")
|
|
for _, file := range files {
|
|
if strings.HasPrefix(file.Name(), "data-mock-") {
|
|
os.RemoveAll(file.Name())
|
|
}
|
|
}
|
|
if !silent {
|
|
fmt.Printf("OK\n")
|
|
}
|
|
}
|
|
|
|
type mockServer struct {
|
|
port int
|
|
conn redis.Conn
|
|
ioJSON bool
|
|
dir string
|
|
// alt *mockServer
|
|
}
|
|
|
|
func (mc *mockServer) readAOF() ([]byte, error) {
|
|
return os.ReadFile(filepath.Join(mc.dir, "appendonly.aof"))
|
|
}
|
|
|
|
type MockServerOptions struct {
|
|
AOFData []byte
|
|
Silent bool
|
|
Metrics bool
|
|
}
|
|
|
|
func mockOpenServer(opts MockServerOptions) (*mockServer, error) {
|
|
rand.Seed(time.Now().UnixNano())
|
|
port := rand.Int()%20000 + 20000
|
|
dir := fmt.Sprintf("data-mock-%d", port)
|
|
if !opts.Silent {
|
|
fmt.Printf("Starting test server at port %d\n", port)
|
|
}
|
|
if len(opts.AOFData) > 0 {
|
|
if err := os.MkdirAll(dir, 0777); err != nil {
|
|
return nil, err
|
|
}
|
|
err := os.WriteFile(filepath.Join(dir, "appendonly.aof"),
|
|
opts.AOFData, 0666)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
logOutput := io.Discard
|
|
if os.Getenv("PRINTLOG") == "1" {
|
|
logOutput = os.Stderr
|
|
}
|
|
s := &mockServer{port: port}
|
|
tlog.SetOutput(logOutput)
|
|
go func() {
|
|
sopts := server.Options{
|
|
Host: "localhost",
|
|
Port: port,
|
|
Dir: dir,
|
|
UseHTTP: true,
|
|
DevMode: true,
|
|
}
|
|
if opts.Metrics {
|
|
sopts.MetricsAddr = ":4321"
|
|
}
|
|
if err := server.Serve(sopts); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
if err := s.waitForStartup(); err != nil {
|
|
s.Close()
|
|
return nil, err
|
|
}
|
|
s.dir = dir
|
|
return s, nil
|
|
}
|
|
|
|
func (s *mockServer) waitForStartup() error {
|
|
var lerr error
|
|
start := time.Now()
|
|
for {
|
|
if time.Since(start) > time.Second*5 {
|
|
if lerr != nil {
|
|
return lerr
|
|
}
|
|
return errTimeout
|
|
}
|
|
resp, err := redis.String(s.Do("SET", "please", "allow", "POINT", "33", "-115"))
|
|
if err != nil {
|
|
lerr = err
|
|
} else if resp != "OK" {
|
|
lerr = errors.New("not OK")
|
|
} else {
|
|
resp, err := redis.Int(s.Do("DEL", "please", "allow"))
|
|
if err != nil {
|
|
lerr = err
|
|
} else if resp != 1 {
|
|
lerr = errors.New("not 1")
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
time.Sleep(time.Millisecond * 100)
|
|
}
|
|
}
|
|
|
|
func (mc *mockServer) Close() {
|
|
if mc.conn != nil {
|
|
mc.conn.Close()
|
|
}
|
|
}
|
|
|
|
func (mc *mockServer) ResetConn() {
|
|
if mc.conn != nil {
|
|
mc.conn.Close()
|
|
mc.conn = nil
|
|
}
|
|
}
|
|
|
|
func (s *mockServer) DoPipeline(cmds [][]interface{}) ([]interface{}, error) {
|
|
if s.conn == nil {
|
|
var err error
|
|
s.conn, err = redis.Dial("tcp", fmt.Sprintf(":%d", s.port))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
//defer conn.Close()
|
|
for _, cmd := range cmds {
|
|
if err := s.conn.Send(cmd[0].(string), cmd[1:]...); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := s.conn.Flush(); err != nil {
|
|
return nil, err
|
|
}
|
|
var resps []interface{}
|
|
for i := 0; i < len(cmds); i++ {
|
|
resp, err := s.conn.Receive()
|
|
if err != nil {
|
|
resps = append(resps, err)
|
|
} else {
|
|
resps = append(resps, resp)
|
|
}
|
|
}
|
|
return resps, nil
|
|
}
|
|
func (s *mockServer) Do(commandName string, args ...interface{}) (interface{}, error) {
|
|
resps, err := s.DoPipeline([][]interface{}{
|
|
append([]interface{}{commandName}, args...),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(resps) != 1 {
|
|
return nil, errors.New("invalid number or responses")
|
|
}
|
|
return resps[0], nil
|
|
}
|
|
|
|
func (mc *mockServer) DoBatch(commands ...interface{}) error {
|
|
// Probe for I/O tests
|
|
if len(commands) > 0 {
|
|
if _, ok := commands[0].(*IO); ok {
|
|
var cmds []*IO
|
|
// If the first is an I/O test then all must be
|
|
for _, cmd := range commands {
|
|
if cmd, ok := cmd.(*IO); ok {
|
|
cmds = append(cmds, cmd)
|
|
} else {
|
|
return errors.New("DoBatch cannot mix I/O tests with other kinds")
|
|
}
|
|
}
|
|
for i, cmd := range cmds {
|
|
if err := mc.doIOTest(i, cmd); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var tag string
|
|
for _, commands := range commands {
|
|
switch commands := commands.(type) {
|
|
case string:
|
|
tag = commands
|
|
case [][]interface{}:
|
|
for i := 0; i < len(commands); i += 2 {
|
|
cmds := commands[i]
|
|
if dur, ok := cmds[0].(time.Duration); ok {
|
|
time.Sleep(dur)
|
|
} else {
|
|
if err := mc.DoExpect(commands[i+1], cmds[0].(string), cmds[1:]...); err != nil {
|
|
if tag == "" {
|
|
return fmt.Errorf("batch[%d]: %v", i/2, err)
|
|
} else {
|
|
return fmt.Errorf("batch[%d][%v]: %v", i/2, tag, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tag = ""
|
|
case *IO:
|
|
return errors.New("DoBatch cannot mix I/O tests with other kinds")
|
|
default:
|
|
return fmt.Errorf("Unknown command input")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalize(v interface{}) interface{} {
|
|
switch v := v.(type) {
|
|
default:
|
|
return v
|
|
case []interface{}:
|
|
for i := 0; i < len(v); i++ {
|
|
v[i] = normalize(v[i])
|
|
}
|
|
case []uint8:
|
|
return string(v)
|
|
}
|
|
return v
|
|
}
|
|
func (mc *mockServer) DoExpect(expect interface{}, commandName string, args ...interface{}) error {
|
|
if v, ok := expect.([]interface{}); ok {
|
|
expect = v[0]
|
|
}
|
|
resp, err := mc.Do(commandName, args...)
|
|
if err != nil {
|
|
if exs, ok := expect.(string); ok {
|
|
if err.Error() == exs {
|
|
return nil
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
if b, ok := resp.([]byte); ok && len(b) > 1 && b[0] == '{' {
|
|
b, err = sjson.DeleteBytes(b, "elapsed")
|
|
if err == nil {
|
|
resp = b
|
|
}
|
|
}
|
|
oresp := resp
|
|
resp = normalize(resp)
|
|
if expect == nil && resp != nil {
|
|
return fmt.Errorf("expected '%v', got '%v'", expect, resp)
|
|
}
|
|
if vv, ok := resp.([]interface{}); ok {
|
|
var ss []string
|
|
for _, v := range vv {
|
|
if v == nil {
|
|
ss = append(ss, "nil")
|
|
} else if s, ok := v.(string); ok {
|
|
ss = append(ss, s)
|
|
} else if b, ok := v.([]uint8); ok {
|
|
if b == nil {
|
|
ss = append(ss, "nil")
|
|
} else {
|
|
ss = append(ss, string(b))
|
|
}
|
|
} else {
|
|
ss = append(ss, fmt.Sprintf("%v", v))
|
|
}
|
|
}
|
|
resp = ss
|
|
}
|
|
if b, ok := resp.([]uint8); ok {
|
|
if b == nil {
|
|
resp = nil
|
|
} else {
|
|
resp = string([]byte(b))
|
|
}
|
|
}
|
|
err = func() (err error) {
|
|
defer func() {
|
|
v := recover()
|
|
if v != nil {
|
|
err = fmt.Errorf("panic '%v'", v)
|
|
}
|
|
}()
|
|
if fn, ok := expect.(func(v, org interface{}) (resp, expect interface{})); ok {
|
|
resp, expect = fn(resp, oresp)
|
|
}
|
|
if fn, ok := expect.(func(v interface{}) (resp, expect interface{})); ok {
|
|
resp, expect = fn(resp)
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fn, ok := expect.(func(string) bool); ok {
|
|
if !fn(fmt.Sprintf("%v", resp)) {
|
|
return fmt.Errorf("unexpected for response '%v'", resp)
|
|
}
|
|
} else if fn, ok := expect.(func(string) error); ok {
|
|
err := fn(fmt.Sprintf("%v", resp))
|
|
if err != nil {
|
|
return fmt.Errorf("%s, for response '%v'", err.Error(), resp)
|
|
}
|
|
} else if fmt.Sprintf("%v", resp) != fmt.Sprintf("%v", expect) {
|
|
return fmt.Errorf("expected '%v', got '%v'", expect, resp)
|
|
}
|
|
return nil
|
|
}
|