<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