This is an automated email from the ASF dual-hosted git repository. alpinegizmo pushed a commit to branch flink-40138 in repository https://gitbox.apache.org/repos/asf/flink.git
commit c73cacd92bb9c3617c2e31af44dcc853d75b0dae Author: David Anderson <[email protected]> AuthorDate: Mon Jul 13 17:32:12 2026 -0400 [FLINK-40138][tests] Drive JUnit 5 lifecycle when generating migration test snapshots 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(
