From 9fe4199b04f4f053909fa1c06966b0fe7e90d3f6 Mon Sep 17 00:00:00 2001 From: AndreasTu Date: Thu, 25 Jun 2026 19:37:15 +0200 Subject: [PATCH] Fix Mock does not intercept package-private methods in OSGi correctly 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 --- docs/release_notes.adoc | 1 + .../mock/runtime/ByteBuddyMockFactory.java | 34 +++++++--- .../OsgiPackagePrivateMemberMockSpec.groovy | 54 ++++++++++++++++ .../mock/osgi/OsgiTestClassLoader.groovy | 63 +++++++++++++++++++ .../osgi/testclasses/InvocationFromJava.java | 27 ++++++++ .../testclasses/PkgPrivateMemberClass.java | 22 +++++++ 6 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy create mode 100644 spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy create mode 100644 spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java create mode 100644 spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index ee96a41d16..421c34a138 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -16,6 +16,7 @@ include::include.adoc[] * Fix argument mismatch descriptions for varargs methods by expanding varargs instead of reporting `` spockPull:2315[] * Fix Pattern flags being dropped when `java.util.regex.Pattern` instances are used in Spock regex conditions spockIssue:2298[] * Fix `MockitoMockMaker` throws NPE on null object spockIssue:2337[] +* Fix Mock does not intercept package-private methods in OSGi correctly spockIssue:2384[] === Breaking Changes diff --git a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java index 583a5fef62..eebd3fa55a 100644 --- a/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java +++ b/spock-core/src/main/java/org/spockframework/mock/runtime/ByteBuddyMockFactory.java @@ -29,7 +29,6 @@ import net.bytebuddy.dynamic.Transformer; import net.bytebuddy.dynamic.loading.ClassInjector; import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; -import net.bytebuddy.dynamic.loading.MultipleParentClassLoader; import net.bytebuddy.dynamic.scaffold.TypeValidation; import net.bytebuddy.implementation.FieldAccessor; import net.bytebuddy.implementation.FixedValue; @@ -96,19 +95,36 @@ class ByteBuddyMockFactory { * A mock is considered local, if all additional interfaces of the mock (including {@link ISpockMockObject}) are * loadable by the target class' classloader. * - * @param targetClass The to-be-mocked class + * @param targetClass The to-be-mocked class * @param additionalInterfaces Additional interfaces of the to-be-mocked type * @return true, if this is a local mock. Otherwise false */ @VisibleForTesting static boolean isLocalMock(Class targetClass, Collection> additionalInterfaces) { - // Inspired by https://github.com/mockito/mockito/blob/99426415c0ceb30e55216c3934854528c83f410e/mockito-core/src/main/java/org/mockito/internal/creation/bytebuddy/SubclassBytecodeGenerator.java#L165-L166 - ClassLoader cl = new MultipleParentClassLoader.Builder() - .appendMostSpecific(targetClass) - .appendMostSpecific(additionalInterfaces) - .appendMostSpecific(ISpockMockObject.class) - .build(); - return cl == targetClass.getClassLoader(); + ClassLoader targetLoader = targetClass.getClassLoader(); + if (targetLoader == null) { + targetLoader = ClassLoader.getSystemClassLoader(); + } + Class spockMockClass = loadClassIfAvailableInClassLoader(targetLoader, ISpockMockObject.class); + if (spockMockClass != ISpockMockObject.class) { + //The ISpockMockObject is not visible to the targetLoader, so we can't be local. + return false; + } + for (Class ifClass : additionalInterfaces) { + Class ifClassOfTarget = loadClassIfAvailableInClassLoader(targetLoader, ifClass); + if (ifClassOfTarget != ifClass) { + return false; + } + } + return true; + } + + private static Class loadClassIfAvailableInClassLoader(ClassLoader loader, Class clazz) { + try { + return loader.loadClass(clazz.getName()); + } catch (ClassNotFoundException e) { + return null; + } } Object createMock(IMockMaker.IMockCreationSettings settings) { diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy new file mode 100644 index 0000000000..a05fb2c01f --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiPackagePrivateMemberMockSpec.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2026 the original author or 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 + * https://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.spockframework.smoke.mock.osgi + + +import spock.lang.Issue +import spock.lang.Specification +import spock.util.EmbeddedSpecRunner + +class OsgiPackagePrivateMemberMockSpec extends Specification { + def runner = new EmbeddedSpecRunner(new OsgiTestClassLoader(this.class.classLoader)) + + @SuppressWarnings('UnnecessaryQualifiedReference') + @Issue("https://github.com/spockframework/spock/issues/2384") + def "OSGi package private mock shall be mockable with ByteBuddy"() { + when: + def res = runner.runSpecBody(""" + def mock = Mock(org.spockframework.smoke.mock.osgi.testclasses.PkgPrivateMemberClass, mockMaker: spock.mock.MockMakers.byteBuddy) { + packagePrivate() >> "mocked" + } + + def "mock is called when invoked from Java code"() { + when: + def result = new org.spockframework.smoke.mock.osgi.testclasses.InvocationFromJava(mock).invoke() + + then: + result == "mocked" + } + + def "mock is called when invoked directly from Groovy"() { + when: + def result = mock.packagePrivate() + + then: + result == "mocked" + } +""") + + then: + res.testsSucceededCount == 2 + } +} diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy new file mode 100644 index 0000000000..4411ed79bc --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/OsgiTestClassLoader.groovy @@ -0,0 +1,63 @@ +/* + * Copyright 2026 the original author or 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 + * https://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.spockframework.smoke.mock.osgi + +import groovy.transform.CompileStatic + +import static java.util.Objects.requireNonNull + +/** + * Fakes an OSGi like ClassLoader env, by not using the parent ClassLoader semantics. + */ +@CompileStatic +class OsgiTestClassLoader extends ClassLoader { + private final ClassLoader hostCl + + OsgiTestClassLoader(ClassLoader hostCl) { + super(null) + this.hostCl = requireNonNull(hostCl) + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (!name.startsWith("org.spockframework.smoke.mock.osgi.testclasses")) { + return hostCl.loadClass(name) + } + super.loadClass(name, resolve) + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + def clsFilePath = name + clsFilePath = clsFilePath.replace(".", "/") + clsFilePath += ".class" + def is = hostCl.getResourceAsStream(clsFilePath) + if (is == null) { + throw new ClassNotFoundException(name) + } + def clsData = is.getBytes() + return defineClass(name, clsData, 0, clsData.length) + } + + @Override + URL getResource(String name) { + return hostCl.getResource(name) + } + + @Override + Enumeration getResources(String name) throws IOException { + return hostCl.getResources(name) + } +} diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java new file mode 100644 index 0000000000..f79c96a229 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/InvocationFromJava.java @@ -0,0 +1,27 @@ +/* + * Copyright 2026 the original author or 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 + * https://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.spockframework.smoke.mock.osgi.testclasses; + +public class InvocationFromJava { + PkgPrivateMemberClass obj; + + public InvocationFromJava(PkgPrivateMemberClass obj) { + this.obj = obj; + } + + public Object invoke() { + return obj.packagePrivate(); + } +} diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java new file mode 100644 index 0000000000..4a63b42e32 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/mock/osgi/testclasses/PkgPrivateMemberClass.java @@ -0,0 +1,22 @@ +/* + * Copyright 2026 the original author or 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 + * https://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.spockframework.smoke.mock.osgi.testclasses; + +public class PkgPrivateMemberClass { + + String packagePrivate() { + return "original"; + } +}