Skip to content

Fix Mock does not intercept package-private methods in OSGi correctly#2385

Open
AndreasTu wants to merge 1 commit into
spockframework:masterfrom
AndreasTu:Issue2384
Open

Fix Mock does not intercept package-private methods in OSGi correctly#2385
AndreasTu wants to merge 1 commit into
spockframework:masterfrom
AndreasTu:Issue2384

Conversation

@AndreasTu

Copy link
Copy Markdown
Member

We now do a real search in the mock targets ClassLoader to check if the Spock classes and additional interfaces are visible instead of asking the Parent ClassLoader hierarchy, which does not work in an OSGi environment.

Fixes #2384

@AndreasTu AndreasTu added this to the 2.5 milestone Jun 25, 2026
@AndreasTu AndreasTu requested a review from leonard84 June 25, 2026 17:41
@AndreasTu AndreasTu self-assigned this Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c2a034e7-d8f4-4ce6-b358-fb65efc88ea2

📥 Commits

Reviewing files that changed from the base of the PR and between 081b450 and 9fe4199.

📒 Files selected for processing (6)
  • docs/release_notes.adoc
  • spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java
✅ Files skipped from review due to trivial changes (2)
  • docs/release_notes.adoc
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy
  • spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy

📝 Walkthrough

Walkthrough

The PR updates ByteBuddy mock locality detection, adds an OSGi-style test loader and Java fixtures, and introduces a regression spec that exercises package-private mocking from Groovy and Java. The release notes add the related issue entry.

Changes

OSGi package-private mock fix

