Skip to content

Commit

Permalink
Show the field name in the trace when calling a method on the final f…
Browse files Browse the repository at this point in the history
…ield (#327)
  • Loading branch information
avpotapov00 authored May 9, 2024
1 parent dcb2fa9 commit 196b345
Show file tree
Hide file tree
Showing 13 changed files with 316 additions and 335 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,8 @@ package org.jetbrains.kotlinx.lincheck.strategy.managed
import kotlinx.atomicfu.AtomicArray
import kotlinx.atomicfu.AtomicBooleanArray
import kotlinx.atomicfu.AtomicIntArray
import org.jetbrains.kotlinx.lincheck.allDeclaredFieldWithSuperclasses
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceMethodType.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceNames.AtomicReferenceOwnerWithName.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceNames.TraverseResult.*
import java.lang.reflect.Modifier
import org.jetbrains.kotlinx.lincheck.strategy.managed.OwnerWithName.*
import java.util.*
import java.util.concurrent.atomic.AtomicIntegerArray
import java.util.concurrent.atomic.AtomicLongArray
Expand All @@ -37,16 +34,16 @@ internal object AtomicReferenceNames {
atomicReference: Any,
parameters: Array<Any?>
): AtomicReferenceMethodType {
val receiverAndName = getAtomicReferenceReceiverAndName(testObject, atomicReference)
val receiverAndName = FieldSearchHelper.findFinalFieldWithOwner(testObject, atomicReference)
return if (receiverAndName != null) {
if (isAtomicArrayIndexMethodCall(atomicReference, parameters)) {
when (receiverAndName) {
is InstanceOwnerWithName -> InstanceFieldAtomicArrayMethod(receiverAndName.receiver, receiverAndName.fieldName, parameters[0] as Int)
is InstanceOwnerWithName -> InstanceFieldAtomicArrayMethod(receiverAndName.owner, receiverAndName.fieldName, parameters[0] as Int)
is StaticOwnerWithName -> StaticFieldAtomicArrayMethod(receiverAndName.clazz, receiverAndName.fieldName, parameters[0] as Int)
}
} else {
when (receiverAndName) {
is InstanceOwnerWithName -> AtomicReferenceInstanceMethod(receiverAndName.receiver, receiverAndName.fieldName)
is InstanceOwnerWithName -> AtomicReferenceInstanceMethod(receiverAndName.owner, receiverAndName.fieldName)
is StaticOwnerWithName -> AtomicReferenceStaticMethod(receiverAndName.clazz, receiverAndName.fieldName)
}
}
Expand All @@ -69,70 +66,7 @@ internal object AtomicReferenceNames {
atomicReference is AtomicBooleanArray
}

private fun getAtomicReferenceReceiverAndName(testObject: Any, reference: Any): AtomicReferenceOwnerWithName? =
runCatching {
val visitedObjects: MutableSet<Any> = Collections.newSetFromMap(IdentityHashMap())
return when (val result = findObjectField(testObject, reference, visitedObjects)) {
is FieldName -> result.fieldName
MultipleFieldsMatching, NotFound -> null
}
}.getOrElse { exception ->
exception.printStackTrace()
null
}

private sealed interface TraverseResult {
data object NotFound : TraverseResult
data class FieldName(val fieldName: AtomicReferenceOwnerWithName) : TraverseResult
data object MultipleFieldsMatching : TraverseResult
}

private fun findObjectField(testObject: Any?, value: Any, visitedObjects: MutableSet<Any>): TraverseResult {
if (testObject == null) return NotFound
var fieldName: AtomicReferenceOwnerWithName? = null
// We take all the fields from the hierarchy.
// If two or more fields match (===) the AtomicReference object, we fall back to the default behavior,
// so there is no problem that we can receive some fields of the same name and the same type.
for (field in testObject::class.java.allDeclaredFieldWithSuperclasses) {
if (field.type.isPrimitive || !field.trySetAccessible()) continue
val fieldValue = field.get(testObject)

if (fieldValue in visitedObjects) continue
visitedObjects += testObject

if (fieldValue === value) {
if (fieldName != null) return MultipleFieldsMatching

fieldName = if (Modifier.isStatic(field.modifiers)) {
StaticOwnerWithName(field.name, testObject::class.java)
} else {
InstanceOwnerWithName(field.name, testObject)
}
continue
}
when (val result = findObjectField(fieldValue, value, visitedObjects)) {
is FieldName -> {
if (fieldName != null) {
return MultipleFieldsMatching
} else {
fieldName = result.fieldName
}
}

MultipleFieldsMatching -> return result
NotFound -> {}
}
}
return if (fieldName != null) FieldName(fieldName) else NotFound
}

private sealed class AtomicReferenceOwnerWithName(val fieldName: String) {
class StaticOwnerWithName(fieldName: String, val clazz: Class<*>) :
AtomicReferenceOwnerWithName(fieldName)

class InstanceOwnerWithName(fieldName: String, val receiver: Any) :
AtomicReferenceOwnerWithName(fieldName)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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 kotlinx.atomicfu.AtomicArray
import kotlinx.atomicfu.AtomicRef
import org.jetbrains.kotlinx.lincheck.allDeclaredFieldWithSuperclasses
import org.jetbrains.kotlinx.lincheck.strategy.managed.FieldSearchHelper.TraverseResult.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.OwnerWithName.*
import org.jetbrains.kotlinx.lincheck.util.readFieldViaUnsafe
import sun.misc.Unsafe
import java.lang.reflect.Modifier
import java.util.*
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.atomic.AtomicReferenceArray


/**
* Utility class that helps to determine if the provided field stored in only one
* final field of a tested object.
*/
internal object FieldSearchHelper {

/**
* Determines if the [value] is stored in the only one field of the [testObject] and this
* field is final.
* In case the [value] is not found or accessible by multiple fields, the function returns `null`.
*/
internal fun findFinalFieldWithOwner(testObject: Any, value: Any): OwnerWithName? = runCatching {
val visitedObjects: MutableSet<Any> = Collections.newSetFromMap(IdentityHashMap())
return when (val result = findObjectField(testObject, value, visitedObjects)) {
is FieldName -> result.field
MultipleFieldsMatching, NotFound, FoundInNonFinalField -> null
}
}.getOrElse { exception ->
exception.printStackTrace()
null
}

private sealed interface TraverseResult {
data object NotFound : TraverseResult
data class FieldName(val field: OwnerWithName) : TraverseResult
data object MultipleFieldsMatching : TraverseResult
data object FoundInNonFinalField: TraverseResult
}

private fun findObjectField(testObject: Any?, value: Any, visitedObjects: MutableSet<Any>): TraverseResult {
if (testObject == null) return NotFound
var fieldName: OwnerWithName? = null
// We take all the fields from the hierarchy.
// If two or more fields match (===) the AtomicReference object, we fall back to the default behavior,
// so there is no problem that we can receive some fields of the same name and the same type.
for (field in testObject::class.java.allDeclaredFieldWithSuperclasses) {
if (field.type.isPrimitive) continue
val fieldValue = readFieldViaUnsafe(testObject, field, Unsafe::getObject)

if (fieldValue in visitedObjects) continue
visitedObjects += testObject

if (fieldValue === value) {
if (fieldName != null) return MultipleFieldsMatching
if (!Modifier.isFinal(field.modifiers)) return FoundInNonFinalField

fieldName = if (Modifier.isStatic(field.modifiers)) {
StaticOwnerWithName(field.name, testObject::class.java)
} else {
InstanceOwnerWithName(field.name, testObject)
}
continue
}
when (val result = findObjectField(fieldValue, value, visitedObjects)) {
is FieldName -> {
if (fieldName != null) {
return MultipleFieldsMatching
} else {
fieldName = result.field
}
}

MultipleFieldsMatching, FoundInNonFinalField -> return result
NotFound -> {}
}
}
return if (fieldName != null) FieldName(fieldName) else NotFound
}

}

internal sealed class OwnerWithName(val fieldName: String) {
class StaticOwnerWithName(fieldName: String, val clazz: Class<*>) :
OwnerWithName(fieldName)

class InstanceOwnerWithName(fieldName: String, val owner: Any) :
OwnerWithName(fieldName)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import sun.nio.ch.lincheck.*
import kotlinx.coroutines.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicFieldUpdaterNames.getAtomicFieldUpdaterName
import org.jetbrains.kotlinx.lincheck.strategy.managed.AtomicReferenceMethodType.*
import org.jetbrains.kotlinx.lincheck.strategy.managed.FieldSearchHelper.findFinalFieldWithOwner
import org.jetbrains.kotlinx.lincheck.strategy.managed.ObjectLabelFactory.adornedStringRepresentation
import org.jetbrains.kotlinx.lincheck.strategy.managed.ObjectLabelFactory.cleanObjectNumeration
import org.jetbrains.kotlinx.lincheck.strategy.managed.UnsafeName.*
Expand Down Expand Up @@ -1293,10 +1294,23 @@ abstract class ManagedStrategy(
/**
* Returns beautiful string representation of the [owner].
* If the [owner] is `this` of the current method, then returns `null`.
* Otherwise, we try to find if this [owner] is stored in only one field in the testObject
* and this field is final. If such field is found we construct beautiful representation for
* this field owner (if it's not a current `this`, again) and the field name.
* Otherwise, return beautiful representation for the provided [owner].
*/
private fun findOwnerName(owner: Any): String? {
// If the current owner is this - no owner needed.
if (isOwnerCurrentContext(owner)) return null
return adornedStringRepresentation(owner)
val fieldWithOwner = findFinalFieldWithOwner(runner.testInstance, owner) ?: return adornedStringRepresentation(owner)
// If such a field is found - construct representation with its owner and name.
return if (fieldWithOwner is OwnerWithName.InstanceOwnerWithName) {
val fieldOwner = fieldWithOwner.owner
val fieldName = fieldWithOwner.fieldName
if (!isOwnerCurrentContext(fieldOwner)) {
"${adornedStringRepresentation(fieldOwner)}.$fieldName"
} else fieldName
} else null
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import java.util.concurrent.atomic.*

class AtomicReferencesNamesTest : BaseFailingTest("atomic_references_names_trace.txt") {

private var atomicReference = AtomicReference(Node(1))
private var atomicInteger = AtomicInteger(0)
private var atomicLong = AtomicLong(0L)
private var atomicBoolean = AtomicBoolean(true)
private val atomicReference = AtomicReference(Node(1))
private val atomicInteger = AtomicInteger(0)
private val atomicLong = AtomicLong(0L)
private val atomicBoolean = AtomicBoolean(true)

private var atomicReferenceArray = AtomicReferenceArray(arrayOf(Node(1)))
private var atomicIntegerArray = AtomicIntegerArray(intArrayOf(0))
private var atomicLongArray = AtomicLongArray(longArrayOf(0L))
private val atomicReferenceArray = AtomicReferenceArray(arrayOf(Node(1)))
private val atomicIntegerArray = AtomicIntegerArray(intArrayOf(0))
private val atomicLongArray = AtomicLongArray(longArrayOf(0L))

private var wrapper = AtomicReferenceWrapper()
private val wrapper = AtomicReferenceWrapper()

override fun actionsForTrace() {
atomicReference.compareAndSet(atomicReference.get(), Node(2))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import java.util.concurrent.atomic.AtomicInteger
class InterleavingAnalysisPresentInSpinCycleFirstIterationTest {

// Counter thar causes spin-lock in spinLock operation
private var counter = AtomicInteger(0)
private val counter = AtomicInteger(0)
// Trigger to increment and decrement in spin-cycle to check in causeSpinLock operation
private var shouldAlwaysBeZero = AtomicInteger(0)
private var illegalInterleavingFound = AtomicBoolean(false)
private val shouldAlwaysBeZero = AtomicInteger(0)
private val illegalInterleavingFound = AtomicBoolean(false)

@Operation
fun causeSpinLock() {
Expand Down
Loading

0 comments on commit 196b345

Please sign in to comment.