Skip to content

Commit 8c4ef91

Browse files
authored
Move Intellij plugin related functionality into IdeaPlugin.kt (#465)
--------- Signed-off-by: Evgeniy Moiseenko <[email protected]>
1 parent da422fb commit 8c4ef91

File tree

5 files changed

+244
-235
lines changed

5 files changed

+244
-235
lines changed

src/jvm/main/org/jetbrains/kotlinx/lincheck/IdeaPlugin.kt

Lines changed: 213 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ package org.jetbrains.kotlinx.lincheck
1313

1414
import sun.nio.ch.lincheck.*
1515
import org.jetbrains.kotlinx.lincheck.runner.*
16+
import org.jetbrains.kotlinx.lincheck.strategy.*
17+
import org.jetbrains.kotlinx.lincheck.strategy.managed.*
1618
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.*
19+
import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult
1720
import org.jetbrains.kotlinx.lincheck.util.ThreadMap
1821

1922
const val MINIMAL_PLUGIN_VERSION = "0.2"
@@ -22,18 +25,17 @@ const val MINIMAL_PLUGIN_VERSION = "0.2"
2225

2326
/**
2427
* 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.
2730
* When a failure is found this method is called to provide all required information (trace points, failure type),
2831
* then [beforeEvent] method is called on each trace point.
2932
*
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]).
3234
* @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.
3739
*/
3840
@Suppress("UNUSED_PARAMETER")
3941
fun testFailed(
@@ -42,8 +44,7 @@ fun testFailed(
4244
version: String?,
4345
minimalPluginVersion: String,
4446
exceptions: Array<String>
45-
) {
46-
}
47+
) {}
4748

4849
/**
4950
* Debugger replaces the result of this method to `true` if idea plugin is enabled.
@@ -61,8 +62,8 @@ fun ideaPluginEnabled(): Boolean {
6162
fun lincheckVerificationStarted() {}
6263

6364
/**
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`.
6667
*/
6768
fun shouldReplayInterleaving(): Boolean {
6869
return false // should be replaced with `true` to replay the failure
@@ -71,7 +72,8 @@ fun shouldReplayInterleaving(): Boolean {
7172
/**
7273
* This method is called on every trace point shown to the user,
7374
* 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.
7577
* Then the debugger performs step-out action, so we appear in the user's code.
7678
* That's why this method **must** be called from a user-code, not from a nested function.
7779
*
@@ -80,18 +82,18 @@ fun shouldReplayInterleaving(): Boolean {
8082
*/
8183
@Suppress("UNUSED_PARAMETER")
8284
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
8587
visualize(strategy)
8688
}
8789

8890

8991
/**
9092
* 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.
9294
*
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.).
9597
*
9698
* @param testInstance tested data structure.
9799
* @param numbersArrayMap an array structured like [Object, objectNumber, Object, objectNumber, ...].
@@ -107,8 +109,7 @@ fun visualizeInstance(
107109
numbersArrayMap: Array<Any>,
108110
continuationToLincheckThreadIdMap: Array<Any>,
109111
threadToLincheckThreadIdMap: Array<Any>
110-
) {
111-
}
112+
) {}
112113

113114
/**
114115
* The Debugger creates a breakpoint on this method call to know when the thread is switched.
@@ -125,8 +126,198 @@ fun onThreadSwitchesOrActorFinishes() {}
125126
*/
126127
internal val eventIdStrictOrderingCheck = System.getProperty("lincheck.debug.withEventIdSequentialCheck") != null
127128

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\tat $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 {
130321
val runner = strategy.runner as ParallelThreadsRunner
131322
val testObject = runner.testInstance
132323
val lincheckThreads = runner.executor.threads
@@ -139,7 +330,6 @@ private fun visualize(strategyObject: Any) = runCatching {
139330
visualizeInstance(testObject, objectToNumberMap, continuationToLincheckThreadIdMap, threadToLincheckThreadIdMap)
140331
}
141332

142-
143333
/**
144334
* Creates an array [Object, objectNumber, Object, objectNumber, ...].
145335
* It represents a `Map<Any, Int>`, but due to difficulties with passing objects (Map)
@@ -149,7 +339,6 @@ private fun visualize(strategyObject: Any) = runCatching {
149339
*/
150340
private fun createObjectToNumberMapAsArray(testObject: Any): Array<Any> {
151341
val resultArray = arrayListOf<Any>()
152-
153342
val numbersMap = enumerateObjects(testObject)
154343
numbersMap.forEach { (any, objectNumber) ->
155344
resultArray.add(any)

src/jvm/main/org/jetbrains/kotlinx/lincheck/LinChecker.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,13 @@ class LinChecker(private val testClass: Class<*>, options: Options<*, *>?) {
124124
*/
125125
private fun CTestConfiguration.runReplayForPlugin(failure: LincheckFailure, verifier: Verifier) {
126126
if (ideaPluginEnabled() && this is ModelCheckingCTestConfiguration) {
127-
reporter.logFailedIteration(failure, loggingLevel = LoggingLevel.WARN)
128-
enableReplayModeForIdeaPlugin()
129-
val strategy = createStrategy(failure.scenario)
130-
check(strategy is ModelCheckingStrategy)
131-
strategy.use {
132-
val replayedFailure = it.runIteration(invocationsPerIteration, verifier)
127+
createStrategy(failure.scenario).use { strategy ->
128+
check(strategy is ModelCheckingStrategy)
129+
strategy.enableReplayModeForIdeaPlugin()
130+
val replayedFailure = strategy.runIteration(invocationsPerIteration, verifier)
133131
check(replayedFailure != null)
134132
strategy.runReplayIfPluginEnabled(replayedFailure)
135133
}
136-
} else {
137-
reporter.logFailedIteration(failure)
138134
}
139135
}
140136

src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,13 @@ abstract class ManagedStrategy(
4646
scenario: ExecutionScenario,
4747
private val validationFunction: Actor?,
4848
private val stateRepresentationFunction: Method?,
49-
private val testCfg: ManagedCTestConfiguration
49+
private val testCfg: ManagedCTestConfiguration,
5050
) : Strategy(scenario), EventTracker {
51+
52+
// The flag to enable IntelliJ IDEA plugin mode
53+
var inIdeaPluginReplayMode: Boolean = false
54+
private set
55+
5156
// The number of parallel threads.
5257
protected val nThreads: Int = scenario.nThreads
5358

@@ -250,6 +255,16 @@ abstract class ManagedStrategy(
250255

251256
protected open fun enableSpinCycleReplay() {}
252257

258+
protected open fun initializeReplay() {
259+
cleanObjectNumeration()
260+
resetEventIdProvider()
261+
}
262+
263+
internal fun doReplay(): InvocationResult {
264+
initializeReplay()
265+
return runInvocation()
266+
}
267+
253268
// == BASIC STRATEGY METHODS ==
254269

255270
override fun beforePart(part: ExecutionPart) = runInIgnoredSection {
@@ -1701,6 +1716,10 @@ abstract class ManagedStrategy(
17011716
return constructor(iThread, actorId, callStackTrace[iThread]?.toList() ?: emptyList())
17021717
}
17031718

1719+
fun enableReplayModeForIdeaPlugin() {
1720+
inIdeaPluginReplayMode = true
1721+
}
1722+
17041723
override fun beforeEvent(eventId: Int, type: String) {
17051724
ideaPluginBeforeEvent(eventId, type)
17061725
}
@@ -2034,4 +2053,4 @@ private const val OBSTRUCTION_FREEDOM_SUSPEND_VIOLATION_MESSAGE =
20342053
private const val INFINITE_TIMEOUT = 1000L * 60 * 60 * 24 * 365
20352054

20362055
private fun getTimeOutMs(strategy: ManagedStrategy, defaultTimeOutMs: Long): Long =
2037-
if (strategy is ModelCheckingStrategy && strategy.replay) INFINITE_TIMEOUT else defaultTimeOutMs
2056+
if (strategy.inIdeaPluginReplayMode) INFINITE_TIMEOUT else defaultTimeOutMs

0 commit comments

Comments
 (0)