package game // Owner-boundary concurrency regressions. These exercise the competing // runtime entry points the way production does (connection input, ticks, // disconnects, admin mutations, owner-routed wallet operations) and must // pass under -race with the single-owner design in place. import ( "sync" "testing" "thehouseoficarus/internal/net" ) // TestBaselineRaceSessionInputTickDisconnect exercises the three competing // runtime entry points against each other: connection-goroutine input, the // ticker goroutine, and hub disconnect callbacks. func TestBaselineRaceSessionInputTickDisconnect(t *testing.T) { h := newHarness(t) h.seedAccount("alice") h.seedAccount("bob") h.seedCharacter("alice", "alice") h.seedCharacter("bob", "bob") aSess, _ := h.connect("alice") bSess, _ := h.connect("bob") aLines := h.goInput(aSess) bLines := h.goInput(bSess) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() for i := 0; i < 200; i++ { aLines <- "look" } close(aLines) }() go func() { defer wg.Done() for i := 0; i < 200; i++ { bLines <- "say hi" } close(bLines) }() // Disconnect bob from a third goroutine while everything else runs. go func() { for i := 0; i < 50; i++ { h.hub.PlayersInRoom(1) if i == 25 { h.hub.Remove(bSess) } } }() wg.Wait() } // TestBaselineRaceAdminRewriteWhilePlaying hammers Game.RewriteRoomIDs (the // admin entry point) while players keep moving and ticking. func TestBaselineRaceAdminRewriteWhilePlaying(t *testing.T) { h := newHarness(t) h.seedAccount("carol") h.seedCharacter("carol", "carol") sess, _ := h.connect("carol") lines := h.goInput(sess) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for i := 0; i < 200; i++ { lines <- "go north" } close(lines) }() // Concurrent admin room-ID rewrites targeting the player's live state. for i := 0; i < 20; i++ { h.g.RewriteRoomIDs(map[int]int{2: 2 + i + 1, 2 + i: 2 + i + 1}) } wg.Wait() } // TestBaselineRaceCasinoWallet drives concurrent casino activity through // the owner boundary (as production does): wallet debits/credits from // multiple players' session tasks plus session input, all on one owner. func TestBaselineRaceCasinoWallet(t *testing.T) { h := newHarness(t) for _, n := range []string{"dave", "erin", "frank"} { h.seedAccount(n) p := h.seedCharacter(n, n) p.Credits = 100000 } sessions := make([]*net.Session, 0, 3) for _, n := range []string{"dave", "erin", "frank"} { s, _ := h.connect(n) sessions = append(sessions, s) } var wg sync.WaitGroup for _, s := range sessions { wallet := casinoWallet{g: h.g, p: s.Player} wg.Add(1) go func() { defer wg.Done() for i := 0; i < 100; i++ { h.g.RunOnOwner(func() { wallet.Wager(1) wallet.PayCredits(1) }) } }() // Session input competes with the wallet operations. lines := h.goInput(s) wg.Add(1) go func() { defer wg.Done() for i := 0; i < 100; i++ { lines <- "look" } close(lines) }() } wg.Wait() h.barrier() }