• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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