diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b828b5d5a0..b181b41e90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,6 +188,41 @@ jobs: files: mockito-integration-tests/android-tests/build/reports/coverage/android-tests/debug/connected/report.xml fail_ci_if_error: true + # + # GraalVM native tests job + # + graalvm: + runs-on: ubuntu-latest + if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" + + steps: + - name: 1. Check out code + uses: actions/checkout@v5 + with: + fetch-depth: '0' + + - name: 2. Setup GraalVM + uses: graalvm/setup-graalvm@v1.3.5 + with: + java-version: "21" + distribution: "graalvm" + cache: gradle + + - name: 3. Run GraalVM native tests with metadata + run: ./gradlew :mockito-integration-tests:graalvm-tests:nativeTest -PenableGraalTracingAgent --scan + + - name: 4. Verify GraalVM metadata generation + run: | + REFLECT_CONFIG="mockito-integration-tests/graalvm-tests/src/test/resources/META-INF/native-image/org.mockito/graalvm-tests/reflect-config.json" + test -f "$REFLECT_CONFIG" || { + echo "reflect-config.json not found at $REFLECT_CONFIG" + exit 1 + } + grep -q "org\.mockito\.internal\.creation\.bytebuddy\.codegen\.DummyObject\$MockitoMock" "$REFLECT_CONFIG" || { + echo "DummyObject mock not found in reflect-config.json" + exit 1 + } + # # Release job, only for pushes to the main development branch # @@ -195,7 +230,7 @@ jobs: permissions: contents: write runs-on: ubuntu-latest - needs: [java, android] # both build jobs must pass before we can release + needs: [java, android, graalvm] # all build jobs must pass before we can release if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) diff --git a/mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java b/mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java index 4701e4d140..21193e82da 100644 --- a/mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java +++ b/mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java @@ -34,6 +34,7 @@ import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.dynamic.DynamicType; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy.Default; import net.bytebuddy.dynamic.loading.MultipleParentClassLoader; import net.bytebuddy.dynamic.scaffold.TypeValidation; import net.bytebuddy.implementation.FieldAccessor; @@ -272,7 +273,9 @@ public Class mockClass(MockFeatures features) { .or(hasParameters(whereAny(hasType(isPackagePrivate()))))); } ClassLoadingStrategy strategy; - if (localMock) { + if (GraalImageCode.getCurrent().isDefined()) { + strategy = Default.WRAPPER; + } else if (localMock) { strategy = handler.classLoadingStrategy(features.mockedType); } else if (classLoader == MockAccess.class.getClassLoader()) { strategy = handler.classLoadingStrategy(InjectionBase.class); diff --git a/mockito-integration-tests/graalvm-tests/.gitignore b/mockito-integration-tests/graalvm-tests/.gitignore new file mode 100644 index 0000000000..538ef4fcab --- /dev/null +++ b/mockito-integration-tests/graalvm-tests/.gitignore @@ -0,0 +1 @@ +src/test/resources/META-INF/native-image/org.mockito/graalvm-tests/ diff --git a/mockito-integration-tests/graalvm-tests/build.gradle.kts b/mockito-integration-tests/graalvm-tests/build.gradle.kts new file mode 100644 index 0000000000..6d5a71df45 --- /dev/null +++ b/mockito-integration-tests/graalvm-tests/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("java") + id("mockito.test-conventions") + id("org.graalvm.buildtools.native") version "0.11.0" +} + +description = "Test suite for exercising subclass mock maker with GraalVM native image" + +dependencies { + implementation(project(":mockito-core")) + implementation(project(":mockito-extensions:mockito-subclass")) + testImplementation(libs.junit4) + testImplementation(libs.assertj) + testImplementation(libs.junit.platform.launcher) +} + +// org.graalvm.buildtools.native:0.11.0 requires Java >=17 +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +graalvmNative { + binaries { + named("test") { + buildArgs.addAll("--verbose", "-O0") + } + } + + agent { + enabled = project.hasProperty("enableGraalTracingAgent") + defaultMode = "standard" + + metadataCopy { + inputTaskNames.add("test") + outputDirectories.add("src/test/resources/META-INF/native-image/org.mockito/graalvm-tests") + mergeWithExisting = false + } + } +} + +tasks { + test { + forkEvery = 1 + if (project.hasProperty("enableGraalTracingAgent")) { + finalizedBy("metadataCopy") + } + } +} diff --git a/mockito-integration-tests/graalvm-tests/src/test/java/org/mockito/graalvm/DummyObject.java b/mockito-integration-tests/graalvm-tests/src/test/java/org/mockito/graalvm/DummyObject.java new file mode 100644 index 0000000000..ea1efa8144 --- /dev/null +++ b/mockito-integration-tests/graalvm-tests/src/test/java/org/mockito/graalvm/DummyObject.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.graalvm; + +public class DummyObject { + public String getValue() { + return "original"; + } +} diff --git a/mockito-integration-tests/graalvm-tests/src/test/java/org/mockito/graalvm/GraalVMSubclassMockMakerTest.java b/mockito-integration-tests/graalvm-tests/src/test/java/org/mockito/graalvm/GraalVMSubclassMockMakerTest.java new file mode 100644 index 0000000000..2acf954c1e --- /dev/null +++ b/mockito-integration-tests/graalvm-tests/src/test/java/org/mockito/graalvm/GraalVMSubclassMockMakerTest.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.graalvm; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import org.junit.Test; + +public final class GraalVMSubclassMockMakerTest { + + @Test + public void test_subclass_mock_maker_with_simple_object() { + DummyObject dummyMock = mock(DummyObject.class, withSettings()); + + when(dummyMock.getValue()).thenReturn("mocked"); + + assertEquals("mocked", dummyMock.getValue()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0645b1a815..2033ecc64a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,6 +45,7 @@ include( "mockito-integration-tests:osgi-tests", "mockito-integration-tests:programmatic-tests", "mockito-integration-tests:java-21-tests", + "mockito-integration-tests:graalvm-tests", ) // https://developer.android.com/studio/command-line/variables#envar