commit 1790b94b6e8ff1d72ae878ea1a01756bc310d481 Author: h7x4 Date: Fri Oct 14 12:51:19 2022 +0200 Initial commit diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..70dfc61 --- /dev/null +++ b/build.sbt @@ -0,0 +1,6 @@ +libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0" % "test" + +resolvers += "Central" at "https://central.maven.org/maven2/" + +scalacOptions := Seq("-unchecked", "-deprecation", "-feature", "-language:postfixOps") + diff --git a/src/main/scala/Account.scala b/src/main/scala/Account.scala new file mode 100644 index 0000000..542d7bb --- /dev/null +++ b/src/main/scala/Account.scala @@ -0,0 +1,21 @@ +import exceptions._ + +class Account(val bank: Bank, initialBalance: Double) { + + class Balance(var amount: Double) {} + + val balance = new Balance(initialBalance) + + // TODO + // for project task 1.2: implement functions + // for project task 1.3: change return type and update function bodies + def withdraw(amount: Double): Unit = ??? + def deposit (amount: Double): Unit = ??? + def getBalanceAmount: Double = ??? + + def transferTo(account: Account, amount: Double) = { + bank addTransactionToQueue (this, account, amount) + } + + +} diff --git a/src/main/scala/Bank.scala b/src/main/scala/Bank.scala new file mode 100644 index 0000000..3cb66b0 --- /dev/null +++ b/src/main/scala/Bank.scala @@ -0,0 +1,28 @@ +class Bank(val allowedAttempts: Integer = 3) { + + private val transactionsQueue: TransactionQueue = new TransactionQueue() + private val processedTransactions: TransactionQueue = new TransactionQueue() + + def addTransactionToQueue(from: Account, to: Account, amount: Double): Unit = ??? + // TODO + // project task 2 + // create a new transaction object and put it in the queue + // spawn a thread that calls processTransactions + + private def processTransactions: Unit = ??? + // TOO + // project task 2 + // Function that pops a transaction from the queue + // and spawns a thread to execute the transaction. + // Finally do the appropriate thing, depending on whether + // the transaction succeeded or not + + def addAccount(initialBalance: Double): Account = { + new Account(this, initialBalance) + } + + def getProcessedTransactionsAsList: List[Transaction] = { + processedTransactions.iterator.toList + } + +} diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala new file mode 100644 index 0000000..aaf47e4 --- /dev/null +++ b/src/main/scala/Main.scala @@ -0,0 +1,12 @@ + +object Main extends App { + + def thread(body: => Unit): Thread = { + val t = new Thread { + override def run() = body + } + t.start + t + } + +} \ No newline at end of file diff --git a/src/main/scala/Transaction.scala b/src/main/scala/Transaction.scala new file mode 100644 index 0000000..3f13c1d --- /dev/null +++ b/src/main/scala/Transaction.scala @@ -0,0 +1,59 @@ +import exceptions._ +import scala.collection.mutable + +object TransactionStatus extends Enumeration { + val SUCCESS, PENDING, FAILED = Value +} + +class TransactionQueue { + + // TODO + // project task 1.1 + // Add datastructure to contain the transactions + + // Remove and return the first element from the queue + def pop: Transaction = ??? + + // Return whether the queue is empty + def isEmpty: Boolean = ??? + + // Add new element to the back of the queue + def push(t: Transaction): Unit = ??? + + // Return the first element from the queue without removing it + def peek: Transaction = ??? + + // Return an iterator to allow you to iterate over the queue + def iterator: Iterator[Transaction] = ??? +} + +class Transaction(val transactionsQueue: TransactionQueue, + val processedTransactions: TransactionQueue, + val from: Account, + val to: Account, + val amount: Double, + val allowedAttemps: Int) extends Runnable { + + var status: TransactionStatus.Value = TransactionStatus.PENDING + var attempt = 0 + + override def run: Unit = { + + def doTransaction() = { + // TODO - project task 3 + // Extend this method to satisfy requirements. + from withdraw amount + to deposit amount + } + + // TODO - project task 3 + // make the code below thread safe + if (status == TransactionStatus.PENDING) { + doTransaction + Thread.sleep(50) // you might want this to make more room for + // new transactions to be added to the queue + } + + + } +} diff --git a/src/main/scala/exceptions/IllegalAmountException.scala b/src/main/scala/exceptions/IllegalAmountException.scala new file mode 100644 index 0000000..c651b08 --- /dev/null +++ b/src/main/scala/exceptions/IllegalAmountException.scala @@ -0,0 +1,5 @@ +package exceptions + +class IllegalAmountException (message: String = null, cause: Throwable = null) extends RuntimeException(message, cause) { + +} \ No newline at end of file diff --git a/src/main/scala/exceptions/NoSufficientFundsException.scala b/src/main/scala/exceptions/NoSufficientFundsException.scala new file mode 100644 index 0000000..dcaedf8 --- /dev/null +++ b/src/main/scala/exceptions/NoSufficientFundsException.scala @@ -0,0 +1,5 @@ +package exceptions + +class NoSufficientFundsException(message: String = null, cause: Throwable = null) extends RuntimeException(message, cause) { + +} \ No newline at end of file diff --git a/src/test/scala/AccountTests.scala b/src/test/scala/AccountTests.scala new file mode 100644 index 0000000..e549894 --- /dev/null +++ b/src/test/scala/AccountTests.scala @@ -0,0 +1,199 @@ +import org.scalatest.FunSuite +import exceptions._ + +class AccountTests extends FunSuite { + + val bank = new Bank() + + test("Test 01: Valid account withdrawal") { + val acc = new Account(bank, 500) + val result = acc.withdraw(250) + assert(acc.getBalanceAmount == 250) + assert(result.isLeft) + } + + test("Test 02: Invalid account withdrawal should throw exception") { + val acc = new Account(bank, 500) + val result = acc.withdraw(750) + assert(acc.getBalanceAmount == 500) + assert(result.isRight) + } + + test("Test 03: Withdrawal of negative amount should throw exception") { + val acc = new Account(bank, 500) + val result = acc.withdraw(-100) + assert(acc.getBalanceAmount == 500) + assert(result.isRight) + } + + test("Test 04: Valid account deposit") { + val acc = new Account(bank, 500) + val result = acc.deposit(250) + assert(acc.getBalanceAmount == 750) + assert(result.isLeft) + } + + test("Test 05: Deposit of negative amount should throw exception") { + val acc = new Account(bank, 500) + val result = acc.deposit(-50) + assert(acc.getBalanceAmount == 500) + assert(result.isRight) + } + + test("Test 06: Correct balance amount after several withdrawals and deposits") { + val acc = new Account(bank, 50000) + val first = Main.thread { + for (i <- 0 until 100) { + acc.withdraw(10); Thread.sleep(10) + } + } + val second = Main.thread { + for (i <- 0 until 100) { + acc.deposit(5); Thread.sleep(20) + } + } + val third = Main.thread { + for (i <- 0 until 100) { + acc.withdraw(50); Thread.sleep(10) + } + } + val fourth = Main.thread { + for (i <- 0 until 100) { + acc.deposit(100); Thread.sleep(10) + } + } + first.join() + second.join() + third.join() + fourth.join() + assert(acc.getBalanceAmount == 54500) + } + + +} + +class AccountTransferTests extends FunSuite { + + + test("Test 07: Valid transfer between accounts") { + val bank = new Bank() + + val acc1 = bank.addAccount(100) + val acc2 = bank.addAccount(200) + + acc1 transferTo(acc2, 50) + + while (bank.getProcessedTransactionsAsList.size != 1) { + Thread.sleep(100) + } + + assert(bank.getProcessedTransactionsAsList.last.status == TransactionStatus.SUCCESS) + assert((acc1.getBalanceAmount == 50) && (acc2.getBalanceAmount == 250)) + } + + test("Test 08: Transfer of negative amount between accounts should fail") { + val bank = new Bank() + + val acc1 = bank.addAccount(500) + val acc2 = bank.addAccount(1000) + + acc1 transferTo(acc2, -100) + + while (bank.getProcessedTransactionsAsList.size != 1) { + Thread.sleep(100) + } + + assert(bank.getProcessedTransactionsAsList.last.status == TransactionStatus.FAILED) + assert((acc1.getBalanceAmount == 500) && (acc2.getBalanceAmount == 1000)) + } + + + test("Test 09: Invalid transfer between accounts due to insufficient funds should lead to transaction status FAILED and no money should be transferred between accounts") { + val bank = new Bank() + val acc1 = new Account(bank, 100) + val acc2 = new Account(bank, 1000) + + acc1 transferTo(acc2, 150) + + while (bank.getProcessedTransactionsAsList.size != 1) { + Thread.sleep(100) + } + + assert(bank.getProcessedTransactionsAsList.last.status == TransactionStatus.FAILED) + assert((acc1.getBalanceAmount == 100) && (acc2.getBalanceAmount == 1000)) + + } + + + test("Test 10: Correct balance amounts after several transfers") { + val bank = new Bank() + + val acc1 = new Account(bank, 3000) + val acc2 = new Account(bank, 5000) + val first = Main.thread { + for (i <- 0 until 100) { + bank addTransactionToQueue(acc1, acc2, 30) + } + } + val second = Main.thread { + for (i <- 0 until 100) { + bank addTransactionToQueue(acc2, acc1, 23) + } + } + first.join() + second.join() + + while (bank.getProcessedTransactionsAsList.size != 200) { + Thread.sleep(100) + } + + assert((acc1.getBalanceAmount == 2300) && (acc2.getBalanceAmount == 5700)) + + } + + test("Test 11: Failed transactions should retry and potentially succeed with multiple allowed attempts") { + var failed = 0 + for (x <- 1 to 100) { + val bank = new Bank(allowedAttempts = 3) + + val acc1 = new Account(bank, 100) + val acc2 = new Account(bank, 100) + val acc3 = new Account(bank, 100) + + for (i <- 1 to 6) { acc1 transferTo (acc2, 50) } + for (j <- 1 to 2) { acc3 transferTo (acc1, 50) } + + while (bank.getProcessedTransactionsAsList.size != 8) { + Thread.sleep(100) + } + + if (!(acc1.getBalanceAmount == 0 + && acc2.getBalanceAmount == 300 + && acc3.getBalanceAmount == 0)) failed += 1 + } + assert(failed <= 5) + + } + + test("Test 12: Some transactions should be stopped with only one allowed attempt") { + var failed = 0 + for (x <- 1 to 100) { + val bank = new Bank(allowedAttempts = 1) + + val acc1 = new Account(bank, 100) + val acc2 = new Account(bank, 100) + val acc3 = new Account(bank, 100) + + for (i <- 1 to 6) { acc1 transferTo (acc2, 50) } + for (j <- 1 to 2) { acc3 transferTo (acc1, 50) } + + while (bank.getProcessedTransactionsAsList.size != 8) { + Thread.sleep(100) + } + + if (!(acc2.getBalanceAmount != 300 && acc3.getBalanceAmount == 0)) failed += 1 + } + assert(failed <= 5) + } + +}