Skip to content

Commit 266a28b

Browse files
authored
tests: thread-safe test suites (#353)
This makes steps towards making test suites thread-safe. That is, multiple suites can be executed in parallel, but test cases within the same suite are still run sequentially. * add locks around systemtests * DataStructureAnalysis: avoid global NodeCounter this replaces the global singleton NodeCounter with a new instance per DSA execution. this allows multiple DSA runs to run in parallel. the NodeCounter is constructed by the DataStructureAnalysis class, then passed through an implicit "using" parameter to the classes which require it. * remove reset method from NodeCounter * refactor to LockManager class * lock around testResults * scalafmt * greatly simplify Lockmanager by not using RWLock LockManager now uses a single lock around the map and this is shared for reads and writes. this will increase lock contention. however, the locked duration will be extremely small since it only wraps a single hashmap operation. this should have no discernible performance impact. * Apply suggestions from code review * SystemTests: shuffle tests honestly, i think this might be annoying, especially if you try to run the tests locally to reproduce an issue and the order keeps changing. * SystemTests: insert suite name into output file names this should avoid clashes where test suites try to write to the same output file. SystemTestsBAP and SystemTestsGTIRB are overriden to have no suffix, to maintain compatibility with the update-expected sbt/mill jobs. * Revert "SystemTests: shuffle tests" This reverts commit f4b3a53. * ci: run 3 suites in parallel -T is used to re-sort the test cases into the original order before printing them in batches for each suite to avoid interleaving individual cases from multiple suites running in parallel. * RegionTimer: use atomic long instead of instance vars * ExtraSpecTests: retry on failure
1 parent b45b98f commit 266a28b

File tree

8 files changed

+120
-39
lines changed

8 files changed

+120
-39
lines changed

.github/workflows/run-examples.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ jobs:
6565
- run: dotnet tool install --global boogie --version '3.4.3'
6666

6767
- run: ./mill compile
68-
- run: ./scripts/scalatest.sh -oID -n test_util.tags.StandardSystemTest
68+
- run: ./scripts/scalatest.sh -oID -PS3 -T 1000000 -n test_util.tags.StandardSystemTest
6969

7070
- uses: actions/upload-artifact@v4
7171
with:
@@ -116,7 +116,7 @@ jobs:
116116
- run: dotnet tool install --global boogie --version '3.4.3'
117117

118118
- run: ./mill test.compile
119-
- run: ./scripts/scalatest.sh -oID -n test_util.tags.UnitTest
119+
- run: ./scripts/scalatest.sh -oID -PS3 -T 1000000 -n test_util.tags.UnitTest
120120

121121
# every test with package prefix:
122122
# sbt "show test:definedTests"
@@ -144,4 +144,4 @@ jobs:
144144
- run: echo "All systemtest suites:" && ./mill test.testOnly '*SystemTests*' -- -z 'xxxx'
145145

146146
- run: ./mill compile
147-
- run: ./scripts/scalatest.sh -oID -n test_util.tags.AnalysisSystemTest
147+
- run: ./scripts/scalatest.sh -oID -PS3 -T 1000000 -n test_util.tags.AnalysisSystemTest

src/main/scala/analysis/data_structure_analysis/DataStructureAnalysis.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class DataStructureAnalysis(
3535
params: Map[Procedure, Set[Variable]]
3636
) extends Analysis[Map[Procedure, Graph]] {
3737

38+
private val nodeCounter = NodeCounter()
39+
given NodeCounter = nodeCounter
40+
3841
val local: mutable.Map[Procedure, Graph] = mutable.Map()
3942
val bottomUp: mutable.Map[Procedure, Graph] = mutable.Map()
4043
val topDown: mutable.Map[Procedure, Graph] = mutable.Map()
@@ -58,7 +61,6 @@ class DataStructureAnalysis(
5861
private val queue = mutable.Queue[Procedure]()
5962

6063
override def analyze(): Map[Procedure, Graph] = {
61-
NodeCounter.reset()
6264
var domain: Set[Procedure] = Set(program.mainProcedure)
6365
val stack: mutable.Stack[Procedure] = mutable.Stack()
6466
stack.pushAll(program.mainProcedure.calls)

src/main/scala/analysis/data_structure_analysis/Graph.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import scala.util.control.Breaks.{break, breakable}
2626
* @param writesTo
2727
* @param params
2828
*/
29-
class Graph(
29+
class Graph(using NodeCounter)(
3030
val proc: Procedure,
3131
constProp: Map[CFGPosition, Map[Variable, FlatElement[BitVecLiteral]]],
3232
varToSym: Map[CFGPosition, Map[Variable, Set[SymbolicAddress]]],

src/main/scala/analysis/data_structure_analysis/LocalPhase.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import scala.util.control.Breaks.{break, breakable}
2626
* @param params
2727
* mapping from procedures to their parameters
2828
*/
29-
class LocalPhase(
29+
class LocalPhase(using NodeCounter)(
3030
proc: Procedure,
3131
symResults: Map[CFGPosition, Map[SymbolicAddress, TwoElement]],
3232
constProp: Map[CFGPosition, Map[Variable, FlatElement[BitVecLiteral]]],

src/main/scala/analysis/data_structure_analysis/Utility.scala

+19-8
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@ import scala.collection.mutable
1212
import scala.collection.mutable.ArrayBuffer
1313
import scala.util.control.Breaks.{break, breakable}
1414

15-
object NodeCounter {
15+
/**
16+
* An incrementing counter for generating node IDs.
17+
* An instance of this should be constructed and managed
18+
* by the entry point of the DSA. A NodeCounter instance
19+
* cannot be reset and should not be re-used for multiple
20+
* analysis runs (a new instance should be constructed if
21+
* needed).
22+
*
23+
* Generally, this is injected to the classes which require it
24+
* by a "using" parameter.
25+
*/
26+
class NodeCounter {
1627
private var counter: Int = 0
1728

1829
def getCounter: Int = {
1930
counter = counter + 1
2031
counter
2132
}
22-
23-
def reset(): Unit = {
24-
counter = 0
25-
}
2633
}
2734

2835
class Flags() {
@@ -57,7 +64,11 @@ class Flags() {
5764

5865
/** a Data structure Node
5966
*/
60-
class Node(val graph: Option[Graph], var size: BigInt = 0, val id: Int = NodeCounter.getCounter) {
67+
class Node(using nodeCounter: NodeCounter)(
68+
val graph: Option[Graph],
69+
var size: BigInt = 0,
70+
val id: Int = nodeCounter.getCounter
71+
) {
6172

6273
val term: DSAUniTerm = DSAUniTerm(this)
6374
val children: mutable.Map[Node, BigInt] = mutable.Map()
@@ -174,7 +185,7 @@ class Node(val graph: Option[Graph], var size: BigInt = 0, val id: Int = NodeCou
174185
* @param offset
175186
* the offset of the cell
176187
*/
177-
class Cell(val node: Option[Node], val offset: BigInt) {
188+
class Cell(using NodeCounter)(val node: Option[Node], val offset: BigInt) {
178189
var largestAccessedSize: Int = 0
179190

180191
// the cell's pointee
@@ -220,7 +231,7 @@ case class Slice(cell: Cell, internalOffset: BigInt) {
220231
* @param graph
221232
* caller's DSG
222233
*/
223-
class CallSite(val call: DirectCall, val graph: Graph) {
234+
class CallSite(using NodeCounter)(val call: DirectCall, val graph: Graph) {
224235
val proc: Procedure = call.target
225236
val paramCells: mutable.Map[Variable, Slice] =
226237
graph.params(proc).foldLeft(mutable.Map[Variable, Slice]()) { (m, reg) =>

src/main/scala/util/PerformanceTimer.scala

+6-18
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,20 @@
11
package util
22
import scala.collection.mutable
33
import scala.collection
4+
import java.util.concurrent.atomic.AtomicLong
45

56
case class RegionTimer(name: String) {
6-
private var total: Long = 0
7-
private var entered: Long = 0
8-
private var inside: Boolean = false
7+
private val total: AtomicLong = AtomicLong(0)
98

109
def within[T](body: => T): T = {
11-
enter()
10+
val begin = System.currentTimeMillis()
1211
val result = body
13-
exit()
12+
val finish = System.currentTimeMillis()
13+
val _ = total.addAndGet(finish - begin)
1414
result
1515
}
1616

17-
def enter() = {
18-
require(!inside)
19-
inside = true
20-
entered = System.currentTimeMillis()
21-
}
22-
23-
def exit() = {
24-
require(inside)
25-
inside = false
26-
total += (System.currentTimeMillis() - entered)
27-
}
28-
29-
def getTotal() = total
17+
def getTotal(): Long = total.get
3018

3119
override def toString() = {
3220
s"$name : ${getTotal()} (ms)"

src/test/scala/SystemTests.scala

+38-7
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,26 @@ import test_util.BASILTest
1111
import test_util.BASILTest.*
1212
import test_util.Histogram
1313
import test_util.TestConfig
14+
import test_util.LockManager
1415
import test_util.TestCustomisation
1516

1617
/** Add more tests by simply adding them to the programs directory. Refer to the existing tests for the expected
1718
* directory structure and file-name patterns.
1819
*/
1920

20-
trait SystemTests extends AnyFunSuite, BASILTest, Retries, TestCustomisation {
21+
object SystemTests {
22+
23+
/** Locks are shared by all SystemTests instances. */
24+
val locks = LockManager[String]()
25+
}
26+
27+
trait SystemTests extends AnyFunSuite, BASILTest, TestCustomisation {
28+
29+
/**
30+
* A suffix appended to output file names, in order to avoid clashes between test suites.
31+
*/
32+
def testSuiteSuffix = "_" + this.getClass.getSimpleName
33+
2134
case class TestResult(
2235
name: String,
2336
passed: Boolean,
@@ -154,15 +167,23 @@ trait SystemTests extends AnyFunSuite, BASILTest, Retries, TestCustomisation {
154167
def runTest(path: String, name: String, variation: String, conf: TestConfig): Unit = {
155168
val directoryPath = path + "/" + name + "/"
156169
val variationPath = directoryPath + variation + "/" + name
157-
val inputPath = if conf.useBAPFrontend then variationPath + ".adt" else variationPath + ".gts"
158-
val BPLPath = if conf.useBAPFrontend then variationPath + "_bap.bpl" else variationPath + "_gtirb.bpl"
170+
val suiteSuffix = testSuiteSuffix
171+
172+
// input files:
173+
val inputPath = variationPath + (if conf.useBAPFrontend then ".adt" else ".gts")
159174
val specPath = directoryPath + name + ".spec"
160175
val RELFPath = variationPath + ".relf"
161-
val resultPath =
162-
if conf.useBAPFrontend then variationPath + "_bap_result.txt" else variationPath + "_gtirb_result.txt"
163-
val testSuffix = if conf.useBAPFrontend then ":BAP" else ":GTIRB"
176+
177+
// output files:
178+
val lifterString = if conf.useBAPFrontend then s"_bap" else s"_gtirb"
179+
val BPLPath = variationPath + lifterString + suiteSuffix + ".bpl"
180+
val resultPath = variationPath + lifterString + suiteSuffix + "_result.txt"
181+
182+
// reference file:
164183
val expectedOutPath = if conf.useBAPFrontend then variationPath + ".expected" else variationPath + "_gtirb.expected"
165184

185+
val testSuffix = if conf.useBAPFrontend then ":BAP" else ":GTIRB"
186+
166187
Logger.info(s"$name/$variation$testSuffix")
167188
val timer = PerformanceTimer(s"test $name/$variation$testSuffix")
168189
runBASIL(inputPath, RELFPath, Some(specPath), BPLPath, conf.staticAnalysisConfig, conf.simplify)
@@ -227,7 +248,9 @@ trait SystemTests extends AnyFunSuite, BASILTest, Retries, TestCustomisation {
227248
translateTime,
228249
verifyTime
229250
)
230-
testResults.append(result)
251+
testResults.synchronized {
252+
testResults.append(result)
253+
}
231254
}
232255
if (!passed) fail(boogieFailureMsg.get)
233256
}
@@ -250,6 +273,7 @@ trait SystemTests extends AnyFunSuite, BASILTest, Retries, TestCustomisation {
250273

251274
@test_util.tags.StandardSystemTest
252275
class SystemTestsBAP extends SystemTests {
276+
override def testSuiteSuffix = ""
253277
runTests("correct", TestConfig(useBAPFrontend = true, expectVerify = true, checkExpected = true, logResults = true))
254278
runTests(
255279
"incorrect",
@@ -262,6 +286,7 @@ class SystemTestsBAP extends SystemTests {
262286

263287
@test_util.tags.StandardSystemTest
264288
class SystemTestsGTIRB extends SystemTests {
289+
override def testSuiteSuffix = ""
265290
runTests("correct", TestConfig(useBAPFrontend = false, expectVerify = true, checkExpected = true, logResults = true))
266291
runTests(
267292
"incorrect",
@@ -276,6 +301,12 @@ class SystemTestsGTIRB extends SystemTests {
276301
@test_util.tags.StandardSystemTest
277302
class ExtraSpecTests extends SystemTests {
278303

304+
override def customiseTestsByName(name: String) = super.customiseTestsByName(name).orElse {
305+
name match {
306+
case _ => Mode.Retry("timeout issues")
307+
}
308+
}
309+
279310
// some of these tests have time out issues so they need more time, but some still time out even with this for unclear reasons
280311
val boogieFlags = Seq("/timeLimit:30", "/proverOpt:O:smt.array.extensional=false")
281312
runTests(
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package test_util
2+
3+
import scala.collection.mutable.HashMap
4+
5+
/**
6+
* Manages a number of mutex locks, each identified by a key.
7+
* Users should construct an instance of this class and place
8+
* it in a shared location. The `withLock` method takes a lock
9+
* name and executes the body while holding the specified lock.
10+
*
11+
* The key values will be used as HashMap keys, so they must be
12+
* hashable with sensible equality.
13+
*/
14+
class LockManager[T] {
15+
16+
/**
17+
* The map containing the locks. The "locks" are simply AnyRef
18+
* objects which act as locks through the `synchronized` method.
19+
* Additionally, the map object itself is used to synchronise
20+
* reads/writes to the map.
21+
*
22+
* This is a mutable map.
23+
*/
24+
private val locksMap: HashMap[T, AnyRef] = HashMap()
25+
26+
/**
27+
* Executes the given body while holding the given lock.
28+
* The named lock will be created if needed.
29+
*
30+
* Usage:
31+
*
32+
* val manager = LockManager[String]()
33+
*
34+
* // ...
35+
*
36+
* manager.withLock("lock name") {
37+
* println("this code will hold the lock!")
38+
* }
39+
*/
40+
def withLock(key: T)(body: => Unit): Unit = {
41+
val lock = locksMap.synchronized {
42+
locksMap.getOrElseUpdate(key, Object())
43+
}
44+
45+
lock.synchronized {
46+
body
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)