<lambda>null1 package com.android.testutils
2
3 import android.os.SystemClock
4 import java.util.concurrent.CyclicBarrier
5 import kotlin.system.measureTimeMillis
6 import kotlin.test.assertEquals
7 import kotlin.test.assertFails
8 import kotlin.test.assertNull
9 import kotlin.test.assertTrue
10
11 // The table contains pairs associating a regexp with the code to run. The statement is matched
12 // against each matcher in sequence and when a match is found the associated code is run, passing
13 // it the TrackRecord under test and the result of the regexp match.
14 typealias InterpretMatcher<T> = Pair<Regex, (ConcurrentInterpreter<T>, T, MatchResult) -> Any?>
15
16 // The default unit of time for interpreted tests
17 val INTERPRET_TIME_UNIT = 40L // ms
18
19 /**
20 * A small interpreter for testing parallel code. The interpreter will read a list of lines
21 * consisting of "|"-separated statements. Each column runs in a different concurrent thread
22 * and all threads wait for each other in between lines. Each statement is split on ";" then
23 * matched with regular expressions in the instructionTable constant, which contains the
24 * code associated with each statement. The interpreter supports an object being passed to
25 * the interpretTestSpec() method to be passed in each lambda (think about the object under
26 * test), and an optional transform function to be executed on the object at the start of
27 * every thread.
28 *
29 * The time unit is defined in milliseconds by the interpretTimeUnit member, which has a default
30 * value but can be passed to the constructor. Whitespace is ignored.
31 *
32 * The interpretation table has to be passed as an argument. It's a table associating a regexp
33 * with the code that should execute, as a function taking three arguments : the interpreter,
34 * the regexp match, and the object. See the individual tests for the DSL of that test.
35 * Implementors for new interpreting languages are encouraged to look at the defaultInterpretTable
36 * constant below for an example of how to write an interpreting table.
37 * Some expressions already exist by default and can be used by all interpreters. They include :
38 * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
39 * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
40 * string "null" or an int. Returns Unit.
41 * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
42 * y time units.
43 * EXPR // any text : comments are ignored.
44 */
45 open class ConcurrentInterpreter<T>(
46 localInterpretTable: List<InterpretMatcher<T>>,
47 val interpretTimeUnit: Long = INTERPRET_TIME_UNIT
48 ) {
49 private val interpretTable: List<InterpretMatcher<T>> =
50 localInterpretTable + getDefaultInstructions()
51
52 // Split the line into multiple statements separated by ";" and execute them. Return whatever
53 // the last statement returned.
54 fun interpretMultiple(instr: String, r: T): Any? {
55 return instr.split(";").map { interpret(it.trim(), r) }.last()
56 }
57
58 // Match the statement to a regex and interpret it.
59 fun interpret(instr: String, r: T): Any? {
60 val (matcher, code) =
61 interpretTable.find { instr matches it.first } ?: throw SyntaxException(instr)
62 val match = matcher.matchEntire(instr) ?: throw SyntaxException(instr)
63 return code(this, r, match)
64 }
65
66 // Spins as many threads as needed by the test spec and interpret each program concurrently,
67 // having all threads waiting on a CyclicBarrier after each line.
68 // |lineShift| says how many lines after the call the spec starts. This is used for error
69 // reporting. Unfortunately AFAICT there is no way to get the line of an argument rather
70 // than the line at which the expression starts.
71 fun interpretTestSpec(
72 spec: String,
73 initial: T,
74 lineShift: Int = 0,
75 threadTransform: (T) -> T = { it }
76 ) {
77 // For nice stack traces
78 val callSite = getCallingMethod()
79 val lines = spec.trim().trim('\n').split("\n").map { it.split("|") }
80 // |threads| contains arrays of strings that make up the statements of a thread : in other
81 // words, it's an array that contains a list of statements for each column in the spec.
82 val threadCount = lines[0].size
83 assertTrue(lines.all { it.size == threadCount })
84 val threadInstructions = (0 until threadCount).map { i -> lines.map { it[i].trim() } }
85 val barrier = CyclicBarrier(threadCount)
86 var crash: InterpretException? = null
87 threadInstructions.mapIndexed { threadIndex, instructions ->
88 Thread {
89 val threadLocal = threadTransform(initial)
90 barrier.await()
91 var lineNum = 0
92 instructions.forEach {
93 if (null != crash) return@Thread
94 lineNum += 1
95 try {
96 interpretMultiple(it, threadLocal)
97 } catch (e: Throwable) {
98 // If fail() or some exception was called, the thread will come here ; if
99 // the exception isn't caught the process will crash, which is not nice for
100 // testing. Instead, catch the exception, cancel other threads, and report
101 // nicely. Catch throwable because fail() is AssertionError, which inherits
102 // from Error.
103 crash = InterpretException(threadIndex, it,
104 callSite.lineNumber + lineNum + lineShift,
105 callSite.className, callSite.methodName, callSite.fileName, e)
106 }
107 barrier.await()
108 }
109 }.also { it.start() }
110 }.forEach { it.join() }
111 // If the test failed, crash with line number
112 crash?.let { throw it }
113 }
114
115 // Helper to get the stack trace for a calling method
116 private fun getCallingStackTrace(): Array<StackTraceElement> {
117 try {
118 throw RuntimeException()
119 } catch (e: RuntimeException) {
120 return e.stackTrace
121 }
122 }
123
124 // Find the calling method. This is the first method in the stack trace that is annotated
125 // with @Test.
126 fun getCallingMethod(): StackTraceElement {
127 val stackTrace = getCallingStackTrace()
128 return stackTrace.find { element ->
129 val clazz = Class.forName(element.className)
130 // Because the stack trace doesn't list the formal arguments, find all methods with
131 // this name and return this name if any of them is annotated with @Test.
132 clazz.declaredMethods
133 .filter { method -> method.name == element.methodName }
134 .any { method -> method.getAnnotation(org.junit.Test::class.java) != null }
135 } ?: stackTrace[3]
136 // If no method is annotated return the 4th one, because that's what it usually is :
137 // 0 is getCallingStackTrace, 1 is this method, 2 is ConcurrentInterpreter#interpretTestSpec
138 }
139 }
140
getDefaultInstructionsnull141 private fun <T> getDefaultInstructions() = listOf<InterpretMatcher<T>>(
142 // Interpret an empty line as doing nothing.
143 Regex("") to { _, _, _ -> null },
144 // Ignore comments.
rnull145 Regex("(.*)//.*") to { i, t, r -> i.interpret(r.strArg(1), t) },
146 // Interpret "XXX time x..y" : run XXX and check it took at least x and not more than y
rnull147 Regex("""(.*)\s*time\s*(\d+)\.\.(\d+)""") to { i, t, r ->
148 val time = measureTimeMillis { i.interpret(r.strArg(1), t) }
149 assertTrue(time in r.timeArg(2)..r.timeArg(3), "$time not in ${r.timeArg(2)..r.timeArg(3)}")
150 },
151 // Interpret "XXX = YYY" : run XXX and assert its return value is equal to YYY. "null" supported
rnull152 Regex("""(.*)\s*=\s*(null|\d+)""") to { i, t, r ->
153 i.interpret(r.strArg(1), t).also {
154 if ("null" == r.strArg(2)) assertNull(it) else assertEquals(r.intArg(2), it)
155 }
156 },
157 // Interpret sleep. Optional argument for the count, in INTERPRET_TIME_UNIT units.
rnull158 Regex("""sleep(\((\d+)\))?""") to { i, t, r ->
159 SystemClock.sleep(if (r.strArg(2).isEmpty()) i.interpretTimeUnit else r.timeArg(2))
160 },
rnull161 Regex("""(.*)\s*fails""") to { i, t, r ->
162 assertFails { i.interpret(r.strArg(1), t) }
163 }
164 )
165
166 class SyntaxException(msg: String, cause: Throwable? = null) : RuntimeException(msg, cause)
167 class InterpretException(
168 threadIndex: Int,
169 instr: String,
170 lineNum: Int,
171 className: String,
172 methodName: String,
173 fileName: String,
174 cause: Throwable
175 ) : RuntimeException("Failure: $instr", cause) {
176 init {
177 stackTrace = arrayOf(StackTraceElement(
178 className,
179 "$methodName:thread$threadIndex",
180 fileName,
181 lineNum)) + super.getStackTrace()
182 }
183 }
184
185 // Some small helpers to avoid to say the large ".groupValues[index].trim()" every time
strArgnull186 fun MatchResult.strArg(index: Int) = this.groupValues[index].trim()
187 fun MatchResult.intArg(index: Int) = strArg(index).toInt()
188 fun MatchResult.timeArg(index: Int) = INTERPRET_TIME_UNIT * intArg(index)
189