Chalard Jean | 48c6c7d | 2020-06-25 23:39:15 +0900 | [diff] [blame] | 1 | 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 | |
| 141 | private fun <T> getDefaultInstructions() = listOf<InterpretMatcher<T>>( |
| 142 | // Interpret an empty line as doing nothing. |
| 143 | Regex("") to { _, _, _ -> null }, |
| 144 | // Ignore comments. |
| 145 | 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 |
| 147 | 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 |
| 152 | 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. |
| 158 | Regex("""sleep(\((\d+)\))?""") to { i, t, r -> |
| 159 | SystemClock.sleep(if (r.strArg(2).isEmpty()) i.interpretTimeUnit else r.timeArg(2)) |
| 160 | }, |
| 161 | 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 |
| 186 | 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) |