From c00da54ecddabb9e218029466bb35bca040b87fb Mon Sep 17 00:00:00 2001 From: Dimitri Sokolyuk Date: Sat, 27 Aug 2016 16:48:26 +0200 Subject: Slove bank account --- go/bank-account/README.md | 42 +++++ go/bank-account/bank_account.go | 48 ++++++ go/bank-account/bank_account_test.go | 289 +++++++++++++++++++++++++++++++++++ 3 files changed, 379 insertions(+) create mode 100644 go/bank-account/README.md create mode 100644 go/bank-account/bank_account.go create mode 100644 go/bank-account/bank_account_test.go (limited to 'go') diff --git a/go/bank-account/README.md b/go/bank-account/README.md new file mode 100644 index 0000000..b8f5d8b --- /dev/null +++ b/go/bank-account/README.md @@ -0,0 +1,42 @@ +# 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://exercism.io/languages/go). + + + +## Submitting Incomplete Problems +It's possible to submit an incomplete solution so you can see how others have completed the exercise. + diff --git a/go/bank-account/bank_account.go b/go/bank-account/bank_account.go new file mode 100644 index 0000000..bfaa894 --- /dev/null +++ b/go/bank-account/bank_account.go @@ -0,0 +1,48 @@ +package account + +import "sync" + +type Account struct { + balance int64 + open bool + sync.Mutex +} + +func Open(inital int64) *Account { + if inital < 0 { + return nil + } + return &Account{balance: inital, open: true} +} + +func (a *Account) Close() (int64, bool) { + a.Lock() + defer a.Unlock() + if a.open { + a.open = false + return a.balance, true + } + return 0, false +} + +func (a *Account) Balance() (int64, bool) { + a.Lock() + defer a.Unlock() + if a.open { + return a.balance, true + } + return 0, false +} + +func (a *Account) Deposit(amount int64) (int64, bool) { + a.Lock() + defer a.Unlock() + if a.open { + if a.balance < -amount { + return 0, false + } + a.balance += amount + return a.balance, true + } + return 0, false +} diff --git a/go/bank-account/bank_account_test.go b/go/bank-account/bank_account_test.go new file mode 100644 index 0000000..26e5b2c --- /dev/null +++ b/go/bank-account/bank_account_test.go @@ -0,0 +1,289 @@ +// API: +// +// Open(initalDeposit int64) *Account +// (Account) Close() (payout int64, ok bool) +// (Account) Balance() (balance int64, ok bool) +// (Account) Deposit(amount int64) (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) + } +} -- cgit v1.2.3