aboutsummaryrefslogtreecommitdiff
path: root/internal/game/baseline_race_test.go
blob: 4efc907187d597efab203a6998a588690a4d695c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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()
}