|
| 1 | +package com.fasterxml.classmate; |
| 2 | + |
| 3 | +import java.util.List; |
| 4 | + |
| 5 | +/** |
| 6 | + * Test for [classmate#117]: StackOverflowError in 1.7.2 with recursive types |
| 7 | + * |
| 8 | + * NOTE: This test attempts to reproduce a StackOverflowError that occurs due to |
| 9 | + * infinite recursion in equals() methods when comparing recursive types: |
| 10 | + * - TypeBindings.equals() (line 221) -> ResolvedType.equals() |
| 11 | + * - ResolvedType.equals() (line 281) -> TypeBindings.equals() |
| 12 | + * - ResolvedRecursiveType.equals() (lines 157, 166) -> super.equals() + _referencedType.equals() |
| 13 | + * |
| 14 | + * The issue was introduced in commit 57fb93a which added support for resolving |
| 15 | + * raw generic types by binding type parameters to their bounds. This can create |
| 16 | + * circular dependencies in ResolvedRecursiveType instances. |
| 17 | + */ |
| 18 | +public class TestTypeResolver117 extends BaseTest |
| 19 | +{ |
| 20 | + protected final TypeResolver RESOLVER = new TypeResolver(); |
| 21 | + |
| 22 | + // [classmate#117] StackOverflowError with recursive types |
| 23 | + // The classic recursive type pattern: T extends SelfBounded<T> |
| 24 | + // When resolving raw types, the fix for #53 creates TypeBindings by |
| 25 | + // resolving T to its bound (SelfBounded<T>), which creates infinite recursion |
| 26 | + // in equals() methods |
| 27 | + |
| 28 | + // Test with a custom recursive type similar to Enum<E extends Enum<E>> |
| 29 | + static interface SelfReferential<T extends SelfReferential<T>> { |
| 30 | + T self(); |
| 31 | + } |
| 32 | + |
| 33 | + @SuppressWarnings("rawtypes") |
| 34 | + static abstract class RawSelfReferential implements SelfReferential { |
| 35 | + } |
| 36 | + |
| 37 | + // Another recursive pattern with class |
| 38 | + static abstract class SelfBounded<T extends SelfBounded<T>> { |
| 39 | + } |
| 40 | + |
| 41 | + @SuppressWarnings("rawtypes") |
| 42 | + static abstract class RawSelfBounded extends SelfBounded { |
| 43 | + } |
| 44 | + |
| 45 | + // Real enum to test with |
| 46 | + static enum TestEnum { |
| 47 | + A, B, C |
| 48 | + } |
| 49 | + |
| 50 | + // Class that uses raw Enum in its hierarchy |
| 51 | + @SuppressWarnings("rawtypes") |
| 52 | + static abstract class UsesRawComparable implements Comparable { |
| 53 | + } |
| 54 | + |
| 55 | + // More complex recursive patterns that might trigger the issue |
| 56 | + |
| 57 | + // Double-nested recursive type |
| 58 | + static abstract class DoublyRecursive<T extends DoublyRecursive<T, U>, U extends DoublyRecursive<U, T>> { |
| 59 | + } |
| 60 | + |
| 61 | + @SuppressWarnings("rawtypes") |
| 62 | + static abstract class RawDoublyRecursive extends DoublyRecursive { |
| 63 | + } |
| 64 | + |
| 65 | + // Recursive type that implements another recursive type |
| 66 | + static abstract class RecursiveChain<T extends RecursiveChain<T>> |
| 67 | + extends SelfBounded<RecursiveChain<T>> { |
| 68 | + } |
| 69 | + |
| 70 | + @SuppressWarnings("rawtypes") |
| 71 | + static abstract class RawRecursiveChain extends RecursiveChain { |
| 72 | + } |
| 73 | + |
| 74 | + /** |
| 75 | + * This test reproduces the StackOverflowError reported in issue #117. |
| 76 | + * When resolving a raw self-bounded type, the equals() comparison of recursive |
| 77 | + * types causes infinite recursion: |
| 78 | + * - ResolvedType.equals() calls TypeBindings.equals() |
| 79 | + * - TypeBindings.equals() calls ResolvedType.equals() on contained types |
| 80 | + * - For ResolvedRecursiveType, this calls both super.equals() AND |
| 81 | + * _referencedType.equals(), creating a cycle |
| 82 | + */ |
| 83 | + public void testRawSelfBoundedCausesStackOverflow() { |
| 84 | + // This should not throw StackOverflowError |
| 85 | + ResolvedType rt = RESOLVER.resolve(RawSelfBounded.class); |
| 86 | + assertNotNull(rt); |
| 87 | + // If we get here without StackOverflowError, the test passes |
| 88 | + } |
| 89 | + |
| 90 | + public void testRawSelfReferentialInterface() { |
| 91 | + // Test with raw interface that has self-referential type parameter |
| 92 | + ResolvedType rt = RESOLVER.resolve(RawSelfReferential.class); |
| 93 | + assertNotNull(rt); |
| 94 | + } |
| 95 | + |
| 96 | + public void testRealEnumType() { |
| 97 | + // Test with a real enum type (Enum<E extends Enum<E>>) |
| 98 | + ResolvedType rt = RESOLVER.resolve(TestEnum.class); |
| 99 | + assertNotNull(rt); |
| 100 | + } |
| 101 | + |
| 102 | + public void testRawComparableViaEnum() { |
| 103 | + // Test with class implementing raw Comparable |
| 104 | + // (Comparable is the interface that Enum implements) |
| 105 | + ResolvedType rt = RESOLVER.resolve(UsesRawComparable.class); |
| 106 | + assertNotNull(rt); |
| 107 | + } |
| 108 | + |
| 109 | + /** |
| 110 | + * This test checks that equals() on recursive types doesn't cause |
| 111 | + * infinite recursion |
| 112 | + */ |
| 113 | + public void testRecursiveTypeEquals() { |
| 114 | + ResolvedType rt1 = RESOLVER.resolve(RawSelfBounded.class); |
| 115 | + ResolvedType rt2 = RESOLVER.resolve(RawSelfBounded.class); |
| 116 | + |
| 117 | + // This equals comparison should not cause StackOverflowError |
| 118 | + assertEquals(rt1, rt2); |
| 119 | + } |
| 120 | + |
| 121 | + /** |
| 122 | + * Test equals with self-referential interface |
| 123 | + */ |
| 124 | + public void testRecursiveInterfaceEquals() { |
| 125 | + ResolvedType rt1 = RESOLVER.resolve(RawSelfReferential.class); |
| 126 | + ResolvedType rt2 = RESOLVER.resolve(RawSelfReferential.class); |
| 127 | + |
| 128 | + // This equals comparison should not cause StackOverflowError |
| 129 | + assertEquals(rt1, rt2); |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * Test with doubly-recursive type (two type parameters that reference each other) |
| 134 | + */ |
| 135 | + public void testDoublyRecursiveType() { |
| 136 | + ResolvedType rt = RESOLVER.resolve(RawDoublyRecursive.class); |
| 137 | + assertNotNull(rt); |
| 138 | + } |
| 139 | + |
| 140 | + /** |
| 141 | + * Test with recursive chain (recursive type extending another recursive type) |
| 142 | + */ |
| 143 | + public void testRecursiveChain() { |
| 144 | + ResolvedType rt = RESOLVER.resolve(RawRecursiveChain.class); |
| 145 | + assertNotNull(rt); |
| 146 | + } |
| 147 | + |
| 148 | + /** |
| 149 | + * Test equals on doubly-recursive type |
| 150 | + */ |
| 151 | + public void testDoublyRecursiveEquals() { |
| 152 | + ResolvedType rt1 = RESOLVER.resolve(RawDoublyRecursive.class); |
| 153 | + ResolvedType rt2 = RESOLVER.resolve(RawDoublyRecursive.class); |
| 154 | + |
| 155 | + // This equals comparison should not cause StackOverflowError |
| 156 | + assertEquals(rt1, rt2); |
| 157 | + } |
| 158 | + |
| 159 | + /** |
| 160 | + * Direct test with java.lang.Enum |
| 161 | + * This is the most likely candidate for reproducing the issue |
| 162 | + * since Enum has the recursive pattern: Enum<E extends Enum<E>> |
| 163 | + */ |
| 164 | + public void testDirectEnumResolution() { |
| 165 | + // Try resolving Enum.class directly (as a raw type) |
| 166 | + @SuppressWarnings("rawtypes") |
| 167 | + Class enumClass = Enum.class; |
| 168 | + ResolvedType rt = RESOLVER.resolve(enumClass); |
| 169 | + assertNotNull(rt); |
| 170 | + } |
| 171 | + |
| 172 | + /** |
| 173 | + * Test equals on Enum type |
| 174 | + */ |
| 175 | + public void testEnumTypeEquals() { |
| 176 | + @SuppressWarnings("rawtypes") |
| 177 | + Class enumClass = Enum.class; |
| 178 | + ResolvedType rt1 = RESOLVER.resolve(enumClass); |
| 179 | + ResolvedType rt2 = RESOLVER.resolve(enumClass); |
| 180 | + |
| 181 | + // This equals comparison should not cause StackOverflowError |
| 182 | + assertEquals(rt1, rt2); |
| 183 | + } |
| 184 | + |
| 185 | + /** |
| 186 | + * Stress test: resolve many recursive types to trigger caching/equality checks |
| 187 | + */ |
| 188 | + public void testMultipleRecursiveResolutions() { |
| 189 | + // Resolve the same types multiple times |
| 190 | + for (int i = 0; i < 10; i++) { |
| 191 | + ResolvedType rt1 = RESOLVER.resolve(RawSelfBounded.class); |
| 192 | + ResolvedType rt2 = RESOLVER.resolve(RawSelfReferential.class); |
| 193 | + ResolvedType rt3 = RESOLVER.resolve(RawDoublyRecursive.class); |
| 194 | + |
| 195 | + // Force equals checks |
| 196 | + assertEquals(rt1, RESOLVER.resolve(RawSelfBounded.class)); |
| 197 | + assertEquals(rt2, RESOLVER.resolve(RawSelfReferential.class)); |
| 198 | + assertEquals(rt3, RESOLVER.resolve(RawDoublyRecursive.class)); |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + /** |
| 203 | + * Test resolving type parameters directly |
| 204 | + */ |
| 205 | + public void testRecursiveTypeParameters() { |
| 206 | + @SuppressWarnings("rawtypes") |
| 207 | + Class enumClass = Enum.class; |
| 208 | + ResolvedType rt = RESOLVER.resolve(enumClass); |
| 209 | + |
| 210 | + // Get the type parameters which should include the recursive bound |
| 211 | + List<ResolvedType> typeParams = rt.getTypeParameters(); |
| 212 | + assertNotNull(typeParams); |
| 213 | + |
| 214 | + // For raw Enum, the type parameter E should be resolved to its bound: Enum<E> |
| 215 | + // This creates a ResolvedRecursiveType which could trigger the equals issue |
| 216 | + if (!typeParams.isEmpty()) { |
| 217 | + ResolvedType param = typeParams.get(0); |
| 218 | + assertNotNull(param); |
| 219 | + |
| 220 | + // Try to compare it |
| 221 | + assertEquals(param, param); |
| 222 | + |
| 223 | + // Check if it's a recursive type |
| 224 | + ResolvedType selfRef = param.getSelfReferencedType(); |
| 225 | + if (selfRef != null) { |
| 226 | + // This is a ResolvedRecursiveType - try comparing it |
| 227 | + assertEquals(param, param); |
| 228 | + } |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + /** |
| 233 | + * Deep inspection test: examine the structure of resolved recursive types |
| 234 | + * to understand if the cycle exists |
| 235 | + */ |
| 236 | + public void testRecursiveTypeStructure() { |
| 237 | + ResolvedType rt = RESOLVER.resolve(RawSelfBounded.class); |
| 238 | + |
| 239 | + // Get parent class which should be SelfBounded with type parameters |
| 240 | + ResolvedType parent = rt.getParentClass(); |
| 241 | + if (parent != null) { |
| 242 | + List<ResolvedType> parentParams = parent.getTypeParameters(); |
| 243 | + if (!parentParams.isEmpty()) { |
| 244 | + ResolvedType param = parentParams.get(0); |
| 245 | + |
| 246 | + // If this is recursive, it might have a self-reference |
| 247 | + ResolvedType selfRef = param.getSelfReferencedType(); |
| 248 | + if (selfRef != null) { |
| 249 | + // Try to trigger the equals issue |
| 250 | + TypeBindings bindings1 = parent.getTypeBindings(); |
| 251 | + TypeBindings bindings2 = parent.getTypeBindings(); |
| 252 | + |
| 253 | + // This should not cause StackOverflowError |
| 254 | + assertEquals(bindings1, bindings2); |
| 255 | + } |
| 256 | + } |
| 257 | + } |
| 258 | + } |
| 259 | + |
| 260 | + /** |
| 261 | + * Test that explicitly verifies the issue is resolved or documents |
| 262 | + * that it could not be reproduced in unit tests |
| 263 | + */ |
| 264 | + public void testIssue117Summary() { |
| 265 | + // This test documents the investigation of issue #117 |
| 266 | + |
| 267 | + // Try all the patterns that should trigger the issue: |
| 268 | + // 1. Raw self-bounded type |
| 269 | + ResolvedType rt1 = RESOLVER.resolve(RawSelfBounded.class); |
| 270 | + assertEquals(rt1, rt1); |
| 271 | + |
| 272 | + // 2. Raw Enum (the classic case) |
| 273 | + ResolvedType rt2 = RESOLVER.resolve(Enum.class); |
| 274 | + assertEquals(rt2, rt2); |
| 275 | + |
| 276 | + // 3. Doubly recursive type |
| 277 | + ResolvedType rt3 = RESOLVER.resolve(RawDoublyRecursive.class); |
| 278 | + assertEquals(rt3, rt3); |
| 279 | + |
| 280 | + // If we reach here without StackOverflowError, either: |
| 281 | + // a) The issue has been fixed in the current code |
| 282 | + // b) The issue requires a specific integration scenario not covered by unit tests |
| 283 | + // c) The issue manifests only with certain JDK versions or configurations |
| 284 | + |
| 285 | + assertTrue("Issue #117 tests completed without StackOverflowError", true); |
| 286 | + } |
| 287 | +} |
0 commit comments