diff --git a/integration-tests/integration-tests.gradle b/integration-tests/integration-tests.gradle index 5d864c7ba0..88455c9391 100644 --- a/integration-tests/integration-tests.gradle +++ b/integration-tests/integration-tests.gradle @@ -1,3 +1,7 @@ +plugins { + id 'org.springframework.build.runtimehints-agent' +} + description = "Spring Integration Tests" dependencies { @@ -5,6 +9,7 @@ dependencies { testImplementation(project(":spring-beans")) testImplementation(project(":spring-context")) testImplementation(project(":spring-core")) + testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-aop"))) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-core"))) diff --git a/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java new file mode 100644 index 0000000000..a639b0ff83 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aot/RuntimeHintsAgentTests.java @@ -0,0 +1,268 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.ResourceBundle; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import org.springframework.aot.agent.HintType; +import org.springframework.aot.agent.MethodReference; +import org.springframework.aot.agent.RecordedInvocation; +import org.springframework.aot.agent.RecordedInvocationsListener; +import org.springframework.aot.agent.RecordedInvocationsPublisher; +import org.springframework.aot.agent.RuntimeHintsAgent; +import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link RuntimeHintsAgent}. + * + * @author Brian Clozel + */ +@EnabledIfRuntimeHintsAgent +public class RuntimeHintsAgentTests { + + private static final ClassLoader classLoader = ClassLoader.getSystemClassLoader(); + + private static Constructor defaultConstructor; + + private static Method toStringMethod; + + + @BeforeAll + public static void classSetup() throws NoSuchMethodException { + defaultConstructor = String.class.getConstructor(); + toStringMethod = String.class.getMethod("toString"); + } + + + @ParameterizedTest + @MethodSource("instrumentedReflectionMethods") + void shouldInstrumentReflectionMethods(Runnable runnable, MethodReference methodReference) { + RecordingSession session = RecordingSession.record(runnable); + assertThat(session.recordedInvocations()).hasSize(1); + RecordedInvocation invocation = session.recordedInvocations().findFirst().get(); + assertThat(invocation.getMethodReference()).isEqualTo(methodReference); + assertThat(invocation.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName())); + } + + private static Stream instrumentedReflectionMethods() { + return Stream.of( + Arguments.of((Runnable) () -> { + try { + Class.forName("java.lang.String"); + } + catch (ClassNotFoundException e) { + } + }, MethodReference.of(Class.class, "forName")), + Arguments.of((Runnable) () -> String.class.getClasses(), MethodReference.of(Class.class, "getClasses")), + Arguments.of((Runnable) () -> { + try { + String.class.getConstructor(); + } + catch (NoSuchMethodException e) { + } + }, MethodReference.of(Class.class, "getConstructor")), + Arguments.of((Runnable) () -> String.class.getConstructors(), MethodReference.of(Class.class, "getConstructors")), + Arguments.of((Runnable) () -> String.class.getDeclaredClasses(), MethodReference.of(Class.class, "getDeclaredClasses")), + Arguments.of((Runnable) () -> { + try { + String.class.getDeclaredConstructor(); + } + catch (NoSuchMethodException e) { + } + }, MethodReference.of(Class.class, "getDeclaredConstructor")), + Arguments.of((Runnable) () -> String.class.getDeclaredConstructors(), MethodReference.of(Class.class, "getDeclaredConstructors")), + Arguments.of((Runnable) () -> { + try { + String.class.getDeclaredField("test"); + } + catch (NoSuchFieldException e) { + } + }, MethodReference.of(Class.class, "getDeclaredField")), + Arguments.of((Runnable) () -> String.class.getDeclaredFields(), MethodReference.of(Class.class, "getDeclaredFields")), + Arguments.of((Runnable) () -> { + try { + String.class.getDeclaredMethod("toString"); + } + catch (NoSuchMethodException e) { + } + }, MethodReference.of(Class.class, "getDeclaredMethod")), + Arguments.of((Runnable) () -> String.class.getDeclaredMethods(), MethodReference.of(Class.class, "getDeclaredMethods")), + Arguments.of((Runnable) () -> { + try { + String.class.getField("test"); + } + catch (NoSuchFieldException e) { + } + }, MethodReference.of(Class.class, "getField")), + Arguments.of((Runnable) () -> String.class.getFields(), MethodReference.of(Class.class, "getFields")), + Arguments.of((Runnable) () -> { + try { + String.class.getMethod("toString"); + } + catch (NoSuchMethodException e) { + } + }, MethodReference.of(Class.class, "getMethod")), + Arguments.of((Runnable) () -> String.class.getMethods(), MethodReference.of(Class.class, "getMethods")), + Arguments.of((Runnable) () -> { + try { + classLoader.loadClass("java.lang.String"); + } + catch (ClassNotFoundException e) { + } + }, MethodReference.of(ClassLoader.class, "loadClass")), + Arguments.of((Runnable) () -> { + try { + defaultConstructor.newInstance(); + } + catch (Exception e) { + } + }, MethodReference.of(Constructor.class, "newInstance")), + Arguments.of((Runnable) () -> { + try { + toStringMethod.invoke(""); + } + catch (Exception e) { + } + }, MethodReference.of(Method.class, "invoke")) + ); + } + + @ParameterizedTest + @MethodSource("instrumentedResourceBundleMethods") + void shouldInstrumentResourceBundleMethods(Runnable runnable, MethodReference methodReference) { + RecordingSession session = RecordingSession.record(runnable); + assertThat(session.recordedInvocations(HintType.RESOURCE_BUNDLE)).hasSize(1); + + RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_BUNDLE).findFirst().get(); + assertThat(resolution.getMethodReference()).isEqualTo(methodReference); + assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName())); + } + + + private static Stream instrumentedResourceBundleMethods() { + return Stream.of( + Arguments.of((Runnable) () -> { + try { + ResourceBundle.getBundle("testBundle"); + } + catch (Throwable exc) { + } + }, + MethodReference.of(ResourceBundle.class, "getBundle")) + ); + } + + @ParameterizedTest + @MethodSource("instrumentedResourcePatternMethods") + void shouldInstrumentResourcePatternMethods(Runnable runnable, MethodReference methodReference) { + RecordingSession session = RecordingSession.record(runnable); + assertThat(session.recordedInvocations(HintType.RESOURCE_PATTERN)).hasSize(1); + + RecordedInvocation resolution = session.recordedInvocations(HintType.RESOURCE_PATTERN).findFirst().get(); + assertThat(resolution.getMethodReference()).isEqualTo(methodReference); + assertThat(resolution.getStackFrames()).first().matches(frame -> frame.getClassName().equals(RuntimeHintsAgentTests.class.getName())); + } + + + private static Stream instrumentedResourcePatternMethods() { + return Stream.of( + Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResource("sample.txt"), + MethodReference.of(Class.class, "getResource")), + Arguments.of((Runnable) () -> RuntimeHintsAgentTests.class.getResourceAsStream("sample.txt"), + MethodReference.of(Class.class, "getResourceAsStream")), + Arguments.of((Runnable) () -> classLoader.getResource("sample.txt"), + MethodReference.of(ClassLoader.class, "getResource")), + Arguments.of((Runnable) () -> classLoader.getResourceAsStream("sample.txt"), + MethodReference.of(ClassLoader.class, "getResourceAsStream")), + Arguments.of((Runnable) () -> { + try { + classLoader.getResources("sample.txt"); + } + catch (IOException e) { + } + }, + MethodReference.of(ClassLoader.class, "getResources")), + Arguments.of((Runnable) () -> { + try { + RuntimeHintsAgentTests.class.getModule().getResourceAsStream("sample.txt"); + } + catch (IOException e) { + } + }, + MethodReference.of(Module.class, "getResourceAsStream")), + Arguments.of((Runnable) () -> classLoader.resources("sample.txt"), + MethodReference.of(ClassLoader.class, "resources")) + ); + } + + @Test + void shouldInstrumentStaticMethodHandle() { + RecordingSession session = RecordingSession.record(ClassLoader.class::getClasses); + assertThat(session.recordedInvocations(HintType.REFLECTION)).hasSize(1); + + RecordedInvocation resolution = session.recordedInvocations(HintType.REFLECTION).findFirst().get(); + assertThat(resolution.getMethodReference()).isEqualTo(MethodReference.of(Class.class, "getClasses")); + assertThat(resolution.getStackFrames()).first().extracting(StackWalker.StackFrame::getClassName) + .isEqualTo(RuntimeHintsAgentTests.class.getName() + "$RecordingSession"); + } + + static class RecordingSession implements RecordedInvocationsListener { + + final Deque recordedInvocations = new ArrayDeque<>(); + + static RecordingSession record(Runnable action) { + RecordingSession session = new RecordingSession(); + RecordedInvocationsPublisher.addListener(session); + try { + action.run(); + } + finally { + RecordedInvocationsPublisher.removeListener(session); + } + return session; + } + + @Override + public void onInvocation(RecordedInvocation invocation) { + this.recordedInvocations.addLast(invocation); + } + + Stream recordedInvocations() { + return this.recordedInvocations.stream(); + } + + Stream recordedInvocations(HintType hintType) { + return recordedInvocations().filter(invocation -> invocation.getHintType() == hintType); + } + + } + +} diff --git a/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java b/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java new file mode 100644 index 0000000000..fd7822b6b4 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.test; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; +import org.springframework.aot.test.agent.RuntimeHintsInvocations; +import org.springframework.aot.test.agent.RuntimeHintsRecorder; + +import static org.assertj.core.api.Assertions.assertThat; + + +@EnabledIfRuntimeHintsAgent +class ReflectionInvocationsTests { + + @Test + void sampleTest() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(String.class, hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + + RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> { + SampleReflection sample = new SampleReflection(); + sample.sample(); // does Method[] methods = String.class.getMethods(); + }); + assertThat(invocations).match(hints); + } + + @Test + void multipleCallsTest() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(String.class, hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + hints.reflection().registerType(Integer.class, hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> { + SampleReflection sample = new SampleReflection(); + sample.multipleCalls(); // does Method[] methods = String.class.getMethods(); methods = Integer.class.getMethods(); + }); + assertThat(invocations).match(hints); + } + +} diff --git a/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java b/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java new file mode 100644 index 0000000000..d043ec0b20 --- /dev/null +++ b/integration-tests/src/test/java/org/springframework/aot/test/SampleReflection.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aot.test; + +import java.lang.reflect.Method; + +/** + * @author Brian Clozel + */ +public class SampleReflection { + + public void sample() { + String value = "Sample"; + Method[] methods = String.class.getMethods(); + } + + public void multipleCalls() { + String value = "Sample"; + Method[] methods = String.class.getMethods(); + methods = Integer.class.getMethods(); + } + +}