This is an automated email from the ASF dual-hosted git repository.
alpinegizmo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new 96d0bfb8909 [FLINK-40138][tests] Drive JUnit 5 lifecycle when
generating migration test snapshots (#28753)
96d0bfb8909 is described below
commit 96d0bfb8909382ab755013b1e47620fc1b7d3bc5
Author: David Anderson <[email protected]>
AuthorDate: Wed Jul 15 21:05:38 2026 -0400
[FLINK-40138][tests] Drive JUnit 5 lifecycle when generating migration test
snapshots (#28753)
Generated-by: Claude Opus 4.8 (1M context) <[email protected]>
---
.../flink-migration-test-utils/pom.xml | 11 +
.../test/migration/JUnit5TestEnvironment.java | 543 +++++++++++++++++++++
.../test/migration/SnapshotGeneratorUtils.java | 189 +++++--
3 files changed, 704 insertions(+), 39 deletions(-)
diff --git a/flink-test-utils-parent/flink-migration-test-utils/pom.xml
b/flink-test-utils-parent/flink-migration-test-utils/pom.xml
index 5ba3484453e..88056800df2 100644
--- a/flink-test-utils-parent/flink-migration-test-utils/pom.xml
+++ b/flink-test-utils-parent/flink-migration-test-utils/pom.xml
@@ -51,6 +51,17 @@ under the License.
<artifactId>junit-vintage-engine</artifactId>
</dependency>
+ <!--
+ The snapshot generator drives the JUnit 5 lifecycle of migration
tests (see
+ JUnit5TestEnvironment), so the JUnit 5 API is needed on the main
(compile) classpath,
+ overriding the test scope that is otherwise inherited.
+ -->
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <scope>compile</scope>
+ </dependency>
+
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
diff --git
a/flink-test-utils-parent/flink-migration-test-utils/src/main/java/org/apache/flink/test/migration/JUnit5TestEnvironment.java
b/flink-test-utils-parent/flink-migration-test-utils/src/main/java/org/apache/flink/test/migration/JUnit5TestEnvironment.java
new file mode 100644
index 00000000000..bf5d13569ab
--- /dev/null
+++
b/flink-test-utils-parent/flink-migration-test-utils/src/main/java/org/apache/flink/test/migration/JUnit5TestEnvironment.java
@@ -0,0 +1,543 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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.apache.flink.test.migration;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExecutableInvoker;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.api.extension.TestInstances;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.api.parallel.ExecutionMode;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * Drives the subset of the JUnit 5 lifecycle that the migration test data
generation relies on.
+ *
+ * <p>The {@link MigrationTestsSnapshotGenerator} instantiates the migration
test classes directly
+ * via reflection and invokes their
+ * {@code @SnapshotsGenerator}/{@code @ParameterizedSnapshotsGenerator}
methods outside of any JUnit
+ * engine. Migration tests that have been migrated to JUnit 5 therefore need
their extensions and
+ * lifecycle methods to be driven manually, otherwise resources such as the
{@link
+ * org.apache.flink.runtime.minicluster.MiniCluster} or the executor service
backing the test would
+ * never be set up.
+ *
+ * <p>This environment supports:
+ *
+ * <ul>
+ * <li>{@code @RegisterExtension} fields (static and instance) implementing
the {@link
+ * BeforeAllCallback}, {@link BeforeEachCallback}, {@link
AfterEachCallback}, {@link
+ * AfterAllCallback} and {@link ParameterResolver} extension points,
+ * <li>{@code @BeforeEach}/{@code @AfterEach} lifecycle methods, including
parameter injection
+ * (for example {@link
org.apache.flink.test.junit5.InjectClusterClient}) through the
+ * registered {@link ParameterResolver}s, and
+ * <li>{@code @TempDir} fields.
+ * </ul>
+ *
+ * <p>Class-level extensions registered via {@code @ExtendWith} are
intentionally not driven: they
+ * are concerned with the test runner itself (parameterization of {@code
@TestTemplate} methods,
+ * logging) and are not required to set up the resources used during snapshot
generation.
+ *
+ * <p>The lifecycle is driven so that {@link #beforeAll()} runs before the
test instance is created,
+ * matching JUnit 5 semantics and, crucially, allowing instance field
initializers to reference
+ * resources set up by statically registered extensions.
+ */
+class JUnit5TestEnvironment {
+
+ private final Class<?> testClass;
+ private final GeneratorExtensionContext extensionContext;
+ private final List<Object> staticExtensions = new ArrayList<>();
+ private final List<Object> instanceExtensions = new ArrayList<>();
+ private final List<Path> temporaryDirectories = new ArrayList<>();
+
+ JUnit5TestEnvironment(Class<?> testClass) {
+ this.testClass = testClass;
+ this.extensionContext = new GeneratorExtensionContext(testClass);
+ }
+
+ /**
+ * Runs the {@code beforeAll} callbacks of statically registered
extensions. Must be called
+ * before the test instance is created, because instance field
initializers may already
+ * reference resources set up by these callbacks (for example the executor
service used to build
+ * a {@link
org.apache.flink.util.concurrent.ScheduledExecutorServiceAdapter}).
+ */
+ void beforeAll() throws Exception {
+ collectRegisteredExtensions(null, staticExtensions);
+ for (Object extension : staticExtensions) {
+ if (extension instanceof BeforeAllCallback) {
+ ((BeforeAllCallback) extension).beforeAll(extensionContext);
+ }
+ }
+ }
+
+ /** Collects instance-level extensions and injects {@code @TempDir}
fields. */
+ void initializeTestInstance(Object testInstance) throws Exception {
+ extensionContext.setTestInstance(testInstance);
+ collectRegisteredExtensions(testInstance, instanceExtensions);
+ injectTemporaryDirectories(testInstance);
+ }
+
+ /** Runs {@code beforeEach} callbacks followed by the {@code @BeforeEach}
methods. */
+ void beforeEach(Object testInstance) throws Throwable {
+ for (Object extension : allExtensions()) {
+ if (extension instanceof BeforeEachCallback) {
+ ((BeforeEachCallback) extension).beforeEach(extensionContext);
+ }
+ }
+ for (Method method : lifecycleMethods(BeforeEach.class, true)) {
+ invokeLifecycleMethod(testInstance, method);
+ }
+ }
+
+ /** Runs the {@code @AfterEach} methods followed by the {@code afterEach}
callbacks. */
+ void afterEach(Object testInstance) throws Throwable {
+ for (Method method : lifecycleMethods(AfterEach.class, false)) {
+ invokeLifecycleMethod(testInstance, method);
+ }
+ for (Object extension : reversed(allExtensions())) {
+ if (extension instanceof AfterEachCallback) {
+ ((AfterEachCallback) extension).afterEach(extensionContext);
+ }
+ }
+ }
+
+ /**
+ * Runs the {@code afterAll} callbacks of statically registered
extensions, closes the stored
+ * {@link ExtensionContext.Store.CloseableResource}s and removes the
temporary directories.
+ */
+ void afterAll() throws Exception {
+ for (Object extension : reversed(staticExtensions)) {
+ if (extension instanceof AfterAllCallback) {
+ ((AfterAllCallback) extension).afterAll(extensionContext);
+ }
+ }
+ extensionContext.close();
+ deleteTemporaryDirectories();
+ }
+
+ private List<Object> allExtensions() {
+ List<Object> all = new ArrayList<>(staticExtensions);
+ all.addAll(instanceExtensions);
+ return all;
+ }
+
+ private static List<Object> reversed(List<Object> list) {
+ List<Object> reversed = new ArrayList<>(list);
+ Collections.reverse(reversed);
+ return reversed;
+ }
+
+ private void collectRegisteredExtensions(Object instance, List<Object>
target)
+ throws IllegalAccessException {
+ // Iterate the hierarchy from the topmost ancestor down to the test
class, so that
+ // extensions declared in super classes are set up before those of sub
classes.
+ for (Class<?> clazz : classHierarchy(testClass)) {
+ for (Field field : clazz.getDeclaredFields()) {
+ if (!field.isAnnotationPresent(RegisterExtension.class)) {
+ continue;
+ }
+ boolean isStatic = Modifier.isStatic(field.getModifiers());
+ // When looking up static extensions no instance is available
yet; when looking up
+ // instance extensions we skip the static ones as they were
already collected.
+ if (isStatic != (instance == null)) {
+ continue;
+ }
+ field.setAccessible(true);
+ Object value = field.get(instance);
+ if (value != null) {
+ target.add(value);
+ }
+ }
+ }
+ }
+
+ private void injectTemporaryDirectories(Object instance)
+ throws IOException, IllegalAccessException {
+ for (Class<?> clazz : classHierarchy(testClass)) {
+ for (Field field : clazz.getDeclaredFields()) {
+ if (!field.isAnnotationPresent(TempDir.class)) {
+ continue;
+ }
+ Path directory =
Files.createTempDirectory("migration-test-generator");
+ temporaryDirectories.add(directory);
+ field.setAccessible(true);
+ Class<?> type = field.getType();
+ if (Path.class.equals(type)) {
+ field.set(instance, directory);
+ } else if (File.class.equals(type)) {
+ field.set(instance, directory.toFile());
+ } else {
+ throw new IllegalStateException(
+ "Unsupported @TempDir field type "
+ + type.getName()
+ + " on field "
+ + field
+ + ". Only java.nio.file.Path and
java.io.File are supported.");
+ }
+ }
+ }
+ }
+
+ private void deleteTemporaryDirectories() {
+ for (Path directory : temporaryDirectories) {
+ try (Stream<Path> paths = Files.walk(directory)) {
+ paths.sorted(Comparator.reverseOrder())
+ .forEach(JUnit5TestEnvironment::deleteQuietly);
+ } catch (IOException ignored) {
+ // Best effort clean up of temporary directories.
+ }
+ }
+ }
+
+ private static void deleteQuietly(Path path) {
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException ignored) {
+ // Best effort clean up of temporary directories.
+ }
+ }
+
+ private List<Method> lifecycleMethods(
+ Class<? extends Annotation> annotation, boolean superClassFirst) {
+ List<Class<?>> hierarchy = classHierarchy(testClass);
+ if (!superClassFirst) {
+ Collections.reverse(hierarchy);
+ }
+ List<Method> methods = new ArrayList<>();
+ Set<String> visitedSignatures = new HashSet<>();
+ for (Class<?> clazz : hierarchy) {
+ for (Method method : clazz.getDeclaredMethods()) {
+ if (!method.isAnnotationPresent(annotation)) {
+ continue;
+ }
+ String signature = method.getName() +
Arrays.toString(method.getParameterTypes());
+ if (visitedSignatures.add(signature)) {
+ methods.add(method);
+ }
+ }
+ }
+ return methods;
+ }
+
+ private void invokeLifecycleMethod(Object testInstance, Method method)
throws Throwable {
+ method.setAccessible(true);
+ Object[] arguments = resolveParameters(testInstance, method);
+ try {
+ method.invoke(testInstance, arguments);
+ } catch (InvocationTargetException e) {
+ throw e.getTargetException();
+ }
+ }
+
+ private Object[] resolveParameters(Object testInstance, Method method) {
+ Parameter[] parameters = method.getParameters();
+ Object[] arguments = new Object[parameters.length];
+ for (int i = 0; i < parameters.length; i++) {
+ GeneratorParameterContext parameterContext =
+ new GeneratorParameterContext(parameters[i], i,
Optional.of(testInstance));
+ arguments[i] = resolveParameter(parameterContext);
+ }
+ return arguments;
+ }
+
+ private Object resolveParameter(GeneratorParameterContext
parameterContext) {
+ for (Object extension : allExtensions()) {
+ if (extension instanceof ParameterResolver) {
+ ParameterResolver resolver = (ParameterResolver) extension;
+ if (resolver.supportsParameter(parameterContext,
extensionContext)) {
+ return resolver.resolveParameter(parameterContext,
extensionContext);
+ }
+ }
+ }
+ throw new ParameterResolutionException(
+ "No registered extension can resolve parameter ["
+ + parameterContext.getParameter()
+ + "] of ["
+ +
parameterContext.getParameter().getDeclaringExecutable()
+ + "].");
+ }
+
+ private static List<Class<?>> classHierarchy(Class<?> testClass) {
+ List<Class<?>> hierarchy = new ArrayList<>();
+ for (Class<?> clazz = testClass;
+ clazz != null && clazz != Object.class;
+ clazz = clazz.getSuperclass()) {
+ hierarchy.add(clazz);
+ }
+ // Topmost ancestor first.
+ Collections.reverse(hierarchy);
+ return hierarchy;
+ }
+
+ /**
+ * A minimal {@link ExtensionContext} that provides just enough behavior
for the extensions used
+ * by the migration tests, most importantly a working {@link
ExtensionContext.Store}.
+ */
+ private static final class GeneratorExtensionContext implements
ExtensionContext {
+
+ private final Class<?> testClass;
+ private final Map<Namespace, GeneratorStore> stores = new
ConcurrentHashMap<>();
+ private Object testInstance;
+
+ private GeneratorExtensionContext(Class<?> testClass) {
+ this.testClass = testClass;
+ }
+
+ private void setTestInstance(Object testInstance) {
+ this.testInstance = testInstance;
+ }
+
+ private void close() throws Exception {
+ for (GeneratorStore store : stores.values()) {
+ store.close();
+ }
+ }
+
+ @Override
+ public Optional<ExtensionContext> getParent() {
+ return Optional.empty();
+ }
+
+ @Override
+ public ExtensionContext getRoot() {
+ return this;
+ }
+
+ @Override
+ public String getUniqueId() {
+ return testClass.getName();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return testClass.getSimpleName();
+ }
+
+ @Override
+ public Set<String> getTags() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Optional<AnnotatedElement> getElement() {
+ return Optional.of(testClass);
+ }
+
+ @Override
+ public Optional<Class<?>> getTestClass() {
+ return Optional.of(testClass);
+ }
+
+ @Override
+ public Optional<TestInstance.Lifecycle> getTestInstanceLifecycle() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<Object> getTestInstance() {
+ return Optional.ofNullable(testInstance);
+ }
+
+ @Override
+ public Optional<TestInstances> getTestInstances() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<Method> getTestMethod() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<Throwable> getExecutionException() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional<String> getConfigurationParameter(String key) {
+ return Optional.empty();
+ }
+
+ @Override
+ public <T> Optional<T> getConfigurationParameter(
+ String key, Function<String, T> transformer) {
+ return Optional.empty();
+ }
+
+ @Override
+ public void publishReportEntry(Map<String, String> map) {
+ // Report entries are not collected during snapshot generation.
+ }
+
+ @Override
+ public Store getStore(Namespace namespace) {
+ return stores.computeIfAbsent(namespace, ignored -> new
GeneratorStore());
+ }
+
+ @Override
+ public ExecutionMode getExecutionMode() {
+ return ExecutionMode.SAME_THREAD;
+ }
+
+ @Override
+ public ExecutableInvoker getExecutableInvoker() {
+ throw new UnsupportedOperationException(
+ "ExecutableInvoker is not supported during migration test
data generation.");
+ }
+ }
+
+ /** A minimal {@link ExtensionContext.Store} backed by a map. */
+ private static final class GeneratorStore implements
ExtensionContext.Store {
+
+ private final Map<Object, Object> values = new ConcurrentHashMap<>();
+
+ @Override
+ public Object get(Object key) {
+ return values.get(key);
+ }
+
+ @Override
+ public <V> V get(Object key, Class<V> requiredType) {
+ return requiredType.cast(values.get(key));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <K, V> Object getOrComputeIfAbsent(K key, Function<K, V>
defaultCreator) {
+ return values.computeIfAbsent(key, mapKey ->
defaultCreator.apply((K) mapKey));
+ }
+
+ @Override
+ public <K, V> V getOrComputeIfAbsent(
+ K key, Function<K, V> defaultCreator, Class<V> requiredType) {
+ return requiredType.cast(getOrComputeIfAbsent(key,
defaultCreator));
+ }
+
+ @Override
+ public void put(Object key, Object value) {
+ values.put(key, value);
+ }
+
+ @Override
+ public Object remove(Object key) {
+ return values.remove(key);
+ }
+
+ @Override
+ public <V> V remove(Object key, Class<V> requiredType) {
+ return requiredType.cast(values.remove(key));
+ }
+
+ private void close() throws Exception {
+ Throwable firstError = null;
+ for (Object value : values.values()) {
+ if (value instanceof CloseableResource) {
+ try {
+ ((CloseableResource) value).close();
+ } catch (Throwable t) {
+ if (firstError == null) {
+ firstError = t;
+ } else {
+ firstError.addSuppressed(t);
+ }
+ }
+ }
+ }
+ if (firstError != null) {
+ throw new Exception("Failed to close stored resources.",
firstError);
+ }
+ }
+ }
+
+ /** A minimal {@link ParameterContext} wrapping a single {@link
Parameter}. */
+ private static final class GeneratorParameterContext implements
ParameterContext {
+
+ private final Parameter parameter;
+ private final int index;
+ private final Optional<Object> target;
+
+ private GeneratorParameterContext(Parameter parameter, int index,
Optional<Object> target) {
+ this.parameter = parameter;
+ this.index = index;
+ this.target = target;
+ }
+
+ @Override
+ public Parameter getParameter() {
+ return parameter;
+ }
+
+ @Override
+ public int getIndex() {
+ return index;
+ }
+
+ @Override
+ public Optional<Object> getTarget() {
+ return target;
+ }
+
+ @Override
+ public boolean isAnnotated(Class<? extends Annotation> annotationType)
{
+ return parameter.isAnnotationPresent(annotationType);
+ }
+
+ @Override
+ public <A extends Annotation> Optional<A> findAnnotation(Class<A>
annotationType) {
+ return
Optional.ofNullable(parameter.getAnnotation(annotationType));
+ }
+
+ @Override
+ public <A extends Annotation> List<A>
findRepeatableAnnotations(Class<A> annotationType) {
+ return
Arrays.asList(parameter.getAnnotationsByType(annotationType));
+ }
+ }
+}
diff --git
a/flink-test-utils-parent/flink-migration-test-utils/src/main/java/org/apache/flink/test/migration/SnapshotGeneratorUtils.java
b/flink-test-utils-parent/flink-migration-test-utils/src/main/java/org/apache/flink/test/migration/SnapshotGeneratorUtils.java
index 5fa0542d795..1f9e2bfab04 100644
---
a/flink-test-utils-parent/flink-migration-test-utils/src/main/java/org/apache/flink/test/migration/SnapshotGeneratorUtils.java
+++
b/flink-test-utils-parent/flink-migration-test-utils/src/main/java/org/apache/flink/test/migration/SnapshotGeneratorUtils.java
@@ -35,18 +35,22 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/** A utility class to execute generateSnapshots method for migration tests. */
class SnapshotGeneratorUtils {
static void executeGenerate(Class<?> migrationTestClass, FlinkVersion
flinkVersion)
throws Throwable {
- for (Method method : migrationTestClass.getMethods()) {
+ for (Method method : findGeneratorMethods(migrationTestClass)) {
if
(method.isAnnotationPresent(MigrationTest.SnapshotsGenerator.class)) {
executeGenerateMethods(migrationTestClass, method,
flinkVersion);
} else if (method.isAnnotationPresent(
@@ -55,36 +59,74 @@ class SnapshotGeneratorUtils {
method.getAnnotation(MigrationTest.ParameterizedSnapshotsGenerator.class)
.value();
Method parametersMethod =
- migrationTestClass.getMethod(parametersMethodName,
FlinkVersion.class);
+ findDeclaredMethod(
+ migrationTestClass, parametersMethodName,
FlinkVersion.class);
executeParameterizedGenerateMethods(
migrationTestClass, method, parametersMethod,
flinkVersion);
}
}
}
+ /**
+ * Finds all methods annotated with {@link
MigrationTest.SnapshotsGenerator} or {@link
+ * MigrationTest.ParameterizedSnapshotsGenerator} declared by the test
class or any of its super
+ * classes. Unlike {@link Class#getMethods()}, this also discovers
non-public generator methods.
+ */
+ private static List<Method> findGeneratorMethods(Class<?>
migrationTestClass) {
+ List<Method> methods = new ArrayList<>();
+ Set<String> visitedSignatures = new HashSet<>();
+ for (Class<?> clazz = migrationTestClass;
+ clazz != null && clazz != Object.class;
+ clazz = clazz.getSuperclass()) {
+ for (Method method : clazz.getDeclaredMethods()) {
+ if
(method.isAnnotationPresent(MigrationTest.SnapshotsGenerator.class)
+ || method.isAnnotationPresent(
+
MigrationTest.ParameterizedSnapshotsGenerator.class)) {
+ String signature =
+ method.getName() +
Arrays.toString(method.getParameterTypes());
+ if (visitedSignatures.add(signature)) {
+ methods.add(method);
+ }
+ }
+ }
+ }
+ return methods;
+ }
+
+ /**
+ * Finds a method by name and parameter types declared by the test class
or any of its super
+ * classes. Unlike {@link Class#getMethod(String, Class[])}, this also
discovers non-public
+ * methods, such as the {@code parameters} method referenced by {@link
+ * MigrationTest.ParameterizedSnapshotsGenerator}.
+ */
+ private static Method findDeclaredMethod(
+ Class<?> migrationTestClass, String name, Class<?>...
parameterTypes)
+ throws NoSuchMethodException {
+ for (Class<?> clazz = migrationTestClass;
+ clazz != null && clazz != Object.class;
+ clazz = clazz.getSuperclass()) {
+ try {
+ return clazz.getDeclaredMethod(name, parameterTypes);
+ } catch (NoSuchMethodException ignored) {
+ // Continue searching up the class hierarchy.
+ }
+ }
+ throw new NoSuchMethodException(
+ "Could not find method "
+ + name
+ + Arrays.toString(parameterTypes)
+ + " in "
+ + migrationTestClass
+ + " or any of its super classes.");
+ }
+
private static void executeGenerateMethods(
Class<?> migrationTestClass, Method method, FlinkVersion version)
throws Throwable {
method.setAccessible(true);
- List<TestRule> classRules = getRuleFields(migrationTestClass,
ClassRule.class, null);
-
- executeWithRules(
- classRules,
- new Statement() {
- @Override
- public void evaluate() throws Throwable {
- Object migrationTest =
createMigrationTest(migrationTestClass);
- List<TestRule> rules =
- getRuleFields(migrationTestClass, Rule.class,
migrationTest);
- executeWithRules(
- rules,
- new Statement() {
- @Override
- public void evaluate() throws Throwable {
- method.invoke(migrationTest, version);
- }
- });
- }
- });
+ executeWithLifecycle(
+ migrationTestClass,
+ (migrationTest, invocation) ->
+ invocation.run(() -> invoke(method, migrationTest,
version)));
}
private static void executeParameterizedGenerateMethods(
@@ -95,27 +137,68 @@ class SnapshotGeneratorUtils {
throws Throwable {
method.setAccessible(true);
parametersMethod.setAccessible(true);
- List<TestRule> classRules = getRuleFields(migrationTestClass,
ClassRule.class, null);
+ executeWithLifecycle(
+ migrationTestClass,
+ (migrationTest, invocation) -> {
+ Collection<?> arguments =
+ (Collection<?>) invoke(parametersMethod,
migrationTest, version);
+ for (Object argument : arguments) {
+ invocation.run(() -> invoke(method, migrationTest,
argument));
+ }
+ });
+ }
+ /**
+ * Drives the migration test's lifecycle around the given {@code action}.
Both the JUnit 4
+ * {@code @ClassRule}/{@code @Rule} {@link ExternalResource}s and the
JUnit 5 lifecycle (see
+ * {@link JUnit5TestEnvironment}) are set up, so that migration tests
using either framework
+ * have their resources initialized. The JUnit 5 steps are no-ops for
tests that do not use any
+ * of the supported JUnit 5 constructs, leaving pure JUnit 4 tests
unaffected.
+ *
+ * <p>Class-level setup ({@code @ClassRule}s, {@code beforeAll}) and the
test instance are
+ * created once. The {@code action} then triggers one or more generator
method invocations
+ * through the provided {@link Invocation}, and each invocation is wrapped
in its own instance
+ * lifecycle ({@code @Rule}s, {@code beforeEach}/{@code afterEach}), so
that a fresh set of
+ * per-test resources (such as a {@code MiniCluster}) is provided for
every generated snapshot.
+ */
+ private static void executeWithLifecycle(Class<?> migrationTestClass,
GenerateAction action)
+ throws Throwable {
+ List<TestRule> classRules = getRuleFields(migrationTestClass,
ClassRule.class, null);
executeWithRules(
classRules,
new Statement() {
@Override
public void evaluate() throws Throwable {
- Object migrationTest =
createMigrationTest(migrationTestClass);
- List<TestRule> rules =
- getRuleFields(migrationTestClass, Rule.class,
migrationTest);
- Collection<?> arguments =
- (Collection<?>)
parametersMethod.invoke(migrationTest, version);
- for (Object argument : arguments) {
- executeWithRules(
- rules,
- new Statement() {
- @Override
- public void evaluate() throws
Throwable {
- method.invoke(migrationTest,
argument);
- }
- });
+ JUnit5TestEnvironment environment =
+ new JUnit5TestEnvironment(migrationTestClass);
+ // beforeAll must run before the instance is created,
as instance field
+ // initializers may reference resources set up by
statically registered
+ // extensions.
+ environment.beforeAll();
+ try {
+ Object migrationTest =
createMigrationTest(migrationTestClass);
+ environment.initializeTestInstance(migrationTest);
+ List<TestRule> rules =
+ getRuleFields(migrationTestClass,
Rule.class, migrationTest);
+ Invocation invocation =
+ generatorCall ->
+ executeWithRules(
+ rules,
+ new Statement() {
+ @Override
+ public void evaluate()
throws Throwable {
+
environment.beforeEach(migrationTest);
+ try {
+
generatorCall.run();
+ } finally {
+
environment.afterEach(
+
migrationTest);
+ }
+ }
+ });
+ action.execute(migrationTest, invocation);
+ } finally {
+ environment.afterAll();
}
}
});
@@ -126,6 +209,36 @@ class SnapshotGeneratorUtils {
new RunRules(statement, rules, Description.EMPTY).evaluate();
}
+ private static Object invoke(Method method, Object target, Object... args)
throws Throwable {
+ try {
+ return method.invoke(target, args);
+ } catch (InvocationTargetException e) {
+ throw e.getTargetException();
+ }
+ }
+
+ /**
+ * Generates snapshots using a migration test instance. Every generator
method invocation must
+ * be triggered through the given {@link Invocation} so that it runs
within its own instance
+ * lifecycle.
+ */
+ @FunctionalInterface
+ private interface GenerateAction {
+ void execute(Object migrationTest, Invocation invocation) throws
Throwable;
+ }
+
+ /** Runs a single generator method invocation within a fresh instance
lifecycle. */
+ @FunctionalInterface
+ private interface Invocation {
+ void run(GeneratorCall generatorCall) throws Throwable;
+ }
+
+ /** A single generator method invocation. */
+ @FunctionalInterface
+ private interface GeneratorCall {
+ void run() throws Throwable;
+ }
+
private static List<TestRule> getRuleFields(
Class<?> migrationTestClass, Class<? extends Annotation> tagClass,
Object migrationTest)
throws IllegalAccessException {
@@ -156,13 +269,11 @@ class SnapshotGeneratorUtils {
Constructor<?> constructor = constructors[0];
constructor.setAccessible(true);
- System.out.println("create test!!!");
// Check if we could find method labeled with
@Parameterized.Parameters or @Parameters
for (Method method : migrationTestClass.getMethods()) {
if (Modifier.isStatic(method.getModifiers())
&&
(method.isAnnotationPresent(Parameterized.Parameters.class)
|| method.isAnnotationPresent(Parameters.class))) {
- System.out.println("ffffffff!!!!");
Object argumentLists = method.invoke(null);
if (argumentLists instanceof Collection) {
return constructor.newInstance(