Repository: micronaut-projects/micronaut-test (java)

Issue:
Spock Unroll tests rollback regression
### Expected Behavior

When writing a Spock test that has Unroll semantics (which is now implicit) performs DB operations, the transaction should be rolled back for each iteration that is performed.
This used to be the case in Micronaut 3.

### Actual Behaviour

The transaction is not rolled back until after all iterations of the Unroll are performed.
The transaction is rolled back for each iteration if the MicronautTest annotation has a transactionMode of SINGLE_TRANSACTION.

### Steps To Reproduce

Run the following tests in the linked example application:
UnrollRollbackSpec - proves that the transaction isn't rolled back between iterations
UnrollRollbackSingleTransactionSpec - proves that the transaction is rolled back between iterations when transactionMode is SINGLE_TRANSACTION

### Environment Information

OS: Pop!OS 22.04
Java: 17

### Example Application

https://github.com/mattlong-finocomp/micronaut-unroll-rollback

### Version

4.2.1

Pull request:
fix: add interceptor around parameterized features
When a feature is using `@Unroll` (which is implicit in recent versions of Spock), we needed to add the interceptor to the iteration interceptor handler instead of the standard handler.

This bug was visible in #911 where specs that were parameterized did not properly revert the transaction between iterations.

Closes #911

Patch:
diff --git a/.github/workflows/graalvm-dev.yml b/.github/workflows/graalvm-dev.yml
index ecd870d6..03cb77a9 100644
--- a/.github/workflows/graalvm-dev.yml
+++ b/.github/workflows/graalvm-dev.yml
@@ -28,7 +28,13 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       max-parallel: 6
-      matrix: ${{ fromJson(needs.build_matrix.outputs.matrix) }}
+      matrix:
+        java: ['dev']
+        distribution: ['graalvm-community']
+        native_test_task: ${{ fromJson(needs.build_matrix.outputs.matrix).native_test_task }}
+        include:
+          - java: '22-ea'
+            distribution: 'graalvm'
     env:
       GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }}
       GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USERNAME }}
@@ -39,7 +45,8 @@ jobs:
         uses: micronaut-projects/github-actions/graalvm/pre-build@master
         id: pre-build
         with:
-          java: 'dev'
+          java: ${{ matrix.java }}
+          distribution: ${{ matrix.distribution }}
       - name: Build Steps
         uses: micronaut-projects/github-actions/graalvm/build@master
         id: build
@@ -53,4 +60,4 @@ jobs:
         uses: micronaut-projects/github-actions/graalvm/post-build@master
         id: post-build
         with:
-          java: 'dev'
+          java: ${{ matrix.java }}
diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 9b157ff9..8ca81050 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -45,7 +45,7 @@ jobs:
           fetch-depth: 0
 
       - name: "🔧 Setup GraalVM CE"
-        uses: graalvm/setup-graalvm@v1.1.6
+        uses: graalvm/setup-graalvm@v1.1.7
         with:
           distribution: 'graalvm'
           java-version: ${{ matrix.java }}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6231e4bf..eb8c53bd 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
 [versions]
 micronaut = "4.3.5"
-micronaut-platform = "4.3.1"
+micronaut-platform = "4.3.2"
 micronaut-docs = "2.0.0"
 groovy = "4.0.15"
 
@@ -21,9 +21,9 @@ micronaut-data = "4.4.1"
 micronaut-hibernate-validator = "4.2.0"
 micronaut-logging = "1.1.2"
 micronaut-r2dbc = "5.2.0"
-micronaut-serde = "2.7.0"
+micronaut-serde = "2.8.1"
 micronaut-spring = "5.4.1"
-micronaut-sql = "5.4.0"
+micronaut-sql = "5.5.1"
 micronaut-reactor = "3.2.1"
 micronaut-test-resources = "2.3.3"
 


