Initial Commit
This commit is contained in:
40
go/bank-account/README.md
Normal file
40
go/bank-account/README.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Bank Account
|
||||
|
||||
Bank accounts can be accessed in different ways at the same time.
|
||||
|
||||
A bank account can be accessed in multiple ways. Clients can make
|
||||
deposits and withdrawals using the internet, mobile phones, etc. Shops
|
||||
can charge against the account.
|
||||
|
||||
Create an account that can be accessed from multiple threads/processes
|
||||
(terminology depends on your programming language).
|
||||
|
||||
It should be possible to close an account; operations against a closed
|
||||
account must fail.
|
||||
|
||||
## Instructions
|
||||
|
||||
Run the test file, and fix each of the errors in turn. When you get the
|
||||
first test to pass, go to the first pending or skipped test, and make
|
||||
that pass as well. When all of the tests are passing, feel free to
|
||||
submit.
|
||||
|
||||
Remember that passing code is just the first step. The goal is to work
|
||||
towards a solution that is as readable and expressive as you can make
|
||||
it.
|
||||
|
||||
Have fun!
|
||||
|
||||
To run the tests simply run the command `go test` in the exercise directory.
|
||||
|
||||
If the test suite contains benchmarks, you can run these with the `-bench`
|
||||
flag:
|
||||
|
||||
go test -bench .
|
||||
|
||||
For more detailed info about the Go track see the [help
|
||||
page](http://help.exercism.io/getting-started-with-go.html).
|
||||
|
||||
## Source
|
||||
|
||||
[view source]()
|
58
go/bank-account/account.go
Normal file
58
go/bank-account/account.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package account
|
||||
|
||||
import "sync"
|
||||
|
||||
// Account just represents a user's account
|
||||
type Account struct {
|
||||
sync.RWMutex
|
||||
balance int
|
||||
closed bool
|
||||
}
|
||||
|
||||
// Open returns a new account
|
||||
func Open(amt int) *Account {
|
||||
a := new(Account)
|
||||
_, ok := a.Deposit(amt)
|
||||
if ok {
|
||||
return a
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close returns the payout amount and an 'ok' flag
|
||||
func (a *Account) Close() (int, bool) {
|
||||
a.Lock()
|
||||
ret := a.balance
|
||||
if a.closed {
|
||||
a.Unlock()
|
||||
return 0, false
|
||||
}
|
||||
a.closed = true
|
||||
a.balance = 0
|
||||
a.Unlock()
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// Balance returns the current account balance
|
||||
// and an 'ok' flag
|
||||
func (a *Account) Balance() (int, bool) {
|
||||
if a.closed {
|
||||
return 0, false
|
||||
}
|
||||
return a.balance, true
|
||||
}
|
||||
|
||||
// Deposit takes an amount (can be a withdrawal)
|
||||
// and returns the new balance and an 'ok' flag
|
||||
func (a *Account) Deposit(amount int) (int, bool) {
|
||||
var ret int
|
||||
var ok bool
|
||||
a.Lock()
|
||||
if !a.closed && a.balance+amount >= 0 {
|
||||
a.balance += amount
|
||||
ret = a.balance
|
||||
ok = true
|
||||
}
|
||||
a.Unlock()
|
||||
return ret, ok
|
||||
}
|
289
go/bank-account/bank_account_test.go
Normal file
289
go/bank-account/bank_account_test.go
Normal file
@@ -0,0 +1,289 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user