@@ -13,7 +13,10 @@ package org.jetbrains.kotlinx.lincheck
13
13
14
14
import sun.nio.ch.lincheck.*
15
15
import org.jetbrains.kotlinx.lincheck.runner.*
16
+ import org.jetbrains.kotlinx.lincheck.strategy.*
17
+ import org.jetbrains.kotlinx.lincheck.strategy.managed.*
16
18
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.*
19
+ import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult
17
20
import org.jetbrains.kotlinx.lincheck.util.ThreadMap
18
21
19
22
const val MINIMAL_PLUGIN_VERSION = " 0.2"
@@ -22,18 +25,17 @@ const val MINIMAL_PLUGIN_VERSION = "0.2"
22
25
23
26
/* *
24
27
* Invoked from the strategy [ModelCheckingStrategy] when Lincheck finds a bug.
25
- * The debugger creates a breakpoint on this method, so when it's called, the debugger receives all the information about the
26
- * failed test.
28
+ * The debugger creates a breakpoint on this method, so when it's called,
29
+ * the debugger receives all the information about the failed test.
27
30
* When a failure is found this method is called to provide all required information (trace points, failure type),
28
31
* then [beforeEvent] method is called on each trace point.
29
32
*
30
- * @param failureType string representation of the failure type.
31
- * (`INCORRECT_RESULTS`, `OBSTRUCTION_FREEDOM_VIOLATION`, `UNEXPECTED_EXCEPTION`, `VALIDATION_FAILURE`, `DEADLOCK` or `INTERNAL_BUG`).
33
+ * @param failureType string representation of the failure type (see [LincheckFailure.type]).
32
34
* @param trace failed test trace, where each trace point is represented as a string
33
- * (because it's the easiest way to provide some information to the debugger).
34
- * @param version current Lincheck version
35
- * @param minimalPluginVersion minimal compatible plugin version
36
- * @param exceptions representation of the exceptions with their stacktrace occurred during the execution
35
+ * (because it's the easiest way to provide some information to the debugger).
36
+ * @param version current Lincheck version.
37
+ * @param minimalPluginVersion minimal compatible plugin version.
38
+ * @param exceptions representation of the exceptions with their stacktrace occurred during the execution.
37
39
*/
38
40
@Suppress(" UNUSED_PARAMETER" )
39
41
fun testFailed (
@@ -42,8 +44,7 @@ fun testFailed(
42
44
version : String? ,
43
45
minimalPluginVersion : String ,
44
46
exceptions : Array <String >
45
- ) {
46
- }
47
+ ) {}
47
48
48
49
/* *
49
50
* Debugger replaces the result of this method to `true` if idea plugin is enabled.
@@ -61,8 +62,8 @@ fun ideaPluginEnabled(): Boolean {
61
62
fun lincheckVerificationStarted () {}
62
63
63
64
/* *
64
- * If the debugger needs to replay the execution (due to earlier trace point selection), it replaces the result of this
65
- * method to `true`.
65
+ * If the debugger needs to replay the execution (due to earlier trace point selection),
66
+ * it replaces the result of this method to `true`.
66
67
*/
67
68
fun shouldReplayInterleaving (): Boolean {
68
69
return false // should be replaced with `true` to replay the failure
@@ -71,7 +72,8 @@ fun shouldReplayInterleaving(): Boolean {
71
72
/* *
72
73
* This method is called on every trace point shown to the user,
73
74
* but before the actual event, such as the read/write/MONITORENTER/MONITOREXIT/, etc.
74
- * The Debugger creates a breakpoint inside this method and if [eventId] is the selected one, the breakpoint is triggered.
75
+ * The Debugger creates a breakpoint inside this method and if [eventId] is the selected one,
76
+ * the breakpoint is triggered.
75
77
* Then the debugger performs step-out action, so we appear in the user's code.
76
78
* That's why this method **must** be called from a user-code, not from a nested function.
77
79
*
@@ -80,18 +82,18 @@ fun shouldReplayInterleaving(): Boolean {
80
82
*/
81
83
@Suppress(" UNUSED_PARAMETER" )
82
84
fun beforeEvent (eventId : Int , type : String ) {
83
- val strategy = ThreadDescriptor .getCurrentThreadDescriptor()?.eventTracker
84
- ? : return
85
+ val threadDescriptor = ThreadDescriptor .getCurrentThreadDescriptor() ? : return
86
+ val strategy = threadDescriptor.eventTracker as ? ManagedStrategy ? : return
85
87
visualize(strategy)
86
88
}
87
89
88
90
89
91
/* *
90
92
* This method receives all information about the test object instance to visualize.
91
- * The Debugger creates a breakpoint inside this method and uses this method parameters to create the diagram.
93
+ * The Debugger creates a breakpoint inside this method and uses its parameters to create the diagram.
92
94
*
93
- * We pass Maps as Arrays due to difficulties with passing objects (java.util.Map) to the debugger
94
- * (class version, etc.).
95
+ * We pass Maps as Arrays due to difficulties with passing objects (java.util.Map)
96
+ * to the debugger (class version, etc.).
95
97
*
96
98
* @param testInstance tested data structure.
97
99
* @param numbersArrayMap an array structured like [Object, objectNumber, Object, objectNumber, ...].
@@ -107,8 +109,7 @@ fun visualizeInstance(
107
109
numbersArrayMap : Array <Any >,
108
110
continuationToLincheckThreadIdMap : Array <Any >,
109
111
threadToLincheckThreadIdMap : Array <Any >
110
- ) {
111
- }
112
+ ) {}
112
113
113
114
/* *
114
115
* The Debugger creates a breakpoint on this method call to know when the thread is switched.
@@ -125,8 +126,198 @@ fun onThreadSwitchesOrActorFinishes() {}
125
126
*/
126
127
internal val eventIdStrictOrderingCheck = System .getProperty(" lincheck.debug.withEventIdSequentialCheck" ) != null
127
128
128
- private fun visualize (strategyObject : Any ) = runCatching {
129
- val strategy = strategyObject as ModelCheckingStrategy
129
+ /* *
130
+ * If the plugin enabled and the failure has a trace, passes information about
131
+ * the trace and the failure to the Plugin and run re-run execution to debug it.
132
+ */
133
+ internal fun ManagedStrategy.runReplayIfPluginEnabled (failure : LincheckFailure ) {
134
+ if (inIdeaPluginReplayMode && failure.trace != null ) {
135
+ // Extract trace representation in the appropriate view.
136
+ val trace = constructTraceForPlugin(failure, failure.trace)
137
+ // Collect and analyze the exceptions thrown.
138
+ val (exceptionsRepresentation, internalBugOccurred) = collectExceptionsForPlugin(failure)
139
+ // If an internal bug occurred - print it on the console, no need to debug it.
140
+ if (internalBugOccurred) return
141
+ // Provide all information about the failed test to the debugger.
142
+ testFailed(
143
+ failureType = failure.type,
144
+ trace = trace,
145
+ version = lincheckVersion,
146
+ minimalPluginVersion = MINIMAL_PLUGIN_VERSION ,
147
+ exceptions = exceptionsRepresentation
148
+ )
149
+ // Replay execution while it's needed.
150
+ do {
151
+ doReplay()
152
+ } while (shouldReplayInterleaving())
153
+ }
154
+ }
155
+
156
+ /* *
157
+ * Transforms failure trace to the array of string to pass it to the debugger.
158
+ * (due to difficulties with passing objects like List and TracePoint, as class versions may vary)
159
+ *
160
+ * Each trace point is transformed into the line of the following form:
161
+ * `type,iThread,callDepth,shouldBeExpanded,eventId,representation`.
162
+ *
163
+ * Later, when [testFailed] breakpoint is triggered debugger parses these lines back to trace points.
164
+ *
165
+ * To help the plugin to create an execution view, we provide a type for each trace point.
166
+ * Below are the codes of trace point types.
167
+ *
168
+ * | Value | Code |
169
+ * |--------------------------------|------|
170
+ * | REGULAR | 0 |
171
+ * | ACTOR | 1 |
172
+ * | RESULT | 2 |
173
+ * | SWITCH | 3 |
174
+ * | SPIN_CYCLE_START | 4 |
175
+ * | SPIN_CYCLE_SWITCH | 5 |
176
+ * | OBSTRUCTION_FREEDOM_VIOLATION | 6 |
177
+ */
178
+ private fun constructTraceForPlugin (failure : LincheckFailure , trace : Trace ): Array <String > {
179
+ val nThreads = trace.trace.maxOf { it.iThread } + 1
180
+ val results = failure.results
181
+ val nodesList = constructTraceGraph(nThreads, failure, results, trace, collectExceptionsOrEmpty(failure))
182
+ var sectionIndex = 0
183
+ var node: TraceNode ? = nodesList.firstOrNull()
184
+ val representations = mutableListOf<String >()
185
+ while (node != null ) {
186
+ when (node) {
187
+ is TraceLeafEvent -> {
188
+ val event = node.event
189
+ val eventId = event.eventId
190
+ val representation = event.toStringImpl(withLocation = false )
191
+ val type = when (event) {
192
+ is SwitchEventTracePoint -> {
193
+ when (event.reason) {
194
+ SwitchReason .ActiveLock -> {
195
+ 5
196
+ }
197
+ else -> 3
198
+ }
199
+ }
200
+ is SpinCycleStartTracePoint -> 4
201
+ is ObstructionFreedomViolationExecutionAbortTracePoint -> 6
202
+ else -> 0
203
+ }
204
+
205
+ if (representation.isNotEmpty()) {
206
+ representations.add(" $type ;${node.iThread} ;${node.callDepth} ;${node.shouldBeExpanded(false )} ;${eventId} ;${representation} " )
207
+ }
208
+ }
209
+
210
+ is CallNode -> {
211
+ val beforeEventId = node.call.eventId
212
+ val representation = node.call.toStringImpl(withLocation = false )
213
+ if (representation.isNotEmpty()) {
214
+ representations.add(" 0;${node.iThread} ;${node.callDepth} ;${node.shouldBeExpanded(false )} ;${beforeEventId} ;${representation} " )
215
+ }
216
+ }
217
+
218
+ is ActorNode -> {
219
+ val beforeEventId = - 1
220
+ val representation = node.actorRepresentation
221
+ if (representation.isNotEmpty()) {
222
+ representations.add(" 1;${node.iThread} ;${node.callDepth} ;${node.shouldBeExpanded(false )} ;${beforeEventId} ;${representation} " )
223
+ }
224
+ }
225
+
226
+ is ActorResultNode -> {
227
+ val beforeEventId = - 1
228
+ val representation = node.resultRepresentation.toString()
229
+ representations.add(" 2;${node.iThread} ;${node.callDepth} ;${node.shouldBeExpanded(false )} ;${beforeEventId} ;${representation} ;${node.exceptionNumberIfExceptionResult ? : - 1 } " )
230
+ }
231
+
232
+ else -> {}
233
+ }
234
+
235
+ node = node.next
236
+ if (node == null && sectionIndex != nodesList.lastIndex) {
237
+ node = nodesList[++ sectionIndex]
238
+ }
239
+ }
240
+ return representations.toTypedArray()
241
+ }
242
+
243
+ /* *
244
+ * We provide information about the failure type to the Plugin, but
245
+ * due to difficulties with passing objects like LincheckFailure (as class versions may vary),
246
+ * we use its string representation.
247
+ * The Plugin uses this information to show the failure type to a user.
248
+ */
249
+ private val LincheckFailure .type: String
250
+ get() = when (this ) {
251
+ is IncorrectResultsFailure -> " INCORRECT_RESULTS"
252
+ is ObstructionFreedomViolationFailure -> " OBSTRUCTION_FREEDOM_VIOLATION"
253
+ is UnexpectedExceptionFailure -> " UNEXPECTED_EXCEPTION"
254
+ is ValidationFailure -> " VALIDATION_FAILURE"
255
+ is ManagedDeadlockFailure , is TimeoutFailure -> " DEADLOCK"
256
+ }
257
+
258
+ /* *
259
+ * Processes the exceptions thrown during the execution.
260
+ * @return exceptions string representation to pass to the plugin with a flag,
261
+ * indicating if an internal bug was the cause of the failure, or not.
262
+ */
263
+ private fun collectExceptionsForPlugin (failure : LincheckFailure ): ExceptionProcessingResult {
264
+ val results: ExecutionResult = when (failure) {
265
+ is IncorrectResultsFailure -> failure.results
266
+ is ValidationFailure -> {
267
+ return ExceptionProcessingResult (arrayOf(failure.exception.text), isInternalBugOccurred = false )
268
+ }
269
+ else -> {
270
+ return ExceptionProcessingResult (emptyArray(), isInternalBugOccurred = false )
271
+ }
272
+ }
273
+ return when (val exceptionsProcessingResult = collectExceptionStackTraces(results)) {
274
+ // If some exception was thrown from the Lincheck itself, we'll ask for bug reporting
275
+ is InternalLincheckBugResult ->
276
+ ExceptionProcessingResult (arrayOf(exceptionsProcessingResult.exception.text), isInternalBugOccurred = true )
277
+ // Otherwise collect all the exceptions
278
+ is ExceptionStackTracesResult -> {
279
+ exceptionsProcessingResult.exceptionStackTraces.entries
280
+ .sortedBy { (_, numberAndStackTrace) -> numberAndStackTrace.number }
281
+ .map { (exception, numberAndStackTrace) ->
282
+ val header = exception::class .java.canonicalName + " : " + exception.message
283
+ header + numberAndStackTrace.stackTrace.joinToString(" " ) { " \n\t at $it " }
284
+ }
285
+ .let { ExceptionProcessingResult (it.toTypedArray(), isInternalBugOccurred = false ) }
286
+ }
287
+ }
288
+ }
289
+
290
+ private fun collectExceptionsOrEmpty (failure : LincheckFailure ): Map <Throwable , ExceptionNumberAndStacktrace > {
291
+ if (failure is ValidationFailure ) {
292
+ return mapOf (failure.exception to ExceptionNumberAndStacktrace (1 , failure.exception.stackTrace.toList()))
293
+ }
294
+ val results = (failure as ? IncorrectResultsFailure )?.results ? : return emptyMap()
295
+ return when (val result = collectExceptionStackTraces(results)) {
296
+ is ExceptionStackTracesResult -> result.exceptionStackTraces
297
+ is InternalLincheckBugResult -> emptyMap()
298
+ }
299
+ }
300
+
301
+ /* *
302
+ * Result of creating string representations of exceptions
303
+ * thrown during the execution before passing them to the plugin.
304
+ *
305
+ * @param exceptionsRepresentation string representation of all the exceptions
306
+ * @param isInternalBugOccurred a flag indicating that the exception is caused by a bug in the Lincheck.
307
+ */
308
+ @Suppress(" ArrayInDataClass" )
309
+ private data class ExceptionProcessingResult (
310
+ val exceptionsRepresentation : Array <String >,
311
+ val isInternalBugOccurred : Boolean
312
+ )
313
+
314
+ /* *
315
+ * Collects all the necessary data to pass to the debugger plugin and calls [visualizeInstance].
316
+ *
317
+ * @param strategy The managed strategy used to obtain data to be passed into the debugger plugin.
318
+ * Used to collect the data about the test instance, object numbers, threads, and continuations.
319
+ */
320
+ private fun visualize (strategy : ManagedStrategy ) = runCatching {
130
321
val runner = strategy.runner as ParallelThreadsRunner
131
322
val testObject = runner.testInstance
132
323
val lincheckThreads = runner.executor.threads
@@ -139,7 +330,6 @@ private fun visualize(strategyObject: Any) = runCatching {
139
330
visualizeInstance(testObject, objectToNumberMap, continuationToLincheckThreadIdMap, threadToLincheckThreadIdMap)
140
331
}
141
332
142
-
143
333
/* *
144
334
* Creates an array [Object, objectNumber, Object, objectNumber, ...].
145
335
* It represents a `Map<Any, Int>`, but due to difficulties with passing objects (Map)
@@ -149,7 +339,6 @@ private fun visualize(strategyObject: Any) = runCatching {
149
339
*/
150
340
private fun createObjectToNumberMapAsArray (testObject : Any ): Array <Any > {
151
341
val resultArray = arrayListOf<Any >()
152
-
153
342
val numbersMap = enumerateObjects(testObject)
154
343
numbersMap.forEach { (any, objectNumber) ->
155
344
resultArray.add(any)
0 commit comments