1 /* <lambda>null2 * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 */ 4 5 package kotlinx.coroutines 6 7 import java.lang.ref.* 8 import java.lang.reflect.* 9 import java.text.* 10 import java.util.* 11 import java.util.Collections.* 12 import java.util.concurrent.* 13 import java.util.concurrent.atomic.* 14 import java.util.concurrent.locks.* 15 import kotlin.test.* 16 17 object FieldWalker { 18 sealed class Ref { 19 object RootRef : Ref() 20 class FieldRef(val parent: Any, val name: String) : Ref() 21 class ArrayRef(val parent: Any, val index: Int) : Ref() 22 } 23 24 private val fieldsCache = HashMap<Class<*>, List<Field>>() 25 26 init { 27 // excluded/terminal classes (don't walk them) 28 fieldsCache += listOf( 29 Any::class, String::class, Thread::class, Throwable::class, StackTraceElement::class, 30 WeakReference::class, ReferenceQueue::class, AbstractMap::class, Enum::class, 31 ReentrantLock::class, ReentrantReadWriteLock::class, SimpleDateFormat::class, ThreadPoolExecutor::class, 32 ) 33 .map { it.java } 34 .associateWith { emptyList() } 35 } 36 37 /* 38 * Reflectively starts to walk through object graph and returns identity set of all reachable objects. 39 * Use [walkRefs] if you need a path from root for debugging. 40 */ 41 public fun walk(root: Any?): Set<Any> = walkRefs(root, false).keys 42 43 public fun assertReachableCount(expected: Int, root: Any?, rootStatics: Boolean = false, predicate: (Any) -> Boolean) { 44 val visited = walkRefs(root, rootStatics) 45 val actual = visited.keys.filter(predicate) 46 if (actual.size != expected) { 47 val textDump = actual.joinToString("") { "\n\t" + showPath(it, visited) } 48 assertEquals( 49 expected, actual.size, 50 "Unexpected number objects. Expected $expected, found ${actual.size}$textDump" 51 ) 52 } 53 } 54 55 /* 56 * Reflectively starts to walk through object graph and map to all the reached object to their path 57 * in from root. Use [showPath] do display a path if needed. 58 */ 59 private fun walkRefs(root: Any?, rootStatics: Boolean): IdentityHashMap<Any, Ref> { 60 val visited = IdentityHashMap<Any, Ref>() 61 if (root == null) return visited 62 visited[root] = Ref.RootRef 63 val stack = ArrayDeque<Any>() 64 stack.addLast(root) 65 var statics = rootStatics 66 while (stack.isNotEmpty()) { 67 val element = stack.removeLast() 68 try { 69 visit(element, visited, stack, statics) 70 statics = false // only scan root static when asked 71 } catch (e: Exception) { 72 error("Failed to visit element ${showPath(element, visited)}: $e") 73 } 74 } 75 return visited 76 } 77 78 private fun showPath(element: Any, visited: Map<Any, Ref>): String { 79 val path = ArrayList<String>() 80 var cur = element 81 while (true) { 82 when (val ref = visited.getValue(cur)) { 83 Ref.RootRef -> break 84 is Ref.FieldRef -> { 85 cur = ref.parent 86 path += "|${ref.parent.javaClass.simpleName}::${ref.name}" 87 } 88 is Ref.ArrayRef -> { 89 cur = ref.parent 90 path += "[${ref.index}]" 91 } 92 else -> { 93 // Nothing, kludge for IDE 94 } 95 } 96 } 97 path.reverse() 98 return path.joinToString("") 99 } 100 101 private fun visit(element: Any, visited: IdentityHashMap<Any, Ref>, stack: ArrayDeque<Any>, statics: Boolean) { 102 val type = element.javaClass 103 when { 104 // Special code for arrays 105 type.isArray && !type.componentType.isPrimitive -> { 106 @Suppress("UNCHECKED_CAST") 107 val array = element as Array<Any?> 108 array.forEachIndexed { index, value -> 109 push(value, visited, stack) { Ref.ArrayRef(element, index) } 110 } 111 } 112 // Special code for platform types that cannot be reflectively accessed on modern JDKs 113 type.name.startsWith("java.") && element is Collection<*> -> { 114 element.forEachIndexed { index, value -> 115 push(value, visited, stack) { Ref.ArrayRef(element, index) } 116 } 117 } 118 type.name.startsWith("java.") && element is Map<*, *> -> { 119 push(element.keys, visited, stack) { Ref.FieldRef(element, "keys") } 120 push(element.values, visited, stack) { Ref.FieldRef(element, "values") } 121 } 122 element is AtomicReference<*> -> { 123 push(element.get(), visited, stack) { Ref.FieldRef(element, "value") } 124 } 125 element is AtomicReferenceArray<*> -> { 126 for (index in 0 until element.length()) { 127 push(element[index], visited, stack) { Ref.ArrayRef(element, index) } 128 } 129 } 130 element is AtomicLongFieldUpdater<*> -> { 131 /* filter it out here to suppress its subclasses too */ 132 } 133 // All the other classes are reflectively scanned 134 else -> fields(type, statics).forEach { field -> 135 push(field.get(element), visited, stack) { Ref.FieldRef(element, field.name) } 136 // special case to scan Throwable cause (cannot get it reflectively) 137 if (element is Throwable) { 138 push(element.cause, visited, stack) { Ref.FieldRef(element, "cause") } 139 } 140 } 141 } 142 } 143 144 private inline fun push(value: Any?, visited: IdentityHashMap<Any, Ref>, stack: ArrayDeque<Any>, ref: () -> Ref) { 145 if (value != null && !visited.containsKey(value)) { 146 visited[value] = ref() 147 stack.addLast(value) 148 } 149 } 150 151 private fun fields(type0: Class<*>, rootStatics: Boolean): List<Field> { 152 fieldsCache[type0]?.let { return it } 153 val result = ArrayList<Field>() 154 var type = type0 155 var statics = rootStatics 156 while (true) { 157 val fields = type.declaredFields.filter { 158 !it.type.isPrimitive 159 && (statics || !Modifier.isStatic(it.modifiers)) 160 && !(it.type.isArray && it.type.componentType.isPrimitive) 161 && it.name != "previousOut" // System.out from TestBase that we store in a field to restore later 162 } 163 check(fields.isEmpty() || !type.name.startsWith("java.")) { 164 """ 165 Trying to walk trough JDK's '$type' will get into illegal reflective access on JDK 9+. 166 Either modify your test to avoid usage of this class or update FieldWalker code to retrieve 167 the captured state of this class without going through reflection (see how collections are handled). 168 """.trimIndent() 169 } 170 fields.forEach { it.isAccessible = true } // make them all accessible 171 result.addAll(fields) 172 type = type.superclass 173 statics = false 174 val superFields = fieldsCache[type] // will stop at Any anyway 175 if (superFields != null) { 176 result.addAll(superFields) 177 break 178 } 179 } 180 fieldsCache[type0] = result 181 return result 182 } 183 } 184