Skip to content
Snippets Groups Projects
TestHelper.scala 5.72 KiB
Newer Older
Matt Bovel's avatar
Matt Bovel committed
package instrumentation

import scala.util.Random
Matt Bovel's avatar
Matt Bovel committed
import scala.collection.mutable.{Map as MutableMap}
Matt Bovel's avatar
Matt Bovel committed
import Stats.*
Matt Bovel's avatar
Matt Bovel committed

object TestHelper:
  val noOfSchedules = 10000 // set this to 100k during deployment
Matt Bovel's avatar
Matt Bovel committed
  val readWritesPerThread =
    20 // maximum number of read/writes possible in one thread
Matt Bovel's avatar
Matt Bovel committed
  val contextSwitchBound = 10
  val testTimeout = 240 // the total time out for a test in seconds
Matt Bovel's avatar
Matt Bovel committed
  val schedTimeout =
    15 // the total time out for execution of a schedule in secs
Matt Bovel's avatar
Matt Bovel committed

  // Helpers
  /*def testManySchedules(op1: => Any): Unit = testManySchedules(List(() => op1))
  def testManySchedules(op1: => Any, op2: => Any): Unit = testManySchedules(List(() => op1, () => op2))
  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))*/

Matt Bovel's avatar
Matt Bovel committed
  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])
        )
    )
Matt Bovel's avatar
Matt Bovel committed
  /** @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
  ) =
Matt Bovel's avatar
Matt Bovel committed
    var timeout = testTimeout * 1000L
    val threadIds = (1 to numThreads)
Matt Bovel's avatar
Matt Bovel committed
    // (1 to scheduleLength).flatMap(_ => threadIds).toList.permutations.take(noOfSchedules).foreach {
Matt Bovel's avatar
Matt Bovel committed
    val schedules = (new ScheduleGenerator(numThreads)).schedules()
    var schedsExplored = 0
Matt Bovel's avatar
Matt Bovel committed
    schedules.takeWhile(_ =>
      schedsExplored <= noOfSchedules && timeout > 0
    ).foreach {
      // case _ if timeout <= 0 => // break
Matt Bovel's avatar
Matt Bovel committed
      case schedule =>
        schedsExplored += 1
        val schedr = new Scheduler(schedule)
Matt Bovel's avatar
Matt Bovel committed
        // println("Exploring Sched: "+schedule)
Matt Bovel's avatar
Matt Bovel committed
        val (threadOps, assertion) = ops(schedr)
        if threadOps.size != numThreads then
Matt Bovel's avatar
Matt Bovel committed
          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
Matt Bovel's avatar
Matt Bovel committed
          case Timeout(msg) =>
Matt Bovel's avatar
Matt Bovel committed
            throw new java.lang.AssertionError(
              "assertion failed\n" + "The schedule took too long to complete. A possible deadlock! \n" + msg
            )
Matt Bovel's avatar
Matt Bovel committed
          case Except(msg, stkTrace) =>
Matt Bovel's avatar
Matt Bovel committed
            val traceStr = "Thread Stack trace: \n" + stkTrace.map(
              " at " + _.toString
            ).mkString("\n")
            throw new java.lang.AssertionError(
              "assertion failed\n" + msg + "\n" + traceStr
            )
Matt Bovel's avatar
Matt Bovel committed
          case RetVal(threadRes) =>
            // check the assertion
            val (success, custom_msg) = assertion(threadRes)
            if !success then
Matt Bovel's avatar
Matt Bovel committed
              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)
Matt Bovel's avatar
Matt Bovel committed
    }
    if timeout <= 0 then
Matt Bovel's avatar
Matt Bovel committed
      throw new java.lang.AssertionError(
        "Test took too long to complete! Cannot check all schedules as your code is too slow!"
      )
Matt Bovel's avatar
Matt Bovel committed
  /** A schedule generator that is based on the context bound
    */
Matt Bovel's avatar
Matt Bovel committed
  class ScheduleGenerator(numThreads: Int):
    val scheduleLength = readWritesPerThread * numThreads
Matt Bovel's avatar
Matt Bovel committed
    val rands =
      (1 to scheduleLength).map(i =>
        new Random(0xcafe * i)
      ) // random numbers for choosing a thread at each position
Matt Bovel's avatar
Matt Bovel committed
    def schedules(): LazyList[List[Int]] =
      var contextSwitches = 0
Matt Bovel's avatar
Matt Bovel committed
      var contexts =
        List[Int]() // a stack of thread ids in the order of context-switches
Matt Bovel's avatar
Matt Bovel committed
      val remainingOps = MutableMap[Int, Int]()
Matt Bovel's avatar
Matt Bovel committed
      remainingOps ++= (1 to numThreads).map(i =>
        (i, readWritesPerThread)
      ) // num ops remaining in each thread
Matt Bovel's avatar
Matt Bovel committed
      val liveThreads = (1 to numThreads).toSeq.toBuffer

Matt Bovel's avatar
Matt Bovel committed
      /** Updates remainingOps and liveThreads once a thread is chosen for a
        * position in the schedule
        */
Matt Bovel's avatar
Matt Bovel committed
      def updateState(tid: Int): Unit =
        val remOps = remainingOps(tid)
        if remOps == 0 then
          liveThreads -= tid
        else
          remainingOps += (tid -> (remOps - 1))
      val schedule = rands.foldLeft(List[Int]()) {
        case (acc, r) if contextSwitches < contextSwitchBound =>
          val tid = liveThreads(r.nextInt(liveThreads.size))
          contexts match
Matt Bovel's avatar
Matt Bovel committed
            case prev :: tail
                if prev != tid => // we have a new context switch here
Matt Bovel's avatar
Matt Bovel committed
              contexts +:= tid
              contextSwitches += 1
            case prev :: tail =>
            case _ => // init case
              contexts +:= tid
          updateState(tid)
          acc :+ tid
Matt Bovel's avatar
Matt Bovel committed
        case (
              acc,
              _
            ) => // here context-bound has been reached so complete the schedule without any more context switches
Matt Bovel's avatar
Matt Bovel committed
          if !contexts.isEmpty then
            contexts = contexts.dropWhile(remainingOps(_) == 0)
          val tid = contexts match
            case top :: tail => top
Matt Bovel's avatar
Matt Bovel committed
            case _ =>
              liveThreads(
                0
              ) // here, there has to be threads that have not even started
Matt Bovel's avatar
Matt Bovel committed
          updateState(tid)
          acc :+ tid
      }
      schedule #:: schedules()