|
1 | 1 | package com.nordstrom.automation.junit; |
2 | 2 |
|
| 3 | +import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey; |
| 4 | + |
3 | 5 | import java.util.Map; |
| 6 | +import java.util.Objects; |
4 | 7 | import java.util.concurrent.Callable; |
5 | 8 | import java.util.concurrent.ConcurrentHashMap; |
| 9 | +import java.util.concurrent.ConcurrentMap; |
| 10 | + |
| 11 | +import org.junit.runners.model.FrameworkMethod; |
6 | 12 | import org.slf4j.Logger; |
7 | 13 | import org.slf4j.LoggerFactory; |
8 | 14 |
|
| 15 | +import com.google.common.base.Function; |
| 16 | + |
| 17 | +import net.bytebuddy.implementation.bind.annotation.Argument; |
9 | 18 | import net.bytebuddy.implementation.bind.annotation.RuntimeType; |
10 | 19 | import net.bytebuddy.implementation.bind.annotation.SuperCall; |
11 | 20 | import net.bytebuddy.implementation.bind.annotation.This; |
12 | 21 |
|
13 | | -import static com.nordstrom.automation.junit.LifecycleHooks.toMapKey; |
14 | | - |
15 | 22 | /** |
16 | 23 | * This class declares the interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest |
17 | 24 | * createTest} method. |
18 | 25 | */ |
19 | | -@SuppressWarnings("squid:S1118") |
20 | 26 | public class CreateTest { |
21 | 27 |
|
22 | | - private static final Map<String, Object> TARGET_TO_RUNNER = new ConcurrentHashMap<>(); |
23 | | - private static final Map<String, Object> RUNNER_TO_TARGET = new ConcurrentHashMap<>(); |
| 28 | + private static final Map<Integer, Object> HASHCODE_TO_TARGET = new ConcurrentHashMap<>(); |
| 29 | + private static final Map<String, FrameworkMethod> TARGET_TO_METHOD = new ConcurrentHashMap<>(); |
| 30 | + private static final ThreadLocal<ConcurrentMap<Integer, DepthGauge>> METHOD_DEPTH; |
| 31 | + private static final Function<Integer, DepthGauge> NEW_INSTANCE; |
24 | 32 | private static final Logger LOGGER = LoggerFactory.getLogger(CreateTest.class); |
| 33 | + |
| 34 | + static { |
| 35 | + METHOD_DEPTH = new ThreadLocal<ConcurrentMap<Integer, DepthGauge>>() { |
| 36 | + @Override |
| 37 | + protected ConcurrentMap<Integer, DepthGauge> initialValue() { |
| 38 | + return new ConcurrentHashMap<>(); |
| 39 | + } |
| 40 | + }; |
| 41 | + NEW_INSTANCE = new Function<Integer, DepthGauge>() { |
| 42 | + @Override |
| 43 | + public DepthGauge apply(Integer input) { |
| 44 | + return new DepthGauge(); |
| 45 | + } |
| 46 | + }; |
| 47 | + } |
25 | 48 |
|
26 | 49 | /** |
27 | 50 | * Interceptor for the {@link org.junit.runners.BlockJUnit4ClassRunner#createTest createTest} method. |
28 | 51 | * |
29 | 52 | * @param runner target {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} object |
| 53 | + * @param method {@link FrameworkMethod} for which this test class instance is being created |
30 | 54 | * @param proxy callable proxy for the intercepted method |
31 | 55 | * @return {@code anything} - JUnit test class instance |
32 | 56 | * @throws Exception {@code anything} (exception thrown by the intercepted method) |
33 | 57 | */ |
34 | 58 | @RuntimeType |
35 | | - public static Object intercept(@This final Object runner, |
| 59 | + public static Object intercept(@This final Object runner, @Argument(0) final FrameworkMethod method, |
36 | 60 | @SuperCall final Callable<?> proxy) throws Exception { |
37 | 61 |
|
| 62 | + Integer hashCode = Objects.hash(runner, method); |
| 63 | + DepthGauge depthGauge = LifecycleHooks.computeIfAbsent(METHOD_DEPTH.get(), hashCode, NEW_INSTANCE); |
| 64 | + depthGauge.increaseDepth(); |
| 65 | + |
38 | 66 | Object target = LifecycleHooks.callProxy(proxy); |
39 | | - // apply parameter-based global timeout |
40 | | - TimeoutUtils.applyTestTimeout(runner, target); |
41 | 67 |
|
42 | | - if (null == TARGET_TO_RUNNER.put(toMapKey(target), runner)) { |
| 68 | + if (0 == depthGauge.decreaseDepth()) { |
| 69 | + METHOD_DEPTH.remove(); |
43 | 70 | LOGGER.debug("testObjectCreated: {}", target); |
44 | | - RUNNER_TO_TARGET.put(toMapKey(runner), target); |
| 71 | + TARGET_TO_METHOD.put(toMapKey(target), method); |
| 72 | + |
| 73 | + // apply parameter-based global timeout |
| 74 | + TimeoutUtils.applyTestTimeout(runner, method, target); |
| 75 | + |
| 76 | + // if notifier hasn't been initialized yet |
| 77 | + if ( ! EachTestNotifierInit.setTestTarget(runner, method, target)) { |
| 78 | + // store target for subsequent retrieval |
| 79 | + HASHCODE_TO_TARGET.put(hashCode, target); |
| 80 | + } |
45 | 81 |
|
46 | 82 | for (TestObjectWatcher watcher : LifecycleHooks.getObjectWatchers()) { |
47 | | - watcher.testObjectCreated(target, runner); |
| 83 | + watcher.testObjectCreated(runner, method, target); |
48 | 84 | } |
49 | 85 | } |
50 | 86 |
|
51 | 87 | return target; |
52 | 88 | } |
53 | 89 |
|
54 | 90 | /** |
55 | | - * Get the class runner associated with the specified instance. |
| 91 | + * Get test class instance associated with the specified runner/method pair. |
| 92 | + * <p> |
| 93 | + * <b>NOTE</b>: This method can only be called once per target, as it removes the mapping. |
56 | 94 | * |
57 | | - * @param target instance of JUnit test class |
58 | | - * @return {@link org.junit.runners.BlockJUnit4ClassRunner BlockJUnit4ClassRunner} for specified instance |
| 95 | + * @param runner JUnit class runner |
| 96 | + * @param method JUnit framework method |
| 97 | + * @return target test class instance |
59 | 98 | */ |
60 | | - static Object getRunnerForTarget(Object target) { |
61 | | - return TARGET_TO_RUNNER.get(toMapKey(target)); |
| 99 | + static Object getTargetFor(Object runner, FrameworkMethod method) { |
| 100 | + return HASHCODE_TO_TARGET.remove(Objects.hash(runner, method)); |
62 | 101 | } |
63 | 102 |
|
64 | 103 | /** |
65 | | - * Get the JUnit test class instance for the specified class runner. |
| 104 | + * Get the method for which the specified test class instance was created. |
66 | 105 | * |
67 | | - * @param runner JUnit class runner |
68 | | - * @return JUnit test class instance for specified runner |
| 106 | + * @param target test class instance |
| 107 | + * @return JUnit framework method |
69 | 108 | */ |
70 | | - static Object getTargetForRunner(Object runner) { |
71 | | - return RUNNER_TO_TARGET.get(toMapKey(runner)); |
| 109 | + static FrameworkMethod getMethodFor(Object target) { |
| 110 | + return TARGET_TO_METHOD.get(toMapKey(target)); |
72 | 111 | } |
73 | 112 |
|
74 | 113 | /** |
75 | | - * Release runner/target mappings. |
| 114 | + * Release the mappings associated with the specified runner/method/target group. |
76 | 115 | * |
77 | 116 | * @param runner JUnit class runner |
| 117 | + * @param method JUnit framework method |
| 118 | + * @param target test class instance |
78 | 119 | */ |
79 | | - static void releaseMappingsFor(Object runner) { |
80 | | - Object target = RUNNER_TO_TARGET.remove(toMapKey(runner)); |
| 120 | + static void releaseMappingsFor(Object runner, FrameworkMethod method, Object target) { |
| 121 | + HASHCODE_TO_TARGET.remove(Objects.hash(runner, method)); |
81 | 122 | if (target != null) { |
82 | | - TARGET_TO_RUNNER.remove(toMapKey(target)); |
| 123 | + TARGET_TO_METHOD.remove(toMapKey(target)); |
83 | 124 | } |
84 | 125 | } |
85 | 126 | } |
0 commit comments