diff --git a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java
index 6d7b4b5d5..e55b1454b 100644
--- a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java
+++ b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java
@@ -36,6 +36,8 @@ public interface EventTracker {
void beforeNewObjectCreation(String className);
void afterNewObjectCreation(Object obj);
+ long getNextObjectId();
+ void advanceCurrentObjectId(long oldId);
void updateSnapshotBeforeConstructorCall(Object[] objs);
diff --git a/bootstrap/src/sun/nio/ch/lincheck/Injections.java b/bootstrap/src/sun/nio/ch/lincheck/Injections.java
index 9e3bd4d0e..bb47ffd29 100644
--- a/bootstrap/src/sun/nio/ch/lincheck/Injections.java
+++ b/bootstrap/src/sun/nio/ch/lincheck/Injections.java
@@ -421,6 +421,40 @@ public static void updateSnapshotBeforeConstructorCall(Object[] objs) {
getEventTracker().updateSnapshotBeforeConstructorCall(objs);
}
+ /**
+ * Retrieves the next object id, used for identity hash code substitution, and then advances it by one.
+ */
+ public static long getNextObjectId() {
+ return getEventTracker().getNextObjectId();
+ }
+
+ /**
+ * Advances the current object id with the delta, associated with the old id {@code oldId},
+ * previously received with {@code getNextObjectId}.
+ *
+ * If for the given {@code oldId} there is no saved {@code newId},
+ * the function saves the current object id and associates it with the {@code oldId}.
+ * On subsequent re-runs, when for the given {@code oldId} there exists a saved {@code newId},
+ * the function sets the counter to the {@code newId}.
+ *
+ * This function is typically used to account for some cached computations:
+ * on the first run the actual computation is performed and its result is cached,
+ * and on subsequent runs the cached value is re-used.
+ * One example of such a situation is the {@code invokedynamic} instruction.
+ *
+ * In such cases, on the first run, the performed computation may allocate more objects,
+ * assigning more object ids to them.
+ * On subsequent runs, however, these objects will not be allocated, and thus the object ids numbering may vary.
+ * To account for this, before the first invocation of the cached computation,
+ * the last allocated object id {@code oldId} can be saved, and after the computation,
+ * the new last object id can be associated with it via a call {@code advanceCurrentObjectId(oldId)}.
+ * On subsequent re-runs, the cached computation will be skipped, but the
+ * current object id will still be advanced by the required delta via a call to {@code advanceCurrentObjectId(oldId)}.
+ */
+ public static void advanceCurrentObjectId(long oldId) {
+ getEventTracker().advanceCurrentObjectId(oldId);
+ }
+
/**
* Called from the instrumented code to replace [java.lang.Object.hashCode] method call with some
* deterministic value.
diff --git a/build.gradle.kts b/build.gradle.kts
index cfcd93914..67b1308b6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -28,6 +28,22 @@ repositories {
mavenCentral()
}
+fun SourceDirectorySet.configureTestSources() {
+ srcDir("src/jvm/test-common")
+ val testInTraceDebuggerMode: String by project
+ if (testInTraceDebuggerMode.toBoolean().also(::println)) {
+ srcDir("src/jvm/test-trace-debugger")
+ } else {
+ srcDir("src/jvm/test")
+ val jdkToolchainVersion: String by project
+ if (jdkToolchainVersion.toInt() >= 11) {
+ srcDir("src/jvm/test-jdk11")
+ } else {
+ srcDir("src/jvm/test-jdk8")
+ }
+ }
+}
+
kotlin {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
@@ -60,13 +76,7 @@ kotlin {
}
val jvmTest by getting {
- kotlin.srcDir("src/jvm/test")
- val jdkToolchainVersion: String by project
- if (jdkToolchainVersion.toInt() >= 11) {
- kotlin.srcDir("src/jvm/test-jdk11")
- } else {
- kotlin.srcDir("src/jvm/test-jdk8")
- }
+ kotlin.configureTestSources()
val junitVersion: String by project
val jctoolsVersion: String by project
@@ -105,7 +115,7 @@ sourceSets.main {
}
sourceSets.test {
- java.srcDirs("src/jvm/test")
+ java.configureTestSources()
resources {
srcDir("src/jvm/test/resources")
}
@@ -149,6 +159,10 @@ tasks {
if (withEventIdSequentialCheck.toBoolean()) {
extraArgs.add("-Dlincheck.debug.withEventIdSequentialCheck=true")
}
+ val testInTraceDebuggerMode: String by project
+ if (testInTraceDebuggerMode.toBoolean()) {
+ extraArgs.add("-Dlincheck.traceDebuggerMode=true")
+ }
extraArgs.add("-Dlincheck.version=$version")
jvmArgs(extraArgs)
}
diff --git a/gradle.properties b/gradle.properties
index a47475d24..3f34c76cc 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -19,6 +19,7 @@ jdkToolchainVersion=17
runAllTestsInSeparateJVMs=false
instrumentAllClasses=false
withEventIdSequentialCheck=false
+testInTraceDebuggerMode=false
kotlinVersion=1.9.25
kotlinxCoroutinesVersion=1.7.3
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/TraceDebugger.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/TraceDebugger.kt
new file mode 100644
index 000000000..a76177830
--- /dev/null
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/TraceDebugger.kt
@@ -0,0 +1,14 @@
+/*
+ * Lincheck
+ *
+ * Copyright (C) 2019 - 2025 JetBrains s.r.o.
+ *
+ * This Source Code Form is subject to the terms of the
+ * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.jetbrains.kotlinx.lincheck
+
+val TRACE_DEBUGGER_MODE = System.getProperty("lincheck.traceDebuggerMode", "false").toBoolean()
+val isLoopDetectorEnabled get() = System.getProperty("lincheck.loopDetectorEnabled", "true").toBoolean()
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt
index f05a17960..f83f0fa13 100644
--- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ManagedStrategy.kt
@@ -66,10 +66,13 @@ abstract class ManagedStrategy(
protected val currentActorId = mutableThreadMapOf()
// Detector of loops or hangs (i.e. active locks).
- internal val loopDetector: LoopDetector = LoopDetector(testCfg.hangingDetectionThreshold)
+ internal val loopDetector: LoopDetector? =
+ if (isLoopDetectorEnabled) LoopDetector(testCfg.hangingDetectionThreshold) else null
// Tracker of objects' allocations and object graph topology.
protected abstract val objectTracker: ObjectTracker
+ // Tracker of objects' identity hash codes.
+ internal abstract val identityHashCodeTracker: ObjectIdentityHashCodeTracker
// Tracker of the monitors' operations.
protected abstract val monitorTracker: MonitorTracker
// Tracker of the thread parking.
@@ -201,7 +204,7 @@ abstract class ManagedStrategy(
protected open fun initializeInvocation() {
traceCollector = if (collectTrace) TraceCollector() else null
suddenInvocationResult = null
- loopDetector.initialize()
+ loopDetector?.initialize()
objectTracker.reset()
monitorTracker.reset()
parkingTracker.reset()
@@ -260,7 +263,7 @@ abstract class ManagedStrategy(
POST -> 0
VALIDATION -> 0
}
- loopDetector.beforePart(nextThread)
+ loopDetector?.beforePart(nextThread)
threadScheduler.scheduleThread(nextThread)
}
@@ -282,12 +285,13 @@ abstract class ManagedStrategy(
}
collectTrace = true
- loopDetector.enableReplayMode(
+ loopDetector?.enableReplayMode(
failDueToDeadlockInTheEnd =
result is ManagedDeadlockInvocationResult ||
result is ObstructionFreedomViolationInvocationResult
)
cleanObjectNumeration()
+ identityHashCodeTracker.resetObjectIds()
runner.close()
runner = createRunner()
@@ -395,7 +399,7 @@ abstract class ManagedStrategy(
* In the considered example, we will retain that we will switch soon after
* the spin cycle in thread 1, so no bug will appear.
*/
- loopDetector.replayModeEnabled ->
+ loopDetector?.replayModeEnabled == true ->
loopDetector.shouldSwitchInReplayMode()
/*
* In the regular mode, we use loop detector only to determine should we
@@ -406,13 +410,13 @@ abstract class ManagedStrategy(
(runner.currentExecutionPart == PARALLEL) && shouldSwitch(iThread)
}
// check if live-lock is detected
- val decision = loopDetector.visitCodeLocation(iThread, codeLocation)
+ val decision = loopDetector?.visitCodeLocation(iThread, codeLocation)
// if we reached maximum number of events threshold, then fail immediately
if (decision == LoopDetector.Decision.EventsThresholdReached) {
failDueToDeadlock()
}
// if any kind of live-lock was detected, check for obstruction-freedom violation
- if (decision.isLivelockDetected) {
+ if (decision?.isLivelockDetected == true) {
failIfObstructionFreedomIsRequired {
if (decision is LoopDetector.Decision.LivelockFailureDetected) {
// if failure is detected, add a special obstruction-freedom violation
@@ -432,14 +436,14 @@ abstract class ManagedStrategy(
}
// if live-lock was detected, and replay was requested,
// then abort current execution and start the replay
- if (decision.isReplayRequired) {
+ if (decision?.isReplayRequired == true) {
abortWithSuddenInvocationResult(SpinCycleFoundAndReplayRequired)
}
// if the current thread in a live-lock, then try to switch to another thread
if (decision is LoopDetector.Decision.LivelockThreadSwitch) {
val switchHappened = switchCurrentThread(iThread, BlockingReason.LiveLocked, tracePoint)
if (switchHappened) {
- loopDetector.initializeFirstCodeLocationAfterSwitch(codeLocation)
+ loopDetector?.initializeFirstCodeLocationAfterSwitch(codeLocation)
}
traceCollector?.passCodeLocation(tracePoint)
return
@@ -448,12 +452,12 @@ abstract class ManagedStrategy(
if (shouldSwitch) {
val switchHappened = switchCurrentThread(iThread, tracePoint = tracePoint)
if (switchHappened) {
- loopDetector.initializeFirstCodeLocationAfterSwitch(codeLocation)
+ loopDetector?.initializeFirstCodeLocationAfterSwitch(codeLocation)
}
traceCollector?.passCodeLocation(tracePoint)
return
}
- if (!loopDetector.replayModeEnabled) {
+ if (loopDetector != null && !loopDetector.replayModeEnabled) {
loopDetector.onNextExecutionPoint(codeLocation)
}
traceCollector?.passCodeLocation(tracePoint)
@@ -581,7 +585,7 @@ abstract class ManagedStrategy(
open fun onThreadFinish(iThread: Int) {
threadScheduler.awaitTurn(iThread)
threadScheduler.finishThread(iThread)
- loopDetector.onThreadFinish(iThread)
+ loopDetector?.onThreadFinish(iThread)
traceCollector?.onThreadFinish()
unblockJoiningThreads(iThread)
val nextThread = chooseThreadSwitch(iThread, true)
@@ -614,7 +618,7 @@ abstract class ManagedStrategy(
currentActorId[iThread] = actorId
callStackTrace[iThread]!!.clear()
suspendedFunctionsStack[iThread]!!.clear()
- loopDetector.onActorStart(iThread)
+ loopDetector?.onActorStart(iThread)
enterTestingCode()
}
@@ -708,7 +712,7 @@ abstract class ManagedStrategy(
@JvmName("setNextThread")
private fun setCurrentThread(nextThread: Int) {
- loopDetector.onThreadSwitch(nextThread)
+ loopDetector?.onThreadSwitch(nextThread)
threadScheduler.scheduleThread(nextThread)
}
@@ -943,7 +947,7 @@ abstract class ManagedStrategy(
lastReadTracePoint[iThread] = tracePoint
}
newSwitchPoint(iThread, codeLocation, tracePoint)
- loopDetector.beforeReadField(obj)
+ loopDetector?.beforeReadField(obj)
return@runInIgnoredSection true
}
@@ -970,7 +974,7 @@ abstract class ManagedStrategy(
lastReadTracePoint[iThread] = tracePoint
}
newSwitchPoint(iThread, codeLocation, tracePoint)
- loopDetector.beforeReadArrayElement(array, index)
+ loopDetector?.beforeReadArrayElement(array, index)
true
}
@@ -980,7 +984,8 @@ abstract class ManagedStrategy(
lastReadTracePoint[iThread]?.initializeReadValue(adornedStringRepresentation(value))
lastReadTracePoint[iThread] = null
}
- loopDetector.afterRead(value)
+ loopDetector?.afterRead(value)
+ Unit
}
override fun beforeWriteField(obj: Any?, className: String, fieldName: String, value: Any?, codeLocation: Int,
@@ -1010,7 +1015,7 @@ abstract class ManagedStrategy(
null
}
newSwitchPoint(iThread, codeLocation, tracePoint)
- loopDetector.beforeWriteField(obj, value)
+ loopDetector?.beforeWriteField(obj, value)
return@runInIgnoredSection true
}
@@ -1036,7 +1041,7 @@ abstract class ManagedStrategy(
null
}
newSwitchPoint(iThread, codeLocation, tracePoint)
- loopDetector.beforeWriteArrayElement(array, index, value)
+ loopDetector?.beforeWriteArrayElement(array, index, value)
true
}
@@ -1086,9 +1091,16 @@ abstract class ManagedStrategy(
LincheckJavaAgent.ensureClassHierarchyIsTransformed(className)
}
+ override fun advanceCurrentObjectId(oldId: Long) {
+ identityHashCodeTracker.advanceCurrentObjectId(oldId)
+ }
+
+ override fun getNextObjectId(): Long = identityHashCodeTracker.getNextObjectId()
+
override fun afterNewObjectCreation(obj: Any) {
if (obj is String || obj is Int || obj is Long || obj is Byte || obj is Char || obj is Float || obj is Double) return
runInIgnoredSection {
+ identityHashCodeTracker.afterNewTrackedObjectCreation(obj)
objectTracker.registerNewObject(obj)
}
}
@@ -1230,11 +1242,11 @@ abstract class ManagedStrategy(
if (guarantee == ManagedGuaranteeType.TREAT_AS_ATOMIC) {
// re-use last call trace point
newSwitchPoint(iThread, codeLocation, callStackTrace[iThread]!!.lastOrNull()?.tracePoint)
- loopDetector.passParameters(params)
+ loopDetector?.passParameters(params)
}
// notify loop detector about the method call
if (guarantee == null) {
- loopDetector.beforeMethodCall(codeLocation, params)
+ loopDetector?.beforeMethodCall(codeLocation, params)
}
// method's guarantee
guarantee
@@ -1251,7 +1263,7 @@ abstract class ManagedStrategy(
override fun onMethodCallReturn(result: Any?) {
runInIgnoredSection {
- loopDetector.afterMethodCall()
+ loopDetector?.afterMethodCall()
if (collectTrace) {
val iThread = threadScheduler.getCurrentThreadId()
// this case is possible and can occur when we resume the coroutine,
@@ -1278,7 +1290,7 @@ abstract class ManagedStrategy(
override fun onMethodCallException(t: Throwable) {
runInIgnoredSection {
- loopDetector.afterMethodCall()
+ loopDetector?.afterMethodCall()
}
if (collectTrace) {
runInIgnoredSection {
@@ -1788,6 +1800,7 @@ abstract class ManagedStrategy(
}
fun checkActiveLockDetected() {
+ if (loopDetector == null) return
val currentThreadId = threadScheduler.getCurrentThreadId()
if (!loopDetector.replayModeCurrentlyInSpinCycle) return
if (spinCycleStartAdded) {
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectIdentityHashCodeTracker.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectIdentityHashCodeTracker.kt
new file mode 100644
index 000000000..a631d4366
--- /dev/null
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/ObjectIdentityHashCodeTracker.kt
@@ -0,0 +1,110 @@
+/*
+ * Lincheck
+ *
+ * Copyright (C) 2019 - 2024 JetBrains s.r.o.
+ *
+ * This Source Code Form is subject to the terms of the
+ * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.jetbrains.kotlinx.lincheck.strategy.managed
+
+import org.jetbrains.kotlinx.lincheck.TRACE_DEBUGGER_MODE
+import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicLong
+
+
+private typealias Id = Long
+
+/**
+ * This class stores initial identity hashcodes of created objects.
+ * When the program is rerun the firstly calculated hashcodes are left as is and used to
+ * substitute new hashcodes via [sun.misc.Unsafe] in the object headers.
+ *
+ * To guarantee correct work, ensure that replays are deterministic.
+ */
+internal class ObjectIdentityHashCodeTracker {
+ companion object {
+ /**
+ * Offset (in bytes) of identity hashcode in an object header.
+ *
+ * @see JVM Anatomy Quark #26: Identity Hash Code
+ */
+ private const val IDENTITY_HASHCODE_OFFSET: Long = 1L
+ }
+ private val initialHashCodes = ConcurrentHashMap()
+ private val nextObjectId = AtomicLong(0)
+ private val objectIdAdvances = ConcurrentHashMap()
+
+ /**
+ * This method substitutes identity hash code of the object in its header with the initial (from the first test execution) identity hashcode.
+ * @return id of the created object.
+ */
+ fun afterNewTrackedObjectCreation(obj: Any): Id {
+ val currentObjectId = if (TRACE_DEBUGGER_MODE) getNextObjectId() else 0
+ val initialIdentityHashCode = getInitialIdentityHashCode(
+ objectId = currentObjectId,
+ identityHashCode = if (TRACE_DEBUGGER_MODE) System.identityHashCode(obj) else 0
+ )
+ // ATTENTION: bizarre and crazy code below (might not work for all JVM implementations)
+ UnsafeHolder.UNSAFE.putInt(obj, IDENTITY_HASHCODE_OFFSET, initialIdentityHashCode)
+ return currentObjectId
+ }
+
+ /**
+ * Resets ids numeration and starts them from 0.
+ */
+ fun resetObjectIds() {
+ if (!TRACE_DEBUGGER_MODE) return
+ nextObjectId.set(0)
+ }
+
+ /**
+ * Advances the current object id with the delta, associated with the old id {@code oldId},
+ * previously received with {@code getNextObjectId}.
+ *
+ * If for the given {@code oldId} there is no saved {@code newId},
+ * the function saves the current object id and associates it with the {@code oldId}.
+ * On subsequent re-runs, when for the given {@code oldId} there exists a saved {@code newId},
+ * the function sets the counter to the {@code newId}.
+ *
+ * This function is typically used to account for some cached computations:
+ * on the first run the actual computation is performed and its result is cached,
+ * and on subsequent runs the cached value is re-used.
+ * One example of such a situation is the {@code invokedynamic} instruction.
+ *
+ * In such cases, on the first run, the performed computation may allocate more objects,
+ * assigning more object ids to them.
+ * On subsequent runs, however, these objects will not be allocated, and thus the object ids numbering may vary.
+ * To account for this, before the first invocation of the cached computation,
+ * the last allocated object id {@code oldId} can be saved, and after the computation,
+ * the new last object id can be associated with it via a call {@code advanceCurrentObjectId(oldId)}.
+ * On subsequent re-runs, the cached computation will be skipped, but the
+ * current object id will still be advanced by the required delta via a call to {@code advanceCurrentObjectId(oldId)}.
+ */
+ fun advanceCurrentObjectId(oldObjectId: Id) {
+ if (!TRACE_DEBUGGER_MODE) return
+ val newObjectId = nextObjectId.get()
+ val existingAdvance = objectIdAdvances.putIfAbsent(oldObjectId, newObjectId)
+ if (existingAdvance != null) {
+ nextObjectId.set(existingAdvance)
+ }
+ }
+
+ /**
+ * @return id of current object and increments the global counter.
+ */
+ fun getNextObjectId(): Id {
+ if (!TRACE_DEBUGGER_MODE) return 0
+ return nextObjectId.getAndIncrement()
+ }
+
+ /**
+ * @return initial identity hashcode for object with specified [objectId].
+ * If this is first time function is called for this object, then provided [identityHashCode] is treated as initial.
+ */
+ private fun getInitialIdentityHashCode(objectId: Id, identityHashCode: Int): Int =
+ initialHashCodes.getOrPut(objectId) { identityHashCode }
+}
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt
index 58397fa80..e26f0c2d3 100644
--- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingStrategy.kt
@@ -55,6 +55,8 @@ internal class ModelCheckingStrategy(
// Tracker of objects' allocations and object graph topology.
override val objectTracker: ObjectTracker = LocalObjectManager()
+ // Tracker of objects' identity hash codes.
+ override val identityHashCodeTracker: ObjectIdentityHashCodeTracker = ObjectIdentityHashCodeTracker()
// Tracker of the monitors' operations.
override val monitorTracker: MonitorTracker = ModelCheckingMonitorTracker()
// Tracker of the thread parking.
@@ -63,6 +65,7 @@ internal class ModelCheckingStrategy(
override fun nextInvocation(): Boolean {
currentInterleaving = root.nextInterleaving()
?: return false
+ identityHashCodeTracker.resetObjectIds()
return true
}
@@ -133,6 +136,7 @@ internal class ModelCheckingStrategy(
private fun doReplay(): InvocationResult {
cleanObjectNumeration()
+ identityHashCodeTracker.resetObjectIds()
currentInterleaving = currentInterleaving.copy()
resetEventIdProvider()
return runInvocation()
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt
index a7fe7d17a..3a4269d4c 100644
--- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt
@@ -80,17 +80,19 @@ internal class LincheckClassVisitor(
}
}
fun MethodVisitor.newAdapter() = GeneratorAdapter(this, access, methodName, desc)
- if (methodName == "" ||
- // Debugger implicitly evaluates toString for variables rendering
- // We need to disable breakpoints in such a case, as the numeration will break.
- // Breakpoints are disabled as we do not instrument toString and enter an ignored section,
- // so there are no beforeEvents inside.
- ideaPluginEnabled && methodName == "toString" && desc == "()Ljava/lang/String;") {
+ if (methodName == "") {
mv = WrapMethodInIgnoredSectionTransformer(fileName, className, methodName, mv.newAdapter())
return mv
}
- if (methodName == "") {
+ // Debugger implicitly evaluates toString for variables rendering
+ // We need to disable breakpoints in such a case, as the numeration will break.
+ // Breakpoints are disabled as we do not instrument toString and enter an ignored section,
+ // so there are no beforeEvents inside.
+ if (methodName == "" || ideaPluginEnabled && methodName == "toString" && desc == "()Ljava/lang/String;") {
mv = ObjectCreationTransformer(fileName, className, methodName, mv.newAdapter())
+ if (TRACE_DEBUGGER_MODE) {
+ mv = DeterministicInvokeDynamicTransformer(fileName, className, methodName, mv.newAdapter())
+ }
mv = run {
val st = ConstructorArgumentsSnapshotTrackerTransformer(fileName, className, methodName, mv.newAdapter(), classVisitor::isInstanceOf)
val sv = SharedMemoryAccessTransformer(fileName, className, methodName, st.newAdapter())
@@ -148,7 +150,9 @@ internal class LincheckClassVisitor(
mv = WaitNotifyTransformer(fileName, className, methodName, mv.newAdapter())
mv = ParkingTransformer(fileName, className, methodName, mv.newAdapter())
mv = ObjectCreationTransformer(fileName, className, methodName, mv.newAdapter())
- mv = DeterministicHashCodeTransformer(fileName, className, methodName, mv.newAdapter())
+ if (TRACE_DEBUGGER_MODE) {
+ mv = DeterministicInvokeDynamicTransformer(fileName, className, methodName, mv.newAdapter())
+ }
mv = DeterministicTimeTransformer(mv.newAdapter())
mv = DeterministicRandomTransformer(fileName, className, methodName, mv.newAdapter())
// `SharedMemoryAccessTransformer` goes first because it relies on `AnalyzerAdapter`,
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt
index a2e998105..5659dc3bc 100644
--- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt
@@ -158,6 +158,50 @@ internal fun GeneratorAdapter.storeArguments(methodDescriptor: String): IntArray
return storeLocals(argumentTypes)
}
+/**
+ * Executes a try-catch-finally block within the context of the GeneratorAdapter.
+ *
+ * Attention: this method does not insert `finally` blocks before return statements.
+ *
+ * @param tryBlock The code block to be executed in the try section.
+ * @param exceptionType The type of exception to be caught in the catch section, or null to catch all exceptions.
+ * @param catchBlock The code block to be executed in the catch section if an exception is thrown. By default, it re-throws the exception.
+ * @param finallyBlock The code block to be executed in the finally section. This is optional.
+ */
+internal fun GeneratorAdapter.tryCatchFinally(
+ tryBlock: GeneratorAdapter.() -> Unit,
+ exceptionType: Type? = null,
+ catchBlock: GeneratorAdapter.() -> Unit = { throwException() },
+ finallyBlock: (GeneratorAdapter.() -> Unit)? = null,
+) {
+ val startTryBlockLabel = newLabel()
+ val endTryBlockLabel = newLabel()
+ val exceptionHandlerLabel = newLabel()
+ val endLabel = newLabel()
+ visitTryCatchBlock(
+ startTryBlockLabel,
+ endTryBlockLabel,
+ exceptionHandlerLabel,
+ exceptionType?.internalName
+ )
+ visitLabel(startTryBlockLabel)
+ tryBlock()
+ visitLabel(endTryBlockLabel)
+ if (finallyBlock != null) finallyBlock()
+ goTo(endLabel)
+ visitLabel(exceptionHandlerLabel)
+ if (finallyBlock != null) {
+ val exception = newLocal(exceptionType ?: getType(Throwable::class.java))
+ storeLocal(exception)
+ finallyBlock()
+ loadLocal(exception)
+ catchBlock()
+ } else {
+ catchBlock()
+ }
+ visitLabel(endLabel)
+}
+
/**
* Copies arguments of the method in the local variables.
*
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt
index f2f958e64..fceed5ee5 100644
--- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterminismTransformers.kt
@@ -18,53 +18,6 @@ import org.jetbrains.kotlinx.lincheck.transformation.*
import sun.nio.ch.lincheck.*
import java.util.*
-/**
- * [DeterministicHashCodeTransformer] tracks invocations of [Object.hashCode] and [System.identityHashCode] methods,
- * and replaces them with the [Injections.hashCodeDeterministic] and [Injections.identityHashCodeDeterministic] calls.
- *
- * This transformation aims to prevent non-determinism due to the native [hashCode] implementation,
- * which typically returns memory address of the object.
- * There is no guarantee that memory addresses will be the same in different runs.
- */
-internal class DeterministicHashCodeTransformer(
- fileName: String,
- className: String,
- methodName: String,
- adapter: GeneratorAdapter,
-) : ManagedStrategyMethodVisitor(fileName, className, methodName, adapter) {
-
- override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) = adapter.run {
- when {
- name == "hashCode" && desc == "()I" -> {
- invokeIfInTestingCode(
- original = {
- visitMethodInsn(opcode, owner, name, desc, itf)
- },
- code = {
- invokeStatic(Injections::hashCodeDeterministic)
- }
- )
- }
-
- owner == "java/lang/System" && name == "identityHashCode" && desc == "(Ljava/lang/Object;)I" -> {
- invokeIfInTestingCode(
- original = {
- visitMethodInsn(opcode, owner, name, desc, itf)
- },
- code = {
- invokeStatic(Injections::identityHashCodeDeterministic)
- }
- )
- }
-
- else -> {
- visitMethodInsn(opcode, owner, name, desc, itf)
- }
- }
- }
-
-}
-
/**
* [DeterministicTimeTransformer] tracks invocations of [System.nanoTime] and [System.currentTimeMillis] methods,
* and replaces them with stubs to prevent non-determinism.
diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterministicInvokeDynamicTransformer.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterministicInvokeDynamicTransformer.kt
new file mode 100644
index 000000000..b80412348
--- /dev/null
+++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/DeterministicInvokeDynamicTransformer.kt
@@ -0,0 +1,150 @@
+/*
+ * Lincheck
+ *
+ * Copyright (C) 2019 - 2024 JetBrains s.r.o.
+ *
+ * This Source Code Form is subject to the terms of the
+ * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.jetbrains.kotlinx.lincheck.transformation.transformers
+
+import org.jetbrains.kotlinx.lincheck.transformation.*
+import org.objectweb.asm.Handle
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+import org.objectweb.asm.commons.GeneratorAdapter
+import org.objectweb.asm.commons.Method
+import sun.nio.ch.lincheck.Injections
+import java.lang.invoke.CallSite
+import java.lang.invoke.MethodHandles
+import java.lang.invoke.MethodType
+
+
+/**
+ * A specialized visitor for handling `INVOKEDYNAMIC` instructions in JVM bytecode.
+ *
+ * This transformer implements a deterministic execution strategy for dynamic method invocations,
+ * which normally rely on the call-site creation and caching called done by JVM itself,
+ * leading to the difference between the first and the following executions.
+ */
+internal class DeterministicInvokeDynamicTransformer(
+ fileName: String,
+ className: String,
+ methodName: String,
+ adapter: GeneratorAdapter
+) : ManagedStrategyMethodVisitor(fileName, className, methodName, adapter) {
+ override fun visitInvokeDynamicInsn(
+ name: String,
+ descriptor: String,
+ bootstrapMethodHandle: Handle,
+ vararg bootstrapMethodArguments: Any?
+ ) = adapter.run {
+ invokeIfInTestingCode(
+ original = { visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments) },
+ ) {
+ val arguments = storeArguments(descriptor)
+
+ // InvokeDynamic execution consists of the following steps:
+ // 1. Calling bootstrap method, which creates the actual function to call later;
+ // 2. Caching it by JVM;
+ // 3. Calling it by JVM.
+ // On the subsequent runs, JVM uses cached function.
+
+ // The current implementation performs these steps manually each time.
+ // Once native calls are supported, it would be possible to cache `MethodHandle`s creation.
+
+ // Bootstrap method is a function, which creates the actual call-site.
+ // Its first three parameters are predefined and are supplied by JVM when it invokes `invokedynamic`.
+ // After those parameters, there are regular parameters.
+ // Also, there can be `vararg` parameters.
+
+ // (predefined, predefined, predefined, regular, regular, [Ljava/lang/Object; <- is vararg)
+ // vararg parameter might exist or not
+
+ // https://openjdk.org/jeps/309
+ // https://www.infoq.com/articles/Invokedynamic-Javas-secret-weapon/
+ // https://www.baeldung.com/java-string-concatenation-invoke-dynamic
+
+ val bootstrapMethodParameterTypes = Type.getArgumentTypes(bootstrapMethodHandle.desc)
+ require(bootstrapMethodParameterTypes[0] == Type.getType(MethodHandles.Lookup::class.java))
+ require(bootstrapMethodParameterTypes[1] == Type.getType(String::class.java))
+ require(bootstrapMethodParameterTypes[2] == Type.getType(MethodType::class.java))
+
+ // pushing predefined arguments manually
+ invokeStatic(MethodHandles::lookup)
+ visitLdcInsn(name)
+ visitLdcInsn(Type.getMethodType(descriptor))
+ val jvmPredefinedParametersCount = 3
+
+ val isMethodWithVarArgs = isMethodVarArgs(bootstrapMethodHandle)
+ val notVarargsArgumentsCount = bootstrapMethodParameterTypes.size - jvmPredefinedParametersCount - (if (isMethodWithVarArgs) 1 else 0)
+ val varargsArgumentsCount = bootstrapMethodArguments.size - notVarargsArgumentsCount
+ // adding regular arguments
+ for (arg in bootstrapMethodArguments.take(notVarargsArgumentsCount)) {
+ visitLdcInsn(arg)
+ }
+ // adding vararg
+ if (isMethodWithVarArgs) {
+ val varargArguments = bootstrapMethodArguments.takeLast(varargsArgumentsCount)
+ visitLdcInsn(varargsArgumentsCount)
+ // [Ljava/lang/Object; -> Ljava/lang/Object;, [[Ljava/lang/Object; -> [Ljava/lang/Object;
+ val arrayElementType = bootstrapMethodParameterTypes.last().descriptor.drop(1).let(Type::getType)
+ newArray(arrayElementType)
+ for ((i, arg) in varargArguments.withIndex()) {
+ dup()
+ visitLdcInsn(i)
+ visitLdcInsn(arg)
+ if (arg != null && arrayElementType.sort == Type.OBJECT) {
+ box(Type.getType(arg.javaClass))
+ }
+ arrayStore(arrayElementType)
+ }
+ }
+
+ // creating java.lang.invoke.MethodHandle manually
+ invokeInIgnoredSection {
+ visitMethodInsn(
+ Opcodes.INVOKESTATIC,
+ bootstrapMethodHandle.owner,
+ bootstrapMethodHandle.name,
+ bootstrapMethodHandle.desc,
+ bootstrapMethodHandle.isInterface
+ )
+ invokeVirtual(
+ Type.getType(CallSite::class.java),
+ Method.getMethod(CallSite::class.java.getMethod("dynamicInvoker"))
+ )
+ }
+ // todo cache all above when native calls are available
+ loadLocals(arguments)
+ advancingCounter {
+ visitMethodInsn(
+ Opcodes.INVOKEVIRTUAL, "java/lang/invoke/MethodHandle", "invokeExact", descriptor, false
+ )
+ }
+ }
+ }
+
+ private fun isMethodVarArgs(
+ bootstrapMethodHandle: Handle,
+ ): Boolean {
+ val ownerClassName = bootstrapMethodHandle.owner.replace('/', '.')
+ val methods = Class.forName(ownerClassName).declaredMethods
+ return methods.single { Type.getMethodDescriptor(it) == bootstrapMethodHandle.desc }.isVarArgs
+ }
+
+ private fun GeneratorAdapter.advancingCounter(code: GeneratorAdapter.() -> Unit) {
+ invokeStatic(Injections::getNextObjectId)
+ val oldId = newLocal(Type.LONG_TYPE)
+ storeLocal(oldId)
+ tryCatchFinally(
+ tryBlock = code,
+ finallyBlock = {
+ loadLocal(oldId)
+ invokeStatic(Injections::advanceCurrentObjectId)
+ }
+ )
+ }
+}
diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt b/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt
similarity index 100%
rename from src/jvm/test/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt
rename to src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/KotlinJdkVersionTest.kt
diff --git a/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/util/JdkUtils.kt b/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/util/JdkUtils.kt
new file mode 100644
index 000000000..d65f7bc8f
--- /dev/null
+++ b/src/jvm/test-common/org/jetbrains/kotlinx/lincheck_test/util/JdkUtils.kt
@@ -0,0 +1,20 @@
+package org.jetbrains.kotlinx.lincheck_test.util
+
+/*
+ * Lincheck
+ *
+ * Copyright (C) 2019 - 2025 JetBrains s.r.o.
+ *
+ * This Source Code Form is subject to the terms of the
+ * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/**
+ * Indicates whether the current Java Development Kit (JDK) version is JDK 8.
+ *
+ * This property checks the system's Java specification version
+ * and determines if the major version corresponds to '8', signifying JDK 8.
+ */
+// java.specification.version is "1.$x" for Java prior to 8 and "$x" for the newer ones
+internal val isJdk8 = System.getProperty("java.specification.version").removePrefix("1.") == "8"
diff --git a/src/jvm/test-trace-debugger/AbstractNativeCallTest.kt b/src/jvm/test-trace-debugger/AbstractNativeCallTest.kt
new file mode 100644
index 000000000..a11ad3740
--- /dev/null
+++ b/src/jvm/test-trace-debugger/AbstractNativeCallTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Lincheck
+ *
+ * Copyright (C) 2019 - 2025 JetBrains s.r.o.
+ *
+ * This Source Code Form is subject to the terms of the
+ * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.jetbrains.kotlinx.lincheck_test
+
+import org.jetbrains.kotlinx.lincheck.ExceptionResult
+import org.jetbrains.kotlinx.lincheck.checkImpl
+import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult
+import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario
+import org.jetbrains.kotlinx.lincheck.execution.parallelResults
+import org.jetbrains.kotlinx.lincheck.isLoopDetectorEnabled
+import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions
+import org.jetbrains.kotlinx.lincheck.verifier.Verifier
+import org.junit.Test
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+
+/**
+ * Checks for absence of non-determinism and absence (or existence) of exceptions.
+ */
+abstract class AbstractNativeCallTest {
+ private fun testTraceDebugger() {
+ val oldStdOut = System.out
+ val oldErr = System.err
+ val stdOutOutputCollector = ByteArrayOutputStream()
+ val myStdOut = PrintStream(stdOutOutputCollector)
+ val stdErrOutputCollector = ByteArrayOutputStream()
+ val myStdErr = PrintStream(stdErrOutputCollector)
+ System.setOut(myStdOut)
+ System.setErr(myStdErr)
+ val oldIsLoopDetectorEnabled = isLoopDetectorEnabled
+ val loopDetectorPropertyName = "lincheck.loopDetectorEnabled"
+ System.setProperty(loopDetectorPropertyName, "false")
+ try {
+ ModelCheckingOptions()
+ .actorsBefore(0)
+ .actorsAfter(0)
+ .iterations(30)
+ .threads(2)
+ .minimizeFailedScenario(false)
+ .actorsPerThread(1)
+ .verifier(FailingVerifier::class.java)
+ .customize()
+ .checkImpl(this::class.java) { lincheckFailure ->
+ val results = lincheckFailure?.results?.parallelResults?.flatten()?.takeIf { it.isNotEmpty() }
+ require(results != null) { lincheckFailure.toString() }
+ if (shouldFail()) {
+ require(results.all { it is ExceptionResult })// { lincheckFailure.toString() }
+ } else {
+ require(results.none { it is ExceptionResult })// { lincheckFailure.toString() }
+ }
+ }
+ } finally {
+ val forbiddenString = "Non-determinism found."
+ System.setOut(oldStdOut)
+ System.setErr(oldErr)
+ val stdOutOutput = stdOutOutputCollector.toString()
+ println(stdOutOutput)
+ val stdErrOutput = stdErrOutputCollector.toString()
+ System.err.print(stdErrOutput)
+ require(!stdOutOutput.contains(forbiddenString) && !stdErrOutput.contains(forbiddenString))
+ System.setProperty(loopDetectorPropertyName, oldIsLoopDetectorEnabled.toString())
+ }
+ }
+
+ open fun shouldFail() = false
+
+ @Test
+ fun test() = testTraceDebugger()
+
+ open fun ModelCheckingOptions.customize(): ModelCheckingOptions = this
+}
+
+@Suppress("UNUSED_PARAMETER")
+class FailingVerifier(sequentialSpecification: Class<*>) : Verifier {
+ override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?) = false
+}
diff --git a/src/jvm/test-trace-debugger/HashCodeTests.kt b/src/jvm/test-trace-debugger/HashCodeTests.kt
new file mode 100644
index 000000000..0f85f212b
--- /dev/null
+++ b/src/jvm/test-trace-debugger/HashCodeTests.kt
@@ -0,0 +1,177 @@
+/*
+ * Lincheck
+ *
+ * Copyright (C) 2019 - 2025 JetBrains s.r.o.
+ *
+ * This Source Code Form is subject to the terms of the
+ * Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+ * with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.jetbrains.kotlinx.lincheck_test
+
+import org.jetbrains.kotlinx.lincheck.annotations.Operation
+import org.jetbrains.kotlinx.lincheck.check
+import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions
+import org.junit.Test
+import java.util.*
+
+abstract class HashCodeTest : AbstractNativeCallTest()
+
+class SimpleHashCodeTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { Any().hashCode() }
+}
+
+class IdentityHashCodeTest : HashCodeTest() {
+ @Operation
+ fun operation(): List = List(100) { System.identityHashCode(Any()) }
+}
+
+
+class ObjectsHashCodeTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { Objects.hashCode(Any()) }
+}
+
+
+class ObjectsHashCodeVarargTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { Objects.hash(Any(), Any()) }
+}
+
+class SimpleToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { Any().toString() }
+}
+
+class HashCodeToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { Any().hashCode().toString() }
+}
+
+class StringBuilderToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { buildString { appendLine(Any()) } }
+}
+
+class StringBuilderHashCodeToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { buildString { appendLine(Any().hashCode()) } }
+}
+
+class InvokeDynamicToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { "${Any()} ${Any()}" }
+}
+
+class InvokeDynamicInnerClassCreationTest : HashCodeTest() {
+ class A {
+ override fun toString(): String = Any().hashCode().toString()
+ }
+ @Operation
+ fun operation(): List = List(100) { "${A()} ${A()}" }
+}
+
+class InvokeDynamicHashCodeToStringTest : HashCodeTest() {
+ @Operation
+ fun operation(): List = List(100) { "${Any().hashCode()} ${Any().hashCode()}" }
+}
+
+class StringFormatToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { String.format("%s %s", Any(), Any()) }
+}
+
+class StringFormatHashCodeToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { String.format("%s %s", Any().hashCode(), Any().hashCode()) }
+}
+
+data class Wrapper(val value: Any)
+
+class WrapperHashCodeTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { Wrapper(Any()).hashCode() }
+}
+
+class WrapperToStringTest : HashCodeTest() {
+ @Operation
+ fun operation() = List(100) { Wrapper(Any()).toString() }
+}
+
+class InitInternalToStringTest : HashCodeTest() {
+ class A {
+ internal val value = "${Any()} ${Any()}"
+ override fun toString(): String = value
+ }
+ @Operation
+ fun operation() = List(100) { "${A()} ${A()}" }
+}
+
+class ClassInitInternalToStringTest : HashCodeTest() {
+ object A {
+ @JvmStatic
+ internal val value = "${Any()} ${Any()}"
+ }
+ fun f(): String = A.value
+
+ @Operation
+ fun operation() = List(100) { "${f()} ${f()}" }
+}
+
+class FailingInvokeDynamicTest : HashCodeTest() {
+ private class A {
+ override fun toString(): String {
+ throw IllegalStateException()
+ }
+ }
+ @Operation
+ fun operation(): List {
+ try {
+ List(100) { "${A()} ${A()}" }
+ } catch (_: Throwable) {
+ // ignore
+ }
+ return List(100) { Any().toString() }
+ }
+}
+
+class FailingInvokeDynamicWithStateTest : HashCodeTest() {
+ private class A {
+ var x = 0
+ override fun toString(): String {
+ try {
+ x = Any().hashCode()
+ throw IllegalStateException()
+ } finally {
+ x = x xor Any().hashCode()
+ }
+ }
+ }
+
+ @Operation
+ fun operation(): List {
+ try {
+ List(100) { "${A()} ${A()} ${A().x}" }
+ } catch (_: Throwable) {
+ // ignore
+ }
+ return List(100) { Any().toString() + " " + A().x }
+ }
+}
+
+class IdentityHashCodeDiffersTest() {
+ @Operation
+ fun operation() {
+ val objects = Array(10) { Any() }
+ // identityHashCode should differ
+ check(objects.map { it.hashCode() }.distinct().size > 1)
+ }
+
+ @Test
+ fun test() = ModelCheckingOptions()
+ .iterations(1)
+ .threads(1)
+ .check(this::class)
+}
diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt
index 119114916..658ef6307 100644
--- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt
+++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/representation/CustomThreadsRepresentationTest.kt
@@ -102,7 +102,12 @@ class CustomThreadsRepresentationTest {
testClass = this::class,
testOperation = this::livelock,
invocations = 1_000,
- outputFileName = if (isJdk8) "custom_threads_livelock_trace_jdk8.txt" else "custom_threads_livelock_trace.txt",
+ outputFileName = when {
+ isJdk8 && TRACE_DEBUGGER_MODE -> "custom_threads_livelock_trace_jdk8_with_trace_debugger.txt"
+ isJdk8 -> "custom_threads_livelock_trace_jdk8.txt"
+ TRACE_DEBUGGER_MODE -> "custom_threads_livelock_trace_with_trace_debugger.txt"
+ else -> "custom_threads_livelock_trace.txt"
+ },
)
fun incorrectConcurrentLinkedDeque() {
diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt
index 26d343f97..88a3b7977 100644
--- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt
+++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/transformation/HashCodeTests.kt
@@ -12,7 +12,6 @@ package org.jetbrains.kotlinx.lincheck_test.transformation
import org.jetbrains.kotlinx.lincheck.*
import org.jetbrains.kotlinx.lincheck.annotations.Operation
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.*
-import org.jetbrains.kotlinx.lincheck.verifier.*
import org.junit.*
/**
@@ -77,16 +76,3 @@ class IdentityHashCodeOnNullTest {
@Test
fun test() = ModelCheckingOptions().check(this::class)
}
-
-@Ignore // TODO: easier to support when `javaagent` is merged
-class IdentityHashCodeDiffersTest() {
- @Operation
- fun operation() {
- val objects = Array(1000) { Any() }
- // identityHashCode should differ
- check(objects.map { it.hashCode() }.distinct().size > 1)
- }
-
- @Test
- fun test() = ModelCheckingOptions().check(this::class)
-}
\ No newline at end of file
diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt
index b3f19e5d7..b9c11ab5a 100644
--- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt
+++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/util/TestUtils.kt
@@ -109,11 +109,3 @@ class StringPoolGenerator(randomProvider: RandomProvider, configuration: String)
strings[random.nextInt(strings.size)]
}
-/**
- * Indicates whether the current Java Development Kit (JDK) version is JDK 8.
- *
- * This property checks the system's Java specification version
- * and determines if the major version corresponds to '8', signifying JDK 8.
- */
-// java.specification.version is "1.$x" for Java prior to 8 and "$x" for the newer ones
-internal val isJdk8 = System.getProperty("java.specification.version").removePrefix("1.") == "8"
diff --git a/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_jdk8_with_trace_debugger.txt b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_jdk8_with_trace_debugger.txt
new file mode 100644
index 000000000..71e4df7ed
--- /dev/null
+++ b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_jdk8_with_trace_debugger.txt
@@ -0,0 +1,73 @@
+= The execution has hung =
+| ------------------ |
+| Thread 1 |
+| ------------------ |
+| livelock(): |
+| ------------------ |
+
+
+The following interleaving leads to the error:
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Thread 1 | Thread 2 | Thread 3 |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| livelock(): | | |
+| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| switch (reason: waiting for Thread 2 to finish) | | |
+| | | run() |
+| | | SpinLockTestKt.withLock(SpinLock#2,livelock$t2$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | switch |
+| | run() | |
+| | SpinLockTestKt.withLock(SpinLock#1,livelock$t1$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | livelock$t1$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | |
+| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | |
+| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | /* The following events repeat infinitely: */ | |
+| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | |
+| | └╶╶ switch (reason: active lock detected) | |
+| | | livelock$t2$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) |
+| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) |
+| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | /* The following events repeat infinitely: */ |
+| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) |
+| | | └╶╶ switch (reason: active lock detected) |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+All unfinished threads are in deadlock
+
+Detailed trace:
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Thread 1 | Thread 2 | Thread 3 |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| livelock(): | | |
+| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| switch (reason: waiting for Thread 2 to finish) | | |
+| | | run() |
+| | | SpinLockTestKt.withLock(SpinLock#2,livelock$t2$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | AtomicBoolean#1.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) |
+| | | switch |
+| | run() | |
+| | SpinLockTestKt.withLock(SpinLock#1,livelock$t1$1$1#1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | AtomicBoolean#2.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) | |
+| | livelock$t1$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | |
+| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | |
+| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | /* The following events repeat infinitely: */ | |
+| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | |
+| | └╶╶ switch (reason: active lock detected) | |
+| | | livelock$t2$1$1#1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) |
+| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) |
+| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | /* The following events repeat infinitely: */ |
+| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) |
+| | | └╶╶ switch (reason: active lock detected) |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+All unfinished threads are in deadlock
diff --git a/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_with_trace_debugger.txt b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_with_trace_debugger.txt
new file mode 100644
index 000000000..7f685fa63
--- /dev/null
+++ b/src/jvm/test/resources/expected_logs/custom_threads_livelock_trace_with_trace_debugger.txt
@@ -0,0 +1,73 @@
+= The execution has hung =
+| ------------------ |
+| Thread 1 |
+| ------------------ |
+| livelock(): |
+| ------------------ |
+
+
+The following interleaving leads to the error:
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Thread 1 | Thread 2 | Thread 3 |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| livelock(): | | |
+| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| switch (reason: waiting for Thread 2 to finish) | | |
+| | | run() |
+| | | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t2$1$1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | switch |
+| | run() | |
+| | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t1$1$1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | CustomThreadsRepresentationTest$livelock$t1$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | |
+| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | |
+| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | /* The following events repeat infinitely: */ | |
+| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | |
+| | └╶╶ switch (reason: active lock detected) | |
+| | | CustomThreadsRepresentationTest$livelock$t2$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) |
+| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) |
+| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | /* The following events repeat infinitely: */ |
+| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) |
+| | | └╶╶ switch (reason: active lock detected) |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+All unfinished threads are in deadlock
+
+Detailed trace:
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Thread 1 | Thread 2 | Thread 3 |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| livelock(): | | |
+| Thread#2.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| Thread#3.start() at CustomThreadsRepresentationTest.livelock(CustomThreadsRepresentationTest.kt:95) | | |
+| switch (reason: waiting for Thread 2 to finish) | | |
+| | | run() |
+| | | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t2$1$1) at CustomThreadsRepresentationTest.livelock$lambda$6(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | AtomicBoolean#1.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) |
+| | | switch |
+| | run() | |
+| | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t1$1$1) at CustomThreadsRepresentationTest.livelock$lambda$5(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | AtomicBoolean#2.compareAndSet(false,true): true at SpinLock.lock(SpinLockTest.kt:83) | |
+| | CustomThreadsRepresentationTest$livelock$t1$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) | |
+| | invoke() at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:81) | |
+| | SpinLockTestKt.withLock(SpinLock#2,CustomThreadsRepresentationTest$livelock$t1$1$1$1) at CustomThreadsRepresentationTest$livelock$t1$1$1.invoke(CustomThreadsRepresentationTest.kt:82) | |
+| | SpinLock#2.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) | |
+| | /* The following events repeat infinitely: */ | |
+| | ┌╶> AtomicBoolean#1.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) | |
+| | └╶╶ switch (reason: active lock detected) | |
+| | | CustomThreadsRepresentationTest$livelock$t2$1$1.invoke() at SpinLockTestKt.withLock(SpinLockTest.kt:96) |
+| | | invoke() at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:88) |
+| | | SpinLockTestKt.withLock(SpinLock#1,CustomThreadsRepresentationTest$livelock$t2$1$1$1) at CustomThreadsRepresentationTest$livelock$t2$1$1.invoke(CustomThreadsRepresentationTest.kt:89) |
+| | | SpinLock#1.lock() at SpinLockTestKt.withLock(SpinLockTest.kt:94) |
+| | | /* The following events repeat infinitely: */ |
+| | | ┌╶> AtomicBoolean#2.compareAndSet(false,true): false at SpinLock.lock(SpinLockTest.kt:83) |
+| | | └╶╶ switch (reason: active lock detected) |
+| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+All unfinished threads are in deadlock