diff --git a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java index d41ada7a5..e530a32e9 100644 --- a/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java +++ b/bootstrap/src/sun/nio/ch/lincheck/EventTracker.java @@ -32,6 +32,8 @@ public interface EventTracker { void beforeNewObjectCreation(String className); void afterNewObjectCreation(Object obj); + void updateSnapshotBeforeConstructorCall(Object[] objs); + boolean beforeReadField(Object obj, String className, String fieldName, int codeLocation, boolean isStatic, boolean isFinal); boolean beforeReadArrayElement(Object array, int index, int codeLocation); diff --git a/bootstrap/src/sun/nio/ch/lincheck/Injections.java b/bootstrap/src/sun/nio/ch/lincheck/Injections.java index 5bbfb0b87..77a188f69 100644 --- a/bootstrap/src/sun/nio/ch/lincheck/Injections.java +++ b/bootstrap/src/sun/nio/ch/lincheck/Injections.java @@ -294,6 +294,15 @@ public static void afterNewObjectCreation(Object obj) { getEventTracker().afterNewObjectCreation(obj); } + /** + * Called from instrumented code before constructors' invocations, + * where passed objects are subtypes of the constructor class type. + * Required to update the static memory snapshot. + */ + public static void updateSnapshotBeforeConstructorCall(Object[] objs) { + getEventTracker().updateSnapshotBeforeConstructorCall(objs); + } + /** * Called from the instrumented code to replace [java.lang.Object.hashCode] method call with some * deterministic value. diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt index 737efa6bd..ffa170053 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/ObjectTraverser.kt @@ -22,7 +22,7 @@ package org.jetbrains.kotlinx.lincheck import org.jetbrains.kotlinx.lincheck.strategy.managed.ObjectLabelFactory.getObjectNumber import org.jetbrains.kotlinx.lincheck.util.* -import java.lang.reflect.Modifier +import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe import java.math.BigDecimal import java.math.BigInteger import kotlin.coroutines.Continuation diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt index 69eb5c4d6..e18c13a4a 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/Utils.kt @@ -10,13 +10,12 @@ package org.jetbrains.kotlinx.lincheck import kotlinx.coroutines.* -import sun.nio.ch.lincheck.* import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.strategy.managed.* import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer -import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder +import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe import org.jetbrains.kotlinx.lincheck.verifier.* -import org.jetbrains.kotlinx.lincheck.util.* +import sun.nio.ch.lincheck.* import java.io.PrintWriter import java.io.StringWriter import java.lang.ref.* @@ -213,21 +212,63 @@ internal val Throwable.text: String get() { return writer.buffer.toString() } +/** + * Returns all found fields in the hierarchy. + * Multiple fields with the same name and the same type may be returned + * if they appear in the subclass and a parent class. + */ +internal val Class<*>.allDeclaredFieldWithSuperclasses get(): List { + val fields: MutableList = ArrayList() + var currentClass: Class<*>? = this + while (currentClass != null) { + val declaredFields: Array = currentClass.declaredFields + fields.addAll(declaredFields) + currentClass = currentClass.superclass + } + return fields +} -@Suppress("DEPRECATION") -internal fun findFieldNameByOffset(targetType: Class<*>, offset: Long): String? { - // Extract the private offset value and find the matching field. - for (field in targetType.declaredFields) { +/** + * Finds a public/protected/private/internal field in the class and its superclasses/interfaces by name. + * + * @param fieldName the name of the field to find. + * @return the [java.lang.reflect.Field] object if found, or `null` if not found. + */ +internal fun Class<*>.findField(fieldName: String): Field { + // Search in the class hierarchy + var clazz: Class<*>? = this + while (clazz != null) { + // Check class itself try { - if (Modifier.isNative(field.modifiers)) continue - val fieldOffset = if (Modifier.isStatic(field.modifiers)) UnsafeHolder.UNSAFE.staticFieldOffset(field) - else UnsafeHolder.UNSAFE.objectFieldOffset(field) - if (fieldOffset == offset) return field.name - } catch (t: Throwable) { - t.printStackTrace() + return clazz.getDeclaredField(fieldName) + } + catch (_: NoSuchFieldException) {} + + // Check interfaces + for (interfaceClass in clazz.interfaces) { + try { + return interfaceClass.getDeclaredField(fieldName) + } + catch (_: NoSuchFieldException) {} } + + // Move up the hierarchy + clazz = clazz.superclass } - return null // Field not found + + throw NoSuchFieldException("Class '${this.name}' does not have field '$fieldName'") +} + +/** + * Reads a [field] of the owner object [obj] via Unsafe, in case of failure fallbacks into reading the field via reflection. + */ +internal fun readFieldSafely(obj: Any?, field: Field): kotlin.Result { + // we wrap an unsafe read into `runCatching` to handle `UnsupportedOperationException`, + // which can be thrown, for instance, when attempting to read + // a field of a hidden or record class (starting from Java 15); + // in this case we fall back to read via reflection + return runCatching { readFieldViaUnsafe(obj, field) } + .recoverCatching { field.apply { isAccessible = true }.get(obj) } } /** diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt index d388e70a5..574e55971 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/Strategy.kt @@ -11,6 +11,7 @@ package org.jetbrains.kotlinx.lincheck.strategy import org.jetbrains.kotlinx.lincheck.runner.* import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedStrategy import org.jetbrains.kotlinx.lincheck.strategy.managed.Trace import org.jetbrains.kotlinx.lincheck.verifier.Verifier import java.io.Closeable @@ -102,13 +103,21 @@ abstract class Strategy protected constructor( */ fun Strategy.runIteration(invocations: Int, verifier: Verifier): LincheckFailure? { for (invocation in 0 until invocations) { - if (!nextInvocation()) - return null + if (!nextInvocation()) return null val result = runInvocation() - val failure = verify(result, verifier) - if (failure != null) - return failure + + val failure = try { + verify(result, verifier) + } finally { + // verifier calls `@Operation`s of the class under test which can + // modify the static memory; thus, we need to restore initial values + if (this is ManagedStrategy) { + restoreStaticMemorySnapshot() + } + } + if (failure != null) return failure } + return null } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt index 53dbae8ec..04c2d5b76 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/AtomicFieldUpdaterNames.kt @@ -10,8 +10,8 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed -import org.jetbrains.kotlinx.lincheck.findFieldNameByOffset import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder.UNSAFE +import org.jetbrains.kotlinx.lincheck.util.findFieldNameByOffsetViaUnsafe import java.util.concurrent.atomic.AtomicIntegerFieldUpdater import java.util.concurrent.atomic.AtomicLongFieldUpdater import java.util.concurrent.atomic.AtomicReferenceFieldUpdater @@ -37,7 +37,7 @@ internal object AtomicFieldUpdaterNames { val offsetField = updater.javaClass.getDeclaredField("offset") val offset = UNSAFE.getLong(updater, UNSAFE.objectFieldOffset(offsetField)) - return findFieldNameByOffset(targetType, offset) + return findFieldNameByOffsetViaUnsafe(targetType, offset) } catch (t: Throwable) { t.printStackTrace() } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/FieldSearchHelper.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/FieldSearchHelper.kt index 4a6d342c5..ce13fca98 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/FieldSearchHelper.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/FieldSearchHelper.kt @@ -57,7 +57,7 @@ internal object FieldSearchHelper { traverseObjectGraph( testObject, traverseStaticFields = true, - onArrayElement = { _, _, _ -> null }, // do not traverse array elements further + onArrayElement = null, // do not traverse array elements further onField = { ownerObject, field, fieldValue -> when { field.type.isPrimitive || fieldValue == null -> null 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 fa8fd92d1..169e329fa 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 @@ -82,6 +82,9 @@ abstract class ManagedStrategy( // Tracker of the thread parking. protected abstract val parkingTracker: ParkingTracker + // Snapshot of the memory, reachable from static fields + protected val staticMemorySnapshot = SnapshotTracker() + // InvocationResult that was observed by the strategy during the execution (e.g., a deadlock). @Volatile protected var suddenInvocationResult: InvocationResult? = null @@ -216,13 +219,24 @@ abstract class ManagedStrategy( parkingTracker.reset() } + /** + * Restores recorded values of all memory reachable from static state. + */ + fun restoreStaticMemorySnapshot() { + staticMemorySnapshot.restoreValues() + } + /** * Runs the current invocation. */ override fun runInvocation(): InvocationResult { while (true) { initializeInvocation() - val result = runner.run() + val result: InvocationResult = try { + runner.run() + } finally { + restoreStaticMemorySnapshot() + } // In case the runner detects a deadlock, some threads can still manipulate the current strategy, // so we're not interested in suddenInvocationResult in this case // and immediately return RunnerTimeoutInvocationResult. @@ -742,6 +756,7 @@ abstract class ManagedStrategy( */ override fun beforeReadField(obj: Any?, className: String, fieldName: String, codeLocation: Int, isStatic: Boolean, isFinal: Boolean) = runInIgnoredSection { + updateSnapshotOnFieldAccess(obj, className.canonicalClassName, fieldName) // We need to ensure all the classes related to the reading object are instrumented. // The following call checks all the static fields. if (isStatic) { @@ -778,6 +793,7 @@ abstract class ManagedStrategy( /** Returns true if a switch point is created. */ override fun beforeReadArrayElement(array: Any, index: Int, codeLocation: Int): Boolean = runInIgnoredSection { + updateSnapshotOnArrayElementAccess(array, index) if (!objectTracker.shouldTrackObjectAccess(array)) { return@runInIgnoredSection false } @@ -813,6 +829,7 @@ abstract class ManagedStrategy( override fun beforeWriteField(obj: Any?, className: String, fieldName: String, value: Any?, codeLocation: Int, isStatic: Boolean, isFinal: Boolean): Boolean = runInIgnoredSection { + updateSnapshotOnFieldAccess(obj, className.canonicalClassName, fieldName) objectTracker.registerObjectLink(fromObject = obj ?: StaticObject, toObject = value) if (!objectTracker.shouldTrackObjectAccess(obj ?: StaticObject)) { return@runInIgnoredSection false @@ -842,6 +859,7 @@ abstract class ManagedStrategy( } override fun beforeWriteArrayElement(array: Any, index: Int, value: Any?, codeLocation: Int): Boolean = runInIgnoredSection { + updateSnapshotOnArrayElementAccess(array, index) objectTracker.registerObjectLink(fromObject = array, toObject = value) if (!objectTracker.shouldTrackObjectAccess(array)) { return@runInIgnoredSection false @@ -909,6 +927,29 @@ abstract class ManagedStrategy( } } + /** + * Tracks a specific field of an [obj], if the [obj] is either `null` (which means that field is static), + * or one this objects which contains it is already stored. + */ + fun updateSnapshotOnFieldAccess(obj: Any?, className: String, fieldName: String) = runInIgnoredSection { + staticMemorySnapshot.trackField(obj, className, fieldName) + } + + /** + * Tracks a specific [array] element at [index], if the [array] is already tracked. + */ + fun updateSnapshotOnArrayElementAccess(array: Any, index: Int) = runInIgnoredSection { + staticMemorySnapshot.trackArrayCell(array, index) + } + + /** + * Tracks all objects in [objs] eagerly. + * Required as a trick to overcome issue with leaking this in constructors, see https://github.com/JetBrains/lincheck/issues/424. + */ + override fun updateSnapshotBeforeConstructorCall(objs: Array) = runInIgnoredSection { + staticMemorySnapshot.trackObjects(objs) + } + private fun methodGuaranteeType(owner: Any?, className: String, methodName: String): ManagedGuaranteeType? = runInIgnoredSection { userDefinedGuarantees?.forEach { guarantee -> val ownerName = owner?.javaClass?.canonicalName ?: className diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/SnapshotTracker.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/SnapshotTracker.kt new file mode 100644 index 000000000..258d2b967 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/SnapshotTracker.kt @@ -0,0 +1,255 @@ +/* + * 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.findField +import org.jetbrains.kotlinx.lincheck.isPrimitiveWrapper +import org.jetbrains.kotlinx.lincheck.readFieldSafely +import org.jetbrains.kotlinx.lincheck.strategy.managed.SnapshotTracker.MemoryNode.* +import org.jetbrains.kotlinx.lincheck.util.* +import java.lang.Class +import java.lang.reflect.Field +import java.lang.reflect.Modifier +import java.util.Collections +import java.util.IdentityHashMap +import java.util.Stack +import java.util.concurrent.atomic.AtomicIntegerArray +import java.util.concurrent.atomic.AtomicLongArray +import java.util.concurrent.atomic.AtomicReferenceArray + + +/** + * Manages a snapshot of the global static state. + * + * This class only tracks static memory and memory reachable from it, + * referenced from the test code. So the whole static state is not recorded, but only a subset of that, + * which is named *snapshot*. + */ +class SnapshotTracker { + private val trackedObjects = IdentityHashMap>() + + private sealed class MemoryNode( + val initialValue: Any? + ) { + abstract class FieldNode(val field: Field, initialValue: Any?) : MemoryNode(initialValue) + + class RegularFieldNode(field: Field, initialValue: Any?) : FieldNode(field, initialValue) + class StaticFieldNode(field: Field, initialValue: Any?) : FieldNode(field, initialValue) + class ArrayCellNode(val index: Int, initialValue: Any?) : MemoryNode(initialValue) + } + + fun trackField(obj: Any?, className: String, fieldName: String) { + if (obj != null && obj !in trackedObjects) return + + val clazz = getDeclaringClass(obj, className, fieldName) + val field = clazz.findField(fieldName) + val readResult = readFieldSafely(obj, field) + + if (readResult.isSuccess) { + val fieldValue = readResult.getOrNull() + if ( + trackSingleField(obj, clazz, field, fieldValue) && + shouldTrackEagerly(fieldValue) + ) { + trackReachableObjectSubgraph(fieldValue!!) + } + } + } + + fun trackArrayCell(array: Any, index: Int) { + if (array !in trackedObjects) return + + val readResult = runCatching { readArrayElementViaUnsafe(array, index) } + + if (readResult.isSuccess) { + val elementValue = readResult.getOrNull() + if ( + trackSingleArrayCell(array, index, elementValue) && + shouldTrackEagerly(elementValue) + ) { + trackReachableObjectSubgraph(elementValue!!) + } + } + } + + fun trackObjects(objs: Array) { + // in case this works too slowly, an optimization could be used + // see https://github.com/JetBrains/lincheck/pull/418/commits/0d708b84dd2bfd5dbfa961549dda128d91dc3a5b#diff-a684b1d7deeda94bbf907418b743ae2c0ec0a129760d3b87d00cdf5adfab56c4R146-R199 + objs + .filter { it != null && it in trackedObjects } + .forEach { trackReachableObjectSubgraph(it!!) } + } + + fun restoreValues() { + val visitedObjects = Collections.newSetFromMap(IdentityHashMap()) + trackedObjects.keys + .filterIsInstance>() + .forEach { restoreValues(it, visitedObjects) } + } + + /** + * @return `true` if the [fieldValue] is a trackable object, and it is added + * as a parent object for its own fields for further lazy tracking. + */ + private fun trackSingleField( + obj: Any?, + clazz: Class<*>, + field: Field, + fieldValue: Any? + ): Boolean { + val nodesList = + if (obj != null) trackedObjects[obj] + else trackedObjects.getOrPut(clazz) { mutableListOf() } + + if ( + nodesList == null || // parent object is not tracked + nodesList + .filterIsInstance() + .any { it.field.name == field.name } // field is already tracked + ) return false + + val childNode = createFieldNode(obj, field, fieldValue) + + nodesList.add(childNode) + val isFieldValueTrackable = isTrackableObject(childNode.initialValue) + + if (isFieldValueTrackable) { + trackedObjects.putIfAbsent(childNode.initialValue, mutableListOf()) + } + + return isFieldValueTrackable + } + + /** + * @return `true` if the [elementValue] is a trackable object, and it is added + * as a parent object for its own fields for further lazy tracking. + */ + private fun trackSingleArrayCell( + array: Any, + index: Int, + elementValue: Any? + ): Boolean { + val nodesList = trackedObjects[array] + + if ( + nodesList == null || // array is not tracked + nodesList.any { it is ArrayCellNode && it.index == index } // this array cell is already tracked + ) return false + + val childNode = createArrayCellNode(index, elementValue) + + nodesList.add(childNode) + val isElementValueTrackable = isTrackableObject(childNode.initialValue) + + if (isElementValueTrackable) { + trackedObjects.putIfAbsent(childNode.initialValue, mutableListOf()) + } + + return isElementValueTrackable + } + + private fun trackReachableObjectSubgraph(obj: Any) { + traverseObjectGraph( + obj, + onArrayElement = { array, index, elementValue -> + trackSingleArrayCell(array, index, elementValue) + elementValue + }, + onField = { owner, field, fieldValue -> + // optimization to track only `value` field of java atomic classes + if (isAtomicJava(owner) && field.name != "value") null + // do not traverse fields of var-handles + else if (owner.javaClass.typeName == "java.lang.invoke.VarHandle") null + else { + trackSingleField(owner, owner.javaClass, field, fieldValue) + fieldValue + } + } + ) + } + + private fun shouldTrackEagerly(obj: Any?): Boolean { + if (obj == null) return false + return ( + // TODO: in further development of snapshot restoring feature this check should be removed + // (and only check for java atomic classes should be inserted), see https://github.com/JetBrains/lincheck/pull/418#issue-2595977113 + // right now it is needed for collections to be restored properly (because of missing support for `System.arrayCopy()` and other similar methods) + obj.javaClass.name.startsWith("java.util.") + ) + } + + private fun restoreValues(root: Any, visitedObjects: MutableSet) { + val stackOfObjects: Stack = Stack() + stackOfObjects.push(root) + + while (stackOfObjects.isNotEmpty()) { + val obj = stackOfObjects.pop() + if (obj in visitedObjects) continue + visitedObjects.add(obj) + + trackedObjects[obj]!! + .forEach { node -> + if (node is ArrayCellNode) { + val index = node.index + val initialValue = node.initialValue + + when (obj) { + // No need to add support for writing to atomicfu array elements, + // because atomicfu arrays are compiled to atomic java arrays + is AtomicReferenceArray<*> -> @Suppress("UNCHECKED_CAST") (obj as AtomicReferenceArray).set( + index, + initialValue + ) + + is AtomicIntegerArray -> obj.set(index, initialValue as Int) + is AtomicLongArray -> obj.set(index, initialValue as Long) + else -> writeArrayElementViaUnsafe(obj, index, initialValue) + } + } else if (!Modifier.isFinal((node as FieldNode).field.modifiers)) { + writeFieldViaUnsafe( + if (node is StaticFieldNode) null else obj, + node.field, + node.initialValue + ) + } + + if (isTrackableObject(node.initialValue)) { + stackOfObjects.push(node.initialValue!!) + } + } + } + } + + private fun isTrackableObject(value: Any?): Boolean { + return ( + value != null && + !value.javaClass.isPrimitive && + !value.javaClass.isEnum && + !value.isPrimitiveWrapper + ) + } + + private fun getDeclaringClass(obj: Any?, className: String, fieldName: String): Class<*> { + return Class.forName(className).let { + if (obj != null) it + else it.findField(fieldName).declaringClass + } + } + + private fun createFieldNode(obj: Any?, field: Field, value: Any?): MemoryNode { + return if (obj == null) StaticFieldNode(field, value) + else RegularFieldNode(field, value) + } + + private fun createArrayCellNode(index: Int, value: Any?): MemoryNode { + return ArrayCellNode(index, value) + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/UnsafeNames.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/UnsafeNames.kt index 90912729e..0ae61d1cc 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/UnsafeNames.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/UnsafeNames.kt @@ -10,9 +10,9 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed -import org.jetbrains.kotlinx.lincheck.findFieldNameByOffset import org.jetbrains.kotlinx.lincheck.strategy.managed.UnsafeName.* import org.jetbrains.kotlinx.lincheck.util.UnsafeHolder +import org.jetbrains.kotlinx.lincheck.util.findFieldNameByOffsetViaUnsafe /** * Helper object to provide the field name and the owner of the Unsafe method call. @@ -29,7 +29,7 @@ internal object UnsafeNames { if (secondParameter is Long) { return if (firstParameter is Class<*>) { // The First parameter is a Class object in case of static field access. - val fieldName = findFieldNameByOffset(firstParameter, secondParameter) + val fieldName = findFieldNameByOffsetViaUnsafe(firstParameter, secondParameter) ?: return TreatAsDefaultMethod UnsafeStaticMethod(firstParameter, fieldName, parameters.drop(2)) } else if (firstParameter != null && firstParameter::class.java.isArray) { @@ -46,7 +46,7 @@ internal object UnsafeNames { ) } else if (firstParameter != null) { // Then is an instance method call. - val fieldName = findFieldNameByOffset(firstParameter::class.java, secondParameter) + val fieldName = findFieldNameByOffsetViaUnsafe(firstParameter::class.java, secondParameter) ?: return TreatAsDefaultMethod UnsafeInstanceMethod( owner = firstParameter, diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/VarHandleNames.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/VarHandleNames.kt index ba15855bf..16e1a81a8 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/VarHandleNames.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/VarHandleNames.kt @@ -10,8 +10,8 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed -import org.jetbrains.kotlinx.lincheck.findFieldNameByOffset import org.jetbrains.kotlinx.lincheck.strategy.managed.VarHandleMethodType.* +import org.jetbrains.kotlinx.lincheck.util.findFieldNameByOffsetViaUnsafe import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe import sun.misc.Unsafe import java.lang.reflect.Field @@ -102,7 +102,7 @@ internal object VarHandleNames { override fun getMethodType(varHandle: Any, parameters: Array): VarHandleMethodType { val ownerType = readFieldViaUnsafe(varHandle, receiverTypeField, Unsafe::getObject) as Class<*> val fieldOffset = readFieldViaUnsafe(varHandle, fieldOffsetField, Unsafe::getLong) - val fieldName = findFieldNameByOffset(ownerType, fieldOffset) ?: return TreatAsDefaultMethod + val fieldName = findFieldNameByOffsetViaUnsafe(ownerType, fieldOffset) ?: return TreatAsDefaultMethod val firstParameter = parameters.firstOrNull() ?: return TreatAsDefaultMethod if (!ownerType.isInstance(firstParameter)) return TreatAsDefaultMethod @@ -124,7 +124,7 @@ internal object VarHandleNames { val ownerType = readFieldViaUnsafe(varHandle, receiverTypeField, Unsafe::getObject) as Class<*> val fieldOffset = readFieldViaUnsafe(varHandle, fieldOffsetField, Unsafe::getLong) - val fieldName = findFieldNameByOffset(ownerType, fieldOffset) ?: return TreatAsDefaultMethod + val fieldName = findFieldNameByOffsetViaUnsafe(ownerType, fieldOffset) ?: return TreatAsDefaultMethod return StaticVarHandleMethod(ownerType, fieldName, parameters.toList()) } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt index 2fa92c3a2..3a31ec2f2 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/strategy/managed/modelchecking/ModelCheckingCTestConfiguration.kt @@ -23,9 +23,9 @@ import java.lang.reflect.* */ class ModelCheckingCTestConfiguration(testClass: Class<*>, iterations: Int, threads: Int, actorsPerThread: Int, actorsBefore: Int, actorsAfter: Int, generatorClass: Class, verifierClass: Class, - checkObstructionFreedom: Boolean, hangingDetectionThreshold: Int, invocationsPerIteration: Int, - guarantees: List, minimizeFailedScenario: Boolean, - sequentialSpecification: Class<*>, timeoutMs: Long, + checkObstructionFreedom: Boolean, hangingDetectionThreshold: Int, + invocationsPerIteration: Int, guarantees: List, + minimizeFailedScenario: Boolean, sequentialSpecification: Class<*>, timeoutMs: Long, customScenarios: List ) : ManagedCTestConfiguration( testClass = testClass, 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 84810546f..779003164 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckClassVisitor.kt @@ -18,11 +18,10 @@ import org.objectweb.asm.commons.* import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.* import org.jetbrains.kotlinx.lincheck.transformation.transformers.* import sun.nio.ch.lincheck.* -import kotlin.collections.HashSet internal class LincheckClassVisitor( private val instrumentationMode: InstrumentationMode, - classVisitor: ClassVisitor + private val classVisitor: SafeClassWriter ) : ClassVisitor(ASM_API, classVisitor) { private val ideaPluginEnabled = ideaPluginEnabled() private var classVersion = 0 @@ -91,6 +90,14 @@ internal class LincheckClassVisitor( } if (methodName == "") { mv = ObjectCreationTransformer(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()) + val aa = AnalyzerAdapter(className, access, methodName, desc, sv) + + sv.analyzer = aa + aa + } return mv } /* Wrap `ClassLoader::loadClass` calls into ignored sections @@ -145,8 +152,10 @@ internal class LincheckClassVisitor( // which should be put in front of the byte-code transformer chain, // so that it can correctly analyze the byte-code and compute required type-information mv = run { - val sv = SharedMemoryAccessTransformer(fileName, className, methodName, mv.newAdapter()) + val st = ConstructorArgumentsSnapshotTrackerTransformer(fileName, className, methodName, mv.newAdapter(), classVisitor::isInstanceOf) + val sv = SharedMemoryAccessTransformer(fileName, className, methodName, st.newAdapter()) val aa = AnalyzerAdapter(className, access, methodName, desc, sv) + sv.analyzer = aa aa } diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt index f5eec2e4c..4d5e26e98 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/LincheckJavaAgent.kt @@ -15,11 +15,10 @@ import org.jetbrains.kotlinx.lincheck.canonicalClassName import org.jetbrains.kotlinx.lincheck.runInIgnoredSection import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.MODEL_CHECKING import org.jetbrains.kotlinx.lincheck.transformation.InstrumentationMode.STRESS -import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.shouldTransform import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.isEagerlyInstrumentedClass +import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.shouldTransform import org.jetbrains.kotlinx.lincheck.transformation.LincheckClassFileTransformer.transformedClassesStress import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.INSTRUMENT_ALL_CLASSES -import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentation import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentationMode import org.jetbrains.kotlinx.lincheck.transformation.LincheckJavaAgent.instrumentedClasses import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java index 52adc124f..5cb0ef130 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/SafeClassWriter.java @@ -35,6 +35,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Objects; /** @@ -53,6 +54,10 @@ public SafeClassWriter(ClassReader cr, ClassLoader loader, final int flags) { this.loader = loader != null ? loader : ClassLoader.getSystemClassLoader(); } + public boolean isInstanceOf(final String actualType, final String expectedSuperType) { + return Objects.equals(getCommonSuperClass(actualType, expectedSuperType), expectedSuperType); + } + @Override protected String getCommonSuperClass(final String type1, final String type2) { try { 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 7f7cd072b..58a9cda87 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/TransformationUtils.kt @@ -306,6 +306,25 @@ internal inline fun GeneratorAdapter.invokeInIgnoredSection( ) } +/** + * @param type asm type descriptor. + * @return whether [type] is a java array type (primitive or reference). + */ +internal fun isArray(type: Type): Boolean = type.sort == Type.ARRAY + +/** + * @param type asm type descriptor. + * @return whether [type] is a non-reference primitive type (e.g. `int`, `boolean`, etc.). + */ +internal fun isPrimitive(type: Type): Boolean { + return when (type.sort) { + Type.BOOLEAN, Type.CHAR, Type.BYTE, + Type.SHORT, Type.INT, Type.FLOAT, + Type.LONG, Type.DOUBLE, Type.VOID -> true + else -> false + } +} + private val isCoroutineStateMachineClassMap = ConcurrentHashMap() internal fun isCoroutineStateMachineClass(internalClassName: String): Boolean { diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/ConstructorArgumentsSnapshotTrackerTransformer.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/ConstructorArgumentsSnapshotTrackerTransformer.kt new file mode 100644 index 000000000..f4d96f172 --- /dev/null +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/ConstructorArgumentsSnapshotTrackerTransformer.kt @@ -0,0 +1,89 @@ +/* + * 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.Opcodes.* +import org.objectweb.asm.Type.* +import org.objectweb.asm.commons.GeneratorAdapter +import sun.nio.ch.lincheck.Injections + +internal class ConstructorArgumentsSnapshotTrackerTransformer( + fileName: String, + className: String, + methodName: String, + adapter: GeneratorAdapter, + // `SafeClassWriter::isInstanceOf` method which checks the subclassing without loading the classes to VM + private val isInstanceOf: (actualType: String, expectedSuperType: String) -> Boolean +) : ManagedStrategyMethodVisitor(fileName, className, methodName, adapter) { + + /** + * Searches for invocations of constructors `className::(args)` and inserts bytecode + * that will extract all objects from `args` which types are subtypes of the class `className`. + * Snapshot tracker then tracks the extracted objects eagerly. + * + * This is a hack solution to the problem of impossibility of identifying whether + * some object on stack in the constructor is a `this` reference or not. + * So for instructions `GETFIELD`/`PUTFIELD` in constructors handler of `visitFieldInsn(...)` method below + * we ignore the accesses to fields which owners are the same type as the constructor owner. + * The optimization is done to avoid 'leaking this' problem during bytecode verification. + * But sometimes `GETFIELD`/`PUTFIELD` are invoked on fields with owner objects are the same type as a constructor owner, + * but which are not `this`. This can happen when: + * - a non-static object is accessed which was passed to constructor arguments + * - a non-static object is accessed which was created locally in constructor + * + * We only case about the 1nd case, because for the 3nd case when dealing with + * a locally constructed object it can't be reached from static memory, so no need to track its fields. + * The 2nd case where such object is passed via constructor argument is handled here. + */ + override fun visitMethodInsn(opcode: Int, owner: String, name: String, desc: String, itf: Boolean) = adapter.run { + if (name == "") { + val matchedArguments = getArgumentTypes(desc).toList() + .mapIndexed { index, type -> + if ( + !isArray(type) && + !isPrimitive(type) && + isInstanceOf(type.className.replace('.', '/'), owner) + ) { + index + } + else { + null + } + } + .filterNotNull() + .toIntArray() + + if (matchedArguments.isEmpty()) { + visitMethodInsn(opcode, owner, name, desc, itf) + return + } + + invokeIfInTestingCode( + original = { visitMethodInsn(opcode, owner, name, desc, itf) }, + code = { + // STACK: args + val arguments = storeArguments(desc) + val matchedLocals = arguments.filterIndexed { index, _ -> matchedArguments.contains(index) }.toIntArray() + // STACK: + pushArray(matchedLocals) + // STACK: array + invokeStatic(Injections::updateSnapshotBeforeConstructorCall) + // STACK: + loadLocals(arguments) + // STACK: args + visitMethodInsn(opcode, owner, name, desc, itf) + } + ) + } + else visitMethodInsn(opcode, owner, name, desc, itf) + } +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/SharedMemoryAccessTransformer.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/SharedMemoryAccessTransformer.kt index de1020306..67080c113 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/SharedMemoryAccessTransformer.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/transformation/transformers/SharedMemoryAccessTransformer.kt @@ -33,7 +33,13 @@ internal class SharedMemoryAccessTransformer( lateinit var analyzer: AnalyzerAdapter override fun visitFieldInsn(opcode: Int, owner: String, fieldName: String, desc: String) = adapter.run { - if (isCoroutineInternalClass(owner) || isCoroutineStateMachineClass(owner)) { + if ( + isCoroutineInternalClass(owner) || + isCoroutineStateMachineClass(owner) || + // when initializing our own fields in constructor, we do not want to track that; + // otherwise `VerifyError` will be thrown, see https://github.com/JetBrains/lincheck/issues/424 + (methodName == "" && className == owner) + ) { visitFieldInsn(opcode, owner, fieldName, desc) return } @@ -305,13 +311,13 @@ internal class SharedMemoryAccessTransformer( else -> throw IllegalStateException("Unexpected opcode: $opcode") } - /* - * Tries to obtain the type of array elements by inspecting the type of the array itself. - * To do this, the method queries the analyzer to get the type of accessed array - * which should lie on the stack. - * If the analyzer does not know the type, then return null - * (according to the ASM docs, this can happen, for example, when the visited instruction is unreachable). - */ + /* + * Tries to obtain the type of array elements by inspecting the type of the array itself. + * To do this, the method queries the analyzer to get the type of accessed array + * which should lie on the stack. + * If the analyzer does not know the type, then return null + * (according to the ASM docs, this can happen, for example, when the visited instruction is unreachable). + */ private fun getArrayAccessTypeFromStack(position: Int): Type? { if (analyzer.stack == null) return null val arrayDesc = analyzer.stack[analyzer.stack.size - position] diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/AtomicMethods.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/AtomicMethods.kt index 72fbc1c48..6c73c0b43 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/AtomicMethods.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/AtomicMethods.kt @@ -92,7 +92,6 @@ internal fun isAtomicFUClass(className: String) = className == "kotlinx.atomicfu.AtomicInt" || className == "kotlinx.atomicfu.AtomicLong" - internal fun isAtomicMethod(className: String, methodName: String) = isAtomicClass(className) && methodName in atomicMethods @@ -113,7 +112,6 @@ internal fun isAtomicFUArray(receiver: Any?) = receiver is kotlinx.atomicfu.AtomicIntArray || receiver is kotlinx.atomicfu.AtomicLongArray - internal fun isAtomicArrayClass(className: String) = // java.util.concurrent className == "java.util.concurrent.atomic.AtomicReferenceArray" || diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/ObjectGraph.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/ObjectGraph.kt index f523143ff..e31286643 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/ObjectGraph.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/ObjectGraph.kt @@ -16,6 +16,9 @@ import java.lang.reflect.* import java.util.* +private typealias FieldCallback = (obj: Any, field: Field, value: Any?) -> Any? +private typealias ArrayElementCallback = (array: Any, index: Int, element: Any?) -> Any? + /** * Traverses a subgraph of objects reachable from a given root object in BFS order. * @@ -41,11 +44,11 @@ import java.util.* * @param onArrayElement callback for array elements traversal, accepts `(array, index, elementValue)`. * Returns an object to be traversed next. */ -internal inline fun traverseObjectGraph( +internal fun traverseObjectGraph( root: Any, traverseStaticFields: Boolean = false, - onField: (obj: Any, field: Field, value: Any?) -> Any?, - onArrayElement: (array: Any, index: Int, element: Any?) -> Any?, + onField: FieldCallback?, + onArrayElement: ArrayElementCallback?, ) { val queue = ArrayDeque() val visitedObjects = Collections.newSetFromMap(IdentityHashMap()) @@ -75,12 +78,13 @@ internal inline fun traverseObjectGraph( val currentObj = queue.removeFirst() when { - currentObj.javaClass.isArray || isAtomicArray(currentObj) -> { + onArrayElement != null && + (currentObj.javaClass.isArray || isAtomicArray(currentObj)) -> { traverseArrayElements(currentObj) { _ /* currentObj */, index, elementValue -> processNextObject(onArrayElement(currentObj, index, elementValue)) } } - else -> { + onField != null -> { traverseObjectFields(currentObj, traverseStaticFields = traverseStaticFields ) { _ /* currentObj */, field, fieldValue -> @@ -103,7 +107,7 @@ internal inline fun traverseObjectGraph( * * @see traverseObjectGraph */ -internal inline fun traverseObjectGraph( +internal fun traverseObjectGraph( root: Any, traverseStaticFields: Boolean = false, onObject: (obj: Any) -> Any? @@ -164,12 +168,7 @@ internal inline fun traverseObjectFields( ) { obj.javaClass.allDeclaredFieldWithSuperclasses.forEach { field -> if (!traverseStaticFields && Modifier.isStatic(field.modifiers)) return@forEach - // we wrap an unsafe read into `runCatching` to handle `UnsupportedOperationException`, - // which can be thrown, for instance, when attempting to read - // a field of a hidden or record class (starting from Java 15); - // in this case we fall back to read via reflection - val result = runCatching { readFieldViaUnsafe(obj, field) } - .recoverCatching { field.apply { isAccessible = true }.get(obj) } + val result = readFieldSafely(obj, field) // do not pass non-readable fields if (result.isSuccess) { val fieldValue = result.getOrNull() @@ -222,4 +221,4 @@ internal fun getAtomicArrayLength(arr: Any): Int { isAtomicFUArray(arr) -> arr.javaClass.getMethod("getSize").invoke(arr) as Int else -> error("Argument is not atomic array") } -} +} \ No newline at end of file diff --git a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt index 7bda522fd..f891c0c5b 100644 --- a/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt +++ b/src/jvm/main/org/jetbrains/kotlinx/lincheck/util/UnsafeHolder.kt @@ -79,4 +79,81 @@ internal fun getArrayElementOffsetViaUnsafe(arr: Any, index: Int): Long { val baseOffset = UnsafeHolder.UNSAFE.arrayBaseOffset(clazz).toLong() val indexScale = UnsafeHolder.UNSAFE.arrayIndexScale(clazz).toLong() return baseOffset + index * indexScale +} + +@Suppress("DEPRECATION") +internal inline fun writeFieldViaUnsafe(obj: Any?, field: Field, value: Any?, setter: Unsafe.(Any?, Long, Any?) -> Unit) { + if (Modifier.isStatic(field.modifiers)) { + val base = UnsafeHolder.UNSAFE.staticFieldBase(field) + val offset = UnsafeHolder.UNSAFE.staticFieldOffset(field) + return UnsafeHolder.UNSAFE.setter(base, offset, value) + } else { + val offset = UnsafeHolder.UNSAFE.objectFieldOffset(field) + return UnsafeHolder.UNSAFE.setter(obj, offset, value) + } +} + +@Suppress("NAME_SHADOWING") +internal fun writeFieldViaUnsafe(obj: Any?, field: Field, value: Any?) { + if (!field.type.isPrimitive) { + return writeFieldViaUnsafe(obj, field, value, Unsafe::putObject) + } + return when (field.type) { + Boolean::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putBoolean(obj, field, value as Boolean) } + Byte::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putByte(obj, field, value as Byte) } + Char::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putChar(obj, field, value as Char) } + Short::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putShort(obj, field, value as Short) } + Int::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putInt(obj, field, value as Int) } + Long::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putLong(obj, field, value as Long) } + Double::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putDouble(obj, field, value as Double) } + Float::class.javaPrimitiveType -> writeFieldViaUnsafe(obj, field, value) { obj, field, value -> putFloat(obj, field, value as Float) } + else -> error("No more types expected") + } +} + +internal fun writeArrayElementViaUnsafe(arr: Any, index: Int, value: Any?): Any? { + val offset = getArrayElementOffsetViaUnsafe(arr, index) + val componentType = arr::class.java.componentType + + if (!componentType.isPrimitive) { + return UnsafeHolder.UNSAFE.putObject(arr, offset, value) + } + + return when (componentType) { + Boolean::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putBoolean(arr, offset, value as Boolean) + Byte::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putByte(arr, offset, value as Byte) + Char::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putChar(arr, offset, value as Char) + Short::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putShort(arr, offset, value as Short) + Int::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putInt(arr, offset, value as Int) + Long::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putLong(arr, offset, value as Long) + Double::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putDouble(arr, offset, value as Double) + Float::class.javaPrimitiveType -> UnsafeHolder.UNSAFE.putFloat(arr, offset, value as Float) + else -> error("No more primitive types expected") + } +} + +@Suppress("DEPRECATION") +internal fun getFieldOffsetViaUnsafe(field: Field): Long { + return if (Modifier.isStatic(field.modifiers)) { + UnsafeHolder.UNSAFE.staticFieldOffset(field) + } + else { + UnsafeHolder.UNSAFE.objectFieldOffset(field) + } +} + +@Suppress("DEPRECATION") +internal fun findFieldNameByOffsetViaUnsafe(targetType: Class<*>, offset: Long): String? { + // Extract the private offset value and find the matching field. + for (field in targetType.declaredFields) { + try { + if (Modifier.isNative(field.modifiers)) continue + val fieldOffset = if (Modifier.isStatic(field.modifiers)) UnsafeHolder.UNSAFE.staticFieldOffset(field) + else UnsafeHolder.UNSAFE.objectFieldOffset(field) + if (fieldOffset == offset) return field.name + } catch (t: Throwable) { + t.printStackTrace() + } + } + return null // Field not found } \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt index 8bed18304..22d413037 100644 --- a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/AbstractLincheckTest.kt @@ -13,7 +13,6 @@ import org.jetbrains.kotlinx.lincheck.* import org.jetbrains.kotlinx.lincheck.strategy.* import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions import org.jetbrains.kotlinx.lincheck.strategy.stress.* -import org.jetbrains.kotlinx.lincheck.verifier.* import org.jetbrains.kotlinx.lincheck_test.util.* import org.junit.* import kotlin.reflect.* diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/AbstractSnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/AbstractSnapshotTest.kt new file mode 100644 index 000000000..28e87646d --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/AbstractSnapshotTest.kt @@ -0,0 +1,45 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.ExceptionResult +import org.jetbrains.kotlinx.lincheck.check +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.parallelResults +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import org.jetbrains.kotlinx.lincheck.verifier.Verifier +import org.junit.Test + +abstract class AbstractSnapshotTest { + abstract class SnapshotVerifier() : Verifier { + protected fun checkForExceptions(results: ExecutionResult?) { + results?.parallelResults?.forEach { threadsResults -> + threadsResults.forEach { result -> + if (result is ExceptionResult) { + throw result.throwable + } + } + } + } + } + + protected open fun > O.customize() {} + + @Test + open fun testModelChecking() = ModelCheckingOptions() + .iterations(1) + .actorsBefore(0) + .actorsAfter(0) + .actorsPerThread(2) + .apply { customize() } + .check(this::class) +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ArraySnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ArraySnapshotTest.kt new file mode 100644 index 000000000..559811584 --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ArraySnapshotTest.kt @@ -0,0 +1,82 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions +import kotlin.random.Random + + +private var intArray = intArrayOf(1, 2, 3) + +class StaticIntArraySnapshotTest : AbstractSnapshotTest() { + companion object { + private var ref = intArray + private var values = intArray.copyOf() + } + + class StaticIntArrayVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(intArray == ref) + check(ref.contentEquals(values)) + return true + } + } + + override fun > O.customize() { + verifier(StaticIntArrayVerifier::class.java) + } + + @Operation + fun modify() { + intArray[0]++ + } +} + + +private class X(var value: Int) { + @OptIn(ExperimentalStdlibApi::class) + override fun toString() = "X@${this.hashCode().toHexString()}($value)" +} + +private var objArray = arrayOf(X(1), X(2), X(3)) + +class StaticObjectArraySnapshotTest : AbstractSnapshotTest() { + companion object { + private var ref: Array = objArray + private var elements: Array = Array(3) { null }.also { objArray.forEachIndexed { index, x -> it[index] = x } } + private var values: Array = Array(3) { 0 }.also { objArray.forEachIndexed { index, x -> it[index] = x.value } } + } + + class StaticObjectArrayVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(objArray == ref) + check(objArray.contentEquals(elements)) + check(objArray.map { it.value }.toTypedArray().contentEquals(values)) + return true + } + } + + override fun > O.customize() { + verifier(StaticObjectArrayVerifier::class.java) + } + + @Operation + fun modify() { + objArray[0].value++ + objArray[1].value-- + objArray[2] = X(Random.nextInt()) + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/AtomicFUSnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/AtomicFUSnapshotTest.kt new file mode 100644 index 000000000..67b1bd49a --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/AtomicFUSnapshotTest.kt @@ -0,0 +1,122 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import kotlinx.atomicfu.AtomicIntArray +import kotlinx.atomicfu.atomic +import kotlinx.atomicfu.atomicArrayOfNulls +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions +import kotlin.random.Random + +class AtomicFUSnapshotTest : AbstractSnapshotTest() { + companion object { + private class Wrapper(var x: Int) + + // TODO: atomicfu classes like AtomicInt, AtomicRef are compiled to pure field + java atomic field updater. + // Because of this, methods, such as `AtomicInt::getAndIncrement()`, will not be tracked as modification of `value` field. + // Tracking of atomicfu classes should be implemented eagerly, the same way as for java atomics + // in order to handle modification that do not reference `value` field directly. + // If eager traversal does not help/not possible to reach `value` field, then such indirect methods should be handled separately, + // the same way as reflexivity, var-handles, and unsafe by tracking the creation of java afu and retrieving the name of the associated field. + private val atomicFUInt = atomic(1) + private val atomicFURef = atomic(Wrapper(1)) + + private val atomicFUIntArray = AtomicIntArray(3) + private val atomicFURefArray = atomicArrayOfNulls(3) + + init { + for (i in 0..atomicFUIntArray.size - 1) { + atomicFUIntArray[i].value = i + 1 + } + + for (i in 0..atomicFURefArray.size - 1) { + atomicFURefArray[i].value = Wrapper(i + 1) + } + } + + // remember values to restore + private val atomicFURefValue = atomicFURef.value + private val atomicFUIntValues: List = mutableListOf().apply { + for (i in 0..atomicFUIntArray.size - 1) { + add(atomicFUIntArray[i].value) + } + } + private val atomicFURefArrayValues: List = mutableListOf().apply { + for (i in 0..atomicFURefArray.size - 1) { + add(atomicFURefArray[i].value!!) + } + } + } + + class AtomicFUSnapshotVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + + check(atomicFUInt.value == 1) + + check(atomicFURef.value == atomicFURefValue) + check(atomicFURef.value.x == 1) + + for (i in 0..atomicFUIntArray.size - 1) { + check(atomicFUIntArray[i].value == atomicFUIntValues[i]) + } + + for (i in 0..atomicFURefArray.size - 1) { + check(atomicFURefArray[i].value == atomicFURefArrayValues[i]) + check(atomicFURefArray[i].value!!.x == i + 1) + } + + return true + } + } + + override fun > O.customize() { + verifier(AtomicFUSnapshotVerifier::class.java) + threads(1) + iterations(100) + invocationsPerIteration(1) + actorsPerThread(10) + } + + @Operation + fun modifyAtomicFUInt() { + // TODO: `atomicFUInt.incrementAndGet()` this is not recognized as `value` field modification right now + atomicFUInt.value = Random.nextInt() + } + + @Operation + fun modifyAtomicFURef() { + atomicFURef.value = Wrapper(Random.nextInt()) + } + + @Operation + fun modifyAtomicFURefValue() { + atomicFURef.value.x = Random.nextInt() + } + + @Operation + fun modifyAtomicFUIntArray() { + atomicFUIntArray[Random.nextInt(0, atomicFUIntArray.size)].value = Random.nextInt() + } + + @Operation + fun modifyAtomicFURefArray() { + atomicFURefArray[Random.nextInt(0, atomicFURefArray.size)].value = Wrapper(Random.nextInt()) + } + + @Operation + fun modifyAtomicFURefArrayValues() { + atomicFURefArray[Random.nextInt(0, atomicFURefArray.size)].value!!.x = Random.nextInt() + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/CollectionSnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/CollectionSnapshotTest.kt new file mode 100644 index 000000000..669358aee --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/CollectionSnapshotTest.kt @@ -0,0 +1,328 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.LoggingLevel +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.check +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions +import java.util.PriorityQueue +import java.util.Queue +import java.util.concurrent.ConcurrentHashMap +import kotlin.random.Random + + +abstract class CollectionSnapshotTest : AbstractSnapshotTest() { + + override fun testModelChecking() = ModelCheckingOptions() + .actorsBefore(0) + .actorsAfter(0) + .iterations(100) + .invocationsPerIteration(1) + .threads(1) + .actorsPerThread(10) + .apply { customize() } + .check(this::class) + + @Operation + open fun addElement() {} + + @Operation + open fun removeElement() {} + + @Operation + open fun updateElement() {} + + @Operation + open fun clear() {} + + @Operation + open fun reassign() {} +} + +class SetSnapshotTest : CollectionSnapshotTest() { + companion object { + private class Wrapper(var value: Int) + private var staticSet = mutableSetOf() + + // remember values for restoring + private val ref = staticSet + private val a = Wrapper(1) + private val b = Wrapper(2) + private val c = Wrapper(3) + init { + staticSet.addAll(listOf(a, b, c)) + } + } + + class SetVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(staticSet == ref) + check(staticSet.size == 3 && staticSet.containsAll(listOf(a, b, c))) + check(a.value == 1 && b.value == 2 && c.value == 3) + return true + } + } + + override fun > O.customize() { + verifier(SetVerifier::class.java) + } + + @Operation + override fun addElement() { + staticSet.add(Wrapper(Random.nextInt())) + } + + @Operation + override fun removeElement() { + staticSet.remove(staticSet.randomOrNull() ?: return) + } + + @Operation + override fun updateElement() { + staticSet.randomOrNull()?.value = Random.nextInt() + } + + @Operation + override fun clear() { + staticSet.clear() + } + + @Operation + override fun reassign() { + staticSet = mutableSetOf() + } +} + +class MapSnapshotTest : CollectionSnapshotTest() { + companion object { + private class Wrapper(var value: Int) + private var staticMap = mutableMapOf() + + // remember values for restoring + private val ref = staticMap + private val a = Wrapper(1) + private val b = Wrapper(2) + private val c = Wrapper(3) + init { + staticMap.put(1, a) + staticMap.put(2, b) + staticMap.put(3, c) + } + } + + class MapVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(staticMap == ref) + check(staticMap.size == 3 && staticMap[1] == a && staticMap[2] == b && staticMap[3] == c) + check(a.value == 1 && b.value == 2 && c.value == 3) + return true + } + } + + override fun > O.customize() { + verifier(MapVerifier::class.java) + } + + @Operation + override fun addElement() { + staticMap.put(Random.nextInt(), Wrapper(Random.nextInt())) + } + + @Operation + override fun removeElement() { + staticMap.remove(staticMap.keys.randomOrNull() ?: return) + } + + @Operation + override fun updateElement() { + staticMap.entries.randomOrNull()?.value?.value = Random.nextInt() + } + + @Operation + override fun clear() { + staticMap.clear() + } + + @Operation + override fun reassign() { + staticMap = mutableMapOf() + } +} + +class ListSnapshotTest : CollectionSnapshotTest() { + companion object { + private class Wrapper(var value: Int) + private var staticList = mutableListOf() + + // remember values for restoring + private val ref = staticList + private val a = Wrapper(1) + private val b = Wrapper(2) + private val c = Wrapper(3) + init { + staticList.addAll(listOf(a, b, c)) + } + } + + class ListVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(staticList == ref) + check(staticList.size == 3 && staticList[0] == a && staticList[1] == b && staticList[2] == c) + check(a.value == 1 && b.value == 2 && c.value == 3) + return true + } + } + + override fun > O.customize() { + verifier(ListVerifier::class.java) + } + + @Operation + override fun addElement() { + staticList.add(Wrapper(Random.nextInt())) + } + + @Operation + override fun removeElement() { + staticList.remove(staticList.randomOrNull() ?: return) + } + + @Operation + override fun updateElement() { + staticList.randomOrNull()?.value = Random.nextInt() + } + + @Operation + override fun clear() { + staticList.clear() + } + + @Operation + override fun reassign() { + staticList = mutableListOf() + } +} + +class PriorityQueueSnapshotTest : CollectionSnapshotTest() { + companion object { + private var staticQueue: Queue = PriorityQueue() + + // remember values for restoring + private val ref = staticQueue + init { + staticQueue.addAll(listOf(3, 1, 2)) + } + } + + class PriorityQueueVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(staticQueue == ref) + check(staticQueue.size == 3 && staticQueue.containsAll(listOf(1, 2, 3))) + var i = 1 + while (staticQueue.isNotEmpty()) { + check(staticQueue.remove() == i++) + } + staticQueue.addAll(listOf(3, 1, 2)) + return true + } + } + + override fun > O.customize() { + verifier(PriorityQueueVerifier::class.java) + } + + @Operation + override fun addElement() { + staticQueue.add(Random.nextInt()) + } + + @Operation + override fun removeElement() { + staticQueue.remove(staticQueue.randomOrNull() ?: return) + } + + @Operation + override fun clear() { + staticQueue.clear() + } + + @Operation + override fun reassign() { + staticQueue = PriorityQueue() + } +} + +class ConcurrentMapSnapshotTest : CollectionSnapshotTest() { + companion object { + private class Wrapper(var value: Int) + private var staticCMap = ConcurrentHashMap() + + // remember values for restoring + private val ref = staticCMap + private val a = Wrapper(1) + private val b = Wrapper(2) + private val c = Wrapper(3) + init { + staticCMap.putAll(mapOf( + 1 to a, + 2 to b, + 3 to c + )) + } + } + + class ConcurrentMapVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(staticCMap == ref) + check(staticCMap.size == 3 && staticCMap[1] == a && staticCMap[2] == b && staticCMap[3] == c) + check(a.value == 1 && b.value == 2 && c.value == 3) + return true + } + } + + override fun > O.customize() { + verifier(ConcurrentMapVerifier::class.java) + } + + @Operation + override fun addElement() { + staticCMap.put(Random.nextInt(), Wrapper(Random.nextInt())) + } + + @Operation + override fun updateElement() { + staticCMap.values.randomOrNull()?.value = Random.nextInt() + } + + @Operation + override fun removeElement() { + staticCMap.remove(staticCMap.keys.randomOrNull() ?: return) + } + + @Operation + override fun clear() { + staticCMap.clear() + } + + @Operation + override fun reassign() { + staticCMap = ConcurrentHashMap() + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ConstructorArgumentsTrackingTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ConstructorArgumentsTrackingTest.kt new file mode 100644 index 000000000..93ec164a4 --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ConstructorArgumentsTrackingTest.kt @@ -0,0 +1,60 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions + + +class ConstructorArgumentsTrackingTest : AbstractSnapshotTest() { + class ConstructorNotThisModificationVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults( + scenario: ExecutionScenario?, + results: ExecutionResult? + ): Boolean { + checkForExceptions(results) + check(staticNode.a == 1) + check(staticSubNode.a == 2) + return true + } + } + + override fun > O.customize() { + verifier(ConstructorNotThisModificationVerifier::class.java) + threads(1) + invocationsPerIteration(1) + actorsPerThread(1) + } + + open class Node(var a: Int) { + constructor(other: Node, other2: SubNode) : this(2) { + other.a = 0 // this change should be tracked because no 'leaking this' problem exists here + + val castedOther2 = other2 as Node + castedOther2.a = 0 // this change should be tracked because no 'leaking this' problem exists here + } + } + + class SubNode(a: Int) : Node(a) + + companion object { + val staticNode = Node(1) + val staticSubNode = SubNode(2) + } + + @Operation + @Suppress("UNUSED_VARIABLE") + fun modify() { + val localNode = Node(staticNode, staticSubNode) + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/EnumSnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/EnumSnapshotTest.kt new file mode 100644 index 000000000..6a2856325 --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/EnumSnapshotTest.kt @@ -0,0 +1,56 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions + + +private enum class Values { + A, B, C; +} +private class EnumHolder(var x: Values, var y: Values) + +private var global = EnumHolder(Values.A, Values.B) + +class EnumSnapshotTest : AbstractSnapshotTest() { + companion object { + private var initA: EnumHolder = global + private var initX: Values = global.x + private var initY: Values = global.y + } + + class StaticEnumVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(global == initA) + check(global.x == initX) + check(global.y == initY) + return true + } + } + + override fun > O.customize() { + verifier(StaticEnumVerifier::class.java) + } + + @Operation + fun modifyFields() { + // modified fields of the initial instance + global.x = Values.B + global.y = Values.C + + // assign different instance to the variable + global = EnumHolder(Values.C, Values.C) + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/InnerClassConstructorTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/InnerClassConstructorTest.kt new file mode 100644 index 000000000..5f866dd85 --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/InnerClassConstructorTest.kt @@ -0,0 +1,50 @@ +package org.jetbrains.kotlinx.lincheck_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.Options +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.* +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions +import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions + + +private class Outer { + class C(@JvmField var value: Int) + + @JvmField + var c = C(1) + + inner class Inner { + val linkToOuterValue: C + init { + linkToOuterValue = this@Outer.c + } + + fun changeA() { + linkToOuterValue.value = 2 + } + } +} + +private val a = Outer() + +class InnerClassConstructorTest : AbstractSnapshotTest() { + class InnerClassVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(a.c.value == 1) + return true + } + } + + override fun > O.customize() { + iterations(1) + invocationsPerIteration(1) + verifier(InnerClassVerifier::class.java) + } + + @Operation + fun test() { + val b = a.Inner() + b.changeA() + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/JavaAtomicsSnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/JavaAtomicsSnapshotTest.kt new file mode 100644 index 000000000..540ee68d3 --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/JavaAtomicsSnapshotTest.kt @@ -0,0 +1,100 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicIntegerArray +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.atomic.AtomicReferenceArray +import kotlin.random.Random + + +class JavaAtomicsSnapshotTest : AbstractSnapshotTest() { + companion object { + private class Wrapper(var x: Int) + + private var atomicInt = AtomicInteger(1) + private var atomicRef = AtomicReference(Wrapper(1)) + private var atomicIntArray = AtomicIntegerArray(3).apply { for (i in 0..length() - 1) set(i, i + 1); } + private var atomicRefArray = AtomicReferenceArray(3).apply { for (i in 0..length() - 1) set(i, Wrapper(i + 1)); } + + // remember values to restore + private var intRef: AtomicInteger = atomicInt + private var intValue: Int = atomicInt.get() + private var refRef: AtomicReference = atomicRef + private var refValue: Wrapper = atomicRef.get() + private var intArrayRef: AtomicIntegerArray = atomicIntArray + private var refArrayRef: AtomicReferenceArray = atomicRefArray + private var refArrayValues: List = mutableListOf().apply { for (i in 0..atomicRefArray.length() - 1) add(atomicRefArray.get(i)) } + } + + class StaticObjectVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(atomicInt == intRef) + check(atomicInt.get() == intValue) + + check(atomicRef == refRef) + check(atomicRef.get() == refValue) + check(atomicRef.get().x == 1) + + check(atomicIntArray == intArrayRef) + for (i in 0..atomicIntArray.length() - 1) { + check(atomicIntArray.get(i) == i + 1) + } + + check(atomicRefArray == refArrayRef) + for (i in 0..atomicRefArray.length() - 1) { + val wrapper = atomicRefArray.get(i) + check(wrapper == refArrayValues[i]) + check(wrapper.x == i + 1) + } + return true + } + } + + override fun > O.customize() { + verifier(StaticObjectVerifier::class.java) + threads(1) + iterations(100) + invocationsPerIteration(1) + actorsPerThread(10) + } + + @Operation + fun modifyInt() { + atomicInt.getAndIncrement() + } + + @Operation + fun modifyRef() { + atomicRef.set(Wrapper(atomicInt.get() + 1)) + } + + @Operation + fun modifyIntArray() { + atomicIntArray.getAndIncrement(Random.nextInt(0, atomicIntArray.length())) + } + + @Operation + fun modifyRefArray() { + atomicRefArray.set(Random.nextInt(0, atomicRefArray.length()), Wrapper(Random.nextInt())) + } + + @Operation + fun modifyRefArrayValues() { + atomicRefArray.get(Random.nextInt(0, atomicRefArray.length())).x = Random.nextInt() + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ObjectAsFieldSnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ObjectAsFieldSnapshotTest.kt new file mode 100644 index 000000000..5077e7e98 --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ObjectAsFieldSnapshotTest.kt @@ -0,0 +1,59 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions + + +private object Static1 { + var f1: Static2? = Static2 + var f2: Int = 0 +} + +private object Static2 { + var f1: Int = 1 + var f2: String = "abc" +} + +class ObjectAsFieldSnapshotTest : AbstractSnapshotTest() { + companion object { + private val initS1f1 = Static1.f1 + private val initS1f2 = Static1.f2 + + private val initS2f1 = Static2.f1 + private val initS2f2 = Static2.f2 + } + + class StaticObjectAsFieldVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(Static1.f1 == initS1f1 && Static1.f2 == initS1f2) + check(Static2.f1 == initS2f1 && Static2.f2 == initS2f2) + return true + } + } + + override fun > O.customize() { + verifier(StaticObjectAsFieldVerifier::class.java) + } + + @Operation + fun modify() { + Static2.f1 = 10 + Static2.f2 = "cba" + + Static1.f1 = null + Static1.f2 = 10 + } +} \ No newline at end of file diff --git a/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ObjectCycleSnapshotTest.kt b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ObjectCycleSnapshotTest.kt new file mode 100644 index 000000000..3577b4fff --- /dev/null +++ b/src/jvm/test/org/jetbrains/kotlinx/lincheck_test/strategy/modelchecking/snapshot/ObjectCycleSnapshotTest.kt @@ -0,0 +1,51 @@ +/* + * 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_test.strategy.modelchecking.snapshot + +import org.jetbrains.kotlinx.lincheck.annotations.Operation +import org.jetbrains.kotlinx.lincheck.execution.ExecutionResult +import org.jetbrains.kotlinx.lincheck.execution.ExecutionScenario +import org.jetbrains.kotlinx.lincheck.strategy.managed.ManagedOptions + +private class A(var b: B) +private class B(var a: A? = null) + +private var globalA = A(B()) + +class ObjectCycleSnapshotTest : AbstractSnapshotTest() { + companion object { + private var initA = globalA + private var initB = globalA.b + + init { + globalA.b.a = globalA + } + } + + class StaticObjectCycleVerifier(@Suppress("UNUSED_PARAMETER") sequentialSpecification: Class<*>) : SnapshotVerifier() { + override fun verifyResults(scenario: ExecutionScenario?, results: ExecutionResult?): Boolean { + checkForExceptions(results) + check(globalA == initA) + check(globalA.b == initB) + check(globalA.b.a == globalA) + return true + } + } + + override fun > O.customize() { + verifier(StaticObjectCycleVerifier::class.java) + } + + @Operation + fun modify() { + globalA = A(B()) + } +} \ No newline at end of file diff --git a/src/jvm/test/resources/expected_logs/atomic_references_names_trace.txt b/src/jvm/test/resources/expected_logs/atomic_references_names_trace.txt index aced75030..91f5e5e8e 100644 --- a/src/jvm/test/resources/expected_logs/atomic_references_names_trace.txt +++ b/src/jvm/test/resources/expected_logs/atomic_references_names_trace.txt @@ -13,38 +13,38 @@ The following interleaving leads to the error: | ----------- | Detailed trace: -| --------------------------------------------------------------------------------------------------------------------------------------------- | -| Thread 1 | -| --------------------------------------------------------------------------------------------------------------------------------------------- | -| operation() | -| atomicReference.get(): Node#1 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:29) | -| atomicReference.compareAndSet(Node#1,Node#2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:29) | -| atomicReference.set(Node#3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:30) | -| atomicInteger.get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:32) | -| atomicInteger.compareAndSet(0,2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:32) | -| atomicInteger.set(3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:33) | -| atomicLong.get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:35) | -| atomicLong.compareAndSet(0,2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:35) | -| atomicLong.set(3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:36) | -| atomicBoolean.get(): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:38) | -| atomicBoolean.compareAndSet(true,true): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:38) | -| atomicBoolean.set(false) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:39) | -| atomicReferenceArray[0].get(): Node#4 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:41) | -| atomicReferenceArray[0].compareAndSet(Node#4,Node#5): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:41) | -| atomicReferenceArray[0].set(Node#6) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:42) | -| atomicIntegerArray[0].get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:44) | -| atomicIntegerArray[0].compareAndSet(0,1): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:44) | -| atomicIntegerArray[0].set(2) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:45) | -| atomicLongArray[0].get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:47) | -| atomicLongArray[0].compareAndSet(0,1): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:47) | -| atomicLongArray[0].set(2) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:48) | -| wrapper.reference.set(Node#7) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:50) | -| wrapper.array[0].compareAndSet(1,2): false at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:51) | -| AtomicReferencesNamesTest.staticValue.compareAndSet(0,2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:53) | -| AtomicReferencesNamesTest.staticValue.set(0) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:54) | -| AtomicReferenceWrapper.staticValue.compareAndSet(1,2): false at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:56) | -| AtomicReferenceWrapper.staticValue.set(3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:57) | -| AtomicReferencesNamesTest.staticArray[1].compareAndSet(0,1): false at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:59) | -| AtomicReferenceWrapper.staticArray[1].compareAndSet(0,1): false at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:60) | -| result: void | -| --------------------------------------------------------------------------------------------------------------------------------------------- | +| -------------------------------------------------------------------------------------------------------------------------------------------- | +| Thread 1 | +| -------------------------------------------------------------------------------------------------------------------------------------------- | +| operation() | +| atomicReference.get(): Node#1 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:29) | +| atomicReference.compareAndSet(Node#1,Node#2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:29) | +| atomicReference.set(Node#3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:30) | +| atomicInteger.get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:32) | +| atomicInteger.compareAndSet(0,2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:32) | +| atomicInteger.set(3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:33) | +| atomicLong.get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:35) | +| atomicLong.compareAndSet(0,2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:35) | +| atomicLong.set(3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:36) | +| atomicBoolean.get(): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:38) | +| atomicBoolean.compareAndSet(true,true): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:38) | +| atomicBoolean.set(false) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:39) | +| atomicReferenceArray[0].get(): Node#4 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:41) | +| atomicReferenceArray[0].compareAndSet(Node#4,Node#5): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:41) | +| atomicReferenceArray[0].set(Node#6) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:42) | +| atomicIntegerArray[0].get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:44) | +| atomicIntegerArray[0].compareAndSet(0,1): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:44) | +| atomicIntegerArray[0].set(2) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:45) | +| atomicLongArray[0].get(): 0 at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:47) | +| atomicLongArray[0].compareAndSet(0,1): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:47) | +| atomicLongArray[0].set(2) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:48) | +| wrapper.reference.set(Node#7) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:50) | +| wrapper.array[0].compareAndSet(1,2): false at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:51) | +| AtomicReferencesNamesTest.staticValue.compareAndSet(0,2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:53) | +| AtomicReferencesNamesTest.staticValue.set(0) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:54) | +| AtomicReferenceWrapper.staticValue.compareAndSet(1,2): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:56) | +| AtomicReferenceWrapper.staticValue.set(3) at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:57) | +| AtomicReferencesNamesTest.staticArray[1].compareAndSet(0,1): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:59) | +| AtomicReferenceWrapper.staticArray[1].compareAndSet(0,1): true at AtomicReferencesNamesTest.operation(AtomicReferencesNamesTests.kt:60) | +| result: void | +| -------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_boolean_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_boolean_representation.txt index 7258d7d6c..70b79d682 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_boolean_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_boolean_representation.txt @@ -13,19 +13,19 @@ The following interleaving leads to the error: | ----------- | Detailed trace: -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Thread 1 | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| operation() | -| number.READ: false at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:216) | -| number.compareAndSet(false,false): true at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:216) | -| number.set(true) at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:217) | -| VarHandleBooleanRepresentationTest.staticNumber.READ: false at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:219) | -| VarHandleBooleanRepresentationTest.staticNumber.compareAndSet(false,false): true at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:219) | -| VarHandleBooleanRepresentationTest.staticNumber.set(false) at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:220) | -| array.READ: BooleanArray#1 at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:222) | -| BooleanArray#1[1].compareAndSet(false,false): true at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:222) | -| array.READ: BooleanArray#1 at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:223) | -| BooleanArray#1[1].set(true) at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:223) | -| result: void | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Thread 1 | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| operation() | +| number.READ: false at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:216) | +| number.compareAndSet(false,false): true at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:216) | +| number.set(true) at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:217) | +| VarHandleBooleanRepresentationTest.staticNumber.READ: true at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:219) | +| VarHandleBooleanRepresentationTest.staticNumber.compareAndSet(true,false): true at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:219) | +| VarHandleBooleanRepresentationTest.staticNumber.set(false) at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:220) | +| array.READ: BooleanArray#1 at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:222) | +| BooleanArray#1[1].compareAndSet(false,false): true at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:222) | +| array.READ: BooleanArray#1 at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:223) | +| BooleanArray#1[1].set(true) at VarHandleBooleanRepresentationTest.operation(VarHandleRepresentationTests.kt:223) | +| result: void | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_byte_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_byte_representation.txt index 032774f9a..0a573ceac 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_byte_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_byte_representation.txt @@ -20,8 +20,8 @@ Detailed trace: | number.READ: 1 at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:142) | | number.compareAndSet(1,1): true at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:142) | | number.set(2) at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:143) | -| VarHandleByteRepresentationTest.staticNumber.READ: 3 at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:145) | -| VarHandleByteRepresentationTest.staticNumber.compareAndSet(3,1): true at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:145) | +| VarHandleByteRepresentationTest.staticNumber.READ: 2 at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:145) | +| VarHandleByteRepresentationTest.staticNumber.compareAndSet(2,1): true at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:145) | | VarHandleByteRepresentationTest.staticNumber.set(3) at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:146) | | array.READ: ByteArray#1 at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:148) | | ByteArray#1[1].compareAndSet(3,1): false at VarHandleByteRepresentationTest.operation(VarHandleRepresentationTests.kt:148) | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_char_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_char_representation.txt index 78673fe2c..625ae7d04 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_char_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_char_representation.txt @@ -20,8 +20,8 @@ Detailed trace: | number.READ: '1' at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:179) | | number.compareAndSet('1','1'): true at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:179) | | number.set('2') at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:180) | -| VarHandleCharRepresentationTest.staticNumber.READ: '3' at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:182) | -| VarHandleCharRepresentationTest.staticNumber.compareAndSet('3','1'): true at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:182) | +| VarHandleCharRepresentationTest.staticNumber.READ: '2' at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:182) | +| VarHandleCharRepresentationTest.staticNumber.compareAndSet('2','1'): true at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:182) | | VarHandleCharRepresentationTest.staticNumber.set('3') at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:183) | | array.READ: CharArray#1 at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:185) | | CharArray#1[1].compareAndSet('3','1'): false at VarHandleCharRepresentationTest.operation(VarHandleRepresentationTests.kt:185) | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_double_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_double_representation.txt index 59ba6afda..c21798dad 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_double_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_double_representation.txt @@ -20,8 +20,8 @@ Detailed trace: | number.READ: 1.0 at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:327) | | number.compareAndSet(1.0,1.0): true at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:327) | | number.set(2.0) at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:328) | -| VarHandleDoubleRepresentationTest.staticNumber.READ: 3.0 at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:330) | -| VarHandleDoubleRepresentationTest.staticNumber.compareAndSet(3.0,1.0): true at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:330) | +| VarHandleDoubleRepresentationTest.staticNumber.READ: 2.0 at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:330) | +| VarHandleDoubleRepresentationTest.staticNumber.compareAndSet(2.0,1.0): true at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:330) | | VarHandleDoubleRepresentationTest.staticNumber.set(3.0) at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:331) | | array.READ: DoubleArray#1 at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:333) | | DoubleArray#1[1].compareAndSet(3.0,1.0): false at VarHandleDoubleRepresentationTest.operation(VarHandleRepresentationTests.kt:333) | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_float_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_float_representation.txt index d9d34d5ea..c4863d63f 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_float_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_float_representation.txt @@ -20,8 +20,8 @@ Detailed trace: | number.READ: 1.0 at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:290) | | number.compareAndSet(1.0,1.0): true at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:290) | | number.set(2.0) at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:291) | -| VarHandleFloatRepresentationTest.staticNumber.READ: 3.0 at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:293) | -| VarHandleFloatRepresentationTest.staticNumber.compareAndSet(3.0,1.0): true at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:293) | +| VarHandleFloatRepresentationTest.staticNumber.READ: 2.0 at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:293) | +| VarHandleFloatRepresentationTest.staticNumber.compareAndSet(2.0,1.0): true at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:293) | | VarHandleFloatRepresentationTest.staticNumber.set(3.0) at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:294) | | array.READ: FloatArray#1 at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:296) | | FloatArray#1[1].compareAndSet(3.0,1.0): false at VarHandleFloatRepresentationTest.operation(VarHandleRepresentationTests.kt:296) | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_int_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_int_representation.txt index 533825f6d..5eb62b287 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_int_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_int_representation.txt @@ -20,8 +20,8 @@ Detailed trace: | number.READ: 1 at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:70) | | number.compareAndSet(1,2): true at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:70) | | number.set(3) at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:71) | -| VarHandleIntRepresentationTest.staticNumber.READ: 3 at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:73) | -| VarHandleIntRepresentationTest.staticNumber.compareAndSet(3,2): true at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:73) | +| VarHandleIntRepresentationTest.staticNumber.READ: 1 at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:73) | +| VarHandleIntRepresentationTest.staticNumber.compareAndSet(1,2): true at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:73) | | VarHandleIntRepresentationTest.staticNumber.set(3) at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:74) | | array.READ: IntArray#1 at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:76) | | IntArray#1[1].compareAndSet(1,1): true at VarHandleIntRepresentationTest.operation(VarHandleRepresentationTests.kt:76) | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_long_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_long_representation.txt index 6562e4810..43a04a483 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_long_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_long_representation.txt @@ -20,8 +20,8 @@ Detailed trace: | number.READ: 1 at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:253) | | number.compareAndSet(1,1): true at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:253) | | number.set(2) at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:254) | -| VarHandleLongRepresentationTest.staticNumber.READ: 3 at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:256) | -| VarHandleLongRepresentationTest.staticNumber.compareAndSet(3,1): true at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:256) | +| VarHandleLongRepresentationTest.staticNumber.READ: 2 at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:256) | +| VarHandleLongRepresentationTest.staticNumber.compareAndSet(2,1): true at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:256) | | VarHandleLongRepresentationTest.staticNumber.set(3) at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:257) | | array.READ: LongArray#1 at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:259) | | LongArray#1[1].compareAndSet(3,1): false at VarHandleLongRepresentationTest.operation(VarHandleRepresentationTests.kt:259) | diff --git a/src/jvm/test/resources/expected_logs/var_handle/varhandle_short_representation.txt b/src/jvm/test/resources/expected_logs/var_handle/varhandle_short_representation.txt index eb91b96d9..e68841103 100644 --- a/src/jvm/test/resources/expected_logs/var_handle/varhandle_short_representation.txt +++ b/src/jvm/test/resources/expected_logs/var_handle/varhandle_short_representation.txt @@ -20,8 +20,8 @@ Detailed trace: | number.READ: 1 at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:105) | | number.compareAndSet(1,1): true at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:105) | | number.set(2) at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:106) | -| VarHandleShortRepresentationTest.staticNumber.READ: 3 at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:108) | -| VarHandleShortRepresentationTest.staticNumber.compareAndSet(3,1): true at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:108) | +| VarHandleShortRepresentationTest.staticNumber.READ: 2 at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:108) | +| VarHandleShortRepresentationTest.staticNumber.compareAndSet(2,1): true at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:108) | | VarHandleShortRepresentationTest.staticNumber.set(3) at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:109) | | array.READ: ShortArray#1 at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:111) | | ShortArray#1[1].compareAndSet(3,1): false at VarHandleShortRepresentationTest.operation(VarHandleRepresentationTests.kt:111) |