<lambda>null1 package com.android.testutils
2 
3 import android.os.SystemClock
4 import java.util.concurrent.CyclicBarrier
5 import kotlin.test.assertEquals
6 import kotlin.test.assertFails
7 import kotlin.test.assertNull
8 import kotlin.test.assertTrue
9 
10 // The table contains pairs associating a regexp with the code to run. The statement is matched
11 // against each matcher in sequence and when a match is found the associated code is run, passing
12 // it the TrackRecord under test and the result of the regexp match.
13 typealias InterpretMatcher<T> = Pair<Regex, (ConcurrentInterpreter<T>, T, MatchResult) -> Any?>
14 
15 // The default unit of time for interpreted tests
16 const val INTERPRET_TIME_UNIT = 60L // ms
17 
18 /**
19  * A small interpreter for testing parallel code.
20  *
21  * The interpreter will read a list of lines consisting of "|"-separated statements, e.g. :
22  *   sleep 2 ; unblock thread2 | wait thread2 time 2..5
23  *   sendMessage "x"           | obtainMessage = "x" time 0..1
24  *
25  * Each column runs in a different concurrent thread and all threads wait for each other in
26  * between lines. Each statement is split on ";" then matched with regular expressions in the
27  * instructionTable constant, which contains the code associated with each statement. The
28  * interpreter supports an object being passed to the interpretTestSpec() method to be passed
29  * in each lambda (think about the object under test), and an optional transform function to be
30  * executed on the object at the start of every thread.
31  *
32  * The time unit is defined in milliseconds by the interpretTimeUnit member, which has a default
33  * value but can be passed to the constructor. Whitespace is ignored.
34  *
35  * The interpretation table has to be passed as an argument. It's a table associating a regexp
36  * with the code that should execute, as a function taking three arguments : the interpreter,
37  * the regexp match, and the object. See the individual tests for the DSL of that test.
38  * Implementors for new interpreting languages are encouraged to look at the defaultInterpretTable
39  * constant below for an example of how to write an interpreting table.
40  * Some expressions already exist by default and can be used by all interpreters. Refer to
41  * getDefaultInstructions() below for a list and documentation.
42  */
43 open class ConcurrentInterpreter<T>(localInterpretTable: List<InterpretMatcher<T>>) {
44     private val interpretTable: List<InterpretMatcher<T>> =
45             localInterpretTable + getDefaultInstructions()
46     // The last time the thread became blocked, with base System.currentTimeMillis(). This should
47     // be set immediately before any time the thread gets blocked.
48     internal val lastBlockedTime = ThreadLocal<Long>()
49 
50     // Split the line into multiple statements separated by ";" and execute them. Return whatever
51     // the last statement returned.
52     fun interpretMultiple(instr: String, r: T): Any? {
53         return instr.split(";").map { interpret(it.trim(), r) }.last()
54     }
55 
56     // Match the statement to a regex and interpret it.
57     fun interpret(instr: String, r: T): Any? {
58         val (matcher, code) =
59                 interpretTable.find { instr matches it.first } ?: throw SyntaxException(instr)
60         val match = matcher.matchEntire(instr) ?: throw SyntaxException(instr)
61         return code(this, r, match)
62     }
63 
64     /**
65      * Spins as many threads as needed by the test spec and interpret each program concurrently.
66      *
67      * All threads wait 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      *
72      * This method is mostly meant for implementations that extend the ConcurrentInterpreter
73      * class to add their own directives and instructions. These may need to operate on some
74      * data, which can be passed in |initial|. For example, an interpreter specialized in callbacks
75      * may want to pass the callback there. In some cases, it's necessary that each thread
76      * performs a transformation *after* it starts on that value before starting ; in this case,
77      * the transformation can be passed to |threadTransform|. The default is to return |initial| as
78      * is. Look at some existing child classes of this interpreter for some examples of how this
79      * can be used.
80      *
81      * @param spec The test spec, as a string of lines separated by pipes.
82      * @param initial An initial value passed to all threads.
83      * @param lineShift How many lines after the call the spec starts, for error reporting.
84      * @param threadTransform an optional transformation that each thread will apply to |initial|
85      */
86     fun interpretTestSpec(
87         spec: String,
88         initial: T,
89         lineShift: Int = 0,
90         threadTransform: (T) -> T = { it }
91     ) {
92         // For nice stack traces
93         val callSite = getCallingMethod()
94         val lines = spec.trim().trim('\n').split("\n").map { it.split("|") }
95         // |lines| contains arrays of strings that make up the statements of a thread : in other
96         // words, it's an array that contains a list of statements for each column in the spec.
97         // E.g. if the string is """
98         //   a | b | c
99         //   d | e | f
100         // """, then lines is [ [ "a", "b", "c" ], [ "d", "e", "f" ] ].
101         val threadCount = lines[0].size
102         assertTrue(lines.all { it.size == threadCount })
103         val threadInstructions = (0 until threadCount).map { i -> lines.map { it[i].trim() } }
104         // |threadInstructions| is a list where each element is the list of instructions for the
105         // thread at the index. In other words, it's just |lines| transposed. In the example
106         // above, it would be [ [ "a", "d" ], [ "b", "e" ], [ "c", "f" ] ]
107         // mapIndexed below will pass in |instructions| the list of instructions for this thread.
108         val barrier = CyclicBarrier(threadCount)
109         var crash: InterpretException? = null
110         threadInstructions.mapIndexed { threadIndex, instructions ->
111             Thread {
112                 val threadLocal = threadTransform(initial)
113                 lastBlockedTime.set(System.currentTimeMillis())
114                 barrier.await()
115                 var lineNum = 0
116                 instructions.forEach {
117                     if (null != crash) return@Thread
118                     lineNum += 1
119                     try {
120                         interpretMultiple(it, threadLocal)
121                     } catch (e: Throwable) {
122                         // If fail() or some exception was called, the thread will come here ; if
123                         // the exception isn't caught the process will crash, which is not nice for
124                         // testing. Instead, catch the exception, cancel other threads, and report
125                         // nicely. Catch throwable because fail() is AssertionError, which inherits
126                         // from Error.
127                         crash = InterpretException(threadIndex, it,
128                                 callSite.lineNumber + lineNum + lineShift,
129                                 callSite.className, callSite.methodName, callSite.fileName, e)
130                     }
131                     lastBlockedTime.set(System.currentTimeMillis())
132                     barrier.await()
133                 }
134             }.also { it.start() }
135         }.forEach { it.join() }
136         // If the test failed, crash with line number
137         crash?.let { throw it }
138     }
139 
140     // Helper to get the stack trace for a calling method
141     private fun getCallingStackTrace(): Array<StackTraceElement> {
142         try {
143             throw RuntimeException()
144         } catch (e: RuntimeException) {
145             return e.stackTrace
146         }
147     }
148 
149     // Find the calling method. This is the first method in the stack trace that is annotated
150     // with @Test.
151     fun getCallingMethod(): StackTraceElement {
152         val stackTrace = getCallingStackTrace()
153         return stackTrace.find { element ->
154             val clazz = Class.forName(element.className)
155             // Because the stack trace doesn't list the formal arguments, find all methods with
156             // this name and return this name if any of them is annotated with @Test.
157             clazz.declaredMethods
158                     .filter { method -> method.name == element.methodName }
159                     .any { method -> method.getAnnotation(org.junit.Test::class.java) != null }
160         } ?: stackTrace[3]
161         // If no method is annotated return the 4th one, because that's what it usually is :
162         // 0 is getCallingStackTrace, 1 is this method, 2 is ConcurrentInterpreter#interpretTestSpec
163     }
164 }
165 
166 /**
167  * Default instructions available to all interpreters.
168  * sleep(x) : sleeps for x time units and returns Unit ; sleep alone means sleep(1)
169  * EXPR = VALUE : asserts that EXPR equals VALUE. EXPR is interpreted. VALUE can either be the
170  *   string "null" or an int. Returns Unit.
171  * EXPR time x..y : measures the time taken by EXPR and asserts it took at least x and at most
172  *   y time units.
173  * EXPR // any text : comments are ignored.
174  * EXPR fails : checks that EXPR throws some exception.
175  */
getDefaultInstructionsnull176 private fun <T> getDefaultInstructions() = listOf<InterpretMatcher<T>>(
177     // Interpret an empty line as doing nothing.
178     Regex("") to { _, _, _ -> null },
179     // Ignore comments.
rnull180     Regex("(.*)//.*") to { i, t, r -> i.interpret(r.strArg(1), t) },
181     // Interpret "XXX time x..y" : run XXX and check it took at least x and not more than y
rnull182     Regex("""(.*)\s*time\s*(\d+)\.\.(\d+)""") to { i, t, r ->
183         val lateStart = System.currentTimeMillis()
184         i.interpret(r.strArg(1), t)
185         val end = System.currentTimeMillis()
186         // There is uncertainty in measuring time.
187         // It takes some (small) time for the thread to even measure the time at which it
188         // starts interpreting the instruction. It is therefore possible that thread A sleeps for
189         // n milliseconds, and B expects to have waited for at least n milliseconds, but because
190         // B started measuring after 1ms or so, B thinks it didn't wait long enough.
191         // To avoid this, when the `time` instruction tests the instruction took at least X and
192         // at most Y, it tests X against a time measured since *before* the thread blocked but
193         // Y against a time measured as late as possible. This ensures that the timer is
194         // sufficiently lenient in both directions that there are no flaky measures.
195         val minTime = end - lateStart
196         val maxTime = end - i.lastBlockedTime.get()!!
197 
198         assertTrue(maxTime >= r.timeArg(2),
199                 "Should have taken at least ${r.timeArg(2)} but took less than $maxTime")
200         assertTrue(minTime <= r.timeArg(3),
201                 "Should have taken at most ${r.timeArg(3)} but took more than $minTime")
202     },
203     // Interpret "XXX = YYY" : run XXX and assert its return value is equal to YYY. "null" supported
rnull204     Regex("""(.*)\s*=\s*(null|\d+)""") to { i, t, r ->
205         i.interpret(r.strArg(1), t).also {
206             if ("null" == r.strArg(2)) assertNull(it) else assertEquals(r.intArg(2), it)
207         }
208     },
209     // Interpret sleep. Optional argument for the count, in INTERPRET_TIME_UNIT units.
rnull210     Regex("""sleep(\((\d+)\))?""") to { i, t, r ->
211         SystemClock.sleep(if (r.strArg(2).isEmpty()) INTERPRET_TIME_UNIT else r.timeArg(2))
212     },
rnull213     Regex("""(.*)\s*fails""") to { i, t, r ->
214         assertFails { i.interpret(r.strArg(1), t) }
215     }
216 )
217 
218 class SyntaxException(msg: String, cause: Throwable? = null) : RuntimeException(msg, cause)
219 class InterpretException(
220     threadIndex: Int,
221     instr: String,
222     lineNum: Int,
223     className: String,
224     methodName: String,
225     fileName: String,
226     cause: Throwable
227 ) : RuntimeException("Failure: $instr", cause) {
228     init {
229         stackTrace = arrayOf(StackTraceElement(
230                 className,
231                 "$methodName:thread$threadIndex",
232                 fileName,
233                 lineNum)) + super.getStackTrace()
234     }
235 }
236 
237 // Some small helpers to avoid to say the large ".groupValues[index].trim()" every time
strArgnull238 fun MatchResult.strArg(index: Int) = this.groupValues[index].trim()
239 fun MatchResult.intArg(index: Int) = strArg(index).toInt()
240 fun MatchResult.timeArg(index: Int) = INTERPRET_TIME_UNIT * intArg(index)
241