diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/EnabledIfRuntimeHintsAgent.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/EnabledIfRuntimeHintsAgent.java new file mode 100644 index 00000000000..36ef2803b43 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/EnabledIfRuntimeHintsAgent.java @@ -0,0 +1,63 @@ +/* + * 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.agent; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.aot.agent.RuntimeHintsAgent; + +/** + * {@code @EneabledIfRuntimeHintsAgent} signals that the annotated test class or test method + * is only enabled if the {@link RuntimeHintsAgent} is loaded on the current JVM. + * + *
+ * @EnabledIfRuntimeHintsAgent
+ * class MyTestCases {
+ *
+ * @Test
+ * void hintsForMethodsReflectionShouldMatch() {
+ * RuntimeHints hints = new RuntimeHints();
+ * hints.reflection().registerType(String.class,
+ * hint -> hint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS));
+ *
+ * RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
+ * Method[] methods = String.class.getMethods();
+ * });
+ * assertThat(invocations).match(hints);
+ * }
+ *
+ * }
+ *
+ *
+ * @author Brian Clozel
+ * @since 6.0
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@ExtendWith(RuntimeHintsAgentCondition.class)
+@Tag("RuntimeHintsTests")
+public @interface EnabledIfRuntimeHintsAgent {
+
+}
diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsAgentCondition.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsAgentCondition.java
new file mode 100644
index 00000000000..9dfcddb7d73
--- /dev/null
+++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsAgentCondition.java
@@ -0,0 +1,46 @@
+/*
+ * 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.agent;
+
+import org.junit.jupiter.api.extension.ConditionEvaluationResult;
+import org.junit.jupiter.api.extension.ExecutionCondition;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import org.springframework.aot.agent.RuntimeHintsAgent;
+
+import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation;
+
+/**
+ * {@link ExecutionCondition} for {@link EnabledIfRuntimeHintsAgent @EnabledIfRuntimeHintsAgent}.
+ *
+ * @author Brian Clozel
+ */
+public class RuntimeHintsAgentCondition implements ExecutionCondition {
+
+
+ @Override
+ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
+ return findAnnotation(context.getElement(), EnabledIfRuntimeHintsAgent.class)
+ .map(annotation -> checkRuntimeHintsAgentPresence())
+ .orElse(ConditionEvaluationResult.enabled("@RuntimeHintsTest is not present"));
+ }
+
+ static ConditionEvaluationResult checkRuntimeHintsAgentPresence() {
+ return RuntimeHintsAgent.isLoaded() ? ConditionEvaluationResult.enabled("RuntimeHintsAgent is loaded")
+ : ConditionEvaluationResult.disabled("RuntimeHintsAgent is not loaded on the current JVM");
+ }
+}
diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocations.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocations.java
new file mode 100644
index 00000000000..753d0186dcb
--- /dev/null
+++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocations.java
@@ -0,0 +1,50 @@
+/*
+ * 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.agent;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.assertj.core.api.AssertProvider;
+
+import org.springframework.aot.agent.RecordedInvocation;
+
+/**
+ * A wrapper for {@link RecordedInvocation} that is the starting point for {@code RuntimeHints} AssertJ assertions.
+ *
+ * @author Brian Clozel
+ * @since 6.0
+ * @see RuntimeHintsInvocationsAssert
+ */
+public class RuntimeHintsInvocations implements AssertProvider+ * Example:
+ * RuntimeHints hints = new RuntimeHints(); + * hints.reflection().registerType(MyType.class); + * assertThat(invocations).allMatch(hints);+ * @param runtimeHints the runtime hints configuration to test against + * @throws AssertionError if any of the recorded invocations has no match in the provided hints + */ + public void match(RuntimeHints runtimeHints) { + Assert.notNull(runtimeHints, "RuntimeHints should not be null"); + configureRuntimeHints(runtimeHints); + List
+ * Example:
+ * assertThat(invocations).hasCount(42);+ * @param count the expected invocations count + * @return {@code this} assertion object. + * @throws AssertionError if the number of recorded invocations doesn't match the expected one + */ + public RuntimeHintsInvocationsAssert hasCount(long count) { + isNotNull(); + long invocationsCount = this.actual.recordedInvocations().count(); + if(invocationsCount != count) { + throwAssertionError(new BasicErrorMessageFactory("%nNumber of recorded invocations does not match, expected <%n> but got <%n>.", + invocationsCount, count)); + } + return this; + } + + +} diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsRecorder.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsRecorder.java new file mode 100644 index 00000000000..abad3a85528 --- /dev/null +++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsRecorder.java @@ -0,0 +1,76 @@ +/* + * 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.agent; + +import java.util.ArrayDeque; +import java.util.Deque; + +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.hint.RuntimeHints; +import org.springframework.util.Assert; + +/** + * Invocations relevant to {@link RuntimeHints} recorded during the execution of a block + * of code instrumented by the {@link RuntimeHintsAgent}. + * + * @author Brian Clozel + * @since 6.0 + */ +public final class RuntimeHintsRecorder { + + private final RuntimeHintsInvocationsListener listener; + + private RuntimeHintsRecorder() { + this.listener = new RuntimeHintsInvocationsListener(); + } + + /** + * Record all method invocations relevant to {@link RuntimeHints} that happened + * during the execution of the given action. + * @param action the block of code we want to record invocations from + * @return the recorded invocations + */ + public synchronized static RuntimeHintsInvocations record(Runnable action) { + Assert.notNull(action, "Runnable action should not be null"); + Assert.isTrue(RuntimeHintsAgent.isLoaded(), "RuntimeHintsAgent should be loaded in the current JVM"); + RuntimeHintsRecorder recorder = new RuntimeHintsRecorder(); + RecordedInvocationsPublisher.addListener(recorder.listener); + try { + action.run(); + } + finally { + RecordedInvocationsPublisher.removeListener(recorder.listener); + } + return new RuntimeHintsInvocations(recorder.listener.recordedInvocations.stream().toList()); + } + + + private static final class RuntimeHintsInvocationsListener implements RecordedInvocationsListener { + + private final Deque