package tests import ( "fmt" "math/rand" "os" "os/signal" "runtime" "strings" "sync" "syscall" "testing" "time" "github.com/gomodule/redigo/redis" "github.com/tidwall/limiter" "go.uber.org/atomic" ) const ( clear = "\x1b[0m" bright = "\x1b[1m" dim = "\x1b[2m" black = "\x1b[30m" red = "\x1b[31m" green = "\x1b[32m" yellow = "\x1b[33m" blue = "\x1b[34m" magenta = "\x1b[35m" cyan = "\x1b[36m" white = "\x1b[37m" ) func TestIntegration(t *testing.T) { mockCleanup(true) defer mockCleanup(true) ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) go func() { <-ch mockCleanup(false) os.Exit(1) }() regTestGroup("keys", subTestKeys) regTestGroup("json", subTestJSON) regTestGroup("search", subTestSearch) regTestGroup("testcmd", subTestTestCmd) regTestGroup("client", subTestClient) regTestGroup("scripts", subTestScripts) regTestGroup("fence", subTestFence) regTestGroup("info", subTestInfo) regTestGroup("timeouts", subTestTimeout) regTestGroup("metrics", subTestMetrics) regTestGroup("aof", subTestAOF) regTestGroup("follower", subTestFollower) runTestGroups(t) } var allGroups []*testGroup func runTestGroups(t *testing.T) { limit := runtime.NumCPU() if limit > 16 { limit = 16 } l := limiter.New(limit) // Initialize all stores as "skipped", but they'll be unset if the test is // not actually skipped. for _, g := range allGroups { for _, s := range g.subs { s.skipped.Store(true) } } for _, g := range allGroups { func(g *testGroup) { t.Run(g.name, func(t *testing.T) { for _, s := range g.subs { func(s *testGroupSub) { t.Run(s.name, func(t *testing.T) { s.skipped.Store(false) var wg sync.WaitGroup wg.Add(1) var err error go func() { l.Begin() defer func() { l.End() wg.Done() }() err = s.run() }() if false { t.Parallel() t.Run("bg", func(t *testing.T) { wg.Wait() if err != nil { t.Fatal(err) } }) } }) }(s) } }) }(g) } done := make(chan bool) go func() { defer func() { done <- true }() for { finished := true for _, g := range allGroups { skipped := true for _, s := range g.subs { if !s.skipped.Load() { skipped = false break } } if !skipped && !g.printed.Load() { fmt.Printf(bright+"Testing %s\n"+clear, g.name) g.printed.Store(true) } const frtmp = "[" + magenta + " " + clear + "] %s (running) " for _, s := range g.subs { if !s.skipped.Load() && !s.printedName.Load() { fmt.Printf(frtmp, s.name) s.printedName.Store(true) } if s.done.Load() && !s.printedResult.Load() { fmt.Printf("\r") msg := fmt.Sprintf(frtmp, s.name) fmt.Print(strings.Repeat(" ", len(msg))) fmt.Printf("\r") if s.err != nil { fmt.Printf("["+red+"fail"+clear+"] %s\n", s.name) } else { fmt.Printf("["+green+"ok"+clear+"] %s\n", s.name) } s.printedResult.Store(true) } if !s.skipped.Load() && !s.done.Load() { finished = false break } } if !finished { break } } if finished { break } time.Sleep(time.Second / 4) } }() <-done var fail bool for _, g := range allGroups { for _, s := range g.subs { if s.err != nil { t.Errorf("%s/%s/%s\n%s", t.Name(), g.name, s.name, s.err) fail = true } } } if fail { t.Fail() } } type testGroup struct { name string subs []*testGroupSub printed atomic.Bool } type testGroupSub struct { g *testGroup name string fn func(mc *mockServer) error err error skipped atomic.Bool done atomic.Bool printedName atomic.Bool printedResult atomic.Bool } func regTestGroup(name string, fn func(g *testGroup)) { g := &testGroup{name: name} allGroups = append(allGroups, g) fn(g) } func (g *testGroup) regSubTest(name string, fn func(mc *mockServer) error) { s := &testGroupSub{g: g, name: name, fn: fn} g.subs = append(g.subs, s) } func (s *testGroupSub) run() (err error) { // This all happens in a background routine. defer func() { s.err = err s.done.Store(true) }() return func() error { mc, err := mockOpenServer(MockServerOptions{ Silent: true, Metrics: true, }) if err != nil { return err } defer mc.Close() return s.fn(mc) }() } func BenchmarkAll(b *testing.B) { mockCleanup(true) defer mockCleanup(true) ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) go func() { <-ch mockCleanup(true) os.Exit(1) }() mc, err := mockOpenServer(MockServerOptions{ Silent: true, Metrics: true, }) if err != nil { b.Fatal(err) } defer mc.Close() runSubBenchmark(b, "search", mc, subBenchSearch) } func loadBenchmarkPoints(b *testing.B, mc *mockServer) (err error) { const nPoints = 200000 rand.Seed(time.Now().UnixNano()) // add a bunch of points for i := 0; i < nPoints; i++ { val := fmt.Sprintf("val:%d", i) var resp string var lat, lon, fval float64 fval = rand.Float64() lat = rand.Float64()*180 - 90 lon = rand.Float64()*360 - 180 resp, err = redis.String(mc.conn.Do("SET", "mykey", val, "FIELD", "foo", fval, "POINT", lat, lon)) if err != nil { return } if resp != "OK" { err = fmt.Errorf("expected 'OK', got '%s'", resp) return } } return } func runSubBenchmark(b *testing.B, name string, mc *mockServer, bench func(t *testing.B, mc *mockServer)) { b.Run(name, func(b *testing.B) { bench(b, mc) }) } func runBenchStep(b *testing.B, mc *mockServer, name string, step func(mc *mockServer) error) { b.Helper() b.Run(name, func(b *testing.B) { b.Helper() if err := func() error { // reset the current server mc.ResetConn() defer mc.ResetConn() // clear the database so the test is consistent if err := mc.DoBatch([][]interface{}{ {"OUTPUT", "resp"}, {"OK"}, {"FLUSHDB"}, {"OK"}, }); err != nil { return err } err := loadBenchmarkPoints(b, mc) if err != nil { return err } b.ResetTimer() for i := 0; i < b.N; i++ { if err := step(mc); err != nil { return err } } return nil }(); err != nil { b.Fatal(err) } }) }