package oltp
import (
"sync"
"testing"
)
// race runs fn on n goroutines released together, and returns how
// many returned nil — the count a check-then-write race inflates.
func race(n int, fn func(i int) error) (ok int, errs []error) {
var start, done sync.WaitGroup
start.Add(1)
results := make([]error, n)
for i := 0; i < n; i++ {
done.Add(1)
go func(i int) {
defer done.Done()
start.Wait()
results[i] = fn(i)
}(i)
}
start.Done()
done.Wait()
for _, err := range results {
if err == nil {
ok++
} else {
errs = append(errs, err)
}
}
return ok, errs
}
func TestTwoRedemptionsOfOneInviteAtOnceLetExactlyOneThrough(t *testing.T) {
db := testDB(t)
token, err := db.InviteAccount("eve", "[email protected]", false)
if err != nil {
t.Fatal(err)
}
passwords := []string{"first-password-1", "second-password-2"}
ok, errs := race(len(passwords), func(i int) error {
_, err := db.RedeemInvite(token, passwords[i])
return err
})
if ok != 1 {
t.Fatalf("FAIL: %d of %d concurrent redemptions of one invite succeeded (errors %v)", ok, len(passwords), errs)
}
t.Logf("ok: one redemption won, the other was refused: %v", errs)
signIns := 0
for _, p := range passwords {
if _, ok := db.Authenticate("eve", p); ok {
signIns++
}
}
if signIns != 1 {
t.Fatalf("FAIL: %d passwords sign eve in after the race, want the winner's only", signIns)
}
t.Logf("ok: exactly the winning password signs in")
}
// rounds repeats a race whose window is a few statements wide: one
// round rarely lands both goroutines inside it, hundreds nearly always
// do.
const rounds = 300
func TestTwoAdminsDemotingEachOtherAtOnceLeaveTheRepositoryAnAdmin(t *testing.T) {
db := testDB(t)
for _, n := range []string{"alice", "bob"} {
if err := db.AddAccount(n, "pw12345678", false); err != nil {
t.Fatal(err)
}
}
// Each demotes the other: alone, each save leaves one admin and
// the guard passes it.
pairs := [][2]string{{"alice", "bob"}, {"bob", "alice"}}
for round := 0; round < rounds; round++ {
for _, n := range []string{"alice", "bob"} {
if err := db.Grant(n, "repo", RepoAdmin); err != nil {
t.Fatal(err)
}
}
ok, errs := race(len(pairs), func(i int) error {
_, err := db.SetCapsGuarded(pairs[i][0], pairs[i][1], "repo", Write.Caps())
return err
})
if ok != 1 {
t.Fatalf("FAIL: round %d: %d of 2 crossing demotions succeeded (errors %v)", round, ok, errs)
}
if !IsRefusal(errs[0]) {
t.Fatalf("FAIL: round %d: the losing demotion failed with %v, want the guard's refusal", round, errs[0])
}
if before, _, err := db.adminCount("repo", "", ""); err != nil || before != 1 {
t.Fatalf("FAIL: round %d: the repository has %d admins after the race (%v), want 1", round, before, err)
}
}
t.Logf("ok: in %d rounds one demotion went through, the other was refused, and one admin remained", rounds)
}
func TestTwoOwnersRemovingEachOtherAtOnceLeaveTheTeamAnOwner(t *testing.T) {
for _, tc := range []struct {
name string
apply func(db *DB, actor, account string) error
}{
{"removal", func(db *DB, actor, account string) error {
_, err := db.RemoveTeamMemberGuarded(actor, "crew", account)
return err
}},
{"demotion", func(db *DB, actor, account string) error {
_, err := db.SetTeamMemberGuarded(actor, "crew", account, "", false, true)
return err
}},
} {
t.Run(tc.name, func(t *testing.T) {
db := testDB(t)
for _, n := range []string{"alice", "bob"} {
if err := db.AddAccount(n, "pw12345678", false); err != nil {
t.Fatal(err)
}
}
if err := db.AddTeam("crew", "Crew", "", "alice"); err != nil {
t.Fatal(err)
}
pairs := [][2]string{{"alice", "bob"}, {"bob", "alice"}}
for round := 0; round < rounds; round++ {
for _, n := range []string{"alice", "bob"} {
if err := db.SetTeamMember("crew", n, "", true, true); err != nil {
t.Fatal(err)
}
}
ok, errs := race(len(pairs), func(i int) error {
return tc.apply(db, pairs[i][0], pairs[i][1])
})
if ok != 1 {
t.Fatalf("FAIL: round %d: %d of 2 crossing %ss succeeded (errors %v)", round, ok, tc.name, errs)
}
if !IsRefusal(errs[0]) {
t.Fatalf("FAIL: round %d: the losing %s failed with %v, want the guard's refusal", round, tc.name, errs[0])
}
if n, err := db.TeamOwnerCount("crew"); err != nil || n != 1 {
t.Fatalf("FAIL: round %d: the team has %d owners after the race (%v), want 1", round, n, err)
}
}
t.Logf("ok: in %d rounds one %s went through, the other was refused, and one owner remained", rounds, tc.name)
})
}
}