Muscat provides APIs to test your concurrent code under multiple interleavings of shared operations executed by concurrent threads.
The code for the threads, inputs, and assertions to be tested have to be provided by the user much like a unit testing framework. Muscat can systematically explore multiple interleavings between selected operations, marked by
users, when they are executed concurrently by multiple threads.
It can explore interleavings even in the presence of synchronization primitives: synchronized, wait, notify, and notifyAll.
Muscat presents an interleaved execution trace for failed test cases, and
can detect and report deadlocks that happen at runtime.
This library is primarly intented to be used for testing small pieces of concurrent code. It was able to explore several tens of thousands of interleavings per second on such programs. In principle, Muscat could be used to test arbitrary Scala programs, provided the configuration parameters are set sufficiently high.
You can find some small illustrative example and a test suite here: src/main/scala/ch/epfl/lara/concprog/SimpleCounter.Scala and src/test/scala/ch/epfl/lara/concprog/SimpleCounterSuite.Scala. The example presents a simple concurrent program that implements the compareAndSet operation using the synchronized construct. It illustrates how to write unit tests using APIs provided by Muscat.
To run the example and test Muscat code do:
sbt test
To use Muscat into other projects, do:
sbt package
and import the jar file generated in the target directory.
Below we present an overview of how to use Muscat.
For a more detailed description and technical details see the whitepaper, which appeared in
Scala 2016.
Muscat can be used to test programs that access or synchronize on objects shared across multiple threads.
To be able to test with Muscat, the shared classes must extend a library trait and should wrap their atomic operations within a library construct exec.
Below we illustate it on a simple example. (You may want to follow the below described design pattern as it restricts the overheads of scheduling to the unit tests, isolating them from the normal workflow).
Let class Base be the class that is accessible by multiple threads and needs to be tested in a concurrent setting.
For example, class Base could be a concurrent queue or list.
-
Make
class Baseextend (or implement) the traitch.epfl.lara.concprog.instrumentation.monitors.Monitor. Now thesynchronized,wait,notify,notifyAllmethods invoked on instances ofclass Basecan be instrumented by the APIs provided by Muscat. Also, make sure that every indivisible operation such as read/write of a shared mutable field that may run concurrently is a separate (protected) overridable method of the class. Example:AbstractSimpleCounter.scala -
To create a schedulable version of
class Base, create aclass Derivedwhich does the following:extends class Base- accept one extra parameter
schedulerof typeScheduler - extends one of the available monitors in the package
ch.epfl.lara.concprog.instrumentation.monitorsSchedulableMonitor- This monitors the behavior of the synchronization primitivessynchronized,wait,notifyandnotifyAll.SchedulableMonitor with LockFreeMonitor- This makes synchronization primitives throw an exception, and is meant to test lock-free data structures.
- does
import scheduler._at the start ofclass Derived. - overrides or defines the indivisible operations from
class Basewrapping the indivisible operations withexec. The constructexechas the following signature:exec(operation)(msgA, Some(res => msgB))whereoperationis a call-by-name parameter that is indivisible operation,msgAis the message that is logged before the operation, andmsgBis the message to log after the operation, which can refer to the resultresof the computation. Example:SchedulableSimpleCounter.scala
Now the methods of
class Derivedcan be tested by Muscat on multiple interleavings of the shared operations marked withexecas described shortly. Note that operations marked withexecneed not necessarily be a read/write of a field but instead marks the granularity of an indivisible operations. Any context-switches within these operations are not explored by Muscat.
You may have a look at the first three test cases in the file
SimpleCounterSuite.scala.
Muscat provides two functions testSequential to test single-threaded (or sequential) behavior, and testManySchedules to
test multi-threaded (or concurrent) behavior.
-
testSequentialavailable inch.epfl.lara.concprog.instrumentation.TestHelper._. takes one argument: a lamdba from aSchedulerto the the code that needs to be tested. The code can access any data structure or methods defined in the program. However, when it accessesSchedulableclasses such asclass Derivedits operations will be systematicaly interleaved. Note that theSchedulerargument of the lambda shall be passed to the constructor ofclass Derived, which requires a scheduler. The code should return a(Boolean, String)pair where theBooleanindicates if the test succeeded, and theStringis displayed if the test failed. The operation has a default timeout, which can be adjusted by the user. -
testManySchedulesis available inch.epfl.lara.concprog.instrumentation.TestHelper._. It requires
- The number
nof threads to run in parallel. - A lambda from
Schedulerto a pair consisting of:- A list of
nlambdas accepting no argument and performing some operations. Each lamdba corresponds to the code that would be executed by the threads - A lambda to which the result of the
nthreads (encoded as aList[Any]) would be fed. The lambda should return a pair(Boolean, String)where theBooleanindicates if the test succeeded, and theStringis displayed if the test failed. Note that the code executed by threads can also manipulate other mutable state if necessary that is not wrapped insideexec. For instance, to track values other than those that are returned. The tool cannot detect bugs resulting because of such mutable states.
- A list of
The exploration of interleavings can be configured using the following parameters found in the file ch.epfl.lara.concprog.instrumentation.TestHelper._
contextSwitchBound: Maximum number of context-switches possible in the schedules generated for testingreadWritesPerThread: Maximum number ofexecoperations that can be performed by the threads. If more operations are performed by the threads, they would not be tested for multiple interleavings.noOfSchedules: Maximum number of schedules or interleavings to test. The interleavings are uniformly randomly sampled by default.testTimeout: The time out for a unit test in seconds. A test is considered to have failed if it exceeds the time out.scheduleTimeout: The time out for a single interleaving. The interleaving is considered buggy and reported to the user if it exceeds this timeout.
If you need to debug code with complex logic and want to add more statements, you can do the following:
- Make
class Baseabstract and add the following to it: (You may want to do this only during testing, and restoreBaseback to its original form)
def scheduler: Scheduler
lazy val ss = scheduler
import ss._-
Make sure
class Baseis never instantiated, onlyclass Derivedis. You just made the scheduler passed toDerivedavailable to class Base. -
Everywhere you need to add a log line, just do:
log("Your log line.")It will be prefixed with the thread number in the buggy trace.
Yes please have a look at the file SimpleCounterSuite.scala and especially the interlock test.
The file has buggy and non-buggy implementations of lock-free and blocking programs
and some examples for how to test them.
Also, the whitepaper illustrates the system a producer-consumer example.
Please feel free to contact us if you are intersted in using the library and need some help in getting started.
- Ravichandhran Madhavan, Github username: ravimad, [kmrc87 at gmail dot com]
- Mikael Mayer, Github username: MikaelMayer