exercism/go/bank-account/bank_account_test.go

290 lines
7.1 KiB
Go

// API:
//
// Open(initalDeposit int64) *Account
// (Account) Close() (payout int64, ok bool)
// (Account) Balance() (balance int64, ok bool)
// (Account) Deposit(amount uint64) (newBalance int64, ok bool)
//
// If Open is given a negative initial deposit, it must return nil.
// Deposit must handle a negative amount as a withdrawal.
// If any Account method is called on an closed account, it must not modify
// the account and must return ok = false.
package account
import (
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestSeqOpenBalanceClose(t *testing.T) {
// open account
const amt = 10
a := Open(amt)
if a == nil {
t.Fatalf("Open(%d) = nil, want non-nil *Account.", amt)
}
t.Logf("Account 'a' opened with initial balance of %d.", amt)
// verify balance after open
switch b, ok := a.Balance(); {
case !ok:
t.Fatal("a.Balance() returned !ok, want ok.")
case b != amt:
t.Fatalf("a.Balance() = %d, want %d", b, amt)
}
// close account
switch p, ok := a.Close(); {
case !ok:
t.Fatalf("a.Close() returned !ok, want ok.")
case p != amt:
t.Fatalf("a.Close() returned payout = %d, want %d.", p, amt)
}
t.Log("Account 'a' closed.")
// verify balance no longer accessible
if b, ok := a.Balance(); ok {
t.Log("Balance still available on closed account.")
t.Fatalf("a.Balance() = %d, %t. Want ok == false", b, ok)
}
}
func TestSeqOpenDepositClose(t *testing.T) {
// open account
const openAmt = 10
a := Open(openAmt)
if a == nil {
t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
}
t.Logf("Account 'a' opened with initial balance of %d.", openAmt)
// deposit
const depAmt = 20
const newAmt = openAmt + depAmt
switch b, ok := a.Deposit(depAmt); {
case !ok:
t.Fatalf("a.Deposit(%d) returned !ok, want ok.", depAmt)
case b != openAmt+depAmt:
t.Fatalf("a.Deposit(%d) = %d, want new balance = %d", depAmt, b, newAmt)
}
t.Logf("Deposit of %d accepted to account 'a'", depAmt)
// close account
switch p, ok := a.Close(); {
case !ok:
t.Fatalf("a.Close() returned !ok, want ok.")
case p != newAmt:
t.Fatalf("a.Close() returned payout = %d, want %d.", p, newAmt)
}
t.Log("Account 'a' closed.")
// verify deposits no longer accepted
if b, ok := a.Deposit(1); ok {
t.Log("Deposit accepted on closed account.")
t.Fatalf("a.Deposit(1) = %d, %t. Want ok == false", b, ok)
}
}
func TestMoreSeqCases(t *testing.T) {
// open account 'a' as before
const openAmt = 10
a := Open(openAmt)
if a == nil {
t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
}
t.Logf("Account 'a' opened with initial balance of %d.", openAmt)
// open account 'z' with zero balance
z := Open(0)
if z == nil {
t.Fatal("Open(0) = nil, want non-nil *Account.")
}
t.Log("Account 'z' opened with initial balance of 0.")
// attempt to open account with negative opening balance
if Open(-10) != nil {
t.Fatal("Open(-10) seemed to work, " +
"want nil result for negative opening balance.")
}
// verify both balances a and z still there
switch b, ok := a.Balance(); {
case !ok:
t.Fatal("a.Balance() returned !ok, want ok.")
case b != openAmt:
t.Fatalf("a.Balance() = %d, want %d", b, openAmt)
}
switch b, ok := z.Balance(); {
case !ok:
t.Fatal("z.Balance() returned !ok, want ok.")
case b != 0:
t.Fatalf("z.Balance() = %d, want 0", b)
}
// withdrawals
const wAmt = 3
const newAmt = openAmt - wAmt
switch b, ok := a.Deposit(-wAmt); {
case !ok:
t.Fatalf("a.Deposit(%d) returned !ok, want ok.", -wAmt)
case b != newAmt:
t.Fatalf("a.Deposit(%d) = %d, want new balance = %d", -wAmt, b, newAmt)
}
t.Logf("Withdrawal of %d accepted from account 'a'", wAmt)
if _, ok := z.Deposit(-1); ok {
t.Fatal("z.Deposit(-1) returned ok, want !ok.")
}
// verify both balances
switch b, ok := a.Balance(); {
case !ok:
t.Fatal("a.Balance() returned !ok, want ok.")
case b != newAmt:
t.Fatalf("a.Balance() = %d, want %d", b, newAmt)
}
switch b, ok := z.Balance(); {
case !ok:
t.Fatal("z.Balance() returned !ok, want ok.")
case b != 0:
t.Fatalf("z.Balance() = %d, want 0", b)
}
// close just z
switch p, ok := z.Close(); {
case !ok:
t.Fatalf("z.Close() returned !ok, want ok.")
case p != 0:
t.Fatalf("z.Close() returned payout = %d, want 0.", p)
}
t.Log("Account 'z' closed.")
// verify 'a' balance one more time
switch b, ok := a.Balance(); {
case !ok:
t.Fatal("a.Balance() returned !ok, want ok.")
case b != newAmt:
t.Fatalf("a.Balance() = %d, want %d", b, newAmt)
}
}
func TestConcClose(t *testing.T) {
if runtime.NumCPU() < 2 {
t.Skip("Multiple CPU cores required for concurrency tests.")
}
if runtime.GOMAXPROCS(0) < 2 {
runtime.GOMAXPROCS(2)
}
// test competing close attempts
for rep := 0; rep < 1000; rep++ {
const openAmt = 10
a := Open(openAmt)
if a == nil {
t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
}
var start sync.WaitGroup
start.Add(1)
const closeAttempts = 10
res := make(chan string)
for i := 0; i < closeAttempts; i++ {
go func() { // on your mark,
start.Wait() // get set...
switch p, ok := a.Close(); {
case !ok:
if p != 0 {
t.Errorf("a.Close() = %d, %t. "+
"Want payout = 0 for unsuccessful close", p, ok)
res <- "fail"
} else {
res <- "already closed"
}
case p != openAmt:
t.Errorf("a.Close() = %d, %t. "+
"Want payout = %d for successful close", p, ok, openAmt)
res <- "fail"
default:
res <- "close" // exactly one goroutine should reach here
}
}()
}
start.Done() // ...go
var closes, fails int
for i := 0; i < closeAttempts; i++ {
switch <-res {
case "close":
closes++
case "fail":
fails++
}
}
switch {
case fails > 0:
t.FailNow() // error already logged by other goroutine
case closes == 0:
t.Fatal("Concurrent a.Close() attempts all failed. " +
"Want one to succeed.")
case closes > 1:
t.Fatalf("%d concurrent a.Close() attempts succeeded, "+
"each paying out %d!. Want just one to succeed.",
closes, openAmt)
}
}
}
func TestConcDeposit(t *testing.T) {
if runtime.NumCPU() < 2 {
t.Skip("Multiple CPU cores required for concurrency tests.")
}
if runtime.GOMAXPROCS(0) < 2 {
runtime.GOMAXPROCS(2)
}
a := Open(0)
if a == nil {
t.Fatal("Open(0) = nil, want non-nil *Account.")
}
const amt = 10
const c = 1000
var negBal int32
var start, g sync.WaitGroup
start.Add(1)
g.Add(3 * c)
for i := 0; i < c; i++ {
go func() { // deposit
start.Wait()
a.Deposit(amt) // ignore return values
g.Done()
}()
go func() { // withdraw
start.Wait()
for {
if _, ok := a.Deposit(-amt); ok {
break
}
time.Sleep(time.Microsecond) // retry
}
g.Done()
}()
go func() { // watch that balance stays >= 0
start.Wait()
if p, _ := a.Balance(); p < 0 {
atomic.StoreInt32(&negBal, 1)
}
g.Done()
}()
}
start.Done()
g.Wait()
if negBal == 1 {
t.Fatal("Balance went negative with concurrent deposits and " +
"withdrawals. Want balance always >= 0.")
}
if p, ok := a.Balance(); !ok || p != 0 {
t.Fatalf("After equal concurrent deposits and withdrawals, "+
"a.Balance = %d, %t. Want 0, true", p, ok)
}
}