Skip to content

Commit 76b4e05

Browse files
authored
Replace reflection with delegation (#119)
1 parent 559a7a8 commit 76b4e05

File tree

17 files changed

+566
-289
lines changed

17 files changed

+566
-289
lines changed

pom.xml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<settings.version>2.3.10</settings.version>
3333
<junit.version>4.13.2</junit.version>
3434
<testng.version>6.10</testng.version>
35-
<bytebuddy.version>1.11.13</bytebuddy.version>
35+
<bytebuddy.version>1.12.8</bytebuddy.version>
3636
<logback.version>1.2.5</logback.version>
3737
<junitparams.version>1.1.1</junitparams.version>
3838
<powermock.version>2.0.9</powermock.version>
@@ -224,7 +224,10 @@
224224
<groupId>org.apache.maven.plugins</groupId>
225225
<artifactId>maven-surefire-plugin</artifactId>
226226
<configuration>
227-
<argLine>-javaagent:src/test/resources/test-agent.jar</argLine>
227+
<argLine>
228+
-javaagent:src/test/resources/test-agent.jar
229+
--add-opens java.base/java.lang=ALL-UNNAMED
230+
</argLine>
228231
</configuration>
229232
</plugin>
230233
<plugin>
@@ -298,7 +301,7 @@
298301
<configuration>
299302
<archive>
300303
<manifestEntries>
301-
<Premain-Class>com.nordstrom.automation.junit.LifecycleHooks</Premain-Class>
304+
<Premain-Class>com.nordstrom.automation.junit.JUnitAgent</Premain-Class>
302305
<Can-Redefine-Classes>false</Can-Redefine-Classes>
303306
<Can-Retransform-Classes>true</Can-Retransform-Classes>
304307
</manifestEntries>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.lang.annotation.Annotation;
4+
5+
/**
6+
* This interface declares the annotations accessor method for the {@code Description} class.
7+
*/
8+
public interface AnnotationsAccessor {
9+
10+
/**
11+
* Get the annotations of this description.
12+
*
13+
* @return array of {@link Annotation} objects
14+
*/
15+
Annotation[] annotations();
16+
17+
}

src/main/java/com/nordstrom/automation/junit/AtomicTest.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import java.lang.annotation.Annotation;
66
import java.util.ArrayList;
7+
import java.util.Arrays;
78
import java.util.Collection;
89
import java.util.List;
910
import java.util.Objects;
@@ -36,7 +37,8 @@ public class AtomicTest {
3637
private Throwable thrown;
3738

3839
private static final Pattern PARAM = Pattern.compile("[(\\[]");
39-
40+
private static final List<Class<? extends Annotation>> TEST_TYPES = Arrays.asList(Test.class, Theory.class);
41+
4042
public AtomicTest(Description description) {
4143
this.runner = Run.getThreadRunner();
4244
this.description = description;
@@ -118,27 +120,22 @@ public boolean includes(FrameworkMethod method) {
118120
}
119121

120122
/**
121-
* Determine if this atomic test represents a "theory" method.
123+
* Determine if this atomic test represents a "theory" method permutation.
122124
*
123-
* @return {@code true} if this atomic test represents a "theory" method; otherwise {@code false}
125+
* @return {@code true} if this atomic test represents a permutation; otherwise {@code false}
124126
*/
125127
public boolean isTheory() {
126128
return isTheory(description);
127129
}
128130

129131
/**
130-
* Determine if the specified description represents a "theory" method.
132+
* Determine if the specified description represents a "theory" method permutation.
131133
*
132134
* @param description JUnit method description
133-
* @return {@code true} if the specified description represents a "theory" method; otherwise {@code false}
135+
* @return {@code true} if the specified description represents a permutation; otherwise {@code false}
134136
*/
135137
public static boolean isTheory(Description description) {
136-
try {
137-
String uniqueId = LifecycleHooks.getFieldValue(description, "fUniqueId");
138-
return ((uniqueId != null) && (uniqueId.startsWith("theory-id: ")));
139-
} catch (IllegalAccessException | NoSuchFieldException | SecurityException e) {
140-
return false;
141-
}
138+
return DescribeChild.isPermutation(description);
142139
}
143140

144141
/**
@@ -157,11 +154,21 @@ public boolean isTest() {
157154
* @return {@code true} if description represents a test method; otherwise {@code false}
158155
*/
159156
public static boolean isTest(Description description) {
157+
return (getTestAnnotation(description) != null);
158+
}
159+
160+
/**
161+
* Get the annotation that marks the specified description as a test method.
162+
*
163+
* @param description JUnit description object
164+
* @return if description represents a test method, the {@link Test} or {@link Theory} annotation;
165+
* otherwise {@code null}
166+
*/
167+
public static Annotation getTestAnnotation(Description description) {
160168
for (Annotation annotation : description.getAnnotations()) {
161-
if (annotation instanceof Test) return true;
162-
if (annotation instanceof Theory) return true;
169+
if (TEST_TYPES.contains(annotation.annotationType())) return annotation;
163170
}
164-
return false;
171+
return null;
165172
}
166173

167174
/**
Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package com.nordstrom.automation.junit;
22

3-
import java.lang.reflect.Field;
43
import java.util.Arrays;
54
import java.util.concurrent.Callable;
65

76
import org.junit.experimental.theories.PotentialAssignment.CouldNotGenerateValueException;
87
import org.junit.experimental.theories.Theories.TheoryAnchor;
8+
import org.junit.experimental.theories.Theory;
99
import org.junit.experimental.theories.internal.Assignments;
1010
import org.junit.runner.Description;
1111
import org.junit.runners.model.FrameworkMethod;
@@ -20,18 +20,7 @@
2020
*/
2121
public class DescribeChild {
2222

23-
private static final Field uniqueId;
24-
25-
static {
26-
Field field = null;
27-
try {
28-
field = Description.class.getDeclaredField("fUniqueId");
29-
field.setAccessible(true);
30-
} catch (NoSuchFieldException | SecurityException e) {
31-
field = null;
32-
}
33-
uniqueId = field;
34-
}
23+
private static final String PERM_TAG = "theory-id: ";
3524

3625
/**
3726
* Interceptor for the {@link org.junit.runners.ParentRunner#describeChild describeChild} method.
@@ -59,17 +48,22 @@ public static Description intercept(@This final Object runner,
5948
method.getName(), method.getAnnotations());
6049
}
6150

62-
// if able to override [uniqueId] of test
63-
if ((uniqueId != null) && AtomicTest.isTest(description)) {
51+
// if describing a theory method, but not tagged as a permutation
52+
if ((description.getAnnotation(Theory.class) != null) && !isPermutation(description)) {
6453
try {
6554
// get parent of test runner
6655
Object parent = LifecycleHooks.getFieldValue(runner, "this$0");
6756
// if child of TheoryAnchor statement
6857
if (parent instanceof TheoryAnchor) {
6958
// get assignments for this theory permutation
7059
Assignments assignments = LifecycleHooks.getFieldValue(runner, "val$complete");
71-
// inject permutation ID into description
72-
injectPermutationId(description, assignments);
60+
// compute permutation ID
61+
String permutationId = computePermutationId(description, assignments);
62+
// if permutation ID was computed
63+
if (permutationId != null) {
64+
// inject computed permutation ID
65+
((UniqueIdMutator) description).setUniqueId(permutationId);
66+
}
7367
}
7468
} catch (IllegalAccessException | NoSuchFieldException | SecurityException | IllegalArgumentException e) {
7569
// nothing to do here
@@ -79,42 +73,33 @@ public static Description intercept(@This final Object runner,
7973
}
8074

8175
/**
82-
* Inject permutation ID into the specified description, overriding its default ID.
83-
*
76+
* Determine if the specified description represents a "theory" permutation.
77+
*
78+
* @param description JUnit {@link Description} object
79+
* @return {@code true} if permutation is described; otherwise {@code false}
80+
*/
81+
static boolean isPermutation(final Description description) {
82+
return ((UniqueIdAccessor) description).getUniqueId().toString().startsWith(PERM_TAG);
83+
}
84+
85+
/**
86+
* Compute permutation ID for the specified description and assignments.
87+
*
8488
* @param description description of "theory" method
8589
* @param assignments arguments for this permutation
90+
* @return theory method permutation ID (may be {@code null})
8691
*/
87-
private static void injectPermutationId(final Description description, final Assignments assignments) {
92+
private static String computePermutationId(final Description description, final Assignments assignments) {
8893
try {
8994
Object[] args = assignments.getMethodArguments();
9095
Object[] perm = new Object[args.length + 1];
9196
perm[0] = description.getDisplayName();
9297
System.arraycopy(args, 0, perm, 1, args.length);
9398
int permutationId = Arrays.hashCode(perm);
94-
String theoryId = String.format("theory-id: %08X", permutationId);
95-
uniqueId.set(description, theoryId);
96-
} catch (CouldNotGenerateValueException | SecurityException |
97-
IllegalArgumentException | IllegalAccessException eaten) {
98-
// nothing to do here
99-
}
100-
}
101-
102-
/**
103-
* Make a childless copy of the specified description.
104-
*
105-
* @param description JUnit description
106-
* @return copy of the specified description, including unique ID
107-
*/
108-
static Description makeChildlessCopyOf(final Description description) {
109-
Description descripCopy = description.childlessCopy();
110-
if (uniqueId != null) {
111-
try {
112-
uniqueId.set(descripCopy, uniqueId.get(description));
113-
} catch (IllegalArgumentException | IllegalAccessException eaten) {
114-
// nothing to do here
115-
}
99+
return String.format(PERM_TAG + "%08X", permutationId);
100+
} catch (CouldNotGenerateValueException e) {
101+
return null;
116102
}
117-
return descripCopy;
118103
}
119104

120105
}

src/main/java/com/nordstrom/automation/junit/EachTestNotifierInit.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public static void interceptor(@Argument(0) final RunNotifier notifier,
6666
if (description.equals(entry.getValue())) {
6767
// subject method resolved
6868
method = entry.getKey();
69+
break;
6970
}
7071
}
7172
} catch (IllegalAccessException | NoSuchFieldException | SecurityException e) {
@@ -189,6 +190,7 @@ static void releaseMappingsFor(EachTestNotifier notifier) {
189190
RunReflectiveCall.releaseCallableOf(description);
190191
ArtifactCollector.releaseWatchersOf(description);
191192
CreateTest.releaseMappingsFor(atomicTest.getRunner(), atomicTest.getIdentity(), target);
193+
GetAnnotations.releaseAnnotationsFor(atomicTest.getIdentity());
192194
}
193195

194196
/**
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.util.Objects;
5+
6+
import org.junit.runners.model.FrameworkMethod;
7+
8+
import net.bytebuddy.implementation.bind.annotation.Argument;
9+
import net.bytebuddy.implementation.bind.annotation.This;
10+
11+
/**
12+
* This class declares the interceptor for the {@link org.junit.runners.model.FrameworkMethod#getAnnotation} method.
13+
*/
14+
public class GetAnnotation {
15+
16+
/**
17+
* Interceptor for the {@link org.junit.runners.model.FrameworkMethod#getAnnotation} method.
18+
* <p>
19+
* <b>NOTE</b>: This interceptor does <b>not</b> invoke the original implementation in {@link FrameworkMethod}. It
20+
* relies instead on the cached annotations collected by the {@link GetAnnotations} class, which injects a proxied
21+
* replacement for the <b>{@code @Test}</b> annotation that enables global test timeout management.
22+
*
23+
* @param <T> desired annotation type
24+
* @param method target {@link FrameworkMethod} object
25+
* @param annotationType desired annotation type
26+
* @return this element's annotation for the specified annotation type if present on this element, else null
27+
* @throws NullPointerException if the given annotation class is {@code null}
28+
*/
29+
public static <T extends Annotation> T intercept(@This final FrameworkMethod method, @Argument(0) final Class<T> annotationType) {
30+
Objects.requireNonNull(annotationType);
31+
for (Annotation annotation : GetAnnotations.getAnnotationsFor(method)) {
32+
if (annotation.annotationType().equals(annotationType)) {
33+
return annotationType.cast(annotation);
34+
}
35+
}
36+
return null;
37+
}
38+
39+
/**
40+
* Inject the specified proxy annotation into the indicated method.
41+
*
42+
* @param method target {@link FrameworkMethod} object
43+
* @param proxyAnnotation mutable proxy annotation ({@link MutableTest})
44+
*/
45+
static void injectProxy(FrameworkMethod method, Annotation proxyAnnotation) {
46+
Annotation[] annotations = GetAnnotations.getAnnotationsFor(method);
47+
for (int i = 0; i < annotations.length; i++) {
48+
if (annotations[i].annotationType().equals(proxyAnnotation.annotationType())) {
49+
annotations[i] = proxyAnnotation;
50+
break;
51+
}
52+
}
53+
}
54+
55+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.nordstrom.automation.junit;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.util.Map;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
7+
import org.junit.runners.model.FrameworkMethod;
8+
9+
import net.bytebuddy.implementation.bind.annotation.This;
10+
11+
/**
12+
* This class declares the interceptor for the {@link org.junit.runners.model.FrameworkMethod#getAnnotations} method.
13+
*/
14+
public class GetAnnotations {
15+
16+
private static final Map<Integer, Annotation[]> ANNOTATIONS = new ConcurrentHashMap<>();
17+
18+
/**
19+
* Interceptor for the {@link org.junit.runners.model.FrameworkMethod#getAnnotations} method.
20+
*
21+
* @param method target {@link FrameworkMethod} object
22+
* @return the annotations attached to the target method
23+
* @throws Exception {@code anything} (exception thrown by the intercepted method)
24+
*/
25+
public static Annotation[] intercept(@This final FrameworkMethod method) throws Exception {
26+
return getAnnotationsFor(method);
27+
}
28+
29+
/**
30+
* Returns the annotations on the specified method.
31+
* <p>
32+
* <b>NOTE</b>: This method caches the annotations attached to the Java {@link Method} wrapped by the specified
33+
* JUnit framework method and returns this on subsequent calls.
34+
*
35+
* @param method target {@link FrameworkMethod} object
36+
* @return array of annotations for the specified method
37+
*/
38+
static Annotation[] getAnnotationsFor(FrameworkMethod method) {
39+
Annotation[] annotations = ANNOTATIONS.get(method.hashCode());
40+
if (annotations == null) {
41+
annotations = method.getMethod().getAnnotations();
42+
ANNOTATIONS.put(method.hashCode(), annotations);
43+
}
44+
return annotations;
45+
}
46+
47+
/**
48+
* Release the cached annotations for the specified JUnit framework method.
49+
*
50+
* @param method target {@link FrameworkMethod} object
51+
*/
52+
static void releaseAnnotationsFor(FrameworkMethod method) {
53+
ANNOTATIONS.remove(method.hashCode());
54+
}
55+
56+
}

0 commit comments

Comments
 (0)