Skip to content

Commit 52ec031

Browse files
committed
Improve RuntimeHintsAgent test coverage
In order to write proper integration tests for the `RuntimeHintsAgent`, we need to load the java agent on the JVM and instrument test code to check that invocations are properly recorded. This commit adds the relevant build configuration to the integration-tests module and adds reflection tests for the agent. Closes gh-27981
1 parent fc1408f commit 52ec031

File tree

4 files changed

+367
-0
lines changed

4 files changed

+367
-0
lines changed

integration-tests/integration-tests.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
plugins {
2+
id 'org.springframework.build.runtimehints-agent'
3+
}
4+
15
description = "Spring Integration Tests"
26

37
dependencies {
48
testImplementation(project(":spring-aop"))
59
testImplementation(project(":spring-beans"))
610
testImplementation(project(":spring-context"))
711
testImplementation(project(":spring-core"))
12+
testImplementation(project(":spring-core-test"))
813
testImplementation(testFixtures(project(":spring-aop")))
914
testImplementation(testFixtures(project(":spring-beans")))
1015
testImplementation(testFixtures(project(":spring-core")))
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot;
18+
19+
import java.io.IOException;
20+
import java.lang.reflect.Constructor;
21+
import java.lang.reflect.Method;
22+
import java.util.ArrayDeque;
23+
import java.util.Deque;
24+
import java.util.ResourceBundle;
25+
import java.util.stream.Stream;
26+
27+
import org.junit.jupiter.api.BeforeAll;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.params.ParameterizedTest;
30+
import org.junit.jupiter.params.provider.Arguments;
31+
import org.junit.jupiter.params.provider.MethodSource;
32+
33+
import org.springframework.aot.agent.HintType;
34+
import org.springframework.aot.agent.MethodReference;
35+
import org.springframework.aot.agent.RecordedInvocation;
36+
import org.springframework.aot.agent.RecordedInvocationsListener;
37+
import org.springframework.aot.agent.RecordedInvocationsPublisher;
38+
import org.springframework.aot.agent.RuntimeHintsAgent;
39+
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
40+
41+
import static org.assertj.core.api.Assertions.assertThat;
42+
43+
/**
44+
* Integration tests for {@link RuntimeHintsAgent}.
45+
*
46+
* @author Brian Clozel
47+
*/
48+
@EnabledIfRuntimeHintsAgent
49+
public class RuntimeHintsAgentTests {
50+
51+
private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
52+
53+
private static Constructor<String> defaultConstructor;
54+
55+
private static Method toStringMethod;
56+
57+
58+
@BeforeAll
59+
public static void classSetup() throws NoSuchMethodException {
60+
defaultConstructor = String.class.getConstructor();
61+
toStringMethod = String.class.getMethod("toString");
62+
}
63+
64+
65+
@ParameterizedTest
66+
@MethodSource("instrumentedReflectionMethods")
67+
void shouldInstrumentReflectionMethods(Runnable runnable, MethodReference methodReference) {
68+
RecordingSession session = RecordingSession.record(runnable);
69+
assertThat(session.recordedInvocations()).hasSize(1);
70+
RecordedInvocation invocation = session.recordedInvocations().findFirst().get();
71+
assertThat(invocation.getMethodReference()).isEqualTo(methodReference);
72+
assertThat(invocation.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName()));
73+
}
74+
75+
private static Stream<Arguments> instrumentedReflectionMethods() {
76+
return Stream.of(
77+
Arguments.of((Runnable) () -> {
78+
try {
79+
Class.forName("java.lang.String");
80+
}
81+
catch (ClassNotFoundException e) {
82+
}
83+
}, MethodReference.of(Class.class, "forName")),
84+
Arguments.of((Runnable) () -> String.class.getClasses(), MethodReference.of(Class.class, "getClasses")),
85+
Arguments.of((Runnable) () -> {
86+
try {
87+
String.class.getConstructor();
88+
}
89+
catch (NoSuchMethodException e) {
90+
}
91+
}, MethodReference.of(Class.class, "getConstructor")),
92+
Arguments.of((Runnable) () -> String.class.getConstructors(), MethodReference.of(Class.class, "getConstructors")),
93+
Arguments.of((Runnable) () -> String.class.getDeclaredClasses(), MethodReference.of(Class.class, "getDeclaredClasses")),
94+
Arguments.of((Runnable) () -> {
95+
try {
96+
String.class.getDeclaredConstructor();
97+
}
98+
catch (NoSuchMethodException e) {
99+
}
100+
}, MethodReference.of(Class.class, "getDeclaredConstructor")),
101+
Arguments.of((Runnable) () -> String.class.getDeclaredConstructors(), MethodReference.of(Class.class, "getDeclaredConstructors")),
102+
Arguments.of((Runnable) () -> {
103+
try {
104+
String.class.getDeclaredField("test");
105+
}
106+
catch (NoSuchFieldException e) {
107+
}
108+
}, MethodReference.of(Class.class, "getDeclaredField")),
109+
Arguments.of((Runnable) () -> String.class.getDeclaredFields(), MethodReference.of(Class.class, "getDeclaredFields")),
110+
Arguments.of((Runnable) () -> {
111+
try {
112+
String.class.getDeclaredMethod("toString");
113+
}
114+
catch (NoSuchMethodException e) {
115+
}
116+
}, MethodReference.of(Class.class, "getDeclaredMethod")),
117+
Arguments.of((Runnable) () -> String.class.getDeclaredMethods(), MethodReference.of(Class.class, "getDeclaredMethods")),
118+
Arguments.of((Runnable) () -> {
119+
try {
120+
String.class.getField("test");
121+
}
122+
catch (NoSuchFieldException e) {
123+
}
124+
}, MethodReference.of(Class.class, "getField")),
125+
Arguments.of((Runnable) () -> String.class.getFields(), MethodReference.of(Class.class, "getFields")),
126+
Arguments.of((Runnable) () -> {
127+
try {
128+
String.class.getMethod("toString");
129+
}
130+
catch (NoSuchMethodException e) {
131+
}
132+
}, MethodReference.of(Class.class, "getMethod")),
133+
Arguments.of((Runnable) () -> String.class.getMethods(), MethodReference.of(Class.class, "getMethods")),
134+
Arguments.of((Runnable) () -> {
135+
try {
136+
classLoader.loadClass("java.lang.String");
137+
}
138+
catch (ClassNotFoundException e) {
139+
}
140+
}, MethodReference.of(ClassLoader.class, "loadClass")),
141+
Arguments.of((Runnable) () -> {
142+
try {
143+
defaultConstructor.newInstance();
144+
}
145+
catch (Exception e) {
146+
}
147+
}, MethodReference.of(Constructor.class, "newInstance")),
148+
Arguments.of((Runnable) () -> {
149+
try {
150+
toStringMethod.invoke("");
151+
}
152+
catch (Exception e) {
153+
}
154+
}, MethodReference.of(Method.class, "invoke"))
155+
);
156+
}
157+
158+
@ParameterizedTest
159+
@MethodSource("instrumentedResourceBundleMethods")
160+
void shouldInstrumentResourceBundleMethods(Runnable runnable, MethodReference methodReference) {
161+
RecordingSession session = RecordingSession.record(runnable);
162+
assertThat(session.recordedInvocations(HintType.RESOURCE_BUNDLE)).hasSize(1);
163+
164+
RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_BUNDLE).findFirst().get();
165+
assertThat(resolution.getMethodReference()).isEqualTo(methodReference);
166+
assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName()));
167+
}
168+
169+
170+
private static Stream<Arguments> instrumentedResourceBundleMethods() {
171+
return Stream.of(
172+
Arguments.of((Runnable) () -> {
173+
try {
174+
ResourceBundle.getBundle("testBundle");
175+
}
176+
catch (Throwable exc) {
177+
}
178+
},
179+
MethodReference.of(ResourceBundle.class, "getBundle"))
180+
);
181+
}
182+
183+
@ParameterizedTest
184+
@MethodSource("instrumentedResourcePatternMethods")
185+
void shouldInstrumentResourcePatternMethods(Runnable runnable, MethodReference methodReference) {
186+
RecordingSession session = RecordingSession.record(runnable);
187+
assertThat(session.recordedInvocations(HintType.RESOURCE_PATTERN)).hasSize(1);
188+
189+
RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_PATTERN).findFirst().get();
190+
assertThat(resolution.getMethodReference()).isEqualTo(methodReference);
191+
assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName()));
192+
}
193+
194+
195+
private static Stream<Arguments> instrumentedResourcePatternMethods() {
196+
return Stream.of(
197+
Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResource("sample.txt"),
198+
MethodReference.of(Class.class, "getResource")),
199+
Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResourceAsStream("sample.txt"),
200+
MethodReference.of(Class.class, "getResourceAsStream")),
201+
Arguments.of((Runnable) () -> classLoader.getResource("sample.txt"),
202+
MethodReference.of(ClassLoader.class, "getResource")),
203+
Arguments.of((Runnable) () -> classLoader.getResourceAsStream("sample.txt"),
204+
MethodReference.of(ClassLoader.class, "getResourceAsStream")),
205+
Arguments.of((Runnable) () -> {
206+
try {
207+
classLoader.getResources("sample.txt");
208+
}
209+
catch (IOException e) {
210+
}
211+
},
212+
MethodReference.of(ClassLoader.class, "getResources")),
213+
Arguments.of((Runnable) () -> {
214+
try {
215+
RuntimeHintsAgentTests.class.getModule().getResourceAsStream("sample.txt");
216+
}
217+
catch (IOException e) {
218+
}
219+
},
220+
MethodReference.of(Module.class, "getResourceAsStream")),
221+
Arguments.of((Runnable) () -> classLoader.resources("sample.txt"),
222+
MethodReference.of(ClassLoader.class, "resources"))
223+
);
224+
}
225+
226+
@Test
227+
void shouldInstrumentStaticMethodHandle() {
228+
RecordingSession session = RecordingSession.record(ClassLoader.class::getClasses);
229+
assertThat(session.recordedInvocations(HintType.REFLECTION)).hasSize(1);
230+
231+
RecordedInvocation resolution = session.recordedInvocations(HintType.REFLECTION).findFirst().get();
232+
assertThat(resolution.getMethodReference()).isEqualTo(MethodReference.of(Class.class, "getClasses"));
233+
assertThat(resolution.getStackFrames()).first().extracting(StackWalker.StackFrame::getClassName)
234+
.isEqualTo(RuntimeHintsAgentTests.class.getName() + "$RecordingSession");
235+
}
236+
237+
static class RecordingSession implements RecordedInvocationsListener {
238+
239+
final Deque<RecordedInvocation> recordedInvocations = new ArrayDeque<>();
240+
241+
static RecordingSession record(Runnable action) {
242+
RecordingSession session = new RecordingSession();
243+
RecordedInvocationsPublisher.addListener(session);
244+
try {
245+
action.run();
246+
}
247+
finally {
248+
RecordedInvocationsPublisher.removeListener(session);
249+
}
250+
return session;
251+
}
252+
253+
@Override
254+
public void onInvocation(RecordedInvocation invocation) {
255+
this.recordedInvocations.addLast(invocation);
256+
}
257+
258+
Stream<RecordedInvocation> recordedInvocations() {
259+
return this.recordedInvocations.stream();
260+
}
261+
262+
Stream<RecordedInvocation> recordedInvocations(HintType hintType) {
263+
return recordedInvocations().filter(invocation -> invocation.getHintType() == hintType);
264+
}
265+
266+
}
267+
268+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.test;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.aot.hint.MemberCategory;
22+
import org.springframework.aot.hint.RuntimeHints;
23+
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
24+
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
25+
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
26+
27+
import static org.assertj.core.api.Assertions.assertThat;
28+
29+
30+
@EnabledIfRuntimeHintsAgent
31+
class ReflectionInvocationsTests {
32+
33+
@Test
34+
void sampleTest() {
35+
RuntimeHints hints = new RuntimeHints();
36+
hints.reflection().registerType(String.class, hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS));
37+
38+
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
39+
SampleReflection sample = new SampleReflection();
40+
sample.sample(); // does Method[] methods = String.class.getMethods();
41+
});
42+
assertThat(invocations).match(hints);
43+
}
44+
45+
@Test
46+
void multipleCallsTest() {
47+
RuntimeHints hints = new RuntimeHints();
48+
hints.reflection().registerType(String.class, hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS));
49+
hints.reflection().registerType(Integer.class, hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS));
50+
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
51+
SampleReflection sample = new SampleReflection();
52+
sample.multipleCalls(); // does Method[] methods = String.class.getMethods(); methods = Integer.class.getMethods();
53+
});
54+
assertThat(invocations).match(hints);
55+
}
56+
57+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.test;
18+
19+
import java.lang.reflect.Method;
20+
21+
/**
22+
* @author Brian Clozel
23+
*/
24+
public class SampleReflection {
25+
26+
public void sample() {
27+
String value = "Sample";
28+
Method[] methods = String.class.getMethods();
29+
}
30+
31+
public void multipleCalls() {
32+
String value = "Sample";
33+
Method[] methods = String.class.getMethods();
34+
methods = Integer.class.getMethods();
35+
}
36+
37+
}

0 commit comments

Comments
 (0)