Skip to content

Commit 46f8ece

Browse files
ndkovaleupp
andauthored
Transform lambdas correctly and enhance the GPMC API shape (#545)
* Lambdas are transformed correctly * Use `Runnable` in `runConcurrentTest` for better interop with Java * Java API is supported --------- Signed-off-by: Evgeniy Moiseenko <[email protected]> Co-authored-by: Evgeniy Moiseenko <[email protected]>
1 parent f7b0954 commit 46f8ece

File tree

101 files changed

+6033
-5650
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+6033
-5650
lines changed

bootstrap/src/sun/nio/ch/lincheck/Injections.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ public static boolean inAnalyzedCode() {
132132
public static boolean beforeThreadFork(Thread forkedThread) {
133133
// TestThread is handled separately
134134
if (forkedThread instanceof TestThread) return false;
135+
// If thread is started return immediately, as in this case, JVM will throw an `IllegalThreadStateException`
136+
if (forkedThread.getState() != Thread.State.NEW) return false;
135137
ThreadDescriptor descriptor = ThreadDescriptor.getCurrentThreadDescriptor();
136138
if (descriptor == null) {
137139
return false;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ private fun visualize(strategy: ManagedStrategy) = runCatching {
398398
val lincheckThreads = runner.executor.threads
399399
val testObject = runner.testInstance.takeIf {
400400
// in general-purpose model checking mode `testObject` is null
401-
it !is GeneralPurposeModelCheckingWrapper<*>
401+
it !is GeneralPurposeModelCheckingWrapper
402402
}
403403
visualizeInstance(testObject,
404404
objectToNumberMap = createObjectToNumberMapAsArray(testObject),

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

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
99
*/
1010

11+
@file:JvmName("Lincheck")
12+
1113
package org.jetbrains.kotlinx.lincheck
1214

1315
import org.jetbrains.kotlinx.lincheck.execution.*
@@ -31,13 +33,15 @@ annotation class ExperimentalModelCheckingAPI
3133
* @param block lambda which body will be a target for the interleavings exploration.
3234
*/
3335
@ExperimentalModelCheckingAPI
34-
fun <R> runConcurrentTest(
36+
@JvmOverloads
37+
fun runConcurrentTest(
3538
invocations: Int = DEFAULT_INVOCATIONS_COUNT,
36-
block: () -> R
39+
block: Runnable
3740
) {
41+
// TODO: do not use DSL to avoid spending 300ms in Kotlin Reflection
3842
val scenario = scenario {
3943
parallel {
40-
thread { actor(GeneralPurposeModelCheckingWrapper<R>::run, block) }
44+
thread { actor(GeneralPurposeModelCheckingWrapper::runGPMCTest, block) }
4145
}
4246
}
4347

@@ -74,8 +78,8 @@ fun <R> runConcurrentTest(
7478
}
7579
}
7680

77-
internal class GeneralPurposeModelCheckingWrapper<R>() {
78-
fun run(block: () -> R) = block()
81+
internal class GeneralPurposeModelCheckingWrapper {
82+
fun runGPMCTest(block: Runnable) = block.run()
7983
}
8084

8185
/**

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ internal fun StringBuilder.appendFailure(failure: LincheckFailure): StringBuilde
413413

414414
internal fun isGeneralPurposeModelCheckingScenario(scenario: ExecutionScenario): Boolean {
415415
val actor = scenario.parallelExecution.getOrNull(0)?.getOrNull(0)
416-
return (actor?.method == GeneralPurposeModelCheckingWrapper<*>::run.javaMethod)
416+
return (actor?.method == GeneralPurposeModelCheckingWrapper::runGPMCTest.javaMethod)
417417
}
418418

419419
private data class ExecutionResultsRepresentationData(
@@ -632,7 +632,7 @@ internal fun collectExceptionStackTraces(executionResult: ExecutionResult): Exce
632632
}
633633
val stackTrace = exception.stackTrace
634634
// filter lincheck methods
635-
.filter { LINCHECK_PACKAGE_NAME !in it.className }
635+
.filter { !isInLincheckPackage(it.className) }
636636
exceptionStackTraces[exception] = ExceptionNumberAndStacktrace(index + 1, stackTrace)
637637
}
638638
return ExceptionStackTracesResult(exceptionStackTraces)
@@ -643,11 +643,10 @@ private fun Throwable.isInternalLincheckBug(): Boolean {
643643
// so we filter out stack trace elements of these runner routines
644644
val testStackTrace = stackTrace.takeWhile { LINCHECK_RUNNER_PACKAGE_NAME !in it.className }
645645
// collect Lincheck functions from the stack trace
646-
val lincheckStackFrames = testStackTrace.filter { LINCHECK_PACKAGE_NAME in it.className }
646+
val lincheckStackFrames = testStackTrace.filter { isInLincheckPackage(it.className) }
647647
// special handling of `cancelByLincheck` primitive and general purpose model checking function call
648-
val lincheckLegalStackFrames = listOf("cancelByLincheck", "GeneralPurposeModelCheckingWrapper.run")
649-
if (lincheckStackFrames.size == 1 &&
650-
lincheckLegalStackFrames.any { it in lincheckStackFrames[0].toString() }) {
648+
val lincheckLegalStackFrames = listOf("cancelByLincheck", "runGPMCTest")
649+
if (lincheckStackFrames.all { it.methodName in lincheckLegalStackFrames }) {
651650
return false
652651
}
653652
// otherwise, if the stack trace contains any Lincheck functions, we classify it as a Lincheck bug

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,12 @@ internal fun Class<*>.findField(fieldName: String): Field {
243243
*/
244244
internal class LincheckInternalBugException(cause: Throwable): Exception(cause)
245245

246+
internal fun isInLincheckPackage(className: String) =
247+
className.startsWith(LINCHECK_PACKAGE_NAME) || className.startsWith(LINCHECK_BOOTSTRAP_PACKAGE_NAME)
248+
246249
internal const val LINCHECK_PACKAGE_NAME = "org.jetbrains.kotlinx.lincheck."
247250
internal const val LINCHECK_RUNNER_PACKAGE_NAME = "org.jetbrains.kotlinx.lincheck.runner."
251+
internal const val LINCHECK_BOOTSTRAP_PACKAGE_NAME = "sun.nio.ch.lincheck."
248252

249253
internal fun <T> Class<T>.newDefaultInstance(): T {
250254
@Suppress("UNCHECKED_CAST")

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ private fun removeSyntheticFieldAccessTracePoints(trace: List<TracePoint>) {
391391
}
392392
}
393393

394-
private fun isSyntheticFieldAccess(methodName: String): Boolean =
394+
private fun isSyntheticFieldAccess(methodName: String): Boolean =
395395
methodName.contains("access\$get") || methodName.contains("access\$set")
396396

397397
/**
@@ -435,7 +435,7 @@ private fun compressCallStackTrace(
435435
removed.add(nextElement.id)
436436
continue
437437
}
438-
438+
439439
// Check if current and next are compressible
440440
if (isCompressiblePair(currentElement.tracePoint.methodName, nextElement.tracePoint.methodName)) {
441441
// Combine fields of next and current, and store in current
@@ -474,7 +474,7 @@ private fun actorNodeResultRepresentation(result: Result?, failure: LincheckFail
474474
}
475475

476476
/**
477-
* Used by [compressCallStackTrace] to remove the two `invoke()` lines at the beginning of
477+
* Used by [compressCallStackTrace] to remove the two `invoke()` lines at the beginning of
478478
* a user-defined thread trace.
479479
*/
480480
private fun isUserThreadStart(currentElement: CallStackTraceElement, nextElement: CallStackTraceElement): Boolean =
@@ -529,7 +529,7 @@ private fun isDefaultPair(currentName: String, nextName: String): Boolean =
529529
*
530530
*/
531531
private fun isAccessPair(currentName: String, nextName: String): Boolean =
532-
currentName == "access$${nextName}"
532+
currentName == "access$${nextName}"
533533

534534
/**
535535
* Helper class to provider execution results, including a validation function result
@@ -710,7 +710,7 @@ internal abstract class TraceInnerNode(prefixProvider: PrefixProvider, iThread:
710710
fun addInternalEvent(node: TraceNode) {
711711
_internalEvents.add(node)
712712
}
713-
713+
714714
}
715715

716716
internal class CallNode(
@@ -755,12 +755,12 @@ internal class ActorNode(
755755
val actorRepresentation =
756756
prefix + actorRepresentation + if (resultRepresentation != null) ": $resultRepresentation" else ""
757757
traceRepresentation.add(TraceEventRepresentation(iThread, actorRepresentation))
758-
758+
759759
if (!shouldBeExpanded(verboseTrace)) {
760760
if (isCustomThreadActor) directChildren.forEach { it.addRepresentationTo(traceRepresentation, true) }
761761
lastState?.let { traceRepresentation.add(stateEventRepresentation(iThread, it)) }
762762
return lastInternalEvent.next
763-
}
763+
}
764764
return next
765765
}
766766
}

src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -239,13 +239,13 @@ internal object LincheckJavaAgent {
239239
*
240240
* The function is called upon a test instance creation, to ensure that all the classes related to it are transformed.
241241
*
242-
* @param testInstance the object to be transformed
242+
* @param obj the object to be transformed
243243
*/
244-
fun ensureObjectIsTransformed(testInstance: Any) {
244+
fun ensureObjectIsTransformed(obj: Any) {
245245
if (INSTRUMENT_ALL_CLASSES) {
246246
return
247247
}
248-
ensureObjectIsTransformed(testInstance, Collections.newSetFromMap(IdentityHashMap()))
248+
ensureObjectIsTransformed(obj, Collections.newSetFromMap(IdentityHashMap()))
249249
}
250250

251251
/**
@@ -269,27 +269,37 @@ internal object LincheckJavaAgent {
269269
* @param processedObjects A set of processed objects to avoid infinite recursion.
270270
*/
271271
private fun ensureObjectIsTransformed(obj: Any, processedObjects: MutableSet<Any>) {
272-
if (obj is Array<*>) {
273-
obj.filterNotNull().forEach { ensureObjectIsTransformed(it, processedObjects) }
274-
return
275-
}
272+
var clazz: Class<*> = obj.javaClass
273+
val className = clazz.name
276274

277-
if (!instrumentation.isModifiableClass(obj.javaClass) || !shouldTransform(obj.javaClass.name, instrumentationMode)) {
278-
return
275+
when {
276+
isJavaLambdaClass(className) -> {
277+
ensureClassHierarchyIsTransformed(getJavaLambdaEnclosingClass(className))
278+
}
279+
obj is Array<*> -> {
280+
obj.filterNotNull().forEach {
281+
ensureObjectIsTransformed(it, processedObjects)
282+
}
283+
return
284+
}
285+
else -> {
286+
if (!instrumentation.isModifiableClass(obj.javaClass) ||
287+
!shouldTransform(obj.javaClass.name, instrumentationMode)
288+
) {
289+
return
290+
}
291+
ensureClassHierarchyIsTransformed(clazz)
292+
}
279293
}
280294

281295
if (processedObjects.contains(obj)) return
282296
processedObjects += obj
283297

284-
var clazz: Class<*> = obj.javaClass
285-
286-
ensureClassHierarchyIsTransformed(clazz)
287-
288298
while (true) {
289299
clazz.declaredFields
290300
.filter { !it.type.isPrimitive }
291301
.filter { !Modifier.isStatic(it.modifiers) }
292-
.mapNotNull { readFieldViaUnsafe(obj, it, Unsafe::getObject) }
302+
.mapNotNull { readFieldSafely(obj, it).getOrNull() }
293303
.forEach {
294304
ensureObjectIsTransformed(it, processedObjects)
295305
}
@@ -304,17 +314,16 @@ internal object LincheckJavaAgent {
304314
* @param processedObjects Set of objects that have already been processed to prevent duplicate transformation.
305315
*/
306316
private fun ensureClassHierarchyIsTransformed(clazz: Class<*>, processedObjects: MutableSet<Any>) {
307-
if (instrumentation.isModifiableClass(clazz) && shouldTransform(clazz.name, instrumentationMode)) {
317+
if (!shouldTransform(clazz.name, instrumentationMode)) return
318+
if (instrumentation.isModifiableClass(clazz)) {
308319
instrumentedClasses += clazz.name
309320
instrumentation.retransformClasses(clazz)
310-
} else {
311-
return
312321
}
313322
// Traverse static fields.
314323
clazz.declaredFields
315324
.filter { !it.type.isPrimitive }
316325
.filter { Modifier.isStatic(it.modifiers) }
317-
.mapNotNull { readFieldViaUnsafe(null, it, Unsafe::getObject) }
326+
.mapNotNull { readFieldSafely(null, it).getOrNull() }
318327
.forEach {
319328
ensureObjectIsTransformed(it, processedObjects)
320329
}

src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,20 @@ internal fun isCoroutineStateMachineClass(className: String): Boolean {
502502

503503
private val isCoroutineStateMachineClassMap = ConcurrentHashMap<String, Boolean>()
504504

505+
/**
506+
* Test if the given class name corresponds to a Java lambda class.
507+
*/
508+
internal fun isJavaLambdaClass(className: String): Boolean =
509+
className.contains("\$\$Lambda")
510+
511+
/**
512+
* Extracts and returns the enclosing class name of a Java lambda class.
513+
*/
514+
internal fun getJavaLambdaEnclosingClass(className: String): String {
515+
require(isJavaLambdaClass(className)) { "Not a Java lambda class: $className" }
516+
return className.substringBefore("\$\$Lambda")
517+
}
518+
505519
/**
506520
* Tests if the provided [className] contains `"ClassLoader"` as a substring.
507521
*/

src/jvm/main/org/jetbrains/kotlinx/lincheck/util/Logger.kt

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,35 @@ internal object Logger {
3232

3333
inline fun error(lazyMessage: () -> String) = log(LoggingLevel.ERROR, lazyMessage)
3434

35-
fun error(e: Throwable) = error {
36-
StringWriter().use {
37-
e.printStackTrace(PrintWriter(it))
38-
}.toString()
39-
}
40-
4135
inline fun warn(lazyMessage: () -> String) = log(LoggingLevel.WARN, lazyMessage)
4236

4337
inline fun info(lazyMessage: () -> String) = log(LoggingLevel.INFO, lazyMessage)
4438

4539
inline fun debug(lazyMessage: () -> String) = log(LoggingLevel.DEBUG, lazyMessage)
4640

41+
fun error(e: Throwable) = log(LoggingLevel.ERROR, e)
42+
43+
fun warn(e: Throwable) = log(LoggingLevel.WARN, e)
44+
45+
fun info(e: Throwable) = log(LoggingLevel.INFO, e)
46+
47+
fun debug(e: Throwable) = log(LoggingLevel.DEBUG, e)
48+
4749
private inline fun log(logLevel: LoggingLevel, lazyMessage: () -> String) {
4850
if (logLevel >= this.logLevel) {
4951
write(logLevel, lazyMessage(), logWriter)
5052
}
5153
}
5254

55+
private fun log(logLevel: LoggingLevel, throwable: Throwable) {
56+
log(logLevel) {
57+
StringWriter().use { writer ->
58+
throwable.printStackTrace(PrintWriter(writer))
59+
writer.toString()
60+
}
61+
}
62+
}
63+
5364
private fun write(logLevel: LoggingLevel, s: String, writer: Writer) {
5465
try {
5566
writer.write("[${getCurrentTimeStamp()}] [${logLevel.name}] $s$LINE_SEPARATOR")

src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,25 @@ internal fun readFieldViaUnsafe(obj: Any?, field: Field): Any? {
6767
* Reads a [field] of the owner object [obj] via Unsafe,
6868
* in case of failure fallbacks into reading the field via reflection.
6969
*/
70-
internal fun readFieldSafely(obj: Any?, field: Field): kotlin.Result<Any?> {
70+
internal fun readFieldSafely(obj: Any?, field: Field): kotlin.Result<Any?> =
7171
// we wrap an unsafe read into `runCatching` to handle `UnsupportedOperationException`,
7272
// which can be thrown, for instance, when attempting to read
7373
// a field of a hidden or record class (starting from Java 15);
7474
// in this case we fall back to read via reflection
75-
return runCatching { readFieldViaUnsafe(obj, field) }
76-
.recoverCatching { field.apply { isAccessible = true }.get(obj) }
77-
}
75+
runCatching {
76+
readFieldViaUnsafe(obj, field)
77+
}
78+
.onFailure { exception ->
79+
Logger.debug { "Failed to read field ${field.name} via Unsafe" }
80+
Logger.debug(exception)
81+
}
82+
.recoverCatching {
83+
field.apply { isAccessible = true }.get(obj)
84+
}
85+
.onFailure { exception ->
86+
Logger.debug { "Failed to read field ${field.name} via reflection." }
87+
Logger.debug(exception)
88+
}
7889

7990
internal fun readArrayElementViaUnsafe(arr: Any, index: Int): Any? {
8091
val offset = getArrayElementOffsetViaUnsafe(arr, index)

0 commit comments

Comments
 (0)