Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ include::include.adoc[]
* Fix argument mismatch descriptions for varargs methods by expanding varargs instead of reporting `<too few arguments>` 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Class<?>> 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;
}
Comment thread
AndreasTu marked this conversation as resolved.
}
Comment thread
AndreasTu marked this conversation as resolved.

Object createMock(IMockMaker.IMockCreationSettings settings) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
AndreasTu marked this conversation as resolved.
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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
AndreasTu marked this conversation as resolved.
}

@Override
URL getResource(String name) {
return hostCl.getResource(name)
}

@Override
Enumeration<URL> getResources(String name) throws IOException {
return hostCl.getResources(name)
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
}
Loading