| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "regexp" |
| "runtime" |
| "runtime/pprof" |
| "strconv" |
| "testing" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/options" |
| "v.io/x/ref/examples/rps" |
| "v.io/x/ref/lib/xrpc" |
| "v.io/x/ref/services/mounttable/mounttablelib" |
| "v.io/x/ref/test" |
| "v.io/x/ref/test/modules" |
| ) |
| |
| //go:generate v23 test generate |
| |
| var rootMT = modules.Register(func(env *modules.Env, args ...string) error { |
| ctx, shutdown := test.V23Init() |
| defer shutdown() |
| |
| mt, err := mounttablelib.NewMountTableDispatcher(ctx, "", "", "mounttable") |
| if err != nil { |
| return fmt.Errorf("mounttablelib.NewMountTableDispatcher failed: %s", err) |
| } |
| server, err := xrpc.NewDispatchingServer(ctx, "", mt, options.ServesMountTable(true)) |
| if err != nil { |
| return fmt.Errorf("root failed: %v", err) |
| } |
| fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid()) |
| for _, ep := range server.Status().Endpoints { |
| fmt.Fprintf(env.Stdout, "MT_NAME=%s\n", ep.Name()) |
| } |
| modules.WaitForEOF(env.Stdin) |
| return nil |
| }, "rootMT") |
| |
| func startRockPaperScissors(t *testing.T, ctx *context.T, mtAddress string) (*RPS, func()) { |
| ns := v23.GetNamespace(ctx) |
| ns.SetRoots(mtAddress) |
| rpsService := NewRPS(ctx) |
| names := []string{"rps/judge/test", "rps/player/test", "rps/scorekeeper/test"} |
| server, err := xrpc.NewServer(ctx, names[0], rps.RockPaperScissorsServer(rpsService), nil) |
| if err != nil { |
| t.Fatalf("NewServer failed: %v", err) |
| } |
| for _, n := range names[1:] { |
| server.AddName(n) |
| } |
| return rpsService, func() { |
| if err := server.Stop(); err != nil { |
| t.Fatalf("Stop() failed: %v", err) |
| } |
| } |
| } |
| |
| // TestRockPaperScissorsImpl runs one rock-paper-scissors game and verifies |
| // that all the counters are consistent. |
| func TestRockPaperScissorsImpl(t *testing.T) { |
| ctx, shutdown := test.V23Init() |
| defer shutdown() |
| |
| sh, err := modules.NewShell(ctx, nil, testing.Verbose(), t) |
| if err != nil { |
| t.Fatalf("Could not create shell: %v", err) |
| } |
| defer sh.Cleanup(os.Stdout, os.Stderr) |
| h, err := sh.Start(nil, rootMT, "--v23.tcp.address=127.0.0.1:0") |
| if err != nil { |
| if h != nil { |
| h.Shutdown(nil, os.Stderr) |
| } |
| t.Fatalf("unexpected error for root mt: %s", err) |
| } |
| h.ExpectVar("PID") |
| mtAddress := h.ExpectVar("MT_NAME") |
| |
| rpsService, rpsStop := startRockPaperScissors(t, ctx, mtAddress) |
| defer rpsStop() |
| |
| const numGames = 100 |
| for x := 0; x < numGames; x++ { |
| if err := rpsService.Player().InitiateGame(ctx); err != nil { |
| t.Errorf("Failed to initiate game: %v", err) |
| } |
| } |
| rpsService.Player().WaitUntilIdle() |
| |
| // For each game, the player plays twice. So, we expect the player to |
| // show that it played 2×numGames, and won numGames. |
| played, won := rpsService.Player().Stats() |
| if want, got := int64(2*numGames), played; want != got { |
| t.Errorf("Unexpected number of played games. Got %d, want %d", got, want) |
| } |
| if want, got := int64(numGames), won; want != got { |
| t.Errorf("Unexpected number of won games. Got %d, want %d", got, want) |
| } |
| |
| // The Judge ran every game. |
| if want, got := int64(numGames), rpsService.Judge().Stats(); want != got { |
| t.Errorf("Unexpected number of games run. Got %d, want %d", got, want) |
| } |
| |
| // The Score Keeper received one score card per game. |
| if want, got := int64(numGames), rpsService.ScoreKeeper().Stats(); want != got { |
| t.Errorf("Unexpected number of score cards. Got %d, want %d", got, want) |
| } |
| |
| // Check for leaked goroutines. |
| if n := runtime.NumGoroutine(); n > 100 { |
| t.Logf("Num goroutines: %d", n) |
| if leak := reportLeakedGoroutines(t, n/3); len(leak) != 0 { |
| t.Errorf("Potentially leaked goroutines:\n%s", leak) |
| } |
| } |
| } |
| |
| // reportLeakedGoroutines reads the goroutine pprof profile and returns the |
| // goroutines that have more than 'threshold' instances. |
| func reportLeakedGoroutines(t *testing.T, threshold int) string { |
| f, err := ioutil.TempFile("", "test-profile-") |
| if err != nil { |
| t.Fatalf("Failed to create temp file: %v", err) |
| } |
| defer f.Close() |
| defer os.Remove(f.Name()) |
| p := pprof.Lookup("goroutine") |
| if p == nil { |
| t.Fatalf("Failed to lookup the goroutine profile") |
| } |
| if err := p.WriteTo(f, 1); err != nil { |
| t.Fatalf("Failed to write profile: %v", err) |
| } |
| |
| re, err := regexp.Compile(`^([0-9]+) @`) |
| if err != nil { |
| t.Fatalf("Failed to compile regexp") |
| } |
| f.Seek(0, 0) |
| r := bufio.NewReader(f) |
| |
| var buf bytes.Buffer |
| var printing bool |
| for { |
| line, err := r.ReadBytes('\n') |
| if err == io.EOF { |
| break |
| } |
| if err != nil { |
| t.Fatalf("Failed to read bytes: %v", err) |
| } |
| if len(line) <= 1 { |
| printing = false |
| continue |
| } |
| if !printing { |
| if m := re.FindSubmatch(line); len(m) == 2 { |
| count, _ := strconv.Atoi(string(m[1])) |
| if count > threshold { |
| printing = true |
| } |
| } |
| } |
| if printing { |
| buf.Write(line) |
| } |
| } |
| return buf.String() |
| } |