Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • lara/cs206-demos
  • gcharles/cs206-demos
  • gambhir/cs206-demos
3 results
Show changes
package instrumentation
import scala.util.Random
import scala.collection.mutable.{Map => MutableMap}
import scala.collection.mutable.{Map as MutableMap}
import Stats._
import Stats.*
object TestHelper:
val noOfSchedules = 10000 // set this to 100k during deployment
val readWritesPerThread = 20 // maximum number of read/writes possible in one thread
val readWritesPerThread =
20 // maximum number of read/writes possible in one thread
val contextSwitchBound = 10
val testTimeout = 240 // the total time out for a test in seconds
val schedTimeout = 15 // the total time out for execution of a schedule in secs
val schedTimeout =
15 // the total time out for execution of a schedule in secs
// Helpers
/*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1))
......@@ -18,70 +20,102 @@ object TestHelper:
def testManySchedules(op1: => Any, op2: => Any, op3: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3))
def testManySchedules(op1: => Any, op2: => Any, op3: => Any, op4: => Any): Unit = testManySchedules(List(() => op1, () => op2, () => op3, () => op4))*/
def testSequential[T](ops: Scheduler => Any)(assertions: T => (Boolean, String)) =
testManySchedules(1,
(sched: Scheduler) => {
(List(() => ops(sched)),
(res: List[Any]) => assertions(res.head.asInstanceOf[T]))
})
def testSequential[T](ops: Scheduler => Any)(assertions: T => (
Boolean,
String
)) =
testManySchedules(
1,
(sched: Scheduler) =>
(
List(() => ops(sched)),
(res: List[Any]) => assertions(res.head.asInstanceOf[T])
)
)
/**
* @numThreads number of threads
* @ops operations to be executed, one per thread
* @assertion as condition that will executed after all threads have completed (without exceptions)
* the arguments are the results of the threads
*/
def testManySchedules(numThreads: Int,
ops: Scheduler =>
(List[() => Any], // Threads
List[Any] => (Boolean, String)) // Assertion
) =
/** @numThreads
* number of threads
* @ops
* operations to be executed, one per thread
* @assertion
* as condition that will executed after all threads have completed
* (without exceptions) the arguments are the results of the threads
*/
def testManySchedules(
numThreads: Int,
ops: Scheduler => (
List[() => Any], // Threads
List[Any] => (Boolean, String)
) // Assertion
) =
var timeout = testTimeout * 1000L
val threadIds = (1 to numThreads)
//(1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach {
// (1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach {
val schedules = (new ScheduleGenerator(numThreads)).schedules()
var schedsExplored = 0
schedules.takeWhile(_ => schedsExplored <= noOfSchedules && timeout > 0).foreach {
//case _ if timeout <= 0 => // break
schedules.takeWhile(_ =>
schedsExplored <= noOfSchedules && timeout > 0
).foreach {
// case _ if timeout <= 0 => // break
case schedule =>
schedsExplored += 1
val schedr = new Scheduler(schedule)
//println("Exploring Sched: "+schedule)
// println("Exploring Sched: "+schedule)
val (threadOps, assertion) = ops(schedr)
if threadOps.size != numThreads then
throw new IllegalStateException(s"Number of threads: $numThreads, do not match operations of threads: $threadOps")
timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t => timeout -= t } match
throw new IllegalStateException(
s"Number of threads: $numThreads, do not match operations of threads: $threadOps"
)
timed { schedr.runInParallel(schedTimeout * 1000, threadOps) } { t =>
timeout -= t
} match
case Timeout(msg) =>
throw new java.lang.AssertionError("assertion failed\n"+"The schedule took too long to complete. A possible deadlock! \n"+msg)
throw new java.lang.AssertionError(
"assertion failed\n" + "The schedule took too long to complete. A possible deadlock! \n" + msg
)
case Except(msg, stkTrace) =>
val traceStr = "Thread Stack trace: \n"+stkTrace.map(" at "+_.toString).mkString("\n")
throw new java.lang.AssertionError("assertion failed\n"+msg+"\n"+traceStr)
val traceStr = "Thread Stack trace: \n" + stkTrace.map(
" at " + _.toString
).mkString("\n")
throw new java.lang.AssertionError(
"assertion failed\n" + msg + "\n" + traceStr
)
case RetVal(threadRes) =>
// check the assertion
val (success, custom_msg) = assertion(threadRes)
if !success then
val msg = "The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString("\n")
throw new java.lang.AssertionError("Assertion failed: "+msg)
val msg =
"The following schedule resulted in wrong results: \n" + custom_msg + "\n" + schedr.getOperationLog().mkString(
"\n"
)
throw new java.lang.AssertionError("Assertion failed: " + msg)
}
if timeout <= 0 then
throw new java.lang.AssertionError("Test took too long to complete! Cannot check all schedules as your code is too slow!")
throw new java.lang.AssertionError(
"Test took too long to complete! Cannot check all schedules as your code is too slow!"
)
/**
* A schedule generator that is based on the context bound
*/
/** A schedule generator that is based on the context bound
*/
class ScheduleGenerator(numThreads: Int):
val scheduleLength = readWritesPerThread * numThreads
val rands = (1 to scheduleLength).map(i => new Random(0xcafe * i)) // random numbers for choosing a thread at each position
val rands =
(1 to scheduleLength).map(i =>
new Random(0xcafe * i)
) // random numbers for choosing a thread at each position
def schedules(): LazyList[List[Int]] =
var contextSwitches = 0
var contexts = List[Int]() // a stack of thread ids in the order of context-switches
var contexts =
List[Int]() // a stack of thread ids in the order of context-switches
val remainingOps = MutableMap[Int, Int]()
remainingOps ++= (1 to numThreads).map(i => (i, readWritesPerThread)) // num ops remaining in each thread
remainingOps ++= (1 to numThreads).map(i =>
(i, readWritesPerThread)
) // num ops remaining in each thread
val liveThreads = (1 to numThreads).toSeq.toBuffer
/**
* Updates remainingOps and liveThreads once a thread is chosen for a position in the schedule
*/
/** Updates remainingOps and liveThreads once a thread is chosen for a
* position in the schedule
*/
def updateState(tid: Int): Unit =
val remOps = remainingOps(tid)
if remOps == 0 then
......@@ -92,7 +126,8 @@ object TestHelper:
case (acc, r) if contextSwitches < contextSwitchBound =>
val tid = liveThreads(r.nextInt(liveThreads.size))
contexts match
case prev :: tail if prev != tid => // we have a new context switch here
case prev :: tail
if prev != tid => // we have a new context switch here
contexts +:= tid
contextSwitches += 1
case prev :: tail =>
......@@ -100,12 +135,18 @@ object TestHelper:
contexts +:= tid
updateState(tid)
acc :+ tid
case (acc, _) => // here context-bound has been reached so complete the schedule without any more context switches
case (
acc,
_
) => // here context-bound has been reached so complete the schedule without any more context switches
if !contexts.isEmpty then
contexts = contexts.dropWhile(remainingOps(_) == 0)
val tid = contexts match
case top :: tail => top
case _ => liveThreads(0) // here, there has to be threads that have not even started
case _ =>
liveThreads(
0
) // here, there has to be threads that have not even started
updateState(tid)
acc :+ tid
}
......
package instrumentation
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.*
import scala.concurrent.duration.*
import scala.concurrent.ExecutionContext.Implicits.global
import org.junit.Assert.*
......@@ -15,19 +15,22 @@ object TestUtils:
catch
case _: Throwable => return true
return false
def assertDeadlock[T](action: => T): Unit =
try
action
throw new AssertionError("No error detected.")
catch
case e: AssertionError =>
assert(e.getMessage.contains("Deadlock"), "No deadlock detected.")
case e: AssertionError =>
assert(e.getMessage.contains("Deadlock"), "No deadlock detected.")
def assertMaybeDeadlock[T](action: => T): Unit =
try
action
throw new AssertionError("No error detected.")
catch
case e: AssertionError =>
assert(e.getMessage.contains("A possible deadlock!"), "No deadlock detected.")
case e: AssertionError =>
assert(
e.getMessage.contains("A possible deadlock!"),
"No deadlock detected."
)
......@@ -9,17 +9,18 @@ import instrumentation.*
import java.util.concurrent.atomic.AtomicInteger
class Part4Test:
// This test can result in a deadlock because locks can be called in any
// order. Here, Thread 1 locks Node 3 first and then Node 2, whereas Thread 2
// locks Node 2 first and then Node 3. This will lead to a deadlock.
// locks Node 2 first and then Node 3. This will lead to a deadlock.
@Test
def testQuestion9() =
TestUtils.assertDeadlock(
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
......@@ -29,12 +30,15 @@ class Part4Test:
List(
() =>
// Thread 1
var nodes: List[Node] = List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes
lockFun(nodes, increment),
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] = List(allNodes(5), allNodes(2), allNodes(3))
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes
lockFun(nodes, increment),
),
......@@ -42,7 +46,7 @@ class Part4Test:
)
)
)
// This will not lead to a deadlock because the lock acquire happens in a
// particular order. Thread 1 acquires locks in order 1->2->3->4, whereas
// Thread 2 acquires locks in order 2->3->5.
......@@ -51,7 +55,8 @@ class Part4Test:
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
......@@ -61,12 +66,15 @@ class Part4Test:
List(
() =>
// Thread 1
var nodes: List[Node] = List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid > y.guid)
lockFun(nodes, increment),
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] = List(allNodes(5), allNodes(2), allNodes(3))
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid > y.guid)
lockFun(nodes, increment),
),
......@@ -74,16 +82,16 @@ class Part4Test:
)
)
// This will not lead to a deadlock because the lock acquire happens in a
// particular order. Thread 1 acquires locks in order 4->3->2->1, whereas
// Thread 2 acquires locks in order 5->3->2.
// Thread 2 acquires locks in order 5->3->2.
@Test
def testQuestion11() =
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
......@@ -93,12 +101,15 @@ class Part4Test:
List(
() =>
// Thread 1
var nodes: List[Node] = List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] = List(allNodes(5), allNodes(2), allNodes(3))
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
......@@ -109,14 +120,15 @@ class Part4Test:
// This test can result in a deadlock because locks are not called in any
// order. Thread 1 acquire order (3->2->4->1), Thread 2 acquire order
// (2->3->5). Thread 1 locks Node3 first and then Node2, whereas Thread 2
// locks Node 2 first and then Node3. This will lead to a deadlock.
// locks Node 2 first and then Node3. This will lead to a deadlock.
@Test
def testQuestion12() =
TestUtils.assertDeadlock(
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
......@@ -126,12 +138,15 @@ class Part4Test:
List(
() =>
// Thread 1
var nodes: List[Node] = List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.tail.appended(nodes(0))
lockFun(nodes, increment),
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] = List(allNodes(5), allNodes(2), allNodes(3))
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.tail.appended(nodes(0))
lockFun(nodes, increment),
),
......@@ -139,31 +154,35 @@ class Part4Test:
)
)
)
// sum returns wrong answer because there is a data race on the sum variable.
// sum returns wrong answer because there is a data race on the sum variable.
@Test(expected = classOf[AssertionError])
def testQuestion13() =
TestHelper.testManySchedules(
2,
scheduler =>
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
val allNodes =
(for i <- 0 to 6 yield ScheduledNode(i, scheduler)).toList
// Shared by all threads
var sum: Int = 0
def increment(e: Int) =
val previousSum = scheduler.exec{sum}("Get sum")
scheduler.exec{sum = previousSum + e}("Write sum")
val previousSum = scheduler.exec { sum }("Get sum")
scheduler.exec { sum = previousSum + e }("Write sum")
(
List(
() =>
// Thread 1
var nodes: List[Node] = List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] = List(allNodes(5), allNodes(2), allNodes(3))
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
......@@ -184,7 +203,7 @@ class Part4Test:
val allNodes = (for i <- 0 to 6 yield ScheduledNode(i, sched)).toList
val monitor = new MockedMonitor: // Monitor is a type of a lock.
def scheduler = sched
def scheduler = sched
// Shared by all threads
var sum: Int = 0
......@@ -195,12 +214,15 @@ class Part4Test:
List(
() =>
// Thread 1
var nodes: List[Node] = List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] = List(allNodes(5), allNodes(2), allNodes(3))
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
......@@ -211,7 +233,7 @@ class Part4Test:
(true, "")
)
)
// total will give correct output here as it is an atomic instruction.
@Test
def testQuestion15() =
......@@ -229,12 +251,15 @@ class Part4Test:
List(
() =>
// Thread 1
var nodes: List[Node] = List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
var nodes: List[Node] =
List(allNodes(1), allNodes(3), allNodes(2), allNodes(4))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
lockFun(nodes, increment)
,
() =>
// Thread 2
var nodes: List[Node] = List(allNodes(5), allNodes(2), allNodes(3))
var nodes: List[Node] =
List(allNodes(5), allNodes(2), allNodes(3))
nodes = nodes.sortWith((x, y) => x.guid < y.guid)
lockFun(nodes, increment),
),
......@@ -245,6 +270,6 @@ class Part4Test:
(true, "")
)
)
class ScheduledNode(value: Int, val scheduler: Scheduler) extends Node(value) with MockedMonitor
class ScheduledNode(value: Int, val scheduler: Scheduler) extends Node(value)
with MockedMonitor
......@@ -14,7 +14,7 @@ class Part7Test:
@Test
def testQuestion22() =
testNicManagerParallel(2, 3)
@Test
def testQuestion23() =
val nicsManager = NICManager(2)
......@@ -32,11 +32,11 @@ class Part7Test:
@Test
def testQuestion24() =
testNicManagerParallel(3, 2, true)
@Test
def testQuestion24NotLimitingRecvNICs() =
TestUtils.assertMaybeDeadlock(
testNicManagerParallel(3, 2)
testNicManagerParallel(3, 2)
)
def testNicManagerParallel(
......@@ -48,15 +48,16 @@ class Part7Test:
threads,
scheduler =>
val nicsManager = ScheduledNicsManager(nics, scheduler)
val tasks = for i <- 0 until threads yield () =>
// Thread i
val (recvNIC, sendNIC) = nicsManager.assignNICs(limitRecvNICs)
val tasks =
for i <- 0 until threads yield () =>
// Thread i
val (recvNIC, sendNIC) = nicsManager.assignNICs(limitRecvNICs)
// Do something with NICs...
// Do something with NICs...
// Un-assign NICs
nicsManager.nics(recvNIC).assigned = false
nicsManager.nics(sendNIC).assigned = false
// Un-assign NICs
nicsManager.nics(recvNIC).assigned = false
nicsManager.nics(sendNIC).assigned = false
(
tasks.toList,
results =>
......@@ -82,8 +83,9 @@ class Part7Test:
"",
Some(res => f"read NIC.assigned == $res")
)
override def assigned_=(v: Boolean) = scheduler.exec { super.assigned = v }(
f"write NIC.assigned = $v"
)
override def assigned_=(v: Boolean) =
scheduler.exec { super.assigned = v }(
f"write NIC.assigned = $v"
)
override val nics =
(for i <- 0 until n yield ScheduledNIC(i, false, scheduler)).toList
......@@ -146,7 +146,7 @@ class Part8Test:
else validateGraph(insta)
)
)
@Test
def testParallelRemove() =
TestHelper.testManySchedules(
......@@ -181,7 +181,7 @@ class Part8Test:
else validateGraph(insta)
)
)
// We test wrong code here, so we expect an assertion error. You can replace
// the next line by `@Test` if you want to see the error with the failing
// schedule.
......@@ -193,7 +193,7 @@ class Part8Test:
val insta = new Instagram:
override val graph =
ScheduledTrieMap(TrieMap[Int, List[Int]](), scheduler)
// This implementation of `add` is wrong, because two threads might
// allocate the same id.
// Consider the following schedule:
......@@ -265,7 +265,10 @@ class Part8Test:
results =>
val size = insta.graph.size
if insta.graph(u1).size != 1 then
(false, f"Wrong number of users followed by 1: expected 1 but got ${insta.graph(u1)}")
(
false,
f"Wrong number of users followed by 1: expected 1 but got ${insta.graph(u1)}"
)
else validateGraph(insta)
)
)
......@@ -306,7 +309,7 @@ class Part8Test:
else validateGraph(insta)
)
)
@nowarn
def validateGraph(insta: Instagram): (Boolean, String) =
for (a, following) <- insta.graph; b <- following do
......
package midterm23
import instrumentation.*
import java.util.concurrent.atomic.AtomicInteger
class BarberShopSolutionTest extends munit.FunSuite:
val implementations =
Map[String, (Int, Scheduler) => ScheduledBarberShopSolution](
"BarberShopSolution1" -> (new BarberShopSolution1(_)
with ScheduledBarberShopSolution(_)),
"BarberShopSolution2" -> (new BarberShopSolution2(_)
with ScheduledBarberShopSolution(_)),
"BarberShopSolution3" -> (new BarberShopSolution3(_)
with ScheduledBarberShopSolution(_))
)
for (name, makeShop) <- implementations do
for nCustomers <- 1 to 3 do
test(f"$name with $nCustomers customer(s)") {
testBarberShop(nCustomers, makeShop)
}
package midterm23
import instrumentation.{MockedMonitor, Monitor, Scheduler}
import java.util.concurrent.ConcurrentLinkedQueue
import scala.jdk.CollectionConverters.IterableHasAsScala
import scala.annotation.tailrec
trait ScheduledBarberShopSolution(scheduler0: Scheduler)
extends AbstractBarberShopSolution:
enum Event:
case HairCut
case CustomerLeaves
val trace = ConcurrentLinkedQueue[Event]()
override def notifyBarber = scheduler0.exec { super.notifyBarber }(
"",
Some(res => f"read notifyBarber == $res")
)
override def notifyBarber_=(v: Boolean) =
scheduler0.exec { super.notifyBarber = v }(
f"write notifyBarber = $v"
)
override def notifyCustomer = scheduler0.exec { super.notifyCustomer }(
"",
Some(res => f"read notifyCustomer == $res")
)
override def notifyCustomer_=(v: Boolean) =
scheduler0.exec { super.notifyCustomer = v }(
f"write notifyCustomer = $v"
)
override def hairCut() =
log("hair cut")
trace.add(Event.HairCut)
def customerTrace(n: Int): Unit =
this.customer(n)
log("customer leaves")
trace.add(Event.CustomerLeaves)
private val _lockObj = new MockedMonitor:
override def scheduler: Scheduler = scheduler0
override def lockObj: Monitor = _lockObj
override def log(s: String): Unit = scheduler0.log(s)
def nHaircuts: Int =
trace.asScala.filter(_ == Event.HairCut).size
def customerLeftEarly(): Boolean =
@tailrec
def loop(it: Iterable[Event], n: Int): Boolean =
if it.isEmpty then false
else
val newN = it.head match
case Event.HairCut => n + 1
case Event.CustomerLeaves => n - 1
if newN < 0 then true
else loop(it.tail, newN)
loop(trace.asScala, 0)
package midterm23
import instrumentation.{Scheduler, TestHelper}
/** Tests a barber shop implementation.
*
* @param nCustomers
* The number of customers to simulate.
* @param makeShop
* A function that creates a barber shop given the number of customers and a
* scheduler.
* @param checkEarlyCustomer
* If true, checks that no customer left early, i.e. that there is always a
* number of terminated customer threads equal or less than to the number of
* haircuts done.
*/
def testBarberShop(
nCustomers: Int,
makeShop: (Int, Scheduler) => ScheduledBarberShopSolution,
checkEarlyCustomer: Boolean = true
) =
TestHelper.testManySchedules(
nCustomers + 1,
scheduler =>
val barberShop = makeShop(nCustomers, scheduler)
(
(() => barberShop.barber())
:: (for i <- 1 to nCustomers
yield () => barberShop.customerTrace(i)).toList,
results =>
if barberShop.nHaircuts != nCustomers then
(false, f"Unexpected number of hair cuts: ${barberShop.nHaircuts}")
else if checkEarlyCustomer && barberShop.customerLeftEarly() then
(false, f"A customer left early")
else
(true, "")
)
)