Test patch:
diff --git a/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java b/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java
index c86f1f53..0243dbd7 100644
--- a/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java
+++ b/test-spock/src/main/java/io/micronaut/test/extensions/spock/MicronautSpockExtension.java
@@ -65,26 +65,11 @@ public class MicronautSpockExtension<T extends Annotation> extends AbstractMicro
 
         spec.getAllFeatures().forEach(feature -> {
 
-            feature.addInterceptor(invocation -> {
-                TestContext testContext = buildContext(invocation, null);
-                try {
-                    beforeTestMethod(testContext);
-                    interceptTest(new TestMethodInvocationContext<Object>() {
-                        @Override
-                        public TestContext getTestContext() {
-                            return testContext;
-                        }
-
-                        @Override
-                        public Object proceed() throws Throwable {
-                            invocation.proceed();
-                            return null;
-                        }
-                    });
-                } finally {
-                    afterTestMethod(testContext);
-                }
-            });
+            if (feature.isParameterized()) {
+                feature.addIterationInterceptor(this::getInvocationInterceptor);
+            } else {
+                feature.addInterceptor(this::getInvocationInterceptor);
+            }
 
             feature.getFeatureMethod().addInterceptor(invocation -> {
                 try {
@@ -198,6 +183,28 @@ public class MicronautSpockExtension<T extends Annotation> extends AbstractMicro
         });
     }
 
+    @SuppressWarnings("java:S112") // Throwable is what we deal with here
+    private void getInvocationInterceptor(IMethodInvocation invocation) throws Throwable {
+        TestContext testContext = buildContext(invocation, null);
+        try {
+            beforeTestMethod(testContext);
+            interceptTest(new TestMethodInvocationContext<Object>() {
+                @Override
+                public TestContext getTestContext() {
+                    return testContext;
+                }
+
+                @Override
+                public Object proceed() throws Throwable {
+                    invocation.proceed();
+                    return null;
+                }
+            });
+        } finally {
+            afterTestMethod(testContext);
+        }
+    }
+
     private MicronautTestValue buildValueObject(MicronautTest micronautTest) {
         if (micronautTest != null) {
             return new MicronautTestValue(
diff --git a/test-spock/src/test/groovy/io/micronaut/test/spock/intercept/InterceptTestSpec.groovy b/test-spock/src/test/groovy/io/micronaut/test/spock/intercept/InterceptTestSpec.groovy
index 39ae7d76..54b5028b 100644
--- a/test-spock/src/test/groovy/io/micronaut/test/spock/intercept/InterceptTestSpec.groovy
+++ b/test-spock/src/test/groovy/io/micronaut/test/spock/intercept/InterceptTestSpec.groovy
@@ -1,18 +1,3 @@
-/*
- * Copyright 2017-2020 original 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
- *
- * 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.micronaut.test.spock.intercept
 
 import io.micronaut.context.annotation.Property
@@ -21,7 +6,6 @@ import io.micronaut.test.spock.MathService
 import jakarta.inject.Inject
 import spock.lang.Specification
 import spock.lang.Stepwise
-import spock.lang.Unroll
 
 @Stepwise
 @MicronautTest
@@ -34,7 +18,6 @@ class InterceptTestSpec extends Specification {
     @Inject
     TestInterceptor testInterceptor
 
-    @Unroll
     void "should compute #num to #expected"() {
         when:
             def result = mathService.compute(num)
@@ -62,6 +45,8 @@ class InterceptTestSpec extends Specification {
                     'OUT BEFORE $spock_feature_0_0',
                     'IN AFTER $spock_feature_0_0',
                     'OUT AFTER $spock_feature_0_0',
+                    'OUT $spock_feature_0_0', // This is the implicit unroll
+                    'IN $spock_feature_0_0',
                     'IN BEFORE $spock_feature_0_0',
                     'OUT BEFORE $spock_feature_0_0',
                     'IN AFTER $spock_feature_0_0',
@@ -80,13 +65,8 @@ class InterceptTestSpec extends Specification {
                     'OUT BEFORE $spock_feature_0_2',
                     //  This test is $spock_feature_0_2
             ]
+
         then:
-            assert calls.size() == expected.size()
-            for (int i = 0; i < expected.size(); i++) {
-                def a = calls.get(i)
-                def b = expected[i]
-                assert a == b
-            }
+            calls == expected
     }
-
 }