Layer / File(s) Summary
Classloader visibility check
spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java
isLocalMock now loads ISpockMockObject and each additional interface through the target class loader and returns false when any class is not loadable.
OSGi test loader and fixture classes
spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy, spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java, spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java
A parentless OSGi-style test loader is added, along with Java fixtures for a package-private target method and a Java caller.
Embedded OSGi regression spec
spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy, docs/release_notes.adoc
A new embedded Spock spec verifies the mocked package-private method from Groovy and Java, and the release notes record the fix.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A rabbit hopped through OSGi burrows bright,
With ByteBuddy carrots now set just right.
Groovy and Java both give a cheer,
The mocked package-private path is clear!
🐰🥕 Thump-thump, all snug tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fixing OSGi package-private mock interception.
Description check ✅ Passed The description is directly about the OSGi class-loader visibility fix for mocks.
Linked Issues check ✅ Passed The code now checks target-classloader visibility for ISpockMockObject and added interfaces, matching #2384.
Out of Scope Changes check ✅ Passed The changes appear limited to the OSGi mock fix, its tests, and release notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java`:
- Around line 103-124: Handle bootstrap-loaded targets in
ByteBuddyMockFactory.isLocalMock before calling
loadClassIfAvailableInClassLoader, since targetClass.getClassLoader() can be
null. Update the null-handling so a bootstrap-loaded target is treated as
non-local without invoking loader.loadClass(), and make
loadClassIfAvailableInClassLoader safely cope with a null ClassLoader. Use the
existing symbols isLocalMock and loadClassIfAvailableInClassLoader to apply the
fix.

In
`@spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy`:
- Around line 27-30: The embedded spec in OsgiPackagePrivateMemberMockSpec
should explicitly use the ByteBuddy mock maker instead of relying on the
default. Update the Mock call inside the EmbeddedSpecRunner string in
OsgiPackagePrivateMemberMockSpec to pass the mockMaker argument with
spock.mock.MockMakers.byteBuddy so this regression test specifically exercises
the ByteBuddyMockFactory path and does not pass accidentally under a different
default mock maker.

In
`@spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy`:
- Around line 45-50: The resource stream opened in
OsgiTestClassLoader.getResourceAsStream is not closed after reading, which can
leave classpath handles open across repeated test runs. Update the class loading
logic around hostCl.getResourceAsStream and defineClass to read the bytes within
a close-safe block, ensuring the stream is always closed even when
ClassNotFoundException is thrown or class data is loaded successfully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d0aa8646-b26e-4d7d-b597-1b4677c79e86

📥 Commits

Reviewing files that changed from the base of the PR and between db681f8 and 081b450.

📒 Files selected for processing (6)
  • docs/release_notes.adoc
  • spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java
  • spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a bug where Spock mocks fail to intercept package-private methods in OSGi environments by replacing the MultipleParentClassLoader-based locality check with a direct ClassLoader.loadClass identity comparison against the target class's own ClassLoader.

  • ByteBuddyMockFactory.isLocalMock: The old approach (inspired by Mockito) built a combined parent-ClassLoader and compared it by identity to the target loader — this failed in OSGi where bundle ClassLoaders delegate laterally rather than through the parent hierarchy. The new approach asks the target loader to resolve each required class directly and verifies it returns the same Class instance, which correctly handles OSGi-style flat delegation.
  • New test infrastructure: OsgiTestClassLoader simulates a bundle ClassLoader by bypassing parent delegation for a specific test package; OsgiPackagePrivateMemberMockSpec verifies the fix end-to-end from both Java and Groovy call sites.

Confidence Score: 3/5

The core logic change is correct and well-tested for the OSGi scenario, but loadClassIfAvailableInClassLoader will throw NPE rather than return a safe fallback when called with a bootstrap-loaded class, leaving a latent crash for any future caller that doesn't pre-filter JDK types.

The fix correctly addresses the OSGi locality check and is backed by a targeted integration test. The missing null guard on the ClassLoader is a real, reproducible NPE (not caught by the existing ClassNotFoundException handler) that the current production call site happens to avoid through pre-filtering, but which would fire for any direct or future caller that passes a bootstrap-loaded class to isLocalMock.

spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java — specifically loadClassIfAvailableInClassLoader, which needs a null guard for the bootstrap ClassLoader case.

Important Files Changed

Filename Overview
spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java Replaces the MultipleParentClassLoader-based isLocalMock check with a direct loadClass identity comparison; missing null guard for the bootstrap ClassLoader case in loadClassIfAvailableInClassLoader.
spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy New test helper simulating an OSGi-style ClassLoader by bypassing parent delegation for the testclasses package; InputStream from getResourceAsStream is never closed and @Override annotations are missing on loadClass, getResource, and getResources.
spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy New integration test verifying package-private method interception works from both Java and Groovy callers in the simulated OSGi ClassLoader environment.
spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java New test fixture with a package-private method that exercises the mock interception path under the simulated OSGi ClassLoader.
spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java New test helper that invokes the package-private method from Java to verify the mock is intercepted across the language boundary.
docs/release_notes.adoc Adds a release note entry for the OSGi package-private mock fix.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant BB as ByteBuddyMockFactory
    participant DM as determineBestClassLoadingStrategy
    participant LM as isLocalMock
    participant TL as targetClassLoader

    BB->>DM: determineBestClassLoadingStrategy(targetClass, settings)
    DM->>LM: isLocalMock(targetClass, additionalInterfaces)
    LM->>TL: loadClass(ISpockMockObject.class.name)
    TL-->>LM: "loaded Class<?> (or ClassNotFoundException)"
    LM->>LM: "identity check: loaded == ISpockMockObject.class?"
    loop each additionalInterface
        LM->>TL: loadClass(ifClass.name)
        TL-->>LM: "loaded Class<?> (or ClassNotFoundException)"
        LM->>LM: "identity check: loaded == ifClass?"
    end
    LM-->>DM: true (local) / false (non-local)
    alt "isLocal == true"
        DM-->>BB: ClassLoadingStrategy.UsingLookup (package-private access)
    else "isLocal == false"
        DM-->>BB: ClassLoadingStrategy.INJECTION or WRAPPER
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant BB as ByteBuddyMockFactory
    participant DM as determineBestClassLoadingStrategy
    participant LM as isLocalMock
    participant TL as targetClassLoader

    BB->>DM: determineBestClassLoadingStrategy(targetClass, settings)
    DM->>LM: isLocalMock(targetClass, additionalInterfaces)
    LM->>TL: loadClass(ISpockMockObject.class.name)
    TL-->>LM: "loaded Class<?> (or ClassNotFoundException)"
    LM->>LM: "identity check: loaded == ISpockMockObject.class?"
    loop each additionalInterface
        LM->>TL: loadClass(ifClass.name)
        TL-->>LM: "loaded Class<?> (or ClassNotFoundException)"
        LM->>LM: "identity check: loaded == ifClass?"
    end
    LM-->>DM: true (local) / false (non-local)
    alt "isLocal == true"
        DM-->>BB: ClassLoadingStrategy.UsingLookup (package-private access)
    else "isLocal == false"
        DM-->>BB: ClassLoadingStrategy.INJECTION or WRAPPER
    end
Loading

Reviews (1): Last reviewed commit: "Fix Mock does not intercept package-priv..." | Re-trigger Greptile

We now do a real search in the mock targets ClassLoader to check
if the Spock classes and additional interfaces are visible
instead of asking the Parent ClassLoader hierarchy,
which does not work in an OSGi environment.

Fixes spockframework#2384
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mock() does not intercept package-private methods in OSGi on Java 25+

1 participant