diff --git a/build.gradle.kts b/build.gradle.kts index f5e411b56..afb2f323d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,7 @@ allprojects { val declaredVersion = "1.5.0-SNAPSHOT" version = VersionManager.resolveVersion(declaredVersion, project.hasProperty("release")) - extra["generalUtilVersion"] = "1.6.0" + extra["generalUtilVersion"] = "1.7.0" extra["templateApiVersion"] = "1.3.4" extra["coreApiVersion"] = "1.3.3" extra["sqlVersion"] = "1.3.2" diff --git a/core/flamingock-core-commons/build.gradle.kts b/core/flamingock-core-commons/build.gradle.kts index dec8953ee..28d2816bc 100644 --- a/core/flamingock-core-commons/build.gradle.kts +++ b/core/flamingock-core-commons/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.fasterxml.jackson.core:jackson-core:$jacksonVersion") + testImplementation(project(":utils:test-util")) } description = "Shared internal utilities, preview system, and common infrastructure components" diff --git a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java index 7ee167856..e46d04357 100644 --- a/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java +++ b/core/flamingock-core-commons/src/main/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilder.java @@ -36,6 +36,9 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; import java.lang.annotation.Annotation; import java.util.List; import java.util.Optional; @@ -195,6 +198,9 @@ private CodePreviewChange getCodePreviewChange() { private Optional getAnnotatedMethodInfo(TypeElement typeElement, Class annotationType) { + if (typeElement == null) { + return Optional.empty(); + } for (Element enclosedElement : typeElement.getEnclosedElements()) { if (enclosedElement.getKind() == ElementKind.METHOD && enclosedElement.getAnnotation(annotationType) != null) { @@ -208,7 +214,18 @@ private Optional getAnnotatedMethodInfo(TypeElement typeElement, } } - return Optional.empty(); + TypeMirror superclass = typeElement.getSuperclass(); + if (superclass.getKind() != TypeKind.DECLARED) { + return Optional.empty(); + } + Element superclassElement = ((DeclaredType) superclass).asElement(); + + return getAnnotatedMethodInfo( + superclassElement instanceof TypeElement + ? (TypeElement) superclassElement + : null, + annotationType + ); } private PreviewConstructor getPreviewConstructor(TypeElement typeElement) { diff --git a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java index d82f2ba3a..8d0d2d7f4 100644 --- a/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java +++ b/core/flamingock-core-commons/src/test/java/io/flamingock/internal/common/core/preview/builder/CodePreviewChangeBuilderTest.java @@ -16,27 +16,64 @@ package io.flamingock.internal.common.core.preview.builder; import io.flamingock.api.annotations.Change; +import io.flamingock.common.test.processor.AnnotatedElementProcessor; +import io.flamingock.common.test.processor.JavacHelper; +import io.flamingock.internal.common.core.change.RecoveryDescriptor; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.preview.CodePreviewChange; import io.flamingock.internal.common.core.preview.PreviewConstructor; import io.flamingock.internal.common.core.preview.PreviewMethod; -import io.flamingock.internal.common.core.change.RecoveryDescriptor; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static io.flamingock.common.test.processor.JavacHelper.getSourcePath; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; class CodePreviewChangeBuilderTest { + private final CodePreviewChangeBuilder codePreviewChangeBuilder = CodePreviewChangeBuilder.instance(); + private final JavacHelper javacHelper = new JavacHelper(); + + @Test + @DisplayName("inherits apply and rollback methods from its template superclass") + void inheritsApplyAndRollbackMethodsFromTemplateSuperclass() throws IOException, URISyntaxException { + // Arrange + Path sourcePath = getSourcePath(getClass(), "/changes/_0001__ChangeUnderTest.java"); + String expectedRollbackPreviewMethodName = "rollback"; + String expectedApplyPreviewMethodName = "apply"; + AtomicInteger changeCount = new AtomicInteger(0); + + // Act + javacHelper.compile( + sourcePath, + new AnnotatedElementProcessor(Change.class, element -> { + changeCount.incrementAndGet(); + codePreviewChangeBuilder.setTypeElement(element); + }) + ); + + // Assert + assertEquals(1, changeCount.get()); + + CodePreviewChange previewChange = codePreviewChangeBuilder.build(); + assertNotNull(previewChange.getApplyPreviewMethod()); + assertEquals(expectedApplyPreviewMethodName, previewChange.getApplyPreviewMethod().getName()); + assertNotNull(previewChange.getRollbackPreviewMethod()); + assertEquals(expectedRollbackPreviewMethodName, previewChange.getRollbackPreviewMethod().getName()); + } + @Test void shouldBuildNullSourceFileForCodeChanges() { CodePreviewChange preview = CodePreviewChangeBuilder.instance() diff --git a/core/flamingock-core-commons/src/test/resources/changes/_0001__ChangeUnderTest.java b/core/flamingock-core-commons/src/test/resources/changes/_0001__ChangeUnderTest.java new file mode 100644 index 000000000..f9ccf963d --- /dev/null +++ b/core/flamingock-core-commons/src/test/resources/changes/_0001__ChangeUnderTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 + * + * 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 test; + +import io.flamingock.api.annotations.Apply; +import io.flamingock.api.annotations.Change; +import io.flamingock.api.annotations.Rollback; + +abstract class ChangeTemplate { + @Apply + public void apply(String value) { + } + + @Rollback + public void rollback(String value) { + } +} + + +@Change(id = "change-under-test", author = "author") +class _0001__ChangeUnderTest extends ChangeTemplate { +} diff --git a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java index ef2dea64f..ec66efc8c 100644 --- a/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java +++ b/core/flamingock-core/src/main/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilder.java @@ -284,7 +284,8 @@ private Optional getRollbackMethodFromPreview(CodePreviewChange preview) private Method getMethodFromNameAndParameters(String sourceClassName, String methodName, List methodParametersTypesNames) { Class sourceClass = getClassForName(sourceClassName); - return ReflectionUtil.getDeclaredMethodFromParameterTypeNames(sourceClass, methodName, methodParametersTypesNames); + return ReflectionUtil.getMethodFromParameterTypeNames( + sourceClass, methodName, methodParametersTypesNames, true); } private Method getApplyMethodFromAnnotation(Class sourceClass) { diff --git a/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java b/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java index f63446c4d..38a4e177d 100644 --- a/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java +++ b/core/flamingock-core/src/test/java/io/flamingock/internal/core/change/loaded/CodeLoadedChangeBuilderTest.java @@ -247,7 +247,102 @@ void shouldBuildFromAnnotatedClassCorrectlyWhenOrderInAnnotationNull() { assertNull(result.getSourceFile()); } + @Test + void shouldInvokeInheritedPreviewMethodsForBothEditions() throws Exception { + for (boolean legacy : new boolean[]{false, true}) { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + preview(InheritedChange.class.getName(), legacy, true)).build(); + InheritedChange instance = new InheritedChange(); + change.getApplyMethod().invoke(instance, "applied"); + assertEquals("applied", instance.effect); + change.getRollbackMethod().get().invoke(instance); + assertEquals("rolled back", instance.effect); + assertEquals(BaseChange.class, change.getApplyMethod().getDeclaringClass()); + } + } + + @Test + void shouldPreferDirectlyDeclaredPreviewMethods() throws Exception { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + preview(DirectChange.class.getName(), false, true)).build(); + DirectChange instance = new DirectChange(); + change.getApplyMethod().invoke(instance, "applied"); + assertEquals("direct applied", instance.effect); + change.getRollbackMethod().get().invoke(instance); + assertEquals("direct rollback", instance.effect); + assertEquals(DirectChange.class, change.getApplyMethod().getDeclaringClass()); + } + + @Test + void shouldMatchInheritedPrimitiveAndArraySignatures() throws Exception { + CodeLoadedChange change = CodeLoadedChangeBuilder.getInstanceFromPreview( + signaturePreview(InheritedArrayChange.class, "applyArrays", "rollbackArrays", + java.util.Arrays.asList("int", "int[]", "java.lang.String[]"), false)).build(); + InheritedArrayChange instance = new InheritedArrayChange(); + Class[] expected = {int.class, int[].class, String[].class}; + assertArrayEquals(expected, change.getApplyMethod().getParameterTypes()); + assertArrayEquals(expected, change.getRollbackMethod().get().getParameterTypes()); + change.getApplyMethod().invoke(instance, 7, new int[]{8}, new String[]{"apply"}); + assertEquals("apply78", instance.effect); + change.getRollbackMethod().get().invoke(instance, 9, new int[]{10}, new String[]{"rollback"}); + assertEquals("rollback910", instance.effect); + } + + public static class ArrayBaseChange { + String effect; + + public void applyArrays(int value, int[] numbers, String[] text) { + effect = text[0] + value + numbers[0]; + } + + public void rollbackArrays(int value, int[] numbers, String[] text) { + effect = text[0] + value + numbers[0]; + } + } + + public static class InheritedArrayChange extends ArrayBaseChange { } + + private CodePreviewChange signaturePreview(Class source, String apply, String rollback, + java.util.List parameters, boolean legacy) { + return new CodePreviewChange("signature-change", "001", "flamingock", source.getName(), null, + PreviewConstructor.getDefault(), new PreviewMethod(apply, parameters), + new PreviewMethod(rollback, parameters), false, true, false, null, + RecoveryDescriptor.getDefault(), legacy); + } + + public static class BaseChange { + String effect; + + public void apply(String value) { + effect = value; + } + + public void rollback() { + effect = "rolled back"; + } + } + + public static class MiddleChange extends BaseChange { } + + public static class InheritedChange extends MiddleChange { } + + public static class DirectChange extends MiddleChange { + @Override + public void apply(String value) { + effect = "direct " + value; + } + + @Override + public void rollback() { + effect = "direct rollback"; + } + } + private CodePreviewChange preview(String sourceClassPath) { + return preview(sourceClassPath, false, false); + } + + private CodePreviewChange preview(String sourceClassPath, boolean legacy, boolean rollback) { return new CodePreviewChange( "nested-change", "001", @@ -255,14 +350,15 @@ private CodePreviewChange preview(String sourceClassPath) { sourceClassPath, null, PreviewConstructor.getDefault(), - new PreviewMethod("apply", Collections.emptyList()), - null, + new PreviewMethod("apply", rollback + ? Collections.singletonList(String.class.getName()) : Collections.emptyList()), + rollback ? new PreviewMethod("rollback", Collections.emptyList()) : null, false, true, false, null, RecoveryDescriptor.getDefault(), - false); + legacy); } static class Group { diff --git a/core/flamingock-graalvm/build.gradle.kts b/core/flamingock-graalvm/build.gradle.kts index 38dd2f4a0..ef827a063 100644 --- a/core/flamingock-graalvm/build.gradle.kts +++ b/core/flamingock-graalvm/build.gradle.kts @@ -5,6 +5,7 @@ dependencies { // GraalVM SDK for native image support — compileOnly because it's only needed at // native-image build time, never at JVM runtime. compileOnlyApi("org.graalvm.sdk:graal-sdk:22.3.0") + testImplementation("org.graalvm.sdk:graal-sdk:22.3.0") } description = "GraalVM native image support and configuration for Flamingock applications" diff --git a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java index 4daa5525f..a02d36730 100644 --- a/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java +++ b/core/flamingock-graalvm/src/main/java/io/flamingock/graalvm/RegistrationFeature.java @@ -127,12 +127,24 @@ private static void registerProviderInfo() { logger.startRegistrationProcess("user classes"); providers.stream() .flatMap(p -> p.reflectionClasses().stream()) - .forEach(RegistrationFeature::registerClassForReflection); + .forEach(RegistrationFeature::registerUserClassForReflection); logger.completedRegistrationProcess("user classes"); logger.completedRegistrationProcess("provider info"); } + private static void registerUserClassForReflection(String className) { + try { + // Runtime preview lookup also visits ancestors of generated user classes. + for (Class clazz = getClass(className); clazz != null && clazz != Object.class; + clazz = clazz.getSuperclass()) { + registerClassForReflection(clazz); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + private static void registerClassForReflection(String className) { try { registerClassForReflection(getClass(className)); diff --git a/core/flamingock-graalvm/src/test/java/io/flamingock/graalvm/RegistrationFeatureTest.java b/core/flamingock-graalvm/src/test/java/io/flamingock/graalvm/RegistrationFeatureTest.java new file mode 100644 index 000000000..8a0b7f174 --- /dev/null +++ b/core/flamingock-graalvm/src/test/java/io/flamingock/graalvm/RegistrationFeatureTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 + * + * 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 io.flamingock.graalvm; + +import org.graalvm.nativeimage.hosted.RuntimeReflection; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mockStatic; + +class RegistrationFeatureTest { + + private static boolean initialized; + + @Test + void shouldRegisterUserHierarchyWithoutInitialization() throws Exception { + try (MockedStatic reflection = mockStatic(RuntimeReflection.class)) { + Method register = RegistrationFeature.class.getDeclaredMethod("registerUserClassForReflection", String.class); + register.setAccessible(true); + register.invoke(null, ConcreteChange.class.getName()); + + for (Class type : new Class[]{ConcreteChange.class, MiddleChange.class, BaseChange.class}) { + reflection.verify(() -> RuntimeReflection.register(type)); + reflection.verify(() -> RuntimeReflection.register(type.getDeclaredMethods())); + } + reflection.verify(() -> RuntimeReflection.register(Object.class), org.mockito.Mockito.never()); + assertFalse(initialized); + } + } + + static class BaseChange { + public void apply() { } + public void rollback() { } + } + + static class MiddleChange extends BaseChange { + public void middle() { } + } + + static class ConcreteChange extends MiddleChange { + static { + initialized = true; + } + + public void concrete() { } + } +} diff --git a/legacy/mongock-support/build.gradle.kts b/legacy/mongock-support/build.gradle.kts index c0bbd9c00..e409fa3e1 100644 --- a/legacy/mongock-support/build.gradle.kts +++ b/legacy/mongock-support/build.gradle.kts @@ -1,12 +1,7 @@ dependencies { api(project(":core:flamingock-core")) compileOnlyApi("javax.inject:javax.inject:1") + testImplementation(project(":utils:test-util")) } description = "Provides a compatibility layer for Mongock-based applications, including drop-in annotations and audit store migration utilities to Flamingock." - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } -} diff --git a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java index 5eed74b1b..cc2134d20 100644 --- a/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java +++ b/legacy/mongock-support/src/main/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelper.java @@ -17,12 +17,12 @@ import com.github.cloudyrock.mongock.ChangeLog; import com.github.cloudyrock.mongock.ChangeSet; +import io.flamingock.internal.common.core.change.RecoveryDescriptor; +import io.flamingock.internal.common.core.change.TargetSystemDescriptor; import io.flamingock.internal.common.core.error.FlamingockException; import io.flamingock.internal.common.core.preview.CodePreviewChange; import io.flamingock.internal.common.core.preview.PreviewConstructor; import io.flamingock.internal.common.core.preview.PreviewMethod; -import io.flamingock.internal.common.core.change.RecoveryDescriptor; -import io.flamingock.internal.common.core.change.TargetSystemDescriptor; import io.flamingock.internal.common.core.util.TypeElementNameUtils; import io.flamingock.internal.util.ReflectionUtil; import io.mongock.api.annotations.BeforeExecution; @@ -38,6 +38,8 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import java.lang.annotation.Annotation; import java.util.ArrayList; @@ -167,6 +169,9 @@ private void validateChangeUnitAnnotation(ChangeUnit changeUnitAnnotation) { private Optional getAnnotatedMethodInfo(TypeElement typeElement, Class annotationType) { + if (typeElement == null) { + return Optional.empty(); + } for (Element enclosedElement : typeElement.getEnclosedElements()) { if (enclosedElement.getKind() == ElementKind.METHOD && enclosedElement.getAnnotation(annotationType) != null) { @@ -176,7 +181,18 @@ private Optional getAnnotatedMethodInfo(TypeElement typeElement, } } - return Optional.empty(); + TypeMirror superclass = typeElement.getSuperclass(); + if (superclass.getKind() != TypeKind.DECLARED) { + return Optional.empty(); + } + Element superclassElement = ((DeclaredType) superclass).asElement(); + + return getAnnotatedMethodInfo( + superclassElement instanceof TypeElement + ? (TypeElement) superclassElement + : null, + annotationType + ); } private PreviewMethod previewMethodFromExecutableElement(ExecutableElement element) { diff --git a/legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java b/legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java new file mode 100644 index 000000000..cca838740 --- /dev/null +++ b/legacy/mongock-support/src/test/java/io/flamingock/support/mongock/internal/preview/builder/MongockCodePreviewChangeHelperTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 + * + * 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 io.flamingock.support.mongock.internal.preview.builder; + +import io.flamingock.common.test.processor.AnnotatedElementProcessor; +import io.flamingock.common.test.processor.JavacHelper; +import io.flamingock.internal.common.core.preview.CodePreviewChange; +import io.mongock.api.annotations.ChangeUnit; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static io.flamingock.common.test.processor.JavacHelper.getSourcePath; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class MongockCodePreviewChangeHelperTest { + + private final MongockCodePreviewChangeHelper mongockCodePreviewChangeHelper = new MongockCodePreviewChangeHelper(); + private final JavacHelper javacHelper = new JavacHelper(); + + @Test + @DisplayName("inherits execution and rollback methods from its template superclass") + void inheritsExecutionAndRollbackMethodsFromTemplateSuperclass() throws IOException, URISyntaxException { + // Arrange + Path sourcePath = getSourcePath(getClass(), "/changes/ChangeUnitUnderTest.java"); + String expectedRollbackPreviewMethodName = "rollback"; + String expectedExecutePreviewMethodName = "execute"; + List> actualChanges = Collections.synchronizedList(new ArrayList<>()); + + // Act + javacHelper.compile( + sourcePath, + new AnnotatedElementProcessor(ChangeUnit.class, element -> { + List changes = mongockCodePreviewChangeHelper.getCodePreviewChanges(element, "mongock"); + actualChanges.add(changes); + }) + ); + + // Assert + assertEquals(1, actualChanges.size()); + List changes = actualChanges.get(0); + + assertEquals(1, changes.size()); + CodePreviewChange change = changes.get(0); + + assertNotNull(change.getApplyPreviewMethod()); + String actualExecutePreviewMethodName = change.getApplyPreviewMethod().getName(); + assertEquals(expectedExecutePreviewMethodName, actualExecutePreviewMethodName); + + assertNotNull(change.getRollbackPreviewMethod()); + String actualRollbackPreviewMethodName = change.getRollbackPreviewMethod().getName(); + assertEquals(expectedRollbackPreviewMethodName, actualRollbackPreviewMethodName); + } +} diff --git a/legacy/mongock-support/src/test/resources/changes/ChangeUnitUnderTest.java b/legacy/mongock-support/src/test/resources/changes/ChangeUnitUnderTest.java new file mode 100644 index 000000000..52ea94ed2 --- /dev/null +++ b/legacy/mongock-support/src/test/resources/changes/ChangeUnitUnderTest.java @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 + * + * 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 test; + +import io.mongock.api.annotations.ChangeUnit; +import io.mongock.api.annotations.Execution; +import io.mongock.api.annotations.RollbackExecution; + +abstract class ChangeUnitTemplate { + @Execution + public void execute(String value) { + } + + @RollbackExecution + public void rollback(String value) { + } +} + +@ChangeUnit(id = "inherited", order = "001") +class ChangeUnitUnderTest extends ChangeUnitTemplate { +} diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/processor/AnnotatedElementProcessor.java b/utils/test-util/src/main/java/io/flamingock/common/test/processor/AnnotatedElementProcessor.java new file mode 100644 index 000000000..3073abd80 --- /dev/null +++ b/utils/test-util/src/main/java/io/flamingock/common/test/processor/AnnotatedElementProcessor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 + * + * 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 io.flamingock.common.test.processor; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Set; +import java.util.function.Consumer; + +/** + * A simple annotation processor that collects elements annotated with a specific annotation and passes them to a provided consumer. + */ +public class AnnotatedElementProcessor extends AbstractProcessor { + private final Class annotationClass; + private final Consumer elementConsumer; + + /** + * Creates a new AnnotatedElementProcessor for the specified annotation class and element consumer. + * @param annotationClass the annotation class to look for + * @param elementConsumer the consumer to process the annotated elements + */ + public AnnotatedElementProcessor(Class annotationClass, Consumer elementConsumer) { + this.annotationClass = annotationClass; + this.elementConsumer = elementConsumer; + } + + @Override + public Set getSupportedAnnotationTypes() { + return Collections.singleton(annotationClass.getCanonicalName()); + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnvironment) { + for (Element element : roundEnvironment.getElementsAnnotatedWith(annotationClass)) { + elementConsumer.accept((TypeElement) element); + } + return false; + } +} diff --git a/utils/test-util/src/main/java/io/flamingock/common/test/processor/JavacHelper.java b/utils/test-util/src/main/java/io/flamingock/common/test/processor/JavacHelper.java new file mode 100644 index 000000000..21f30c535 --- /dev/null +++ b/utils/test-util/src/main/java/io/flamingock/common/test/processor/JavacHelper.java @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Flamingock (https://www.flamingock.io) + * + * 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 + * + * 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 io.flamingock.common.test.processor; + +import javax.annotation.processing.Processor; +import javax.tools.*; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * A helper class for compiling Java source files with annotation processors during tests. + */ +public class JavacHelper { + private final JavaCompiler compiler; + + /** + * Initializes a new instance of the JavacHelper class and retrieves the system Java compiler. + * Throws an assertion error if the system Java compiler is not available (e.g., when running on a JRE instead of a JDK). + */ + public JavacHelper() { + this.compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(this.compiler, "Tests must run on a JDK with a system Java compiler"); + } + + /** + * Compiles the Java source file at the specified path using the provided annotation processors. + * + * @param sourcePath the path to the Java source file to compile + * @param processors the annotation processors to use during compilation + * @throws IOException if an I/O error occurs during compilation + */ + public void compile(Path sourcePath, Processor... processors) throws IOException { + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + try (StandardJavaFileManager fileManager = + compiler.getStandardFileManager(diagnostics, null, null)) { + fileManager.setLocation(StandardLocation.CLASS_PATH, currentClasspath()); + + JavaCompiler.CompilationTask task = compiler.getTask( + null, + fileManager, + diagnostics, + Collections.singletonList("-proc:only"), + null, + fileManager.getJavaFileObjects(sourcePath.toFile()) + ); + task.setProcessors(Arrays.asList(processors)); + assertTrue(task.call(), () -> diagnostics.getDiagnostics().toString()); + } + } + + /** + * Retrieves the source path of a resource relative to the specified class. + * + * @param klass the class relative to which the resource path is specified + * @param resourcePath the resource path to retrieve + * @return the Path of the resource + * @throws URISyntaxException if the resource URL cannot be converted to a URI + * @throws AssertionError if the resource is not found + */ + public static Path getSourcePath(Class klass, String resourcePath) throws URISyntaxException { + URL sourceResource = klass.getResource(resourcePath); + assertNotNull(sourceResource, "Source resource not found: " + resourcePath); + return Paths.get(sourceResource.toURI()); + } + + private static List currentClasspath() { + return Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator)) + .map(File::new) + .collect(Collectors.toList()); + } +}